diff --git a/catalog/CHANGELOG.md b/catalog/CHANGELOG.md index 99878fca7f5..2cbb430deb3 100644 --- a/catalog/CHANGELOG.md +++ b/catalog/CHANGELOG.md @@ -21,6 +21,7 @@ complete sentence without it. ## Changes +- [Fixed] Search sidebar: the facet "Sort by" control announces what it is — its label used to land on a hidden input, leaving assistive tech to read the control as its current ordering and nothing more ([#5261](https://github.com/quiltdata/quilt/pull/5261)) - [Fixed] Queries: the query selector announces its label to assistive tech, and no longer claims "Custom" is loaded while its helper text reports the query failed to load ([#5260](https://github.com/quiltdata/quilt/pull/5260)) - [Changed] The `data-products` demo fixture data no longer ships in the bundles a browser downloads on the volumes landing; it loads only when the preview is on ([#5259](https://github.com/quiltdata/quilt/pull/5259)) - [Fixed] Quilt+ URI parsing retains the informational `catalog` field, reads a raw `+` in an unencoded path as a `+` rather than a space, and shares its compatibility corpus with quilt3 ([#5255](https://github.com/quiltdata/quilt/pull/5255)) diff --git a/catalog/app/containers/Search/Layout/PackageFilters.spec.tsx b/catalog/app/containers/Search/Layout/PackageFilters.spec.tsx new file mode 100644 index 00000000000..0e951aea976 --- /dev/null +++ b/catalog/app/containers/Search/Layout/PackageFilters.spec.tsx @@ -0,0 +1,47 @@ +import * as React from 'react' +import { render, cleanup, fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, vi, afterEach } from 'vitest' + +import * as SearchUIModel from '../model' + +import { AvailablePackagesMetaFilters } from './PackageFilters' + +vi.mock('constants/config', () => ({ default: {} })) + +function renderFilters() { + return render( + , + ) +} + +describe('containers/Search/Layout/PackageFilters', () => { + afterEach(cleanup) + + describe('the accessible name', () => { + // The exact name, not a substring: the caption alone matches /Sort by/ too, + // so a loosened assertion passes on wiring that drops the ordering. + it('carries the label and the selected ordering', () => { + renderFilters() + expect(screen.getByRole('button', { name: 'Sort by: Name A → Z' })).toBeTruthy() + }) + + it('names the popup listbox too', () => { + renderFilters() + fireEvent.mouseDown(screen.getByRole('button')) + expect(screen.getByRole('listbox', { name: 'Sort by:' })).toBeTruthy() + }) + }) +}) diff --git a/catalog/app/containers/Search/Layout/PackageFilters.tsx b/catalog/app/containers/Search/Layout/PackageFilters.tsx index d7f884c93d6..17d16e8c0f6 100644 --- a/catalog/app/containers/Search/Layout/PackageFilters.tsx +++ b/catalog/app/containers/Search/Layout/PackageFilters.tsx @@ -8,6 +8,7 @@ import * as FiltersUI from 'components/Filters' import Skeleton from 'components/Skeleton' import * as JSONPointer from 'utils/JSONPointer' import * as NamedRoutes from 'utils/NamedRoutes' +import useId from 'utils/useId' import FilterWidget from '../FilterWidget' import { PACKAGE_FILTER_LABELS } from '../i18n' @@ -189,7 +190,9 @@ interface AvailablePackagesMetaFiltersProps { fetching: boolean } -function AvailablePackagesMetaFilters({ +// Exported for testing: the ordering control's accessible name lives in how +// props land on MUI's Select internals, which only a render can assert. +export function AvailablePackagesMetaFilters({ className, filtering, facets, @@ -197,6 +200,8 @@ function AvailablePackagesMetaFilters({ fetching, }: AvailablePackagesMetaFiltersProps) { const classes = useAvailablePackagesMetaFiltersStyles() + const orderLabelId = useId() + const orderButtonId = useId() const [expanded, setExpanded] = React.useState(false) const toggleExpanded = React.useCallback(() => setExpanded((x) => !x), []) @@ -237,7 +242,7 @@ function AvailablePackagesMetaFilters({ on the client-filter path. */} {ordering.offered && (
- + Sort by: @@ -246,7 +251,12 @@ function AvailablePackagesMetaFilters({ disabled={fetching} extents={FACET_ORDERING_VALUES} getOptionLabel={(value) => FACET_ORDERING_LABELS[value]} - inputProps={{ 'aria-labelledby': 'meta-order-label' }} + // `labelId` reaches the focusable display div and names the popup + // listbox; `id` must come with it, since MUI joins the two into + // `aria-labelledby="labelId buttonId"` and `labelId` alone would + // override the div's contents, dropping the ordering from the name. + labelId={orderLabelId} + id={orderButtonId} onChange={(value) => ordering.set(SearchUIModel.parseFacetOrdering(value, ordering.value)) }