diff --git a/docs/assets/plugins/theme-picker/theme-picker.js b/docs/assets/plugins/theme-picker/theme-picker.js index 9b65f50da..627aa2e35 100644 --- a/docs/assets/plugins/theme-picker/theme-picker.js +++ b/docs/assets/plugins/theme-picker/theme-picker.js @@ -47,10 +47,10 @@ Toggle \\ - Light - Dark + Light + Dark - Auto + Auto `; document.querySelector('.sidebar-toggle').insertAdjacentElement('afterend', dropdown); diff --git a/docs/components/breadcrumb.md b/docs/components/breadcrumb.md index 97dad7952..9165a3502 100644 --- a/docs/components/breadcrumb.md +++ b/docs/components/breadcrumb.md @@ -203,9 +203,9 @@ Dropdown menus can be placed in a prefix or suffix slot to provide additional op - Web Design - Web Development - Marketing + Web Design + Web Development + Marketing @@ -235,9 +235,11 @@ const App = () => ( - Web Design - Web Development - Marketing + + Web Design + + Web Development + Marketing diff --git a/docs/components/dropdown.md b/docs/components/dropdown.md index b075245a3..194d721d4 100644 --- a/docs/components/dropdown.md +++ b/docs/components/dropdown.md @@ -14,7 +14,7 @@ Dropdowns are designed to work well with [menus](/components/menu) to provide a Dropdown Item 2 Dropdown Item 3 - Checked + Checkbox Disabled @@ -42,7 +42,9 @@ const App = () => ( Dropdown Item 2 Dropdown Item 3 - Checked + + Checkbox + Disabled diff --git a/docs/components/menu-item.md b/docs/components/menu-item.md index d0ba4c34c..c0171f7f2 100644 --- a/docs/components/menu-item.md +++ b/docs/components/menu-item.md @@ -8,7 +8,7 @@ Option 2 Option 3 - Checked + Checkbox Disabled @@ -31,7 +31,9 @@ const App = () => ( Option 2 Option 3 - Checked + + Checkbox + Disabled @@ -48,30 +50,6 @@ const App = () => ( ## Examples -### Checked - -Use the `checked` attribute to draw menu items in a checked state. - -```html preview - - Option 1 - Option 2 - Option 3 - -``` - -```jsx react -import { SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react'; - -const App = () => ( - - Option 1 - Option 2 - Option 3 - -); -``` - ### Disabled Add the `disabled` attribute to disable the menu item so it cannot be selected. @@ -150,6 +128,34 @@ const App = () => ( ); ``` +### Checkbox Menu Items + +Set the `type` attribute to `checkbox` to create a menu item that will toggle on and off when selected. You can use the `checked` attribute to set the initial state. + +Checkbox menu items are visually indistinguishable from regular menu items. Their ability to be toggled is primarily inferred from context, much like you'd find in the menu of a native app. + +```html preview + + Autosave + Check Spelling + Word Wrap + +``` + +```jsx react +import { SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + Autosave + + Check Spelling + + Word Wrap + +); +``` + ### Value & Selection The `value` attribute can be used to assign a hidden value, such as a unique identifier, to a menu item. When an item is selected, the `sl-select` event will be emitted and a reference to the item will be available at `event.detail.item`. You can use this reference to access the selected item's value, its checked state, and more. @@ -159,6 +165,10 @@ The `value` attribute can be used to assign a hidden value, such as a unique ide Option 1 Option 2 Option 3 + + Checkbox 4 + Checkbox 5 + Checkbox 6 ``` diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index a3fc00b44..1884c1431 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -21,6 +21,9 @@ This release includes a complete rewrite of `` to improve accessibili - 🚨 BREAKING: removed the `sl-label-change` event from `` (listen for `slotchange` instead) - 🚨 BREAKING: removed type to select logic from `` (this was added specifically for `` which no longer uses ``) - 🚨 BREAKING: swatches in `` are no longer present by default (but you can set them using the `swatches` attribute now) +- 🚨 BREAKING: improved the accessibility of `` so checked items are announced as such + - Checkbox menu items must now have `type="checkbox"` before applying the `checked` attribute + - Checkbox menu items will now toggle their `checked` state on their own when selected - Added the `` component - Added Traditional Chinese translation [#1086](https://github.com/shoelace-style/shoelace/pull/1086) - Added support for `swatches` to be an attribute of `` so swatches can be defined declaratively (it was previously a property; use a `;` to separate color values) diff --git a/src/components/menu-item/menu-item.ts b/src/components/menu-item/menu-item.ts index e3363c944..9b28ceb34 100644 --- a/src/components/menu-item/menu-item.ts +++ b/src/components/menu-item/menu-item.ts @@ -35,6 +35,9 @@ export default class SlMenuItem extends ShoelaceElement { @query('slot:not([name])') defaultSlot: HTMLSlotElement; @query('.menu-item') menuItem: HTMLElement; + /** The type of menu item to render. To use `checked`, this value must be set to `checkbox`. */ + @property() type: 'normal' | 'checkbox' = 'normal'; + /** Draws the item in a checked state. */ @property({ type: Boolean, reflect: true }) checked = false; @@ -44,10 +47,6 @@ export default class SlMenuItem extends ShoelaceElement { /** Draws the menu item in a disabled state, preventing selection. */ @property({ type: Boolean, reflect: true }) disabled = false; - firstUpdated() { - this.setAttribute('role', 'menuitem'); - } - private handleDefaultSlotChange() { const textLabel = this.getTextLabel(); @@ -66,10 +65,12 @@ export default class SlMenuItem extends ShoelaceElement { @watch('checked') handleCheckedChange() { - // - // TODO - fix a11y bug - // - // this.setAttribute('aria-checked', this.checked ? 'true' : 'false'); + this.setAttribute('aria-checked', this.checked ? 'true' : 'false'); + + if (this.checked && this.type !== 'checkbox') { + this.checked = false; + console.error('The checked attribute can only be used on menu items with type="checkbox"', this); + } } @watch('disabled') @@ -77,6 +78,11 @@ export default class SlMenuItem extends ShoelaceElement { this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); } + @watch('type') + handleTypeChange() { + this.setAttribute('role', this.type === 'checkbox' ? 'menuitemcheckbox' : 'menuitem'); + } + /** Returns a text label based on the contents of the menu item's default slot. */ getTextLabel() { return getTextContent(this.defaultSlot); diff --git a/src/components/menu/menu.ts b/src/components/menu/menu.ts index bd8c29749..8450a5d48 100644 --- a/src/components/menu/menu.ts +++ b/src/components/menu/menu.ts @@ -31,7 +31,7 @@ export default class SlMenu extends ShoelaceElement { private getAllItems(options: { includeDisabled: boolean } = { includeDisabled: true }) { return [...this.defaultSlot.assignedElements({ flatten: true })].filter((el: HTMLElement) => { - if (el.getAttribute('role') !== 'menuitem') { + if (!this.isMenuItem(el)) { return false; } @@ -48,6 +48,10 @@ export default class SlMenu extends ShoelaceElement { const item = target.closest('sl-menu-item'); if (item?.disabled === false) { + if (item.type === 'checkbox') { + item.checked = !item.checked; + } + this.emit('sl-select', { detail: { item } }); } } @@ -102,7 +106,7 @@ export default class SlMenu extends ShoelaceElement { private handleMouseDown(event: MouseEvent) { const target = event.target as HTMLElement; - if (target.getAttribute('role') === 'menuitem') { + if (this.isMenuItem(target)) { this.setCurrentItem(target as SlMenuItem); } } @@ -116,6 +120,13 @@ export default class SlMenu extends ShoelaceElement { } } + private isMenuItem(item: HTMLElement) { + return ( + item.tagName.toLowerCase() === 'sl-menu-item' || + ['menuitem', 'menuitemcheckbox', 'menuitemradio'].includes(item.getAttribute('role') ?? '') + ); + } + /** * @internal Gets the current menu item, which is the menu item that has `tabindex="0"` within the roving tab index. * The menu item may or may not have focus, but for keyboard interaction purposes it's considered the "active" item.