From cc18a90a860f8483e8cfb6fb5342c9a27592e4b1 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Thu, 12 Oct 2023 12:11:20 -0400 Subject: [PATCH] backport PR 1600 --- docs/pages/resources/changelog.md | 1 + package.json | 16 ++--------- src/components/menu-item/menu-item.styles.ts | 24 ++++++++++++++++ .../menu-item/submenu-controller.ts | 28 +++++++++++++++++++ 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/docs/pages/resources/changelog.md b/docs/pages/resources/changelog.md index 0b312467a..2cd3ba672 100644 --- a/docs/pages/resources/changelog.md +++ b/docs/pages/resources/changelog.md @@ -16,6 +16,7 @@ New versions of Web Awesome are released as-needed and generally occur when a cr - Changed the `sl` prefix to `wa` for Web Awesome, including tags, events, etc. - Changed `primary` variants to `brand` in all components +- Improved submenu selection by implementing the [safe triangle](https://www.smashingmagazine.com/2023/08/better-context-menus-safe-triangles/) method [#1550] - Removed `default` from `` and made `neutral` the new default - Removed the `circle` modifier from `` because button's no longer have a set height diff --git a/package.json b/package.json index 3c45dd21f..9339be4eb 100644 --- a/package.json +++ b/package.json @@ -25,15 +25,8 @@ "./dist/react/*": "./dist/react/*", "./dist/translations/*": "./dist/translations/*" }, - "files": [ - "dist", - "cdn" - ], - "keywords": [ - "web components", - "custom elements", - "components" - ], + "files": ["dist", "cdn"], + "keywords": ["web components", "custom elements", "components"], "repository": { "type": "git", "url": "git+https://github.com/shoelace-style/shoelace.git" @@ -136,9 +129,6 @@ "user-agent-data-types": "^0.3.0" }, "lint-staged": { - "*.{ts,js}": [ - "eslint --max-warnings 0 --cache --fix", - "prettier --write" - ] + "*.{ts,js}": ["eslint --max-warnings 0 --cache --fix", "prettier --write"] } } diff --git a/src/components/menu-item/menu-item.styles.ts b/src/components/menu-item/menu-item.styles.ts index 2de777921..18bb7af6c 100644 --- a/src/components/menu-item/menu-item.styles.ts +++ b/src/components/menu-item/menu-item.styles.ts @@ -7,6 +7,14 @@ export default css` :host { --submenu-offset: -2px; + /* Private */ + --safe-triangle-cursor-x: 0; + --safe-triangle-cursor-y: 0; + --safe-triangle-submenu-start-x: 0; + --safe-triangle-submenu-start-y: 0; + --safe-triangle-submenu-end-x: 0; + --safe-triangle-submenu-end-y: 0; + display: block; } @@ -60,6 +68,22 @@ export default css` margin-inline-start: var(--wa-space-xs); } + /* Safe triangle */ + .menu-item--submenu-expanded::after { + content: ''; + position: fixed; + z-index: calc(var(--wa-z-index-dropdown) - 1); + top: 0; + right: 0; + bottom: 0; + left: 0; + clip-path: polygon( + var(--safe-triangle-cursor-x) var(--safe-triangle-cursor-y), + var(--safe-triangle-submenu-start-x) var(--safe-triangle-submenu-start-y), + var(--safe-triangle-submenu-end-x) var(--safe-triangle-submenu-end-y) + ); + } + :host(:focus-visible) { outline: none; } diff --git a/src/components/menu-item/submenu-controller.ts b/src/components/menu-item/submenu-controller.ts index 535c76459..58ceb74bd 100644 --- a/src/components/menu-item/submenu-controller.ts +++ b/src/components/menu-item/submenu-controller.ts @@ -49,6 +49,7 @@ export class SubmenuController implements ReactiveController { 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); @@ -61,6 +62,7 @@ export class SubmenuController implements ReactiveController { 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; } } @@ -68,6 +70,7 @@ export class SubmenuController implements ReactiveController { 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); @@ -77,11 +80,18 @@ export class SubmenuController implements ReactiveController { 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.hasSlotController.test('submenu')) { this.enableSubmenu(); @@ -188,6 +198,24 @@ export class SubmenuController implements ReactiveController { 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 = this.localize.dir() === '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) {