From 85ebbbba646aef59b3d63c3a1cc1ed4d39d9d210 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Wed, 28 Feb 2024 12:07:01 -0600 Subject: [PATCH] STCOM-1192 Group checkboxes in FilterGroups (a11y) (#2226) * accept full label id from outside of the component via headerProps * allow for elements in label proptype. Derive string from within FormatMessage, if supplied. * Get string from possible FormattedMessage in group label for Accordion id of FilterGroup * log changes --- CHANGELOG.md | 1 + lib/Accordion/Accordion.js | 6 +-- lib/FilterControlGroup/FilterControlGroup.js | 26 ++++----- lib/FilterGroups/FilterGroups.js | 56 ++++++++++++-------- 4 files changed, 53 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8669fcbc4..2bbc95075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ * Apply highest z-index to focused `` within `AccordionSet`. Refs STCOM-1257. * Upgrade storybook to v7. Refs STCOM-1176. * Fix bug with MCL not re-enabling paging buttons after more data was loaded. Refs STCOM-1262. +* Accessible grouping for filter group checkboxes via `role="group"`. Refs STCOM-1192. ## [12.0.0](https://github.com/folio-org/stripes-components/tree/v12.0.0) (2023-10-11) [Full Changelog](https://github.com/folio-org/stripes-components/compare/v11.0.0...v12.0.0) diff --git a/lib/Accordion/Accordion.js b/lib/Accordion/Accordion.js index 9da575023..8e4b4183a 100644 --- a/lib/Accordion/Accordion.js +++ b/lib/Accordion/Accordion.js @@ -96,7 +96,7 @@ const Accordion = (props) => { }).current; const contentId = useRef(contentIdProp || uniqueId('accordion')).current; const trackingId = useRef(id || uniqueId('acc')).current; - const labelId = useRef(`accordion-toggle-button-${trackingId}`).current; + const labelId = useRef(headerProps?.labelId || `accordion-toggle-button-${trackingId}`).current; const getRef = useRef(() => toggle.current).current; @@ -146,7 +146,7 @@ const Accordion = (props) => { } }, []); // eslint-disable-line react-hooks/exhaustive-deps - const accordionHeaderProps = Object.assign({}, omitProps(props, ['contentHeight']), { + const accordionHeaderProps = Object.assign({}, omitProps(props, ['contentHeight', 'headerProps']), { contentId, toggleRef: (ref) => { toggle.current = ref; }, open: isOpen, @@ -180,7 +180,7 @@ const Accordion = (props) => { className={getContentClass(isOpen)} ref={setContentRef} id={contentId} - aria-labelledby={labelId} + aria-labelledby={accordionHeaderProps.labelId} style={contentHeight ? { height: contentHeight } : null} data-test-accordion-wrapper > diff --git a/lib/FilterControlGroup/FilterControlGroup.js b/lib/FilterControlGroup/FilterControlGroup.js index a1ffa32f4..ac794f964 100644 --- a/lib/FilterControlGroup/FilterControlGroup.js +++ b/lib/FilterControlGroup/FilterControlGroup.js @@ -11,18 +11,20 @@ const propTypes = { style: PropTypes.object, }; -const FilterControlGroup = props => ( - -); +const FilterControlGroup = props => { + return ( + + ); +} FilterControlGroup.propTypes = propTypes; diff --git a/lib/FilterGroups/FilterGroups.js b/lib/FilterGroups/FilterGroups.js index f23389762..d1ebc2c03 100644 --- a/lib/FilterGroups/FilterGroups.js +++ b/lib/FilterGroups/FilterGroups.js @@ -1,6 +1,7 @@ import kebabCase from 'lodash/kebabCase'; -import React from 'react'; +import React, { isValidElement } from 'react'; import PropTypes from 'prop-types'; +import { useIntl } from 'react-intl'; import FilterControlGroup from '../FilterControlGroup'; import css from './FilterGroups.css'; @@ -18,6 +19,14 @@ export const FILTER_SEPARATOR = ','; // a constant used to split filter groups export const FILTER_GROUP_SEPARATOR = '.'; +const getStringFromMessage = (label, formatMessage) => { + if (isValidElement(label) && label?.props?.id) { + return formatMessage({ id: label.props.id }) + } + return label; +} + + // private const FilterCheckbox = (props) => { const { groupName, name, displayName, checked, onChangeFilter: ocf, disabled } = props; @@ -49,24 +58,27 @@ FilterCheckbox.propTypes = { // private const FilterGroup = (props) => { const { label, groupName, names, filters, onChangeFilter: ocf, disabled } = props; - + const { formatMessage } = useIntl(); + const stringLabel = getStringFromMessage(label, formatMessage); return ( - - {names.map((name, index) => ( - ))} - +
+ + {names.map((name, index) => ( + ))} + +
); }; @@ -74,7 +86,7 @@ FilterGroup.propTypes = { disabled: PropTypes.bool, filters: PropTypes.object.isRequired, groupName: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired, names: PropTypes.arrayOf(PropTypes.object).isRequired, onChangeFilter: PropTypes.func.isRequired, }; @@ -260,6 +272,7 @@ export function handleClearAllFilters() { const FilterGroups = (props) => { const { config, filters, onChangeFilter: ocf, onClearFilter } = props; const disableNames = props.disableNames || {}; + const { formatMessage } = useIntl(); const filterGroupNames = (group) => { const names = []; @@ -283,7 +296,8 @@ const FilterGroups = (props) => { {config.map((group, index) => (