Skip to content

Commit dd2a0bc

Browse files
joshblackandreancardonacarmacleodkodiakhq[bot]
authored
fix(combobox): update to match ARIA 1.2 criteria (#7777)
* fix(combobox): update to match WCAG 2.1 AA criteria *  fix(combobox): updated styling on ComboBox *  fix(combobox): updated styling on ComboBox -pt.2 * fix(comobobox): update aria-selected prop attribute * fix(comobobox): update aria-selected prop attribute * fix(combobox): remove aria-owns prop - fix lint * fix(combobox): remove aria-owns prop - remove aria-labelledby fix lint * fix(combobox): add aria commentary * fix(combobox): aria-labelledby refactor * fix(combobox): pass ariaLabel to getMenuProps * fix(combobox): update ariaLabel * fix(combobox): updated selected ui color token * Update packages/components/src/components/list-box/_list-box.scss Co-authored-by: Carolyn MacLeod <Carolyn_MacLeod@ca.ibm.com> * fix(combobox): update aria-selected & aria-current * fix(combobox): update ariaLabel Co-authored-by: Andrea N. Cardona <andreancardona@gmail.com> Co-authored-by: Carolyn MacLeod <Carolyn_MacLeod@ca.ibm.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 4a3955e commit dd2a0bc

File tree

6 files changed

+409
-117
lines changed

6 files changed

+409
-117
lines changed

packages/components/src/components/list-box/_list-box.scss

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -393,18 +393,21 @@ $list-box-menu-width: rem(300px);
393393

394394
// Menu status inside of a `list-box__field`
395395
.#{$prefix}--list-box__menu-icon {
396+
@include button-reset($width: false);
397+
396398
position: absolute;
397-
top: 0;
398399
right: $carbon--spacing-05;
399400
display: flex;
400401
align-items: center;
401-
height: 100%;
402+
justify-content: center;
403+
width: rem(24px);
404+
height: rem(24px);
405+
outline: none;
402406
cursor: pointer;
403407
transition: transform $duration--fast-01 motion(standard, productive);
404408
}
405409

406410
.#{$prefix}--list-box__menu-icon > svg {
407-
height: 100%;
408411
fill: $icon-01;
409412

410413
// Windows, Firefox HCM Fix
@@ -416,20 +419,24 @@ $list-box-menu-width: rem(300px);
416419
}
417420

418421
.#{$prefix}--list-box__menu-icon--open {
422+
justify-content: center;
423+
width: rem(24px);
419424
transform: rotate(180deg);
420425
}
421426

422427
// Selection indicator for a `list-box__field`
423428
.#{$prefix}--list-box__selection {
429+
@include button-reset($width: false);
430+
424431
position: absolute;
425432
top: 50%;
426433
/* to preserve .5rem space between icons according to spec top/transform used to center the combobox clear selection icon in IE11 */
427-
right: rem(33px);
434+
right: rem(36px);
428435
display: flex;
429436
align-items: center;
430437
justify-content: center;
431-
width: rem(30px);
432-
height: rem(30px);
438+
width: rem(24px);
439+
height: rem(24px);
433440
transform: translateY(-50%);
434441
cursor: pointer;
435442
transition: background-color $duration--fast-01 motion(standard, productive);
@@ -751,8 +758,8 @@ $list-box-menu-width: rem(300px);
751758

752759
.#{$prefix}--list-box__menu-item--active:hover,
753760
.#{$prefix}--list-box__menu-item--active.#{$prefix}--list-box__menu-item--highlighted {
754-
background-color: $hover-selected-ui;
755-
border-bottom-color: $hover-selected-ui;
761+
background-color: $selected-ui;
762+
border-bottom-color: $selected-ui;
756763
}
757764

758765
.#{$prefix}--list-box__menu-item--active

packages/react/src/components/ComboBox/ComboBox-test.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
findListBoxNode,
1212
findMenuNode,
1313
findMenuItemNode,
14-
openMenu,
1514
assertMenuOpen,
1615
assertMenuClosed,
1716
generateItems,
@@ -28,6 +27,9 @@ const downshiftActions = {
2827
};
2928
const clearInput = (wrapper) =>
3029
wrapper.instance().handleOnStateChange({ inputValue: '' }, downshiftActions);
30+
const openMenu = (wrapper) => {
31+
wrapper.find(`[role="combobox"]`).simulate('click');
32+
};
3133

3234
describe('ComboBox', () => {
3335
let mockProps;

packages/react/src/components/ComboBox/ComboBox.js

Lines changed: 154 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ import {
1616
WarningFilled16,
1717
} from '@carbon/icons-react';
1818
import ListBox, { PropTypes as ListBoxPropTypes } from '../ListBox';
19+
import { ListBoxTrigger, ListBoxSelection } from '../ListBox/next';
1920
import { match, keys } from '../../internal/keyboard';
2021
import setupGetInstanceId from '../../tools/setupGetInstanceId';
2122
import { mapDownshiftProps } from '../../tools/createPropAdapter';
23+
import mergeRefs from '../../tools/mergeRefs';
2224

2325
const { prefix } = settings;
2426

@@ -242,13 +244,11 @@ export default class ComboBox extends React.Component {
242244
constructor(props) {
243245
super(props);
244246

245-
this.textInput = React.createRef();
246-
247247
this.comboBoxInstanceId = getInstanceId();
248-
249248
this.state = {
250249
inputValue: getInputValue(props, {}),
251250
};
251+
this.textInput = React.createRef();
252252
}
253253

254254
filterItems = (items, itemToString, inputValue) =>
@@ -317,6 +317,10 @@ export default class ComboBox extends React.Component {
317317
event.preventDownshiftDefault = true;
318318
event.persist();
319319
}
320+
321+
if (this.textInput.current) {
322+
this.textInput.current.focus();
323+
}
320324
};
321325

322326
render() {
@@ -382,126 +386,168 @@ export default class ComboBox extends React.Component {
382386
inputId={id}
383387
selectedItem={selectedItem}>
384388
{({
385-
getToggleButtonProps,
386389
getInputProps,
387390
getItemProps,
388391
getLabelProps,
392+
getMenuProps,
393+
getRootProps,
394+
getToggleButtonProps,
389395
isOpen,
390396
inputValue,
391397
selectedItem,
392398
highlightedIndex,
393399
clearSelection,
394400
toggleMenu,
395-
getMenuProps,
396-
}) => (
397-
<div className={wrapperClasses}>
398-
{titleText && (
399-
<label className={titleClasses} {...getLabelProps()}>
400-
{titleText}
401-
</label>
402-
)}
403-
<ListBox
404-
className={className}
405-
disabled={disabled}
406-
invalid={invalid}
407-
aria-label={ariaLabel}
408-
invalidText={invalidText}
409-
isOpen={isOpen}
410-
light={light}
411-
size={size}
412-
warn={warn}
413-
warnText={warnText}>
414-
<ListBox.Field
415-
{...getToggleButtonProps({
416-
disabled,
417-
onClick: this.onToggleClick(isOpen),
418-
})}>
419-
<input
420-
disabled={disabled}
421-
className={inputClasses}
422-
type="text"
423-
tabIndex="0"
424-
aria-autocomplete="list"
425-
ref={this.textInput}
426-
{...rest}
427-
{...getInputProps({
428-
disabled,
429-
placeholder,
430-
onKeyDown: (event) => {
431-
if (match(event, keys.Space)) {
432-
event.stopPropagation();
433-
}
434-
435-
if (match(event, keys.Enter)) {
436-
toggleMenu();
437-
}
438-
},
439-
})}
440-
/>
441-
{invalid && (
442-
<WarningFilled16
443-
className={`${prefix}--list-box__invalid-icon`}
444-
/>
445-
)}
446-
{showWarning && (
447-
<WarningAltFilled16
448-
className={`${prefix}--list-box__invalid-icon ${prefix}--list-box__invalid-icon--warning`}
401+
}) => {
402+
const rootProps = getRootProps(
403+
{},
404+
{
405+
suppressRefError: true,
406+
}
407+
);
408+
const labelProps = getLabelProps();
409+
const buttonProps = getToggleButtonProps({
410+
disabled,
411+
onClick: this.onToggleClick(isOpen),
412+
// When we moved the "root node" of Downshift to the <input> for
413+
// ARIA 1.2 compliance, we unfortunately hit this branch for the
414+
// "mouseup" event that downshift listens to:
415+
// https://github.com/downshift-js/downshift/blob/v5.2.1/src/downshift.js#L1051-L1065
416+
//
417+
// As a result, it will reset the state of the component and so we
418+
// stop the event from propagating to prevent this. This allows the
419+
// toggleMenu behavior for the toggleButton to correctly open and
420+
// close the menu.
421+
onMouseUp(event) {
422+
event.stopPropagation();
423+
},
424+
});
425+
const inputProps = getInputProps({
426+
// Remove excess aria `aria-labelledby`. HTML <label for> provides this aria information.
427+
'aria-labelledby': null,
428+
disabled,
429+
placeholder,
430+
onClick() {
431+
toggleMenu();
432+
},
433+
onKeyDown: (event) => {
434+
if (match(event, keys.Space)) {
435+
event.stopPropagation();
436+
}
437+
438+
if (match(event, keys.Enter)) {
439+
toggleMenu();
440+
}
441+
},
442+
});
443+
444+
return (
445+
<div className={wrapperClasses}>
446+
{titleText && (
447+
<label className={titleClasses} {...labelProps}>
448+
{titleText}
449+
</label>
450+
)}
451+
<ListBox
452+
className={className}
453+
disabled={disabled}
454+
invalid={invalid}
455+
invalidText={invalidText}
456+
isOpen={isOpen}
457+
light={light}
458+
size={size}
459+
warn={warn}
460+
warnText={warnText}>
461+
<div className={`${prefix}--list-box__field`}>
462+
<input
463+
role="combobox"
464+
disabled={disabled}
465+
className={inputClasses}
466+
type="text"
467+
tabIndex="0"
468+
aria-autocomplete="list"
469+
aria-expanded={rootProps['aria-expanded']}
470+
aria-haspopup="listbox"
471+
aria-controls={inputProps['aria-controls']}
472+
{...inputProps}
473+
{...rest}
474+
ref={mergeRefs(this.textInput, rootProps.ref)}
449475
/>
450-
)}
451-
{inputValue && (
452-
<ListBox.Selection
453-
clearSelection={clearSelection}
476+
{invalid && (
477+
<WarningFilled16
478+
className={`${prefix}--list-box__invalid-icon`}
479+
/>
480+
)}
481+
{showWarning && (
482+
<WarningAltFilled16
483+
className={`${prefix}--list-box__invalid-icon ${prefix}--list-box__invalid-icon--warning`}
484+
/>
485+
)}
486+
{inputValue && (
487+
<ListBoxSelection
488+
clearSelection={clearSelection}
489+
translateWithId={translateWithId}
490+
disabled={disabled}
491+
onClearSelection={this.handleSelectionClear}
492+
/>
493+
)}
494+
<ListBoxTrigger
495+
{...buttonProps}
496+
isOpen={isOpen}
454497
translateWithId={translateWithId}
455-
disabled={disabled}
456-
onClearSelection={this.handleSelectionClear}
457498
/>
458-
)}
459-
<ListBox.MenuIcon
460-
isOpen={isOpen}
461-
translateWithId={translateWithId}
462-
/>
463-
</ListBox.Field>
464-
{isOpen && (
499+
</div>
465500
<ListBox.Menu {...getMenuProps({ 'aria-label': ariaLabel })}>
466-
{this.filterItems(items, itemToString, inputValue).map(
467-
(item, index) => {
468-
const itemProps = getItemProps({ item, index });
469-
return (
470-
<ListBox.MenuItem
471-
key={itemProps.id}
472-
isActive={selectedItem === item}
473-
tabIndex="-1"
474-
isHighlighted={
475-
highlightedIndex === index ||
476-
(selectedItem && selectedItem.id === item.id) ||
477-
false
478-
}
479-
title={itemToElement ? item.text : itemToString(item)}
480-
{...itemProps}>
481-
{itemToElement ? (
482-
<ItemToElement key={itemProps.id} {...item} />
483-
) : (
484-
itemToString(item)
485-
)}
486-
{selectedItem === item && (
487-
<Checkmark16
488-
className={`${prefix}--list-box__menu-item__selected-icon`}
489-
/>
490-
)}
491-
</ListBox.MenuItem>
492-
);
493-
}
494-
)}
501+
{isOpen
502+
? this.filterItems(items, itemToString, inputValue).map(
503+
(item, index) => {
504+
const itemProps = getItemProps({
505+
item,
506+
index,
507+
['aria-current']:
508+
selectedItem === item ? true : null,
509+
['aria-selected']:
510+
highlightedIndex === index ? true : null,
511+
});
512+
return (
513+
<ListBox.MenuItem
514+
key={itemProps.id}
515+
isActive={selectedItem === item}
516+
tabIndex="-1"
517+
isHighlighted={
518+
highlightedIndex === index ||
519+
(selectedItem && selectedItem.id === item.id) ||
520+
false
521+
}
522+
title={
523+
itemToElement ? item.text : itemToString(item)
524+
}
525+
{...itemProps}>
526+
{itemToElement ? (
527+
<ItemToElement key={itemProps.id} {...item} />
528+
) : (
529+
itemToString(item)
530+
)}
531+
{selectedItem === item && (
532+
<Checkmark16
533+
className={`${prefix}--list-box__menu-item__selected-icon`}
534+
/>
535+
)}
536+
</ListBox.MenuItem>
537+
);
538+
}
539+
)
540+
: null}
495541
</ListBox.Menu>
542+
</ListBox>
543+
{helperText && !invalid && !warn && (
544+
<div id={comboBoxHelperId} className={helperClasses}>
545+
{helperText}
546+
</div>
496547
)}
497-
</ListBox>
498-
{helperText && !invalid && !warn && (
499-
<div id={comboBoxHelperId} className={helperClasses}>
500-
{helperText}
501-
</div>
502-
)}
503-
</div>
504-
)}
548+
</div>
549+
);
550+
}}
505551
</Downshift>
506552
);
507553
}

0 commit comments

Comments
 (0)