diff --git a/.storybook/snapshots/__snapshots__/mapboxmap--multiple-pins.png b/.storybook/snapshots/__snapshots__/mapboxmap--multiple-pins.png
index 2635ba46..6f63a4a4 100644
Binary files a/.storybook/snapshots/__snapshots__/mapboxmap--multiple-pins.png and b/.storybook/snapshots/__snapshots__/mapboxmap--multiple-pins.png differ
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..0cb1e47d 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,26 @@ 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,
};
const customFacetElement = fieldIdToCustomFacetProps.get(facet.fieldId);
if (customFacetElement) {
facetProps = { ...facetProps, ...customFacetElement.props };
+ if (customFacetElement.props.showOptionsSearchInputLabel === undefined) {
+ facetProps.showOptionsSearchInputLabel = showOptionsSearchInputLabel;
+ }
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 +48,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}
+ {/* Keep the input first so sibling spacing utilities do not add a gap above it. */}
+ {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..40779c15 100644
--- a/tests/components/Facets.test.tsx
+++ b/tests/components/Facets.test.tsx
@@ -133,8 +133,76 @@ describe('Facets', () => {
/>
);
- expect(screen.getByText('Search Products Options')).toBeDefined();
- expect(screen.getByLabelText('Search Products Options')).toBeDefined();
+ expect(screen.getByText('Search Products Options')).not.toHaveClass('sr-only');
+ expect(screen.getByRole('textbox', { name: '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.getByRole('textbox', { name: 'Search Products Options' })).toBeDefined();
+ });
+
+ it('Associates distinct visually hidden labels with automatically rendered facets', () => {
+ const searchableFacets = DisplayableFacets.slice(0, 2).map(facet => ({
+ ...facet,
+ options: Array.from({ length: 11 }, (_, index) => ({
+ ...facet.options[0],
+ displayName: `${facet.displayName} ${index}`
+ }))
+ }));
+ mockAnswersState({
+ ...mockedState,
+ filters: {
+ facets: searchableFacets
+ }
+ });
+
+ render();
+
+ expect(screen.getByText('Search Products Options')).toHaveClass('sr-only');
+ expect(screen.getByRole('textbox', { name: 'Search Products Options' })).toBeDefined();
+ expect(screen.getByText('Search Price Options')).toHaveClass('sr-only');
+ expect(screen.getByRole('textbox', { name: 'Search Price Options' })).toBeDefined();
+ });
+
+ it('Passes the options search input label setting from Facets to a child facet', () => {
+ render(
+
+
+ );
+
+ expect(screen.getByText('Search Products Options')).not.toHaveClass('sr-only');
+ });
+
+ it('Lets an unspecified child label setting inherit from Facets', () => {
+ render(
+
+
+ );
+
+ expect(screen.getByText('Search Products Options')).not.toHaveClass('sr-only');
+ });
+
+ it('Lets a child facet override the label setting from Facets', () => {
+ render(
+
+
+ );
+
+ expect(screen.getByText('Search Products Options')).toHaveClass('sr-only');
});
it('Properly renders an override numerical facet if present', () => {
diff --git a/tests/components/NumericalFacetContent.test.tsx b/tests/components/NumericalFacetContent.test.tsx
index 42e026e1..1e4d86a9 100644
--- a/tests/components/NumericalFacetContent.test.tsx
+++ b/tests/components/NumericalFacetContent.test.tsx
@@ -169,8 +169,20 @@ describe('NumericalFacetContent', () => {
showOptionsSearchInputLabel: true
}));
- expect(screen.getByText(`Search ${numericalFacet.displayName} Options`)).toBeDefined();
- expect(screen.getByLabelText(`Search ${numericalFacet.displayName} Options`)).toBeDefined();
+ const accessibleName = `Search ${numericalFacet.displayName} Options`;
+ expect(screen.getByText(accessibleName)).not.toHaveClass('sr-only');
+ expect(screen.getByRole('textbox', { name: accessibleName })).toBeDefined();
+ });
+
+ it('Associates a visually hidden options search input label by default', () => {
+ render(mockNumericalFacet({
+ fieldId: numericalFacet.fieldId,
+ showMoreLimit: 1
+ }));
+
+ const accessibleName = `Search ${numericalFacet.displayName} Options`;
+ expect(screen.getByText(accessibleName)).toHaveClass('sr-only');
+ expect(screen.getByRole('textbox', { name: accessibleName })).toBeDefined();
});
});
diff --git a/tests/components/SearchI18nextProvider.test.tsx b/tests/components/SearchI18nextProvider.test.tsx
new file mode 100644
index 00000000..aaf34651
--- /dev/null
+++ b/tests/components/SearchI18nextProvider.test.tsx
@@ -0,0 +1,60 @@
+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', () => {
+ 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.getByRole('textbox', {
+ name: `Search ${staticFiltersProps.title} Options`
+ })).toBeDefined();
+ });
+
+ it('Applies an options search input label translation override', async () => {
+ renderStaticFilters('es', {
+ es: { filterGroupSearchInputLabel: 'Filtrar la lista de {{title}}' }
+ });
+
+ expect(await screen.findByRole('textbox', {
+ name: `Filtrar la lista de ${staticFiltersProps.title}`
+ })).toBeDefined();
+ });
+});
diff --git a/tests/components/StandardFacetContent.test.tsx b/tests/components/StandardFacetContent.test.tsx
index e14a74a3..96aabfcc 100644
--- a/tests/components/StandardFacetContent.test.tsx
+++ b/tests/components/StandardFacetContent.test.tsx
@@ -113,8 +113,20 @@ describe('StandardFacetContent', () => {
showOptionsSearchInputLabel: true
}));
- expect(screen.getByText(`Search ${standardFacet.displayName} Options`)).toBeDefined();
- expect(screen.getByLabelText(`Search ${standardFacet.displayName} Options`)).toBeDefined();
+ const accessibleName = `Search ${standardFacet.displayName} Options`;
+ expect(screen.getByText(accessibleName)).not.toHaveClass('sr-only');
+ expect(screen.getByRole('textbox', { name: accessibleName })).toBeDefined();
+ });
+
+ it('Associates a visually hidden options search input label by default', () => {
+ render(mockStandardFacet({
+ fieldId: standardFacet.fieldId,
+ showMoreLimit: 1
+ }));
+
+ const accessibleName = `Search ${standardFacet.displayName} Options`;
+ expect(screen.getByText(accessibleName)).toHaveClass('sr-only');
+ expect(screen.getByRole('textbox', { name: accessibleName })).toBeDefined();
});
});
diff --git a/tests/components/StaticFilters.test.tsx b/tests/components/StaticFilters.test.tsx
index d7a487d2..0dd3ee59 100644
--- a/tests/components/StaticFilters.test.tsx
+++ b/tests/components/StaticFilters.test.tsx
@@ -182,8 +182,44 @@ describe('Static Filters', () => {
/>
);
- expect(screen.getByText(`Search ${staticFiltersProps.title} Options`)).toBeDefined();
- expect(screen.getByLabelText(`Search ${staticFiltersProps.title} Options`)).toBeDefined();
+ const accessibleName = `Search ${staticFiltersProps.title} Options`;
+ expect(screen.getByText(accessibleName)).not.toHaveClass('sr-only');
+ expect(screen.getByRole('textbox', { name: accessibleName })).toBeDefined();
+ });
+
+ it('Associates a visually hidden search input label by default', () => {
+ render();
+
+ const accessibleName = `Search ${staticFiltersProps.title} Options`;
+ expect(screen.getByText(accessibleName)).toHaveClass('sr-only');
+ expect(screen.getByRole('textbox', { name: accessibleName })).toBeDefined();
+ });
+
+ it('Keeps custom visible-label classes from exposing a hidden label', () => {
+ render(
+
+ );
+
+ const label = screen.getByText(`Search ${staticFiltersProps.title} Options`);
+ expect(label).toHaveClass('sr-only');
+ expect(label).not.toHaveClass('custom-visible-label');
+ });
+
+ it('Keeps the search input first when its label is visually hidden', () => {
+ render();
+
+ const searchInput = screen.getByRole('textbox');
+ const label = screen.getByText(`Search ${staticFiltersProps.title} Options`);
+
+ /* 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 () => {