diff --git a/docs/src/content/docs/resources/changelog.md b/docs/src/content/docs/resources/changelog.md index 4c5bd8d09..ebd05c364 100644 --- a/docs/src/content/docs/resources/changelog.md +++ b/docs/src/content/docs/resources/changelog.md @@ -23,6 +23,15 @@ New versions of Web Awesome are released as-needed and generally occur when a cr ## Next +- Added support for `contextElement` to `VirtualElements` in `` [#1874] +- Fixed a bug in `` that caused the rating to not reset in some circumstances [#1877] +- Fixed a bug in `` that caused the menu to not close when rendered in a shadow root [#1878] +- Fixed a bug in `` that caused a new stacking context resulting in tooltips being clipped [#1709] +- Fixed a bug in `` that caused the scroll controls to toggle indefinitely when zoomed in Safari [#1839] +- Fixed a bug in the submenu controller that allowed two submenus to be open at the same time [#1880] + +## 2.14.0 + - Added the Arabic translation [#1852] - Added help text to `` [#1860] - Added help text to `` [#1800] diff --git a/src/components/dialog/dialog.component.ts b/src/components/dialog/dialog.component.ts index b82585562..a9f4869c7 100644 --- a/src/components/dialog/dialog.component.ts +++ b/src/components/dialog/dialog.component.ts @@ -75,6 +75,7 @@ export default class WaDialog extends WebAwesomeElement { private readonly localize = new LocalizeController(this); private originalTrigger: HTMLElement | null; public modal = new Modal(this); + private closeWatcher: CloseWatcher | null; @query('.dialog') dialog: HTMLElement; @query('.dialog__panel') panel: HTMLElement; @@ -112,6 +113,7 @@ export default class WaDialog extends WebAwesomeElement { super.disconnectedCallback(); this.modal.deactivate(); unlockBodyScrolling(this); + this.closeWatcher?.destroy(); } private requestClose(source: 'close-button' | 'keyboard' | 'overlay') { @@ -130,7 +132,14 @@ export default class WaDialog extends WebAwesomeElement { } private addOpenListeners() { - document.addEventListener('keydown', this.handleDocumentKeyDown); + if ('CloseWatcher' in window) { + this.closeWatcher?.destroy(); + this.closeWatcher = new CloseWatcher(); + this.closeWatcher.onclose = () => this.requestClose('keyboard'); + } else { + this.closeWatcher?.destroy(); + document.addEventListener('keydown', this.handleDocumentKeyDown); + } } private removeOpenListeners() { diff --git a/src/components/drawer/drawer.component.ts b/src/components/drawer/drawer.component.ts index 46fd2da3e..8f510c755 100644 --- a/src/components/drawer/drawer.component.ts +++ b/src/components/drawer/drawer.component.ts @@ -82,6 +82,7 @@ export default class WaDrawer extends WebAwesomeElement { private readonly localize = new LocalizeController(this); private originalTrigger: HTMLElement | null; public modal = new Modal(this); + private closeWatcher: CloseWatcher | null; @query('.drawer') drawer: HTMLElement; @query('.drawer__panel') panel: HTMLElement; @@ -130,6 +131,7 @@ export default class WaDrawer extends WebAwesomeElement { disconnectedCallback() { super.disconnectedCallback(); unlockBodyScrolling(this); + this.closeWatcher?.destroy(); } private requestClose(source: 'close-button' | 'keyboard' | 'overlay') { @@ -148,7 +150,16 @@ export default class WaDrawer extends WebAwesomeElement { } private addOpenListeners() { - document.addEventListener('keydown', this.handleDocumentKeyDown); + if ('CloseWatcher' in window) { + this.closeWatcher?.destroy(); + if (!this.contained) { + this.closeWatcher = new CloseWatcher(); + this.closeWatcher.onclose = () => this.requestClose('keyboard'); + } + } else { + document.addEventListener('keydown', this.handleDocumentKeyDown); + this.closeWatcher?.destroy(); + } } private removeOpenListeners() { diff --git a/src/components/dropdown/dropdown.component.ts b/src/components/dropdown/dropdown.component.ts index 2e5a737a1..b0c59a872 100644 --- a/src/components/dropdown/dropdown.component.ts +++ b/src/components/dropdown/dropdown.component.ts @@ -49,6 +49,7 @@ export default class WaDropdown extends WebAwesomeElement { @query('.dropdown__panel') panel: HTMLSlotElement; private readonly localize = new LocalizeController(this); + private closeWatcher: CloseWatcher | null; /** * Indicates whether or not the dropdown is open. You can toggle this attribute to show and hide the dropdown, or you @@ -141,7 +142,7 @@ export default class WaDropdown extends WebAwesomeElement { private handleKeyDown = (event: KeyboardEvent) => { // Close when escape is pressed inside an open dropdown. We need to listen on the panel itself and stop propagation // in case any ancestors are also listening for this key. - if (this.open && event.key === 'Escape') { + if (this.open && event.key === 'Escape' && !this.closeWatcher) { event.stopPropagation(); this.hide(); this.focusOnTrigger(); @@ -335,7 +336,16 @@ export default class WaDropdown extends WebAwesomeElement { addOpenListeners() { this.panel.addEventListener('wa-select', this.handlePanelSelect); - this.panel.addEventListener('keydown', this.handleKeyDown); + if ('CloseWatcher' in window) { + this.closeWatcher?.destroy(); + this.closeWatcher = new CloseWatcher(); + this.closeWatcher.onclose = () => { + this.hide(); + this.focusOnTrigger(); + }; + } else { + this.panel.addEventListener('keydown', this.handleKeyDown); + } document.addEventListener('keydown', this.handleDocumentKeyDown); document.addEventListener('mousedown', this.handleDocumentMouseDown); } @@ -347,6 +357,7 @@ export default class WaDropdown extends WebAwesomeElement { } document.removeEventListener('keydown', this.handleDocumentKeyDown); document.removeEventListener('mousedown', this.handleDocumentMouseDown); + this.closeWatcher?.destroy(); } @watch('open', { waitUntilFirstUpdate: true }) diff --git a/src/components/menu-item/submenu-controller.ts b/src/components/menu-item/submenu-controller.ts index 58ceb74bd..166e9c042 100644 --- a/src/components/menu-item/submenu-controller.ts +++ b/src/components/menu-item/submenu-controller.ts @@ -229,6 +229,7 @@ export class SubmenuController implements ReactiveController { // newly opened menu. private enableSubmenu(delay = true) { if (delay) { + window.clearTimeout(this.enableSubmenuTimer); this.enableSubmenuTimer = window.setTimeout(() => { this.setSubmenuState(true); }, this.submenuOpenDelay); @@ -238,7 +239,7 @@ export class SubmenuController implements ReactiveController { } private disableSubmenu() { - clearTimeout(this.enableSubmenuTimer); + window.clearTimeout(this.enableSubmenuTimer); this.setSubmenuState(false); } diff --git a/src/components/popup/popup.component.ts b/src/components/popup/popup.component.ts index ac72ef904..ec16eb5a9 100644 --- a/src/components/popup/popup.component.ts +++ b/src/components/popup/popup.component.ts @@ -10,10 +10,16 @@ import type { CSSResultGroup } from 'lit'; export interface VirtualElement { getBoundingClientRect: () => DOMRect; + contextElement?: Element; } function isVirtualElement(e: unknown): e is VirtualElement { - return e !== null && typeof e === 'object' && 'getBoundingClientRect' in e; + return ( + e !== null && + typeof e === 'object' && + 'getBoundingClientRect' in e && + ('contextElement' in e ? e instanceof Element : true) + ); } /** diff --git a/src/components/rating/rating.component.ts b/src/components/rating/rating.component.ts index f53ee6676..36e7957c9 100644 --- a/src/components/rating/rating.component.ts +++ b/src/components/rating/rating.component.ts @@ -265,7 +265,6 @@ export default class WaRating extends WebAwesomeElement { 'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1 })} role="presentation" - @mouseenter=${this.handleMouseEnter} >
= index + 1 })} role="presentation" - @mouseenter=${this.handleMouseEnter} > ${unsafeHTML(this.getSymbol(index + 1))} diff --git a/src/components/rating/rating.styles.ts b/src/components/rating/rating.styles.ts index b94c30aab..e302836e2 100644 --- a/src/components/rating/rating.styles.ts +++ b/src/components/rating/rating.styles.ts @@ -57,6 +57,7 @@ export default css` .rating__symbol { transition: var(--wa-transition-fast) scale; + pointer-events: none; } .rating__symbol--hover { diff --git a/src/components/select/select.component.ts b/src/components/select/select.component.ts index 5deb8ede9..3f9c2a294 100644 --- a/src/components/select/select.component.ts +++ b/src/components/select/select.component.ts @@ -90,6 +90,7 @@ export default class WaSelect extends WebAwesomeElement implements WebAwesomeFor private readonly localize = new LocalizeController(this); private typeToSelectString = ''; private typeToSelectTimeout: number; + private closeWatcher: CloseWatcher | null; @query('.select') popup: WaPopup; @query('.select__combobox') combobox: HTMLSlotElement; @@ -230,17 +231,37 @@ export default class WaSelect extends WebAwesomeElement implements WebAwesomeFor // // https://github.com/shoelace-style/shoelace/issues/1763 // - const root = this.getRootNode(); - root.addEventListener('focusin', this.handleDocumentFocusIn); - root.addEventListener('keydown', this.handleDocumentKeyDown); - root.addEventListener('mousedown', this.handleDocumentMouseDown); + document.addEventListener('focusin', this.handleDocumentFocusIn); + document.addEventListener('keydown', this.handleDocumentKeyDown); + document.addEventListener('mousedown', this.handleDocumentMouseDown); + + // If the component is rendered in a shadow root, we need to attach the focusin listener there too + if (this.getRootNode() !== document) { + this.getRootNode().addEventListener('focusin', this.handleDocumentFocusIn); + } + + if ('CloseWatcher' in window) { + this.closeWatcher?.destroy(); + this.closeWatcher = new CloseWatcher(); + this.closeWatcher.onclose = () => { + if (this.open) { + this.hide(); + this.displayInput.focus({ preventScroll: true }); + } + }; + } } private removeOpenListeners() { - const root = this.getRootNode(); - root.removeEventListener('focusin', this.handleDocumentFocusIn); - root.removeEventListener('keydown', this.handleDocumentKeyDown); - root.removeEventListener('mousedown', this.handleDocumentMouseDown); + document.removeEventListener('focusin', this.handleDocumentFocusIn); + document.removeEventListener('keydown', this.handleDocumentKeyDown); + document.removeEventListener('mousedown', this.handleDocumentMouseDown); + + if (this.getRootNode() !== document) { + this.getRootNode().removeEventListener('focusin', this.handleDocumentFocusIn); + } + + this.closeWatcher?.destroy(); } private handleFocus() { diff --git a/src/components/tab-group/tab-group.component.ts b/src/components/tab-group/tab-group.component.ts index 3643edab3..59850bdd4 100644 --- a/src/components/tab-group/tab-group.component.ts +++ b/src/components/tab-group/tab-group.component.ts @@ -338,8 +338,13 @@ export default class WaTabGroup extends WebAwesomeElement { if (this.noScrollControls) { this.hasScrollControls = false; } else { + // In most cases, we can compare scrollWidth to clientWidth to determine if scroll controls should show. However, + // Safari appears to calculate this incorrectly when zoomed at 110%, causing the controls to toggle indefinitely. + // Adding a single pixel to the comparison seems to resolve it. + // + // See https://github.com/shoelace-style/shoelace/issues/1839 this.hasScrollControls = - ['top', 'bottom'].includes(this.placement) && this.nav.scrollWidth > this.nav.clientWidth; + ['top', 'bottom'].includes(this.placement) && this.nav.scrollWidth > this.nav.clientWidth + 1; } } diff --git a/src/components/tooltip/tooltip.component.ts b/src/components/tooltip/tooltip.component.ts index c3ab8c77b..db2db9329 100644 --- a/src/components/tooltip/tooltip.component.ts +++ b/src/components/tooltip/tooltip.component.ts @@ -46,6 +46,7 @@ export default class WaTooltip extends WebAwesomeElement { private hoverTimeout: number; private readonly localize = new LocalizeController(this); + private closeWatcher: CloseWatcher | null; @query('slot:not([name])') defaultSlot: HTMLSlotElement; @query('.tooltip__body') body: HTMLElement; @@ -109,6 +110,7 @@ export default class WaTooltip extends WebAwesomeElement { disconnectedCallback() { // Cleanup this event in case the tooltip is removed while open + this.closeWatcher?.destroy(); document.removeEventListener('keydown', this.handleDocumentKeyDown); } @@ -182,7 +184,15 @@ export default class WaTooltip extends WebAwesomeElement { // Show this.emit('wa-show'); - document.addEventListener('keydown', this.handleDocumentKeyDown); + if ('CloseWatcher' in window) { + this.closeWatcher?.destroy(); + this.closeWatcher = new CloseWatcher(); + this.closeWatcher.onclose = () => { + this.hide(); + }; + } else { + document.addEventListener('keydown', this.handleDocumentKeyDown); + } await stopAnimations(this.body); this.body.hidden = false; @@ -195,6 +205,7 @@ export default class WaTooltip extends WebAwesomeElement { } else { // Hide this.emit('wa-hide'); + this.closeWatcher?.destroy(); document.removeEventListener('keydown', this.handleDocumentKeyDown); await stopAnimations(this.body); diff --git a/src/components/tree/tree.styles.ts b/src/components/tree/tree.styles.ts index 19a68345c..666100ae2 100644 --- a/src/components/tree/tree.styles.ts +++ b/src/components/tree/tree.styles.ts @@ -13,7 +13,6 @@ export default css` --indent-size: var(--wa-space-l); display: block; - isolation: isolate; /* * Tree item indentation uses the "em" unit to increment its width on each level, so setting the font size to zero diff --git a/src/declaration.d.ts b/src/declaration.d.ts index 2b9065278..7a971479e 100644 --- a/src/declaration.d.ts +++ b/src/declaration.d.ts @@ -14,3 +14,25 @@ declare namespace Chai { interface HTMLInputElement { showPicker: () => void; } + +/* eslint-disable */ +interface CloseWatcher extends EventTarget { + new (options?: CloseWatcherOptions): CloseWatcher; + requestClose(): void; + close(): void; + destroy(): void; + + oncancel: (event: Event) => void | null; + onclose: (event: Event) => void | null; +} + +declare const CloseWatcher: CloseWatcher; + +interface CloseWatcherOptions { + signal: AbortSignal; +} + +declare interface Window { + CloseWatcher?: CloseWatcher; +} +/* eslint-enable */