From 4296d4838c949a581577865f286fd63560af5230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aliz=C3=A9=20Debray?= Date: Wed, 18 Dec 2024 16:54:29 +0100 Subject: [PATCH 1/2] chore(components): update the post-menu to be usable inside other components --- .../post-menu-item/post-menu-item.scss | 3 -- .../post-menu-item/post-menu-item.tsx | 6 +-- .../src/components/post-menu/post-menu.scss | 7 ++- .../src/components/post-menu/post-menu.tsx | 44 ++++++++++++------- ...focusable.ts => get-focusable-children.ts} | 4 +- packages/components/src/utils/get-root.ts | 2 +- 6 files changed, 38 insertions(+), 28 deletions(-) delete mode 100644 packages/components/src/components/post-menu-item/post-menu-item.scss rename packages/components/src/utils/{is-focusable.ts => get-focusable-children.ts} (76%) diff --git a/packages/components/src/components/post-menu-item/post-menu-item.scss b/packages/components/src/components/post-menu-item/post-menu-item.scss deleted file mode 100644 index f09d32524a..0000000000 --- a/packages/components/src/components/post-menu-item/post-menu-item.scss +++ /dev/null @@ -1,3 +0,0 @@ -:host { - display: block; -} \ No newline at end of file diff --git a/packages/components/src/components/post-menu-item/post-menu-item.tsx b/packages/components/src/components/post-menu-item/post-menu-item.tsx index b4c7de0469..5f6685d5d9 100644 --- a/packages/components/src/components/post-menu-item/post-menu-item.tsx +++ b/packages/components/src/components/post-menu-item/post-menu-item.tsx @@ -1,14 +1,10 @@ -import { Component, h, Element, Host } from '@stencil/core'; +import { Component, h, Host } from '@stencil/core'; import { version } from '@root/package.json'; @Component({ tag: 'post-menu-item', - shadow: true, - styleUrl: 'post-menu-item.scss', }) export class PostMenuItem { - @Element() host: HTMLPostMenuItemElement; - render() { return ( diff --git a/packages/components/src/components/post-menu/post-menu.scss b/packages/components/src/components/post-menu/post-menu.scss index 95b15284b7..d207ad3f3d 100644 --- a/packages/components/src/components/post-menu/post-menu.scss +++ b/packages/components/src/components/post-menu/post-menu.scss @@ -8,4 +8,9 @@ post-popovercontainer { padding: var(--post-menu-padding); background-color: var(--post-menu-bg, #ffffff); border-color: var(--post-menu-bg, #ffffff); -} \ No newline at end of file +} + +.popover-container { + display: flex; + flex-direction: column; +} diff --git a/packages/components/src/components/post-menu/post-menu.tsx b/packages/components/src/components/post-menu/post-menu.tsx index 865a24a729..775c831172 100644 --- a/packages/components/src/components/post-menu/post-menu.tsx +++ b/packages/components/src/components/post-menu/post-menu.tsx @@ -1,7 +1,17 @@ -import { Component, Element, Event, EventEmitter, h, Host, Method, Prop, State } from '@stencil/core'; +import { + Component, + Element, + Event, + EventEmitter, + h, + Host, + Method, + Prop, + State, +} from '@stencil/core'; import { Placement } from '@floating-ui/dom'; import { version } from '@root/package.json'; -import { isFocusable } from '@/utils/is-focusable'; +import { getFocusableChildren } from '@/utils/get-focusable-children'; import { getRoot } from '@/utils'; @Component({ @@ -76,7 +86,7 @@ export class PostMenu { /** * Displays the popover menu, focusing the first menu item. - * + * * @param target - The HTML element relative to which the popover menu should be displayed. */ @Method() @@ -131,12 +141,15 @@ export class PostMenu { private controlKeyDownHandler(e: KeyboardEvent) { const menuItems = this.getSlottedItems(); + if (!menuItems.length) { return; } - const currentFocusedElement = this.root.activeElement as HTMLElement; // Use root's activeElement - let currentIndex = menuItems.findIndex(el => el === currentFocusedElement); + let currentIndex = menuItems.findIndex(el => { + // Check if the item is currently focused within its rendered scope (document or shadow root) + return el === getRoot(el).activeElement; + }); switch (e.key) { case this.KEYCODES.UP: @@ -169,20 +182,19 @@ export class PostMenu { } } - private getSlottedItems() { + private getSlottedItems(): Element[] { const slot = this.host.shadowRoot.querySelector('slot'); const slottedElements = slot ? slot.assignedElements() : []; - const menuItems = slottedElements - .filter(el => el.tagName === 'POST-MENU-ITEM') - .map(el => { - const slot = el.shadowRoot.querySelector('slot'); - const assignedElements = slot ? slot.assignedElements() : []; - return assignedElements.filter(isFocusable); - }) - .flat(); - - return menuItems; + return ( + slottedElements + // If the element is a slot, get the assigned elements + .flatMap(el => (el instanceof HTMLSlotElement ? el.assignedElements() : el)) + // Filter out elements that have a 'menuitem' role + .filter(el => el.getAttribute('role') === 'menuitem') + // For each menu item, get any focusable children (e.g., buttons, links) + .flatMap(el => Array.from(getFocusableChildren(el))) + ); } render() { diff --git a/packages/components/src/utils/is-focusable.ts b/packages/components/src/utils/get-focusable-children.ts similarity index 76% rename from packages/components/src/utils/is-focusable.ts rename to packages/components/src/utils/get-focusable-children.ts index 66aa7d74dc..68b0679ab6 100644 --- a/packages/components/src/utils/is-focusable.ts +++ b/packages/components/src/utils/get-focusable-children.ts @@ -23,6 +23,6 @@ const focusDisablingSelector = `:where(${[ 'details:not([open]) > *:not(details > summary:first-of-type) *', ].join(',')})`; -export const isFocusable = (element: Element) => { - return element?.matches(focusableSelector) && !element?.matches(focusDisablingSelector); +export const getFocusableChildren = (element: Element): NodeListOf => { + return element.querySelectorAll(`& > ${focusableSelector}:not(${focusDisablingSelector})`); }; diff --git a/packages/components/src/utils/get-root.ts b/packages/components/src/utils/get-root.ts index 0c84953d8c..ad33d956ab 100644 --- a/packages/components/src/utils/get-root.ts +++ b/packages/components/src/utils/get-root.ts @@ -1,4 +1,4 @@ -export function getRoot(element: HTMLElement): Document | ShadowRoot { +export function getRoot(element: Element): Document | ShadowRoot { const root = element.getRootNode(); if (root instanceof Document || root instanceof ShadowRoot) { From 7a703ce83cf3ad2660e3cb5abc6816bd137e3ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aliz=C3=A9=20Debray?= Date: Thu, 19 Dec 2024 15:35:10 +0100 Subject: [PATCH 2/2] Update get-focusable-children.ts --- packages/components/src/utils/get-focusable-children.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/src/utils/get-focusable-children.ts b/packages/components/src/utils/get-focusable-children.ts index 68b0679ab6..12a33efc02 100644 --- a/packages/components/src/utils/get-focusable-children.ts +++ b/packages/components/src/utils/get-focusable-children.ts @@ -21,6 +21,7 @@ const focusDisablingSelector = `:where(${[ '[popover]:not(:popover-open) *', 'details:not([open]) > *:not(details > summary:first-of-type)', 'details:not([open]) > *:not(details > summary:first-of-type) *', + '[tabindex^="-"]', ].join(',')})`; export const getFocusableChildren = (element: Element): NodeListOf => {