mirror of
https://github.com/suitenumerique/docs.git
synced 2026-08-17 21:25:43 +02:00
♿️(frontend) accessibility improvements on search components
Improve accessibility of search components by adding appropriate ARIA attributes and enhancing screen reader compatibility.
This commit is contained in:
@@ -213,7 +213,7 @@ test.describe('Doc grid move', () => {
|
||||
.getByRole('heading', { name: 'Choose a new parent doc' }),
|
||||
).toBeVisible();
|
||||
|
||||
const input = page.getByRole('combobox', { name: 'Quick search input' });
|
||||
const input = page.getByRole('combobox', { name: 'Search' });
|
||||
await input.click();
|
||||
await input.fill(titleDoc2);
|
||||
|
||||
@@ -303,7 +303,7 @@ test.describe('Doc grid move', () => {
|
||||
.getByRole('heading', { name: 'Choose a new parent doc' }),
|
||||
).toBeVisible();
|
||||
|
||||
const input = page.getByRole('combobox', { name: 'Quick search input' });
|
||||
const input = page.getByRole('combobox', { name: 'Search' });
|
||||
await input.click();
|
||||
await input.fill(titleDoc2);
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ test.describe('Document search', () => {
|
||||
|
||||
// Click on the filter to show all docs
|
||||
await page
|
||||
.getByLabel('Search results controls')
|
||||
.getByRole('switch', { name: 'Search in all documents' })
|
||||
.getByText('All docs')
|
||||
.click();
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export const QuickSearch = ({
|
||||
inputValue,
|
||||
showInput = true,
|
||||
label,
|
||||
loading,
|
||||
placeholder,
|
||||
beforeList,
|
||||
children,
|
||||
@@ -64,6 +65,7 @@ export const QuickSearch = ({
|
||||
>
|
||||
{showInput && (
|
||||
<QuickSearchInput
|
||||
label={label}
|
||||
withSeparator={hasChildrens(children)}
|
||||
inputValue={inputValue}
|
||||
onFilter={onFilter}
|
||||
@@ -74,7 +76,7 @@ export const QuickSearch = ({
|
||||
</QuickSearchInput>
|
||||
)}
|
||||
{beforeList}
|
||||
<Command.List id={listId} aria-label={label} role="listbox">
|
||||
<Command.List id={listId} aria-busy={loading}>
|
||||
<Box>{children}</Box>
|
||||
</Command.List>
|
||||
</Command>
|
||||
|
||||
@@ -19,6 +19,7 @@ export const QuickSearchGroup = <T,>({
|
||||
}: Props<T>) => {
|
||||
return (
|
||||
<>
|
||||
{group.groupName && (
|
||||
<Text
|
||||
className="--docs--quick-search-group-title"
|
||||
as="h2"
|
||||
@@ -28,6 +29,7 @@ export const QuickSearchGroup = <T,>({
|
||||
>
|
||||
{group.groupName}
|
||||
</Text>
|
||||
)}
|
||||
<Command.Group
|
||||
key={group.groupName}
|
||||
forceMount={false}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Box } from '../Box';
|
||||
|
||||
type QuickSearchInputProps = {
|
||||
inputValue?: string;
|
||||
label?: string;
|
||||
onFilter?: (str: string) => void;
|
||||
placeholder?: string;
|
||||
withSeparator?: boolean;
|
||||
@@ -18,6 +19,7 @@ type QuickSearchInputProps = {
|
||||
};
|
||||
export const QuickSearchInput = ({
|
||||
inputValue,
|
||||
label,
|
||||
onFilter,
|
||||
placeholder,
|
||||
children,
|
||||
@@ -60,14 +62,12 @@ export const QuickSearchInput = ({
|
||||
<Command.Input
|
||||
ref={inputRef}
|
||||
autoFocus={true}
|
||||
aria-label={t('Quick search input')}
|
||||
aria-label={label ?? t('Search')}
|
||||
aria-controls={listId}
|
||||
value={inputValue}
|
||||
role="combobox"
|
||||
placeholder={placeholder ?? t('Search')}
|
||||
onValueChange={onFilter}
|
||||
maxLength={254}
|
||||
minLength={6}
|
||||
data-testid="quick-search-input"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
export const QuickSearchStyle = createGlobalStyle`
|
||||
& *:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.quick-search-container {
|
||||
[cmdk-root] {
|
||||
width: 100%;
|
||||
|
||||
+4
-2
@@ -95,9 +95,11 @@ export const DocSearchContent = ({
|
||||
: [],
|
||||
});
|
||||
|
||||
if (search) {
|
||||
if (search && !loading) {
|
||||
announce(
|
||||
t('{{count}} result(s) available', { count: elements.length }),
|
||||
elements.length === 0
|
||||
? t('No documents found')
|
||||
: t('{{count}} document found', { count: elements.length }),
|
||||
'polite',
|
||||
);
|
||||
}
|
||||
|
||||
+20
-7
@@ -9,43 +9,56 @@ import { useDocSearchFilterStore } from '../stores/useDocSearchFilterStore';
|
||||
export const DocSearchFilters = () => {
|
||||
const { t } = useTranslation();
|
||||
const { setFilter, filter } = useDocSearchFilterStore();
|
||||
const isAll = filter === 'all';
|
||||
const toggle = () => setFilter(isAll ? 'current' : 'all');
|
||||
|
||||
return (
|
||||
/**
|
||||
* The switch is not focusable, so we wrap it in a div that can be focused
|
||||
* and handle the keydown event to toggle the switch with
|
||||
* space key for accessibility reasons
|
||||
* space or enter key for accessibility reasons
|
||||
*/
|
||||
<Box
|
||||
role="switch"
|
||||
aria-checked={isAll}
|
||||
aria-label={t('Search in all documents')}
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === ' ') {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
setFilter(filter === 'all' ? 'current' : 'all');
|
||||
toggle();
|
||||
}
|
||||
}}
|
||||
$css={css`
|
||||
&:focus-visible .c__switch__rail {
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
|
||||
.c__switch__rail {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px
|
||||
var(--c--contextuals--border--semantic--brand--primary);
|
||||
}
|
||||
}
|
||||
// Remove the default focus style of the switch component
|
||||
.c__checkbox:focus-within {
|
||||
.c__checkbox {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
outline: 0;
|
||||
}
|
||||
& *:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Switch
|
||||
labelSide="right"
|
||||
label={t('All docs')}
|
||||
checked={filter === 'all'}
|
||||
onChange={() => setFilter(filter === 'all' ? 'current' : 'all')}
|
||||
checked={isAll}
|
||||
onChange={toggle}
|
||||
aria-label={t(
|
||||
'Toggle to search in all documents or only in current document',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
+22
-7
@@ -90,12 +90,25 @@ const DocSearchModalGlobal = ({
|
||||
closeOnClickOutside
|
||||
size={isLargeScreen ? ModalSize.LARGE : ModalSize.FULL}
|
||||
hideCloseButton
|
||||
aria-describedby="doc-search-modal-title"
|
||||
aria-label={t('Search for a document')}
|
||||
aria-labelledby="doc-search-modal-title"
|
||||
aria-describedby="doc-search-modal-description"
|
||||
title={
|
||||
<>
|
||||
<Text as="h2" $margin="0" $size="s" $align="flex-start">
|
||||
<Text
|
||||
as="h2"
|
||||
$margin="0"
|
||||
$size="s"
|
||||
$align="flex-start"
|
||||
id="doc-search-modal-title"
|
||||
>
|
||||
{t('Search for a document')}
|
||||
</Text>
|
||||
<Text id="doc-search-modal-description" className="sr-only">
|
||||
{t(
|
||||
'Search documents by name, navigate using arrows, and select a result with Enter.',
|
||||
)}
|
||||
</Text>
|
||||
<Box $position="absolute" $css="top: 4px; right: 4px;">
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the search modal')}
|
||||
@@ -111,7 +124,6 @@ const DocSearchModalGlobal = ({
|
||||
$justify="space-between"
|
||||
className="--docs--doc-search-modal"
|
||||
$padding={{ bottom: 'base' }}
|
||||
aria-label={t('Search modal')}
|
||||
>
|
||||
<QuickSearch
|
||||
label={t('Search documents')}
|
||||
@@ -120,14 +132,17 @@ const DocSearchModalGlobal = ({
|
||||
onFilter={handleInputSearch}
|
||||
beforeList={
|
||||
<Box
|
||||
$margin={{ top: 'sm', horizontal: 'base' }}
|
||||
$margin={{ vertical: 'sm', horizontal: 'base' }}
|
||||
$justify="space-between"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
role="group"
|
||||
aria-label={t('Search results controls')}
|
||||
>
|
||||
<Text $color="textSecondary" $weight="700">
|
||||
<Text
|
||||
$color="textSecondary"
|
||||
$weight="700"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<DocSearchStateText
|
||||
hasResults={results.length > 0}
|
||||
filter={filter}
|
||||
|
||||
@@ -179,7 +179,7 @@ export const DocMoveModal = ({
|
||||
</Text>
|
||||
<Box $position="absolute" $css="top: 4px; right: 4px;">
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the search modal')}
|
||||
aria-label={t('Close the move modal')}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user