mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-10-24 12:13:31 +08:00
feat(UI): visualize input clear button (#48702)
* feat: visualize search bar inputs clear button * chore: remove redundant empty lines * fix: make search bar input horizontally responsive * fix: make search bar input width follow suggestion items * fix: moving clear button to right of the searchbar * feat: add search bar clear button in LP * fix: add a bigger spacing between the icons and the text * fix: make the clear button show visible focus indicator * chore: refactor the css code to avoid redundancy * fix: make the buttons in search bar accessible for screen reader * fix: make the buttons and input in search bar more accessible
This commit is contained in:
@@ -478,10 +478,11 @@
|
||||
"hint": "Hint",
|
||||
"heart": "Heart",
|
||||
"initial": "Initial",
|
||||
"inputReset": "Clear search terms",
|
||||
"info": "Intro Information",
|
||||
"spacer": "Spacer",
|
||||
"toggle": "Toggle Checkmark",
|
||||
"magnifier": "magnifier"
|
||||
"magnifier": "Submit search terms"
|
||||
},
|
||||
"aria": {
|
||||
"fcc-curriculum": "freeCodeCamp Curriculum",
|
||||
|
||||
@@ -6,7 +6,7 @@ const Magnifier = (): JSX.Element => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className='sr-only'>{t('icons.Magnifier')}</span>
|
||||
<span className='sr-only'>{t('icons.magnifier')}</span>
|
||||
<svg
|
||||
className='ais-SearchBox-submitIcon'
|
||||
height='10'
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const InputReset = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className='sr-only'>{t('icons.inputReset')}</span>
|
||||
<svg
|
||||
className='ais-SearchBox-resetIcon'
|
||||
height='10'
|
||||
viewBox='0 0 20 20'
|
||||
width='10'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path d='M8.114 10L.944 2.83 0 1.885 1.886 0l.943.943L10 8.113l7.17-7.17.944-.943L20 1.886l-.943.943-7.17 7.17 7.17 7.17.943.944L18.114 20l-.943-.943-7.17-7.17-7.17 7.17-.944.943L0 18.114l.943-.943L8.113 10z'></path>
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
InputReset.displayName = 'InputReset';
|
||||
export default InputReset;
|
||||
@@ -477,6 +477,10 @@ button.nav-link[aria-disabled='true'] {
|
||||
width: calc(100vw - 17rem);
|
||||
}
|
||||
|
||||
.universal-nav-right .ais-SearchBox-reset {
|
||||
right: 291px;
|
||||
}
|
||||
|
||||
.universal-nav-right .fcc_searchBar .ais-Hits {
|
||||
width: calc(100vw - 17rem);
|
||||
}
|
||||
@@ -485,7 +489,8 @@ button.nav-link[aria-disabled='true'] {
|
||||
outline: 3px solid var(--blue-mid);
|
||||
}
|
||||
|
||||
.ais-SearchBox-submit:focus {
|
||||
.ais-SearchBox-submit:focus,
|
||||
.ais-SearchBox-reset:focus {
|
||||
outline: 3px solid var(--blue-mid);
|
||||
}
|
||||
|
||||
@@ -560,6 +565,10 @@ button.nav-link[aria-disabled='true'] {
|
||||
width: calc(100vw - 30px);
|
||||
}
|
||||
|
||||
.universal-nav-right .ais-SearchBox-reset {
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.universal-nav-right .fcc_searchBar .ais-Hits {
|
||||
width: calc(100% - 30px);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ export class Header extends React.Component<
|
||||
// the search bar should not toggle the menu
|
||||
this.searchBarRef.current &&
|
||||
!this.searchBarRef.current.contains(eventTarget) &&
|
||||
// don't count clicks on searcn bar inputs reset button
|
||||
!eventTarget.closest('.ais-SearchBox-reset') &&
|
||||
// don't count clicks on language button/menu
|
||||
!eventTarget.closest('.nav-lang') &&
|
||||
// don't count clicks on disabled elements
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Magnifier from '../../../assets/icons/Magnifier';
|
||||
import InputReset from '../../../assets/icons/inputReset';
|
||||
import { searchPageUrl } from '../../../utils/algolia-locale-setup';
|
||||
|
||||
type Props = {
|
||||
@@ -12,6 +13,7 @@ const SearchBarOptimized = ({ innerRef }: Props): JSX.Element => {
|
||||
const placeholder = t('search.placeholder');
|
||||
const searchUrl = searchPageUrl;
|
||||
const [value, setValue] = useState('');
|
||||
const inputElementRef = useRef<HTMLInputElement>(null);
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setValue(event.target.value);
|
||||
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
@@ -20,6 +22,10 @@ const SearchBarOptimized = ({ innerRef }: Props): JSX.Element => {
|
||||
window.open(`${searchUrl}?query=${encodeURIComponent(value)}`, '_blank');
|
||||
}
|
||||
};
|
||||
const onClick = () => {
|
||||
setValue('');
|
||||
inputElementRef.current?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='fcc_searchBar' data-testid='fcc_searchBar' ref={innerRef}>
|
||||
@@ -32,11 +38,14 @@ const SearchBarOptimized = ({ innerRef }: Props): JSX.Element => {
|
||||
onSubmit={onSubmit}
|
||||
role='search'
|
||||
>
|
||||
<label className='sr-only' htmlFor='ais-SearchBox-input'>
|
||||
{t ? t('search.label') : ''}
|
||||
</label>
|
||||
<input
|
||||
aria-label='Search'
|
||||
autoCapitalize='off'
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
id='ais-SearchBox-input'
|
||||
className='ais-SearchBox-input'
|
||||
maxLength={512}
|
||||
onChange={onChange}
|
||||
@@ -44,14 +53,20 @@ const SearchBarOptimized = ({ innerRef }: Props): JSX.Element => {
|
||||
spellCheck='false'
|
||||
type='search'
|
||||
value={value}
|
||||
ref={inputElementRef}
|
||||
/>
|
||||
<button
|
||||
className='ais-SearchBox-submit'
|
||||
title='Submit your search query.'
|
||||
type='submit'
|
||||
>
|
||||
<button className='ais-SearchBox-submit' type='submit'>
|
||||
<Magnifier />
|
||||
</button>
|
||||
{value && (
|
||||
<button
|
||||
className='ais-SearchBox-reset'
|
||||
onClick={onClick}
|
||||
type='button'
|
||||
>
|
||||
<InputReset />
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -187,6 +187,8 @@ export class SearchBar extends Component<SearchBarProps, SearchBarState> {
|
||||
render(): JSX.Element {
|
||||
const { isDropdownEnabled, isSearchFocused, innerRef, t } = this.props;
|
||||
const { index } = this.state;
|
||||
const submitTitle = t ? t('icons.magnifier') : '';
|
||||
const resetTitle = t ? t('icons.inputReset') : '';
|
||||
const placeholder = t ? t('search.placeholder') : '';
|
||||
|
||||
return (
|
||||
@@ -210,7 +212,11 @@ export class SearchBar extends Component<SearchBarProps, SearchBarState> {
|
||||
this.handleSearch(e);
|
||||
}}
|
||||
showLoadingIndicator={false}
|
||||
translations={{ placeholder }}
|
||||
translations={{
|
||||
submitTitle: submitTitle,
|
||||
resetTitle: resetTitle,
|
||||
placeholder: placeholder
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ObserveKeys>
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
.ais-Menu-showMore,
|
||||
.ais-RangeInput-submit,
|
||||
.ais-RefinementList-showMore,
|
||||
.ais-SearchBox-submit {
|
||||
.ais-SearchBox-submit,
|
||||
.ais-SearchBox-reset {
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
font: inherit;
|
||||
@@ -47,7 +48,8 @@
|
||||
.ais-Menu-showMore::-moz-focus-inner,
|
||||
.ais-RangeInput-submit::-moz-focus-inner,
|
||||
.ais-RefinementList-showMore::-moz-focus-inner,
|
||||
.ais-SearchBox-submit::-moz-focus-inner {
|
||||
.ais-SearchBox-submit::-moz-focus-inner,
|
||||
.ais-SearchBox-reset::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
@@ -60,7 +62,8 @@
|
||||
.ais-Menu-showMore[disabled],
|
||||
.ais-RangeInput-submit[disabled],
|
||||
.ais-RefinementList-showMore[disabled],
|
||||
.ais-SearchBox-submit[disabled] {
|
||||
.ais-SearchBox-submit[disabled],
|
||||
.ais-SearchBox-reset[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
.ais-Breadcrumb-list,
|
||||
@@ -609,7 +612,9 @@ a[class^='ais-'] {
|
||||
.ais-SearchBox-input:-moz-placeholder {
|
||||
color: var(--gray-15);
|
||||
}
|
||||
.ais-SearchBox-submit {
|
||||
.ais-SearchBox-submit,
|
||||
.ais-SearchBox-reset {
|
||||
position: absolute;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
@@ -617,8 +622,8 @@ a[class^='ais-'] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.ais-SearchBox-submit {
|
||||
position: absolute;
|
||||
.ais-SearchBox-reset {
|
||||
right: 0;
|
||||
}
|
||||
.ais-SearchBox-resetIcon,
|
||||
.ais-SearchBox-loadingIcon {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
color: var(--gray-00);
|
||||
}
|
||||
|
||||
.ais-SearchBox-reset,
|
||||
.ais-SearchBox-loadingIndicator {
|
||||
display: none;
|
||||
}
|
||||
@@ -22,11 +21,15 @@
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.ais-SearchBox-submit {
|
||||
.ais-SearchBox-submit,
|
||||
.ais-SearchBox-reset {
|
||||
margin-top: 6px;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
background-color: var(--gray-75);
|
||||
}
|
||||
|
||||
.ais-SearchBox-submit {
|
||||
padding-inline: 3px;
|
||||
}
|
||||
|
||||
@@ -38,7 +41,7 @@
|
||||
}
|
||||
|
||||
.fcc_searchBar .ais-SearchBox-input {
|
||||
padding-inline-start: 1.5em;
|
||||
padding-inline: 2em;
|
||||
}
|
||||
|
||||
.fcc_searchBar .ais-Hits {
|
||||
@@ -136,16 +139,16 @@ and arrow keys */
|
||||
@media (min-width: 981px) {
|
||||
.ais-SearchBox-input {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.fcc_searchBar {
|
||||
position: relative;
|
||||
max-width: 520px;
|
||||
max-width: 500px;
|
||||
}
|
||||
.fcc_searchBar .ais-Hits {
|
||||
top: auto;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-width: 100%;
|
||||
left: 0px;
|
||||
}
|
||||
.fcc_searchBar .ais-SearchBox-form {
|
||||
|
||||
Reference in New Issue
Block a user