diff --git a/docs/assets/plugins/theme-picker/theme-picker.js b/docs/assets/plugins/theme-picker/theme-picker.js
index 9b65f50d..627aa2e3 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 97dad795..9165a350 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 b075245a..194d721d 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 d0ba4c34..c0171f7f 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 a3fc00b4..1884c143 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 e3363c94..9b28ceb3 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 bd8c2974..8450a5d4 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.