${when(
- this.selected && !this.disabled && this.role === 'option',
+ !this.multiple &&
+ this.selected &&
+ !this.disabled &&
+ this.role === 'option',
() => {
return html`
@@ -537,6 +547,7 @@ export const WithGroups: StoryObj = {
: arguments_.placement}
?disabled=${arguments_.disabled}
?loading=${arguments_.loading}
+ ?multiple=${arguments_.multiple}
?open=${arguments_.open}
?required=${arguments_.required}
.value=${arguments_.value}
@@ -657,6 +668,7 @@ export const WithIcons: StoryObj = {
: arguments_.placement}
?disabled=${arguments_.disabled}
?loading=${arguments_.loading}
+ ?multiple=${arguments_.multiple}
?open=${arguments_.open}
?required=${arguments_.required}
.value=${arguments_.value}
diff --git a/src/select.test.accessibility.multiple.ts b/src/select.test.accessibility.multiple.ts
new file mode 100644
index 000000000..53a9f5e56
--- /dev/null
+++ b/src/select.test.accessibility.multiple.ts
@@ -0,0 +1,102 @@
+import { html } from 'lit';
+import { expect, test } from './playwright/test.js';
+
+test('is accessible', { tag: '@accessibility' }, async ({ mount, page }) => {
+ await mount(
+ () =>
+ html`
+
+
+
+
+
+
+ ☀
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ );
+
+ await expect(page).toBeAccessible('glide-core-select', [
+ // Menu has a similar test and it passes. The difference is that Select changes
+ // the role of Options to "listbox" and the role of Option(s) to "option". This
+ // test passes without whitelisting violations if you remove the sub-Menu.
+ //
+ // My suspicion, which I had while building Select, is that a "listbox" with a
+ // nested "menu" isn't a strictly valid pattern. But what to do if a design calls
+ // for a Menu-like thing that's a form control and has a sub-Menu?
+ 'aria-allowed-attr',
+ 'nested-interactive',
+ ]);
+
+ await mount(
+ () =>
+ html`
+
+
+ `,
+ );
+
+ await expect(page).toBeAccessible('glide-core-select', ['aria-allowed-attr']);
+});
+
+test(
+ 'sets `aria-multiselectable` on its target when multiselect initially',
+ { tag: '@accessibility' },
+ async ({ mount, page }) => {
+ await mount(
+ () => html`
+
+
+
+
+
+
+
+ `,
+ );
+
+ const target = page.getByRole('button');
+
+ await expect(target).toHaveAttribute('aria-multiselectable', 'true');
+ },
+);
+
+test(
+ 'sets `aria-multiselectable` on its target when made multiselect programmatically',
+ { tag: '@accessibility' },
+ async ({ mount, page, setProperty }) => {
+ await mount(
+ () => html`
+
+
+
+
+
+
+
+ `,
+ );
+
+ const host = page.locator('glide-core-select');
+ const target = page.getByRole('button');
+
+ await setProperty(host, 'multiple', true);
+
+ await expect(target).toHaveAttribute('aria-multiselectable', 'true');
+ },
+);
diff --git a/src/select.test.accessibility.single.ts b/src/select.test.accessibility.single.ts
new file mode 100644
index 000000000..21382d159
--- /dev/null
+++ b/src/select.test.accessibility.single.ts
@@ -0,0 +1,102 @@
+import { html } from 'lit';
+import { expect, test } from './playwright/test.js';
+
+test('is accessible', { tag: '@accessibility' }, async ({ mount, page }) => {
+ await mount(
+ () =>
+ html`
+
+
+
+
+
+
+ ☀
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ );
+
+ await expect(page).toBeAccessible('glide-core-select', [
+ // Menu has a similar test and it passes. The difference is that Select changes
+ // the role of Options to "listbox" and the role of Option(s) to "option". This
+ // test passes without whitelisting violations if you remove the sub-Menu.
+ //
+ // My suspicion, which I had while building Select, is that a "listbox" with a
+ // nested "menu" isn't a strictly valid pattern. But what to do if a design calls
+ // for a Menu-like thing that's a form control and has a sub-Menu?
+ 'aria-allowed-attr',
+ 'nested-interactive',
+ ]);
+
+ await mount(
+ () =>
+ html`
+
+
+ `,
+ );
+
+ await expect(page).toBeAccessible('glide-core-select', ['aria-allowed-attr']);
+});
+
+test(
+ 'sets `aria-multiselectable` on its target when single-select initially',
+ { tag: '@accessibility' },
+ async ({ mount, page }) => {
+ await mount(
+ () => html`
+
+
+
+
+
+
+
+ `,
+ );
+
+ const target = page.getByRole('button');
+
+ await expect(target).toHaveAttribute('aria-multiselectable', 'false');
+ },
+);
+
+test(
+ 'set `aria-multiselectable` on its target when made single-select programmatically',
+ { tag: '@accessibility' },
+ async ({ mount, page, setProperty }) => {
+ await mount(
+ () => html`
+
+
+
+
+
+
+
+ `,
+ );
+
+ const host = page.locator('glide-core-select');
+ const target = page.getByRole('button');
+
+ await setProperty(host, 'multiple', false);
+
+ await expect(target).toHaveAttribute('aria-multiselectable', 'false');
+ },
+);
diff --git a/src/select.test.accessibility.ts b/src/select.test.accessibility.ts
index f36f6099b..dc72775ce 100644
--- a/src/select.test.accessibility.ts
+++ b/src/select.test.accessibility.ts
@@ -1,62 +1,8 @@
-import { html } from 'lit';
import { expect, test } from './playwright/test.js';
import type Select from './select.js';
import type Menu from './menu.js';
import Option from './option.js';
-test('is accessible', { tag: '@accessibility' }, async ({ mount, page }) => {
- await mount(
- () =>
- html`
-
-
-
-
-
-
- ☀
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `,
- );
-
- await expect(page).toBeAccessible('glide-core-select', [
- // Menu has a similar test and it passes. The difference is that Select changes
- // the role of Options to "listbox" and the role of Option(s) to "option". This
- // test passes without whitelisting violations if you remove the sub-Menu.
- //
- // My suspicion, which I had while building Select, is that a "listbox" with a
- // nested "menu" isn't a strictly valid pattern. But what to do if a design calls
- // for a Menu-like thing that's a form control and has a sub-Menu?
- 'aria-allowed-attr',
- 'nested-interactive',
- ]);
-
- await mount(
- () =>
- html`
-
-
- `,
- );
-
- await expect(page).toBeAccessible('glide-core-select');
-});
-
test('loading', { tag: '@accessibility' }, async ({ page }) => {
await page.goto('?id=select--select');
diff --git a/src/select.test.forms.ts b/src/select.test.forms.ts
index d14f37429..13fc5b7ab 100644
--- a/src/select.test.forms.ts
+++ b/src/select.test.forms.ts
@@ -472,6 +472,43 @@ test(
},
);
+test(
+ 'sets `aria-invalid` on its target when invalid and its target changes',
+ { tag: '@forms' },
+ async ({ mount, page }) => {
+ await mount(
+ () => html`
+
+ `,
+ );
+
+ let target = page.getByRole('button');
+
+ await target.press('Enter');
+
+ await target.evaluate((element) => {
+ const target = document.createElement('button');
+
+ target.textContent = 'Target';
+ target.slot = 'target';
+
+ element.replaceWith(target);
+ });
+
+ target = page.getByRole('button');
+ await expect(target).toHaveJSProperty('ariaInvalid', 'true');
+ },
+);
+
test(
'is invalid when required and its selected option is disabled programmatically',
{ tag: '@forms' },
@@ -580,7 +617,7 @@ test(
test(
'supports `setValidity()`',
{ tag: '@forms' },
- async ({ callMethod, mount, page }) => {
+ async ({ callMethod, mount, page, setProperty }) => {
await mount(
() => html`