Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(combobox): remove extra or unnecessary ARIA and refactor interactive elements #7659

10 changes: 9 additions & 1 deletion packages/components/src/components/list-box/_list-box.scss
Original file line number Diff line number Diff line change
Expand Up @@ -393,9 +393,15 @@ $list-box-menu-width: rem(300px);

// Menu status inside of a `list-box__field`
.#{$prefix}--list-box__menu-icon {
@include button-reset($width: false);

position: absolute;
top: 0;
right: $carbon--spacing-05;

// TODO: convert to spacing tokens
right: 8px;
padding: 0 8px;

display: flex;
align-items: center;
height: 100%;
Expand All @@ -421,6 +427,8 @@ $list-box-menu-width: rem(300px);

// Selection indicator for a `list-box__field`
.#{$prefix}--list-box__selection {
@include button-reset($width: false);

position: absolute;
top: 50%;
/* to preserve .5rem space between icons according to spec top/transform used to center the combobox clear selection icon in IE11 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,6 @@
.#{$prefix}--text-input {
background-clip: padding-box;
border: rem(2px) solid transparent;
outline: none;
}

.#{$prefix}--multi-select--filterable--input-focused {
@include focus-outline('outline');
}

.#{$prefix}--multi-select--filterable.#{$prefix}--multi-select--selected
Expand Down
234 changes: 133 additions & 101 deletions packages/react/src/components/ComboBox/ComboBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ListBox, { PropTypes as ListBoxPropTypes } from '../ListBox';
import { match, keys } from '../../internal/keyboard';
import setupGetInstanceId from '../../tools/setupGetInstanceId';
import { mapDownshiftProps } from '../../tools/createPropAdapter';
import mergeRefs from '../../tools/mergeRefs';

const { prefix } = settings;

Expand Down Expand Up @@ -228,13 +229,11 @@ export default class ComboBox extends React.Component {
constructor(props) {
super(props);

this.textInput = React.createRef();

this.comboBoxInstanceId = getInstanceId();

this.state = {
inputValue: getInputValue(props, {}),
};
this.textInput = React.createRef();
}

filterItems = (items, itemToString, inputValue) =>
Expand Down Expand Up @@ -303,6 +302,10 @@ export default class ComboBox extends React.Component {
event.preventDownshiftDefault = true;
event.persist();
}

if (this.textInput.current) {
this.textInput.current.focus();
}
};

render() {
Expand Down Expand Up @@ -368,115 +371,144 @@ export default class ComboBox extends React.Component {
getInputProps,
getItemProps,
getLabelProps,
getRootProps,
getMenuProps,
isOpen,
inputValue,
selectedItem,
highlightedIndex,
clearSelection,
toggleMenu,
getMenuProps,
}) => (
<div className={wrapperClasses}>
{titleText && (
<label className={titleClasses} {...getLabelProps()}>
{titleText}
</label>
)}
<ListBox
className={className}
disabled={disabled}
invalid={invalid}
aria-label={ariaLabel}
invalidText={invalidText}
isOpen={isOpen}
light={light}
size={size}>
<ListBox.Field
{...getToggleButtonProps({
disabled,
onClick: this.onToggleClick(isOpen),
})}>
<input
disabled={disabled}
className={inputClasses}
type="text"
tabIndex="0"
aria-autocomplete="list"
ref={this.textInput}
{...rest}
{...getInputProps({
disabled,
placeholder,
onKeyDown: (event) => {
if (match(event, keys.Space)) {
event.stopPropagation();
}

if (match(event, keys.Enter)) {
toggleMenu();
}
},
})}
/>
{invalid && (
<WarningFilled16
className={`${prefix}--list-box__invalid-icon`}
}) => {
const rootProps = getRootProps(
{},
{
suppressRefError: true,
}
);
const labelProps = getLabelProps();
const buttonProps = getToggleButtonProps({
disabled,
onClick: this.onToggleClick(isOpen),
// When we moved the "root node" of Downshift to the <input> for
// WCAG 2.1 compliance, we unfortunately hit this branch for the
// "mouseup" event that downshift listens to:
// https://github.com/downshift-js/downshift/blob/v5.2.1/src/downshift.js#L1051-L1065
//
// As a result, it will reset the state of the component and so we
// stop the event from propagating to prevent this. This allows the
// toggleMenu behavior for the toggleButton to correctly open and
// close the menu.
onMouseUp(event) {
event.stopPropagation();
},
});
const inputProps = getInputProps({
disabled,
placeholder,
onClick() {
toggleMenu();
},
onKeyDown: (event) => {
if (match(event, keys.Space)) {
event.stopPropagation();
}

if (match(event, keys.Enter)) {
toggleMenu();
}
},
});

return (
<div className={wrapperClasses}>
{titleText && (
<label className={titleClasses} {...labelProps}>
{titleText}
</label>
)}
<ListBox
className={className}
disabled={disabled}
invalid={invalid}
invalidText={invalidText}
isOpen={isOpen}
light={light}
size={size}>
<div className={`${prefix}--list-box__field`}>
<input
aria-describedby={comboBoxHelperId}
className={inputClasses}
{...rest}
{...rootProps}
{...inputProps}
ref={mergeRefs(this.textInput, rootProps.ref)}
/>
)}
{inputValue && (
<ListBox.Selection
clearSelection={clearSelection}
{invalid && (
<WarningFilled16
className={`${prefix}--list-box__invalid-icon`}
/>
)}
{inputValue && (
<ListBox.Selection
clearSelection={clearSelection}
disabled={disabled}
onClearSelection={this.handleSelectionClear}
tabIndex="-1"
translateWithId={translateWithId}
/>
)}
<ListBox.MenuIcon
{...buttonProps}
isOpen={isOpen}
tabIndex="-1"
translateWithId={translateWithId}
disabled={disabled}
onClearSelection={this.handleSelectionClear}
/>
</div>
{isOpen && (
<ListBox.Menu {...getMenuProps({ 'aria-label': ariaLabel })}>
{this.filterItems(items, itemToString, inputValue).map(
(item, index) => {
const itemProps = getItemProps({ item, index });
return (
<ListBox.MenuItem
key={itemProps.id}
isActive={selectedItem === item}
tabIndex="-1"
isHighlighted={
highlightedIndex === index ||
(selectedItem && selectedItem.id === item.id) ||
false
}
title={
itemToElement ? item.text : itemToString(item)
}
{...itemProps}>
{itemToElement ? (
<ItemToElement key={itemProps.id} {...item} />
) : (
itemToString(item)
)}
{selectedItem === item && (
<Checkmark16
className={`${prefix}--list-box__menu-item__selected-icon`}
/>
)}
</ListBox.MenuItem>
);
}
)}
</ListBox.Menu>
)}
<ListBox.MenuIcon
isOpen={isOpen}
translateWithId={translateWithId}
/>
</ListBox.Field>
{isOpen && (
<ListBox.Menu {...getMenuProps({ 'aria-label': ariaLabel })}>
{this.filterItems(items, itemToString, inputValue).map(
(item, index) => {
const itemProps = getItemProps({ item, index });
return (
<ListBox.MenuItem
key={itemProps.id}
isActive={selectedItem === item}
tabIndex="-1"
isHighlighted={
highlightedIndex === index ||
(selectedItem && selectedItem.id === item.id) ||
false
}
title={itemToElement ? item.text : itemToString(item)}
{...itemProps}>
{itemToElement ? (
<ItemToElement key={itemProps.id} {...item} />
) : (
itemToString(item)
)}
{selectedItem === item && (
<Checkmark16
className={`${prefix}--list-box__menu-item__selected-icon`}
/>
)}
</ListBox.MenuItem>
);
}
)}
</ListBox.Menu>
</ListBox>
{helperText && !invalid && (
<div id={comboBoxHelperId} className={helperClasses}>
{helperText}
</div>
)}
</ListBox>
{helperText && !invalid && (
<div id={comboBoxHelperId} className={helperClasses}>
{helperText}
</div>
)}
</div>
)}
</div>
);
}}
</Downshift>
);
}
Expand Down
18 changes: 10 additions & 8 deletions packages/react/src/components/ListBox/ListBoxMenuIcon.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,27 @@ export const translationIds = {
};

const defaultTranslations = {
[translationIds['close.menu']]: 'Close menu',
[translationIds['open.menu']]: 'Open menu',
[translationIds['close.menu']]: 'Close',
[translationIds['open.menu']]: 'Open',
};

/**
* `ListBoxMenuIcon` is used to orient the icon up or down depending on the
* state of the menu for a given `ListBox`
*/
const ListBoxMenuIcon = ({ isOpen, translateWithId: t }) => {
const ListBoxMenuIcon = ({ isOpen, translateWithId: t, ...rest }) => {
const className = cx(`${prefix}--list-box__menu-icon`, {
[`${prefix}--list-box__menu-icon--open`]: isOpen,
});
const description = isOpen ? t('close.menu') : t('open.menu');
return (
<div className={className}>
<ChevronDown16 name="chevron--down" aria-label={description}>
<title>{description}</title>
</ChevronDown16>
</div>
<button
{...rest}
aria-label={description}
className={className}
type="button">
<ChevronDown16 />
</button>
);
};

Expand Down
Loading