Skip to content
This repository has been archived by the owner on May 24, 2024. It is now read-only.

[terra-form-select] Fixed focus issue in MultiSelect #4089

Merged
merged 13 commits into from
Apr 29, 2024
2 changes: 1 addition & 1 deletion packages/terra-alert/tests/wdio/alert-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are the additional tab key is required to bring focus to alert content..?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@supreethmr Earlier pressing tab key was not focusing inside the input field, that is the deselect option. Now since there are two options in the list, to get out side of the input box we need to press tab two more times


Terra.validates.element('alert focused');
});
Expand Down
3 changes: 3 additions & 0 deletions packages/terra-core-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* Added
* Added test example for `terra-form-select`.

## 1.73.0 - (April 25, 2024)

* Changed
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div className={cx('content-wrapper')}>
<Select
id="multiple"
onChange={this.handleChange}
placeholder="Select a color"
value={this.state.value}
variant="multiple"
disabled
>
<Select.Option value="blue" display="Blue" />
<Select.Option value="green" display="Green" />
<Select.Option value="purple" display="Purple" />
<Select.Option value="red" display="Red" />
<Select.Option value="violet" display="Violet" />
</Select>
</div>
);
}
}

export default ControlledMultipleDisabled;
6 changes: 6 additions & 0 deletions packages/terra-form-select/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 40 additions & 2 deletions packages/terra-form-select/src/MultiSelect.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

/**
Expand Down Expand Up @@ -185,14 +196,40 @@ 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
*/
display() {
const selectValue = SelectUtil.value(this.props, this.state);

return selectValue.map(tag => (
<Tag value={tag} key={tag} onDeselect={this.handleDeselect}>
<Tag
value={tag}
key={tag}
onDeselect={this.handleDeselect}
disabled={this.props.disabled}
isInputFocused={this.state.isInputFocused}
inputRef={this.inputRef}
>
{SelectUtil.valueDisplay(this.props, tag)}
</Tag>
));
Expand All @@ -218,6 +255,7 @@ class MultiSelect extends React.Component {
required={required}
totalOptions={SelectUtil.getTotalNumberOfOptions(children)}
inputId={inputId}
getInputRef={this.handleInputRef}
>
{children}
</Frame>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' , '<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#c5c5c6" d="M28.2 24L42.9 9.1 40.8 7l-1.7-1.6-.4-.5L24 19.7 9.4 4.9 7.2 7 5.6 8.6l-.5.5L19.8 24 5.1 38.9 7.2 41l1.7 1.6.5.5L24 28.3l14.7 14.8.4-.5 1.7-1.6 2.1-2.1L28.2 24z"/></svg>');
}
Expand Down
6 changes: 6 additions & 0 deletions packages/terra-form-select/src/multiple/Frame.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path d="M4.2 48L0 43.8 43.8 0 48 4.2zM43.8 48L0 4.2 4.2 0 48 43.8z"/></svg>');
}
Expand Down
69 changes: 64 additions & 5 deletions packages/terra-form-select/src/shared/_Tag.jsx
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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 (
<li className={cx('tag', theme.className)}>
<li className={cx('tag', theme.className)} ref={tagRef}>
<span className={cx('display')}>
{children}
</span>
<span className={cx('deselect')} onClick={() => { onDeselect(value); }} role="presentation">
<span
id={`terra-tag-deselect-${value}`}
onKeyDown={handleKeyPress}
className={cx('deselect')}
onClick={() => { if (!disabled) onDeselect(value); }}
tabIndex={!disabled ? 0 : -1}
role="button"
{...attributes}
>
<span className={cx('icon')} />
</span>
</li>
Expand All @@ -38,4 +97,4 @@ const Tag = ({ children, onDeselect, value }) => {

Tag.propTypes = propTypes;

export default Tag;
export default injectIntl(Tag);
6 changes: 6 additions & 0 deletions packages/terra-form-select/src/shared/_Tag.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions packages/terra-form-select/tests/jest/Tag.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import Tag from '../../src/shared/_Tag';

describe('Tag', () => {
it('should render a default Tag', () => {
const wrapper = enzyme.shallow(<Tag value="value" onDeselect={() => {}}>Content</Tag>);
const wrapper = enzymeIntl.shallowWithIntl(
<Tag value="value" onDeselect={() => {}}>Content</Tag>,
);
expect(wrapper).toMatchSnapshot();
});

it('correctly applies the theme context className', () => {
const wrapper = enzyme.mount(
const wrapper = enzymeIntl.mountWithIntl(
<ThemeContextProvider theme={{ className: 'orion-fusion-theme' }}>
<Tag value="value" onDeselect={() => {}}>
Content
Expand Down
Loading
Loading