diff --git a/packages/terra-alert/tests/wdio/alert-spec.js b/packages/terra-alert/tests/wdio/alert-spec.js index fa580ef67be..c206f04c713 100644 --- a/packages/terra-alert/tests/wdio/alert-spec.js +++ b/packages/terra-alert/tests/wdio/alert-spec.js @@ -65,7 +65,7 @@ Terra.describeViewports('Alert', ['tiny', 'large'], () => { it('alert content is focused when rendered with an action element', () => { browser.url('/raw/tests/cerner-terra-core-docs/alert/custom-prop-alert'); - browser.keys(['Tab', 'Tab', 'Tab', 'Tab', 'Enter']); + browser.keys(['Tab', 'Tab', 'Tab', 'Tab', 'Tab', 'Tab', 'Enter']); Terra.validates.element('alert focused'); }); diff --git a/packages/terra-core-docs/CHANGELOG.md b/packages/terra-core-docs/CHANGELOG.md index a64a620c0ab..3bfba3d1691 100644 --- a/packages/terra-core-docs/CHANGELOG.md +++ b/packages/terra-core-docs/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +* Added + * Added test example for `terra-form-select`. + ## 1.73.0 - (April 25, 2024) * Changed diff --git a/packages/terra-core-docs/src/terra-dev-site/test/form-select/ControlMultipleDisabled.test.jsx b/packages/terra-core-docs/src/terra-dev-site/test/form-select/ControlMultipleDisabled.test.jsx new file mode 100644 index 00000000000..b31ad29554e --- /dev/null +++ b/packages/terra-core-docs/src/terra-dev-site/test/form-select/ControlMultipleDisabled.test.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import classNames from 'classnames/bind'; +import Select from 'terra-form-select'; +import styles from './common/Select.test.module.scss'; + +const cx = classNames.bind(styles); + +class ControlledMultipleDisabled extends React.Component { + constructor() { + super(); + + this.state = { value: ['blue', 'red'] }; + this.handleChange = this.handleChange.bind(this); + } + + handleChange(value) { + this.setState({ value }); + } + + render() { + return ( +
+ +
+ ); + } +} + +export default ControlledMultipleDisabled; diff --git a/packages/terra-form-select/CHANGELOG.md b/packages/terra-form-select/CHANGELOG.md index aae2ea33d22..46db636699f 100644 --- a/packages/terra-form-select/CHANGELOG.md +++ b/packages/terra-form-select/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +* Added + * Added visual focus dashed border for `terra-form-select` tags. + +* Fixed + * Fixed accessibility issue in `MultiSelect` component. + ## 6.61.0 - (April 4, 2024) * Fixed diff --git a/packages/terra-form-select/src/MultiSelect.jsx b/packages/terra-form-select/src/MultiSelect.jsx index ede1fe3aea2..cb7c0ca2a66 100644 --- a/packages/terra-form-select/src/MultiSelect.jsx +++ b/packages/terra-form-select/src/MultiSelect.jsx @@ -137,12 +137,23 @@ class MultiSelect extends React.Component { this.state = { value: SelectUtil.defaultValue({ defaultValue, value, multiple: true }), + isInputFocused: false, }; - + this.inputRef = null; this.display = this.display.bind(this); this.handleChange = this.handleChange.bind(this); this.handleDeselect = this.handleDeselect.bind(this); this.handleSelect = this.handleSelect.bind(this); + this.handleFocus = this.handleFocus.bind(this); + this.handleBlur = this.handleBlur.bind(this); + this.handleInputRef = this.handleInputRef.bind(this); + } + + componentWillUnmount() { + if (this.inputRef) { + this.inputRef.removeEventListener('focus', this.handleFocus); + this.inputRef.removeEventListener('blur', this.handleBlur); + } } /** @@ -185,6 +196,25 @@ class MultiSelect extends React.Component { } } + handleFocus() { this.setState({ isInputFocused: true }); } + + handleBlur() { this.setState({ isInputFocused: false }); } + + /** + * Receives the reference to the input element from the Frame component. + * Attaches event listeners to handle focus and blur events, updating the state accordingly. + * @param {HTMLElement} ref - Reference to the input element. + */ + handleInputRef(ref) { + // Receive the input reference from the Frame + this.inputRef = ref; + + if (this.inputRef) { + this.inputRef.addEventListener('focus', this.handleFocus); + this.inputRef.addEventListener('blur', this.handleBlur); + } + } + /** * Returns the appropriate variant display */ @@ -192,7 +222,14 @@ class MultiSelect extends React.Component { const selectValue = SelectUtil.value(this.props, this.state); return selectValue.map(tag => ( - + {SelectUtil.valueDisplay(this.props, tag)} )); @@ -218,6 +255,7 @@ class MultiSelect extends React.Component { required={required} totalOptions={SelectUtil.getTotalNumberOfOptions(children)} inputId={inputId} + getInputRef={this.handleInputRef} > {children} diff --git a/packages/terra-form-select/src/clinical-lowlight-theme/Tag.module.scss b/packages/terra-form-select/src/clinical-lowlight-theme/Tag.module.scss index 0579f6ca9cd..51a18f6edbf 100644 --- a/packages/terra-form-select/src/clinical-lowlight-theme/Tag.module.scss +++ b/packages/terra-form-select/src/clinical-lowlight-theme/Tag.module.scss @@ -20,6 +20,7 @@ --terra-form-select-tag-deselect-hover-border-bottom: 1px solid #181b1d; --terra-form-select-tag-icon-height: 0.7142857142857143rem; --terra-form-select-tag-icon-width: 0.7142857142857143rem; + --terra-form-select-tag-focus-outline: 2px dashed #b2b5b6; @include terra-inline-svg-var('--terra-form-select-tag-icon-background' , ''); } diff --git a/packages/terra-form-select/src/multiple/Frame.jsx b/packages/terra-form-select/src/multiple/Frame.jsx index f1a0c7283f3..88ab29e8efe 100644 --- a/packages/terra-form-select/src/multiple/Frame.jsx +++ b/packages/terra-form-select/src/multiple/Frame.jsx @@ -121,6 +121,10 @@ const propTypes = { * The select value. */ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]), + /** + * Returns the input ref to the Parent component. + */ + getInputRef: PropTypes.func, }; const defaultProps = { @@ -202,6 +206,8 @@ class Frame extends React.Component { // eslint-disable-next-line global-require require('wicg-inert/dist/inert'); } + + this.props.getInputRef(this.input); } componentDidUpdate(previousProps, previousState) { diff --git a/packages/terra-form-select/src/orion-fusion-theme/Tag.module.scss b/packages/terra-form-select/src/orion-fusion-theme/Tag.module.scss index 03920134922..0dcefc319ae 100644 --- a/packages/terra-form-select/src/orion-fusion-theme/Tag.module.scss +++ b/packages/terra-form-select/src/orion-fusion-theme/Tag.module.scss @@ -20,6 +20,8 @@ --terra-form-select-tag-deselect-hover-border-bottom: 1px solid #dedfe0; --terra-form-select-tag-icon-height: 0.91667rem; --terra-form-select-tag-icon-width: 0.91667rem; + --terra-form-select-tag-focus-box-shadow: rgba(76, 178, 233, 0.5) 0 0 1px 3px inset; + --terra-form-select-tag-focus-outline: none; @include terra-inline-svg-var('--terra-form-select-tag-icon-background', ''); } diff --git a/packages/terra-form-select/src/shared/_Tag.jsx b/packages/terra-form-select/src/shared/_Tag.jsx index 773ebf53cd0..fe96d2f635a 100644 --- a/packages/terra-form-select/src/shared/_Tag.jsx +++ b/packages/terra-form-select/src/shared/_Tag.jsx @@ -1,7 +1,8 @@ -import React from 'react'; +import React, { useRef } from 'react'; import PropTypes from 'prop-types'; import classNamesBind from 'classnames/bind'; import ThemeContext from 'terra-theme-context'; +import { injectIntl } from 'react-intl'; import styles from './_Tag.module.scss'; const cx = classNamesBind.bind(styles); @@ -19,17 +20,75 @@ const propTypes = { * The value of the tag. */ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + /** + * Specifies whether the tag is disabled. + */ + disabled: PropTypes.bool, + /** + * @private + * The intl object containing translations. This is retrieved from the context automatically by injectIntl. + */ + intl: PropTypes.shape({ formatMessage: PropTypes.func }).isRequired, + /** + * Ref object for accessing the underlying input element of the tag component. + */ + inputRef: PropTypes.shape({ + focus: PropTypes.instanceOf(Element), + }), + /** + * Specifies whether the input focus is set to true or false. + * Default is false. + */ + isInputFocused: PropTypes.bool, }; /* eslint-disable jsx-a11y/no-static-element-interactions */ -const Tag = ({ children, onDeselect, value }) => { +const Tag = ({ + children, onDeselect, value, disabled, intl, inputRef, isInputFocused, +}) => { const theme = React.useContext(ThemeContext); + const tagRef = useRef(null); + + const handleKeyPress = (event) => { + if ((event.key === 'Enter' || event.key === 'Backspace') && !disabled) { + event.stopPropagation(); + onDeselect(value); + const previousLi = tagRef.current.previousElementSibling; + if (previousLi) { + const deselectElement = previousLi.querySelector(':scope > :nth-child(2)'); + if (deselectElement) { + deselectElement.focus(); + } + } else { + const nextLi = tagRef.current.nextElementSibling; + if (nextLi) { + const nextFocusableElement = nextLi.querySelector(':scope > :nth-child(2)'); + if (nextFocusableElement) { + nextFocusableElement.focus(); + return; + } + } + inputRef.focus(); + } + } + }; + + const attributes = isInputFocused ? { role: 'presentation' } + : { role: 'button', 'aria-label': intl.formatMessage({ id: 'Terra.form.select.deselect' }, { text: children }) }; return ( -
  • +
  • {children} - { onDeselect(value); }} role="presentation"> + { if (!disabled) onDeselect(value); }} + tabIndex={!disabled ? 0 : -1} + role="button" + {...attributes} + >
  • @@ -38,4 +97,4 @@ const Tag = ({ children, onDeselect, value }) => { Tag.propTypes = propTypes; -export default Tag; +export default injectIntl(Tag); diff --git a/packages/terra-form-select/src/shared/_Tag.module.scss b/packages/terra-form-select/src/shared/_Tag.module.scss index 59fc66068fe..31176e0d9fa 100644 --- a/packages/terra-form-select/src/shared/_Tag.module.scss +++ b/packages/terra-form-select/src/shared/_Tag.module.scss @@ -43,6 +43,12 @@ background: var(--terra-form-select-tag-deselect-hover-background, #b9bbbc); border-bottom: var(--terra-form-select-tag-deselect-hover-border-bottom, 0.14286rem solid #8f8f90); } + + &:focus { + outline: var(--terra-form-select-tag-focus-outline, 2px dashed #000); + outline-offset: -2px; + box-shadow: var(--terra-form-select-tag-focus-box-shadow, none); + } } .icon { diff --git a/packages/terra-form-select/tests/jest/Tag.test.jsx b/packages/terra-form-select/tests/jest/Tag.test.jsx index aa3530322d7..931e13986a1 100644 --- a/packages/terra-form-select/tests/jest/Tag.test.jsx +++ b/packages/terra-form-select/tests/jest/Tag.test.jsx @@ -5,12 +5,14 @@ import Tag from '../../src/shared/_Tag'; describe('Tag', () => { it('should render a default Tag', () => { - const wrapper = enzyme.shallow( {}}>Content); + const wrapper = enzymeIntl.shallowWithIntl( + {}}>Content, + ); expect(wrapper).toMatchSnapshot(); }); it('correctly applies the theme context className', () => { - const wrapper = enzyme.mount( + const wrapper = enzymeIntl.mountWithIntl( {}}> Content diff --git a/packages/terra-form-select/tests/jest/__snapshots__/Tag.test.jsx.snap b/packages/terra-form-select/tests/jest/__snapshots__/Tag.test.jsx.snap index 9de44f073d3..84e2dc7f0fa 100644 --- a/packages/terra-form-select/tests/jest/__snapshots__/Tag.test.jsx.snap +++ b/packages/terra-form-select/tests/jest/__snapshots__/Tag.test.jsx.snap @@ -2,55 +2,133 @@ exports[`Tag correctly applies the theme context className 1`] = ` - -
  • - - Content - - - -
  • -
    + className="display" + > + Content + + + + + +
    +
    `; exports[`Tag should render a default Tag 1`] = ` -
  • - - Content - - - - -
  • + Content +
    `; diff --git a/packages/terra-form-select/tests/wdio/select-spec.js b/packages/terra-form-select/tests/wdio/select-spec.js index 2b57bd8b8f2..0f665b41628 100644 --- a/packages/terra-form-select/tests/wdio/select-spec.js +++ b/packages/terra-form-select/tests/wdio/select-spec.js @@ -2376,6 +2376,30 @@ Terra.describeViewports('Select', ['tiny'], () => { it('should display selected option', () => { Terra.validates.element('tag controlled selected option'); }); + + it('should focus deselect on pressing tab key', () => { + $('[data-terra-select]').click(); + $('#terra-select-option-blue').click(); + $('#terra-select-option-red').click(); + $('#root').click(); + browser.keys('Tab'); + expect($('#terra-tag-deselect-blue')).toBeFocused(); + browser.keys('Tab'); + expect($('#terra-tag-deselect-red')).toBeFocused(); + }); + }); + }); + + describe('Tag Variant - controlled multiple disabled', () => { + before(() => { + browser.url('/raw/tests/cerner-terra-core-docs/form-select/control-multiple-disabled'); + }); + it('should not focus deselect on pressing tab key if disabled', () => { + $('#root').click(); + browser.keys('Tab'); + expect($('#terra-tag-deselect-blue')).not.toBeFocused(); + browser.keys('Tab'); + expect($('#terra-tag-deselect-red')).not.toBeFocused(); }); }); diff --git a/packages/terra-form-select/translations/de.json b/packages/terra-form-select/translations/de.json index 04e5549ed9c..80986069007 100644 --- a/packages/terra-form-select/translations/de.json +++ b/packages/terra-form-select/translations/de.json @@ -27,5 +27,6 @@ "Terra.form.select.option": "Optionen", "Terra.form.select.optGroup": "Gruppe {text}", "Terra.form.select.defaultComboboxDisplay": "Auswählen oder eingeben", - "Terra.form.select.resultsText": "Ergebnisse mit '{text}'" + "Terra.form.select.resultsText": "Ergebnisse mit '{text}'", + "Terra.form.select.deselect": "Auswahl aufheben {text}" } diff --git a/packages/terra-form-select/translations/en-GB.json b/packages/terra-form-select/translations/en-GB.json index cae04c8cb5b..feb794c473d 100644 --- a/packages/terra-form-select/translations/en-GB.json +++ b/packages/terra-form-select/translations/en-GB.json @@ -27,5 +27,6 @@ "Terra.form.select.option": "Options", "Terra.form.select.optGroup": "Group {text}", "Terra.form.select.defaultComboboxDisplay": "Select or Enter", - "Terra.form.select.resultsText": "Results that contain \"{text}\"" + "Terra.form.select.resultsText": "Results that contain \"{text}\"", + "Terra.form.select.deselect": "Deselect {text}" } diff --git a/packages/terra-form-select/translations/en-US.json b/packages/terra-form-select/translations/en-US.json index d03c91fc4c9..7f470d2ebed 100644 --- a/packages/terra-form-select/translations/en-US.json +++ b/packages/terra-form-select/translations/en-US.json @@ -26,5 +26,6 @@ "Terra.form.select.menu": "Menu", "Terra.form.select.option": "Options", "Terra.form.select.defaultComboboxDisplay": "Select or Enter", - "Terra.form.select.resultsText": "Results that contain \"{text}\"" + "Terra.form.select.resultsText": "Results that contain \"{text}\"", + "Terra.form.select.deselect": "Deselect {text}" } diff --git a/packages/terra-form-select/translations/en.json b/packages/terra-form-select/translations/en.json index fbf016d3ada..cf5695b0d16 100644 --- a/packages/terra-form-select/translations/en.json +++ b/packages/terra-form-select/translations/en.json @@ -27,5 +27,6 @@ "Terra.form.select.option": "Options", "Terra.form.select.optGroup": "Group {text}", "Terra.form.select.defaultComboboxDisplay": "Select or Enter", - "Terra.form.select.resultsText": "Results that contain \"{text}\"" + "Terra.form.select.resultsText": "Results that contain \"{text}\"", + "Terra.form.select.deselect": "Deselect {text}" } diff --git a/packages/terra-form-select/translations/es.json b/packages/terra-form-select/translations/es.json index 7f2393322fc..b97947557e2 100755 --- a/packages/terra-form-select/translations/es.json +++ b/packages/terra-form-select/translations/es.json @@ -27,5 +27,6 @@ "Terra.form.select.option": "Opciones", "Terra.form.select.optGroup": "Grupo {text}", "Terra.form.select.defaultComboboxDisplay": "Seleccionar o escribir", - "Terra.form.select.resultsText":"Resultados que contienen \"{text}\"" + "Terra.form.select.resultsText": "Resultados que contienen \"{text}\"", + "Terra.form.select.deselect": "Deseleccionar {text}" } diff --git a/packages/terra-form-select/translations/fr.json b/packages/terra-form-select/translations/fr.json index c8266894a39..b169edc31ba 100644 --- a/packages/terra-form-select/translations/fr.json +++ b/packages/terra-form-select/translations/fr.json @@ -27,5 +27,6 @@ "Terra.form.select.option": "Options", "Terra.form.select.optGroup": "Groupe {text}", "Terra.form.select.defaultComboboxDisplay": "Sélectionner ou saisir", - "Terra.form.select.resultsText": "Résultats contenant « {text} »" + "Terra.form.select.resultsText": "Résultats contenant « {text} »", + "Terra.form.select.deselect": "Désélectionner {text}" } diff --git a/packages/terra-form-select/translations/nl.json b/packages/terra-form-select/translations/nl.json index 1b2ad90f0d4..b5fe4a9d999 100644 --- a/packages/terra-form-select/translations/nl.json +++ b/packages/terra-form-select/translations/nl.json @@ -27,5 +27,6 @@ "Terra.form.select.option": "Opties", "Terra.form.select.optGroup": "Groeperen {text}", "Terra.form.select.defaultComboboxDisplay": "Selecteer of druk op Enter", - "Terra.form.select.resultsText": "Resultaten met \"{text}\"" + "Terra.form.select.resultsText": "Resultaten met \"{text}\"", + "Terra.form.select.deselect": "Deselecteren {text}" } diff --git a/packages/terra-form-select/translations/pt.json b/packages/terra-form-select/translations/pt.json index 4824ba3e9e9..78bf00b3c78 100644 --- a/packages/terra-form-select/translations/pt.json +++ b/packages/terra-form-select/translations/pt.json @@ -27,5 +27,6 @@ "Terra.form.select.option": "Opções", "Terra.form.select.optGroup": "Grupo {text}", "Terra.form.select.defaultComboboxDisplay": "Selecione ou pressione Enter", - "Terra.form.select.resultsText": "Resultados que contém \"{text}\"" + "Terra.form.select.resultsText": "Resultados que contém \"{text}\"", + "Terra.form.select.deselect": "Cancelar seleção {text}" } diff --git a/packages/terra-form-select/translations/sv.json b/packages/terra-form-select/translations/sv.json index ac1f7a1724a..0547d9f71fe 100644 --- a/packages/terra-form-select/translations/sv.json +++ b/packages/terra-form-select/translations/sv.json @@ -27,5 +27,6 @@ "Terra.form.select.option": "Alternativ", "Terra.form.select.optGroup": "Grupp {text}", "Terra.form.select.defaultComboboxDisplay": "Välj eller ange", - "Terra.form.select.resultsText": "Resultat som innehåller \"{text}\"" + "Terra.form.select.resultsText": "Resultat som innehåller \"{text}\"", + "Terra.form.select.deselect": "Avmarkera {text}" }