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