diff --git a/packages/carbon-web-components/src/components/combo-box/combo-box.ts b/packages/carbon-web-components/src/components/combo-box/combo-box.ts index f4995d67e0b..47f1fb30701 100644 --- a/packages/carbon-web-components/src/components/combo-box/combo-box.ts +++ b/packages/carbon-web-components/src/components/combo-box/combo-box.ts @@ -1,7 +1,7 @@ /** * @license * - * Copyright IBM Corp. 2019, 2023 + * Copyright IBM Corp. 2019, 2024 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -17,6 +17,8 @@ import CDSDropdown, { DROPDOWN_KEYBOARD_ACTION } from '../dropdown/dropdown'; import CDSComboBoxItem from './combo-box-item'; import styles from './combo-box.scss'; import { carbonElement as customElement } from '../../globals/decorators/carbon-element'; +import { ifDefined } from 'lit/directives/if-defined'; +import ifNonEmpty from '../../globals/directives/if-non-empty'; export { DROPDOWN_DIRECTION, DROPDOWN_SIZE } from '../dropdown/dropdown'; @@ -200,11 +202,12 @@ class CDSComboBox extends CDSDropdown { ), (item) => { (item as CDSComboBoxItem).selected = false; + item.setAttribute('aria-selected', 'false'); } ); if (itemToSelect) { itemToSelect.selected = true; - this._assistiveStatusText = this.selectedItemAssistiveText; + itemToSelect.setAttribute('aria-selected', 'true'); } this._handleUserInitiatedToggle(false); } @@ -214,8 +217,10 @@ class CDSComboBox extends CDSDropdown { disabled, inputLabel, label, + open, readOnly, value, + _activeDescendant: activeDescendant, _filterInputValue: filterInputValue, _handleInput: handleInput, } = this; @@ -225,17 +230,29 @@ class CDSComboBox extends CDSDropdown { [`${prefix}--text-input--empty`]: !value, }); + let activeDescendantFallback: string | undefined; + if (open && !activeDescendant) { + const constructor = this.constructor as typeof CDSDropdown; + const items = this.querySelectorAll(constructor.selectorItem); + activeDescendantFallback = items[0]?.id; + } + return html` `; @@ -268,7 +285,7 @@ class CDSComboBox extends CDSDropdown { * The `aria-label` attribute for the icon to clear selection. */ @property({ attribute: 'clear-selection-label' }) - clearSelectionLabel = ''; + clearSelectionLabel = 'Clear selection'; /** * The `aria-label` attribute for the `` for filtering. diff --git a/packages/carbon-web-components/src/components/dropdown/dropdown-item.ts b/packages/carbon-web-components/src/components/dropdown/dropdown-item.ts index 736770aabe3..8a29d6d5c81 100644 --- a/packages/carbon-web-components/src/components/dropdown/dropdown-item.ts +++ b/packages/carbon-web-components/src/components/dropdown/dropdown-item.ts @@ -1,7 +1,7 @@ /** * @license * - * Copyright IBM Corp. 2019, 2023 + * Copyright IBM Corp. 2019, 2024 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -58,6 +58,21 @@ class CDSDropdownItem extends LitElement { @property() value = ''; + connectedCallback() { + super.connectedCallback(); + if (!this.hasAttribute('role')) { + this.setAttribute('role', 'option'); + } + if (!this.hasAttribute('id')) { + this.setAttribute( + 'id', + `${prefix}-dropdown-item-${(this.constructor as typeof CDSDropdownItem) + .id++}` + ); + } + this.setAttribute('aria-selected', String(this.selected)); + } + render() { const { selected } = this; return html` @@ -73,6 +88,13 @@ class CDSDropdownItem extends LitElement { `; } + /** + * Store an identifier for use in composing this item's id. + * + * Auto-increments anytime a new dropdown-item appears. + */ + static id = 0; + static styles = styles; } diff --git a/packages/carbon-web-components/src/components/dropdown/dropdown-story.ts b/packages/carbon-web-components/src/components/dropdown/dropdown-story.ts index b02a7dbb52d..e6870404f4a 100644 --- a/packages/carbon-web-components/src/components/dropdown/dropdown-story.ts +++ b/packages/carbon-web-components/src/components/dropdown/dropdown-story.ts @@ -1,7 +1,7 @@ /** * @license * - * Copyright IBM Corp. 2019, 2023 + * Copyright IBM Corp. 2019, 2024 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -143,6 +143,7 @@ export const WithLayer = () => { export const Playground = (args) => { const { + ariaLabel, open, direction, disabled, @@ -162,6 +163,7 @@ export const Playground = (args) => { return html` { Playground.parameters = { knobs: { [`${prefix}-dropdown`]: () => ({ + ariaLabel: textNullable('aria-label (aria-label)', ''), open: boolean('Open (open)', false), direction: select('Direction', directionOptions, null), disabled: boolean('Disabled (disabled)', false), diff --git a/packages/carbon-web-components/src/components/dropdown/dropdown.ts b/packages/carbon-web-components/src/components/dropdown/dropdown.ts index 6f1ebbc4425..a322a4e18ec 100644 --- a/packages/carbon-web-components/src/components/dropdown/dropdown.ts +++ b/packages/carbon-web-components/src/components/dropdown/dropdown.ts @@ -10,7 +10,7 @@ import { classMap } from 'lit/directives/class-map.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { LitElement, html, TemplateResult } from 'lit'; -import { property, query } from 'lit/decorators.js'; +import { property, query, state } from 'lit/decorators.js'; import { prefix } from '../../globals/settings'; import ChevronDown16 from '@carbon/icons/lib/chevron--down/16'; import WarningFilled16 from '@carbon/icons/lib/warning--filled/16'; @@ -71,10 +71,8 @@ class CDSDropdown extends ValidityMixin( */ protected _hasSlug = false; - /** - * The latest status of this dropdown, for screen reader to accounce. - */ - protected _assistiveStatusText?: string; + @state() + protected _activeDescendant?: string; /** * The content of the selected item. @@ -123,16 +121,18 @@ class CDSDropdown extends ValidityMixin( protected _selectionDidChange(itemToSelect?: CDSDropdownItem) { if (itemToSelect) { this.value = itemToSelect.value; + this._activeDescendant = itemToSelect.id; forEach( this.querySelectorAll( (this.constructor as typeof CDSDropdown).selectorItemSelected ), (item) => { (item as CDSDropdownItem).selected = false; + item.setAttribute('aria-selected', 'false'); } ); itemToSelect.selected = true; - this._assistiveStatusText = this.selectedItemAssistiveText; + itemToSelect.setAttribute('aria-selected', 'true'); this._handleUserInitiatedToggle(false); } } @@ -322,23 +322,7 @@ class CDSDropdown extends ValidityMixin( if (!disabled) { if (this.dispatchEvent(new CustomEvent(eventBeforeToggle, init))) { this.open = force; - if (this.open) { - this._assistiveStatusText = this.selectingItemsAssistiveText; - } else { - const { - selectedItemAssistiveText, - label, - _assistiveStatusText: assistiveStatusText, - _selectedItemContent: selectedItemContent, - } = this; - const selectedItemText = - (selectedItemContent && selectedItemContent.textContent) || label; - if ( - selectedItemText && - assistiveStatusText !== selectedItemAssistiveText - ) { - this._assistiveStatusText = selectedItemText; - } + if (!this.open) { forEach( this.querySelectorAll( (this.constructor as typeof CDSDropdown).selectorItemHighlighted @@ -401,11 +385,10 @@ class CDSDropdown extends ValidityMixin( // IE falls back to the old behavior. nextItem.scrollIntoView({ block: 'nearest' }); - const nextItemText = nextItem.textContent; - if (nextItemText) { - this._assistiveStatusText = nextItemText; + const nextItemId = nextItem.id; + if (nextItemId) { + this._activeDescendant = nextItemId; } - this.requestUpdate(); } /* eslint-disable class-methods-use-this */ @@ -453,8 +436,10 @@ class CDSDropdown extends ValidityMixin( return html`