diff --git a/packages/webawesome/docs/docs/components/menu-item.md b/packages/webawesome/docs/docs/components/menu-item.md
deleted file mode 100644
index 889862036..000000000
--- a/packages/webawesome/docs/docs/components/menu-item.md
+++ /dev/null
@@ -1,125 +0,0 @@
----
-title: Menu Item
-description: Menu items provide options for the user to pick from in a menu.
-tags: component
-parent: menu
-icon: menu
----
-
-```html {.example}
-
- Option 1
- Option 2
- Option 3
-
- Checkbox
- Disabled
-
-
- Prefix Icon
-
-
-
- Suffix Icon
-
-
-
-```
-
-## Examples
-
-### Prefix & Suffix
-
-Add content to the start and end of menu items using the `prefix` and `suffix` slots.
-
-```html {.example}
-
-
-
- Home
-
-
-
-
- Messages
- 12
-
-
-
-
-
-
- Settings
-
-
-```
-
-### Disabled
-
-Add the `disabled` attribute to disable the menu item so it cannot be selected.
-
-```html {.example}
-
- Option 1
- Option 2
- Option 3
-
-```
-
-### Loading
-
-Use the `loading` attribute to indicate that a menu item is busy. Like a disabled menu item, clicks will be suppressed until the loading state is removed.
-
-```html {.example}
-
- Option 1
- Option 2
- Option 3
-
-```
-
-### 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 {.example}
-
- 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 `wa-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.
-
-```html {.example}
-
-
-
-```
-
diff --git a/packages/webawesome/docs/docs/components/menu-label.md b/packages/webawesome/docs/docs/components/menu-label.md
deleted file mode 100644
index b4d1de9db..000000000
--- a/packages/webawesome/docs/docs/components/menu-label.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-title: Menu Label
-description: Menu labels are used to describe a group of menu items.
-tags: component
-parent: menu
-icon: menu
----
-
-```html {.example}
-
- Fruits
- Apple
- Banana
- Orange
-
- Vegetables
- Broccoli
- Carrot
- Zucchini
-
-```
diff --git a/packages/webawesome/docs/docs/components/menu.md b/packages/webawesome/docs/docs/components/menu.md
deleted file mode 100644
index e66b68a02..000000000
--- a/packages/webawesome/docs/docs/components/menu.md
+++ /dev/null
@@ -1,77 +0,0 @@
----
-title: Menu
-description: Menus provide a list of options for the user to choose from.
-tags: [actions, apps]
-icon: menu
----
-
-You can use [menu items](/docs/components/menu-item), [menu labels](/docs/components/menu-label), and [dividers](/docs/components/divider) to compose a menu. Menus support keyboard interactions, including type-to-select an option.
-
-```html {.example}
-
- Undo
- Redo
-
- Cut
- Copy
- Paste
- Delete
-
-```
-
-:::info
-Menus are intended for system menus (dropdown menus, select menus, context menus, etc.). They should not be mistaken for navigation menus which serve a different purpose and have a different semantic meaning. If you're building navigation, use `` and `` elements instead.
-:::
-
-## Examples
-
-### In Dropdowns
-
-Menus work really well when used inside [dropdowns](/docs/components/dropdown).
-
-```html {.example}
-
- Edit
-
- Cut
- Copy
- Paste
-
-
-```
-
-### Submenus
-
-To create a submenu, nest an `` in any [menu item](/docs/components/menu-item).
-
-```html {.example}
-
- Undo
- Redo
-
- Cut
- Copy
- Paste
-
-
- Find
-
- Find…
- Find Next
- Find Previous
-
-
-
- Transformations
-
- Make uppercase
- Make lowercase
- Capitalize
-
-
-
-```
-
-:::warning
-As a UX best practice, avoid using more than one level of submenus when possible.
-:::
diff --git a/packages/webawesome/docs/docs/resources/changelog.md b/packages/webawesome/docs/docs/resources/changelog.md
index 77ce2f1f0..637c0071e 100644
--- a/packages/webawesome/docs/docs/resources/changelog.md
+++ b/packages/webawesome/docs/docs/resources/changelog.md
@@ -31,7 +31,7 @@ During the alpha period, things might break! We take breaking changes very serio
- `` => ``
- `` => ``
- 🚨 BREAKING: removed the `size` attribute from ``; please set the size of child elements on the children directly
-- 🚨 BREAKING: Greatly simplified the sizing strategy across components and utilities
+- 🚨 BREAKING: greatly simplified the sizing strategy across components and utilities
- Removed `--wa-size`, `--wa-size-smaller`, `--wa-size-larger`, `--wa-space`, `--wa-space-smaller`, and `--wa-space-larger`
- Added tokens for `--wa-form-control-padding-inline`, `--wa-form-control-padding-block`, and `--wa-form-control-toggle-size`
- Refactored default `--wa-font-size-*` values to use an apparent 1.125 ratio and round rendered values to the nearest whole pixel
@@ -39,6 +39,9 @@ During the alpha period, things might break! We take breaking changes very serio
- Updated components to use relative `em` values for internal padding and margin wherever appropriate
- 🚨 BREAKING: removed the `hint` property and slot from ``; please apply hints directly to `` instead
- 🚨 BREAKING: removed ``; use ` ` instead
+- 🚨 BREAKING: completely reworked `` to be easier to use
+ - Added ``, greatly simplifying the dropdown's markup structure
+ - Removed ``, ``, and ``; use `` and native headings instead
- Added a new free component: `` (#2 of 14 per stretch goals)
- Added a new free component: `` (#3 of 14 per stretch goals)
- Added a `min-block-size` to `` to ensure the divider is visible regardless of container height [issue:675]
diff --git a/packages/webawesome/src/components/dropdown/dropdown.test.ts b/packages/webawesome/src/components/dropdown/dropdown.test.ts
deleted file mode 100644
index a64a7d547..000000000
--- a/packages/webawesome/src/components/dropdown/dropdown.test.ts
+++ /dev/null
@@ -1,405 +0,0 @@
-import { expect, waitUntil } from '@open-wc/testing';
-import { sendKeys, sendMouse } from '@web/test-runner-commands';
-import { html } from 'lit';
-import sinon from 'sinon';
-import { clickOnElement } from '../../internal/test.js';
-import { fixtures } from '../../internal/test/fixture.js';
-import type WaDropdown from './dropdown.js';
-
-describe('', () => {
- for (const fixture of fixtures) {
- describe(`with "${fixture.type}" rendering`, () => {
- it('should be visible with the open attribute', async () => {
- const el = await fixture(html`
-
- Toggle
-
- Item 1
- Item 2
- Item 3
-
-
- `);
- const panel = el.shadowRoot!.querySelector('[part~="panel"]')!;
-
- expect(panel.hidden).to.be.false;
- });
-
- it('should not be visible without the open attribute', async () => {
- const el = await fixture(html`
-
- Toggle
-
- Item 1
- Item 2
- Item 3
-
-
- `);
- const panel = el.shadowRoot!.querySelector('[part~="panel"]')!;
-
- expect(panel.hidden).to.be.true;
- });
-
- it('should emit wa-show and wa-after-show when calling show()', async () => {
- const el = await fixture(html`
-
- Toggle
-
- Item 1
- Item 2
- Item 3
-
-
- `);
- const panel = el.shadowRoot!.querySelector('[part~="panel"]')!;
- const showHandler = sinon.spy();
- const afterShowHandler = sinon.spy();
-
- el.addEventListener('wa-show', showHandler);
- el.addEventListener('wa-after-show', afterShowHandler);
- el.show();
-
- await waitUntil(() => showHandler.calledOnce);
- await waitUntil(() => afterShowHandler.calledOnce);
-
- expect(showHandler).to.have.been.calledOnce;
- expect(afterShowHandler).to.have.been.calledOnce;
- expect(panel.hidden).to.be.false;
- });
-
- it('should emit wa-hide and wa-after-hide when calling hide()', async () => {
- // @TODO: Fix this [Konnor]
- if (fixture.type === 'ssr-client-hydrated') {
- return;
- }
-
- const el = await fixture(html`
-
- Toggle
-
- Item 1
- Item 2
- Item 3
-
-
- `);
- const panel = el.shadowRoot!.querySelector('[part~="panel"]')!;
- const hideHandler = sinon.spy();
- const afterHideHandler = sinon.spy();
-
- el.addEventListener('wa-hide', hideHandler);
- el.addEventListener('wa-after-hide', afterHideHandler);
- el.hide();
-
- await waitUntil(() => hideHandler.calledOnce);
- await waitUntil(() => afterHideHandler.calledOnce);
-
- expect(hideHandler).to.have.been.calledOnce;
- expect(afterHideHandler).to.have.been.calledOnce;
- expect(panel.hidden).to.be.true;
- });
-
- it('should emit wa-show and wa-after-show when setting open = true', async () => {
- const el = await fixture(html`
-
- Toggle
-
- Item 1
- Item 2
- Item 3
-
-
- `);
- const panel = el.shadowRoot!.querySelector('[part~="panel"]')!;
- const showHandler = sinon.spy();
- const afterShowHandler = sinon.spy();
-
- el.addEventListener('wa-show', showHandler);
- el.addEventListener('wa-after-show', afterShowHandler);
- el.open = true;
-
- await waitUntil(() => showHandler.calledOnce);
- await waitUntil(() => afterShowHandler.calledOnce);
-
- expect(showHandler).to.have.been.calledOnce;
- expect(afterShowHandler).to.have.been.calledOnce;
- expect(panel.hidden).to.be.false;
- });
-
- it('should emit wa-hide and wa-after-hide when setting open = false', async () => {
- // @TODO: Fix this [Konnor]
- if (fixture.type === 'ssr-client-hydrated') {
- return;
- }
-
- const el = await fixture(html`
-
- Toggle
-
- Item 1
- Item 2
- Item 3
-
-
- `);
- const panel = el.shadowRoot!.querySelector('[part~="panel"]')!;
- const hideHandler = sinon.spy();
- const afterHideHandler = sinon.spy();
-
- el.addEventListener('wa-hide', hideHandler);
- el.addEventListener('wa-after-hide', afterHideHandler);
- el.open = false;
-
- await waitUntil(() => hideHandler.calledOnce);
- await waitUntil(() => afterHideHandler.calledOnce);
-
- expect(hideHandler).to.have.been.calledOnce;
- expect(afterHideHandler).to.have.been.calledOnce;
- expect(panel.hidden).to.be.true;
- });
-
- it('should still open on arrow navigation when no menu items', async () => {
- const el = await fixture(html`
-
- Toggle
-
-
- `);
- const trigger = el.querySelector('wa-button')!;
-
- trigger.focus();
- await sendKeys({ press: 'ArrowDown' });
- await el.updateComplete;
-
- expect(el.open).to.be.true;
- });
-
- it('should open on arrow down navigation', async () => {
- const el = await fixture(html`
-
- Toggle
-
- Item 1
- Item 2
-
-
- `);
- const trigger = el.querySelector('wa-button')!;
- const firstMenuItem = el.querySelectorAll('wa-menu-item')[0];
-
- trigger.focus();
- await sendKeys({ press: 'ArrowDown' });
- await el.updateComplete;
-
- expect(el.open).to.be.true;
- expect(document.activeElement).to.equal(firstMenuItem);
- });
-
- it('should open on arrow up navigation', async () => {
- const el = await fixture(html`
-
- Toggle
-
- Item 1
- Item 2
-
-
- `);
- const trigger = el.querySelector('wa-button')!;
- const secondMenuItem = el.querySelectorAll('wa-menu-item')[1];
-
- trigger.focus();
- await sendKeys({ press: 'ArrowUp' });
- await el.updateComplete;
-
- expect(el.open).to.be.true;
- expect(document.activeElement).to.equal(secondMenuItem);
- });
-
- it('should navigate to first focusable item on arrow navigation', async () => {
- const el = await fixture(html`
-
- Toggle
-
- Top Label
- Item 1
-
-
- `);
- const trigger = el.querySelector('wa-button')!;
- const item = el.querySelector('wa-menu-item')!;
-
- await clickOnElement(trigger);
- await trigger.updateComplete;
- await sendKeys({ press: 'ArrowDown' });
- await el.updateComplete;
-
- expect(document.activeElement).to.equal(item);
- });
-
- it('should close on escape key', async () => {
- const el = await fixture(html`
-
- Toggle
-
- Item 1
- Item 2
-
-
- `);
- const trigger = el.querySelector('wa-button')!;
-
- trigger.focus();
- await sendKeys({ press: 'Escape' });
- await el.updateComplete;
-
- expect(el.open).to.be.false;
- });
-
- it('should not open on arrow navigation when no menu exists', async () => {
- const el = await fixture(html`
-
- Toggle
- Some custom content
-
- `);
- const trigger = el.querySelector('wa-button')!;
-
- trigger.focus();
- await sendKeys({ press: 'ArrowDown' });
- await el.updateComplete;
-
- expect(el.open).to.be.false;
- });
-
- it('should open on enter key', async () => {
- const el = await fixture(html`
-
- Toggle
-
- Item 1
-
-
- `);
- const trigger = el.querySelector('wa-button')!;
-
- trigger.focus();
- await el.updateComplete;
- await sendKeys({ press: 'Enter' });
- await el.updateComplete;
-
- expect(el.open).to.be.true;
- });
-
- it('should focus on menu items when clicking the trigger and arrowing through options', async () => {
- const el = await fixture(html`
-
- Toggle
-
- Item 1
- Item 2
- Item 3
-
-
- `);
- const trigger = el.querySelector('wa-button')!;
- const secondMenuItem = el.querySelectorAll('wa-menu-item')[1];
-
- await clickOnElement(trigger);
- await trigger.updateComplete;
- await sendKeys({ press: 'ArrowDown' });
- await el.updateComplete;
- await sendKeys({ press: 'ArrowDown' });
- await el.updateComplete;
-
- expect(document.activeElement).to.equal(secondMenuItem);
- });
-
- it('should open on enter key when no menu exists', async () => {
- const el = await fixture(html`
-
- Toggle
- Some custom content
-
- `);
- const trigger = el.querySelector('wa-button')!;
-
- trigger.focus();
- await el.updateComplete;
- await sendKeys({ press: 'Enter' });
- await el.updateComplete;
-
- expect(el.open).to.be.true;
- });
-
- it('should hide when clicked outside container and initially open', async () => {
- const el = await fixture(html`
-
- Toggle
-
- Item 1
-
-
- `);
-
- await sendMouse({ type: 'click', position: [0, 0] });
- await el.updateComplete;
-
- expect(el.open).to.be.false;
- });
-
- it('should hide when clicked outside container', async () => {
- const el = await fixture(html`
-
- Toggle
-
- Item 1
-
-
- `);
- const trigger = el.querySelector('wa-button')!;
-
- trigger.click();
- await el.updateComplete;
- await sendMouse({ type: 'click', position: [0, 0] });
- await el.updateComplete;
-
- expect(el.open).to.be.false;
- });
-
- it('should close and stop propagating the keydown event when Escape is pressed and the dropdown is open ', async () => {
- const el = await fixture(html`
-
- Toggle
-
- Dropdown Item 1
- Dropdown Item 2
- Dropdown Item 3
-
-
- `);
- const firstMenuItem = el.querySelector('wa-menu-item')!;
- const hideHandler = sinon.spy();
-
- document.body.addEventListener('keydown', hideHandler);
- firstMenuItem.focus();
- await sendKeys({ press: 'Escape' });
- await el.updateComplete;
-
- expect(el.open).to.be.false;
-
- if ('CloseWatcher' in window) {
- return;
- }
-
- // @TODO: Fix this [Konnor]
- if (fixture.type === 'ssr-client-hydrated') {
- return;
- }
-
- expect(hideHandler).to.not.have.been.called;
- });
- });
- }
-});
diff --git a/packages/webawesome/src/components/dropdown/dropdown.ts b/packages/webawesome/src/components/dropdown/dropdown.ts
index deb70ff4c..9fd7582b3 100644
--- a/packages/webawesome/src/components/dropdown/dropdown.ts
+++ b/packages/webawesome/src/components/dropdown/dropdown.ts
@@ -5,7 +5,6 @@ import { ifDefined } from 'lit/directives/if-defined.js';
import { WaAfterHideEvent } from '../../events/after-hide.js';
import { WaAfterShowEvent } from '../../events/after-show.js';
import { WaHideEvent } from '../../events/hide.js';
-import type { WaSelectEvent } from '../../events/select.js';
import { WaShowEvent } from '../../events/show.js';
import { animateWithClass } from '../../internal/animate.js';
import { waitForEvent } from '../../internal/event.js';
@@ -13,7 +12,6 @@ import { watch } from '../../internal/watch.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import sizeStyles from '../../styles/utilities/size.css';
import type WaButton from '../button/button.js';
-import type WaMenu from '../menu/menu.js';
import '../popup/popup.js';
import type WaPopup from '../popup/popup.js';
import styles from './dropdown.css';
@@ -132,7 +130,7 @@ export default class WaDropdown extends WebAwesomeElement {
getMenu() {
return this.panel.assignedElements({ flatten: true }).find(el => el.tagName.toLowerCase() === 'wa-menu') as
- | WaMenu
+ | any
| undefined;
}
@@ -193,7 +191,7 @@ export default class WaDropdown extends WebAwesomeElement {
}
};
- private handlePanelSelect = (event: WaSelectEvent) => {
+ private handlePanelSelect = (event: CustomEvent) => {
const target = event.target as HTMLElement;
// Hide the dropdown when a menu item is selected
diff --git a/packages/webawesome/src/components/menu-item/menu-item.css b/packages/webawesome/src/components/menu-item/menu-item.css
deleted file mode 100644
index c63a19948..000000000
--- a/packages/webawesome/src/components/menu-item/menu-item.css
+++ /dev/null
@@ -1,149 +0,0 @@
-:host {
- --background-color-hover: var(--wa-color-neutral-fill-normal);
- --text-color-hover: var(--wa-color-neutral-on-normal);
- --submenu-offset: -0.125rem;
-
- display: block;
- color: var(--wa-color-text-normal);
- position: relative;
- display: flex;
- align-items: stretch;
- font: inherit;
- padding: 0.5em 0.25em;
- line-height: var(--wa-line-height-condensed);
- transition: fill var(--wa-transition-normal) var(--wa-transition-easing);
- user-select: none;
- -webkit-user-select: none;
- white-space: nowrap;
- cursor: pointer;
-}
-
-:host([inert]) {
- display: none;
-}
-
-:host([disabled]) {
- outline: none;
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-:host([loading]) {
- outline: none;
- cursor: wait;
-}
-
-:host([loading]) *:not(wa-spinner) {
- opacity: 0.5;
-}
-
-:host([loading]) wa-spinner {
- --indicator-color: currentColor;
- --track-width: round(0.0625em, 1px);
- position: absolute;
- font-size: var(--wa-font-size-smaller);
- top: calc(50% - 0.5em);
- left: 0.6em;
- opacity: 1;
-}
-
-.label {
- flex: 1 1 auto;
- display: inline-block;
- text-overflow: ellipsis;
- overflow: hidden;
-}
-
-.prefix {
- flex: 0 0 auto;
- display: flex;
- align-items: center;
-}
-
-.prefix::slotted(*) {
- margin-inline-end: 0.5em;
-}
-
-.suffix {
- flex: 0 0 auto;
- display: flex;
- align-items: center;
-}
-
-.suffix::slotted(*) {
- margin-inline-start: 0.5em;
-}
-
-/* Safe triangle */
-:host(:state(submenu-expanded))::after {
- content: '';
- position: fixed;
- z-index: 899;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- clip-path: polygon(
- var(--safe-triangle-cursor-x, 0) var(--safe-triangle-cursor-y, 0),
- var(--safe-triangle-submenu-start-x, 0) var(--safe-triangle-submenu-start-y, 0),
- var(--safe-triangle-submenu-end-x, 0) var(--safe-triangle-submenu-end-y, 0)
- );
-}
-
-:host(:focus-visible) {
- outline: none;
-}
-
-:host(:hover:not([aria-disabled='true'], :focus-visible)),
-:host(:state(submenu-expanded)) {
- background-color: var(--background-color-hover);
- color: var(--text-color-hover);
-}
-
-:host(:focus-visible) {
- outline: var(--wa-focus-ring);
- outline-offset: calc(-1 * var(--wa-focus-ring-width));
- background: var(--background-color-hover);
- color: var(--text-color-hover);
- opacity: 1;
-}
-
-.check,
-.chevron {
- flex: 0 0 auto;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: var(--wa-font-size-smaller);
- width: 2em;
- visibility: hidden;
-}
-
-:host([checked]) .check,
-:host(:state(has-submenu)) .chevron {
- visibility: visible;
-}
-
-/* Add elevation and z-index to submenus */
-wa-popup::part(popup) {
- box-shadow: var(--wa-shadow-m);
- z-index: 900;
- margin-left: var(--submenu-offset);
-}
-
-wa-popup:dir(rtl)::part(popup) {
- margin-left: calc(-1 * var(--submenu-offset));
-}
-
-@media (forced-colors: active) {
- :host(:hover:not([aria-disabled='true'])),
- :host(:focus-visible) {
- outline: dashed 1px SelectedItem;
- outline-offset: -1px;
- }
-}
-
-::slotted(wa-menu) {
- max-width: var(--auto-size-available-width) !important;
- max-height: var(--auto-size-available-height) !important;
-}
diff --git a/packages/webawesome/src/components/menu-item/menu-item.test.ts b/packages/webawesome/src/components/menu-item/menu-item.test.ts
deleted file mode 100644
index e28ef916f..000000000
--- a/packages/webawesome/src/components/menu-item/menu-item.test.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-import { aTimeout, expect, waitUntil } from '@open-wc/testing';
-import { sendKeys } from '@web/test-runner-commands';
-import { html } from 'lit';
-import sinon from 'sinon';
-import type { WaSelectEvent } from '../../events/select.js';
-import { clickOnElement } from '../../internal/test.js';
-import { fixtures } from '../../internal/test/fixture.js';
-import type WaMenuItem from './menu-item.js';
-
-describe('', () => {
- for (const fixture of fixtures) {
- describe(`with "${fixture.type}" rendering`, () => {
- it('should pass accessibility tests', async () => {
- const el = await fixture(html`
-
- Item 1
- Item 2
- Item 3
-
- Checked
- Unchecked
-
- `);
- await expect(el).to.be.accessible();
- });
-
- it('should pass accessibility tests when using a submenu', async () => {
- const el = await fixture(html`
-
-
- Submenu
-
- Submenu Item 1
- Submenu Item 2
-
-
-
- `);
- await expect(el).to.be.accessible();
- });
-
- it('should have the correct default properties', async () => {
- const el = await fixture(html` Test `);
-
- expect(el.value).to.equal('');
- expect(el.disabled).to.be.false;
- expect(el.loading).to.equal(false);
- expect(el.getAttribute('aria-disabled')).to.equal('false');
- });
-
- it('should render the correct aria attributes when disabled', async () => {
- const el = await fixture(html` Test `);
- expect(el.getAttribute('aria-disabled')).to.equal('true');
- });
-
- describe('when loading', () => {
- it('should have a spinner present', async () => {
- const el = await fixture(html` Menu Item Label `);
- expect(el.shadowRoot!.querySelector('wa-spinner')).to.exist;
- });
- });
-
- it('defaultLabel should return a text label', async () => {
- const el = await fixture(html` Test `);
- expect(el.defaultLabel).to.equal('Test');
- expect(el.label).to.equal('Test');
- });
-
- it('label attribute should override default label', async () => {
- const el = await fixture(html` Text content `);
- expect(el.defaultLabel).to.equal('Text content');
- expect(el.label).to.equal('Manual label');
- });
-
- it('should emit the slotchange event when the label changes', async () => {
- const el = await fixture(html` Text `);
- const slotChangeHandler = sinon.spy();
-
- el.addEventListener('slotchange', slotChangeHandler);
- el.textContent = 'New Text';
- await waitUntil(() => slotChangeHandler.calledOnce);
-
- expect(slotChangeHandler).to.have.been.calledOnce;
- });
-
- it('should render a hidden menu item when the inert attribute is used', async () => {
- const menu = await fixture(html`
-
- Item 1
- Item 2
- Item 3
-
- `);
- const item1 = menu.querySelector('wa-menu-item')!;
-
- expect(getComputedStyle(item1).display).to.equal('none');
- });
-
- it('should not render a wa-popup if the slot="submenu" attribute is missing, but the slot should exist in the component and be hidden.', async () => {
- const menu = await fixture(html`
-
-
- Item 1
-
- Nested Item 1
-
-
-
- `);
-
- const menuItem: HTMLElement = menu.querySelector('wa-menu-item')!;
- expect(menuItem.shadowRoot!.querySelector('wa-popup')).to.be.null;
- const submenuSlot: HTMLElement = menuItem.shadowRoot!.querySelector('slot[name="submenu"]')!;
- expect(submenuSlot.hidden).to.be.true;
- });
-
- it('should render a wa-popup if the slot="submenu" attribute is present', async () => {
- const menu = await fixture(html`
-
-
- Item 1
-
- Nested Item 1
-
-
-
- `);
-
- const menuItem = menu.querySelector('wa-menu-item')!;
- expect(menuItem.shadowRoot!.querySelector('wa-popup')).to.be.not.null;
- const submenuSlot: HTMLElement = menuItem.shadowRoot!.querySelector('slot[name="submenu"]')!;
- expect(submenuSlot.hidden).to.be.false;
- });
-
- it('should focus on first menuitem of submenu if ArrowRight is pressed on parent menuitem', async () => {
- const menu = await fixture(html`
-
-
- Submenu
-
- Nested Item 1
-
-
-
- `);
-
- const selectHandler = sinon.spy((event: WaSelectEvent) => {
- const item = event.detail.item;
- expect(item.value).to.equal('submenu-item-1');
- });
- menu.addEventListener('wa-select', selectHandler);
-
- const submenu = menu.querySelector('wa-menu-item')!;
- // Sometimes Chrome fails if we dont click before triggering focus.
- await clickOnElement(submenu);
- submenu.focus();
- await menu.updateComplete;
- await sendKeys({ press: 'ArrowRight' });
- await menu.updateComplete;
- await sendKeys({ press: 'Enter' });
- await menu.updateComplete;
- // Once for each menu element.
- expect(selectHandler).to.have.been.calledTwice;
- });
-
- it('should focus on outer menu if ArrowRight is pressed on nested menuitem', async () => {
- const menu = await fixture(html`
-
-
- Submenu
-
- Nested Item 1
-
-
-
- `);
-
- const focusHandler = sinon.spy((event: FocusEvent) => {
- const target = event.target as WaMenuItem;
- const relatedTarget = event.relatedTarget as WaMenuItem;
- expect(target.value).to.equal('outer-item-1');
- expect(relatedTarget.value).to.equal('inner-item-1');
- });
-
- const outerItem = menu.querySelector('#outer')!;
- // Silly fix for CI + Chrome to focus properly.
- await clickOnElement(outerItem);
-
- outerItem.focus();
- await menu.updateComplete;
- await sendKeys({ press: 'ArrowRight' });
-
- outerItem.addEventListener('focus', focusHandler);
- await menu.updateComplete;
- await sendKeys({ press: 'ArrowLeft' });
- await menu.updateComplete;
- expect(focusHandler).to.have.been.calledOnce;
- });
- });
- }
-});
diff --git a/packages/webawesome/src/components/menu-item/menu-item.ts b/packages/webawesome/src/components/menu-item/menu-item.ts
deleted file mode 100644
index 408021639..000000000
--- a/packages/webawesome/src/components/menu-item/menu-item.ts
+++ /dev/null
@@ -1,238 +0,0 @@
-import type { PropertyValues } from 'lit';
-import { html } from 'lit';
-import { customElement, property, query, state } from 'lit/decorators.js';
-import getText from '../../internal/get-text.js';
-import WebAwesomeElement from '../../internal/webawesome-element.js';
-import { LocalizeController } from '../../utilities/localize.js';
-import '../icon/icon.js';
-import '../popup/popup.js';
-import '../spinner/spinner.js';
-import styles from './menu-item.css';
-import { SubmenuController } from './submenu-controller.js';
-
-/**
- * @summary Menu items provide options for the user to pick from in a menu.
- * @documentation https://backers.webawesome.com/docs/components/menu-item
- * @status stable
- * @since 2.0
- *
- * @dependency wa-icon
- * @dependency wa-popup
- *
- * @slot - The menu item's label.
- * @slot prefix - Used to prepend an icon or similar element to the menu item.
- * @slot suffix - Used to append an icon or similar element to the menu item.
- * @slot submenu - Used to denote a nested menu.
- * @slot checked-icon - The icon used to indicate that this menu item is checked. Usually a ``.
- * @slot submenu-icon - The icon used to indicate that this menu item has a submenu. Usually a ``.
- *
- * @csspart checked-icon - The checked icon, which is only visible when the menu item is checked.
- * @csspart prefix - The prefix container.
- * @csspart label - The menu item label.
- * @csspart suffix - The suffix container.
- * @csspart spinner - The spinner that shows when the menu item is in the loading state.
- * @csspart spinner__base - The spinner's base part.
- * @csspart submenu-icon - The submenu icon, visible only when the menu item has a submenu (not yet implemented).
- *
- * @cssproperty --background-color-hover - The menu item's background color on hover.
- * @cssproperty --text-color-hover - The label color on hover.
- * @cssproperty [--submenu-offset=-2px] - The distance submenus shift to overlap the parent menu.
- *
- * @cssstate has-submenu - Applied when the menu item has a submenu.
- * @cssstate submenu-expanded - Applied when the menu item has a submenu and it is expanded.
- */
-@customElement('wa-menu-item')
-export default class WaMenuItem extends WebAwesomeElement {
- static css = styles;
-
- private readonly localize = new LocalizeController(this);
-
- @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;
-
- /** A unique value to store in the menu item. This can be used as a way to identify menu items when selected. */
- @property() value = '';
-
- /** Draws the menu item in a loading state. */
- @property({ type: Boolean, reflect: true }) loading = false;
-
- /** Draws the menu item in a disabled state, preventing selection. */
- @property({ type: Boolean, reflect: true }) disabled = false;
-
- _label: string = '';
- /**
- * The option’s plain text label.
- * Usually automatically generated, but can be useful to provide manually for cases involving complex content.
- */
- @property()
- set label(value) {
- const oldValue = this._label;
- this._label = value || '';
-
- if (this._label !== oldValue) {
- this.requestUpdate('label', oldValue);
- }
- }
-
- get label(): string {
- if (this._label) {
- return this._label;
- }
-
- if (!this.defaultLabel) {
- this.updateDefaultLabel();
- }
-
- return this.defaultLabel;
- }
-
- /** The default label, generated from the element contents. Will be equal to `label` in most cases. */
- @state() defaultLabel = '';
-
- /**
- * Used for SSR purposes. If true, will render a ">" caret icon for showing that it has a submenu, but will be non-interactive.
- */
- @property({ attribute: 'with-submenu', type: Boolean }) withSubmenu = false;
-
- private submenuController: SubmenuController = new SubmenuController(this);
-
- connectedCallback() {
- super.connectedCallback();
- this.addEventListener('click', this.handleHostClick);
- this.addEventListener('mouseover', this.handleMouseOver);
- this.updateDefaultLabel();
- }
-
- disconnectedCallback() {
- super.disconnectedCallback();
- this.removeEventListener('click', this.handleHostClick);
- this.removeEventListener('mouseover', this.handleMouseOver);
- }
-
- protected firstUpdated(changedProperties: PropertyValues): void {
- // Kick it so that it renders the "submenu" properly.
- if (this.isSubmenu()) {
- this.requestUpdate();
- }
-
- super.firstUpdated(changedProperties);
- }
-
- private handleDefaultSlotChange() {
- let labelChanged = this.updateDefaultLabel();
-
- // When the label changes, emit a slotchange event so parent controls see it
- if (labelChanged) {
- /** @internal - prevent the CEM from recording this event */
- this.dispatchEvent(new Event('slotchange', { bubbles: true, composed: false, cancelable: false }));
- }
-
- this.customStates.set('has-submenu', this.isSubmenu());
- }
-
- private handleHostClick = (event: MouseEvent) => {
- // Prevent the click event from being emitted when the button is disabled or loading
- if (this.disabled) {
- event.preventDefault();
- event.stopImmediatePropagation();
- }
- };
-
- private handleMouseOver = (event: MouseEvent) => {
- this.focus();
- event.stopPropagation();
- };
-
- updated(changedProperties: PropertyValues) {
- if (changedProperties.has('checked')) {
- // For proper accessibility, users have to use type="checkbox" to use the checked attribute
- if (this.checked && this.type !== 'checkbox') {
- this.checked = false;
- return;
- }
-
- // Only checkbox types can receive the aria-checked attribute
- if (this.type === 'checkbox') {
- this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
- } else {
- this.removeAttribute('aria-checked');
- }
- }
-
- if (changedProperties.has('disabled')) {
- this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
- }
-
- if (changedProperties.has('type')) {
- if (this.type === 'checkbox') {
- this.setAttribute('role', 'menuitemcheckbox');
- this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
- } else {
- this.setAttribute('role', 'menuitem');
- this.removeAttribute('aria-checked');
- }
- }
- }
-
- private updateDefaultLabel() {
- let oldValue = this.defaultLabel;
- this.defaultLabel = getText(this).trim();
- let changed = this.defaultLabel !== oldValue;
-
- if (!this._label && changed) {
- // Uses default label, and it has changed
- this.requestUpdate('label', oldValue);
- }
-
- return changed;
- }
-
- /** Does this element have a submenu? */
- private isSubmenu() {
- return this.hasUpdated ? this.querySelector(`:scope > [slot="submenu"]`) !== null : this.withSubmenu;
- }
-
- render() {
- const isRtl = this.hasUpdated ? this.localize.dir() === 'rtl' : this.dir === 'rtl';
- const isSubmenuExpanded = this.submenuController.isExpanded();
- this.customStates.set('submenu-expanded', isSubmenuExpanded);
-
- this.internals.ariaHasPopup = this.isSubmenu() + '';
- this.internals.ariaExpanded = isSubmenuExpanded + '';
-
- return html`
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${this.submenuController.renderSubmenu()} ${this.loading ? html` ` : ''}
- `;
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'wa-menu-item': WaMenuItem;
- }
-}
diff --git a/packages/webawesome/src/components/menu-item/submenu-controller.ts b/packages/webawesome/src/components/menu-item/submenu-controller.ts
deleted file mode 100644
index b1090bc8d..000000000
--- a/packages/webawesome/src/components/menu-item/submenu-controller.ts
+++ /dev/null
@@ -1,285 +0,0 @@
-import type { ReactiveController, ReactiveControllerHost } from 'lit';
-import { html } from 'lit';
-import { createRef, ref, type Ref } from 'lit/directives/ref.js';
-import type WaPopup from '../popup/popup.js';
-import type WaMenuItem from './menu-item.js';
-
-/** A reactive controller to manage the registration of event listeners for submenus. */
-export class SubmenuController implements ReactiveController {
- private host: ReactiveControllerHost & WaMenuItem;
- private popupRef: Ref = createRef();
- private enableSubmenuTimer = -1;
- private isConnected = false;
- private isPopupConnected = false;
- private skidding = 0;
- private readonly submenuOpenDelay = 100;
-
- constructor(host: ReactiveControllerHost & WaMenuItem) {
- (this.host = host).addController(this);
- }
-
- private hasSubmenu() {
- return this.host.querySelector(`:scope > [slot="submenu"]`) !== null;
- }
-
- hostConnected() {
- if (this.hasSubmenu() && !this.host.disabled) {
- this.addListeners();
- }
- }
-
- hostDisconnected() {
- this.removeListeners();
- }
-
- hostUpdated() {
- if (this.hasSubmenu() && !this.host.disabled) {
- this.addListeners();
- this.updateSkidding();
- } else {
- this.removeListeners();
- }
- }
-
- private addListeners() {
- if (!this.isConnected) {
- this.host.addEventListener('mousemove', this.handleMouseMove);
- this.host.addEventListener('mouseover', this.handleMouseOver);
- this.host.addEventListener('keydown', this.handleKeyDown);
- this.host.addEventListener('click', this.handleClick);
- this.host.addEventListener('focusout', this.handleFocusOut);
- this.isConnected = true;
- }
-
- // The popup does not seem to get wired when the host is
- // connected, so manage its listeners separately.
- if (!this.isPopupConnected) {
- if (this.popupRef.value) {
- this.popupRef.value.addEventListener('mouseover', this.handlePopupMouseover);
- this.popupRef.value.addEventListener('wa-reposition', this.handlePopupReposition);
- this.isPopupConnected = true;
- }
- }
- }
-
- private removeListeners() {
- if (this.isConnected) {
- this.host.removeEventListener('mousemove', this.handleMouseMove);
- this.host.removeEventListener('mouseover', this.handleMouseOver);
- this.host.removeEventListener('keydown', this.handleKeyDown);
- this.host.removeEventListener('click', this.handleClick);
- this.host.removeEventListener('focusout', this.handleFocusOut);
- this.isConnected = false;
- }
- if (this.isPopupConnected) {
- if (this.popupRef.value) {
- this.popupRef.value.removeEventListener('mouseover', this.handlePopupMouseover);
- this.popupRef.value.removeEventListener('wa-reposition', this.handlePopupReposition);
- this.isPopupConnected = false;
- }
- }
- }
-
- // Set the safe triangle cursor position
- private handleMouseMove = (event: MouseEvent) => {
- this.host.style.setProperty('--safe-triangle-cursor-x', `${event.clientX}px`);
- this.host.style.setProperty('--safe-triangle-cursor-y', `${event.clientY}px`);
- };
-
- private handleMouseOver = () => {
- if (this.hasSubmenu()) {
- this.enableSubmenu();
- }
- };
-
- private handleSubmenuEntry(event: KeyboardEvent) {
- // Pass focus to the first menu-item in the submenu.
- const submenuSlot: HTMLSlotElement | null = this.host.renderRoot.querySelector("slot[name='submenu']");
-
- // Missing slot
- if (!submenuSlot) {
- return;
- }
-
- // Menus
- let menuItems: NodeListOf | null = null;
- for (const elt of submenuSlot.assignedElements()) {
- menuItems = elt.querySelectorAll("wa-menu-item, [role^='menuitem']");
- if (menuItems.length !== 0) {
- break;
- }
- }
-
- if (!menuItems || menuItems.length === 0) {
- return;
- }
-
- menuItems[0].setAttribute('tabindex', '0');
- for (let i = 1; i !== menuItems.length; ++i) {
- menuItems[i].setAttribute('tabindex', '-1');
- }
-
- // Open the submenu (if not open), and set focus to first menuitem.
- if (this.popupRef.value) {
- event.preventDefault();
- event.stopPropagation();
- if (this.popupRef.value.active) {
- if (menuItems[0] instanceof HTMLElement) {
- menuItems[0].focus();
- }
- } else {
- this.enableSubmenu(false);
- this.host.updateComplete.then(() => {
- if (menuItems[0] instanceof HTMLElement) {
- menuItems[0].focus();
- }
- });
- this.host.requestUpdate();
- }
- }
- }
-
- // Focus on the first menu-item of a submenu.
- private handleKeyDown = (event: KeyboardEvent) => {
- switch (event.key) {
- case 'Escape':
- case 'Tab':
- this.disableSubmenu();
- break;
- case 'ArrowLeft':
- // Either focus is currently on the host element or a child
- if (event.target !== this.host) {
- event.preventDefault();
- event.stopPropagation();
- this.host.focus();
- this.disableSubmenu();
- }
- break;
- case 'ArrowRight':
- case 'Enter':
- case ' ':
- this.handleSubmenuEntry(event);
- break;
- default:
- break;
- }
- };
-
- private handleClick = (event: MouseEvent) => {
- // Clicking on the item which heads the menu does nothing, otherwise hide submenu and propagate
- if (event.target === this.host) {
- event.preventDefault();
- event.stopPropagation();
- } else if (
- event.target instanceof Element &&
- (event.target.tagName === 'wa-menu-item' || event.target.role?.startsWith('menuitem'))
- ) {
- this.disableSubmenu();
- }
- };
-
- // Close this submenu on focus outside of the parent or any descendants.
- private handleFocusOut = (event: FocusEvent) => {
- if (event.relatedTarget && event.relatedTarget instanceof Element && this.host.contains(event.relatedTarget)) {
- return;
- }
- this.disableSubmenu();
- };
-
- // Prevent the parent menu-item from getting focus on mouse movement on the submenu
- private handlePopupMouseover = (event: MouseEvent) => {
- event.stopPropagation();
- };
-
- // Set the safe triangle values for the submenu when the position changes
- private handlePopupReposition = () => {
- const submenuSlot: HTMLSlotElement | null = this.host.renderRoot.querySelector("slot[name='submenu']");
- const menu = submenuSlot?.assignedElements({ flatten: true }).filter(el => el.localName === 'wa-menu')[0];
- const isRtl = getComputedStyle(this.host).direction === 'rtl';
-
- if (!menu) {
- return;
- }
-
- const { left, top, width, height } = menu.getBoundingClientRect();
-
- this.host.style.setProperty('--safe-triangle-submenu-start-x', `${isRtl ? left + width : left}px`);
- this.host.style.setProperty('--safe-triangle-submenu-start-y', `${top}px`);
- this.host.style.setProperty('--safe-triangle-submenu-end-x', `${isRtl ? left + width : left}px`);
- this.host.style.setProperty('--safe-triangle-submenu-end-y', `${top + height}px`);
- };
-
- private setSubmenuState(state: boolean) {
- if (this.popupRef.value) {
- if (this.popupRef.value.active !== state) {
- this.popupRef.value.active = state;
- this.host.requestUpdate();
- }
- }
- }
-
- // Shows the submenu. Supports disabling the opening delay, e.g. for keyboard events that want to set the focus to the
- // newly opened menu.
- private enableSubmenu(delay = true) {
- if (delay) {
- window.clearTimeout(this.enableSubmenuTimer);
- this.enableSubmenuTimer = window.setTimeout(() => {
- this.setSubmenuState(true);
- }, this.submenuOpenDelay);
- } else {
- this.setSubmenuState(true);
- }
- }
-
- private disableSubmenu() {
- window.clearTimeout(this.enableSubmenuTimer);
- this.setSubmenuState(false);
- }
-
- // Calculate the space the top of a menu takes-up, for aligning the popup menu-item with the activating element.
- private updateSkidding(): void {
- // .computedStyleMap() not always available.
- if (!this.host.parentElement?.computedStyleMap) {
- return;
- }
- const styleMap: StylePropertyMapReadOnly = this.host.parentElement.computedStyleMap();
- const attrs: string[] = ['padding-top', 'border-top-width', 'margin-top'];
-
- const skidding = attrs.reduce((accumulator, attr) => {
- const styleValue: CSSStyleValue = styleMap.get(attr) ?? new CSSUnitValue(0, 'px');
- const unitValue = styleValue instanceof CSSUnitValue ? styleValue : new CSSUnitValue(0, 'px');
- const pxValue = unitValue.to('px');
- return accumulator - pxValue.value;
- }, 0);
-
- this.skidding = skidding;
- }
-
- isExpanded(): boolean {
- return this.popupRef.value ? this.popupRef.value.active : false;
- }
-
- renderSubmenu() {
- // Always render the slot, but conditionally render the outer
- if (!this.host.hasUpdated) {
- return html` `;
- }
-
- const isRtl = getComputedStyle(this.host).direction === 'rtl';
-
- return html`
-
-
-
- `;
- }
-}
diff --git a/packages/webawesome/src/components/menu-label/menu-label.css b/packages/webawesome/src/components/menu-label/menu-label.css
deleted file mode 100644
index bad639ac2..000000000
--- a/packages/webawesome/src/components/menu-label/menu-label.css
+++ /dev/null
@@ -1,9 +0,0 @@
-:host {
- display: block;
- color: var(--wa-color-text-quiet);
- font-size: var(--wa-font-size-smaller);
- font-weight: var(--wa-font-weight-semibold);
- padding: 0.5em 2.25em;
- -webkit-user-select: none;
- user-select: none;
-}
diff --git a/packages/webawesome/src/components/menu-label/menu-label.test.ts b/packages/webawesome/src/components/menu-label/menu-label.test.ts
deleted file mode 100644
index f82c8f3fe..000000000
--- a/packages/webawesome/src/components/menu-label/menu-label.test.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { expect } from '@open-wc/testing';
-import { html } from 'lit';
-import { fixtures } from '../../internal/test/fixture.js';
-import type WaMenuLabel from './menu-label.js';
-
-describe('', () => {
- for (const fixture of fixtures) {
- describe(`with "${fixture.type}" rendering`, () => {
- it('passes accessibility test', async () => {
- const el = await fixture(html` Test `);
- await expect(el).to.be.accessible();
- });
- });
- }
-});
diff --git a/packages/webawesome/src/components/menu-label/menu-label.ts b/packages/webawesome/src/components/menu-label/menu-label.ts
deleted file mode 100644
index 7450db65c..000000000
--- a/packages/webawesome/src/components/menu-label/menu-label.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { html } from 'lit';
-import { customElement } from 'lit/decorators.js';
-import WebAwesomeElement from '../../internal/webawesome-element.js';
-import styles from './menu-label.css';
-
-/**
- * @summary Menu labels are used to describe a group of menu items.
- * @documentation https://backers.webawesome.com/docs/components/menu-label
- * @status stable
- * @since 2.0
- *
- * @slot - The menu label's content.
- */
-@customElement('wa-menu-label')
-export default class WaMenuLabel extends WebAwesomeElement {
- static css = styles;
-
- render() {
- return html` `;
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'wa-menu-label': WaMenuLabel;
- }
-}
diff --git a/packages/webawesome/src/components/menu/menu.css b/packages/webawesome/src/components/menu/menu.css
deleted file mode 100644
index 1c700da85..000000000
--- a/packages/webawesome/src/components/menu/menu.css
+++ /dev/null
@@ -1,15 +0,0 @@
-:host {
- display: block;
- position: relative;
- text-align: start;
- background-color: var(--wa-color-surface-raised);
- border: var(--wa-border-style) var(--wa-border-width-s) var(--wa-color-surface-border);
- border-radius: var(--wa-border-radius-m);
- padding: 0.5em 0;
- overflow: auto;
- overscroll-behavior: none;
-}
-
-::slotted(wa-divider) {
- --spacing: 0.5em;
-}
diff --git a/packages/webawesome/src/components/menu/menu.test.ts b/packages/webawesome/src/components/menu/menu.test.ts
deleted file mode 100644
index 1d1afb512..000000000
--- a/packages/webawesome/src/components/menu/menu.test.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { expect } from '@open-wc/testing';
-import { sendKeys } from '@web/test-runner-commands';
-import { html } from 'lit';
-import sinon from 'sinon';
-import type { WaSelectEvent } from '../../events/select.js';
-import { clickOnElement } from '../../internal/test.js';
-import { fixtures } from '../../internal/test/fixture.js';
-import type WaMenu from './menu.js';
-
-describe('', () => {
- for (const fixture of fixtures) {
- describe(`with "${fixture.type}" rendering`, () => {
- it('emits wa-select with the correct event detail when clicking an item', async () => {
- const menu = await fixture(html`
-
- Item 1
- Item 2
- Item 3
- Item 4
-
- `);
- const item2 = menu.querySelectorAll('wa-menu-item')[1];
- const selectHandler = sinon.spy((event: WaSelectEvent) => {
- const item = event.detail.item;
- if (item !== item2) {
- expect.fail('Incorrect event detail emitted with wa-select');
- }
- });
-
- menu.addEventListener('wa-select', selectHandler);
- await clickOnElement(item2);
-
- expect(selectHandler).to.have.been.calledOnce;
- });
-
- it('can be selected via keyboard', async () => {
- const menu = await fixture(html`
-
- Item 1
- Item 2
- Item 3
- Item 4
-
- `);
- const [item1, item2] = menu.querySelectorAll('wa-menu-item');
- const selectHandler = sinon.spy((event: WaSelectEvent) => {
- const item = event.detail.item;
- if (item !== item2) {
- expect.fail('Incorrect item selected');
- }
- });
-
- menu.addEventListener('wa-select', selectHandler);
-
- item1.focus();
- await item1.updateComplete;
- await sendKeys({ press: 'ArrowDown' });
- await sendKeys({ press: 'Enter' });
-
- expect(selectHandler).to.have.been.calledOnce;
- });
-
- it('does not select disabled items when clicking', async () => {
- const menu = await fixture(html`
-
- Item 1
- Item 2
- Item 3
- Item 4
-
- `);
- const item2 = menu.querySelectorAll('wa-menu-item')[1];
- const selectHandler = sinon.spy();
-
- menu.addEventListener('wa-select', selectHandler);
-
- await clickOnElement(item2);
-
- expect(selectHandler).to.not.have.been.calledOnce;
- });
-
- it('does not select disabled items when pressing enter', async () => {
- const menu = await fixture(html`
-
- Item 1
- Item 2
- Item 3
- Item 4
-
- `);
- const [item1, item2] = menu.querySelectorAll('wa-menu-item');
- const selectHandler = sinon.spy();
-
- menu.addEventListener('wa-select', selectHandler);
-
- item1.focus();
- await item1.updateComplete;
- await sendKeys({ press: 'ArrowDown' });
- expect(document.activeElement).to.equal(item2);
- await sendKeys({ press: 'Enter' });
- await item2.updateComplete;
-
- expect(selectHandler).to.not.have.been.called;
- });
-
- // @see https://github.com/shoelace-style/shoelace/issues/1596
- it('Should fire "wa-select" when clicking an element within a menu-item', async () => {
- // eslint-disable-next-line
- const selectHandler = sinon.spy(() => {});
-
- const menu: WaMenu = await fixture(html`
-
-
- Menu item
-
-
- `);
-
- menu.addEventListener('wa-select', selectHandler);
- const span = menu.querySelector('span')!;
- await clickOnElement(span);
-
- expect(selectHandler).to.have.been.calledOnce;
- });
- });
- }
-});
diff --git a/packages/webawesome/src/components/menu/menu.ts b/packages/webawesome/src/components/menu/menu.ts
deleted file mode 100644
index f95ad2faa..000000000
--- a/packages/webawesome/src/components/menu/menu.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-import { html } from 'lit';
-import { customElement, property, query } from 'lit/decorators.js';
-import { WaSelectEvent } from '../../events/select.js';
-import WebAwesomeElement from '../../internal/webawesome-element.js';
-import sizeStyles from '../../styles/utilities/size.css';
-import '../menu-item/menu-item.js';
-import type WaMenuItem from '../menu-item/menu-item.js';
-import styles from './menu.css';
-
-export interface MenuSelectEventDetail {
- item: WaMenuItem;
-}
-
-/**
- * @summary Menus provide a list of options for the user to choose from.
- * @documentation https://backers.webawesome.com/docs/components/menu
- * @status stable
- * @since 2.0
- *
- * @dependency wa-menu-item
- *
- * @slot - The menu's content, including menu items, menu labels, and dividers.
- *
- * @event {{ item: WaMenuItem }} wa-select - Emitted when a menu item is selected.
- */
-@customElement('wa-menu')
-export default class WaMenu extends WebAwesomeElement {
- static css = [sizeStyles, styles];
-
- /** The component's size. */
- @property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
-
- @query('slot') defaultSlot: HTMLSlotElement;
-
- connectedCallback() {
- super.connectedCallback();
- this.setAttribute('role', 'menu');
- }
-
- private handleClick(event: MouseEvent) {
- const menuItemTypes = ['menuitem', 'menuitemcheckbox'];
-
- const target = event.composedPath().find((el: Element) => menuItemTypes.includes(el?.getAttribute?.('role') || ''));
-
- if (!target) return;
-
- // This isn't true. But we use it for TypeScript checks below.
- const item = target as WaMenuItem;
-
- if (item.type === 'checkbox') {
- item.checked = !item.checked;
- }
-
- this.dispatchEvent(new WaSelectEvent({ item }));
- }
-
- private handleKeyDown(event: KeyboardEvent) {
- // Make a selection when pressing enter or space
- if (event.key === 'Enter' || event.key === ' ') {
- const item = this.getCurrentItem();
- event.preventDefault();
- event.stopPropagation();
-
- // Simulate a click to support @click handlers on menu items that also work with the keyboard
- item?.click();
- }
-
- // Move the selection when pressing down or up
- else if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
- const items = this.getAllItems();
- const activeItem = this.getCurrentItem();
- let index = activeItem ? items.indexOf(activeItem) : 0;
-
- if (items.length > 0) {
- event.preventDefault();
- event.stopPropagation();
-
- if (event.key === 'ArrowDown') {
- index++;
- } else if (event.key === 'ArrowUp') {
- index--;
- } else if (event.key === 'Home') {
- index = 0;
- } else if (event.key === 'End') {
- index = items.length - 1;
- }
-
- if (index < 0) {
- index = items.length - 1;
- }
- if (index > items.length - 1) {
- index = 0;
- }
-
- this.setCurrentItem(items[index]);
- items[index].focus();
- }
- }
- }
-
- private handleMouseDown(event: MouseEvent) {
- const target = event.target as HTMLElement;
-
- if (this.isMenuItem(target)) {
- this.setCurrentItem(target as WaMenuItem);
- }
- }
-
- private handleSlotChange() {
- const items = this.getAllItems();
-
- // Reset the roving tab index when the slotted items change
- if (items.length > 0) {
- this.setCurrentItem(items[0]);
- }
- }
-
- private isMenuItem(item: HTMLElement) {
- return (
- item.tagName.toLowerCase() === 'wa-menu-item' ||
- ['menuitem', 'menuitemcheckbox', 'menuitemradio'].includes(item.getAttribute('role') ?? '')
- );
- }
-
- /** @internal Gets all slotted menu items, ignoring dividers, headers, and other elements. */
- getAllItems() {
- return [...this.defaultSlot.assignedElements({ flatten: true })].filter((el: HTMLElement) => {
- if (el.inert || !this.isMenuItem(el)) {
- return false;
- }
- return true;
- }) as WaMenuItem[];
- }
-
- /**
- * @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.
- */
- getCurrentItem() {
- return this.getAllItems().find(i => i.getAttribute('tabindex') === '0');
- }
-
- /**
- * @internal Sets the current menu item to the specified element. This sets `tabindex="0"` on the target element and
- * `tabindex="-1"` to all other items. This method must be called prior to setting focus on a menu item.
- */
- setCurrentItem(item: WaMenuItem) {
- const items = this.getAllItems();
-
- // Update tab indexes
- items.forEach(i => {
- i.setAttribute('tabindex', i === item ? '0' : '-1');
- });
- }
-
- render() {
- return html`
-
- `;
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'wa-menu': WaMenu;
- }
-}
diff --git a/packages/webawesome/src/events/events.ts b/packages/webawesome/src/events/events.ts
index e9ef29957..5e103616d 100644
--- a/packages/webawesome/src/events/events.ts
+++ b/packages/webawesome/src/events/events.ts
@@ -19,7 +19,6 @@ export type { WaMutationEvent } from './mutation.js';
export type { WaRemoveEvent } from './remove.js';
export type { WaRepositionEvent } from './reposition.js';
export type { WaResizeEvent } from './resize.js';
-export type { WaSelectEvent } from './select.js';
export type { WaSelectionChangeEvent } from './selection-change.js';
export type { WaShowEvent } from './show.js';
export type { WaSlideChangeEvent } from './slide-change.js';
diff --git a/packages/webawesome/src/events/select.ts b/packages/webawesome/src/events/select.ts
deleted file mode 100644
index 667263445..000000000
--- a/packages/webawesome/src/events/select.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type WaMenuItem from '../components/menu-item/menu-item.js';
-
-export class WaSelectEvent extends Event {
- readonly detail;
-
- constructor(detail: WaSelectEventDetail) {
- super('wa-select', { bubbles: true, cancelable: false, composed: true });
- this.detail = detail;
- }
-}
-
-interface WaSelectEventDetail {
- item: WaMenuItem;
-}
-
-declare global {
- interface GlobalEventHandlersEventMap {
- 'wa-select': WaSelectEvent;
- }
-}