diff --git a/packages/action-menu/test/action-menu-sync.test.ts b/packages/action-menu/test/action-menu-sync.test.ts index 9f9303113a..50c3b70ed9 100644 --- a/packages/action-menu/test/action-menu-sync.test.ts +++ b/packages/action-menu/test/action-menu-sync.test.ts @@ -11,206 +11,11 @@ governing permissions and limitations under the License. */ import '@spectrum-web-components/action-menu/sync/sp-action-menu.js'; -import { ActionMenu } from '@spectrum-web-components/action-menu'; import '@spectrum-web-components/icons-workflow/icons/sp-icon-settings.js'; import '@spectrum-web-components/menu/sp-menu.js'; import '@spectrum-web-components/menu/sp-menu-item.js'; import '@spectrum-web-components/menu/sp-menu-divider.js'; -import { - elementUpdated, - expect, - fixture, - html, - oneEvent, -} from '@open-wc/testing'; -import { testForLitDevWarnings } from '../../../test/testing-helpers'; -const deprecatedActionMenuFixture = async (): Promise => - await fixture( - html` - - - Deselect - Select Inverse - Feather... - Select and Mask... - - Save Selection - Make Work Path - - - ` - ); +import { testActionMenu } from './'; -const actionMenuFixture = async (): Promise => - await fixture( - html` - - Deselect - Select Inverse - Feather... - Select and Mask... - - Save Selection - Make Work Path - - ` - ); - -describe('Action menu', () => { - testForLitDevWarnings(async () => await actionMenuFixture()); - it('loads', async () => { - const el = await actionMenuFixture(); - await elementUpdated(el); - - expect(el).to.not.be.undefined; - - await expect(el).to.be.accessible(); - }); - it('loads - [slot="label"]', async () => { - const el = await fixture( - html` - - More Actions - Deselect - Select Inverse - Feather... - Select and Mask... - - Save Selection - Make Work Path - - ` - ); - - await elementUpdated(el); - - await expect(el).to.be.accessible(); - }); - it('loads - [custom icon]', async () => { - const el = await fixture( - html` - - - Deselect - Select Inverse - Feather... - Select and Mask... - - Save Selection - Make Work Path - - ` - ); - - await elementUpdated(el); - - await expect(el).to.be.accessible(); - }); - it('stays `quiet`', async () => { - const el = await actionMenuFixture(); - await elementUpdated(el); - - expect(el.quiet).to.be.true; - - el.quiet = false; - await elementUpdated(el); - - expect(el.quiet).to.be.true; - }); - it('stay `valid`', async () => { - const el = await actionMenuFixture(); - - await elementUpdated(el); - - expect(el.invalid).to.be.false; - - el.invalid = true; - await elementUpdated(el); - - expect(el.invalid).to.be.false; - }); - it('focus()', async () => { - const el = await actionMenuFixture(); - - await elementUpdated(el); - - el.focus(); - - expect(document.activeElement).to.equal(el); - expect(el.shadowRoot.activeElement).to.equal(el.focusElement); - - const opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - - expect(document.activeElement).to.not.equal(el); - - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(document.activeElement).to.equal(el); - expect(el.shadowRoot.activeElement).to.equal(el.focusElement); - }); - it('opens unmeasured', async () => { - const el = await actionMenuFixture(); - - await elementUpdated(el); - const button = el.button as HTMLButtonElement; - - button.click(); - await elementUpdated(el); - expect(el.open).to.be.true; - }); - it('opens unmeasured with deprecated syntax', async () => { - const el = await deprecatedActionMenuFixture(); - - await elementUpdated(el); - const button = el.button as HTMLButtonElement; - - button.click(); - await elementUpdated(el); - expect(el.open).to.be.true; - }); - it('toggles open/close multiple time', async () => { - const el = await actionMenuFixture(); - - await elementUpdated(el); - let items = el.querySelectorAll('sp-menu-item'); - const count = items.length; - expect(items.length).to.equal(count); - - let opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - - expect(el.open).to.be.true; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(0); - - let closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(count); - - opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - - expect(el.open).to.be.true; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(0); - - closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(count); - }); -}); +testActionMenu('sync'); diff --git a/packages/action-menu/test/action-menu.test.ts b/packages/action-menu/test/action-menu.test.ts index fbca06ab01..638b660289 100644 --- a/packages/action-menu/test/action-menu.test.ts +++ b/packages/action-menu/test/action-menu.test.ts @@ -11,324 +11,11 @@ governing permissions and limitations under the License. */ import '@spectrum-web-components/action-menu/sp-action-menu.js'; -import { ActionMenu } from '@spectrum-web-components/action-menu'; import '@spectrum-web-components/icons-workflow/icons/sp-icon-settings.js'; import '@spectrum-web-components/menu/sp-menu.js'; import '@spectrum-web-components/menu/sp-menu-item.js'; import '@spectrum-web-components/menu/sp-menu-divider.js'; -import { - elementUpdated, - expect, - fixture, - html, - oneEvent, -} from '@open-wc/testing'; -import { testForLitDevWarnings } from '../../../test/testing-helpers'; -import type { MenuItem } from '@spectrum-web-components/menu/src/MenuItem.js'; -import type { Menu } from '@spectrum-web-components/menu'; -const deprecatedActionMenuFixture = async (): Promise => - await fixture( - html` - - - Deselect - Select Inverse - Feather... - Select and Mask... - - Save Selection - Make Work Path - - - ` - ); +import { testActionMenu } from './'; -const actionMenuFixture = async (): Promise => - await fixture( - html` - - Deselect - Select Inverse - Feather... - Select and Mask... - - Save Selection - Make Work Path - - ` - ); - -const actionSubmenuFixture = async (): Promise => - await fixture( - html` - - One - - Two - - - B should be selected - - A - - B - - C - - - - ` - ); - -describe('Action menu', () => { - testForLitDevWarnings(async () => await actionMenuFixture()); - it('loads', async () => { - const el = await actionMenuFixture(); - await elementUpdated(el); - - expect(el).to.not.be.undefined; - - await expect(el).to.be.accessible(); - }); - it('loads - [slot="label"]', async () => { - const el = await fixture( - html` - - More Actions - Deselect - Select Inverse - Feather... - Select and Mask... - - Save Selection - Make Work Path - - ` - ); - - await elementUpdated(el); - - await expect(el).to.be.accessible(); - }); - it('loads - [custom icon]', async () => { - const el = await fixture( - html` - - - Deselect - Select Inverse - Feather... - Select and Mask... - - Save Selection - Make Work Path - - ` - ); - - await elementUpdated(el); - - await expect(el).to.be.accessible(); - }); - it('stays `quiet`', async () => { - const el = await actionMenuFixture(); - await elementUpdated(el); - - expect(el.quiet).to.be.true; - - el.quiet = false; - await elementUpdated(el); - - expect(el.quiet).to.be.true; - }); - it('stay `valid`', async () => { - const el = await actionMenuFixture(); - - await elementUpdated(el); - - expect(el.invalid).to.be.false; - - el.invalid = true; - await elementUpdated(el); - - expect(el.invalid).to.be.false; - }); - it('focus()', async () => { - const el = await actionMenuFixture(); - - await elementUpdated(el); - - el.focus(); - - expect(document.activeElement).to.equal(el); - expect(el.shadowRoot.activeElement).to.equal(el.focusElement); - - const opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - - expect(document.activeElement).to.not.equal(el); - - const closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(document.activeElement).to.equal(el); - expect(el.shadowRoot.activeElement).to.equal(el.focusElement); - }); - it('opens unmeasured', async () => { - const el = await actionMenuFixture(); - - await elementUpdated(el); - const button = el.button as HTMLButtonElement; - - button.click(); - await elementUpdated(el); - expect(el.open).to.be.true; - }); - it('opens unmeasured with deprecated syntax', async () => { - const el = await deprecatedActionMenuFixture(); - - await elementUpdated(el); - const button = el.button as HTMLButtonElement; - - button.click(); - await elementUpdated(el); - expect(el.open).to.be.true; - }); - it('toggles open/close multiple time', async () => { - const el = await actionMenuFixture(); - - await elementUpdated(el); - let items = el.querySelectorAll('sp-menu-item'); - const count = items.length; - expect(items.length).to.equal(count); - - let opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - - expect(el.open).to.be.true; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(0); - - let closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(count); - - opened = oneEvent(el, 'sp-opened'); - el.open = true; - await opened; - - expect(el.open).to.be.true; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(0); - - closed = oneEvent(el, 'sp-closed'); - el.open = false; - await closed; - - expect(el.open).to.be.false; - items = el.querySelectorAll('sp-menu-item'); - expect(items.length).to.equal(count); - }); - it('allows submenu items to be selected', async () => { - const root = await actionSubmenuFixture(); - const menuItem = root.querySelector('#item-with-submenu') as Menu; - const submenu = menuItem.querySelector( - 'sp-menu[slot="submenu"]' - ) as Menu; - const selectedItem = submenu.querySelector( - '#sub-selected-item' - ) as MenuItem; - - expect(selectedItem.selected, 'item should be initially selected').to.be - .true; - - let opened = oneEvent(root, 'sp-opened'); - root.click(); - await opened; - expect(root.open).to.be.true; - - opened = oneEvent(menuItem, 'sp-opened'); - menuItem.dispatchEvent( - new PointerEvent('pointerenter', { bubbles: true }) - ); - await opened; - const overlays = document.querySelectorAll('active-overlay'); - expect(overlays.length).to.equal(2); - - await elementUpdated(submenu); - expect( - selectedItem.selected, - 'initially selected item should maintain selection' - ).to.be.true; - }); - it('allows top-level selection state to change', async () => { - const root = await actionSubmenuFixture(); - const unselectedItem = root.querySelector('sp-menu-item') as MenuItem; - const selectedItem = root.querySelector( - '#root-selected-item' - ) as MenuItem; - let selected = true; - - selectedItem.addEventListener('click', () => { - selected = !selected; - selectedItem.selected = selected; - }); - - expect(unselectedItem.textContent).to.include('One'); - expect(unselectedItem.selected).to.be.false; - expect(selectedItem.textContent).to.include('Two'); - expect(selectedItem.selected).to.be.true; - - let opened = oneEvent(root, 'sp-opened'); - root.click(); - await opened; - - // close by clicking selected - // (with event listener: should set selected = false) - let closed = oneEvent(root, 'sp-closed'); - selectedItem.click(); - await closed; - - opened = oneEvent(root, 'sp-opened'); - root.click(); - await opened; - - // close by clicking unselected - // (no event listener: should remain selected = false) - closed = oneEvent(root, 'sp-closed'); - unselectedItem.click(); - await closed; - - opened = oneEvent(root, 'sp-opened'); - root.click(); - await opened; - - expect(unselectedItem.textContent).to.include('One'); - expect(unselectedItem.selected).to.be.false; - expect(selectedItem.textContent).to.include('Two'); - expect(selectedItem.selected).to.be.false; - - // close by clicking selected - // (with event listener: should set selected = false) - closed = oneEvent(root, 'sp-closed'); - selectedItem.click(); - await closed; - - opened = oneEvent(root, 'sp-opened'); - root.click(); - await opened; - - expect(unselectedItem.textContent).to.include('One'); - expect(unselectedItem.selected).to.be.false; - expect(selectedItem.textContent).to.include('Two'); - expect(selectedItem.selected).to.be.true; - }); -}); +testActionMenu('async'); diff --git a/packages/action-menu/test/index.ts b/packages/action-menu/test/index.ts new file mode 100644 index 0000000000..219e88e8ec --- /dev/null +++ b/packages/action-menu/test/index.ts @@ -0,0 +1,423 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { + elementUpdated, + expect, + fixture, + html, + oneEvent, +} from '@open-wc/testing'; +import { testForLitDevWarnings } from '../../../test/testing-helpers'; + +import { spy } from 'sinon'; + +import type { ActionMenu } from '@spectrum-web-components/action-menu'; +import type { Menu, MenuItem } from '@spectrum-web-components/menu'; + +const deprecatedActionMenuFixture = async (): Promise => + await fixture( + html` + + + Deselect + Select Inverse + Feather... + Select and Mask... + + Save Selection + Make Work Path + + + ` + ); + +const actionMenuFixture = async (): Promise => + await fixture( + html` + + Deselect + Select Inverse + Feather... + Select and Mask... + + Save Selection + Make Work Path + + ` + ); + +const actionSubmenuFixture = async (): Promise => + await fixture( + html` + + One + + Two + + + B should be selected + + A + + B + + C + + + + ` + ); + +export const testActionMenu = (mode: 'sync' | 'async'): void => { + describe(`Action menu: ${mode}`, () => { + testForLitDevWarnings(async () => await actionMenuFixture()); + it('loads', async () => { + const el = await actionMenuFixture(); + await elementUpdated(el); + + expect(el).to.not.be.undefined; + + await expect(el).to.be.accessible(); + }); + it('loads - [slot="label"]', async () => { + const el = await fixture( + html` + + More Actions + Deselect + Select Inverse + Feather... + Select and Mask... + + Save Selection + Make Work Path + + ` + ); + + await elementUpdated(el); + + await expect(el).to.be.accessible(); + }); + it('loads - [custom icon]', async () => { + const el = await fixture( + html` + + + Deselect + Select Inverse + Feather... + Select and Mask... + + Save Selection + Make Work Path + + ` + ); + + await elementUpdated(el); + + await expect(el).to.be.accessible(); + }); + it('dispatches change events, no [href]', async () => { + const changeSpy = spy(); + + const el = await fixture( + html` + changeSpy()} + > + + Deselect + Select Inverse + Feather... + Select and Mask... + + Save Selection + Make Work Path + + ` + ); + + expect(changeSpy.callCount).to.equal(0); + expect(el.open).to.be.false; + + const menuItem2 = el.querySelector( + 'sp-menu-item:nth-child(2)' + ) as MenuItem; + + const opened = oneEvent(el, 'sp-opened'); + el.click(); + await opened; + await elementUpdated(el); + + expect(el.open).to.be.true; + + const closed = oneEvent(el, 'sp-closed'); + menuItem2.click(); + await closed; + await elementUpdated(el); + + expect(el.open).to.be.false; + expect(changeSpy.callCount).to.equal(1); + }); + it('closes when Menu Item has [href]', async () => { + const changeSpy = spy(); + + const el = await fixture( + html` + changeSpy()} + > + + Deselect + Select Inverse + Feather... + Select and Mask... + + Save Selection + + Make Work Path + + + ` + ); + + expect(changeSpy.callCount).to.equal(0); + expect(el.open).to.be.false; + + const menuItem2 = el.querySelector( + 'sp-menu-item:nth-child(2)' + ) as MenuItem; + + const opened = oneEvent(el, 'sp-opened'); + el.click(); + await opened; + await elementUpdated(el); + + expect(el.open).to.be.true; + + const closed = oneEvent(el, 'sp-closed'); + menuItem2.click(); + await closed; + await elementUpdated(el); + + expect(el.open).to.be.false; + expect(changeSpy.callCount).to.equal(0); + }); + it('stays `quiet`', async () => { + const el = await actionMenuFixture(); + await elementUpdated(el); + + expect(el.quiet).to.be.true; + + el.quiet = false; + await elementUpdated(el); + + expect(el.quiet).to.be.true; + }); + it('stay `valid`', async () => { + const el = await actionMenuFixture(); + + await elementUpdated(el); + + expect(el.invalid).to.be.false; + + el.invalid = true; + await elementUpdated(el); + + expect(el.invalid).to.be.false; + }); + it('focus()', async () => { + const el = await actionMenuFixture(); + + await elementUpdated(el); + + el.focus(); + + expect(document.activeElement).to.equal(el); + expect(el.shadowRoot.activeElement).to.equal(el.focusElement); + + const opened = oneEvent(el, 'sp-opened'); + el.open = true; + await opened; + + expect(document.activeElement).to.not.equal(el); + + const closed = oneEvent(el, 'sp-closed'); + el.open = false; + await closed; + + expect(document.activeElement).to.equal(el); + expect(el.shadowRoot.activeElement).to.equal(el.focusElement); + }); + it('opens unmeasured', async () => { + const el = await actionMenuFixture(); + + await elementUpdated(el); + const button = el.button as HTMLButtonElement; + + button.click(); + await elementUpdated(el); + expect(el.open).to.be.true; + }); + it('opens unmeasured with deprecated syntax', async () => { + const el = await deprecatedActionMenuFixture(); + + await elementUpdated(el); + const button = el.button as HTMLButtonElement; + + button.click(); + await elementUpdated(el); + expect(el.open).to.be.true; + }); + it('toggles open/close multiple time', async () => { + const el = await actionMenuFixture(); + + await elementUpdated(el); + let items = el.querySelectorAll('sp-menu-item'); + const count = items.length; + expect(items.length).to.equal(count); + + let opened = oneEvent(el, 'sp-opened'); + el.open = true; + await opened; + + expect(el.open).to.be.true; + items = el.querySelectorAll('sp-menu-item'); + expect(items.length).to.equal(0); + + let closed = oneEvent(el, 'sp-closed'); + el.open = false; + await closed; + + expect(el.open).to.be.false; + items = el.querySelectorAll('sp-menu-item'); + expect(items.length).to.equal(count); + + opened = oneEvent(el, 'sp-opened'); + el.open = true; + await opened; + + expect(el.open).to.be.true; + items = el.querySelectorAll('sp-menu-item'); + expect(items.length).to.equal(0); + + closed = oneEvent(el, 'sp-closed'); + el.open = false; + await closed; + + expect(el.open).to.be.false; + items = el.querySelectorAll('sp-menu-item'); + expect(items.length).to.equal(count); + }); + it('allows submenu items to be selected', async () => { + const root = await actionSubmenuFixture(); + const menuItem = root.querySelector('#item-with-submenu') as Menu; + const submenu = menuItem.querySelector( + 'sp-menu[slot="submenu"]' + ) as Menu; + const selectedItem = submenu.querySelector( + '#sub-selected-item' + ) as MenuItem; + + expect(selectedItem.selected, 'item should be initially selected') + .to.be.true; + + let opened = oneEvent(root, 'sp-opened'); + root.click(); + await opened; + expect(root.open).to.be.true; + + opened = oneEvent(menuItem, 'sp-opened'); + menuItem.dispatchEvent( + new PointerEvent('pointerenter', { bubbles: true }) + ); + await opened; + const overlays = document.querySelectorAll('active-overlay'); + expect(overlays.length).to.equal(2); + + await elementUpdated(submenu); + expect( + selectedItem.selected, + 'initially selected item should maintain selection' + ).to.be.true; + }); + it('allows top-level selection state to change', async () => { + const root = await actionSubmenuFixture(); + const unselectedItem = root.querySelector( + 'sp-menu-item' + ) as MenuItem; + const selectedItem = root.querySelector( + '#root-selected-item' + ) as MenuItem; + let selected = true; + + selectedItem.addEventListener('click', () => { + selected = !selected; + selectedItem.selected = selected; + }); + + expect(unselectedItem.textContent).to.include('One'); + expect(unselectedItem.selected).to.be.false; + expect(selectedItem.textContent).to.include('Two'); + expect(selectedItem.selected).to.be.true; + + let opened = oneEvent(root, 'sp-opened'); + root.click(); + await opened; + + // close by clicking selected + // (with event listener: should set selected = false) + let closed = oneEvent(root, 'sp-closed'); + selectedItem.click(); + await closed; + + opened = oneEvent(root, 'sp-opened'); + root.click(); + await opened; + + // close by clicking unselected + // (no event listener: should remain selected = false) + closed = oneEvent(root, 'sp-closed'); + unselectedItem.click(); + await closed; + + opened = oneEvent(root, 'sp-opened'); + root.click(); + await opened; + + expect(unselectedItem.textContent).to.include('One'); + expect(unselectedItem.selected).to.be.false; + expect(selectedItem.textContent).to.include('Two'); + expect(selectedItem.selected).to.be.false; + + // close by clicking selected + // (with event listener: should set selected = false) + closed = oneEvent(root, 'sp-closed'); + selectedItem.click(); + await closed; + + opened = oneEvent(root, 'sp-opened'); + root.click(); + await opened; + + expect(unselectedItem.textContent).to.include('One'); + expect(unselectedItem.selected).to.be.false; + expect(selectedItem.textContent).to.include('Two'); + expect(selectedItem.selected).to.be.true; + }); + }); +}; diff --git a/packages/menu/src/Menu.ts b/packages/menu/src/Menu.ts index 4b187f9708..b25e290d89 100644 --- a/packages/menu/src/Menu.ts +++ b/packages/menu/src/Menu.ts @@ -287,6 +287,14 @@ export class Menu extends SpectrumElement { return el.getAttribute('role') === this.childRole; }) as MenuItem; if (target?.href && target.href.length) { + // This event will NOT ALLOW CANCELATION as link action + // cancelation should occur on the `` itself. + this.dispatchEvent( + new Event('change', { + bubbles: true, + composed: true, + }) + ); return; } else if ( target?.menuData.selectionRoot === this && diff --git a/packages/picker/src/Picker.ts b/packages/picker/src/Picker.ts index 3e8b2ab502..3d1b3b8dff 100644 --- a/packages/picker/src/Picker.ts +++ b/packages/picker/src/Picker.ts @@ -195,10 +195,16 @@ export class PickerBase extends SizedMixin(Focusable) { } public handleChange(event: Event): void { - event.stopPropagation(); const target = event.target as Menu; const [selected] = target.selectedItems; - this.setValueFromItem(selected, event); + if (event.cancelable) { + event.stopPropagation(); + this.setValueFromItem(selected, event); + } else { + // Non-cancelable "change" events announce a selection with no value + // change that should close the Picker element. + this.open = false; + } } protected onKeydown = (event: KeyboardEvent): void => {