From be04e9a221e7b9e38995297e70d4517b3fae6468 Mon Sep 17 00:00:00 2001 From: Bryce Moore Date: Fri, 30 Jun 2023 07:43:45 -0400 Subject: [PATCH] PoC working of ArrowRight to focus on submenu. --- src/components/menu-item/menu-item.ts | 27 ++++++++ .../menu-item/submenu-controller.ts | 63 +++++++++++++++++++ src/components/popup/popup.ts | 13 +++- 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/components/menu-item/menu-item.ts b/src/components/menu-item/menu-item.ts index 0b1936752d..fb4c8b383c 100644 --- a/src/components/menu-item/menu-item.ts +++ b/src/components/menu-item/menu-item.ts @@ -60,6 +60,7 @@ export default class SlMenuItem extends ShoelaceElement { constructor() { super(); this.addEventListener('click', this.handleHostClick); + this.addEventListener('keydown', this.handleKeyDown); this.submenuController = new SubmenuController(this, this.hasSlotController, this.localize); } @@ -86,6 +87,28 @@ export default class SlMenuItem extends ShoelaceElement { event.stopImmediatePropagation(); } }; + + private handleKeyDown(event: KeyboardEvent) { + console.log(` handleKeyDown: ${event.key}`); + } + +/* + // Make a selection when pressing enter + if (event.key === 'Enter') { + event.preventDefault(); + + // Simulate a click to support @click handlers on menu items that also work with the keyboard + // item?.click(); + } + + // Prevent scrolling when space is pressed + if (event.key === ' ') { + + event.preventDefault(); + } + } +*/ + @watch('checked') handleCheckedChange() { @@ -111,9 +134,13 @@ export default class SlMenuItem extends ShoelaceElement { @watch('type') handleTypeChange() { + console.log(`handleTypeChange() ${this.getTextLabel()} : submenu? ${this.hasSlotController.test("submenu")}`); if (this.type === 'checkbox') { this.setAttribute('role', 'menuitemcheckbox'); this.setAttribute('aria-checked', this.checked ? 'true' : 'false'); + } else if (this.hasSlotController.test('submenu')) { + this.setAttribute('role', 'menu'); + this.removeAttribute('aria-checked'); } else { this.setAttribute('role', 'menuitem'); this.removeAttribute('aria-checked'); diff --git a/src/components/menu-item/submenu-controller.ts b/src/components/menu-item/submenu-controller.ts index c49b2416b8..e3e45701ba 100644 --- a/src/components/menu-item/submenu-controller.ts +++ b/src/components/menu-item/submenu-controller.ts @@ -12,6 +12,7 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit'; export class SubmenuController implements ReactiveController { private host: ReactiveControllerHost & SlMenuItem; private popupRef: Ref = createRef(); + private mouseOutTimer: number = -1; private isActive: boolean = false; @@ -50,6 +51,7 @@ export class SubmenuController implements ReactiveController { if (!this.isActive) { this.host.addEventListener('mouseover', this.handleMouseOver); this.host.addEventListener('mouseout', this.handleMouseOut); + this.host.addEventListener('keydown', (event) => { this.handleKeyDown(event) }); this.isActive = true; } } @@ -58,6 +60,7 @@ export class SubmenuController implements ReactiveController { if (this.isActive) { this.host.removeEventListener('mouseover', this.handleMouseOver); this.host.removeEventListener('mouseout', this.handleMouseOut); + this.host.removeEventListener('keydown', this.handleKeyDown); this.isActive = false; } } @@ -79,6 +82,65 @@ export class SubmenuController implements ReactiveController { }, 100); } }; + + private isMenuItem(item: HTMLElement) { + return ( + item.tagName.toLowerCase() === 'sl-menu-item' || + ['menuitem', 'menuitemcheckbox', 'menuitemradio'].includes(item.getAttribute('role') ?? '') + ); + } + + /** @internal Gets all slotted menu items, ignoring dividers, headers, and other elements. */ + getAllItems() { + const rr = this.host.renderRoot; + const slotQS : HTMLSlotElement = rr.querySelector("slot[name='submenu']") as HTMLSlotElement; + console.log(slotQS); + const aEs = slotQS.assignedElements({ flatten: true }); + console.log(aEs[0]); + const menuItems = aEs.filter((el: HTMLElement) => { + if (el.inert || !this.isMenuItem(el)) { + return false; + } + return true; + }) as SlMenuItem[]; + return [...menuItems]; + } + +/* + return [...this.host.renderRoot.querySelector("slot[name='submenu']").assignedElements({ flatten: true }).filter((el: HTMLElement) => { + if (el.inert || !this.isMenuItem(el)) { + return false; + } + return true; + }) as SlMenuItem[]; + } + */ + + private handleKeyDown(event: KeyboardEvent) { + console.log(`submenuController.handleKeyDown: ${event.key}`); + switch(event.key) { + case "ArrowRight": + console.log("ArrowRight detected."); + //let items = this.getAllItems(); + // find *a* menu-item + // let item = this.host.renderRoot.querySelector("sl-menu-item, [role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']") + //let item = this.host.renderRoot.querySelector("sl-menu-item"); + let item : HTMLSlotElement = this.host.renderRoot.querySelector("slot[name='submenu']") as HTMLSlotElement; + console.log(item); + console.log(item!.assignedElements()[0]); + console.log(item!.assignedElements()[0].querySelector("sl-menu-item")); + item!.assignedElements()[0].querySelector("sl-menu-item")!.focus(); + break; + } + } + + show() { + + } + + hide() { + + } renderSubmenu() { // Always render the slot. Conditionally render the outer sl-popup. @@ -99,6 +161,7 @@ export class SubmenuController implements ReactiveController { placement=${isLtr ? 'right-start' : 'left-start'} anchor="anchor" flip + flip-fallback-strategy="best-fit" strategy="fixed" > diff --git a/src/components/popup/popup.ts b/src/components/popup/popup.ts index f2ac9fb0a3..86ec2e62f7 100644 --- a/src/components/popup/popup.ts +++ b/src/components/popup/popup.ts @@ -1,4 +1,4 @@ -import { arrow, autoUpdate, computePosition, flip, offset, platform, shift, size } from '@floating-ui/dom'; +import { arrow, autoPlacement, autoUpdate, computePosition, flip, offset, platform, shift, size } from '@floating-ui/dom'; import { classMap } from 'lit/directives/class-map.js'; import { customElement, property, query } from 'lit/decorators.js'; import { html } from 'lit'; @@ -114,6 +114,11 @@ export default class SlPopup extends ShoelaceElement { */ @property({ type: Boolean }) flip = false; + /** + * When set, placement of the popup will choose the placement that has the most space avaiable automatically. + */ + @property({ attribute: 'auto-placement', type: Boolean }) autoPlacement = false; + /** * If the preferred placement doesn't fit, popup will be tested in these fallback placements until one fits. Must be a * string of any number of placements separated by a space, e.g. "top bottom left". If no placement fits, the flip @@ -305,8 +310,14 @@ export default class SlPopup extends ShoelaceElement { this.popup.style.width = ''; this.popup.style.height = ''; } + + // Then we check for autoPlacement + if (this.autoPlacement) { + middleware.push(autoPlacement()); + } // Then we flip + // TODO Don't do both if (this.flip) { middleware.push( flip({