diff --git a/.changeset/vast-onions-return.md b/.changeset/vast-onions-return.md new file mode 100644 index 0000000000..242cccc707 --- /dev/null +++ b/.changeset/vast-onions-return.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-components': patch +--- + +Updated `` to emit events: postBeforeShow, postShow, postHide, postBeforeToggle, and postToggle. Updated `` and `` to consume the `onPostToggle` event emitted by ``. diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index d2cca62794..3dd945df8d 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -806,6 +806,10 @@ declare global { new (): HTMLPostPopoverElement; }; interface HTMLPostPopovercontainerElementEventMap { + "postBeforeShow": { first?: boolean }; + "postShow": { first?: boolean }; + "postHide": any; + "postBeforeToggle": { willOpen: boolean; first?: boolean }; "postToggle": { isOpen: boolean; first?: boolean }; } interface HTMLPostPopovercontainerElement extends Components.PostPopovercontainer, HTMLStencilElement { @@ -1259,6 +1263,22 @@ declare namespace LocalJSX { * @default false */ "manualClose"?: boolean; + /** + * Fires whenever the popovercontainer is about to be shown, passing in event.detail a `first` boolean, which is true if it is to be shown for the first time. + */ + "onPostBeforeShow"?: (event: PostPopovercontainerCustomEvent<{ first?: boolean }>) => void; + /** + * Fires whenever the popovercontainer is about to be shown or hidden, passing in event.detail an object containing two booleans: `willOpen`, which is true if the popovercontainer is about to be opened and false if it is about to be closed, and `first`, which is true if it is to be opened for the first time. + */ + "onPostBeforeToggle"?: (event: PostPopovercontainerCustomEvent<{ willOpen: boolean; first?: boolean }>) => void; + /** + * Fires whenever the popovercontainer is hidden + */ + "onPostHide"?: (event: PostPopovercontainerCustomEvent) => void; + /** + * Fires whenever the popovercontainer is shown, passing in event.detail a `first` boolean, which is true if it is shown for the first time. + */ + "onPostShow"?: (event: PostPopovercontainerCustomEvent<{ first?: boolean }>) => void; /** * Fires whenever the popovercontainer gets shown or hidden, passing in event.detail an object containing two booleans: `isOpen`, which is true if the popovercontainer was opened and false if it was closed, and `first`, which is true if it was opened for the first time. */ diff --git a/packages/components/src/components/post-menu/post-menu.tsx b/packages/components/src/components/post-menu/post-menu.tsx index e8763c616d..4465d8e50e 100644 --- a/packages/components/src/components/post-menu/post-menu.tsx +++ b/packages/components/src/components/post-menu/post-menu.tsx @@ -14,7 +14,7 @@ import { Placement } from '@floating-ui/dom'; import { PLACEMENT_TYPES } from '@/types'; import { version } from '@root/package.json'; import { getFocusableChildren } from '@/utils/get-focusable-children'; -import { getRoot, checkEmptyOrOneOf, EventFrom } from '@/utils'; +import { getRoot, checkEmptyOrOneOf } from '@/utils'; /** * @part menu - The container element that holds the list of menu items. @@ -83,14 +83,10 @@ export class PostMenu { disconnectedCallback() { this.host.removeEventListener('keydown', this.handleKeyDown); this.host.removeEventListener('click', this.handleClick); - this.popoverRef?.removeEventListener('postToggle', this.handlePostToggle); } componentDidLoad() { this.validatePlacement(); - if (this.popoverRef) { - this.popoverRef.addEventListener('postToggle', this.handlePostToggle); - } } /** @@ -131,7 +127,7 @@ export class PostMenu { } } - private handleKeyDown = (e: KeyboardEvent) => { + private readonly handleKeyDown = (e: KeyboardEvent) => { e.stopPropagation(); if (e.key === this.KEYCODES.ESCAPE) { @@ -144,43 +140,36 @@ export class PostMenu { } }; - @EventFrom('post-popovercontainer') - private handlePostToggle = (event: CustomEvent<{ isOpen: boolean; first?: boolean }>) => { - this.isVisible = event.detail.isOpen; - this.toggleMenu.emit(this.isVisible); - - requestAnimationFrame(() => { - if (this.isVisible) { - this.lastFocusedElement = this.root?.activeElement as HTMLElement; - - const menuItems = this.getSlottedItems(); - - if (event.detail.first) { - if (menuItems.length > 0) { - // Add role="menu" to the popovercontainer - this.host.setAttribute('role', 'menu'); - - // Add role="menuitem" to the focusable elements - menuItems.forEach(item => { - item.setAttribute('role', 'menuitem'); - }); - - // Add aria-label to the menu - if (this.label) this.host.setAttribute('aria-label', this.label); - } - } - - (menuItems[0] as HTMLElement).focus(); - } else if (this.lastFocusedElement) { - setTimeout(() => { - // This timeout is added for NVDA to announce the menu as collapsed - this.lastFocusedElement.focus(); - }, 0); + private readonly handlePostToggled = ( + event: CustomEvent<{ isOpen: boolean; first?: boolean }>, + ) => { + this.isVisible = event.detail.isOpen; + this.toggleMenu.emit(this.isVisible); + + if (this.isVisible) { + this.lastFocusedElement = this.root?.activeElement as HTMLElement; + + const menuItems = this.getSlottedItems(); + if (menuItems.length > 0) { + (menuItems[0] as HTMLElement).focus(); + + // Only for the first open + if (event.detail.first) { + // Add "menu" and "menuitem" aria roles and aria-label + this.host.setAttribute('role', 'menu'); + menuItems.forEach(item => { + item.setAttribute('role', 'menuitem'); + }); + + if (this.label) this.host.setAttribute('aria-label', this.label); } - }); - }; + } + } else if (this.lastFocusedElement) { + this.lastFocusedElement.focus(); + } + }; - private handleClick = (e: MouseEvent) => { + private readonly handleClick = (e: MouseEvent) => { const target = e.target as HTMLElement; if (['BUTTON', 'A', 'INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName)) { this.toggle(this.host); @@ -245,7 +234,11 @@ export class PostMenu { render() { return ( - (this.popoverRef = e)}> + (this.popoverRef = e)} + >
diff --git a/packages/components/src/components/post-popovercontainer/post-popovercontainer.tsx b/packages/components/src/components/post-popovercontainer/post-popovercontainer.tsx index 36cef649b7..4fe16330c2 100644 --- a/packages/components/src/components/post-popovercontainer/post-popovercontainer.tsx +++ b/packages/components/src/components/post-popovercontainer/post-popovercontainer.tsx @@ -75,7 +75,27 @@ export class PostPopovercontainer { private eventTarget: Element; private clearAutoUpdate: () => void; private toggleTimeoutId: number; - private firstOpen: boolean = true; + private first: boolean = true; + + /** + * Fires whenever the popovercontainer is about to be shown, passing in event.detail a `first` boolean, which is true if it is to be shown for the first time. + */ + @Event() postBeforeShow: EventEmitter<{ first?: boolean }>; + + /** + * Fires whenever the popovercontainer is shown, passing in event.detail a `first` boolean, which is true if it is shown for the first time. + */ + @Event() postShow: EventEmitter<{ first?: boolean }>; + + /** + * Fires whenever the popovercontainer is hidden. + */ + @Event() postHide: EventEmitter; + + /** + * Fires whenever the popovercontainer is about to be shown or hidden, passing in event.detail an object containing two booleans: `willOpen`, which is true if the popovercontainer is about to be opened and false if it is about to be closed, and `first`, which is true if it is to be opened for the first time. + */ + @Event() postBeforeToggle: EventEmitter<{ willOpen: boolean; first?: boolean }>; /** * Fires whenever the popovercontainer gets shown or hidden, passing in event.detail an object containing two booleans: `isOpen`, which is true if the popovercontainer was opened and false if it was closed, and `first`, which is true if it was opened for the first time. @@ -161,7 +181,6 @@ export class PostPopovercontainer { @Method() async show(target: HTMLElement) { if (this.toggleTimeoutId) return; - this.eventTarget = target; this.calculatePosition(); this.host.showPopover(); @@ -175,6 +194,7 @@ export class PostPopovercontainer { if (!this.toggleTimeoutId) { this.eventTarget = null; this.host.hidePopover(); + this.postHide.emit(); } } @@ -206,27 +226,48 @@ export class PostPopovercontainer { const isOpen = e.newState === 'open'; if (isOpen) { - const content = this.host.querySelector('.popover-content'); - this.startAutoupdates(); - if (content && this.animation === 'pop-in') { - popIn(content); - } + this.handleOpen(); + } else { + this.handleClose(); + } + } + + private handleOpen() { + const content = this.host.querySelector('.popover-content'); + this.startAutoupdates(); - if (this.safeSpace) - window.addEventListener('mousemove', this.mouseTrackingHandler.bind(this)); + if (content) { + const animation = popIn(content); - // Emit event with `first` flag only true on the first open - if (this.firstOpen) { - this.postToggle.emit({ isOpen, first: this.firstOpen }); - this.firstOpen = false; - return; + if (animation?.playState === 'running') { + this.postBeforeToggle.emit({ willOpen: true, first: this.first }); + this.postBeforeShow.emit({ first: this.first }); } - } else { - if (typeof this.clearAutoUpdate === 'function') this.clearAutoUpdate(); - if (this.safeSpace) - window.removeEventListener('mousemove', this.mouseTrackingHandler.bind(this)); + + animation?.finished.then(() => { + this.postToggle.emit({ isOpen: true, first: this.first }); + this.postShow.emit({ first: this.first }); + + if (this.first) this.first = false; + }); + } + + if (this.safeSpace) { + window.addEventListener('mousemove', this.mouseTrackingHandler.bind(this)); } - this.postToggle.emit({ isOpen: isOpen, first: false }); + } + + private handleClose() { + if (typeof this.clearAutoUpdate === 'function') { + this.clearAutoUpdate(); + } + + if (this.safeSpace) { + window.removeEventListener('mousemove', this.mouseTrackingHandler.bind(this)); + } + + this.postToggle.emit({ isOpen: false, first: this.first }); + this.postHide.emit({ first: this.first }); } /** diff --git a/packages/components/src/components/post-popovercontainer/readme.md b/packages/components/src/components/post-popovercontainer/readme.md index 2fa98dec60..6a2d696ef6 100644 --- a/packages/components/src/components/post-popovercontainer/readme.md +++ b/packages/components/src/components/post-popovercontainer/readme.md @@ -17,9 +17,13 @@ ## Events -| Event | Description | Type | -| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | -| `postToggle` | Fires whenever the popovercontainer gets shown or hidden, passing in event.detail an object containing two booleans: `isOpen`, which is true if the popovercontainer was opened and false if it was closed, and `first`, which is true if it was opened for the first time. | `CustomEvent<{ isOpen: boolean; first?: boolean; }>` | +| Event | Description | Type | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | +| `postBeforeShow` | Fires whenever the popovercontainer is about to be shown, passing in event.detail a `first` boolean, which is true if it is to be shown for the first time. | `CustomEvent<{ first?: boolean; }>` | +| `postBeforeToggle` | Fires whenever the popovercontainer is about to be shown or hidden, passing in event.detail an object containing two booleans: `willOpen`, which is true if the popovercontainer is about to be opened and false if it is about to be closed, and `first`, which is true if it is to be opened for the first time. | `CustomEvent<{ willOpen: boolean; first?: boolean; }>` | +| `postHide` | Fires whenever the popovercontainer is hidden | `CustomEvent` | +| `postShow` | Fires whenever the popovercontainer is shown, passing in event.detail a `first` boolean, which is true if it is shown for the first time. | `CustomEvent<{ first?: boolean; }>` | +| `postToggle` | Fires whenever the popovercontainer gets shown or hidden, passing in event.detail an object containing two booleans: `isOpen`, which is true if the popovercontainer was opened and false if it was closed, and `first`, which is true if it was opened for the first time. | `CustomEvent<{ isOpen: boolean; first?: boolean; }>` | ## Methods