diff --git a/docs/search-ui-react.facetsprops.md b/docs/search-ui-react.facetsprops.md
index b9e42187..516e136d 100644
--- a/docs/search-ui-react.facetsprops.md
+++ b/docs/search-ui-react.facetsprops.md
@@ -148,6 +148,25 @@ boolean
_(Optional)_ Whether or not a search is automatically run when a filter is selected. Defaults to true.
+
+
+
+_(Optional)_ Whether or not to display the options search input label visibly on facets that render an options search input. Defaults to false, and is overridden by the same prop on a child facet.
+
+
diff --git a/docs/search-ui-react.facetsprops.showoptionssearchinputlabel.md b/docs/search-ui-react.facetsprops.showoptionssearchinputlabel.md
new file mode 100644
index 00000000..4665cf66
--- /dev/null
+++ b/docs/search-ui-react.facetsprops.showoptionssearchinputlabel.md
@@ -0,0 +1,18 @@
+
+
+[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [FacetsProps](./search-ui-react.facetsprops.md) > [showOptionsSearchInputLabel](./search-ui-react.facetsprops.showoptionssearchinputlabel.md)
+
+## FacetsProps.showOptionsSearchInputLabel property
+
+Whether or not to display the options search input label visibly on facets that render an options search input. Defaults to false, and is overridden by the same prop on a child facet.
+
+**Signature:**
+
+```typescript
+showOptionsSearchInputLabel?: boolean;
+```
+
+## Remarks
+
+The label is always associated with the search input so that it has an accessible name. When false, the label is visually hidden but remains available to assistive technology. Hierarchical facets do not render an options search input, so this has no effect on them.
+
diff --git a/docs/search-ui-react.filtergroupprops.md b/docs/search-ui-react.filtergroupprops.md
index ef91c8db..741cb994 100644
--- a/docs/search-ui-react.filtergroupprops.md
+++ b/docs/search-ui-react.filtergroupprops.md
@@ -183,7 +183,7 @@ boolean
-_(Optional)_ Whether or not to display the visible search input label. Defaults to false.
+_(Optional)_ Whether or not to display the search input label visibly. Defaults to false.
diff --git a/docs/search-ui-react.filtergroupprops.showoptionssearchinputlabel.md b/docs/search-ui-react.filtergroupprops.showoptionssearchinputlabel.md
index f9fcf5dd..a8512cee 100644
--- a/docs/search-ui-react.filtergroupprops.showoptionssearchinputlabel.md
+++ b/docs/search-ui-react.filtergroupprops.showoptionssearchinputlabel.md
@@ -4,10 +4,15 @@
## FilterGroupProps.showOptionsSearchInputLabel property
-Whether or not to display the visible search input label. Defaults to false.
+Whether or not to display the search input label visibly. Defaults to false.
**Signature:**
```typescript
showOptionsSearchInputLabel?: boolean;
```
+
+## Remarks
+
+The label is always associated with the search input so that it has an accessible name. When false, the label is visually hidden but remains available to assistive technology.
+
diff --git a/docs/search-ui-react.standardfacetprops.md b/docs/search-ui-react.standardfacetprops.md
index 01eeb0d4..e481e192 100644
--- a/docs/search-ui-react.standardfacetprops.md
+++ b/docs/search-ui-react.standardfacetprops.md
@@ -183,7 +183,7 @@ boolean
-_(Optional)_ Whether or not to show the label for the search bar when present. Defaults to false.
+_(Optional)_ Whether or not to display the search input label visibly. Defaults to false.
diff --git a/docs/search-ui-react.standardfacetprops.showoptionssearchinputlabel.md b/docs/search-ui-react.standardfacetprops.showoptionssearchinputlabel.md
index ec43d247..a8449e15 100644
--- a/docs/search-ui-react.standardfacetprops.showoptionssearchinputlabel.md
+++ b/docs/search-ui-react.standardfacetprops.showoptionssearchinputlabel.md
@@ -4,10 +4,15 @@
## StandardFacetProps.showOptionsSearchInputLabel property
-Whether or not to show the label for the search bar when present. Defaults to false.
+Whether or not to display the search input label visibly. Defaults to false.
**Signature:**
```typescript
showOptionsSearchInputLabel?: boolean;
```
+
+## Remarks
+
+The label is always associated with the search input so that it has an accessible name. When false, the label is visually hidden but remains available to assistive technology.
+
diff --git a/docs/search-ui-react.staticfiltersprops.md b/docs/search-ui-react.staticfiltersprops.md
index 4ad9fdbc..595e2ec1 100644
--- a/docs/search-ui-react.staticfiltersprops.md
+++ b/docs/search-ui-react.staticfiltersprops.md
@@ -183,7 +183,7 @@ boolean
-_(Optional)_ Whether or not to display the visible search input label. Defaults to false.
+_(Optional)_ Whether or not to display the search input label visibly. Defaults to false.
diff --git a/docs/search-ui-react.staticfiltersprops.showoptionssearchinputlabel.md b/docs/search-ui-react.staticfiltersprops.showoptionssearchinputlabel.md
index af6b96e4..fb6ba190 100644
--- a/docs/search-ui-react.staticfiltersprops.showoptionssearchinputlabel.md
+++ b/docs/search-ui-react.staticfiltersprops.showoptionssearchinputlabel.md
@@ -4,10 +4,15 @@
## StaticFiltersProps.showOptionsSearchInputLabel property
-Whether or not to display the visible search input label. Defaults to false.
+Whether or not to display the search input label visibly. Defaults to false.
**Signature:**
```typescript
showOptionsSearchInputLabel?: boolean;
```
+
+## Remarks
+
+The label is always associated with the search input so that it has an accessible name. When false, the label is visually hidden but remains available to assistive technology.
+
diff --git a/etc/search-ui-react.api.md b/etc/search-ui-react.api.md
index 9e8114b2..564351e4 100644
--- a/etc/search-ui-react.api.md
+++ b/etc/search-ui-react.api.md
@@ -262,6 +262,7 @@ export interface FacetsProps {
hierarchicalFieldIds?: string[];
onlyRenderChildren?: boolean;
searchOnChange?: boolean;
+ showOptionsSearchInputLabel?: boolean;
}
// @public
@@ -1174,7 +1175,7 @@ export interface VisualAutocompleteConfig {
// Warnings were encountered during analysis:
//
-// dist/index.d.ts:1921:5 - (ae-forgotten-export) The symbol "translations" needs to be exported by the entry point index.d.ts
+// dist/index.d.ts:1934:5 - (ae-forgotten-export) The symbol "translations" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)
diff --git a/src/components/FacetProps.ts b/src/components/FacetProps.ts
index 282aaa5f..9236966b 100644
--- a/src/components/FacetProps.ts
+++ b/src/components/FacetProps.ts
@@ -33,6 +33,16 @@ export interface FacetsProps {
excludedFieldIds?: string[],
/** List of field ids that should be rendered as hierarchical facets. */
hierarchicalFieldIds?: string[],
+ /**
+ * Whether or not to display the options search input label visibly on facets that render an
+ * options search input. Defaults to false, and is overridden by the same prop on a child facet.
+ *
+ * @remarks
+ * The label is always associated with the search input so that it has an accessible name.
+ * When false, the label is visually hidden but remains available to assistive technology.
+ * Hierarchical facets do not render an options search input, so this has no effect on them.
+ */
+ showOptionsSearchInputLabel?: boolean,
/** The custom facet components that will override the default rendering.
*
* @remarks
@@ -66,10 +76,7 @@ export interface StandardFacetProps {
showMoreLimit?: number,
/** CSS classes for customizing the component styling. */
customCssClasses?: FilterGroupCssClasses,
- /**
- * Whether or not to show the label for the search bar when present.
- * Defaults to false.
- */
+ /** {@inheritDoc FilterGroupProps.showOptionsSearchInputLabel} */
showOptionsSearchInputLabel?: boolean
}
diff --git a/src/components/Facets.tsx b/src/components/Facets.tsx
index 871abad0..5d9f2dcf 100644
--- a/src/components/Facets.tsx
+++ b/src/components/Facets.tsx
@@ -40,6 +40,7 @@ export function Facets(props: FacetsProps): React.JSX.Element {
hierarchicalFieldIds,
excludedFieldIds = [],
customCssClasses = {},
+ showOptionsSearchInputLabel,
} = props;
const resolvedHierarchicalFieldIds = useMemo(() => hierarchicalFieldIds ?? [], [hierarchicalFieldIds]);
@@ -89,6 +90,7 @@ export function Facets(props: FacetsProps): React.JSX.Element {
facetsCustomCssClasses={customCssClasses}
fieldIdToCustomFacetProps={fieldIdToCustomFacetProps}
hierarchicalFieldIds={resolvedHierarchicalFieldIds}
+ showOptionsSearchInputLabel={showOptionsSearchInputLabel}
/>
{(i < facets.length - 1)
&& }
@@ -140,6 +142,7 @@ export function HierarchicalFacet(props: HierarchicalFacetProps) { return null;
* @param facetsCustomCssClasses - {@link FacetsCssClasses}
* @param fieldIdToCustomFacetProps - a map of fieldId to facet props
* @param hierarchicalFieldIds - a list of hierarchical field ids
+ * @param showOptionsSearchInputLabel - whether to display the options search input label visibly
* @returns {@link ReactElement}
*
* @internal
@@ -149,20 +152,27 @@ export function Facet({
facetsCustomCssClasses,
fieldIdToCustomFacetProps,
hierarchicalFieldIds,
+ showOptionsSearchInputLabel,
}: {
facet: DisplayableFacet,
facetsCustomCssClasses: FacetsCssClasses,
fieldIdToCustomFacetProps: Map,
- hierarchicalFieldIds: string[]
+ hierarchicalFieldIds: string[],
+ showOptionsSearchInputLabel?: boolean
}) {
let facetType: FacetType;
let facetProps: FacetProps = {
fieldId: facet.fieldId,
label: facet.displayName,
+ ...showOptionsSearchInputLabel !== undefined && { showOptionsSearchInputLabel },
};
const customFacetElement = fieldIdToCustomFacetProps.get(facet.fieldId);
if (customFacetElement) {
- facetProps = { ...facetProps, ...customFacetElement.props };
+ // A prop passed as undefined counts as unspecified, so that values supplied by Facets are
+ // inherited instead of erased.
+ const specifiedCustomProps = Object.fromEntries(
+ Object.entries(customFacetElement.props).filter(([, value]) => value !== undefined));
+ facetProps = { ...facetProps, ...specifiedCustomProps };
facetType = getFacetTypeFromReactElementType(
(typeof customFacetElement.type === 'function')
? customFacetElement.type.name : '');
diff --git a/src/components/FilterGroup.tsx b/src/components/FilterGroup.tsx
index a90a9b7d..ad77bd3b 100644
--- a/src/components/FilterGroup.tsx
+++ b/src/components/FilterGroup.tsx
@@ -50,7 +50,13 @@ export interface FilterGroupProps {
defaultExpanded?: boolean,
/** Whether or not to display a text input to search for filter options. */
searchable?: boolean,
- /** Whether or not to display the visible search input label. Defaults to false. */
+ /**
+ * Whether or not to display the search input label visibly. Defaults to false.
+ *
+ * @remarks
+ * The label is always associated with the search input so that it has an accessible name.
+ * When false, the label is visually hidden but remains available to assistive technology.
+ */
showOptionsSearchInputLabel?: boolean,
/** CSS classes for customizing the component styling. */
customCssClasses?: FilterGroupCssClasses,
@@ -83,11 +89,9 @@ export function FilterGroup({
...optionInput && { input: optionInput }
};
}, [customCssClasses]);
- const searchInputLabel = showOptionsSearchInputLabel
- ? t('filterGroupSearchInputLabel', {
- title
- })
- : undefined;
+ const searchInputLabel = t('filterGroupSearchInputLabel', {
+ title
+ });
return (
}
}
) => {
@@ -42,13 +55,18 @@ export function SearchInput(props: SearchInputProps): React.JSX.Element {
const labelId = useId('filters-search-input-label');
const inputId = useId('filter-group-search-input');
+ const labelElement = label && (
+
+ );
return (
<>
- {label && (
-
- )}
+ {!visuallyHiddenLabel && labelElement}
+ {/* A hidden label follows the input so that the input remains the first child of its
+ container. Sibling spacing utilities such as Tailwind's space-y-* skip the first child,
+ so a preceding label would add a gap above the input even though the label itself takes
+ up no space. htmlFor keeps the association regardless of order. */}
+ {visuallyHiddenLabel && labelElement}
>
);
}
diff --git a/src/components/SearchI18nextProvider.tsx b/src/components/SearchI18nextProvider.tsx
index 707efcae..a053c878 100644
--- a/src/components/SearchI18nextProvider.tsx
+++ b/src/components/SearchI18nextProvider.tsx
@@ -28,6 +28,7 @@ type translationKeys =
'dismiss' |
'dropDownScreenReaderInstructions' |
'feedback' |
+ 'filterGroupSearchInputLabel' |
'invalidRange' |
'max' |
'min' |
diff --git a/tests/components/Facets.test.tsx b/tests/components/Facets.test.tsx
index 6b91a0d6..6fc0c61a 100644
--- a/tests/components/Facets.test.tsx
+++ b/tests/components/Facets.test.tsx
@@ -133,7 +133,50 @@ describe('Facets', () => {
/>
);
- expect(screen.getByText('Search Products Options')).toBeDefined();
+ expect(screen.getByText('Search Products Options')).not.toHaveClass('sr-only');
+ expect(screen.getByLabelText('Search Products Options')).toBeDefined();
+ });
+
+ it('Associates a visually hidden options search input label by default', () => {
+ render(
+
+
+ );
+
+ expect(screen.getByText('Search Products Options')).toHaveClass('sr-only');
+ expect(screen.getByLabelText('Search Products Options')).toBeDefined();
+ });
+
+ it('Passes the options search input label setting from Facets to a facet', () => {
+ render(
+
+
+ );
+
+ expect(screen.getByText('Search Products Options')).not.toHaveClass('sr-only');
+ expect(screen.getByLabelText('Search Products Options')).toBeDefined();
+ });
+
+ it('A facet passing the options search input label setting as undefined inherits Facets', () => {
+ render(
+
+
+ );
+
+ expect(screen.getByText('Search Products Options')).not.toHaveClass('sr-only');
+ });
+
+ it('A facet overrides the options search input label setting from Facets', () => {
+ render(
+
+
+ );
+
+ expect(screen.getByText('Search Products Options')).toHaveClass('sr-only');
expect(screen.getByLabelText('Search Products Options')).toBeDefined();
});
diff --git a/tests/components/NumericalFacetContent.test.tsx b/tests/components/NumericalFacetContent.test.tsx
index 42e026e1..5424cc2e 100644
--- a/tests/components/NumericalFacetContent.test.tsx
+++ b/tests/components/NumericalFacetContent.test.tsx
@@ -169,7 +169,19 @@ describe('NumericalFacetContent', () => {
showOptionsSearchInputLabel: true
}));
- expect(screen.getByText(`Search ${numericalFacet.displayName} Options`)).toBeDefined();
+ const label = screen.getByText(`Search ${numericalFacet.displayName} Options`);
+ expect(label).not.toHaveClass('sr-only');
+ expect(screen.getByLabelText(`Search ${numericalFacet.displayName} Options`)).toBeDefined();
+ });
+
+ it('Associates a visually hidden options search input label by default', () => {
+ render(mockNumericalFacet({
+ fieldId: numericalFacet.fieldId,
+ showMoreLimit: 1
+ }));
+
+ const label = screen.getByText(`Search ${numericalFacet.displayName} Options`);
+ expect(label).toHaveClass('sr-only');
expect(screen.getByLabelText(`Search ${numericalFacet.displayName} Options`)).toBeDefined();
});
});
diff --git a/tests/components/SearchI18nextProvider.test.tsx b/tests/components/SearchI18nextProvider.test.tsx
new file mode 100644
index 00000000..85dd13d1
--- /dev/null
+++ b/tests/components/SearchI18nextProvider.test.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { SearchHeadlessContext } from '@yext/search-headless-react';
+import {
+ SearchI18nextProvider,
+ SearchTranslationOverrides
+} from '../../src/components/SearchI18nextProvider';
+import { StaticFilters } from '../../src/components';
+import { staticFilters, staticFiltersProps } from '../__fixtures__/data/filters';
+import { generateMockedHeadless } from '../__fixtures__/search-headless';
+import { i18nInstance } from '../../src/utils';
+
+const NAMESPACE = 'search-ui-react';
+const LABEL_KEY = 'filterGroupSearchInputLabel';
+const originalEsLabel: string =
+ i18nInstance.getResource('es', NAMESPACE, LABEL_KEY);
+
+function renderStaticFilters(
+ locale?: string,
+ translationOverrides?: SearchTranslationOverrides
+) {
+ const searcher = generateMockedHeadless({
+ filters: { static: staticFilters },
+ vertical: { verticalKey: 'vertical1' },
+ meta: { searchType: 'vertical', locale }
+ });
+
+ return render(
+
+
+
+
+ );
+}
+
+describe('SearchI18nextProvider', () => {
+ // Overrides are added to a module scoped i18next instance, so undo them to keep tests
+ // independent of the order they run in.
+ afterEach(async () => {
+ i18nInstance.addResourceBundle(
+ 'es', NAMESPACE, { [LABEL_KEY]: originalEsLabel }, true, true);
+ await i18nInstance.changeLanguage('en');
+ });
+
+ it('Uses the default options search input label when no override is given', () => {
+ renderStaticFilters();
+
+ expect(screen.getByLabelText(`Search ${staticFiltersProps.title} Options`)).toBeDefined();
+ });
+
+ it('Applies a filterGroupSearchInputLabel override to the options search input', async () => {
+ renderStaticFilters('es', {
+ es: { filterGroupSearchInputLabel: 'Filtrar la lista de {{title}}' }
+ });
+
+ expect(
+ await screen.findByLabelText(`Filtrar la lista de ${staticFiltersProps.title}`)
+ ).toBeDefined();
+ });
+
+ it('Restores the default label once an override is undone', () => {
+ renderStaticFilters();
+
+ expect(screen.getByLabelText(`Search ${staticFiltersProps.title} Options`)).toBeDefined();
+ });
+});
diff --git a/tests/components/StandardFacetContent.test.tsx b/tests/components/StandardFacetContent.test.tsx
index e14a74a3..0188078d 100644
--- a/tests/components/StandardFacetContent.test.tsx
+++ b/tests/components/StandardFacetContent.test.tsx
@@ -113,7 +113,19 @@ describe('StandardFacetContent', () => {
showOptionsSearchInputLabel: true
}));
- expect(screen.getByText(`Search ${standardFacet.displayName} Options`)).toBeDefined();
+ const label = screen.getByText(`Search ${standardFacet.displayName} Options`);
+ expect(label).not.toHaveClass('sr-only');
+ expect(screen.getByLabelText(`Search ${standardFacet.displayName} Options`)).toBeDefined();
+ });
+
+ it('Associates a visually hidden options search input label by default', () => {
+ render(mockStandardFacet({
+ fieldId: standardFacet.fieldId,
+ showMoreLimit: 1
+ }));
+
+ const label = screen.getByText(`Search ${standardFacet.displayName} Options`);
+ expect(label).toHaveClass('sr-only');
expect(screen.getByLabelText(`Search ${standardFacet.displayName} Options`)).toBeDefined();
});
});
diff --git a/tests/components/StaticFilters.test.tsx b/tests/components/StaticFilters.test.tsx
index d7a487d2..4b9613ad 100644
--- a/tests/components/StaticFilters.test.tsx
+++ b/tests/components/StaticFilters.test.tsx
@@ -182,10 +182,48 @@ describe('Static Filters', () => {
/>
);
- expect(screen.getByText(`Search ${staticFiltersProps.title} Options`)).toBeDefined();
+ const label = screen.getByText(`Search ${staticFiltersProps.title} Options`);
+ expect(label).not.toHaveClass('sr-only');
expect(screen.getByLabelText(`Search ${staticFiltersProps.title} Options`)).toBeDefined();
});
+ it('Associates a visually hidden search input label by default', () => {
+ render();
+
+ const label = screen.getByText(`Search ${staticFiltersProps.title} Options`);
+ expect(label).toHaveClass('sr-only');
+ expect(screen.getByLabelText(`Search ${staticFiltersProps.title} Options`)).toBeDefined();
+ });
+
+ it('Uses the searchInputLabel class in place of sr-only on a hidden label', () => {
+ render(
+
+ );
+
+ const label = screen.getByText(`Search ${staticFiltersProps.title} Options`);
+ expect(label).toHaveClass('custom-hidden');
+ expect(label).not.toHaveClass('sr-only');
+ });
+
+ it('Keeps the search input first in its container when the label is visually hidden', () => {
+ render();
+
+ const searchInput = screen.getByRole('textbox');
+ const label = screen.getByText(`Search ${staticFiltersProps.title} Options`);
+
+ // Sibling order is the subject here: an element in front of the input gives it a space-y-*
+ // margin that the input should not have when the label is hidden.
+ /* eslint-disable testing-library/no-node-access */
+ expect(searchInput.previousElementSibling).toBeNull();
+ expect(searchInput.nextElementSibling).toBe(label);
+ /* eslint-enable testing-library/no-node-access */
+ });
+
it('Clicking a filter option executes a search when searchOnChange is true', async () => {
const actions = spyOnActions();
render();