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

Commit

Permalink
[terra-form-select] Fixed focus issue in MultiSelect (#4089)
Browse files Browse the repository at this point in the history
Co-authored-by: Avijit Das <[email protected]>
Co-authored-by: Supreeth <[email protected]>
  • Loading branch information
3 people authored Apr 29, 2024
1 parent ab02fbc commit 5d2f4a7
Show file tree
Hide file tree
Showing 22 changed files with 329 additions and 53 deletions.
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']);

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

0 comments on commit 5d2f4a7

Please sign in to comment.