️(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:
Anthony LC
2026-06-10 16:51:02 +02:00
parent 7edbd4efcb
commit 6ce853312f
10 changed files with 69 additions and 39 deletions
@@ -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%;
@@ -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',
);
}
@@ -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>
);
@@ -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>