From cea790b4c7d87fdfba5a4ef9470da64d52fdd52a Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Thu, 21 May 2026 11:52:21 +0200 Subject: [PATCH 01/50] =?UTF-8?q?=F0=9F=9A=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/afraid-parents-nail.md | 20 ++++++ packages/components/button/package.json | 11 ++- packages/components/button/src/button.spec.ts | 72 +++++++++++++++++++ .../components/button/src/button.stories.ts | 30 ++++---- packages/components/button/src/button.ts | 63 +++++++++++++++- yarn.lock | 6 ++ 6 files changed, 183 insertions(+), 19 deletions(-) create mode 100644 .changeset/afraid-parents-nail.md diff --git a/.changeset/afraid-parents-nail.md b/.changeset/afraid-parents-nail.md new file mode 100644 index 0000000000..608780b8d4 --- /dev/null +++ b/.changeset/afraid-parents-nail.md @@ -0,0 +1,20 @@ +--- +'@sl-design-system/button': minor +--- + +Add `tooltip` property to button + +Previously, adding a tooltip to a button required adding a sibling `` element manually and wiring up the correct `aria-describedby` or `aria-labelledby` relationship by hand. This was especially cumbersome for icon-only buttons, where the tooltip doubles as the accessible label. + +The new `tooltip` property improves the Developer Experience by letting you attach a tooltip directly on the button: + +```html + + + +``` + +The button handles all the accessibility wiring automatically: + +- For **icon-only buttons** the tooltip text acts as the accessible label (`aria-labelledby`). +- For **text buttons** the tooltip text acts as an accessible description (`aria-describedby`). diff --git a/packages/components/button/package.json b/packages/components/button/package.json index 7183e6f73d..1cc920d6b1 100644 --- a/packages/components/button/package.json +++ b/packages/components/button/package.json @@ -39,6 +39,15 @@ "test": "echo \"Error: run tests from monorepo root.\" && exit 1" }, "dependencies": { - "@sl-design-system/shared": "^0.12.0" + "@sl-design-system/shared": "^0.12.0", + "@sl-design-system/tooltip": "^2.0.0" + }, + "devDependencies": { + "@open-wc/scoped-elements": "^3.0.6", + "lit": "^3.3.2" + }, + "peerDependencies": { + "@open-wc/scoped-elements": "^3.0.6", + "lit": "^3.1.4" } } diff --git a/packages/components/button/src/button.spec.ts b/packages/components/button/src/button.spec.ts index 5f89edd570..88f8098bdc 100644 --- a/packages/components/button/src/button.spec.ts +++ b/packages/components/button/src/button.spec.ts @@ -649,4 +649,76 @@ describe('sl-button', () => { }); }); }); + + describe('tooltip', () => { + it('should not have a tooltip by default', async () => { + el = await fixture(html`Hello world`); + + expect(el.renderRoot.querySelector('sl-tooltip')).to.be.null; + }); + + it('should render an sl-tooltip when the tooltip property is set', async () => { + el = await fixture(html`Hello world`); + + expect(el.renderRoot.querySelector('sl-tooltip')).to.exist; + }); + + it('should set the tooltip text content', async () => { + el = await fixture(html`Hello world`); + + expect(el.renderRoot.querySelector('sl-tooltip')).to.have.text('My tooltip'); + }); + + it('should set aria-describedby on the inner button when a text button has a tooltip', async () => { + el = await fixture(html`Hello world`); + button = el.renderRoot.querySelector('button')!; + + expect(button).to.have.attribute('aria-describedby', 'tooltip'); + expect(button).not.to.have.attribute('aria-labelledby'); + }); + + it('should set aria-labelledby on the inner button when an icon-only button has a tooltip', async () => { + // eslint-disable-next-line slds/button-has-label + el = await fixture(html``); + el.tooltip = 'Mark as favorite'; + await el.updateComplete; + button = el.renderRoot.querySelector('button')!; + + expect(button).to.have.attribute('aria-labelledby', 'tooltip'); + expect(button).not.to.have.attribute('aria-describedby'); + }); + + it('should remove the tooltip when the tooltip property is unset', async () => { + el = await fixture(html`Hello world`); + el.tooltip = undefined; + await el.updateComplete; + + expect(el.renderRoot.querySelector('sl-tooltip')).to.be.null; + }); + + it('should include both the tooltip and aria-labelledby element in ariaLabelledByElements for icon-only buttons', async () => { + const wrapper = await fixture(html` +
+ Favorite star + + + +
+ `); + + el = wrapper.querySelector('sl-button')!; + el.tooltip = 'Mark as favorite'; + await el.updateComplete; + + const tooltipEl = el.renderRoot.querySelector('sl-tooltip')!, + labelEl = wrapper.querySelector('#icon-btn-label')!, + ariaLabelElements = getForwardedAriaProperty( + el, + 'ariaLabelledByElements' as keyof HTMLElement + ) as Element[]; + + expect(ariaLabelElements).to.include(labelEl); + expect(ariaLabelElements).to.include(tooltipEl); + }); + }); }); diff --git a/packages/components/button/src/button.stories.ts b/packages/components/button/src/button.stories.ts index 78ffe28f38..d15afea550 100644 --- a/packages/components/button/src/button.stories.ts +++ b/packages/components/button/src/button.stories.ts @@ -1,3 +1,4 @@ +/* eslint-disable slds/button-has-label */ import { faPlus, faUniversalAccess } from '@fortawesome/pro-regular-svg-icons'; import '@sl-design-system/avatar/register.js'; import '@sl-design-system/dialog/register.js'; @@ -11,7 +12,10 @@ import { ifDefined } from 'lit/directives/if-defined.js'; import '../register.js'; import { type Button } from './button.js'; -interface Props extends Pick { +interface Props extends Pick< + Button, + 'disabled' | 'fill' | 'shape' | 'size' | 'tooltip' | 'variant' +> { icon: string; text: string; } @@ -60,9 +64,9 @@ export default { options: ['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'inverted'] } }, - render: ({ disabled, fill, icon, shape, size, text, variant }) => { - const startIcon = icon === 'start' ? html`` : ''; - const endIcon = icon === 'end' ? html`` : ''; + render: ({ disabled, fill, icon, shape, size, text, tooltip, variant }) => { + const startIcon = icon === 'start' ? html`` : '', + endIcon = icon === 'end' ? html`` : ''; return html` + tooltip=${ifDefined(tooltip)} + variant=${ifDefined(variant)}> ${startIcon}${text}${endIcon} `; @@ -129,8 +133,7 @@ export const Disabled: Story = { fill=${ifDefined(fill)} shape=${ifDefined(shape)} size=${ifDefined(size)} - variant=${ifDefined(variant)} - > + variant=${ifDefined(variant)}> Disabled button + variant=${ifDefined(variant)}> Disabled (ARIA only) button @@ -156,17 +158,13 @@ export const IconOnly: Story = { technologies can convey the purpose of the button to users.

+ tooltip="Always have a tooltip for icon-only buttons to explain their purpose." + variant=${ifDefined(variant)}> - - Always have a tooltip for icon-only buttons to explain their purpose. - `; } }; diff --git a/packages/components/button/src/button.ts b/packages/components/button/src/button.ts index d2a4cbcf15..049328b5cb 100644 --- a/packages/components/button/src/button.ts +++ b/packages/components/button/src/button.ts @@ -1,11 +1,14 @@ +import { ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; import { closestElementComposed } from '@sl-design-system/shared'; import { ForwardAriaMixin } from '@sl-design-system/shared/mixins.js'; +import { Tooltip } from '@sl-design-system/tooltip'; import { type CSSResultGroup, LitElement, type PropertyValues, type TemplateResult, - html + html, + nothing } from 'lit'; import { property, query } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; @@ -45,10 +48,17 @@ export type ButtonVariant = * * @csspart button - The internal <button> element. */ -export class Button extends ForwardAriaMixin(LitElement) { +export class Button extends ForwardAriaMixin(ScopedElementsMixin(LitElement)) { /** @internal */ static formAssociated = true; + /** @internal */ + static get scopedElements() { + return { + 'sl-tooltip': Tooltip + }; + } + /** @internal */ static override shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, @@ -61,6 +71,9 @@ export class Button extends ForwardAriaMixin(LitElement) { /** Observe changes to the slotted content that aren't caught by the `slotchange` event. */ #observer = new MutationObserver(() => this.#onUpdate()); + /** Aria-labelledby elements forwarded from the host by the ForwardAriaMixin. */ + #forwardedLabelElements: Element[] = []; + /** Stores tabIndex set before the button is rendered. */ #tabIndex = 0; @@ -129,6 +142,9 @@ export class Button extends ForwardAriaMixin(LitElement) { } } + /** The text that will be shown in a tooltip. */ + @property() tooltip?: string; + /** * The type of the button. Can be used to mimic the functionality of submit and reset buttons in * native HTML buttons. @@ -156,11 +172,23 @@ export class Button extends ForwardAriaMixin(LitElement) { super.disconnectedCallback(); } + override updated(changes: PropertyValues): void { + super.updated(changes); + + this.#syncAriaLabelledBy(); + } + override firstUpdated(changes: PropertyValues): void { super.firstUpdated(changes); this.setProxyTarget(this.button); + // Capture any aria-labelledby elements the mixin just forwarded to the inner button. + this.#forwardedLabelElements = [ + ...((this.button as unknown as { ariaLabelledByElements: Element[] | null }) + .ariaLabelledByElements ?? []) + ]; + if (this.hasAttribute('tabindex')) { this.tabIndex = parseInt(this.getAttribute('tabindex') ?? '0'); } @@ -177,9 +205,21 @@ export class Button extends ForwardAriaMixin(LitElement) { (this.getRootNode() as Document | ShadowRoot).getElementById?.(this.commandFor) ?? null; } + // If the button is icon only, the tooltip functions as the label, otherwise it functions as the description. + let ariaLabelledBy: string | undefined, ariaDescribedBy: string | undefined; + if (this.tooltip) { + if (this.internals.states.has('icon-only')) { + ariaLabelledBy = 'tooltip'; + } else { + ariaDescribedBy = 'tooltip'; + } + } + return html` + ${this.tooltip ? html`${this.tooltip}` : nothing} `; } @@ -240,4 +281,22 @@ export class Button extends ForwardAriaMixin(LitElement) { this.internals.states.delete('icon-only'); } } + + #syncAriaLabelledBy(): void { + if (!this.#forwardedLabelElements.length) { + return; + } + + const buttonEl = this.button as unknown as { ariaLabelledByElements: Element[] | null }; + + if (this.tooltip && this.internals.states.has('icon-only')) { + const tooltipEl = this.renderRoot.querySelector('sl-tooltip'); + if (tooltipEl) { + buttonEl.ariaLabelledByElements = [...this.#forwardedLabelElements, tooltipEl]; + return; + } + } + + buttonEl.ariaLabelledByElements = [...this.#forwardedLabelElements]; + } } diff --git a/yarn.lock b/yarn.lock index 29fe9f244a..b58782f935 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5438,7 +5438,13 @@ __metadata: version: 0.0.0-use.local resolution: "@sl-design-system/button@workspace:packages/components/button" dependencies: + "@open-wc/scoped-elements": "npm:^3.0.6" "@sl-design-system/shared": "npm:^0.12.0" + "@sl-design-system/tooltip": "npm:^2.0.0" + lit: "npm:^3.3.2" + peerDependencies: + "@open-wc/scoped-elements": ^3.0.6 + lit: ^3.1.4 languageName: unknown linkType: soft From a0b4a8c7b244670ffbd4ec7cdf4868500b784742 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Thu, 21 May 2026 15:34:47 +0200 Subject: [PATCH 02/50] =?UTF-8?q?=F0=9F=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/button/src/button.spec.ts | 29 ++- .../components/button/src/button.stories.ts | 9 + packages/components/button/src/button.ts | 36 +-- packages/components/tooltip/src/tooltip2.scss | 28 ++ .../tooltip/src/tooltip2.stories.ts | 23 ++ packages/components/tooltip/src/tooltip2.ts | 241 ++++++++++++++++++ 6 files changed, 330 insertions(+), 36 deletions(-) create mode 100644 packages/components/tooltip/src/tooltip2.scss create mode 100644 packages/components/tooltip/src/tooltip2.stories.ts create mode 100644 packages/components/tooltip/src/tooltip2.ts diff --git a/packages/components/button/src/button.spec.ts b/packages/components/button/src/button.spec.ts index 88f8098bdc..83549c3738 100644 --- a/packages/components/button/src/button.spec.ts +++ b/packages/components/button/src/button.spec.ts @@ -696,19 +696,38 @@ describe('sl-button', () => { expect(el.renderRoot.querySelector('sl-tooltip')).to.be.null; }); - it('should include both the tooltip and aria-labelledby element in ariaLabelledByElements for icon-only buttons', async () => { + it('should include both the tooltip and aria-describedby element in ariaDescribedByElements', async () => { + const wrapper = await fixture(html` +
+ Additional description + Click me +
+ `); + + el = wrapper.querySelector('sl-button')!; + + const tooltipEl = el.renderRoot.querySelector('sl-tooltip')!, + descEl = wrapper.querySelector('#btn-desc')!, + ariaDescElements = getForwardedAriaProperty( + el, + 'ariaDescribedByElements' as keyof HTMLElement + ) as Element[]; + + expect(ariaDescElements).to.include(descEl); + expect(ariaDescElements).to.include(tooltipEl); + }); + + it('should include both the tooltip and aria-labelledby element in ariaLabelledByElements', async () => { const wrapper = await fixture(html`
Favorite star - - + + Hello world
`); el = wrapper.querySelector('sl-button')!; - el.tooltip = 'Mark as favorite'; - await el.updateComplete; const tooltipEl = el.renderRoot.querySelector('sl-tooltip')!, labelEl = wrapper.querySelector('#icon-btn-label')!, diff --git a/packages/components/button/src/button.stories.ts b/packages/components/button/src/button.stories.ts index d15afea550..b5ab017b64 100644 --- a/packages/components/button/src/button.stories.ts +++ b/packages/components/button/src/button.stories.ts @@ -514,3 +514,12 @@ export const All: Story = { `; } }; + +export const DoubleLabel: Story = { + render: () => html` +

This is the button's label as well.

+ + + + ` +}; diff --git a/packages/components/button/src/button.ts b/packages/components/button/src/button.ts index 049328b5cb..7b4bc9705e 100644 --- a/packages/components/button/src/button.ts +++ b/packages/components/button/src/button.ts @@ -71,9 +71,6 @@ export class Button extends ForwardAriaMixin(ScopedElementsMixin(LitElement)) { /** Observe changes to the slotted content that aren't caught by the `slotchange` event. */ #observer = new MutationObserver(() => this.#onUpdate()); - /** Aria-labelledby elements forwarded from the host by the ForwardAriaMixin. */ - #forwardedLabelElements: Element[] = []; - /** Stores tabIndex set before the button is rendered. */ #tabIndex = 0; @@ -172,23 +169,11 @@ export class Button extends ForwardAriaMixin(ScopedElementsMixin(LitElement)) { super.disconnectedCallback(); } - override updated(changes: PropertyValues): void { - super.updated(changes); - - this.#syncAriaLabelledBy(); - } - override firstUpdated(changes: PropertyValues): void { super.firstUpdated(changes); this.setProxyTarget(this.button); - // Capture any aria-labelledby elements the mixin just forwarded to the inner button. - this.#forwardedLabelElements = [ - ...((this.button as unknown as { ariaLabelledByElements: Element[] | null }) - .ariaLabelledByElements ?? []) - ]; - if (this.hasAttribute('tabindex')) { this.tabIndex = parseInt(this.getAttribute('tabindex') ?? '0'); } @@ -275,28 +260,17 @@ export class Button extends ForwardAriaMixin(ScopedElementsMixin(LitElement)) { el.children[0].nodeName === 'SL-ICON'); } + const hasIconOnly = this.internals.states.has('icon-only'); + if (iconOnly) { this.internals.states.add('icon-only'); } else { this.internals.states.delete('icon-only'); } - } - - #syncAriaLabelledBy(): void { - if (!this.#forwardedLabelElements.length) { - return; - } - const buttonEl = this.button as unknown as { ariaLabelledByElements: Element[] | null }; - - if (this.tooltip && this.internals.states.has('icon-only')) { - const tooltipEl = this.renderRoot.querySelector('sl-tooltip'); - if (tooltipEl) { - buttonEl.ariaLabelledByElements = [...this.#forwardedLabelElements, tooltipEl]; - return; - } + // Trigger an update when the icon-only state changes + if (hasIconOnly !== iconOnly) { + this.requestUpdate(); } - - buttonEl.ariaLabelledByElements = [...this.#forwardedLabelElements]; } } diff --git a/packages/components/tooltip/src/tooltip2.scss b/packages/components/tooltip/src/tooltip2.scss new file mode 100644 index 0000000000..6a453e134a --- /dev/null +++ b/packages/components/tooltip/src/tooltip2.scss @@ -0,0 +1,28 @@ +:host { + background: var(--sl-elevation-surface-raised-inverted); + border: 0; + border-radius: var(--sl-size-borderRadius-default); + box-sizing: border-box; + color: var(--sl-color-foreground-inverted-plain); + font-weight: var(--sl-text-new-typeset-fontWeight-regular); + margin: var(--sl-size-100); + opacity: 0; + padding: var(--sl-size-100) var(--sl-size-150); + position-area: bottom; + + @media (prefers-reduced-motion: no-preference) { + transition-duration: 150ms; + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 1, 1); + } +} + +:host(:popover-open) { + @starting-style { + display: block; + opacity: 0; + } + + opacity: 1; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); +} diff --git a/packages/components/tooltip/src/tooltip2.stories.ts b/packages/components/tooltip/src/tooltip2.stories.ts new file mode 100644 index 0000000000..7d5e8d682a --- /dev/null +++ b/packages/components/tooltip/src/tooltip2.stories.ts @@ -0,0 +1,23 @@ +import '@sl-design-system/button/register.js'; +import { Tooltip2 } from './tooltip2.js'; + +try { + customElements.define('sl-tooltip2', Tooltip2); +} catch { + /* empty */ +} + +export default { + title: 'Overlay/Tooltip2', + parameters: { + layout: 'centered' + }, + render: () => { + return ` + Hover me + Tooltip content + `; + } +}; + +export const Basic = {}; diff --git a/packages/components/tooltip/src/tooltip2.ts b/packages/components/tooltip/src/tooltip2.ts new file mode 100644 index 0000000000..a30f99198b --- /dev/null +++ b/packages/components/tooltip/src/tooltip2.ts @@ -0,0 +1,241 @@ +import { CSSResultGroup, LitElement, PropertyValues, TemplateResult, html } from 'lit'; +import { property, state } from 'lit/decorators.js'; +import styles from './tooltip2.scss.js'; + +let nextUniqueId = 0; + +const SHOW_DELAY = 150, + HIDE_DELAY = 0; + +/** + * A tooltip component that can be used to display additional information about an element when the + * user hovers over it, focuses it, or clicks it. The tooltip is positioned relative to an anchor + * element, which can be specified using the `for` attribute. + * + * The tooltip will automatically determine the appropriate ARIA relation to use based on the `type` + * property. By default, it will use `ariaLabelledByElements`, but if `type` is set to + * `description`, it will use `ariaDescribedByElements` instead. + */ +export class Tooltip2 extends LitElement { + /** @internal */ + static override styles: CSSResultGroup = styles; + + /** Controller for managing event listeners. */ + #eventController = new AbortController(); + + /** Timeout ID for the hover delay. */ + #hoverTimeout?: ReturnType; + + /** @internal The element this tooltip is anchored to. */ + @state() anchor?: HTMLElement | null; + + /** The ID of the element this tooltip is for. */ + @property() for?: string; + + /** + * Controls how the tooltip is activated. Possible options include `click`, `hover`, `focus`, and + * `manual`. Multiple options can be passed by separating them with a space. When manual is used, + * the tooltip must be activated programmatically. + * + * @default 'focus hover' + */ + @property() trigger = 'focus hover'; + + /** + * The type of tooltip. Used to determine the ARIA relation that should be used. + * + * @default 'label' + */ + @property() type?: 'description' | 'label'; + + override connectedCallback() { + super.connectedCallback(); + + this.setAttribute('aria-hidden', 'true'); + this.setAttribute('popover', 'manual'); + this.setAttribute('role', 'tooltip'); + + if (!this.id) { + this.id = `sl-tooltip-${nextUniqueId++}`; + } + + if (this.#eventController.signal.aborted) { + this.#eventController = new AbortController(); + } + + const { signal } = this.#eventController; + + this.addEventListener('beforetoggle', this.#onBeforeToggle, { signal }); + this.addEventListener('mouseout', this.#onMouseOut, { signal }); + + // Re-establish the anchor relationship if the tooltip is moved to a different root + if (this.anchor && this.for) { + this.anchor = null; // triggers #updateAnchor() + } else if (this.for) { + this.#updateAnchor(); + } + } + + override disconnectedCallback() { + this.#eventController.abort(); + + // Remove the event handler in case the tooltip is still open when disconnected + document.removeEventListener('keydown', this.#onKeydown); + + if (this.anchor) { + this.#removeAriaRelation(this.anchor); + } + + super.disconnectedCallback(); + } + + override willUpdate(changes: PropertyValues): void { + super.willUpdate(changes); + + if (changes.has('anchor') || changes.has('for')) { + this.#updateAnchor(); + } + } + + override render(): TemplateResult { + return html``; + } + + #onBeforeToggle = (event: ToggleEvent): void => { + if (event.newState === 'open') { + document.addEventListener('keydown', this.#onKeydown); + } else { + document.removeEventListener('keydown', this.#onKeydown); + } + }; + + #onBlur = (): void => { + if (this.#hasTrigger('focus')) { + this.hidePopover(); + } + }; + + #onClick = (): void => { + if (this.#hasTrigger('click')) { + if (this.matches(':popover-open')) { + this.hidePopover(); + } else { + this.showPopover(); + } + } + }; + + #onFocus = (): void => { + if (this.#hasTrigger('focus')) { + this.showPopover(); + } + }; + + #onKeydown = (event: KeyboardEvent): void => { + if (event.key === 'Escape') { + this.hidePopover(); + } + }; + + #onMouseOver = (): void => { + if (this.#hasTrigger('hover')) { + clearTimeout(this.#hoverTimeout); + + this.#hoverTimeout = setTimeout(() => { + this.showPopover(); + }, SHOW_DELAY); + } + }; + + #onMouseOut = (): void => { + if (this.#hasTrigger('hover')) { + // Don't hide the popover if either the anchor or the popover itself is still hovered + const anchorHovered = Boolean(this.anchor?.matches(':hover')), + tooltipHovered = this.matches(':hover'); + if (anchorHovered || tooltipHovered) { + return; + } + + clearTimeout(this.#hoverTimeout); + + if (!(anchorHovered || tooltipHovered)) { + this.#hoverTimeout = setTimeout(() => { + this.hidePopover(); + }, HIDE_DELAY); + } + } + }; + + #hasTrigger(trigger: string): boolean { + return this.trigger.split(' ').includes(trigger); + } + + #updateAnchor(): void { + if (!this.for) { + this.anchor = undefined; + return; + } + + const rootNode = this.getRootNode() as Document | ShadowRoot | null; + if (!rootNode) { + this.anchor = undefined; + return; + } + + const newAnchor = this.for ? rootNode.getElementById(this.for) : null, + oldAnchor = this.anchor; + if (newAnchor === oldAnchor) { + return; + } + + const { signal } = this.#eventController; + + if (newAnchor) { + this.#addAriaRelation(newAnchor); + + newAnchor.addEventListener('blur', this.#onBlur, { capture: true, signal }); + newAnchor.addEventListener('click', this.#onClick, { signal }); + newAnchor.addEventListener('focus', this.#onFocus, { capture: true, signal }); + newAnchor.addEventListener('mouseover', this.#onMouseOver, { signal }); + newAnchor.addEventListener('mouseout', this.#onMouseOut, { signal }); + newAnchor.style.anchorName = `--${this.id}`; + this.style.positionAnchor = `--${this.id}`; + } + + if (oldAnchor) { + this.#removeAriaRelation(oldAnchor); + + oldAnchor.removeEventListener('blur', this.#onBlur, { capture: true }); + oldAnchor.removeEventListener('click', this.#onClick); + oldAnchor.removeEventListener('focus', this.#onFocus, { capture: true }); + oldAnchor.removeEventListener('mouseover', this.#onMouseOver); + oldAnchor.removeEventListener('mouseout', this.#onMouseOut); + oldAnchor.style.anchorName = ''; + this.style.positionAnchor = ''; + } + + this.anchor = newAnchor; + } + + #getAriaPropertyFromType( + type?: 'description' | 'label' + ): 'ariaDescribedByElements' | 'ariaLabelledByElements' { + return type === 'description' ? 'ariaDescribedByElements' : 'ariaLabelledByElements'; + } + + #addAriaRelation(element: Element): void { + const ariaProperty = this.#getAriaPropertyFromType(this.type); + + const refs = element[ariaProperty] ?? []; + if (!refs.includes(this)) { + element[ariaProperty] = [...refs, this]; + } + } + + #removeAriaRelation(element: Element): void { + const ariaProperty = this.#getAriaPropertyFromType(this.type); + + const refs = element[ariaProperty] ?? []; + element[ariaProperty] = refs.filter((ref: Element) => ref !== this); + } +} From b087599c2967732089aab7e10189181ee819f168 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Thu, 21 May 2026 15:44:51 +0200 Subject: [PATCH 03/50] =?UTF-8?q?=F0=9F=92=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/tooltip/src/tooltip2.scss | 3 ++- .../components/tooltip/src/tooltip2.stories.ts | 16 ++++++++++++++-- packages/components/tooltip/src/tooltip2.ts | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/components/tooltip/src/tooltip2.scss b/packages/components/tooltip/src/tooltip2.scss index 6a453e134a..866dc61292 100644 --- a/packages/components/tooltip/src/tooltip2.scss +++ b/packages/components/tooltip/src/tooltip2.scss @@ -8,7 +8,8 @@ margin: var(--sl-size-100); opacity: 0; padding: var(--sl-size-100) var(--sl-size-150); - position-area: bottom; + position-area: top; + position-try-fallbacks: bottom; @media (prefers-reduced-motion: no-preference) { transition-duration: 150ms; diff --git a/packages/components/tooltip/src/tooltip2.stories.ts b/packages/components/tooltip/src/tooltip2.stories.ts index 7d5e8d682a..2952cdcc1d 100644 --- a/packages/components/tooltip/src/tooltip2.stories.ts +++ b/packages/components/tooltip/src/tooltip2.stories.ts @@ -12,10 +12,22 @@ export default { parameters: { layout: 'centered' }, - render: () => { + argTypes: { + position: { + control: 'inline-radio', + options: ['top', 'right', 'bottom', 'left'] + }, + text: { + control: 'text' + } + }, + args: { + text: 'Tooltip text' + }, + render: ({ position, text }) => { return ` Hover me - Tooltip content + ${text} `; } }; diff --git a/packages/components/tooltip/src/tooltip2.ts b/packages/components/tooltip/src/tooltip2.ts index a30f99198b..4371611a0b 100644 --- a/packages/components/tooltip/src/tooltip2.ts +++ b/packages/components/tooltip/src/tooltip2.ts @@ -70,7 +70,7 @@ export class Tooltip2 extends LitElement { // Re-establish the anchor relationship if the tooltip is moved to a different root if (this.anchor && this.for) { - this.anchor = null; // triggers #updateAnchor() + this.anchor = undefined; // triggers #updateAnchor() } else if (this.for) { this.#updateAnchor(); } From 7979d8270c872a096b2af2b4468e8ad331fa53b2 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Thu, 21 May 2026 15:48:50 +0200 Subject: [PATCH 04/50] =?UTF-8?q?=F0=9F=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/tooltip/src/tooltip2.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/tooltip/src/tooltip2.stories.ts b/packages/components/tooltip/src/tooltip2.stories.ts index 2952cdcc1d..0f020de8ec 100644 --- a/packages/components/tooltip/src/tooltip2.stories.ts +++ b/packages/components/tooltip/src/tooltip2.stories.ts @@ -24,7 +24,7 @@ export default { args: { text: 'Tooltip text' }, - render: ({ position, text }) => { + render: ({ position, text }: { position: string; text: string }) => { return ` Hover me ${text} From 58f0fd515b4f29ee89d9e182d5953bb70a78334d Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Thu, 21 May 2026 17:34:15 +0200 Subject: [PATCH 05/50] =?UTF-8?q?=F0=9F=8C=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tooltip/src/edge-cases.stories.ts | 66 +++++++ packages/components/tooltip/src/tooltip2.scss | 2 + .../tooltip/src/tooltip2.stories.ts | 97 +++++++++- packages/components/tooltip/src/tooltip2.ts | 176 +++++++++++++++--- 4 files changed, 309 insertions(+), 32 deletions(-) create mode 100644 packages/components/tooltip/src/edge-cases.stories.ts diff --git a/packages/components/tooltip/src/edge-cases.stories.ts b/packages/components/tooltip/src/edge-cases.stories.ts new file mode 100644 index 0000000000..91138fa6d8 --- /dev/null +++ b/packages/components/tooltip/src/edge-cases.stories.ts @@ -0,0 +1,66 @@ +import { faGear, faPen, faTrash } from '@fortawesome/pro-regular-svg-icons'; +import { faGear as fasGear } from '@fortawesome/pro-solid-svg-icons'; +import '@sl-design-system/button/register.js'; +import '@sl-design-system/button-bar/register.js'; +import '@sl-design-system/card/register.js'; +import { Icon } from '@sl-design-system/icon'; +import '@sl-design-system/icon/register.js'; +import '@sl-design-system/menu/register.js'; +import '@sl-design-system/toggle-button/register.js'; +import '@sl-design-system/toggle-group/register.js'; +import { html } from 'lit'; +import { Tooltip2 } from './tooltip2.js'; + +try { + customElements.define('sl-tooltip2', Tooltip2); +} catch { + /* empty */ +} + +Icon.register(faGear, faPen, faTrash, fasGear); + +export default { + title: 'Overlay/Tooltip2/Edge cases' +}; + +export const DisabledButtons = { + render: () => html` + + Disabled attribute + Tooltip text + + ARIA disabled + Tooltip text + + ` +}; + +export const MenuButton = { + render: () => html` + + + + + Rename... + + + + Delete... + + + Tooltip text + ` +}; + +export const Nested = { + render: () => html` + +

Card title

+ + Hover me + Tooltip text + +
+ Card tooltip + ` +}; diff --git a/packages/components/tooltip/src/tooltip2.scss b/packages/components/tooltip/src/tooltip2.scss index 866dc61292..9b746fc829 100644 --- a/packages/components/tooltip/src/tooltip2.scss +++ b/packages/components/tooltip/src/tooltip2.scss @@ -10,6 +10,8 @@ padding: var(--sl-size-100) var(--sl-size-150); position-area: top; position-try-fallbacks: bottom; + text-align: center; + text-wrap: pretty; @media (prefers-reduced-motion: no-preference) { transition-duration: 150ms; diff --git a/packages/components/tooltip/src/tooltip2.stories.ts b/packages/components/tooltip/src/tooltip2.stories.ts index 0f020de8ec..f373d50061 100644 --- a/packages/components/tooltip/src/tooltip2.stories.ts +++ b/packages/components/tooltip/src/tooltip2.stories.ts @@ -1,6 +1,18 @@ import '@sl-design-system/button/register.js'; +import { Meta } from '@storybook/web-components-vite'; +import { TemplateResult, html, nothing } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; import { Tooltip2 } from './tooltip2.js'; +type Props = Pick & { + maxWidth: number; + position: string; + showHoverExtender: boolean; + text: string; + tooltip(): TemplateResult; + trigger: string[]; +}; + try { customElements.define('sl-tooltip2', Tooltip2); } catch { @@ -13,23 +25,96 @@ export default { layout: 'centered' }, argTypes: { + disabled: { + control: 'boolean' + }, + maxWidth: { + control: 'number' + }, + open: { + control: 'boolean' + }, position: { control: 'inline-radio', options: ['top', 'right', 'bottom', 'left'] }, + showHoverExtender: { + control: 'boolean' + }, text: { control: 'text' + }, + tooltip: { + table: { disable: true } + }, + trigger: { + control: 'inline-check', + options: ['click', 'hover', 'focus', 'manual'] } }, args: { text: 'Tooltip text' }, - render: ({ position, text }: { position: string; text: string }) => { - return ` - Hover me - ${text} - `; + render: ({ + disabled, + maxWidth, + open, + position, + showHoverExtender, + text, + tooltip, + trigger + }) => html` + Anchor + ${tooltip + ? tooltip() + : html` + + ${text} + + `} + + ` +} satisfies Meta; + +export const Basic = {}; + +export const ClickTrigger = { + args: { + text: 'Click again to dismiss', + trigger: ['click'] } }; -export const Basic = {}; +export const Disabled = { + args: { + disabled: true + } +}; + +export const HoverExtender = { + args: { + maxWidth: 200, + showHoverExtender: true, + text: 'The hotpink area extends the hover trigger area, making it easier to keep the tooltip open' + } +}; + +export const Positions = { + args: { + tooltip: () => html` + Top + Right + Bottom + Left + ` + } +}; diff --git a/packages/components/tooltip/src/tooltip2.ts b/packages/components/tooltip/src/tooltip2.ts index 4371611a0b..ec30ac1974 100644 --- a/packages/components/tooltip/src/tooltip2.ts +++ b/packages/components/tooltip/src/tooltip2.ts @@ -15,6 +15,13 @@ const SHOW_DELAY = 150, * The tooltip will automatically determine the appropriate ARIA relation to use based on the `type` * property. By default, it will use `ariaLabelledByElements`, but if `type` is set to * `description`, it will use `ariaDescribedByElements` instead. + * + * @element sl-tooltip2 + * + * @slot - The content of the tooltip. + * + * @csspart arrow - The arrow element that points to the anchor. + * @csspart safe-triangle - An invisible element used to extend the hover area of the tooltip. */ export class Tooltip2 extends LitElement { /** @internal */ @@ -29,9 +36,24 @@ export class Tooltip2 extends LitElement { /** @internal The element this tooltip is anchored to. */ @state() anchor?: HTMLElement | null; + /** + * Stops the tooltip from being displayed. + * + * @default false + */ + @property({ type: Boolean }) disabled?: boolean; + /** The ID of the element this tooltip is for. */ @property() for?: string; + /** + * Setting this will cause the tooltip to show/hide, regardless of trigger. Do not use this + * property to check if the tooltip is showing, use `matches(':popover-open')` instead. + * + * @default false + */ + @property({ type: Boolean }) open?: boolean; + /** * Controls how the tooltip is activated. Possible options include `click`, `hover`, `focus`, and * `manual`. Multiple options can be passed by separating them with a space. When manual is used, @@ -67,6 +89,7 @@ export class Tooltip2 extends LitElement { this.addEventListener('beforetoggle', this.#onBeforeToggle, { signal }); this.addEventListener('mouseout', this.#onMouseOut, { signal }); + this.addEventListener('toggle', this.#onToggle, { signal }); // Re-establish the anchor relationship if the tooltip is moved to a different root if (this.anchor && this.for) { @@ -77,6 +100,8 @@ export class Tooltip2 extends LitElement { } override disconnectedCallback() { + clearTimeout(this.#hoverTimeout); + this.#eventController.abort(); // Remove the event handler in case the tooltip is still open when disconnected @@ -95,14 +120,35 @@ export class Tooltip2 extends LitElement { if (changes.has('anchor') || changes.has('for')) { this.#updateAnchor(); } + + if (changes.has('disabled') && this.disabled) { + this.hidePopover(); + } + + if (changes.has('open')) { + if (this.open) { + this.showPopover(); + } else { + this.hidePopover(); + } + } } override render(): TemplateResult { - return html``; + return html` + +
+
+ `; } #onBeforeToggle = (event: ToggleEvent): void => { if (event.newState === 'open') { + if (this.disabled) { + event.preventDefault(); + return; + } + document.addEventListener('keydown', this.#onKeydown); } else { document.removeEventListener('keydown', this.#onKeydown); @@ -122,10 +168,13 @@ export class Tooltip2 extends LitElement { } else { this.showPopover(); } + } else { + this.hidePopover(); } }; - #onFocus = (): void => { + #onFocus = (event: Event): void => { + console.log('onFocus', event.target); if (this.#hasTrigger('focus')) { this.showPopover(); } @@ -166,10 +215,104 @@ export class Tooltip2 extends LitElement { } }; + #onToggle = (event: ToggleEvent): void => { + if (event.newState === 'open' && this.anchor) { + this.#positionHoverExtender(this.anchor); + } + }; + #hasTrigger(trigger: string): boolean { return this.trigger.split(' ').includes(trigger); } + #getAriaPropertyFromType( + type?: 'description' | 'label' + ): 'ariaDescribedByElements' | 'ariaLabelledByElements' { + return type === 'description' ? 'ariaDescribedByElements' : 'ariaLabelledByElements'; + } + + #addAriaRelation(element: Element): void { + const ariaProperty = this.#getAriaPropertyFromType(this.type); + + const refs = element[ariaProperty] ?? []; + if (!refs.includes(this)) { + element[ariaProperty] = [...refs, this]; + } + } + + #removeAriaRelation(element: Element): void { + const ariaProperty = this.#getAriaPropertyFromType(this.type); + + const refs = element[ariaProperty] ?? []; + element[ariaProperty] = refs.filter((ref: Element) => ref !== this); + } + + #positionHoverExtender(anchor: Element): void { + const extender = this.renderRoot.querySelector('[part="hover-extender"]'); + if (!extender) { + return; + } + + const a = anchor.getBoundingClientRect(), + t = this.getBoundingClientRect(); + + // Determine on which side of the anchor the tooltip ended up (after CSS anchor positioning + // and any position-try fallbacks). We then build a trapezoid whose parallel edges align with + // the touching edges of the anchor and the tooltip, so the user can move the pointer between + // the two without crossing an unhovered area. + let left: number, top: number, width: number, height: number, polygon: string; + + if (t.bottom <= a.top) { + // Tooltip above anchor + left = Math.min(a.left, t.left); + top = t.bottom; + width = Math.max(a.right, t.right) - left; + height = Math.max(0, a.top - t.bottom); + polygon = + `polygon(${t.left - left}px 0, ${t.right - left}px 0, ` + + `${a.right - left}px ${height}px, ${a.left - left}px ${height}px)`; + } else if (t.top >= a.bottom) { + // Tooltip below anchor + left = Math.min(a.left, t.left); + top = a.bottom; + width = Math.max(a.right, t.right) - left; + height = Math.max(0, t.top - a.bottom); + polygon = + `polygon(${a.left - left}px 0, ${a.right - left}px 0, ` + + `${t.right - left}px ${height}px, ${t.left - left}px ${height}px)`; + } else if (t.right <= a.left) { + // Tooltip left of anchor + left = t.right; + top = Math.min(a.top, t.top); + width = Math.max(0, a.left - t.right); + height = Math.max(a.bottom, t.bottom) - top; + polygon = + `polygon(0 ${t.top - top}px, 0 ${t.bottom - top}px, ` + + `${width}px ${a.bottom - top}px, ${width}px ${a.top - top}px)`; + } else if (t.left >= a.right) { + // Tooltip right of anchor + left = a.right; + top = Math.min(a.top, t.top); + width = Math.max(0, t.left - a.right); + height = Math.max(a.bottom, t.bottom) - top; + polygon = + `polygon(0 ${a.top - top}px, 0 ${a.bottom - top}px, ` + + `${width}px ${t.bottom - top}px, ${width}px ${t.top - top}px)`; + } else { + // Tooltip and anchor overlap; no bridge needed. + extender.style.display = 'none'; + return; + } + + extender.style.display = 'block'; + extender.style.position = 'fixed'; + extender.style.left = `${left}px`; + extender.style.top = `${top}px`; + extender.style.width = `${width}px`; + extender.style.height = `${height}px`; + extender.style.clipPath = polygon; + } + #updateAnchor(): void { if (!this.for) { this.anchor = undefined; @@ -198,8 +341,11 @@ export class Tooltip2 extends LitElement { newAnchor.addEventListener('focus', this.#onFocus, { capture: true, signal }); newAnchor.addEventListener('mouseover', this.#onMouseOver, { signal }); newAnchor.addEventListener('mouseout', this.#onMouseOut, { signal }); - newAnchor.style.anchorName = `--${this.id}`; - this.style.positionAnchor = `--${this.id}`; + + const newAnchorName = newAnchor.style.anchorName || `--${this.id}`; + + newAnchor.style.anchorName = newAnchorName; + this.style.positionAnchor = newAnchorName; } if (oldAnchor) { @@ -216,26 +362,4 @@ export class Tooltip2 extends LitElement { this.anchor = newAnchor; } - - #getAriaPropertyFromType( - type?: 'description' | 'label' - ): 'ariaDescribedByElements' | 'ariaLabelledByElements' { - return type === 'description' ? 'ariaDescribedByElements' : 'ariaLabelledByElements'; - } - - #addAriaRelation(element: Element): void { - const ariaProperty = this.#getAriaPropertyFromType(this.type); - - const refs = element[ariaProperty] ?? []; - if (!refs.includes(this)) { - element[ariaProperty] = [...refs, this]; - } - } - - #removeAriaRelation(element: Element): void { - const ariaProperty = this.#getAriaPropertyFromType(this.type); - - const refs = element[ariaProperty] ?? []; - element[ariaProperty] = refs.filter((ref: Element) => ref !== this); - } } From 1e00db65c298e8c2901b0be672792a75f4563ad9 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Thu, 21 May 2026 17:38:33 +0200 Subject: [PATCH 06/50] =?UTF-8?q?=F0=9F=8E=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/tooltip/src/tooltip2.stories.ts | 2 +- packages/components/tooltip/src/tooltip2.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/tooltip/src/tooltip2.stories.ts b/packages/components/tooltip/src/tooltip2.stories.ts index f373d50061..922a8840c0 100644 --- a/packages/components/tooltip/src/tooltip2.stories.ts +++ b/packages/components/tooltip/src/tooltip2.stories.ts @@ -104,7 +104,7 @@ export const HoverExtender = { args: { maxWidth: 200, showHoverExtender: true, - text: 'The hotpink area extends the hover trigger area, making it easier to keep the tooltip open' + text: 'The hotpink area extends the hover trigger area, making it possible to move the mouse from the anchor to the tooltip without it disappearing.' } }; diff --git a/packages/components/tooltip/src/tooltip2.ts b/packages/components/tooltip/src/tooltip2.ts index ec30ac1974..ece82d9e50 100644 --- a/packages/components/tooltip/src/tooltip2.ts +++ b/packages/components/tooltip/src/tooltip2.ts @@ -342,6 +342,7 @@ export class Tooltip2 extends LitElement { newAnchor.addEventListener('mouseover', this.#onMouseOver, { signal }); newAnchor.addEventListener('mouseout', this.#onMouseOut, { signal }); + // Do not overwrite an existing anchor name, as it might be used for something else. const newAnchorName = newAnchor.style.anchorName || `--${this.id}`; newAnchor.style.anchorName = newAnchorName; From b04be06be30c7fe800fce06c3aad223504f55e89 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Thu, 21 May 2026 17:41:18 +0200 Subject: [PATCH 07/50] =?UTF-8?q?=F0=9F=8E=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/tooltip/src/tooltip2.scss | 6 +++++- packages/components/tooltip/src/tooltip2.ts | 2 -- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/components/tooltip/src/tooltip2.scss b/packages/components/tooltip/src/tooltip2.scss index 9b746fc829..82104abf96 100644 --- a/packages/components/tooltip/src/tooltip2.scss +++ b/packages/components/tooltip/src/tooltip2.scss @@ -5,7 +5,7 @@ box-sizing: border-box; color: var(--sl-color-foreground-inverted-plain); font-weight: var(--sl-text-new-typeset-fontWeight-regular); - margin: var(--sl-size-100); + margin: var(--sl-size-150); opacity: 0; padding: var(--sl-size-100) var(--sl-size-150); position-area: top; @@ -29,3 +29,7 @@ opacity: 1; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } + +[part='hover-extender'] { + position: fixed; +} diff --git a/packages/components/tooltip/src/tooltip2.ts b/packages/components/tooltip/src/tooltip2.ts index ece82d9e50..d964276952 100644 --- a/packages/components/tooltip/src/tooltip2.ts +++ b/packages/components/tooltip/src/tooltip2.ts @@ -304,8 +304,6 @@ export class Tooltip2 extends LitElement { return; } - extender.style.display = 'block'; - extender.style.position = 'fixed'; extender.style.left = `${left}px`; extender.style.top = `${top}px`; extender.style.width = `${width}px`; From 61d6159bf7142fa105109895293540a073a5220d Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Thu, 21 May 2026 18:42:25 +0200 Subject: [PATCH 08/50] =?UTF-8?q?=F0=9F=91=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/tooltip/src/tooltip2.scss | 2 +- .../tooltip/src/tooltip2.stories.ts | 21 ++++++------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/components/tooltip/src/tooltip2.scss b/packages/components/tooltip/src/tooltip2.scss index 82104abf96..efeb38d526 100644 --- a/packages/components/tooltip/src/tooltip2.scss +++ b/packages/components/tooltip/src/tooltip2.scss @@ -30,6 +30,6 @@ transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } -[part='hover-extender'] { +[part='hover-bridge'] { position: fixed; } diff --git a/packages/components/tooltip/src/tooltip2.stories.ts b/packages/components/tooltip/src/tooltip2.stories.ts index 922a8840c0..8917722470 100644 --- a/packages/components/tooltip/src/tooltip2.stories.ts +++ b/packages/components/tooltip/src/tooltip2.stories.ts @@ -7,7 +7,7 @@ import { Tooltip2 } from './tooltip2.js'; type Props = Pick & { maxWidth: number; position: string; - showHoverExtender: boolean; + showHoverBridge: boolean; text: string; tooltip(): TemplateResult; trigger: string[]; @@ -38,7 +38,7 @@ export default { control: 'inline-radio', options: ['top', 'right', 'bottom', 'left'] }, - showHoverExtender: { + showHoverBridge: { control: 'boolean' }, text: { @@ -55,16 +55,7 @@ export default { args: { text: 'Tooltip text' }, - render: ({ - disabled, - maxWidth, - open, - position, - showHoverExtender, - text, - tooltip, - trigger - }) => html` + render: ({ disabled, maxWidth, open, position, showHoverBridge, text, tooltip, trigger }) => html` Anchor ${tooltip ? tooltip() @@ -80,7 +71,7 @@ export default { ` } satisfies Meta; @@ -100,11 +91,11 @@ export const Disabled = { } }; -export const HoverExtender = { +export const HoverBridge = { args: { maxWidth: 200, showHoverExtender: true, - text: 'The hotpink area extends the hover trigger area, making it possible to move the mouse from the anchor to the tooltip without it disappearing.' + text: 'The hotpink area bridges the area between anchor and tooltip, making it possible to move the mouse from the anchor to the tooltip without it disappearing.' } }; From cbcb78595d51cdcdb98dc357733199b0067be9ea Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Tue, 26 May 2026 09:23:16 +0200 Subject: [PATCH 09/50] =?UTF-8?q?=F0=9F=90=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grid/src/stories/selection.stories.ts | 8 +- .../src/toggle-button.stories.ts | 46 +- .../toggle-button/src/toggle-button.ts | 128 +- .../tool-bar/src/tool-bar.stories.ts | 37 +- packages/components/tooltip/index.ts | 1 - .../tooltip/src/edge-cases.stories.ts | 10 +- .../tooltip/src/tooltip-directive.spec.ts | 168 -- .../tooltip/src/tooltip-directive.ts | 76 - .../tooltip/src/tooltip-shared.spec.ts | 230 --- packages/components/tooltip/src/tooltip.scss | 78 +- .../components/tooltip/src/tooltip.spec.ts | 1317 -------------- .../components/tooltip/src/tooltip.stories.ts | 370 +--- packages/components/tooltip/src/tooltip.ts | 1506 +++-------------- packages/components/tooltip/src/tooltip2.scss | 35 - .../tooltip/src/tooltip2.stories.ts | 111 -- packages/components/tooltip/src/tooltip2.ts | 364 ---- 16 files changed, 374 insertions(+), 4111 deletions(-) delete mode 100644 packages/components/tooltip/src/tooltip-directive.spec.ts delete mode 100644 packages/components/tooltip/src/tooltip-directive.ts delete mode 100644 packages/components/tooltip/src/tooltip-shared.spec.ts delete mode 100644 packages/components/tooltip/src/tooltip.spec.ts delete mode 100644 packages/components/tooltip/src/tooltip2.scss delete mode 100644 packages/components/tooltip/src/tooltip2.stories.ts delete mode 100644 packages/components/tooltip/src/tooltip2.ts diff --git a/packages/components/grid/src/stories/selection.stories.ts b/packages/components/grid/src/stories/selection.stories.ts index e02c6c0556..17efedbd43 100644 --- a/packages/components/grid/src/stories/selection.stories.ts +++ b/packages/components/grid/src/stories/selection.stories.ts @@ -13,8 +13,6 @@ import { type Student, getStudents } from '@sl-design-system/example-data'; import { Icon } from '@sl-design-system/icon'; import '@sl-design-system/icon/register.js'; import '@sl-design-system/menu/register.js'; -import { tooltip } from '@sl-design-system/tooltip'; -import '@sl-design-system/tooltip/register.js'; import { type StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; import '../../register.js'; @@ -105,10 +103,10 @@ export const Multiple: Story = { Action 1 Action 2 @@ -229,10 +227,10 @@ sl-grid::part(bulk-actions) { Action 1 Action 2 @@ -301,10 +299,10 @@ export const MultipleWithMenuButton: Story = { Action 1 Action 2 diff --git a/packages/components/toggle-button/src/toggle-button.stories.ts b/packages/components/toggle-button/src/toggle-button.stories.ts index 54f1eff0fc..a8947e2996 100644 --- a/packages/components/toggle-button/src/toggle-button.stories.ts +++ b/packages/components/toggle-button/src/toggle-button.stories.ts @@ -8,9 +8,8 @@ import { ifDefined } from 'lit/directives/if-defined.js'; import '../register.js'; import { type ToggleButton, ToggleButtonFill, ToggleButtonSize } from './toggle-button.js'; -type Props = Pick & { +type Props = Pick & { icons(): TemplateResult; - label: string; }; type Story = StoryObj; @@ -20,36 +19,35 @@ export default { title: 'Actions/Toggle button', args: { disabled: false, - label: 'Show settings', - pressed: false + pressed: false, + tooltip: 'Show settings' }, argTypes: { fill: { control: 'inline-radio', options: ['outline', 'solid'] }, + icons: { + table: { disable: true } + }, shape: { control: 'inline-radio', options: ['pill', 'square'] }, - icons: { - table: { disable: true } - }, size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] } }, - render: ({ disabled, fill, icons, label, pressed, shape, size }) => { + render: ({ disabled, fill, icons, pressed, shape, size, tooltip }) => { return html` + tooltip=${ifDefined(tooltip)}> ${icons?.()} `; @@ -95,7 +93,7 @@ export const Errors: Story = { Note: these errors (button turning red and console errors) only show up when running on localhost / in development mode.

- + @@ -103,7 +101,7 @@ export const Errors: Story = { Setting the same icon for both states as "workaround" will not work, you will get the same error

- + @@ -117,84 +115,76 @@ export const All: Story = { return html`
+ tooltip="Show settings"> + tooltip="Show settings">
+ tooltip="Show settings"> + tooltip="Show settings">
+ tooltip="Show settings"> + tooltip="Show settings">
+ tooltip="Show settings"> + tooltip="Show settings"> diff --git a/packages/components/toggle-button/src/toggle-button.ts b/packages/components/toggle-button/src/toggle-button.ts index c7ee815e7e..bbcc5d2794 100644 --- a/packages/components/toggle-button/src/toggle-button.ts +++ b/packages/components/toggle-button/src/toggle-button.ts @@ -1,4 +1,3 @@ -/// import { type ScopedElementsMap, ScopedElementsMixin @@ -8,7 +7,14 @@ import { Icon } from '@sl-design-system/icon'; import { type EventEmitter, EventsController, event } from '@sl-design-system/shared'; import { type SlToggleEvent } from '@sl-design-system/shared/events.js'; import { Tooltip } from '@sl-design-system/tooltip'; -import { type CSSResultGroup, LitElement, PropertyValues, type TemplateResult, html } from 'lit'; +import { + type CSSResultGroup, + LitElement, + PropertyValues, + type TemplateResult, + html, + nothing +} from 'lit'; import { property, state } from 'lit/decorators.js'; import styles from './toggle-button.scss.js'; @@ -52,12 +58,6 @@ export class ToggleButton extends ScopedElementsMixin(LitElement) { keydown: this.#onKeydown }); - /** Either an instanceof of Tooltip, or a cleanup function. */ - #tooltip?: Tooltip | (() => void); - - /** @internal Whether the `aria-label` attribute is being changed internally. */ - #isInternalAriaLabelUpdate = false; - /** @internal The default (non-pressed) icon. */ @state() defaultIcon?: Icon; @@ -91,13 +91,8 @@ export class ToggleButton extends ScopedElementsMixin(LitElement) { /** @internal Emits when the button has been toggled. */ @event({ name: 'sl-toggle' }) toggleEvent!: EventEmitter>; - override attributeChangedCallback(name: string, old: string | null, value: string | null): void { - if (name === 'aria-label' && this.#isInternalAriaLabelUpdate) { - return; - } - - super.attributeChangedCallback(name, old, value); - } + /** The tooltip text for the button. */ + @property() tooltip?: string; override connectedCallback(): void { super.connectedCallback(); @@ -109,18 +104,6 @@ export class ToggleButton extends ScopedElementsMixin(LitElement) { } } - override disconnectedCallback(): void { - if (this.#tooltip instanceof Tooltip) { - this.#tooltip.remove(); - } else if (this.#tooltip) { - this.#tooltip(); - } - - this.#tooltip = undefined; - - super.disconnectedCallback(); - } - override firstUpdated(changes: PropertyValues): void { super.firstUpdated(changes); @@ -155,13 +138,6 @@ export class ToggleButton extends ScopedElementsMixin(LitElement) { if (changes.has('defaultIcon') || changes.has('hasText') || changes.has('pressedIcon')) { this.toggleAttribute('icon-only', this.#isIconOnly()); this.toggleAttribute('text-only', !!this.hasText && !this.defaultIcon && !this.pressedIcon); - - // If the tooltip is still lazy, its ariaRelation might be outdated now that icons have loaded. - // We clear it so that when it is (re)initialized later in this update, it uses the correct relation. - if (typeof this.#tooltip === 'function') { - this.#tooltip(); - this.#tooltip = undefined; - } } if (changes.has('defaultIcon') || changes.has('pressedIcon')) { @@ -170,58 +146,21 @@ export class ToggleButton extends ScopedElementsMixin(LitElement) { }); } - if (this.label) { - if (this.#tooltip instanceof Tooltip) { - if (changes.has('label')) { - this.#tooltip.textContent = this.label; - } - } else if (!this.#tooltip) { - this.#tooltip = Tooltip.lazy( - this, - tooltip => { - this.#tooltip = tooltip; - tooltip.textContent = this.label!; - this.#updateAriaAttributes(); - }, - { - ariaRelation: this.#isIconOnly() ? 'label' : 'description', - context: this.shadowRoot! - } - ); - } - } else { - if (this.#tooltip instanceof Tooltip) { - this.#tooltip.remove(); - this.#tooltip = undefined; - } else if (this.#tooltip) { - this.#tooltip(); - this.#tooltip = undefined; - } - } - if (changes.has('pressed')) { this.setAttribute('aria-pressed', (this.pressed ?? false).toString()); } - - if ( - changes.has('label') || - changes.has('hasText') || - changes.has('defaultIcon') || - changes.has('pressedIcon') - ) { - this.#updateAriaAttributes(); - } } override render(): TemplateResult { return html` -
+
+ ${this.tooltip ? html`${this.tooltip}` : nothing} `; } @@ -261,48 +200,7 @@ export class ToggleButton extends ScopedElementsMixin(LitElement) { .filter(node => node.textContent && node.textContent.trim().length > 0).length; } - /** - * Update aria-label, aria-describedby and aria-labelledby. For icon-only buttons, aria-label is - * removed only after the tooltip is created, setting aria-labelledby as the accessible name. - * Otherwise, aria-label is used as the accessible name, and the tooltip provides a description - * via aria-describedby. - */ - #updateAriaAttributes(): void { - if (!this.label) { - // When the label is cleared, also clear all ARIA attributes that may - // reference a now-removed tooltip to avoid stale relationships. - this.#isInternalAriaLabelUpdate = true; - this.removeAttribute('aria-label'); - this.removeAttribute('aria-labelledby'); - this.removeAttribute('aria-describedby'); - this.#isInternalAriaLabelUpdate = false; - return; - } - - if (this.#tooltip instanceof Tooltip) { - const isIconOnly = this.#isIconOnly(); - this.#isInternalAriaLabelUpdate = true; - if (isIconOnly) { - this.removeAttribute('aria-label'); - this.setAttribute('aria-labelledby', this.#tooltip.id); - this.removeAttribute('aria-describedby'); - } else { - this.setAttribute('aria-label', this.label); - this.setAttribute('aria-describedby', this.#tooltip.id); - this.removeAttribute('aria-labelledby'); - } - this.#isInternalAriaLabelUpdate = false; - } else { - // While the tooltip is lazy, keep aria-label as a fallback - this.#isInternalAriaLabelUpdate = true; - this.setAttribute('aria-label', this.label); - this.#isInternalAriaLabelUpdate = false; - this.removeAttribute('aria-labelledby'); - this.removeAttribute('aria-describedby'); - } - } - - /** @internal Returns true if the button only contains icons and no text. */ + /** Returns true if the button only contains icons and no text. */ #isIconOnly(): boolean { return !this.hasText && (!!this.defaultIcon || !!this.pressedIcon); } diff --git a/packages/components/tool-bar/src/tool-bar.stories.ts b/packages/components/tool-bar/src/tool-bar.stories.ts index a5cfbd0011..1bb5505f92 100644 --- a/packages/components/tool-bar/src/tool-bar.stories.ts +++ b/packages/components/tool-bar/src/tool-bar.stories.ts @@ -35,7 +35,6 @@ import { type SlToggleEvent } from '@sl-design-system/shared/events.js'; import { type ToggleButton } from '@sl-design-system/toggle-button'; import '@sl-design-system/toggle-button/register.js'; import '@sl-design-system/toggle-group/register.js'; -import { tooltip } from '@sl-design-system/tooltip'; import '@sl-design-system/tooltip/register.js'; import { type Meta, type StoryObj } from '@storybook/web-components-vite'; import { type TemplateResult, html, nothing } from 'lit'; @@ -168,13 +167,12 @@ export default { ${itemsOutsideContainer?.(args)} + aria-label="Tool bar example"> ${items?.(args)}
@@ -420,8 +418,7 @@ export const State: Story = { + fill="outline"> Toggle disabled state @@ -447,11 +444,11 @@ export const Tooltips: Story = { Bold - + - + ` @@ -508,8 +505,7 @@ export const IconOnly: Story = { align=${ifDefined(align)} fill=${ifDefined(fill)} style="inline-size: ${width ?? 'auto'}" - aria-label="Icon only tool bar with tooltips" - > + aria-label="Icon only tool bar with tooltips"> @@ -523,8 +519,7 @@ export const IconOnly: Story = { + fill="outline"> Underline (disabled) @@ -571,8 +566,7 @@ export const IconOnly: Story = { align=${ifDefined(align)} fill=${ifDefined(fill)} style="inline-size: ${width ?? 'auto'}" - aria-label="Icon only tool bar with aria-labels" - > + aria-label="Icon only tool bar with aria-labels"> @@ -753,8 +747,7 @@ export const Examples: Story = { aria-label="Page options" contained fill="outline" - style="inline-size: fit-content" - > + style="inline-size: fit-content"> ${pageOptions} + style="inline-size: fit-content"> ${pageOptions} @@ -776,16 +768,14 @@ export const Examples: Story = { contained inverted fill="ghost" - style="inline-size: fit-content" - > + style="inline-size: fit-content"> ${options} + style="inline-size: fit-content"> ${filteringAndSorting} @@ -793,8 +783,7 @@ export const Examples: Story = { aria-label="Filtering and sorting" inverted fill="ghost" - style="inline-size: fit-content" - > + style="inline-size: fit-content"> ${filteringAndSorting}
diff --git a/packages/components/tooltip/index.ts b/packages/components/tooltip/index.ts index 00b5d6e9a1..6fe0ddc668 100644 --- a/packages/components/tooltip/index.ts +++ b/packages/components/tooltip/index.ts @@ -1,2 +1 @@ export * from './src/tooltip.js'; -export * from './src/tooltip-directive.js'; diff --git a/packages/components/tooltip/src/edge-cases.stories.ts b/packages/components/tooltip/src/edge-cases.stories.ts index 91138fa6d8..1d790d4008 100644 --- a/packages/components/tooltip/src/edge-cases.stories.ts +++ b/packages/components/tooltip/src/edge-cases.stories.ts @@ -9,18 +9,12 @@ import '@sl-design-system/menu/register.js'; import '@sl-design-system/toggle-button/register.js'; import '@sl-design-system/toggle-group/register.js'; import { html } from 'lit'; -import { Tooltip2 } from './tooltip2.js'; - -try { - customElements.define('sl-tooltip2', Tooltip2); -} catch { - /* empty */ -} +import '../register.js'; Icon.register(faGear, faPen, faTrash, fasGear); export default { - title: 'Overlay/Tooltip2/Edge cases' + title: 'Overlay/Tooltip/Edge cases' }; export const DisabledButtons = { diff --git a/packages/components/tooltip/src/tooltip-directive.spec.ts b/packages/components/tooltip/src/tooltip-directive.spec.ts deleted file mode 100644 index 9065e55d60..0000000000 --- a/packages/components/tooltip/src/tooltip-directive.spec.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { - type ScopedElementsMap, - ScopedElementsMixin -} from '@open-wc/scoped-elements/lit-element.js'; -import { Button } from '@sl-design-system/button'; -import { type PopoverPosition } from '@sl-design-system/shared'; -import { fixture } from '@sl-design-system/vitest-browser-lit'; -import { LitElement, type TemplateResult, html } from 'lit'; -import { spy, stub } from 'sinon'; -import { describe, expect, it } from 'vitest'; -import { tooltip } from './tooltip-directive.js'; -import { Tooltip } from './tooltip.js'; - -describe('tooltip()', () => { - it('should create a lazy tooltip on the host element', async () => { - const lazySpy = spy(Tooltip, 'lazy'); - - const el = await fixture(html`
Host
`); - - expect(Tooltip.lazy).to.have.been.calledOnce; - expect(Tooltip.lazy).to.have.been.calledWith(el); - - lazySpy.restore(); - }); - - it('should log a warning if the custom element is not defined on the document', async () => { - const consoleWarnStub = stub(console, 'warn'); - - const el: HTMLElement = await fixture(html`
Host
`); - - // Trigger the lazy tooltip creation - el.focus(); - - expect(console.warn).to.have.been.calledOnce; - expect(console.warn).to.have.been.calledWith( - 'The sl-tooltip custom element is not defined in the document. Please make sure to register the sl-tooltip custom element in your application.' - ); - - consoleWarnStub.restore(); - }); - - it('should log a warning if the custom element is not defined on the target shadow root', async () => { - class TooltipNotDefined extends LitElement { - override render(): TemplateResult { - return html`
Host
`; - } - } - - try { - customElements.define('tooltip-not-defined', TooltipNotDefined); - } catch { - // empty - } - - const consoleWarnStub = stub(console, 'warn'); - - const el: LitElement = await fixture(html``), - hostEl = el.renderRoot.querySelector('div'); - - // Trigger the lazy tooltip creation - hostEl?.focus(); - - expect(console.warn).to.have.been.calledOnce; - expect(console.warn).to.have.been.calledWith( - 'The sl-tooltip custom element is not defined in the TOOLTIP-NOT-DEFINED element. Please make sure to register the sl-tooltip custom element in your application.' - ); - - consoleWarnStub.restore(); - }); - - it('should use the parent shadow root to create the tooltip custom element', async () => { - class TooltipDefined extends ScopedElementsMixin(LitElement) { - static get scopedElements(): ScopedElementsMap { - return { - 'sl-button': Button, - 'sl-tooltip': Tooltip - }; - } - - override render(): TemplateResult { - return html`Button`; - } - } - - try { - customElements.define('tooltip-defined', TooltipDefined); - } catch { - // empty - } - - const el: LitElement = await fixture(html``), - button = el.renderRoot.querySelector('sl-button'); - - // Append the host element to the document body - document.body.append(el); - - // Trigger the lazy tooltip creation - button?.focus(); - - const tt = button?.nextElementSibling; - expect(tt).to.match('sl-tooltip'); - expect(tt).to.have.text('content'); - expect(tt?.shadowRoot).be.instanceof(ShadowRoot); - - // Cleanup - el.remove(); - }); - - it('should pass tooltip options via config to the tooltip', async () => { - try { - if (!customElements.get('sl-tooltip')) { - customElements.define('sl-tooltip', Tooltip); - } - } catch { - // empty - } - - const lazySpy = spy(Tooltip, 'lazy'); - - const el: HTMLElement = await fixture( - html`
Host
` - ); - - // Trigger the lazy tooltip creation - el.focus(); - - expect(lazySpy).to.have.been.calledOnce; - expect(lazySpy.getCall(0).args[2]).to.deep.equal({ ariaRelation: 'label' }); - - const tooltipEl = el.nextElementSibling as Tooltip | null; - - expect(tooltipEl).to.exist; - expect(el.getAttribute('aria-labelledby')).to.equal(tooltipEl?.id ?? null); - - lazySpy.restore(); - }); - - it('should apply tooltip properties (position, maxWidth) from config to the tooltip', async () => { - try { - if (!customElements.get('sl-tooltip')) { - customElements.define('sl-tooltip', Tooltip); - } - } catch { - // empty - } - - const el: HTMLElement = await fixture(html` - Button - `); - - // Trigger the lazy tooltip creation - el.focus(); - - const tooltipEl = el.nextElementSibling as Tooltip | null; - - expect(tooltipEl).to.exist; - expect(tooltipEl?.position).to.equal('bottom'); - expect(tooltipEl?.maxWidth).to.equal(150); - expect(tooltipEl).to.have.text('Tooltip example'); - - // Cleanup - el.remove(); - }); -}); diff --git a/packages/components/tooltip/src/tooltip-directive.ts b/packages/components/tooltip/src/tooltip-directive.ts deleted file mode 100644 index d497e1245e..0000000000 --- a/packages/components/tooltip/src/tooltip-directive.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { render } from 'lit'; -import { AsyncDirective } from 'lit/async-directive.js'; -import { type ElementPart, directive } from 'lit/directive.js'; -import { Tooltip, TooltipOptions } from './tooltip.js'; - -/** Configuration options for the tooltip directive. */ -export type TooltipDirectiveConfig = Partial & TooltipProperties; - -/** Tooltip public properties that can be set. */ -type TooltipProperties = { - position?: Tooltip['position']; - maxWidth?: number; -}; - -type TooltipDirectiveParams = [content: unknown, config?: TooltipDirectiveConfig]; - -/** - * Provides a Lit directive tooltip that attaches a lazily created Tooltip instance to a host - * element. - */ -export class TooltipDirective extends AsyncDirective { - config: TooltipDirectiveConfig = {}; - content?: unknown; - part?: ElementPart; - tooltip?: Tooltip | (() => void); - - override disconnected(): void { - if (this.tooltip instanceof HTMLElement) { - this.tooltip.remove(); - } else if (this.tooltip) { - this.tooltip(); - } - - this.tooltip = undefined; - } - - override reconnected(): void { - this.#setup(); - } - - render(_content: unknown, _config?: TooltipDirectiveConfig): void {} - - renderContent(): void { - render(this.content, this.tooltip as Tooltip, this.part!.options); - } - - override update(part: ElementPart, [content, config]: TooltipDirectiveParams): void { - this.content = content; - - if (config) { - this.config = { ...this.config, ...config }; - } - - this.part = part; - - this.#setup(); - } - - #setup(): void { - if (this.part!.element) - this.tooltip ||= Tooltip.lazy( - this.part!.element, - tooltip => { - if (this.isConnected) { - this.tooltip = tooltip; - tooltip.position = this.config.position || 'top'; - tooltip.maxWidth = this.config.maxWidth; - this.renderContent(); - } - }, - { ariaRelation: this.config.ariaRelation } - ); - } -} - -export const tooltip = directive(TooltipDirective); diff --git a/packages/components/tooltip/src/tooltip-shared.spec.ts b/packages/components/tooltip/src/tooltip-shared.spec.ts deleted file mode 100644 index c3fd2e1196..0000000000 --- a/packages/components/tooltip/src/tooltip-shared.spec.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { type Button } from '@sl-design-system/button'; -import '@sl-design-system/button/register.js'; -import '@sl-design-system/button-bar/register.js'; -import { fixture } from '@sl-design-system/vitest-browser-lit'; -import { html } from 'lit'; -import { beforeEach, describe, expect, it } from 'vitest'; -import { userEvent } from 'vitest/browser'; -import '../register.js'; -import { Tooltip } from './tooltip.js'; - -describe('sl-tooltip shared', () => { - let el: HTMLElement; - let buttons: Button[]; - let tooltip: Tooltip; - - const waitFor = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - const waitForPopoverToClose = async (popover: HTMLElement, timeout = 1000): Promise => { - const startedAt = Date.now(); - - while (popover.matches(':popover-open') && Date.now() - startedAt < timeout) { - await waitFor(25); - } - - if (popover.matches(':popover-open')) { - const id = (popover as HTMLElement & { id?: string }).id; - throw new Error( - `Timed out after ${timeout}ms waiting for popover${id ? ` with id "${id}"` : ''} to close.` - ); - } - }; - - beforeEach(async () => { - el = await fixture(html` -
- Button 1 - Button 2 - Shared Tooltip -
- `); - buttons = Array.from(el.querySelectorAll('sl-button')); - tooltip = el.querySelector('sl-tooltip') as Tooltip; - }); - - it('should not stay open when moving rapidly between buttons and then out', async () => { - // 1. Hover first button - buttons[0].dispatchEvent(new Event('pointerover', { bubbles: true })); - await waitFor(Tooltip.hoverShowDelay + 50); - - expect(tooltip.matches(':popover-open')).to.be.true; - expect(tooltip.anchorElement).to.equal(buttons[0]); - - // 2. Move rapidly to second button (wait less than hover show delay) - // Dispatch pointerout on 0 and pointerover on 1 simultaneously (same tick) - buttons[0].dispatchEvent(new Event('pointerout', { bubbles: true })); - buttons[1].dispatchEvent(new Event('pointerover', { bubbles: true })); - - // 3. Immediately move out of second button (same tick or very next) - buttons[1].dispatchEvent(new Event('pointerout', { bubbles: true })); - await userEvent.hover(document.body); - - // Wait for any pending timers/event queue work. - await tooltip.updateComplete; - await waitForPopoverToClose(tooltip, Tooltip.hoverShowDelay + Tooltip.hoverHideDelay + 250); - - // The tooltip should be closed. - expect(tooltip.matches(':popover-open')).to.be.false; - }); - - it('should hide even if multiple pointerover events are fired for different buttons', async () => { - // Hover btn 1 - buttons[0].dispatchEvent(new Event('pointerover', { bubbles: true })); - await waitFor(Tooltip.hoverShowDelay + 50); - - expect(tooltip.matches(':popover-open')).to.be.true; - - // Pointerover btn 2 while the tooltip is already open; this should switch/update the active anchor. - // After pointerout from all anchors, the tooltip should still close. - buttons[1].dispatchEvent(new Event('pointerover', { bubbles: true })); - - // Now move out of BOTH (simulated jump out) - buttons[0].dispatchEvent(new Event('pointerout', { bubbles: true })); - buttons[1].dispatchEvent(new Event('pointerout', { bubbles: true })); - await userEvent.hover(document.body); - - await tooltip.updateComplete; - await waitForPopoverToClose(tooltip, Tooltip.hoverShowDelay + Tooltip.hoverHideDelay + 250); - expect(tooltip.matches(':popover-open')).to.be.false; - }); - - it('should update anchor immediately when moving between buttons while open', async () => { - // 1. Hover first button and wait for it to open - buttons[0].dispatchEvent(new Event('pointerover', { bubbles: true })); - await waitFor(Tooltip.hoverShowDelay + 50); - expect(tooltip.matches(':popover-open')).to.be.true; - expect(tooltip.anchorElement).to.equal(buttons[0]); - const firstInsetInlineStart = tooltip.style.insetInlineStart; - - // 2. Move to second button - // The anchor should update IMMEDIATELY without waiting for hover show delay again - buttons[1].dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - - expect(tooltip.anchorElement).to.equal(buttons[1]); - expect(tooltip.style.insetInlineStart).not.to.equal(firstInsetInlineStart); - }); - - it('should update anchor immediately when tabbing between buttons', async () => { - // 1. Focus first button and wait for it to open - buttons[0].focus(); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - expect(tooltip.matches(':popover-open')).to.be.true; - expect(tooltip.anchorElement).to.equal(buttons[0]); - - // 2. Focus second button - // The anchor should update IMMEDIATELY (well, after focusin and rAF) - buttons[1].focus(); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - - expect(tooltip.anchorElement).to.equal(buttons[1]); - }); - - it('should move the anchor to the next shared button when tabbing in a button bar', async () => { - const tabFixture = await fixture(html` -
- - Button 1 - Button 2 - Button 3 - - Shared Tooltip -
- `); - - const tabButtons = Array.from(tabFixture.querySelectorAll('sl-button')); - const tabTooltip = tabFixture.querySelector('sl-tooltip') as Tooltip; - - tabButtons[0].focus(); - await tabTooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tabTooltip.updateComplete; - - expect(tabTooltip.matches(':popover-open')).to.be.true; - expect(tabTooltip.anchorElement).to.equal(tabButtons[0]); - - await userEvent.tab(); - await tabTooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tabTooltip.updateComplete; - - expect(tabTooltip.anchorElement).to.equal(tabButtons[1]); - }); - - it('should reopen on a different shared button in a button bar after closing', async () => { - const sharedFixture = await fixture(html` -
- - Button 1 - Button 2 - Button 3 - - Shared Tooltip -
- `); - - const sharedButtons = Array.from(sharedFixture.querySelectorAll('sl-button')); - const sharedTooltip = sharedFixture.querySelector('sl-tooltip') as Tooltip; - - await userEvent.hover(sharedButtons[0]); - await sharedTooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await waitFor(Tooltip.hoverShowDelay + 10); - await sharedTooltip.updateComplete; - - expect(sharedTooltip.matches(':popover-open')).to.be.true; - expect(sharedTooltip.anchorElement).to.equal(sharedButtons[0]); - - await userEvent.hover(document.body); - await sharedTooltip.updateComplete; - await waitForPopoverToClose(sharedTooltip, 250); - expect(sharedTooltip.matches(':popover-open')).to.be.false; - - await userEvent.hover(sharedButtons[1]); - await sharedTooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await waitFor(Tooltip.hoverShowDelay + 10); - await sharedTooltip.updateComplete; - - expect(sharedTooltip.matches(':popover-open')).to.be.true; - expect(sharedTooltip.anchorElement).to.equal(sharedButtons[1]); - }); - - it('should close after rapid pointer transitions for shared anchors connected via ElementInternals', async () => { - const internalsFixture = await fixture(html` -
- Button 1 - Button 2 - Shared Tooltip -
- `); - - const internalsButtons = Array.from(internalsFixture.querySelectorAll`; - } -} - -if (!customElements.get('tooltip-assigned-slot-host')) { - customElements.define('tooltip-assigned-slot-host', TooltipAssignedSlotHost); -} - -if (!customElements.get('tooltip-assigned-slot-anchor')) { - customElements.define('tooltip-assigned-slot-anchor', TooltipAssignedSlotAnchor); -} - -describe('sl-tooltip', () => { - let el: HTMLElement; - let button: Button; - let tooltip: Tooltip; - - const waitFor = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - - const showTooltip = async () => { - const pointerOverEvent = new Event('pointerover', { bubbles: true }); - button?.dispatchEvent(pointerOverEvent); - await tooltip.updateComplete; - return await waitFor(Tooltip.hoverShowDelay + 10); - }; - - describe('defaults', () => { - beforeEach(async () => { - el = await fixture(html` -
- Button element - Message with lots of long text, that exceeds 150px easily -
- `); - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - }); - - it('should not show the tooltip by default', () => { - expect(tooltip.matches(':popover-open')).to.be.false; - }); - - it('should toggle the tooltip on focusin and focusout', async () => { - el = await fixture(html` -
- - - Message with lots of long text, that exceeds 150px easily -
- `); - const focusButton = el.querySelector('#focus-target')!, - outsideButton = el.querySelector('#outside-target')!; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - await tooltip.updateComplete; - - const originalMatches = Element.prototype.matches; - const focusVisibleSpy = vi.spyOn(Element.prototype, 'matches').mockImplementation(function ( - this: Element, - selector: string - ): boolean { - if (selector === ':focus-visible' && this === focusButton) { - return true; - } - - return originalMatches.call(this, selector); - }); - - try { - focusButton.focus(); - focusButton.dispatchEvent(new Event('focusin', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - - expect(tooltip.matches(':popover-open')).to.be.true; - } finally { - focusVisibleSpy.mockRestore(); - } - - focusButton.blur(); - focusButton.dispatchEvent(new Event('focusout', { bubbles: true, composed: true })); - outsideButton.focus(); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - - expect(tooltip.matches(':popover-open')).to.be.false; - }); - - it('should toggle the tooltip on pointerover and pointerout', async () => { - const pointerOver = new Event('pointerover', { bubbles: true }); - button?.dispatchEvent(pointerOver); - await waitFor(Tooltip.hoverShowDelay + 10); - expect(tooltip.matches(':popover-open')).to.be.true; - - const pointerEvent = new Event('pointerout', { bubbles: true }); - button?.dispatchEvent(pointerEvent); - await waitFor(Tooltip.hoverHideDelay + 10); - expect(tooltip.matches(':popover-open')).to.be.false; - }); - - it('should not run hide logic on pointerout when tooltip is closed', async () => { - const hidePopoverSpy = vi.spyOn(tooltip, 'hidePopover'); - - try { - expect(isPopoverOpen(tooltip)).to.be.false; - - tooltip.dispatchEvent(new Event('pointerout', { bubbles: true })); - await waitFor(10); - - expect(hidePopoverSpy).not.toHaveBeenCalled(); - } finally { - hidePopoverSpy.mockRestore(); - } - }); - - it('should ignore unrelated focusout events while open', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await waitFor(Tooltip.hoverShowDelay + 10); - expect(tooltip.matches(':popover-open')).to.be.true; - - const other = document.createElement('input'); - el.appendChild(other); - other.dispatchEvent(new Event('focusout', { bubbles: true, composed: true })); - - await tooltip.updateComplete; - expect(tooltip.matches(':popover-open')).to.be.true; - }); - - it('should not switch to focus-open mode on focusin without focus-visible when already open', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await waitFor(Tooltip.hoverShowDelay + 10); - expect(tooltip.matches(':popover-open')).to.be.true; - - button?.dispatchEvent(new Event('focusin', { bubbles: true, composed: true })); - button?.dispatchEvent(new Event('focusout', { bubbles: true, composed: true })); - - await tooltip.updateComplete; - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - - it('should still close on focusout after pointerover when it was opened by focus', async () => { - button?.focus(); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - expect(tooltip.matches(':popover-open')).to.be.true; - - // Pointer over while open should not flip internal mode from focus-open to hover-open. - button?.dispatchEvent(new Event('pointerover', { bubbles: true, composed: true })); - - const originalMatches = button.matches.bind(button); - const matchesSpy = vi - .spyOn(button, 'matches') - .mockImplementation((selector: string): boolean => { - if ( - selector === ':hover' || - selector === ':focus-visible' || - selector === ':focus-within' - ) { - return false; - } - - return originalMatches(selector); - }); - - try { - button?.blur(); - button?.dispatchEvent(new Event('focusout', { bubbles: true, composed: true })); - await waitFor(10); - - expect(tooltip.matches(':popover-open')).to.be.false; - } finally { - matchesSpy.mockRestore(); - } - }); - - it('should stay open on focusout when the anchor remains hovered', async () => { - await tooltip.updateComplete; - - const proxyTarget = ( - button as HTMLElement & { getProxyTarget?(): Element | null } - ).getProxyTarget?.(), - originalElementMatches = Element.prototype.matches; - const focusVisibleSpy = vi.spyOn(Element.prototype, 'matches').mockImplementation(function ( - this: Element, - selector: string - ): boolean { - if (selector === ':focus-visible' && (this === button || this === proxyTarget)) { - return true; - } - - return originalElementMatches.call(this, selector); - }); - - button?.focus(); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - focusVisibleSpy.mockRestore(); - - expect(tooltip.matches(':popover-open')).to.be.true; - - const proxyTargetForHover = ( - button as HTMLElement & { getProxyTarget?(): Element | null } - ).getProxyTarget?.(), - currentAnchor = tooltip.anchorElement, - originalMatches = Element.prototype.matches; - const matchesSpy = vi.spyOn(Element.prototype, 'matches').mockImplementation(function ( - this: Element, - selector: string - ): boolean { - const isAnchorTarget = - this === button || this === proxyTargetForHover || this === currentAnchor; - - if (selector === ':hover' && isAnchorTarget) { - return true; - } - - if (selector === ':focus-within' && isAnchorTarget) { - return false; - } - - return originalMatches.call(this, selector); - }); - - try { - button?.blur(); - button?.dispatchEvent(new Event('focusout', { bubbles: true, composed: true })); - await waitFor(10); - - expect(tooltip.matches(':popover-open')).to.be.true; - } finally { - matchesSpy.mockRestore(); - } - }); - - it('should restore the tooltip to the focused shared anchor after unhovering another shared anchor', async () => { - el = await fixture(html` -
- - - Shared tooltip -
- `); - - const firstButton = el.querySelector('#first')!, - secondButton = el.querySelector('#second')!; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - await tooltip.updateComplete; - - const originalMatches = Element.prototype.matches; - const focusVisibleSpy = vi.spyOn(Element.prototype, 'matches').mockImplementation(function ( - this: Element, - selector: string - ): boolean { - if (selector === ':focus-visible' && this === firstButton) { - return true; - } - - return originalMatches.call(this, selector); - }); - - firstButton.focus(); - firstButton.dispatchEvent(new Event('focusin', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - focusVisibleSpy.mockRestore(); - - expect(tooltip.matches(':popover-open')).to.be.true; - expect(tooltip.anchorElement).to.equal(firstButton); - - secondButton.dispatchEvent(new Event('pointerover', { bubbles: true, composed: true })); - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - expect(tooltip.anchorElement).to.equal(secondButton); - - secondButton.dispatchEvent(new Event('pointerout', { bubbles: true, composed: true })); - await waitFor(Tooltip.hoverHideDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - expect(tooltip.anchorElement).to.equal(firstButton); - }); - - it('should toggle the tooltip on focus and Escape key pressed', async () => { - button?.focus(); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - expect(tooltip.matches(':popover-open')).to.be.true; - - await userEvent.keyboard('{Escape}'); - - expect(tooltip.matches(':popover-open')).to.be.false; - }); - - it('should toggle the tooltip on pointerover and Escape key pressed', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await waitFor(Tooltip.hoverShowDelay + 10); - expect(tooltip.matches(':popover-open')).to.be.true; - - await userEvent.keyboard('{Escape}'); - - expect(tooltip.matches(':popover-open')).to.be.false; - }); - - it('should be positioned at the top by default', () => { - expect(tooltip.position).to.equal('top'); - }); - - it('should set the position to the position option chosen', async () => { - tooltip.setAttribute('position', 'bottom'); - await tooltip.updateComplete; - - expect(tooltip.position).to.equal('bottom'); - }); - - it('should not have a maxWidth by default', async () => { - tooltip.setAttribute('max-width', '150'); - await tooltip.updateComplete; - - await showTooltip(); - - expect(getComputedStyle(tooltip).maxInlineSize).to.equal('150px'); - }); - - it('should show the tooltip on sl-close dispatched outside the tooltip root while anchor keeps focus', async () => { - button?.focus(); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - - expect(tooltip.matches(':popover-open')).to.be.true; - - // Keep the anchor focused, but close the tooltip first so reopening can only come from sl-close handling. - if (tooltip.matches(':popover-open')) { - tooltip.hidePopover(); - } - - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - expect(tooltip.matches(':popover-open')).to.be.false; - - // Dispatch sl-close from outside the tooltip root (e.g. document-level overlay close). - const overlay = document.createElement('div'); - document.body.append(overlay); - overlay.dispatchEvent(new CustomEvent('sl-close', { bubbles: true, composed: true })); - overlay.remove(); - - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - - it('should keep the tooltip open when focus moves between focusable children in the same anchor', async () => { - el = await fixture(html` -
-
- - -
- Message with lots of long text, that exceeds 150px easily -
- `); - - tooltip = el.querySelector('sl-tooltip') as Tooltip; - - const [firstChildButton, secondChildButton] = Array.from( - el.querySelectorAll('button') - ); - - firstChildButton.focus(); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - - expect(tooltip.matches(':popover-open')).to.be.true; - - secondChildButton.focus(); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - }); - - describe('linked via aria-labelledby', () => { - beforeEach(async () => { - el = await fixture(html` -
- Button element - Message with lots of long text, that exceeds 150px easily -
- `); - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - }); - - it('should show the tooltip on pointerover', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - }); - - describe('multiple ids', () => { - beforeEach(async () => { - el = await fixture(html` -
- Other element - Button - Tooltip message -
- `); - - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - }); - - it('should show the tooltip when its id is one of multiple ids in aria-describedby', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - - it('should hide the tooltip on pointerout when its id is one of multiple ids', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - - button?.dispatchEvent(new Event('pointerout', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverHideDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.false; - }); - }); - - describe('multiple ids with aria-labelledby', () => { - beforeEach(async () => { - el = await fixture(html` -
- Other label - Button with multiple label ids - - Tooltip label -
- `); - - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - }); - - it('should show the tooltip when its id is one of multiple ids in aria-labelledby', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - }); - - describe('ElementInternals ariaDescribedByElements', () => { - beforeEach(async () => { - el = await fixture(html` -
- Button - Tooltip via ElementInternals -
- `); - - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - - // Manually set ariaDescribedByElements - if (button.internals) { - button.internals.ariaDescribedByElements = [tooltip]; - } - }); - - it('should show tooltip when referenced via ElementInternals ariaDescribedByElements', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - - it('should hide tooltip on pointerout when referenced via ElementInternals', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - - button?.dispatchEvent(new Event('pointerout', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverHideDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.false; - }); - - it('should show tooltip on focus when referenced via ElementInternals', async () => { - button?.focus(); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - }); - - describe('ElementInternals ariaLabelledByElements', () => { - beforeEach(async () => { - el = await fixture(html` -
- Button - Tooltip label via ElementInternals -
- `); - - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - - // Manually set ariaLabelledByElements - if (button.internals) { - button.internals.ariaLabelledByElements = [tooltip]; - } - }); - - it('should show tooltip when referenced via ElementInternals ariaLabelledByElements', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - - it('should hide tooltip on pointerout when referenced via ElementInternals ariaLabelledByElements', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - - button?.dispatchEvent(new Event('pointerout', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverHideDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.false; - }); - }); - - describe('ElementInternals with multiple elements', () => { - let otherElement: HTMLElement; - - beforeEach(async () => { - el = await fixture(html` -
- Other element - Button - Tooltip message -
- `); - - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - otherElement = el.querySelector('#other-element') as HTMLElement; - - // Set multiple elements in ariaDescribedByElements - if (button.internals) { - button.internals.ariaDescribedByElements = [otherElement, tooltip]; - } - }); - - it('should show tooltip when it is one of multiple elements in ariaDescribedByElements', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - }); - - describe('Element ariaDescribedByElements', () => { - beforeEach(async () => { - el = await fixture(html` -
- Button - Tooltip via Element property -
- `); - - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - - // Set ariaDescribedByElements directly on the element (not via ElementInternals) - button.ariaDescribedByElements = [tooltip]; - }); - - it('should show tooltip when referenced via Element ariaDescribedByElements', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - - it('should hide tooltip on pointerout when referenced via Element ariaDescribedByElements', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - - button?.dispatchEvent(new Event('pointerout', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverHideDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.false; - }); - - it('should show tooltip on focus when referenced via Element ariaDescribedByElements', async () => { - button?.focus(); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - }); - - describe('Element ariaLabelledByElements', () => { - beforeEach(async () => { - el = await fixture(html` -
- Button - Tooltip label via Element property -
- `); - - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - - // Set ariaLabelledByElements directly on the element (not via ElementInternals) - button.ariaLabelledByElements = [tooltip]; - }); - - it('should show tooltip when referenced via Element ariaLabelledByElements', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - - it('should hide tooltip on pointerout when referenced via Element ariaLabelledByElements', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - - button?.dispatchEvent(new Event('pointerout', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverHideDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.false; - }); - - it('should show tooltip on focus when referenced via Element ariaLabelledByElements', async () => { - button?.focus(); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - }); - - describe('with an open popover', () => { - let menuButton: MenuButton; - let innerButton: Button; - let menu: Menu; - - beforeEach(async () => { - el = await fixture(html` -
- - Settings - Rename... - Delete... - - Open settings menu -
- `); - - menuButton = el.querySelector('sl-menu-button') as MenuButton; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - - // Wait for menu-button to set up aria references on the inner button - await tooltip.updateComplete; - - innerButton = menuButton.shadowRoot!.querySelector('sl-button') as Button; - menu = menuButton.shadowRoot!.querySelector('sl-menu') as Menu; - }); - - it('should show tooltip on pointerover of the menu-button', async () => { - menuButton.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - - it('should not show tooltip on pointerover of a menu item when the menu is open', async () => { - innerButton.click(); - await tooltip.updateComplete; - - expect(menu.matches(':popover-open')).to.be.true; - - const menuItem = el.querySelector('sl-menu-item') as HTMLElement; - - menuItem.dispatchEvent(new Event('pointerover', { bubbles: true, composed: true })); - await tooltip.updateComplete; - - expect(tooltip.matches(':popover-open')).to.be.false; - }); - - it('should not show tooltip on focusin of a menu item when the menu is open', async () => { - innerButton.click(); - await tooltip.updateComplete; - - expect(menu.matches(':popover-open')).to.be.true; - - const menuItem = el.querySelector('sl-menu-item') as HTMLElement; - - menuItem.focus(); - await tooltip.updateComplete; - - expect(tooltip.matches(':popover-open')).to.be.false; - }); - - it('should show tooltip on pointerover of the menu-button after the menu is closed', async () => { - innerButton.click(); - await tooltip.updateComplete; - innerButton.click(); - await tooltip.updateComplete; - - expect(menu.matches(':popover-open')).to.be.false; - - menuButton.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - }); - }); - - describe('with a slotted anchor inside a shadow-root host', () => { - let assignedSlotHost: TooltipAssignedSlotHost; - let anchor: Button; - let internalDescription: HTMLElement; - let secondaryDescription: HTMLElement; - let outsideButton: HTMLButtonElement; - let proxyTarget: HTMLButtonElement; - - beforeEach(async () => { - el = await fixture(html` -
- - Anchor - - - Tooltip via assigned slot -
- `); - - assignedSlotHost = el.querySelector('tooltip-assigned-slot-host') as TooltipAssignedSlotHost; - anchor = el.querySelector('sl-button') as Button; - outsideButton = el.querySelector('#outside-focus-target') as HTMLButtonElement; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - internalDescription = assignedSlotHost.shadowRoot!.querySelector( - '#internal-description' - ) as HTMLElement; - secondaryDescription = assignedSlotHost.shadowRoot!.querySelector( - '#secondary-description' - ) as HTMLElement; - proxyTarget = anchor.renderRoot.querySelector('button') as HTMLButtonElement; - - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - }); - - it('should move into the assigned slot root, preserve internals relations, and reopen on repeated focus', async () => { - expect(anchor.assignedSlot).to.exist; - expect(anchor.assignedSlot?.getRootNode()).to.equal(assignedSlotHost.shadowRoot); - - anchor.internals.ariaDescribedByElements = [internalDescription, secondaryDescription]; - proxyTarget.focus(); - document.dispatchEvent(new CustomEvent('sl-close', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - - expect(tooltip.getRootNode()).to.equal(assignedSlotHost.shadowRoot); - expect(tooltip.matches(':popover-open')).to.be.true; - expect([...anchor.internals.ariaDescribedByElements]).to.include.members([ - internalDescription, - secondaryDescription, - tooltip - ]); - expect(tooltip.anchorElement).to.equal(anchor); - - proxyTarget.blur(); - proxyTarget.dispatchEvent(new Event('focusout', { bubbles: true, composed: true })); - outsideButton.focus(); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - - expect(tooltip.matches(':popover-open')).to.be.false; - - proxyTarget.focus(); - document.dispatchEvent(new CustomEvent('sl-close', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - - expect(tooltip.matches(':popover-open')).to.be.true; - expect(tooltip.anchorElement).to.equal(anchor); - expect([...anchor.internals.ariaDescribedByElements]).to.include.members([ - internalDescription, - secondaryDescription, - tooltip - ]); - }); - }); - - describe('with a slotted proxy anchor inside a shadow-root host', () => { - let assignedSlotHost: TooltipAssignedSlotHost; - let anchor: TooltipAssignedSlotAnchor; - let anchorContent: HTMLElement; - - beforeEach(async () => { - el = await fixture(html` -
- - - Anchor - - - Tooltip via nested slot path -
- `); - - assignedSlotHost = el.querySelector('tooltip-assigned-slot-host') as TooltipAssignedSlotHost; - anchor = el.querySelector('tooltip-assigned-slot-anchor') as TooltipAssignedSlotAnchor; - anchorContent = el.querySelector('#anchor-content') as HTMLElement; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - }); - - it('should ignore unrelated slots from the event path when finding the assigned slot root', async () => { - anchorContent.dispatchEvent(new Event('pointerover', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - expect(tooltip.anchorElement).to.equal(anchor); - expect(tooltip.getRootNode()).to.equal(assignedSlotHost.shadowRoot); - }); - }); - - describe('with shared slotted proxy anchors inside a shadow-root host', () => { - let assignedSlotHost: TooltipAssignedSlotHost; - let anchors: TooltipAssignedSlotAnchor[]; - let anchorContents: HTMLElement[]; - - beforeEach(async () => { - el = await fixture(html` -
- - - Anchor 1 - - - Anchor 2 - - - Shared tooltip via proxy targets -
- `); - - assignedSlotHost = el.querySelector('tooltip-assigned-slot-host') as TooltipAssignedSlotHost; - anchors = Array.from(el.querySelectorAll('tooltip-assigned-slot-anchor')); - anchorContents = [ - el.querySelector('#shared-anchor-content-1') as HTMLElement, - el.querySelector('#shared-anchor-content-2') as HTMLElement - ]; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - }); - - it('should reopen for another shared proxy anchor after moving into the assigned slot root', async () => { - anchorContents[0].dispatchEvent(new Event('pointerover', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - expect(tooltip.anchorElement).to.equal(anchors[0]); - expect(tooltip.getRootNode()).to.equal(assignedSlotHost.shadowRoot); - - anchorContents[0].dispatchEvent(new Event('pointerout', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverHideDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.false; - - anchorContents[1].dispatchEvent(new Event('pointerover', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - expect(tooltip.anchorElement).to.equal(anchors[1]); - expect(tooltip.getRootNode()).to.equal(assignedSlotHost.shadowRoot); - }); - }); - - describe('with a slotted native anchor inside a shadow-root host', () => { - let assignedSlotHost: TooltipAssignedSlotHost; - let anchor: HTMLButtonElement; - let externalDescription: HTMLElement; - - beforeEach(async () => { - el = await fixture(html` -
- - - - External description - Tooltip via assigned slot -
- `); - - assignedSlotHost = el.querySelector('tooltip-assigned-slot-host') as TooltipAssignedSlotHost; - anchor = el.querySelector('#native-anchor') as HTMLButtonElement; - externalDescription = el.querySelector('#external-description') as HTMLElement; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - }); - - it('should keep explicit aria-describedby anchors in the light DOM when they cannot preserve the relation after reparenting', async () => { - expect(anchor.assignedSlot?.getRootNode()).to.equal(assignedSlotHost.shadowRoot); - - anchor.dispatchEvent(new Event('pointerover', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - expect(tooltip.getRootNode()).to.equal(anchor.ownerDocument); - expect(Array.from(anchor.ariaDescribedByElements ?? [])).to.include.members([ - externalDescription, - tooltip - ]); - }); - }); - - describe('with native anchors that keep the tooltip in the light DOM', () => { - let slottedAnchor: HTMLButtonElement; - let defaultAnchor: HTMLButtonElement; - - beforeEach(async () => { - el = await fixture(html` -
- - - - - External description - Tooltip via assigned slot -
- `); - - slottedAnchor = el.querySelector('#slotted-anchor') as HTMLButtonElement; - defaultAnchor = el.querySelector('#default-anchor') as HTMLButtonElement; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - }); - - it('should sync and clear the tooltip slot when reparenting is skipped', async () => { - slottedAnchor.dispatchEvent(new Event('pointerover', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.getAttribute('slot')).to.equal('actions'); - - slottedAnchor.dispatchEvent(new Event('pointerout', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverHideDelay + 10); - - defaultAnchor.dispatchEvent(new Event('pointerover', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.hasAttribute('slot')).to.be.false; - }); - }); - - describe('when switching from a shadow-root anchor to a light DOM anchor', () => { - let assignedSlotHost: TooltipAssignedSlotHost; - let shadowAnchor: Button; - let nativeAnchor: HTMLButtonElement; - let externalDescription: HTMLElement; - - beforeEach(async () => { - el = await fixture(html` -
- - Shadow anchor - - - External description - Tooltip via assigned slot -
- `); - - assignedSlotHost = el.querySelector('tooltip-assigned-slot-host') as TooltipAssignedSlotHost; - shadowAnchor = el.querySelector('#shadow-anchor') as Button; - nativeAnchor = el.querySelector('#native-anchor') as HTMLButtonElement; - externalDescription = el.querySelector('#external-description') as HTMLElement; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - }); - - it('should move back into the current anchor tree when reparenting is not allowed', async () => { - shadowAnchor.dispatchEvent(new Event('pointerover', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.getRootNode()).to.equal(assignedSlotHost.shadowRoot); - - shadowAnchor.dispatchEvent(new Event('pointerout', { bubbles: true, composed: true })); - nativeAnchor.dispatchEvent(new Event('pointerover', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - expect(tooltip.getRootNode()).to.equal(nativeAnchor.getRootNode()); - expect(tooltip.previousElementSibling).to.equal(nativeAnchor); - expect(Array.from(nativeAnchor.ariaDescribedByElements ?? [])).to.include.members([ - externalDescription, - tooltip - ]); - }); - }); - - describe('Tooltip lazy()', () => { - let el: HTMLElement, button: Button, innerButton: HTMLButtonElement, tooltip: Tooltip; - - beforeEach(async () => { - el = await fixture(html` -
- Button -
- `); - - button = el.querySelector('sl-button')!; - innerButton = button.renderRoot.querySelector('button')!; - }); - - it('should create a tooltip lazily on pointerover with default aria-describedby', async () => { - Tooltip.lazy(button, createdTooltip => (tooltip = createdTooltip)); - - button.dispatchEvent(new Event('pointerover', { bubbles: true })); - - await tooltip.updateComplete; - - expect(tooltip).to.exist; - expect(tooltip!.id).to.match(/sl-tooltip-(\d+)/); - - const describedBy = button.getAttribute('aria-describedby'), - describedByElements = button.ariaDescribedByElements ?? [], - proxyTarget = ( - button as HTMLElement & { getProxyTarget?(): Element | null } - ).getProxyTarget?.(), - proxyDescribedBy = - proxyTarget instanceof Element ? proxyTarget.getAttribute('aria-describedby') : null, - proxyDescribedByElements = - proxyTarget instanceof Element && 'ariaDescribedByElements' in proxyTarget - ? (proxyTarget.ariaDescribedByElements ?? []) - : [], - hasAriaDescribedBy = - describedBy?.split(/\s+/).includes(tooltip!.id) === true || - describedByElements.includes(tooltip); - - expect( - hasAriaDescribedBy || - proxyDescribedBy?.split(/\s+/).includes(tooltip!.id) === true || - proxyDescribedByElements.includes(tooltip) - ).to.be.true; - expect(button.hasAttribute('aria-labelledby')).to.be.false; - - await waitFor(Tooltip.hoverShowDelay + 10); - expect(tooltip.matches(':popover-open')).to.be.true; - expect(innerButton.ariaDescribedByElements).to.include(tooltip); - expect(innerButton.ariaLabelledByElements).to.be.null; - }); - - it('should keep pending hover show timer cancellable after unrelated focusout', async () => { - Tooltip.lazy(button, createdTooltip => { - tooltip = createdTooltip; - }); - - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - expect(tooltip).to.exist; - - const other = document.createElement('input'); - el.appendChild(other); - other.dispatchEvent(new Event('focusout', { bubbles: true, composed: true })); - - button?.dispatchEvent(new Event('pointerout', { bubbles: true })); - await waitFor(Tooltip.hoverShowDelay + 50); - - expect(tooltip.matches(':popover-open')).to.be.false; - }); - - it('should reopen on repeated hover for a button that forwards ARIA to an inner control', async () => { - Tooltip.lazy(button, createdTooltip => { - tooltip = createdTooltip; - }); - - button.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - expect(innerButton.ariaDescribedByElements).to.include(tooltip); - - innerButton.dispatchEvent(new Event('pointerout', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverHideDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.false; - - innerButton.dispatchEvent(new Event('pointerover', { bubbles: true, composed: true })); - await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - expect(innerButton.ariaDescribedByElements).to.include(tooltip); - }); - - it('should create a tooltip lazily on focusin', async () => { - Tooltip.lazy(button, createdTooltip => (tooltip = createdTooltip)); - - button.focus(); - - await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await tooltip.updateComplete; - - expect(tooltip).to.exist; - expect(tooltip.matches(':popover-open')).to.be.true; - expect(innerButton.ariaDescribedByElements).to.include(tooltip); - }); - - it('should use aria-labelledby when ariaRelation is label', async () => { - Tooltip.lazy(button, createdTooltip => (tooltip = createdTooltip), { ariaRelation: 'label' }); - - button.dispatchEvent(new Event('pointerover', { bubbles: true })); - - await tooltip.updateComplete; - - expect(tooltip).to.exist; - expect(innerButton.ariaDescribedByElements).to.be.null; - expect(innerButton.ariaLabelledByElements).to.include(tooltip); - }); - - it('should only create the tooltip once', async () => { - Tooltip.lazy(button, createdTooltip => (tooltip = createdTooltip)); - - button.dispatchEvent(new Event('pointerover', { bubbles: true })); - button.dispatchEvent(new Event('pointerover', { bubbles: true })); // second should be ignored - - await tooltip.updateComplete; - - expect(el.querySelectorAll('sl-tooltip')).to.have.lengthOf(1); - expect(tooltip).to.exist; - }); - }); - - describe('delay semantics', () => { - beforeEach(() => { - vi.useFakeTimers(); - }); - - beforeEach(async () => { - el = await fixture(html` -
- Button - Tooltip -
- `); - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it('should stay closed before the fixed hover show delay elapses', async () => { - const preShowWait = Math.max(0, Math.floor(Tooltip.hoverShowDelay * 0.5)); - button.dispatchEvent(new Event('pointerover', { bubbles: true })); - await vi.advanceTimersByTimeAsync(preShowWait); - expect(tooltip.matches(':popover-open')).to.be.false; - - await vi.advanceTimersByTimeAsync(Tooltip.hoverShowDelay - preShowWait + 10); - expect(tooltip.matches(':popover-open')).to.be.true; - }); - - it('should stay open before the fixed hover hide delay elapses', async () => { - button.dispatchEvent(new Event('pointerover', { bubbles: true })); - await vi.advanceTimersByTimeAsync(Tooltip.hoverShowDelay + 10); - expect(tooltip.matches(':popover-open')).to.be.true; - - button.dispatchEvent(new Event('pointerout', { bubbles: true })); - await vi.advanceTimersByTimeAsync(Math.max(0, Tooltip.hoverHideDelay - 50)); - expect(tooltip.matches(':popover-open')).to.be.true; - - await vi.advanceTimersByTimeAsync(100); - expect(tooltip.matches(':popover-open')).to.be.false; - }); - }); -}); diff --git a/packages/components/tooltip/src/tooltip.stories.ts b/packages/components/tooltip/src/tooltip.stories.ts index e251b6a042..27a2817f30 100644 --- a/packages/components/tooltip/src/tooltip.stories.ts +++ b/packages/components/tooltip/src/tooltip.stories.ts @@ -1,330 +1,106 @@ import '@sl-design-system/button/register.js'; -import '@sl-design-system/button-bar/register.js'; -import '@sl-design-system/dialog/register.js'; -import '@sl-design-system/icon/register.js'; -import '@sl-design-system/spinner/register.js'; -import { type Meta, type StoryObj } from '@storybook/web-components-vite'; -import { type TemplateResult, html } from 'lit'; -import { styleMap } from 'lit/directives/style-map.js'; +import { Meta } from '@storybook/web-components-vite'; +import { TemplateResult, html, nothing } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; import '../register.js'; -import { tooltip } from './tooltip-directive.js'; -import { type Tooltip } from './tooltip.js'; - -type Props = Pick & { - alignSelf: string; - justifySelf: string; - example?(props: Props): TemplateResult; - message: string; +import { Tooltip } from './tooltip.js'; + +type Props = Pick & { + maxWidth: number; + position: string; + showHoverBridge: boolean; + text: string; + tooltip(): TemplateResult; + trigger: string[]; }; -type Story = StoryObj; export default { title: 'Overlay/Tooltip', - args: { - alignSelf: 'center', - justifySelf: 'center', - maxWidth: 160, - message: 'This is the tooltip message', - position: 'top' + parameters: { + layout: 'centered' }, argTypes: { - alignSelf: { - control: 'inline-radio', - options: ['start', 'center', 'end'] + disabled: { + control: 'boolean' }, - example: { - table: { disable: true } + maxWidth: { + control: 'number' }, - justifySelf: { - control: 'inline-radio', - options: ['start', 'center', 'end'] + open: { + control: 'boolean' }, position: { - control: 'select', - options: [ - 'top', - 'top-start', - 'top-end', - 'right', - 'right-start', - 'right-end', - 'bottom', - 'bottom-start', - 'bottom-end', - 'left', - 'left-start', - 'left-end' - ] + control: 'inline-radio', + options: ['top', 'right', 'bottom', 'left'] + }, + showHoverBridge: { + control: 'boolean' + }, + text: { + control: 'text' + }, + tooltip: { + table: { disable: true } + }, + trigger: { + control: 'inline-check', + options: ['click', 'hover', 'focus', 'manual'] } }, - render: props => { - const { alignSelf, example, justifySelf, message, position, maxWidth } = props; - - return html` - - ${example - ? example?.(props) - : html` - - Button - - ${message} - `} - `; - } + args: { + text: 'Tooltip text' + }, + render: ({ disabled, maxWidth, open, position, showHoverBridge, text, tooltip, trigger }) => html` + Anchor + ${tooltip + ? tooltip() + : html` + + ${text} + + `} + + ` } satisfies Meta; -export const Basic: Story = {}; +export const Basic = {}; -export const Directive: Story = { +export const ClickTrigger = { args: { - example: ({ alignSelf, justifySelf, message }) => html` - - Button - - ` - } -}; - -export const DirectiveWithOptions: Story = { - render: () => { - return html` - -

- This story demonstrates hot to use the tooltip directive with some inline options (custom - 'ariaRelation', custom 'position' and 'maxWidth') on a sl-button. The example - shows how to add a tooltip directly without a separate sl-tooltip element. -

- -
- - - -
- `; + text: 'Click again to dismiss', + trigger: ['click'] } }; -export const Disabled: Story = { +export const Disabled = { args: { - example: ({ alignSelf, justifySelf, message }) => html` -
- Disabled button - Disabled (ARIA only) button -
- ` + disabled: true } }; -export const Shared: Story = { +export const HoverBridge = { args: { - example: ({ alignSelf, justifySelf, message }) => html` - - We - all - share - the - same - tooltip - - ${message} - ` - }, - parameters: { - // Notifies Chromatic to pause the animations at the first frame for this specific story. - chromatic: { pauseAnimationAtEnd: false, prefersReducedMotion: 'reduce' } + maxWidth: 200, + showHoverBridge: true, + text: 'The hotpink area bridges the area between anchor and tooltip, making it possible to move the mouse from the anchor to the tooltip without it disappearing.' } }; -export const NestedChildren: Story = { +export const Positions = { args: { - example: ({ message }) => html` - - This example is not necessarily a good practice, but it shows that the tooltip can be used on - an element that has many (interactive) child elements. -
console.log('Div clicked', e)} - tabindex="0" - > - Some button -

- The div has a tooltip attached, hovering over the child elements will not cause the - tooltip to dissapear. -

-

- Please beware when using the tooltip in a similar scenario: Not all elements are reachable - by all screenreaders. A div for example, without any interactions or a role, will not be - announced in a special way by the screenreader, so it also has no "stop" to read out the - contents of the tooltip. -

-

- Tooltips will be shown for user using keyboard navigation when the element has focus. That - means you can only use tooltips on elements that are focusable, like buttons or links. If - the element you want to describe can not have the focus you will need to use something - like an info button that will show the tooltip. - Some button -

-
-
- - - -
- Tooltip on the div - ${message} + tooltip: () => html` + Top + Right + Bottom + Left ` } }; - -export const IconButton: Story = { - render: () => { - return html` - - - - - - This is the tooltip message that labels the icon only button. - - `; - } -}; - -export const All: Story = { - render: () => { - setTimeout(() => { - document.querySelectorAll('sl-button').forEach(button => { - button.dispatchEvent(new Event('pointerover', { bubbles: true })); - }); - }); - return html` - - Button - This is the tooltip message - `; - } -}; - -export const Dialog: Story = { - render: () => { - const onClick = async (event: Event & { target: HTMLElement }) => { - const dialog = document.createElement('sl-dialog'); - dialog.innerHTML = ` -

Tooltip behavior

-

Opening this dialog hides the tooltip.

-

- If you opened it with keyboard focus on the trigger, the tooltip can reappear after closing (focus returns to - the trigger). -

-

- If you opened it with mouse click, the tooltip stays closed after closing until the trigger is hovered/focused - again. -

- Close - `; - dialog.addEventListener('sl-close', () => dialog.remove()); - event.target.insertAdjacentElement('afterend', dialog); - await dialog.updateComplete; - dialog.showModal(); - }; - - return html` - - Button - This is the tooltip message - `; - } -}; diff --git a/packages/components/tooltip/src/tooltip.ts b/packages/components/tooltip/src/tooltip.ts index 7e093cda5f..27db72607b 100644 --- a/packages/components/tooltip/src/tooltip.ts +++ b/packages/components/tooltip/src/tooltip.ts @@ -1,1387 +1,363 @@ -import { - AnchorController, - EventsController, - type PopoverPosition, - isPopoverOpen -} from '@sl-design-system/shared'; -import { - type CSSResultGroup, - LitElement, - type PropertyValues, - type TemplateResult, - html -} from 'lit'; -import { property } from 'lit/decorators.js'; +import { CSSResultGroup, LitElement, PropertyValues, TemplateResult, html } from 'lit'; +import { property, state } from 'lit/decorators.js'; import styles from './tooltip.scss.js'; -declare global { - interface GlobalEventHandlersEventMap { - 'sl-close': CustomEvent; - } - - interface HTMLElementTagNameMap { - 'sl-tooltip': Tooltip; - } - - interface ShadowRoot { - // Workaround for missing type in @open-wc/scoped-elements - createElement( - tagName: K, - options?: ElementCreationOptions - ): HTMLElementTagNameMap[K]; - } -} - -export interface TooltipOptions { - /** - * This determines the context that is used to create the `` element. If not provided, - * the tooltip will be created on the target element if it has a `shadowRoot`, or the root node of - * the target element. - */ - context?: Document | ShadowRoot; - - /** - * This is the node where the tooltip will be added to. This can be useful when you don't want the - * tooltip to be added next to the anchor element. If not provided, it will be added next to the - * anchor element. - */ - parentNode?: Node; - - /** - * Which ARIA relationship attribute to add to the anchor (`aria-describedby` or - * `aria-labelledby`). Defaults to 'description' ('aria-describedby'). - * - * A good example of when to use `aria-labelledby` is when the tooltip provides a label or title - * for the anchor element, such as an icon only button (so button with only an icon) and no - * visible text. - */ - ariaRelation?: 'description' | 'label'; -} - let nextUniqueId = 0; +const SHOW_DELAY = 150, + HIDE_DELAY = 0; + /** - * Tooltip component. + * A tooltip component that can be used to display additional information about an element when the + * user hovers over it, focuses it, or clicks it. The tooltip is positioned relative to an anchor + * element, which can be specified using the `for` attribute. + * + * The tooltip will automatically determine the appropriate ARIA relation to use based on the `type` + * property. By default, it will use `ariaLabelledByElements`, but if `type` is set to + * `description`, it will use `ariaDescribedByElements` instead. + * + * @element sl-tooltip * - * @slot default - The slot for the tooltip content. + * @slot - The content of the tooltip. + * + * @csspart arrow - The arrow element that points to the anchor. + * @csspart safe-triangle - An invisible element used to extend the hover area of the tooltip. */ export class Tooltip extends LitElement { - /** @internal The default padding of the arrow. */ - static arrowPadding = 16; - - /** @internal The fixed delay before hover-triggered tooltips open. */ - static readonly hoverShowDelay = 500; - - /** @internal The fixed delay before hover-triggered tooltips close. */ - static readonly hoverHideDelay = 200; - - /** @internal The default offset of the tooltip to its anchor. */ - static offset = 12; - /** @internal */ static override styles: CSSResultGroup = styles; - /** @internal The default margin between the tooltip and the viewport. */ - static viewportMargin = 8; - - /** To attach the `sl-tooltip` to the DOM tree and anchor element */ - static lazy( - target: Element, - callback: (target: Tooltip) => void, - options: TooltipOptions = {} - ): () => void { - let created = false; - - const getLazyTargets = (): Array => { - const targets: Array = [target], - shadowRoot = (target as HTMLElement).shadowRoot, - proxyTarget = ( - target as Element & { getProxyTarget?(): Element | null } - ).getProxyTarget?.(); - - if (shadowRoot) { - targets.push(shadowRoot); - } - - if (proxyTarget instanceof Element && proxyTarget !== target) { - targets.push(proxyTarget); - } - - return Array.from(new Set(targets)); - }; - - const removeListeners = () => { - for (const eventTarget of getLazyTargets()) { - ['focusin', 'pointerover'].forEach(eventName => - eventTarget.removeEventListener(eventName, createTooltip) - ); - } - }; - - const createTooltip = (): void => { - if (created) { - return; - } - - created = true; - - let context = options.context; - if (!context && target.shadowRoot?.registry?.get('sl-tooltip')) { - context = target.shadowRoot; - } else if (!context) { - context = target.getRootNode() as Document; - } - - const tooltip = context.createElement('sl-tooltip'); - - if (options.parentNode) { - options.parentNode.appendChild(tooltip); - } else { - target.parentNode!.insertBefore(tooltip, target.nextSibling); - } - - // If the tooltip has no popover property, then the sl-tooltip custom element - // is not defined in either the `options.context` or the document. - if (tooltip.popover === null) { - console.warn( - `The sl-tooltip custom element is not defined in the ${context !== document ? `${(context as ShadowRoot).host.tagName} element` : 'document'}. Please make sure to register the sl-tooltip custom element in your application.` - ); - - tooltip.remove(); - removeListeners(); - - return; - } - - tooltip.id = `sl-tooltip-${nextUniqueId++}`; - - const ariaRelation = options.ariaRelation ?? 'description', - ariaAttribute = ariaRelation === 'label' ? 'labelledby' : 'describedby'; - - target.setAttribute(`aria-${ariaAttribute}`, tooltip.id); - - callback(tooltip); - - tooltip.anchorElement = target as HTMLElement; - - // We only need to create the tooltip once, so ignore all future events. - removeListeners(); - }; - - const cleanup = () => { - removeListeners(); - }; - - for (const eventTarget of getLazyTargets()) { - ['focusin', 'pointerover'].forEach(eventName => - eventTarget.addEventListener(eventName, createTooltip) - ); - } - - return cleanup; - } - - /** Controller for managing anchoring. */ - #anchor = new AnchorController(this, { - arrowElement: '.arrow', - arrowPadding: Tooltip.arrowPadding, - offset: Tooltip.offset, - viewportMargin: Tooltip.viewportMargin - }); - - /** Events controller. */ - #events = new EventsController(this); - - /** Anchors observed for this tooltip, used to avoid full DOM scans on hide. */ - #knownAnchors = new Set(); - - /** Anchors that stay valid even if browser ARIA reflection drops after reparenting. */ - #stableAnchors = new Set(); - - /** Anchors that originally depended on explicit ARIA idrefs. */ - #explicitRelationAnchors = new Set(); + /** Controller for managing event listeners. */ + #eventController = new AbortController(); - /** Whether the current open state was triggered by focus-based interaction. */ - #openedByFocus = false; + /** Timeout ID for the hover delay. */ + #hoverTimeout?: ReturnType; - /** The root where the tooltip was originally connected before any runtime reparenting. */ - #originalRoot?: Node; + /** @internal The element this tooltip is anchored to. */ + @state() anchor?: HTMLElement | null; - /** Roots where reflected-ARIA anchor discovery already performed a full-root scan. */ - #preparedKeyboardAnchorRoots = new WeakSet(); + /** + * Stops the tooltip from being displayed. + * + * @default false + */ + @property({ type: Boolean }) disabled?: boolean; - /** Timer for showing/hiding the tooltip. */ - #timer?: ReturnType; + /** The ID of the element this tooltip is for. */ + @property() for?: string; - /** The maximum width of the tooltip. */ - @property({ type: Number, attribute: 'max-width' }) maxWidth?: number; + /** + * Setting this will cause the tooltip to show/hide, regardless of trigger. Do not use this + * property to check if the tooltip is showing, use `matches(':popover-open')` instead. + * + * @default false + */ + @property({ type: Boolean }) open?: boolean; /** - * The offset distance of the tooltip from its anchor. + * Controls how the tooltip is activated. Possible options include `click`, `hover`, `focus`, and + * `manual`. Multiple options can be passed by separating them with a space. When manual is used, + * the tooltip must be activated programmatically. * - * @default Tooltip.offset (12px) + * @default 'focus hover' */ - @property({ type: Number }) offset?: number; + @property() trigger = 'focus hover'; /** - * Position of the tooltip relative to its anchor. + * The type of tooltip. Used to determine the ARIA relation that should be used. * - * @type {'top' - * | 'right' - * | 'bottom' - * | 'left' - * | 'top-start' - * | 'top-end' - * | 'right-start' - * | 'right-end' - * | 'bottom-start' - * | 'bottom-end' - * | 'left-start' - * | 'left-end'} + * @default 'label' */ - @property() position: PopoverPosition = 'top'; + @property() type?: 'description' | 'label'; - override connectedCallback(): void { + override connectedCallback() { super.connectedCallback(); - this.#originalRoot ??= this.getRootNode(); - + this.setAttribute('aria-hidden', 'true'); this.setAttribute('popover', 'manual'); this.setAttribute('role', 'tooltip'); - this.setAttribute('aria-hidden', 'true'); // Prevent the tooltip from being read by screen readers multiple times - - const root = this.getRootNode(), - documentRoot = this.ownerDocument ?? document; - - if (root instanceof ShadowRoot) { - // lib.dom does not expose these delegated event names through ShadowRootEventMap, - // so EventsController falls through to its Element overload even though a ShadowRoot - // is the correct runtime event target here. - const shadowEventRoot = root as unknown as HTMLElement; - this.#events.listen(shadowEventRoot, 'focusin', this.#onShow); - this.#events.listen(shadowEventRoot, 'focusout', this.#onHide); - this.#events.listen(shadowEventRoot, 'keydown', this.#onKeydown); - this.#events.listen(shadowEventRoot, 'pointerover', this.#onShow); - this.#events.listen(shadowEventRoot, 'pointerout', this.#onHide); - } else { - this.#events.listen(documentRoot, 'focusin', this.#onShow); - this.#events.listen(documentRoot, 'focusout', this.#onHide); - this.#events.listen(documentRoot, 'keydown', this.#onKeydown); - this.#events.listen(documentRoot, 'pointerover', this.#onShow); - this.#events.listen(documentRoot, 'pointerout', this.#onHide); + if (!this.id) { + this.id = `sl-tooltip-${nextUniqueId++}`; } - this.#events.listen(documentRoot, 'click', this.#onHide, { capture: true }); - - if (root instanceof ShadowRoot && this.#originalRoot && root !== this.#originalRoot) { - const isFromCurrentShadowRoot = (event: Event): boolean => { - const origin = event.composedPath()[0]; - - return origin instanceof Node && origin.getRootNode() === root; - }, - forwardDocumentEvent = - (handler: (event: T) => void) => - (event: T): void => { - if (!isFromCurrentShadowRoot(event)) { - handler(event); - } - }; - - this.#events.listen(documentRoot, 'focusin', forwardDocumentEvent(this.#onShow)); - this.#events.listen(documentRoot, 'focusout', forwardDocumentEvent(this.#onHide)); - this.#events.listen(documentRoot, 'keydown', forwardDocumentEvent(this.#onKeydown)); - this.#events.listen(documentRoot, 'pointerover', forwardDocumentEvent(this.#onShow)); - this.#events.listen(documentRoot, 'pointerout', forwardDocumentEvent(this.#onHide)); + if (this.#eventController.signal.aborted) { + this.#eventController = new AbortController(); } - this.#events.listen(documentRoot, 'sl-close', this.#onShow); - } - - override disconnectedCallback(): void { - clearTimeout(this.#timer); - this.#timer = undefined; - - super.disconnectedCallback(); - } - - override render(): TemplateResult { - return html` - -
-
- `; - } - - override willUpdate(changes: PropertyValues): void { - super.willUpdate(changes); - - if (changes.has('maxWidth')) { - this.#anchor.maxWidth = this.maxWidth; - } + const { signal } = this.#eventController; - if (changes.has('offset')) { - this.#anchor.offset = this.offset ?? Tooltip.offset; - } + this.addEventListener('beforetoggle', this.#onBeforeToggle, { signal }); + this.addEventListener('mouseout', this.#onMouseOut, { signal }); + this.addEventListener('toggle', this.#onToggle, { signal }); - if (changes.has('position')) { - this.#anchor.position = this.position; + // Re-establish the anchor relationship if the tooltip is moved to a different root + if (this.anchor && this.for) { + this.anchor = undefined; // triggers #updateAnchor() + } else if (this.for) { + this.#updateAnchor(); } } - #onHide = (event: Event): void => { - // Only clear the timer for focusout when the tooltip was opened by focus; otherwise, - // an unrelated focusout could cancel a pending hover show timer. - if (event.type !== 'focusout' || this.#openedByFocus) { - clearTimeout(this.#timer); - this.#timer = undefined; - } - - if (event.type === 'click') { - this.#hideTooltip(); - return; - } - - if (event.type === 'pointerout' && !isPopoverOpen(this)) { - return; - } - - // Ignore unrelated focusout events when the tooltip was not opened by focus. - // This avoids overriding a pending hover show timer with a no-op timeout. - if (event.type === 'focusout' && !this.#openedByFocus) { - return; - } - - this.#timer = setTimeout( - () => { - const anchorHovered = !!this.anchorElement?.matches(':hover'); - const tooltipHovered = this.matches(':hover'); - const safeTriangleHovered = !!this.renderRoot.querySelector('.safe-triangle:hover'); - - if (event.type === 'focusout') { - if (!this.#openedByFocus) { - return; - } + override disconnectedCallback() { + clearTimeout(this.#hoverTimeout); - // Keep the tooltip visible when pointer hover is still active. - // Without this guard, a focusout can close the tooltip even though the anchor - // remains hovered, and no new pointerover will fire to reopen it. - if (anchorHovered || tooltipHovered || safeTriangleHovered) { - return; - } + this.#eventController.abort(); - const anchorForEvent = this.#findAnchorInEvent(event), - relatedTargetAnchor = - event instanceof FocusEvent - ? this.#findAnchorFromElement(event.relatedTarget as Element | null) - : undefined, - focusedAnchor = relatedTargetAnchor ?? this.#findFocusedAnchor(); - - const movedToAnotherSharedAnchor = - !!focusedAnchor && - focusedAnchor !== this.anchorElement && - (this.#matchesAnchor(focusedAnchor) || this.#stableAnchors.has(focusedAnchor)); - if (movedToAnotherSharedAnchor) { - this.#showTooltip(focusedAnchor, true); - return; - } - - const hasFocusWithinCurrentAnchor = !!this.anchorElement?.matches(':focus-within'); - - // If focus is still logically within this anchor (including descendants), keep the tooltip open. - if (hasFocusWithinCurrentAnchor) { - return; - } - - // Ignore unrelated focusouts. Hide only when the current anchor actually lost focus. - const currentAnchorLostFocus = !!this.anchorElement && !hasFocusWithinCurrentAnchor; - if ( - currentAnchorLostFocus && - (!anchorForEvent || anchorForEvent === this.anchorElement) - ) { - this.#hideTooltip(); - } - return; - } - - const isFocusVisible = !!this.anchorElement?.matches(':focus-visible'); - - // Keep the tooltip open while the current anchor still has keyboard-visible focus. - // This prevents hover-out from hiding a focus-triggered tooltip. - if (anchorHovered || tooltipHovered || safeTriangleHovered || isFocusVisible) { - return; - } - - // When hover leaves a shared anchor, restore the tooltip to the focused anchor - // if the tooltip was originally opened through keyboard focus. - const focusedAnchor = this.#findFocusedAnchor(); - if (this.#openedByFocus && focusedAnchor && this.#matchesAnchor(focusedAnchor)) { - this.#showTooltip(focusedAnchor, true); - return; - } - - // First check known anchors to avoid scanning the whole root on every hide attempt. - const knownAnchors = this.#getKnownAnchors(); - const anyKnownAnchorHovered = knownAnchors.some( - el => el.matches(':hover') || el.matches(':focus-visible') - ); - - if (anyKnownAnchorHovered) { - return; - } - - // Fallback for anchors not yet tracked in #knownAnchors. - const potentialAnchors = Array.from(new Set([...knownAnchors, ...this.#getAriaAnchors()])); - const anyAnchorHovered = potentialAnchors.some( - el => el.matches(':hover') || el.matches(':focus-visible') - ); - - if (!anyAnchorHovered) { - this.#hideTooltip(); - } - }, - event.type === 'focusout' ? 0 : Tooltip.hoverHideDelay - ); - }; + // Remove the event handler in case the tooltip is still open when disconnected + document.removeEventListener('keydown', this.#onKeydown); - #onKeydown(event: KeyboardEvent): void { - if (isPopoverOpen(this) && event.key === 'Escape') { - this.#hideTooltip(); - return; + if (this.anchor) { + this.#removeAriaRelation(this.anchor); } - // `focusin` from delegated focus inside shadow DOM is not always observed on the - // same root as the tooltip. When keyboard focus moves with Tab between shared - // anchors (for example sl-button elements inside sl-button-bar), re-read the - // focused anchor on the next frame and sync the tooltip to that element. - if (event.key === 'Tab' && isPopoverOpen(this) && this.#openedByFocus) { - requestAnimationFrame(() => { - let focusedAnchor = this.#findFocusedAnchor() ?? this.#findKnownFocusedAnchor(); - const currentAnchor = this.anchorElement; - - if (!focusedAnchor && currentAnchor instanceof HTMLElement) { - this.#prepareKeyboardAnchors(currentAnchor); - focusedAnchor = this.#findFocusedAnchor() ?? this.#findKnownFocusedAnchor(); - } - - if ( - focusedAnchor && - focusedAnchor !== this.anchorElement && - this.#matchesAnchor(focusedAnchor) - ) { - this.#showTooltip(focusedAnchor, true); - return; - } - - if ( - focusedAnchor && - focusedAnchor !== this.anchorElement && - this.#knownAnchors.has(focusedAnchor) && - focusedAnchor.matches(':focus-within') - ) { - this.#showTooltip(focusedAnchor, true); - return; - } - - if (!focusedAnchor && !this.anchorElement?.matches(':focus-within')) { - this.#hideTooltip(); - } - }); - } + super.disconnectedCallback(); } - #onShow = (event: Event): void => { - // If the event is sl-close, the event path might not contain the anchor (as it comes from the dialog) - // So we use the activeElement (or shadowRoot.activeElement) as a candidate anchor. - const anchorInEvent = this.#findAnchorInEvent(event), - candidateAnchor = - event.type === 'focusin' || event.type === 'sl-close' ? this.#findFocusedAnchor() : null; - - const anchorElement = anchorInEvent || candidateAnchor; - const anchorRoot = anchorElement - ? this.#findAssignedSlotRoot(anchorElement, event.composedPath()) - : undefined; + override willUpdate(changes: PropertyValues): void { + super.willUpdate(changes); - if (!anchorElement) { - return; + if (changes.has('anchor') || changes.has('for')) { + this.#updateAnchor(); } - const normalizedAnchorElement = this.#normalizeAnchorElement(anchorElement); - - // Ignore events from open popovers that are nested *inside* the anchor (for example a menu item inside - // an open menu-button). Still allow anchors that themselves live inside an open popover, such as a grid - // bulk action button inside a floating action bar. - if (this.#isInsideNestedOpenPopover(event, normalizedAnchorElement)) { - return; + if (changes.has('disabled') && this.disabled) { + this.hidePopover(); } - // Track anchors as soon as they are detected, even when showing is delayed. - this.#knownAnchors.add(normalizedAnchorElement); - - // For hover events - if (event.type === 'pointerover') { - clearTimeout(this.#timer); - this.#timer = undefined; - - // If already open, update anchor immediately to avoid "stickiness" - if (isPopoverOpen(this)) { - this.#showTooltip(normalizedAnchorElement, this.#openedByFocus, anchorRoot); + if (changes.has('open')) { + if (this.open) { + this.showPopover(); } else { - this.#timer = setTimeout( - () => this.#showTooltip(normalizedAnchorElement, false, anchorRoot), - Tooltip.hoverShowDelay - ); + this.hidePopover(); } - return; } + } - // For keyboard navigation (focus events or dialog/popover closing) - if (event.type === 'focusin' || event.type === 'sl-close') { - clearTimeout(this.#timer); - this.#timer = undefined; - - if (!(anchorElement instanceof HTMLElement)) { - return; - } + override render(): TemplateResult { + return html` + +
+
+ `; + } - if ( - !this.#matchesAnchor(normalizedAnchorElement) && - !this.#stableAnchors.has(normalizedAnchorElement) - ) { + #onBeforeToggle = (event: ToggleEvent): void => { + if (event.newState === 'open') { + if (this.disabled) { + event.preventDefault(); return; } - const path = event.composedPath(); - const getHasFocusVisible = (): boolean => - anchorElement.matches(':focus-visible') || - path.some(el => el instanceof Element && el.matches(':focus-visible')); - - // If already open (e.g. tabbing between shared buttons), update anchor immediately - if (isPopoverOpen(this)) { - this.#prepareKeyboardAnchors(normalizedAnchorElement); - this.#showTooltip(normalizedAnchorElement, getHasFocusVisible(), anchorRoot); - } else { - requestAnimationFrame(() => { - const hasFocusVisible = getHasFocusVisible(); - - if (hasFocusVisible) { - this.#prepareKeyboardAnchors(normalizedAnchorElement); - this.#showTooltip(normalizedAnchorElement, true, anchorRoot); - } - }); - } + document.addEventListener('keydown', this.#onKeydown); + } else { + document.removeEventListener('keydown', this.#onKeydown); } }; - /** - * Calculate a "safe triangle" for the submenu to a user can safely move his cursor from the - * trigger to the submenu without the submenu closing. See - * https://www.smashingmagazine.com/2023/08/better-context-menus-safe-triangles - */ - #calculateSafeTriangle(): void { - const actualPlacement = this.getAttribute('actual-placement'); - - if (!actualPlacement || !this.anchorElement) { - return; - } - - const tooltipRect = this.getBoundingClientRect(), - anchorRect = this.anchorElement.getBoundingClientRect(); - let insetBlockStart, - blockSize, - inlineSize, - polygon, - anchorInsetBlockStart = 0, - insetInlineStart, - anchorSideBlockStart = 0, - tootltipSideBlockStart = 0; - - if (actualPlacement.startsWith('top') || actualPlacement.startsWith('bottom')) { - anchorInsetBlockStart = Math.floor(anchorRect.left - tooltipRect.left); - inlineSize = Math.ceil(Math.max(tooltipRect.width, anchorRect.width)); - insetInlineStart = Math.ceil(Math.min(tooltipRect.left, anchorRect.left)); - } - - if (actualPlacement.startsWith('top')) { - blockSize = Math.ceil(anchorRect.top - tooltipRect.bottom) + 2; - insetBlockStart = tooltipRect.bottom - 1; - polygon = `0% 0%, 100% 0, ${anchorInsetBlockStart + anchorRect.width}px 100%, ${anchorInsetBlockStart}px 100%`; - } - - if (actualPlacement.startsWith('bottom')) { - blockSize = Math.ceil(tooltipRect.top - anchorRect.bottom) + 2; - insetBlockStart = anchorRect.bottom - 1; - polygon = `${anchorInsetBlockStart}px 0, ${anchorInsetBlockStart + anchorRect.width}px 0, 100% 100%, 0 100%`; - } - - if (actualPlacement.startsWith('left') || actualPlacement.startsWith('right')) { - blockSize = Math.ceil(Math.max(tooltipRect.height, anchorRect.height)) + 2; - insetBlockStart = Math.min(anchorRect.top, tooltipRect.top) - 1; - anchorInsetBlockStart = anchorRect.top; - anchorSideBlockStart = Math.max(anchorRect.top - tooltipRect.top, 0); - tootltipSideBlockStart = Math.max(tooltipRect.top - anchorRect.top, 0); - } - - if (actualPlacement.startsWith('right')) { - insetInlineStart = Math.ceil(Math.min(tooltipRect.left, anchorRect.right)) - 1; - inlineSize = Math.ceil(tooltipRect.left - anchorRect.right) + 2; - polygon = `0 ${anchorSideBlockStart}px , 100% ${tootltipSideBlockStart}px, - 100% ${tootltipSideBlockStart + tooltipRect.height + 2}px, 0 ${anchorSideBlockStart + anchorRect.height + 2}px`; + #onBlur = (): void => { + if (this.#hasTrigger('focus')) { + this.hidePopover(); } - - if (actualPlacement.startsWith('left')) { - insetInlineStart = Math.ceil(Math.min(tooltipRect.right, anchorRect.left)) - 1; - inlineSize = Math.ceil(anchorRect.left - tooltipRect.right) + 2; - polygon = `0 ${tootltipSideBlockStart}px , 100% ${anchorSideBlockStart}px, - 100% ${anchorSideBlockStart + anchorRect.height + 2}px, 0 ${tootltipSideBlockStart + tooltipRect.height + 2}px`; - } - - const inset = `${insetBlockStart}px auto auto ${insetInlineStart}px`; - const safeTriangle = this.renderRoot.querySelector('.safe-triangle')!; - safeTriangle.style.blockSize = `${blockSize}px`; - safeTriangle.style.clipPath = `polygon(${polygon})`; - safeTriangle.style.inlineSize = `${inlineSize}px`; - safeTriangle.style.inset = inset; - } - - #findAnchorFromElement = (element: Element | null): HTMLElement | undefined => { - if (!element) { - return undefined; - } - - let current: Element | null = element; - - while (current) { - if (current instanceof HTMLElement && this.#stableAnchors.has(current)) { - return current; - } - - if (current instanceof HTMLElement && this.#matchesAnchor(current)) { - return current; - } - - if (current.parentElement) { - current = current.parentElement; - continue; - } - - const shadowRoot = this.#getShadowRoot(current.getRootNode()); - current = shadowRoot?.host ?? null; - } - - return undefined; }; - #findStableAnchorFromElement = (element: Element | null): HTMLElement | undefined => { - if (!element) { - return undefined; - } - - let current: Element | null = element; - - while (current) { - if (current instanceof HTMLElement && this.#stableAnchors.has(current)) { - return current; - } - - if (current.parentElement) { - current = current.parentElement; - continue; + #onClick = (): void => { + if (this.#hasTrigger('click')) { + if (this.matches(':popover-open')) { + this.hidePopover(); + } else { + this.showPopover(); } - - const shadowRoot = this.#getShadowRoot(current.getRootNode()); - current = shadowRoot?.host ?? null; - } - - return undefined; - }; - - #isEventActiveAnchor = ( - element: HTMLElement, - event: Event, - path: EventTarget[], - host: Element - ): boolean => { - if (path.includes(element)) { - return true; - } - - if (host !== event.target) { - return false; - } - - if (event.type === 'pointerover') { - return element.matches(':hover'); - } - - if (event.type === 'focusin' || event.type === 'sl-close') { - return element.matches(':focus-within'); + } else { + this.hidePopover(); } - - return false; }; - /** - * Find the anchor element for a given event. First checks the composed path directly, then - * searches inside shadow roots of elements in the path. This handles cases where the pointer is - * over a host element (e.g. `sl-menu-button`) but the actual anchor (e.g. `sl-button` with - * `ariaDescribedByElements`) is inside its shadow DOM. - */ - #findAnchorInEvent = (event: Event): HTMLElement | undefined => { - const path = event.composedPath(), - escapedId = this.id ? CSS.escape(this.id) : undefined; - - // First check elements directly in the composed path - const anchor = path.find( - (el): el is HTMLElement => el instanceof HTMLElement && this.#matchesAnchor(el) - ); - - if (anchor) { - return anchor; - } - - for (const el of path) { - if (!(el instanceof Element)) { - continue; - } - - const stableAnchor = this.#findStableAnchorFromElement(el); - - if (stableAnchor) { - return stableAnchor; - } - } - - for (const el of path) { - if (el instanceof Element && el.shadowRoot) { - const ariaMatch = escapedId - ? el.shadowRoot.querySelector( - `[aria-describedby~="${escapedId}"], [aria-labelledby~="${escapedId}"]` - ) - : null; - - if ( - ariaMatch instanceof HTMLElement && - this.#isEventActiveAnchor(ariaMatch, event, path, el) && - this.#matchesAnchor(ariaMatch) - ) { - return ariaMatch; - } - - for (const child of Array.from(el.shadowRoot.children)) { - if ( - child instanceof HTMLElement && - this.#isEventActiveAnchor(child, event, path, el) && - this.#matchesAnchor(child) - ) { - return child; - } - } - } + #onFocus = (): void => { + if (this.#hasTrigger('focus')) { + this.showPopover(); } - - return undefined; }; - #findFocusedAnchor = (): HTMLElement | undefined => { - const root = this.getRootNode() as Document | ShadowRoot; - let activeElement: Element | null = root.activeElement || document.activeElement; - - while (activeElement instanceof HTMLElement && activeElement.shadowRoot?.activeElement) { - activeElement = activeElement.shadowRoot.activeElement; + #onKeydown = (event: KeyboardEvent): void => { + if (event.key === 'Escape') { + this.hidePopover(); } - - return this.#findAnchorFromElement(activeElement); }; - #findKnownFocusedAnchor = (): HTMLElement | undefined => - Array.from(this.#knownAnchors).find( - anchor => anchor.isConnected && anchor.matches(':focus-within') - ); - - /** - * Start with cheap lookups: explicit ARIA attributes in this root and anchors we already observed - * before. This covers the common cases without scanning the full DOM. - */ - #seedKnownAnchors = (): void => { - for (const anchor of this.#getAriaAnchors()) { - this.#knownAnchors.add(this.#normalizeAnchorElement(anchor)); - } - - this.#getKnownAnchors(); - }; + #onMouseOver = (): void => { + if (this.#hasTrigger('hover')) { + clearTimeout(this.#hoverTimeout); - /** - * As a last resort for keyboard navigation, scan the root and cache anchors that only expose the - * tooltip relation through reflected/forwarded ARIA. - */ - #discoverAnchorsByScan = (roots: ParentNode[] = this.#getAnchorSearchRoots()): void => { - for (const root of roots) { - for (const element of Array.from(root.querySelectorAll('*'))) { - if (element instanceof HTMLElement && this.#matchesAnchor(element)) { - this.#knownAnchors.add(this.#normalizeAnchorElement(element)); - } - } + this.#hoverTimeout = setTimeout(() => { + this.showPopover(); + }, SHOW_DELAY); } }; - /** - * Shared keyboard flows need a stable cache because browsers can clear reflected ARIA relations - * on proxy targets while focus is moving between anchors. - */ - #prepareKeyboardAnchors = (anchorElement: HTMLElement): void => { - const root = this.getRootNode() as ParentNode, - searchRoots = this.#getAnchorSearchRoots(); - - this.#seedKnownAnchors(); - - if (this.#preparedKeyboardAnchorRoots.has(root)) { - return; - } - - const proxyTarget = ( - anchorElement as Element & { getProxyTarget?(): Element | null } - ).getProxyTarget?.(), - internals = (anchorElement as HTMLElement & { internals?: ElementInternals }).internals, - reliesOnReflectedRelation = - this.#hasAnyReflectedRelation(anchorElement) || - this.#hasAnyReflectedRelation(proxyTarget) || - this.#hasAnyReflectedRelation(internals); - - if (reliesOnReflectedRelation) { - this.#discoverAnchorsByScan(searchRoots); - - for (const searchRoot of searchRoots) { - this.#preparedKeyboardAnchorRoots.add(searchRoot); + #onMouseOut = (): void => { + if (this.#hasTrigger('hover')) { + // Don't hide the popover if either the anchor or the popover itself is still hovered + const anchorHovered = Boolean(this.anchor?.matches(':hover')), + tooltipHovered = this.matches(':hover'); + if (anchorHovered || tooltipHovered) { + return; } - } - }; - - #getAnchorSearchRoots = (): ParentNode[] => { - const roots: ParentNode[] = []; - const seen = new Set(); - let root: Node | null = this.getRootNode(); - - while (root && !seen.has(root)) { - seen.add(root); - if (root instanceof Document) { - roots.push(root); - break; - } + clearTimeout(this.#hoverTimeout); - const shadowRoot = this.#getShadowRoot(root); - if (!shadowRoot) { - break; + if (!(anchorHovered || tooltipHovered)) { + this.#hoverTimeout = setTimeout(() => { + this.hidePopover(); + }, HIDE_DELAY); } - - roots.push(shadowRoot); - root = shadowRoot.host.getRootNode(); - } - - return roots; - }; - - #findAssignedSlotRoot = ( - anchorElement: HTMLElement, - path: EventTarget[] - ): ShadowRoot | undefined => { - const slotInPath = path.find((el): el is HTMLSlotElement => { - const slot = - el instanceof HTMLSlotElement - ? el - : el instanceof Element && el.tagName === 'SLOT' - ? (el as HTMLSlotElement) - : null; - - if (!slot) { - return false; - } - - return slot.assignedNodes({ flatten: true }).includes(anchorElement); - }), - assignedSlot = anchorElement.assignedSlot || slotInPath, - assignedSlotRoot = this.#getShadowRoot(assignedSlot?.getRootNode()); - - return assignedSlotRoot; - }; - - /** - * Event-time slot information can become stale before a delayed show fires. Prefer the anchor's - * current assigned slot root when it exists, and only fall back to the previously captured root - * for cases that are only discoverable from the original event path. - */ - #resolveAnchorRoot = ( - anchorElement: HTMLElement, - anchorRootHint?: ShadowRoot - ): ShadowRoot | undefined => { - const currentAssignedSlotRoot = this.#getShadowRoot(anchorElement.assignedSlot?.getRootNode()); - - return currentAssignedSlotRoot ?? anchorRootHint; - }; - - #getAriaAnchorSelector = (): string | undefined => { - const escapedId = this.id ? CSS.escape(this.id) : undefined; - if (!escapedId) { - return undefined; - } - - return `[aria-describedby~="${escapedId}"], [aria-labelledby~="${escapedId}"]`; - }; - - #getAriaAnchors = (root: ParentNode = this.getRootNode() as ParentNode): HTMLElement[] => { - const selector = this.#getAriaAnchorSelector(); - - if (!selector) { - return []; } - - return Array.from(root.querySelectorAll(selector)); }; - #getKnownAnchors = (): HTMLElement[] => { - const knownAnchors: HTMLElement[] = []; - - for (const anchor of this.#knownAnchors) { - if ( - !anchor.isConnected || - (!this.#stableAnchors.has(anchor) && !this.#matchesAnchor(anchor)) - ) { - this.#knownAnchors.delete(anchor); - this.#stableAnchors.delete(anchor); - this.#explicitRelationAnchors.delete(anchor); - } else { - knownAnchors.push(anchor); - } + #onToggle = (event: ToggleEvent): void => { + if (event.newState === 'open' && this.anchor) { + this.#positionHoverExtender(this.anchor); } - - return knownAnchors; }; - #getShadowRoot = (node: Node | null | undefined): ShadowRoot | undefined => { - if (node instanceof ShadowRoot) { - return node; - } - - // Browser test runners can surface a real ShadowRoot from another realm, - // which makes `instanceof ShadowRoot` return false even though the node - // still exposes `host` and behaves like a shadow root. - if ( - node && - node.nodeType === Node.DOCUMENT_FRAGMENT_NODE && - 'host' in node && - (node.host as Element)?.nodeType === Node.ELEMENT_NODE - ) { - return node as ShadowRoot; - } + #hasTrigger(trigger: string): boolean { + return this.trigger.split(' ').includes(trigger); + } - return undefined; - }; + #getAriaPropertyFromType( + type?: 'description' | 'label' + ): 'ariaDescribedByElements' | 'ariaLabelledByElements' { + return type === 'description' ? 'ariaDescribedByElements' : 'ariaLabelledByElements'; + } - #moveToAnchorRoot = (anchorElement: HTMLElement, anchorRoot?: ShadowRoot): boolean => { - const root = anchorRoot ?? this.#findAssignedSlotRoot(anchorElement, []); + #addAriaRelation(element: Element): void { + const ariaProperty = this.#getAriaPropertyFromType(this.type); - if (root && this.getRootNode() !== root) { - root.append(this); - return true; + const refs = element[ariaProperty] ?? []; + if (!refs.includes(this)) { + element[ariaProperty] = [...refs, this]; } + } - return false; - }; - - #canMoveToAnchorRoot = (anchorElement: HTMLElement): boolean => { - const proxyTarget = ( - anchorElement as Element & { getProxyTarget?(): Element | null } - ).getProxyTarget?.(), - internals = (anchorElement as HTMLElement & { internals?: ElementInternals }).internals, - hasExplicitRelation = - this.#explicitRelationAnchors.has(anchorElement) || - this.#hasAnyExplicitRelation(anchorElement) || - this.#hasAnyExplicitRelation(proxyTarget); - - if (internals) { - return true; - } + #removeAriaRelation(element: Element): void { + const ariaProperty = this.#getAriaPropertyFromType(this.type); - return !hasExplicitRelation; - }; + const refs = element[ariaProperty] ?? []; + element[ariaProperty] = refs.filter((ref: Element) => ref !== this); + } - #syncSlotWithAnchor = (anchorElement: HTMLElement, isInAnchorRoot: boolean): void => { - if (isInAnchorRoot) { - this.removeAttribute('slot'); + #positionHoverExtender(anchor: Element): void { + const extender = this.renderRoot.querySelector('[part="hover-extender"]'); + if (!extender) { return; } - const anchorSlot = anchorElement.getAttribute('slot'); - - if (typeof anchorSlot === 'string' && anchorSlot.length > 0) { - this.setAttribute('slot', anchorSlot); + const a = anchor.getBoundingClientRect(), + t = this.getBoundingClientRect(); + + // Determine on which side of the anchor the tooltip ended up (after CSS anchor positioning + // and any position-try fallbacks). We then build a trapezoid whose parallel edges align with + // the touching edges of the anchor and the tooltip, so the user can move the pointer between + // the two without crossing an unhovered area. + let left: number, top: number, width: number, height: number, polygon: string; + + if (t.bottom <= a.top) { + // Tooltip above anchor + left = Math.min(a.left, t.left); + top = t.bottom; + width = Math.max(a.right, t.right) - left; + height = Math.max(0, a.top - t.bottom); + polygon = + `polygon(${t.left - left}px 0, ${t.right - left}px 0, ` + + `${a.right - left}px ${height}px, ${a.left - left}px ${height}px)`; + } else if (t.top >= a.bottom) { + // Tooltip below anchor + left = Math.min(a.left, t.left); + top = a.bottom; + width = Math.max(a.right, t.right) - left; + height = Math.max(0, t.top - a.bottom); + polygon = + `polygon(${a.left - left}px 0, ${a.right - left}px 0, ` + + `${t.right - left}px ${height}px, ${t.left - left}px ${height}px)`; + } else if (t.right <= a.left) { + // Tooltip left of anchor + left = t.right; + top = Math.min(a.top, t.top); + width = Math.max(0, a.left - t.right); + height = Math.max(a.bottom, t.bottom) - top; + polygon = + `polygon(0 ${t.top - top}px, 0 ${t.bottom - top}px, ` + + `${width}px ${a.bottom - top}px, ${width}px ${a.top - top}px)`; + } else if (t.left >= a.right) { + // Tooltip right of anchor + left = a.right; + top = Math.min(a.top, t.top); + width = Math.max(0, t.left - a.right); + height = Math.max(a.bottom, t.bottom) - top; + polygon = + `polygon(0 ${a.top - top}px, 0 ${a.bottom - top}px, ` + + `${width}px ${t.bottom - top}px, ${width}px ${t.top - top}px)`; } else { - this.removeAttribute('slot'); - } - }; - - #ensureTooltipInList = (elements: readonly Element[] | null | undefined): Element[] => { - const list = elements ? Array.from(elements) : []; - - return list.includes(this) ? list : [...list, this]; - }; - - #requiresFullAnchorDiscovery = (anchorElement: HTMLElement): boolean => { - const proxyTarget = ( - anchorElement as Element & { getProxyTarget?(): Element | null } - ).getProxyTarget?.(), - internals = (anchorElement as HTMLElement & { internals?: ElementInternals }).internals; - - if (this.#hasAnyExplicitRelation(anchorElement)) { - return false; - } - - return ( - this.#hasAnyExplicitRelation(proxyTarget) || - this.#hasAnyReflectedRelation(anchorElement) || - this.#hasAnyReflectedRelation(proxyTarget) || - this.#hasAnyReflectedRelation(internals) - ); - }; - - /** - * Prefer already-known anchors and explicit ARIA selectors before falling back to a full scan. - * This keeps repeated hover/focus updates cheap in the common case. - */ - #collectCheapMatchingAnchors = (anchorElement: HTMLElement): Set => { - const anchors = new Set([anchorElement]); - - for (const knownAnchor of this.#getKnownAnchors()) { - if (this.#matchesAnchor(knownAnchor)) { - anchors.add(this.#normalizeAnchorElement(knownAnchor)); - } - } - - for (const root of this.#getAnchorSearchRoots()) { - for (const ariaAnchor of this.#getAriaAnchors(root)) { - if (this.#matchesAnchor(ariaAnchor)) { - anchors.add(this.#normalizeAnchorElement(ariaAnchor)); - } - } - } - - return anchors; - }; - - /** - * Before moving the tooltip into another root, collect every anchor that currently matches it. - * Shared anchors that rely on forwarded/reflected ARIA can lose their ID-based relation once the - * tooltip leaves the original root, so we mirror the relation onto all of them in one pass. - */ - #collectMatchingAnchors = (anchorElement: HTMLElement): HTMLElement[] => { - const anchors = this.#collectCheapMatchingAnchors(anchorElement); - - if (anchors.size === 1 && this.#requiresFullAnchorDiscovery(anchorElement)) { - for (const root of this.#getAnchorSearchRoots()) { - for (const element of Array.from(root.querySelectorAll('*'))) { - if (element instanceof HTMLElement && this.#matchesAnchor(element)) { - anchors.add(this.#normalizeAnchorElement(element)); - } - } - } - } - - return Array.from(anchors); - }; - - #rememberAnchorRelation = (anchorElement: HTMLElement): void => { - const proxyTarget = ( - anchorElement as Element & { getProxyTarget?(): Element | null } - ).getProxyTarget?.(); - - if (this.#hasAnyExplicitRelation(anchorElement) || this.#hasAnyExplicitRelation(proxyTarget)) { - this.#explicitRelationAnchors.add(anchorElement); - } - }; - - #getElementRelationTargets = ( - anchorElement: HTMLElement, - proxyTarget: Element | null | undefined, - relation: 'description' | 'label' - ): Element[] => { - const attribute = relation === 'label' ? 'aria-labelledby' : 'aria-describedby', - targets = new Set(); - - if ( - this.#hasExplicitRelation(anchorElement, attribute) || - this.#hasReflectedRelation(anchorElement, relation) - ) { - targets.add(anchorElement); - } - - if ( - proxyTarget instanceof Element && - proxyTarget !== anchorElement && - (this.#hasExplicitRelation(proxyTarget, attribute) || - this.#hasReflectedRelation(proxyTarget, relation)) - ) { - targets.add(proxyTarget); - } - - if (targets.size > 0) { - return Array.from(targets); + // Tooltip and anchor overlap; no bridge needed. + extender.style.display = 'none'; + return; } - return proxyTarget instanceof Element && proxyTarget !== anchorElement - ? [proxyTarget] - : [anchorElement]; - }; + extender.style.left = `${left}px`; + extender.style.top = `${top}px`; + extender.style.width = `${width}px`; + extender.style.height = `${height}px`; + extender.style.clipPath = polygon; + } - #setReflectedRelation = ( - target: - | { - ariaDescribedByElements?: readonly Element[] | null; - ariaLabelledByElements?: readonly Element[] | null; - } - | null - | undefined, - relation: 'description' | 'label' - ): void => { - if (!target) { + #updateAnchor(): void { + if (!this.for) { + this.anchor = undefined; return; } - if (relation === 'label') { - (target as { ariaLabelledByElements: Element[] | null }).ariaLabelledByElements = - this.#ensureTooltipInList(target.ariaLabelledByElements); + const rootNode = this.getRootNode() as Document | ShadowRoot | null; + if (!rootNode) { + this.anchor = undefined; return; } - (target as { ariaDescribedByElements: Element[] | null }).ariaDescribedByElements = - this.#ensureTooltipInList(target.ariaDescribedByElements); - }; - - /** - * Checks the raw ARIA attributes because they preserve the original author intent: whether the - * tooltip should behave as a label or as a description. - */ - #hasExplicitRelation = ( - element: Element | null | undefined, - attribute: 'aria-describedby' | 'aria-labelledby' - ): boolean => - typeof this.id === 'string' && - typeof element?.getAttribute(attribute) === 'string' && - element.getAttribute(attribute)!.split(/\s+/).includes(this.id); - - /** - * Checks reflected element lists used by native ARIA reflection APIs and ElementInternals. We use - * this after explicit attributes because these lists do not tell us who set them first. - */ - #hasReflectedRelation = ( - target: - | { - ariaDescribedByElements?: readonly Element[] | null; - ariaLabelledByElements?: readonly Element[] | null; - } - | null - | undefined, - relation: 'description' | 'label' - ): boolean => { - const elements = - relation === 'label' ? target?.ariaLabelledByElements : target?.ariaDescribedByElements; - - return !!elements?.includes(this); - }; - - #hasAnyExplicitRelation = (element: Element | null | undefined): boolean => - this.#hasExplicitRelation(element, 'aria-describedby') || - this.#hasExplicitRelation(element, 'aria-labelledby'); - - #hasAnyReflectedRelation = ( - target: - | { - ariaDescribedByElements?: readonly Element[] | null; - ariaLabelledByElements?: readonly Element[] | null; - } - | null - | undefined - ): boolean => - this.#hasReflectedRelation(target, 'description') || - this.#hasReflectedRelation(target, 'label'); - - /** - * When the tooltip moves into the anchor's shadow root, it can no longer rely on the host's - * original ARIA wiring alone. This method mirrors the existing relation onto ElementInternals - * when available, and otherwise falls back to native ARIA reflection on the anchor/proxy element - * without dropping any previously registered labels/descriptions. - */ - #preserveAnchorRelation = (anchorElement: HTMLElement): void => { - const proxyTarget = ( - anchorElement as Element & { getProxyTarget?(): Element | null } - ).getProxyTarget?.(), - internals = (anchorElement as HTMLElement & { internals?: ElementInternals }).internals; - let relation: 'description' | 'label' = 'description'; - - if ( - this.#hasExplicitRelation(anchorElement, 'aria-labelledby') || - this.#hasExplicitRelation(proxyTarget, 'aria-labelledby') - ) { - relation = 'label'; - } else if ( - this.#hasExplicitRelation(anchorElement, 'aria-describedby') || - this.#hasExplicitRelation(proxyTarget, 'aria-describedby') - ) { - relation = 'description'; - } else if ( - anchorElement.ariaLabelledByElements?.includes(this) || - proxyTarget?.ariaLabelledByElements?.includes(this) || - internals?.ariaLabelledByElements?.includes(this) - ) { - relation = 'label'; - } else if ( - anchorElement.ariaDescribedByElements?.includes(this) || - proxyTarget?.ariaDescribedByElements?.includes(this) || - internals?.ariaDescribedByElements?.includes(this) - ) { - relation = 'description'; - } - - if (internals) { - this.#setReflectedRelation(internals, relation); + const newAnchor = this.for ? rootNode.getElementById(this.for) : null, + oldAnchor = this.anchor; + if (newAnchor === oldAnchor) { return; } - for (const target of this.#getElementRelationTargets(anchorElement, proxyTarget, relation)) { - this.#setReflectedRelation(target, relation); - } - }; - - #hideTooltip = (): void => { - this.hidePopover(); - this.#openedByFocus = false; - }; + const { signal } = this.#eventController; - #isInsideNestedOpenPopover = (event: Event, anchorElement: HTMLElement): boolean => { - const path = event.composedPath(), - anchorIndex = path.findIndex(el => el === anchorElement); + if (newAnchor) { + this.#addAriaRelation(newAnchor); - if (anchorIndex === -1) { - return false; - } + newAnchor.addEventListener('blur', this.#onBlur, { capture: true, signal }); + newAnchor.addEventListener('click', this.#onClick, { signal }); + newAnchor.addEventListener('focus', this.#onFocus, { capture: true, signal }); + newAnchor.addEventListener('mouseover', this.#onMouseOver, { signal }); + newAnchor.addEventListener('mouseout', this.#onMouseOut, { signal }); - return path.some( - (el, index) => - index < anchorIndex && el instanceof HTMLElement && el !== this && isPopoverOpen(el) - ); - }; + // Do not overwrite an existing anchor name, as it might be used for something else. + const newAnchorName = newAnchor.style.anchorName || `--${this.id}`; - /** - * Checks whether an element is connected to this tooltip through any supported ARIA wiring - * (attributes, reflected ARIA element lists, forwarded proxy targets, or ElementInternals). This - * is the core anchor-matching predicate used across hover/focus handling. - */ - #matchesAnchor = (element: Element): boolean => { - if (!this.id || !element || element.nodeType !== Node.ELEMENT_NODE) { - return false; + newAnchor.style.anchorName = newAnchorName; + this.style.positionAnchor = newAnchorName; } - if (this.#hasAnyExplicitRelation(element) || this.#hasAnyReflectedRelation(element)) { - return true; - } + if (oldAnchor) { + this.#removeAriaRelation(oldAnchor); - // Support components that forward ARIA to an internal proxy target (e.g. ForwardAriaMixin). - const proxyTarget = ( - element as Element & { getProxyTarget?(): Element | null } - ).getProxyTarget?.(); - if (proxyTarget instanceof Element && proxyTarget !== element) { - if (this.#hasAnyExplicitRelation(proxyTarget) || this.#hasAnyReflectedRelation(proxyTarget)) { - return true; - } + oldAnchor.removeEventListener('blur', this.#onBlur, { capture: true }); + oldAnchor.removeEventListener('click', this.#onClick); + oldAnchor.removeEventListener('focus', this.#onFocus, { capture: true }); + oldAnchor.removeEventListener('mouseover', this.#onMouseOver); + oldAnchor.removeEventListener('mouseout', this.#onMouseOut); + oldAnchor.style.anchorName = ''; + this.style.positionAnchor = ''; } - // Check ElementInternals ariaDescribedByElements and ariaLabelledByElements - // This handles cases where elements use ElementInternals to connect to the tooltip across shadow DOM boundaries - const internals = (element as HTMLElement & { internals?: ElementInternals }).internals; - - return this.#hasAnyReflectedRelation(internals); - }; - - /** - * Normalizes an internal proxy target back to the public host element when both represent the - * same anchor. This keeps `anchorElement` stable for consumers and tests, even when ARIA is - * forwarded to an internal control inside the component's shadow DOM. - */ - #normalizeAnchorElement = (element: HTMLElement): HTMLElement => { - let normalized = element; - - while (true) { - const rootNode = this.#getShadowRoot(normalized.getRootNode()); - if (!rootNode) { - return normalized; - } - - const host = rootNode.host; - const proxyTarget = ( - host as HTMLElement & { getProxyTarget?(): Element | null } - ).getProxyTarget?.(); - - if (host instanceof HTMLElement && proxyTarget === normalized && this.#matchesAnchor(host)) { - normalized = host; - continue; - } - - return normalized; - } - }; - - #showTooltip = (element: HTMLElement, openedByFocus = false, anchorRoot?: ShadowRoot): void => { - const normalizedElement = this.#normalizeAnchorElement(element); - const wasOpen = isPopoverOpen(this), - anchorChanged = this.anchorElement !== normalizedElement, - targetAnchorRoot = this.#resolveAnchorRoot(normalizedElement, anchorRoot), - canMoveToAnchorRoot = !!targetAnchorRoot && this.#canMoveToAnchorRoot(normalizedElement), - anchorsToPreserve = canMoveToAnchorRoot - ? this.#collectMatchingAnchors(normalizedElement) - : [normalizedElement], - currentTooltipRoot = this.getRootNode(), - wasReparentedFromOriginalRoot = - !!this.#originalRoot && currentTooltipRoot !== this.#originalRoot; - - for (const anchor of anchorsToPreserve) { - this.#rememberAnchorRelation(anchor); - } - - if (canMoveToAnchorRoot) { - this.#moveToAnchorRoot(normalizedElement, targetAnchorRoot); - } else if ( - wasReparentedFromOriginalRoot && - currentTooltipRoot !== normalizedElement.getRootNode() && - normalizedElement.parentElement - ) { - normalizedElement.insertAdjacentElement('afterend', this); - } - - const isInAnchorRoot = !!targetAnchorRoot && this.getRootNode() === targetAnchorRoot; - - this.#openedByFocus = openedByFocus; - this.anchorElement = normalizedElement; - this.#knownAnchors.add(normalizedElement); - this.#syncSlotWithAnchor(normalizedElement, isInAnchorRoot); - - if (isInAnchorRoot) { - for (const anchor of anchorsToPreserve) { - this.#knownAnchors.add(anchor); - this.#stableAnchors.add(anchor); - this.#preserveAnchorRelation(anchor); - } - } - - if (!wasOpen || !isPopoverOpen(this)) { - this.showPopover(); - } else if (anchorChanged) { - this.#anchor.updatePosition(); - } else { - return; - } - - requestAnimationFrame(() => { - this.#calculateSafeTriangle(); - }); - }; + this.anchor = newAnchor; + } } diff --git a/packages/components/tooltip/src/tooltip2.scss b/packages/components/tooltip/src/tooltip2.scss deleted file mode 100644 index efeb38d526..0000000000 --- a/packages/components/tooltip/src/tooltip2.scss +++ /dev/null @@ -1,35 +0,0 @@ -:host { - background: var(--sl-elevation-surface-raised-inverted); - border: 0; - border-radius: var(--sl-size-borderRadius-default); - box-sizing: border-box; - color: var(--sl-color-foreground-inverted-plain); - font-weight: var(--sl-text-new-typeset-fontWeight-regular); - margin: var(--sl-size-150); - opacity: 0; - padding: var(--sl-size-100) var(--sl-size-150); - position-area: top; - position-try-fallbacks: bottom; - text-align: center; - text-wrap: pretty; - - @media (prefers-reduced-motion: no-preference) { - transition-duration: 150ms; - transition-property: opacity; - transition-timing-function: cubic-bezier(0.4, 0, 1, 1); - } -} - -:host(:popover-open) { - @starting-style { - display: block; - opacity: 0; - } - - opacity: 1; - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); -} - -[part='hover-bridge'] { - position: fixed; -} diff --git a/packages/components/tooltip/src/tooltip2.stories.ts b/packages/components/tooltip/src/tooltip2.stories.ts deleted file mode 100644 index 8917722470..0000000000 --- a/packages/components/tooltip/src/tooltip2.stories.ts +++ /dev/null @@ -1,111 +0,0 @@ -import '@sl-design-system/button/register.js'; -import { Meta } from '@storybook/web-components-vite'; -import { TemplateResult, html, nothing } from 'lit'; -import { ifDefined } from 'lit/directives/if-defined.js'; -import { Tooltip2 } from './tooltip2.js'; - -type Props = Pick & { - maxWidth: number; - position: string; - showHoverBridge: boolean; - text: string; - tooltip(): TemplateResult; - trigger: string[]; -}; - -try { - customElements.define('sl-tooltip2', Tooltip2); -} catch { - /* empty */ -} - -export default { - title: 'Overlay/Tooltip2', - parameters: { - layout: 'centered' - }, - argTypes: { - disabled: { - control: 'boolean' - }, - maxWidth: { - control: 'number' - }, - open: { - control: 'boolean' - }, - position: { - control: 'inline-radio', - options: ['top', 'right', 'bottom', 'left'] - }, - showHoverBridge: { - control: 'boolean' - }, - text: { - control: 'text' - }, - tooltip: { - table: { disable: true } - }, - trigger: { - control: 'inline-check', - options: ['click', 'hover', 'focus', 'manual'] - } - }, - args: { - text: 'Tooltip text' - }, - render: ({ disabled, maxWidth, open, position, showHoverBridge, text, tooltip, trigger }) => html` - Anchor - ${tooltip - ? tooltip() - : html` - - ${text} - - `} - - ` -} satisfies Meta; - -export const Basic = {}; - -export const ClickTrigger = { - args: { - text: 'Click again to dismiss', - trigger: ['click'] - } -}; - -export const Disabled = { - args: { - disabled: true - } -}; - -export const HoverBridge = { - args: { - maxWidth: 200, - showHoverExtender: true, - text: 'The hotpink area bridges the area between anchor and tooltip, making it possible to move the mouse from the anchor to the tooltip without it disappearing.' - } -}; - -export const Positions = { - args: { - tooltip: () => html` - Top - Right - Bottom - Left - ` - } -}; diff --git a/packages/components/tooltip/src/tooltip2.ts b/packages/components/tooltip/src/tooltip2.ts deleted file mode 100644 index d964276952..0000000000 --- a/packages/components/tooltip/src/tooltip2.ts +++ /dev/null @@ -1,364 +0,0 @@ -import { CSSResultGroup, LitElement, PropertyValues, TemplateResult, html } from 'lit'; -import { property, state } from 'lit/decorators.js'; -import styles from './tooltip2.scss.js'; - -let nextUniqueId = 0; - -const SHOW_DELAY = 150, - HIDE_DELAY = 0; - -/** - * A tooltip component that can be used to display additional information about an element when the - * user hovers over it, focuses it, or clicks it. The tooltip is positioned relative to an anchor - * element, which can be specified using the `for` attribute. - * - * The tooltip will automatically determine the appropriate ARIA relation to use based on the `type` - * property. By default, it will use `ariaLabelledByElements`, but if `type` is set to - * `description`, it will use `ariaDescribedByElements` instead. - * - * @element sl-tooltip2 - * - * @slot - The content of the tooltip. - * - * @csspart arrow - The arrow element that points to the anchor. - * @csspart safe-triangle - An invisible element used to extend the hover area of the tooltip. - */ -export class Tooltip2 extends LitElement { - /** @internal */ - static override styles: CSSResultGroup = styles; - - /** Controller for managing event listeners. */ - #eventController = new AbortController(); - - /** Timeout ID for the hover delay. */ - #hoverTimeout?: ReturnType; - - /** @internal The element this tooltip is anchored to. */ - @state() anchor?: HTMLElement | null; - - /** - * Stops the tooltip from being displayed. - * - * @default false - */ - @property({ type: Boolean }) disabled?: boolean; - - /** The ID of the element this tooltip is for. */ - @property() for?: string; - - /** - * Setting this will cause the tooltip to show/hide, regardless of trigger. Do not use this - * property to check if the tooltip is showing, use `matches(':popover-open')` instead. - * - * @default false - */ - @property({ type: Boolean }) open?: boolean; - - /** - * Controls how the tooltip is activated. Possible options include `click`, `hover`, `focus`, and - * `manual`. Multiple options can be passed by separating them with a space. When manual is used, - * the tooltip must be activated programmatically. - * - * @default 'focus hover' - */ - @property() trigger = 'focus hover'; - - /** - * The type of tooltip. Used to determine the ARIA relation that should be used. - * - * @default 'label' - */ - @property() type?: 'description' | 'label'; - - override connectedCallback() { - super.connectedCallback(); - - this.setAttribute('aria-hidden', 'true'); - this.setAttribute('popover', 'manual'); - this.setAttribute('role', 'tooltip'); - - if (!this.id) { - this.id = `sl-tooltip-${nextUniqueId++}`; - } - - if (this.#eventController.signal.aborted) { - this.#eventController = new AbortController(); - } - - const { signal } = this.#eventController; - - this.addEventListener('beforetoggle', this.#onBeforeToggle, { signal }); - this.addEventListener('mouseout', this.#onMouseOut, { signal }); - this.addEventListener('toggle', this.#onToggle, { signal }); - - // Re-establish the anchor relationship if the tooltip is moved to a different root - if (this.anchor && this.for) { - this.anchor = undefined; // triggers #updateAnchor() - } else if (this.for) { - this.#updateAnchor(); - } - } - - override disconnectedCallback() { - clearTimeout(this.#hoverTimeout); - - this.#eventController.abort(); - - // Remove the event handler in case the tooltip is still open when disconnected - document.removeEventListener('keydown', this.#onKeydown); - - if (this.anchor) { - this.#removeAriaRelation(this.anchor); - } - - super.disconnectedCallback(); - } - - override willUpdate(changes: PropertyValues): void { - super.willUpdate(changes); - - if (changes.has('anchor') || changes.has('for')) { - this.#updateAnchor(); - } - - if (changes.has('disabled') && this.disabled) { - this.hidePopover(); - } - - if (changes.has('open')) { - if (this.open) { - this.showPopover(); - } else { - this.hidePopover(); - } - } - } - - override render(): TemplateResult { - return html` - -
-
- `; - } - - #onBeforeToggle = (event: ToggleEvent): void => { - if (event.newState === 'open') { - if (this.disabled) { - event.preventDefault(); - return; - } - - document.addEventListener('keydown', this.#onKeydown); - } else { - document.removeEventListener('keydown', this.#onKeydown); - } - }; - - #onBlur = (): void => { - if (this.#hasTrigger('focus')) { - this.hidePopover(); - } - }; - - #onClick = (): void => { - if (this.#hasTrigger('click')) { - if (this.matches(':popover-open')) { - this.hidePopover(); - } else { - this.showPopover(); - } - } else { - this.hidePopover(); - } - }; - - #onFocus = (event: Event): void => { - console.log('onFocus', event.target); - if (this.#hasTrigger('focus')) { - this.showPopover(); - } - }; - - #onKeydown = (event: KeyboardEvent): void => { - if (event.key === 'Escape') { - this.hidePopover(); - } - }; - - #onMouseOver = (): void => { - if (this.#hasTrigger('hover')) { - clearTimeout(this.#hoverTimeout); - - this.#hoverTimeout = setTimeout(() => { - this.showPopover(); - }, SHOW_DELAY); - } - }; - - #onMouseOut = (): void => { - if (this.#hasTrigger('hover')) { - // Don't hide the popover if either the anchor or the popover itself is still hovered - const anchorHovered = Boolean(this.anchor?.matches(':hover')), - tooltipHovered = this.matches(':hover'); - if (anchorHovered || tooltipHovered) { - return; - } - - clearTimeout(this.#hoverTimeout); - - if (!(anchorHovered || tooltipHovered)) { - this.#hoverTimeout = setTimeout(() => { - this.hidePopover(); - }, HIDE_DELAY); - } - } - }; - - #onToggle = (event: ToggleEvent): void => { - if (event.newState === 'open' && this.anchor) { - this.#positionHoverExtender(this.anchor); - } - }; - - #hasTrigger(trigger: string): boolean { - return this.trigger.split(' ').includes(trigger); - } - - #getAriaPropertyFromType( - type?: 'description' | 'label' - ): 'ariaDescribedByElements' | 'ariaLabelledByElements' { - return type === 'description' ? 'ariaDescribedByElements' : 'ariaLabelledByElements'; - } - - #addAriaRelation(element: Element): void { - const ariaProperty = this.#getAriaPropertyFromType(this.type); - - const refs = element[ariaProperty] ?? []; - if (!refs.includes(this)) { - element[ariaProperty] = [...refs, this]; - } - } - - #removeAriaRelation(element: Element): void { - const ariaProperty = this.#getAriaPropertyFromType(this.type); - - const refs = element[ariaProperty] ?? []; - element[ariaProperty] = refs.filter((ref: Element) => ref !== this); - } - - #positionHoverExtender(anchor: Element): void { - const extender = this.renderRoot.querySelector('[part="hover-extender"]'); - if (!extender) { - return; - } - - const a = anchor.getBoundingClientRect(), - t = this.getBoundingClientRect(); - - // Determine on which side of the anchor the tooltip ended up (after CSS anchor positioning - // and any position-try fallbacks). We then build a trapezoid whose parallel edges align with - // the touching edges of the anchor and the tooltip, so the user can move the pointer between - // the two without crossing an unhovered area. - let left: number, top: number, width: number, height: number, polygon: string; - - if (t.bottom <= a.top) { - // Tooltip above anchor - left = Math.min(a.left, t.left); - top = t.bottom; - width = Math.max(a.right, t.right) - left; - height = Math.max(0, a.top - t.bottom); - polygon = - `polygon(${t.left - left}px 0, ${t.right - left}px 0, ` + - `${a.right - left}px ${height}px, ${a.left - left}px ${height}px)`; - } else if (t.top >= a.bottom) { - // Tooltip below anchor - left = Math.min(a.left, t.left); - top = a.bottom; - width = Math.max(a.right, t.right) - left; - height = Math.max(0, t.top - a.bottom); - polygon = - `polygon(${a.left - left}px 0, ${a.right - left}px 0, ` + - `${t.right - left}px ${height}px, ${t.left - left}px ${height}px)`; - } else if (t.right <= a.left) { - // Tooltip left of anchor - left = t.right; - top = Math.min(a.top, t.top); - width = Math.max(0, a.left - t.right); - height = Math.max(a.bottom, t.bottom) - top; - polygon = - `polygon(0 ${t.top - top}px, 0 ${t.bottom - top}px, ` + - `${width}px ${a.bottom - top}px, ${width}px ${a.top - top}px)`; - } else if (t.left >= a.right) { - // Tooltip right of anchor - left = a.right; - top = Math.min(a.top, t.top); - width = Math.max(0, t.left - a.right); - height = Math.max(a.bottom, t.bottom) - top; - polygon = - `polygon(0 ${a.top - top}px, 0 ${a.bottom - top}px, ` + - `${width}px ${t.bottom - top}px, ${width}px ${t.top - top}px)`; - } else { - // Tooltip and anchor overlap; no bridge needed. - extender.style.display = 'none'; - return; - } - - extender.style.left = `${left}px`; - extender.style.top = `${top}px`; - extender.style.width = `${width}px`; - extender.style.height = `${height}px`; - extender.style.clipPath = polygon; - } - - #updateAnchor(): void { - if (!this.for) { - this.anchor = undefined; - return; - } - - const rootNode = this.getRootNode() as Document | ShadowRoot | null; - if (!rootNode) { - this.anchor = undefined; - return; - } - - const newAnchor = this.for ? rootNode.getElementById(this.for) : null, - oldAnchor = this.anchor; - if (newAnchor === oldAnchor) { - return; - } - - const { signal } = this.#eventController; - - if (newAnchor) { - this.#addAriaRelation(newAnchor); - - newAnchor.addEventListener('blur', this.#onBlur, { capture: true, signal }); - newAnchor.addEventListener('click', this.#onClick, { signal }); - newAnchor.addEventListener('focus', this.#onFocus, { capture: true, signal }); - newAnchor.addEventListener('mouseover', this.#onMouseOver, { signal }); - newAnchor.addEventListener('mouseout', this.#onMouseOut, { signal }); - - // Do not overwrite an existing anchor name, as it might be used for something else. - const newAnchorName = newAnchor.style.anchorName || `--${this.id}`; - - newAnchor.style.anchorName = newAnchorName; - this.style.positionAnchor = newAnchorName; - } - - if (oldAnchor) { - this.#removeAriaRelation(oldAnchor); - - oldAnchor.removeEventListener('blur', this.#onBlur, { capture: true }); - oldAnchor.removeEventListener('click', this.#onClick); - oldAnchor.removeEventListener('focus', this.#onFocus, { capture: true }); - oldAnchor.removeEventListener('mouseover', this.#onMouseOver); - oldAnchor.removeEventListener('mouseout', this.#onMouseOut); - oldAnchor.style.anchorName = ''; - this.style.positionAnchor = ''; - } - - this.anchor = newAnchor; - } -} From 6a863c10e0fdab14e8bba7d12efb3b3b8a39879c Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Tue, 26 May 2026 10:22:05 +0200 Subject: [PATCH 10/50] =?UTF-8?q?=F0=9F=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chromatic/package.json | 16 +- package.json | 48 +- packages/angular/package.json | 42 +- packages/components/announcer/package.json | 2 +- packages/components/badge/package.json | 2 +- packages/components/button-bar/package.json | 2 +- packages/components/button/package.json | 2 +- packages/components/calendar/package.json | 2 +- packages/components/card/package.json | 2 +- packages/components/combobox/package.json | 2 +- packages/components/date-field/package.json | 2 +- packages/components/dialog/package.json | 2 +- packages/components/drawer/package.json | 2 +- packages/components/grid/package.json | 2 +- packages/components/icon/package.json | 2 +- packages/components/infotip/package.json | 2 +- packages/components/listbox/package.json | 2 +- packages/components/progress-bar/package.json | 2 +- packages/components/scrollbar/package.json | 2 +- packages/components/select/package.json | 2 +- packages/components/shared/package.json | 2 +- packages/components/skeleton/package.json | 2 +- packages/components/spinner/package.json | 2 +- packages/components/tag/package.json | 2 +- packages/components/time-field/package.json | 2 +- .../components/toggle-button/package.json | 2 +- packages/components/toggle-group/package.json | 2 +- packages/components/tool-bar/package.json | 2 +- packages/components/tree/package.json | 2 +- packages/components/virtual-list/package.json | 2 +- scripts/package.json | 6 +- tools/eslint-config/package.json | 4 +- tools/eslint-plugin-slds/package.json | 2 +- tools/stylelint-config/package.json | 2 +- tools/theme-selector-plugin/package.json | 2 +- tools/vitest-browser-lit/package.json | 6 +- website/package.json | 4 +- yarn.lock | 2377 ++++++++++------- 38 files changed, 1448 insertions(+), 1115 deletions(-) diff --git a/chromatic/package.json b/chromatic/package.json index 5fbcea4d76..f27541e6eb 100644 --- a/chromatic/package.json +++ b/chromatic/package.json @@ -40,15 +40,15 @@ } }, "devDependencies": { - "@storybook/addon-a11y": "^10.3.5", - "@storybook/addon-docs": "^10.3.5", - "@storybook/addon-themes": "^10.3.5", - "@storybook/web-components-vite": "^10.3.5", - "lit": "^3.3.2", - "storybook": "^10.3.5", - "storybook-addon-pseudo-states": "^10.3.5", + "@storybook/addon-a11y": "^10.4.1", + "@storybook/addon-docs": "^10.4.1", + "@storybook/addon-themes": "^10.4.1", + "@storybook/web-components-vite": "^10.4.1", + "lit": "^3.3.3", + "storybook": "^10.4.1", + "storybook-addon-pseudo-states": "^10.4.1", "tslib": "^2.8.1", - "typescript": "^6.0.2", + "typescript": "^6.0.3", "wireit": "^0.14.12" } } diff --git a/package.json b/package.json index 2a3e8c9478..c1889c7bd2 100644 --- a/package.json +++ b/package.json @@ -389,47 +389,47 @@ }, "devDependencies": { "@af-utils/scrollend-polyfill": "^0.0.14", - "@axe-core/playwright": "^4.11.1", - "@changesets/cli": "^2.30.0", + "@axe-core/playwright": "^4.11.3", + "@changesets/cli": "^2.31.0", "@changesets/get-github-info": "^0.8.0", "@custom-elements-manifest/analyzer": "^0.11.0", - "@faker-js/faker": "^10.3.0", - "@lit/localize-tools": "^0.8.1", - "@playwright/test": "^1.58.2", - "@storybook/addon-a11y": "^10.3.5", - "@storybook/addon-docs": "^10.3.5", - "@storybook/addon-vitest": "^10.3.5", - "@storybook/web-components": "^10.3.5", - "@storybook/web-components-vite": "^10.3.5", + "@faker-js/faker": "^10.4.0", + "@lit/localize-tools": "^0.8.2", + "@playwright/test": "^1.60.0", + "@storybook/addon-a11y": "^10.4.1", + "@storybook/addon-docs": "^10.4.1", + "@storybook/addon-vitest": "^10.4.1", + "@storybook/web-components": "^10.4.1", + "@storybook/web-components-vite": "^10.4.1", "@types/chai-datetime": "^1.0.0", "@types/chai-dom": "^1.11.3", - "@types/sinon": "^21.0.0", + "@types/sinon": "^21.0.1", "@types/sinon-chai": "^4.0.0", - "@vitest/browser-playwright": "~4.1.4", - "@vitest/coverage-v8": "~4.1.4", - "@vitest/ui": "~4.1.4", + "@vitest/browser-playwright": "~4.1.7", + "@vitest/coverage-v8": "~4.1.7", + "@vitest/ui": "~4.1.7", "@webcomponents/scoped-custom-element-registry": "^0.0.10", "axe-html-reporter": "^2.2.11", "chai": "^6.2.2", "chai-datetime": "^1.8.1", "chai-dom": "^1.12.1", - "chromatic": "^15.3.0", + "chromatic": "^15.3.1", "eslint": "^9.27.0", "husky": "^9.1.7", "invokers-polyfill": "^1.0.3", - "lint-staged": "^16.4.0", - "lit": "^3.3.2", + "lint-staged": "^17.0.5", + "lit": "^3.3.3", "mockdate": "^3.0.5", - "oxfmt": "^0.46.0", - "playwright": "^1.59.1", - "sinon": "^21.0.3", + "oxfmt": "^0.52.0", + "playwright": "^1.60.0", + "sinon": "^21.1.2", "sinon-chai": "^4.0.1", - "storybook": "^10.3.5", + "storybook": "^10.4.1", "storybook-addon-tag-badges": "^3.1.0", "stylelint": "^16.19.1", - "typescript": "^6.0.2", - "vite": "^8.0.8", - "vitest": "~4.1.4", + "typescript": "^6.0.3", + "vite": "^8.0.14", + "vitest": "~4.1.7", "wireit": "^0.14.12" } } diff --git a/packages/angular/package.json b/packages/angular/package.json index a2c3219870..7bd5315fa2 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -99,37 +99,37 @@ "@sl-design-system/time-field": "^0.1.0" }, "devDependencies": { - "@angular-devkit/architect": "^0.2102.6", - "@angular-devkit/build-angular": "^21.2.6", - "@angular-devkit/core": "^21.2.6", - "@angular/animations": "^21.2.7", - "@angular/cli": "^21.2.6", - "@angular/common": "^21.2.7", - "@angular/compiler": "^21.2.7", - "@angular/compiler-cli": "^21.2.7", - "@angular/core": "^21.2.7", - "@angular/forms": "^21.2.7", - "@angular/platform-browser": "^21.2.7", - "@angular/platform-browser-dynamic": "^21.2.7", - "@angular/router": "^21.2.7", + "@angular-devkit/architect": "^0.2102.12", + "@angular-devkit/build-angular": "^21.2.12", + "@angular-devkit/core": "^21.2.12", + "@angular/animations": "^21.2.14", + "@angular/cli": "^21.2.12", + "@angular/common": "^21.2.14", + "@angular/compiler": "^21.2.14", + "@angular/compiler-cli": "^21.2.14", + "@angular/core": "^21.2.14", + "@angular/forms": "^21.2.14", + "@angular/platform-browser": "^21.2.14", + "@angular/platform-browser-dynamic": "^21.2.14", + "@angular/router": "^21.2.14", "@standard-schema/spec": "^1.1.0", - "@storybook/addon-docs": "^10.3.5", - "@storybook/angular": "^10.3.5", + "@storybook/addon-docs": "^10.4.1", + "@storybook/angular": "^10.4.1", "@types/jasmine": "~6.0.0", - "baseline-browser-mapping": "^2.10.9", - "jasmine-core": "~6.1.0", + "baseline-browser-mapping": "^2.10.32", + "jasmine-core": "~6.2.0", "karma": "~6.4.4", "karma-chrome-launcher": "~3.2.0", "karma-coverage": "~2.2.1", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.2.0", - "ng-packagr": "^21.2.2", + "ng-packagr": "^21.2.3", "rxjs": "~7.8.2", - "storybook": "^10.3.5", + "storybook": "^10.4.1", "tslib": "^2.8.1", - "typescript": "~6.0.2", + "typescript": "~6.0.3", "wireit": "^0.14.12", - "zone.js": "~0.16.1" + "zone.js": "~0.16.2" }, "installConfig": { "hoistingLimits": "workspaces" diff --git a/packages/components/announcer/package.json b/packages/components/announcer/package.json index 634ba7e306..be7a4898de 100644 --- a/packages/components/announcer/package.json +++ b/packages/components/announcer/package.json @@ -44,7 +44,7 @@ "devDependencies": { "@lit/localize": "^0.12.2", "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@lit/localize": "^0.12.1", diff --git a/packages/components/badge/package.json b/packages/components/badge/package.json index 6f89466f65..bddf7589f7 100644 --- a/packages/components/badge/package.json +++ b/packages/components/badge/package.json @@ -39,7 +39,7 @@ "test": "echo \"Error: run tests from monorepo root.\" && exit 1" }, "devDependencies": { - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "lit": "^3.1.4" diff --git a/packages/components/button-bar/package.json b/packages/components/button-bar/package.json index 2ee8e49e2a..0687177f88 100644 --- a/packages/components/button-bar/package.json +++ b/packages/components/button-bar/package.json @@ -42,7 +42,7 @@ "@sl-design-system/button": "^2.0.0" }, "devDependencies": { - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "lit": "^3.1.4" diff --git a/packages/components/button/package.json b/packages/components/button/package.json index 1cc920d6b1..ad32393b53 100644 --- a/packages/components/button/package.json +++ b/packages/components/button/package.json @@ -44,7 +44,7 @@ }, "devDependencies": { "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@open-wc/scoped-elements": "^3.0.6", diff --git a/packages/components/calendar/package.json b/packages/components/calendar/package.json index 2244998028..c526669cfd 100644 --- a/packages/components/calendar/package.json +++ b/packages/components/calendar/package.json @@ -47,7 +47,7 @@ "devDependencies": { "@lit/localize": "^0.12.2", "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@lit/localize": "^0.12.1", diff --git a/packages/components/card/package.json b/packages/components/card/package.json index e40ca16cab..14d9edbcb0 100644 --- a/packages/components/card/package.json +++ b/packages/components/card/package.json @@ -39,7 +39,7 @@ "test": "echo \"Error: run tests from monorepo root.\" && exit 1" }, "devDependencies": { - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "lit": "^3.1.4" diff --git a/packages/components/combobox/package.json b/packages/components/combobox/package.json index 9e922808f5..a2da27c603 100644 --- a/packages/components/combobox/package.json +++ b/packages/components/combobox/package.json @@ -47,7 +47,7 @@ }, "devDependencies": { "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@open-wc/scoped-elements": "^3.0.6", diff --git a/packages/components/date-field/package.json b/packages/components/date-field/package.json index 800b4f82d9..7c342d633c 100644 --- a/packages/components/date-field/package.json +++ b/packages/components/date-field/package.json @@ -49,7 +49,7 @@ }, "devDependencies": { "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@open-wc/scoped-elements": "^3.0.6", diff --git a/packages/components/dialog/package.json b/packages/components/dialog/package.json index 2b68bfa93b..42729a903b 100644 --- a/packages/components/dialog/package.json +++ b/packages/components/dialog/package.json @@ -47,7 +47,7 @@ "devDependencies": { "@lit/localize": "^0.12.2", "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@lit/localize": "^0.12.1", diff --git a/packages/components/drawer/package.json b/packages/components/drawer/package.json index 850a1dd255..b8a5d194a5 100644 --- a/packages/components/drawer/package.json +++ b/packages/components/drawer/package.json @@ -44,7 +44,7 @@ }, "devDependencies": { "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@open-wc/scoped-elements": "^3.0.6", diff --git a/packages/components/grid/package.json b/packages/components/grid/package.json index a75c05305b..19fbc5446f 100644 --- a/packages/components/grid/package.json +++ b/packages/components/grid/package.json @@ -61,7 +61,7 @@ "@lit/localize": "^0.12.2", "@open-wc/scoped-elements": "^3.0.6", "@sl-design-system/example-data": "^1.0.0", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@lit-labs/virtualizer": "^2.0.13", diff --git a/packages/components/icon/package.json b/packages/components/icon/package.json index f2c2de4b60..7f0be9b8d0 100644 --- a/packages/components/icon/package.json +++ b/packages/components/icon/package.json @@ -39,7 +39,7 @@ "test": "echo \"Error: run tests from monorepo root.\" && exit 1" }, "devDependencies": { - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "lit": "^3.1.4" diff --git a/packages/components/infotip/package.json b/packages/components/infotip/package.json index a6c2520843..783930e95b 100644 --- a/packages/components/infotip/package.json +++ b/packages/components/infotip/package.json @@ -46,7 +46,7 @@ "devDependencies": { "@lit/localize": "^0.12.2", "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@lit/localize": "^0.12.1", diff --git a/packages/components/listbox/package.json b/packages/components/listbox/package.json index d6419069d8..18d89eda65 100644 --- a/packages/components/listbox/package.json +++ b/packages/components/listbox/package.json @@ -44,7 +44,7 @@ "devDependencies": { "@lit-labs/virtualizer": "^2.1.1", "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@lit-labs/virtualizer": "^2.0.13", diff --git a/packages/components/progress-bar/package.json b/packages/components/progress-bar/package.json index 8ce00d286f..f6213056b7 100644 --- a/packages/components/progress-bar/package.json +++ b/packages/components/progress-bar/package.json @@ -44,7 +44,7 @@ "devDependencies": { "@lit/localize": "^0.12.2", "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@lit/localize": "^0.12.1", diff --git a/packages/components/scrollbar/package.json b/packages/components/scrollbar/package.json index da8eeb6acf..aac1d953af 100644 --- a/packages/components/scrollbar/package.json +++ b/packages/components/scrollbar/package.json @@ -39,7 +39,7 @@ "test": "echo \"Error: run tests from monorepo root.\" && exit 1" }, "devDependencies": { - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "lit": "^3.1.4" diff --git a/packages/components/select/package.json b/packages/components/select/package.json index 8f9e932d3a..9f1b87bb30 100644 --- a/packages/components/select/package.json +++ b/packages/components/select/package.json @@ -47,7 +47,7 @@ "devDependencies": { "@lit/localize": "^0.12.2", "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@lit/localize": "^0.12.1", diff --git a/packages/components/shared/package.json b/packages/components/shared/package.json index c0a0bc93ca..ce9d04b914 100644 --- a/packages/components/shared/package.json +++ b/packages/components/shared/package.json @@ -57,7 +57,7 @@ "devDependencies": { "@floating-ui/dom": "^1.7.6", "@open-wc/dedupe-mixin": "^2.0.1", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@floating-ui/dom": "^1.6.5", diff --git a/packages/components/skeleton/package.json b/packages/components/skeleton/package.json index f90ceef38b..38fa4d0a67 100644 --- a/packages/components/skeleton/package.json +++ b/packages/components/skeleton/package.json @@ -39,7 +39,7 @@ "test": "echo \"Error: run tests from monorepo root.\" && exit 1" }, "devDependencies": { - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "lit": "^3.1.4" diff --git a/packages/components/spinner/package.json b/packages/components/spinner/package.json index 03aae31a99..126ce17fb4 100644 --- a/packages/components/spinner/package.json +++ b/packages/components/spinner/package.json @@ -39,7 +39,7 @@ "test": "echo \"Error: run tests from monorepo root.\" && exit 1" }, "devDependencies": { - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "lit": "^3.1.4" diff --git a/packages/components/tag/package.json b/packages/components/tag/package.json index a704699851..40e0bb10c6 100644 --- a/packages/components/tag/package.json +++ b/packages/components/tag/package.json @@ -46,7 +46,7 @@ "devDependencies": { "@lit/localize": "^0.12.2", "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@lit/localize": "^0.12.1", diff --git a/packages/components/time-field/package.json b/packages/components/time-field/package.json index a1bbe49701..12ba004aac 100644 --- a/packages/components/time-field/package.json +++ b/packages/components/time-field/package.json @@ -46,7 +46,7 @@ }, "devDependencies": { "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@open-wc/scoped-elements": "^3.0.6", diff --git a/packages/components/toggle-button/package.json b/packages/components/toggle-button/package.json index 21c5e334f8..a2d718f89f 100644 --- a/packages/components/toggle-button/package.json +++ b/packages/components/toggle-button/package.json @@ -45,7 +45,7 @@ }, "devDependencies": { "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@open-wc/scoped-elements": "^3.0.6", diff --git a/packages/components/toggle-group/package.json b/packages/components/toggle-group/package.json index 8565e24b3d..44431725e4 100644 --- a/packages/components/toggle-group/package.json +++ b/packages/components/toggle-group/package.json @@ -44,7 +44,7 @@ }, "devDependencies": { "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "lit": "^3.1.4" diff --git a/packages/components/tool-bar/package.json b/packages/components/tool-bar/package.json index e610c11b14..65f47bc94b 100644 --- a/packages/components/tool-bar/package.json +++ b/packages/components/tool-bar/package.json @@ -48,7 +48,7 @@ "devDependencies": { "@lit/localize": "^0.12.2", "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@open-wc/scoped-elements": "^3.0.6" diff --git a/packages/components/tree/package.json b/packages/components/tree/package.json index 0c061c8ade..e381860c62 100644 --- a/packages/components/tree/package.json +++ b/packages/components/tree/package.json @@ -50,7 +50,7 @@ }, "devDependencies": { "@open-wc/scoped-elements": "^3.0.6", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@open-wc/scoped-elements": "^3.0.6", diff --git a/packages/components/virtual-list/package.json b/packages/components/virtual-list/package.json index 03d805cbbd..c200f721c6 100644 --- a/packages/components/virtual-list/package.json +++ b/packages/components/virtual-list/package.json @@ -43,7 +43,7 @@ }, "devDependencies": { "@tanstack/virtual-core": "^3.13.23", - "lit": "^3.3.2" + "lit": "^3.3.3" }, "peerDependencies": { "@tanstack/virtual-core": "^3.13.12", diff --git a/scripts/package.json b/scripts/package.json index 67fe4daa47..7560ece926 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -25,12 +25,12 @@ "@fortawesome/sharp-solid-svg-icons": "^7.2.0", "@tokens-studio/sd-transforms": "^2.0.1", "@tokens-studio/sdk": "^3.0.0", - "cssnano": "^7.1.4", + "cssnano": "^7.1.9", "esbuild": "^0.25.12", "fast-glob": "^3.3.3", "graphql": "^16.13.1", - "postcss": "^8.5.8", - "sass": "^1.99.0", + "postcss": "^8.5.15", + "sass": "^1.100.0", "style-dictionary": "^5.1.1" } } diff --git a/tools/eslint-config/package.json b/tools/eslint-config/package.json index 306123db7b..83960bae15 100644 --- a/tools/eslint-config/package.json +++ b/tools/eslint-config/package.json @@ -48,7 +48,7 @@ }, "devDependencies": { "eslint": "^9.27.0", - "storybook": "^10.3.5", - "typescript": "^6.0.2" + "storybook": "^10.4.1", + "typescript": "^6.0.3" } } diff --git a/tools/eslint-plugin-slds/package.json b/tools/eslint-plugin-slds/package.json index 15d616e175..9176e696e2 100644 --- a/tools/eslint-plugin-slds/package.json +++ b/tools/eslint-plugin-slds/package.json @@ -34,6 +34,6 @@ "devDependencies": { "eslint": "^9.27.0", "eslint-plugin-lit-a11y": "^4.1.4", - "mocha": "11.7.5" + "mocha": "11.7.6" } } diff --git a/tools/stylelint-config/package.json b/tools/stylelint-config/package.json index 9159a37110..c70a50b59f 100644 --- a/tools/stylelint-config/package.json +++ b/tools/stylelint-config/package.json @@ -23,7 +23,7 @@ "test": "echo \"Error: run tests from monorepo root.\" && exit 1" }, "dependencies": { - "postcss": "^8.5.8", + "postcss": "^8.5.15", "postcss-lit": "^1.4.1", "stylelint-config-standard": "^38.0.0", "stylelint-config-standard-scss": "^15.0.1", diff --git a/tools/theme-selector-plugin/package.json b/tools/theme-selector-plugin/package.json index cd21a800a9..f90b39ceea 100644 --- a/tools/theme-selector-plugin/package.json +++ b/tools/theme-selector-plugin/package.json @@ -18,6 +18,6 @@ "@typescript-eslint/eslint-plugin": "^8.58.0", "@typescript-eslint/parser": "^8.58.0", "eslint": "^9.27.0", - "typescript": "^6.0.2" + "typescript": "^6.0.3" } } diff --git a/tools/vitest-browser-lit/package.json b/tools/vitest-browser-lit/package.json index 7c1976931b..c8c5dcc0b4 100644 --- a/tools/vitest-browser-lit/package.json +++ b/tools/vitest-browser-lit/package.json @@ -28,8 +28,8 @@ "vitest": ">=2.1.0" }, "devDependencies": { - "@vitest/browser": "^4.1.4", - "lit": "^3.3.2", - "vitest": "^4.1.4" + "@vitest/browser": "^4.1.7", + "lit": "^3.3.3", + "vitest": "^4.1.7" } } diff --git a/website/package.json b/website/package.json index 230d01988b..900486649a 100644 --- a/website/package.json +++ b/website/package.json @@ -189,10 +189,10 @@ "markdown-it": "^14.1.0", "markdown-it-anchor": "^8.6.7", "markdown-it-attrs": "^4.1.6", - "sass": "^1.99.0", + "sass": "^1.100.0", "slugify": "^1.6.8", "tiny-glob": "^0.2.9", - "typescript": "^6.0.2", + "typescript": "^6.0.3", "wireit": "^0.14.12" } } diff --git a/yarn.lock b/yarn.lock index b58782f935..5c8653b3ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -327,27 +327,27 @@ __metadata: languageName: node linkType: hard -"@angular-devkit/architect@npm:0.2102.6, @angular-devkit/architect@npm:^0.2102.6": - version: 0.2102.6 - resolution: "@angular-devkit/architect@npm:0.2102.6" +"@angular-devkit/architect@npm:0.2102.12, @angular-devkit/architect@npm:^0.2102.12": + version: 0.2102.12 + resolution: "@angular-devkit/architect@npm:0.2102.12" dependencies: - "@angular-devkit/core": "npm:21.2.6" + "@angular-devkit/core": "npm:21.2.12" rxjs: "npm:7.8.2" bin: architect: bin/cli.js - checksum: 10c0/55c7f00b3097db16db4760172d7bd2e92a71c9b042d9131db5ba619833da25c4d4dea8d60304ff9d9d2753e20e79e1e1b55688af2bade1f69947edc1dc2b40a3 + checksum: 10c0/0341e985e5c3e344caecd91264ba485f5c631cb745854f9513c2d4ce7861c315b99dcd92190b90ab8fda0a11bd8201509dc643260aacf5f2b45a6d80bb6258d7 languageName: node linkType: hard -"@angular-devkit/build-angular@npm:^21.2.6": - version: 21.2.6 - resolution: "@angular-devkit/build-angular@npm:21.2.6" +"@angular-devkit/build-angular@npm:^21.2.12": + version: 21.2.12 + resolution: "@angular-devkit/build-angular@npm:21.2.12" dependencies: "@ampproject/remapping": "npm:2.3.0" - "@angular-devkit/architect": "npm:0.2102.6" - "@angular-devkit/build-webpack": "npm:0.2102.6" - "@angular-devkit/core": "npm:21.2.6" - "@angular/build": "npm:21.2.6" + "@angular-devkit/architect": "npm:0.2102.12" + "@angular-devkit/build-webpack": "npm:0.2102.12" + "@angular-devkit/core": "npm:21.2.12" + "@angular/build": "npm:21.2.12" "@babel/core": "npm:7.29.0" "@babel/generator": "npm:7.29.1" "@babel/helper-annotate-as-pure": "npm:7.27.3" @@ -355,10 +355,10 @@ __metadata: "@babel/plugin-transform-async-generator-functions": "npm:7.29.0" "@babel/plugin-transform-async-to-generator": "npm:7.28.6" "@babel/plugin-transform-runtime": "npm:7.29.0" - "@babel/preset-env": "npm:7.29.0" - "@babel/runtime": "npm:7.28.6" + "@babel/preset-env": "npm:7.29.2" + "@babel/runtime": "npm:7.29.2" "@discoveryjs/json-ext": "npm:0.6.3" - "@ngtools/webpack": "npm:21.2.6" + "@ngtools/webpack": "npm:21.2.12" ansi-colors: "npm:4.1.3" autoprefixer: "npm:10.4.27" babel-loader: "npm:10.0.0" @@ -380,7 +380,7 @@ __metadata: ora: "npm:9.3.0" picomatch: "npm:4.0.4" piscina: "npm:5.1.4" - postcss: "npm:8.5.6" + postcss: "npm:8.5.12" postcss-loader: "npm:8.2.0" resolve-url-loader: "npm:5.0.0" rxjs: "npm:7.8.2" @@ -405,7 +405,7 @@ __metadata: "@angular/platform-browser": ^21.0.0 "@angular/platform-server": ^21.0.0 "@angular/service-worker": ^21.0.0 - "@angular/ssr": ^21.2.6 + "@angular/ssr": ^21.2.12 "@web/test-runner": ^0.20.0 browser-sync: ^3.0.2 jest: ^30.2.0 @@ -447,26 +447,26 @@ __metadata: optional: true tailwindcss: optional: true - checksum: 10c0/2186841ce13af269dd3ce04a65d9bbbe9ac1b6ba778b28ad708dcddf85f14e84c3447a550b15b2b7f7637488afed742191e3d3cad17380016e0f382fe6e92249 + checksum: 10c0/9008897d86fa4b1dbe2b004275db13e6e262ae86357a691d32f14c57f53de65ed63602058ba8528ebcb99f98d3c55a69d1002b4d668bf4f204bd4a3af79b22eb languageName: node linkType: hard -"@angular-devkit/build-webpack@npm:0.2102.6": - version: 0.2102.6 - resolution: "@angular-devkit/build-webpack@npm:0.2102.6" +"@angular-devkit/build-webpack@npm:0.2102.12": + version: 0.2102.12 + resolution: "@angular-devkit/build-webpack@npm:0.2102.12" dependencies: - "@angular-devkit/architect": "npm:0.2102.6" + "@angular-devkit/architect": "npm:0.2102.12" rxjs: "npm:7.8.2" peerDependencies: webpack: ^5.30.0 webpack-dev-server: ^5.0.2 - checksum: 10c0/4c7eeccb5288dce34364762a16cd6ffc271300d23902b3b11b5316b92c779db4d2ec15b802acccd304c88143e84d79d85784c872a5dc8f6019051401df8aa2ba + checksum: 10c0/31670b9b95c35921b5b71960bd6d1e7dd0c8748b45b9683811d9f987749f8403775792b20f230d3e2045b90ba22b31eaab6d8af49fcdf3b2c96de6f6b1a6f068 languageName: node linkType: hard -"@angular-devkit/core@npm:21.2.6, @angular-devkit/core@npm:^21.2.6": - version: 21.2.6 - resolution: "@angular-devkit/core@npm:21.2.6" +"@angular-devkit/core@npm:21.2.12, @angular-devkit/core@npm:^21.2.12": + version: 21.2.12 + resolution: "@angular-devkit/core@npm:21.2.12" dependencies: ajv: "npm:8.18.0" ajv-formats: "npm:3.0.1" @@ -479,40 +479,40 @@ __metadata: peerDependenciesMeta: chokidar: optional: true - checksum: 10c0/48f4bfd5f9a2804f0d1aa3528e7adef4f77138256911086007bae195ef46d1daeab05bb33b9714f250831121df52e79db7f9be39631ad612386e0e07f6872bd5 + checksum: 10c0/35d9803bd87b62f29c226665b7b0c98fd7faf4ca5df49439cd8ab91389fd256d019e36bbb356e271b8da7789d13198e6efa9a3bb936be6a3da2cb40f755ec2a2 languageName: node linkType: hard -"@angular-devkit/schematics@npm:21.2.6": - version: 21.2.6 - resolution: "@angular-devkit/schematics@npm:21.2.6" +"@angular-devkit/schematics@npm:21.2.12": + version: 21.2.12 + resolution: "@angular-devkit/schematics@npm:21.2.12" dependencies: - "@angular-devkit/core": "npm:21.2.6" + "@angular-devkit/core": "npm:21.2.12" jsonc-parser: "npm:3.3.1" magic-string: "npm:0.30.21" ora: "npm:9.3.0" rxjs: "npm:7.8.2" - checksum: 10c0/b286dc6dca144a389ec162ec118588ca77a5b11bc8f895e00ff7ed6fabd210ad96ab24830831de7947c2988edbb81dbfa19d9d055e13614bb21cf385d07cda0e + checksum: 10c0/c7de5aeeafbf71571f02fc1857ffb6d4292542f72d9c804bff1d2bc52041db83e0b4d45b4145f4628254e23ba3e1dbb20d98b181d622069bb6a25dbf4d7a7fcf languageName: node linkType: hard -"@angular/animations@npm:^21.2.7": - version: 21.2.7 - resolution: "@angular/animations@npm:21.2.7" +"@angular/animations@npm:^21.2.14": + version: 21.2.14 + resolution: "@angular/animations@npm:21.2.14" dependencies: tslib: "npm:^2.3.0" peerDependencies: - "@angular/core": 21.2.7 - checksum: 10c0/6483d5d9857cb491efbfb7a023269084849a1eaeff4fcd09b34cd441fc6cc8690fb9eb34015bfff806087c1ab10e786de8dc587137030d8ea80bc6c337cd8144 + "@angular/core": 21.2.14 + checksum: 10c0/5479d916350438d97129fbc3b92dacf4150d5bae1c812f0b73c7f5dab0d1a14f356136fc1c9f1a00b9187b8dee506e37e9677dc4bb63fbcbc079ea677235c4d0 languageName: node linkType: hard -"@angular/build@npm:21.2.6": - version: 21.2.6 - resolution: "@angular/build@npm:21.2.6" +"@angular/build@npm:21.2.12": + version: 21.2.12 + resolution: "@angular/build@npm:21.2.12" dependencies: "@ampproject/remapping": "npm:2.3.0" - "@angular-devkit/architect": "npm:0.2102.6" + "@angular-devkit/architect": "npm:0.2102.12" "@babel/core": "npm:7.29.0" "@babel/helper-annotate-as-pure": "npm:7.27.3" "@babel/helper-split-export-declaration": "npm:7.24.7" @@ -537,7 +537,7 @@ __metadata: source-map-support: "npm:0.5.21" tinyglobby: "npm:0.2.15" undici: "npm:7.24.4" - vite: "npm:7.3.1" + vite: "npm:7.3.2" watchpack: "npm:2.5.1" peerDependencies: "@angular/compiler": ^21.0.0 @@ -547,7 +547,7 @@ __metadata: "@angular/platform-browser": ^21.0.0 "@angular/platform-server": ^21.0.0 "@angular/service-worker": ^21.0.0 - "@angular/ssr": ^21.2.6 + "@angular/ssr": ^21.2.12 karma: ^6.4.0 less: ^4.2.0 ng-packagr: ^21.0.0 @@ -584,21 +584,21 @@ __metadata: optional: true vitest: optional: true - checksum: 10c0/d197a3c521546b5f7f7b98f540f052d217e3e12945fc2f2358b824302e9bf53321ffb0554ba02e3996b8bbf89aaa2d45f7f8202551ea5145a8e7bf7706b6c8df + checksum: 10c0/83e93cbfa500b19f5b30152f0d73588820ce8f583b0d76bf670ac970fefe0b76485d7933eefd0d411fe931ff3d0ef53ea233c92ec613de5192caafdadc10a082 languageName: node linkType: hard -"@angular/cli@npm:^21.2.6": - version: 21.2.6 - resolution: "@angular/cli@npm:21.2.6" +"@angular/cli@npm:^21.2.12": + version: 21.2.12 + resolution: "@angular/cli@npm:21.2.12" dependencies: - "@angular-devkit/architect": "npm:0.2102.6" - "@angular-devkit/core": "npm:21.2.6" - "@angular-devkit/schematics": "npm:21.2.6" + "@angular-devkit/architect": "npm:0.2102.12" + "@angular-devkit/core": "npm:21.2.12" + "@angular-devkit/schematics": "npm:21.2.12" "@inquirer/prompts": "npm:7.10.1" "@listr2/prompt-adapter-inquirer": "npm:3.0.5" "@modelcontextprotocol/sdk": "npm:1.26.0" - "@schematics/angular": "npm:21.2.6" + "@schematics/angular": "npm:21.2.12" "@yarnpkg/lockfile": "npm:1.1.0" algoliasearch: "npm:5.48.1" ini: "npm:6.0.0" @@ -612,25 +612,25 @@ __metadata: zod: "npm:4.3.6" bin: ng: bin/ng.js - checksum: 10c0/ca806e6b47bfbbe6db5ed04ab2410ca30ca21d5177c05ee3d80f0b1db734a1d8780cb4b686f060211d64db45e5b1d3e622d08129809bfb6f5a920bc399b791d0 + checksum: 10c0/60d29a6b92ed2712896e817decf3fef2ca12dcb36306eccf8fab845a1183018c2fe23ec934be63a9e7fa3c2af2116d484c8714f6e64f932ae1b982f0df6b568c languageName: node linkType: hard -"@angular/common@npm:^21.2.7": - version: 21.2.7 - resolution: "@angular/common@npm:21.2.7" +"@angular/common@npm:^21.2.14": + version: 21.2.14 + resolution: "@angular/common@npm:21.2.14" dependencies: tslib: "npm:^2.3.0" peerDependencies: - "@angular/core": 21.2.7 + "@angular/core": 21.2.14 rxjs: ^6.5.3 || ^7.4.0 - checksum: 10c0/3d365406cc12a479e1bb3c7b4656621a9b65dba370c45c24a76323eb9874d1b7d2ff5c2621fd9bc98419dec1f7531c84a649afd5e7d8f12c77e72b50625ae96d + checksum: 10c0/989d5040d097f0a3323b84d6ee1d54fbf3569342396fa9450ef12fa2ac5bcc854ece96f9c2727311b152c6579fbe8eb146970e9586a843b70b85d73defd8bcae languageName: node linkType: hard -"@angular/compiler-cli@npm:^21.2.7": - version: 21.2.7 - resolution: "@angular/compiler-cli@npm:21.2.7" +"@angular/compiler-cli@npm:^21.2.14": + version: 21.2.14 + resolution: "@angular/compiler-cli@npm:21.2.14" dependencies: "@babel/core": "npm:7.29.0" "@jridgewell/sourcemap-codec": "npm:^1.4.14" @@ -641,7 +641,7 @@ __metadata: tslib: "npm:^2.3.0" yargs: "npm:^18.0.0" peerDependencies: - "@angular/compiler": 21.2.7 + "@angular/compiler": 21.2.14 typescript: ">=5.9 <6.1" peerDependenciesMeta: typescript: @@ -649,26 +649,26 @@ __metadata: bin: ng-xi18n: bundles/src/bin/ng_xi18n.js ngc: bundles/src/bin/ngc.js - checksum: 10c0/c63ea08186defed54b7ce7f7221a889b7af04cbf31cfaae10616c9f86ca74433ecccf5eec27175a7c1720ed0083ae80fc4e18125e2ff6eb2cbe76ec141690702 + checksum: 10c0/4ee0db5ce0b5692b80e6a4f945494b59532dc1693ef12991538fbf205032c6f2cdfc476c1e948e9f6cd3812eb22289d2a6369ddf31616a4bd491fe957e7bb8a2 languageName: node linkType: hard -"@angular/compiler@npm:^21.2.7": - version: 21.2.7 - resolution: "@angular/compiler@npm:21.2.7" +"@angular/compiler@npm:^21.2.14": + version: 21.2.14 + resolution: "@angular/compiler@npm:21.2.14" dependencies: tslib: "npm:^2.3.0" - checksum: 10c0/8970060026a70164533327ba9b2bbc91fef98779656ac39f4be6f1f1e64db076936dd1de7f1e07b2352ce87b9b5259a9c621c9130e388dc01eaffccba4b95dab + checksum: 10c0/e25b21842df594f017354c2ba41449f6633b2d955d04edb6191818e0a15f60c74502dd625ac859d6184678f67ee746089819263c4440942529925ad3e446500e languageName: node linkType: hard -"@angular/core@npm:^21.2.7": - version: 21.2.7 - resolution: "@angular/core@npm:21.2.7" +"@angular/core@npm:^21.2.14": + version: 21.2.14 + resolution: "@angular/core@npm:21.2.14" dependencies: tslib: "npm:^2.3.0" peerDependencies: - "@angular/compiler": 21.2.7 + "@angular/compiler": 21.2.14 rxjs: ^6.5.3 || ^7.4.0 zone.js: ~0.15.0 || ~0.16.0 peerDependenciesMeta: @@ -676,66 +676,66 @@ __metadata: optional: true zone.js: optional: true - checksum: 10c0/33b2593f4631cab0c3f39e8d7e1a760b466f2253a7c5508e161ad22212c85965e6e898104c4a029f55e10df35439208e119b53cba3c20085c9bc12a28a609d71 + checksum: 10c0/6cc48c5634d2ff513892bbab06f0b470cfbc55231ffa97cf729276823bc428f0696b529b675da554cf269381965692fee572f9f54542ff74aab1dcf0acf09612 languageName: node linkType: hard -"@angular/forms@npm:^21.2.7": - version: 21.2.7 - resolution: "@angular/forms@npm:21.2.7" +"@angular/forms@npm:^21.2.14": + version: 21.2.14 + resolution: "@angular/forms@npm:21.2.14" dependencies: "@standard-schema/spec": "npm:^1.0.0" tslib: "npm:^2.3.0" peerDependencies: - "@angular/common": 21.2.7 - "@angular/core": 21.2.7 - "@angular/platform-browser": 21.2.7 + "@angular/common": 21.2.14 + "@angular/core": 21.2.14 + "@angular/platform-browser": 21.2.14 rxjs: ^6.5.3 || ^7.4.0 - checksum: 10c0/e2ede117d7c0e46c1b0c89f2e3e658bbcc6e6db48f326fa161e34397e1cfce38dfb1a14eec8e3a71ceff3c824360c48681e5dbdc7e4deaf00222a981104aca9f + checksum: 10c0/60d05f602fb8c98b1aac759f4b628d419d78a9d6ef0263549d00e87c4adb3b004f3b0051ea58fa92c67cc94fff73a5935b1c756ae0e68b54007122c31a789eab languageName: node linkType: hard -"@angular/platform-browser-dynamic@npm:^21.2.7": - version: 21.2.7 - resolution: "@angular/platform-browser-dynamic@npm:21.2.7" +"@angular/platform-browser-dynamic@npm:^21.2.14": + version: 21.2.14 + resolution: "@angular/platform-browser-dynamic@npm:21.2.14" dependencies: tslib: "npm:^2.3.0" peerDependencies: - "@angular/common": 21.2.7 - "@angular/compiler": 21.2.7 - "@angular/core": 21.2.7 - "@angular/platform-browser": 21.2.7 - checksum: 10c0/90172dad15019b997f66e058dae1817200eaf419d20ec50c4a6098d9fdc067d9fb0d659270e06c2dfc3e9d5c180d6c0fc9bc64cb614d3c95da88aedbb5b22159 + "@angular/common": 21.2.14 + "@angular/compiler": 21.2.14 + "@angular/core": 21.2.14 + "@angular/platform-browser": 21.2.14 + checksum: 10c0/4db11437355d54034c0e1cc4f274e2bf33ca0579b55bda9107f49ded5aa06b404afa8b485cd6f5352cae65156d5d441f674a7c3fd838ca99a7cd51a9aabb8dd2 languageName: node linkType: hard -"@angular/platform-browser@npm:^21.2.7": - version: 21.2.7 - resolution: "@angular/platform-browser@npm:21.2.7" +"@angular/platform-browser@npm:^21.2.14": + version: 21.2.14 + resolution: "@angular/platform-browser@npm:21.2.14" dependencies: tslib: "npm:^2.3.0" peerDependencies: - "@angular/animations": 21.2.7 - "@angular/common": 21.2.7 - "@angular/core": 21.2.7 + "@angular/animations": 21.2.14 + "@angular/common": 21.2.14 + "@angular/core": 21.2.14 peerDependenciesMeta: "@angular/animations": optional: true - checksum: 10c0/985d4a0a2f659b31f41d58a3025d60d6e8b7ecd039731000dc8d5340537e8ae0411472d3d16926be9e815a81b912c60ff797f13004ad4c5ab812e9afbd124c07 + checksum: 10c0/69c7ac9758cbb06e24b130abeec4674b931b9ff5a7701fe5dbceeed8fc92b921f91efda943891f73bb0e28fcba62c9c73eb6825c08ed41ae9e308833e0532cc2 languageName: node linkType: hard -"@angular/router@npm:^21.2.7": - version: 21.2.7 - resolution: "@angular/router@npm:21.2.7" +"@angular/router@npm:^21.2.14": + version: 21.2.14 + resolution: "@angular/router@npm:21.2.14" dependencies: tslib: "npm:^2.3.0" peerDependencies: - "@angular/common": 21.2.7 - "@angular/core": 21.2.7 - "@angular/platform-browser": 21.2.7 + "@angular/common": 21.2.14 + "@angular/core": 21.2.14 + "@angular/platform-browser": 21.2.14 rxjs: ^6.5.3 || ^7.4.0 - checksum: 10c0/ac451bee1b4f322a7a3c44930bc578c662338e1ce3c38049c55ba4857f8dc9a85eea97659086e66344326e92a994897e7d870c9620e1b89226dcffebc52f4c77 + checksum: 10c0/042288ae2bef9177fe732bbf81d8272cac23bcd6403698af9cb1687763b7625b1db0c6711cac55165e83a967582035806247d54b7ba86931bb063236b2e26488 languageName: node linkType: hard @@ -775,14 +775,14 @@ __metadata: languageName: node linkType: hard -"@axe-core/playwright@npm:^4.11.1": - version: 4.11.2 - resolution: "@axe-core/playwright@npm:4.11.2" +"@axe-core/playwright@npm:^4.11.3": + version: 4.11.3 + resolution: "@axe-core/playwright@npm:4.11.3" dependencies: - axe-core: "npm:~4.11.3" + axe-core: "npm:~4.11.4" peerDependencies: playwright-core: ">= 1.0.0" - checksum: 10c0/cb6f259c969668ae41b1c56e9fbf8d380d4b9440ef76f3037faa615fdcd36a748f53451116bb53b101dae52c6adc9183ce8de7f62819beb3866989849a9834b6 + checksum: 10c0/da1854726dbc461a71ac25e0435f5dd9b7d143dc9142f53b1aeb4a8d7edcb4533eddb59949e8a07c4f4e3dce85ae43b7f249b3801e8b255f605fc974b94616fe languageName: node linkType: hard @@ -1780,9 +1780,9 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:7.29.0": - version: 7.29.0 - resolution: "@babel/preset-env@npm:7.29.0" +"@babel/preset-env@npm:7.29.2": + version: 7.29.2 + resolution: "@babel/preset-env@npm:7.29.2" dependencies: "@babel/compat-data": "npm:^7.29.0" "@babel/helper-compilation-targets": "npm:^7.28.6" @@ -1856,7 +1856,7 @@ __metadata: semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/08737e333a538703ba20e9e93b5bfbc01abbb9d3b2519b5b62ad05d3b6b92d79445b1dac91229b8cfcfb0b681b22b7c6fa88d7c1cc15df1690a23b21287f55b6 + checksum: 10c0/d49cb005f2dbc3f2293ab6d80ee8f1380e6215af5518fe26b087c8961c1ea8ebaa554dfce589abe1fbebac25ad7c2515d943dec3859ea2d4981a3f8f4711c580 languageName: node linkType: hard @@ -1873,10 +1873,10 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:7.28.6, @babel/runtime@npm:^7.5.5": - version: 7.28.6 - resolution: "@babel/runtime@npm:7.28.6" - checksum: 10c0/358cf2429992ac1c466df1a21c1601d595c46930a13c1d4662fde908d44ee78ec3c183aaff513ecb01ef8c55c3624afe0309eeeb34715672dbfadb7feedb2c0d +"@babel/runtime@npm:7.29.2, @babel/runtime@npm:^7.5.5": + version: 7.29.2 + resolution: "@babel/runtime@npm:7.29.2" + checksum: 10c0/30b80a0140d16467792e1bbeb06f655b0dab70407da38dfac7fedae9c859f9ae9d846ef14ad77bd3814c064295fe9b1bc551f1541ea14646ae9f22b71a8bc17a languageName: node linkType: hard @@ -1978,11 +1978,11 @@ __metadata: languageName: node linkType: hard -"@changesets/apply-release-plan@npm:^7.1.0": - version: 7.1.0 - resolution: "@changesets/apply-release-plan@npm:7.1.0" +"@changesets/apply-release-plan@npm:^7.1.1": + version: 7.1.1 + resolution: "@changesets/apply-release-plan@npm:7.1.1" dependencies: - "@changesets/config": "npm:^3.1.3" + "@changesets/config": "npm:^3.1.4" "@changesets/get-version-range-type": "npm:^0.4.0" "@changesets/git": "npm:^3.0.4" "@changesets/should-skip-package": "npm:^0.1.2" @@ -1995,21 +1995,21 @@ __metadata: prettier: "npm:^2.7.1" resolve-from: "npm:^5.0.0" semver: "npm:^7.5.3" - checksum: 10c0/c8b4fa55f204a0c343c450ca44ae32a892752eaa81b594fb8941e9d1eb8675aba6245c8d80e5e9726e915d2643c542d22cba40d430c76a71ff438ad368d91f5c + checksum: 10c0/27de184e74e8e48b43fca1f73e7c7a2887b0cdacfe7ba9c09cdc4547dff0de1587bed5fe2d5ec0a3754fa422f6b8a528e0ac452c22ac7a6ae5211f5ac089bfb2 languageName: node linkType: hard -"@changesets/assemble-release-plan@npm:^6.0.9": - version: 6.0.9 - resolution: "@changesets/assemble-release-plan@npm:6.0.9" +"@changesets/assemble-release-plan@npm:^6.0.10": + version: 6.0.10 + resolution: "@changesets/assemble-release-plan@npm:6.0.10" dependencies: "@changesets/errors": "npm:^0.2.0" - "@changesets/get-dependents-graph": "npm:^2.1.3" + "@changesets/get-dependents-graph": "npm:^2.1.4" "@changesets/should-skip-package": "npm:^0.1.2" "@changesets/types": "npm:^6.1.0" "@manypkg/get-packages": "npm:^1.1.3" semver: "npm:^7.5.3" - checksum: 10c0/128f87975f65d9ceb2c997df186a5deae8637fd3868098bb4fb9772f35fdd3b47883ccbdc2761d0468e60a83ef4e2c1561a8e58f8052bfe2daf1ea046803fe1a + checksum: 10c0/a0ea336a5f19f8d0a97b684983bcd9c3bb8d6881b7b6abd5b482b301795ae4600924c188982f5f98dc48ac88e94a063b66ab72659041eb2623ade3e35f05d555 languageName: node linkType: hard @@ -2022,17 +2022,17 @@ __metadata: languageName: node linkType: hard -"@changesets/cli@npm:^2.30.0": - version: 2.30.0 - resolution: "@changesets/cli@npm:2.30.0" +"@changesets/cli@npm:^2.31.0": + version: 2.31.0 + resolution: "@changesets/cli@npm:2.31.0" dependencies: - "@changesets/apply-release-plan": "npm:^7.1.0" - "@changesets/assemble-release-plan": "npm:^6.0.9" + "@changesets/apply-release-plan": "npm:^7.1.1" + "@changesets/assemble-release-plan": "npm:^6.0.10" "@changesets/changelog-git": "npm:^0.2.1" - "@changesets/config": "npm:^3.1.3" + "@changesets/config": "npm:^3.1.4" "@changesets/errors": "npm:^0.2.0" - "@changesets/get-dependents-graph": "npm:^2.1.3" - "@changesets/get-release-plan": "npm:^4.0.15" + "@changesets/get-dependents-graph": "npm:^2.1.4" + "@changesets/get-release-plan": "npm:^4.0.16" "@changesets/git": "npm:^3.0.4" "@changesets/logger": "npm:^0.1.1" "@changesets/pre": "npm:^2.0.2" @@ -2054,23 +2054,23 @@ __metadata: term-size: "npm:^2.1.0" bin: changeset: bin.js - checksum: 10c0/2b06343ae6df20b627ee89027f4078c074bdd758f82bb5dbf16ef7c4900138f733b59ceeb1c960fca1e9e59cf6973bb4d5984e4c7dd6d50a3949b39c490f31e0 + checksum: 10c0/3b15f4f5fc7ccaa0b82ca4f9803977ed141b6bed66f83cf8004c2f4ab8e3a00c3a813569b76e4c757d0a8ca5e778bcb6df6e4df91be6c98e0dfaa2cff87c9434 languageName: node linkType: hard -"@changesets/config@npm:^3.1.3": - version: 3.1.3 - resolution: "@changesets/config@npm:3.1.3" +"@changesets/config@npm:^3.1.4": + version: 3.1.4 + resolution: "@changesets/config@npm:3.1.4" dependencies: "@changesets/errors": "npm:^0.2.0" - "@changesets/get-dependents-graph": "npm:^2.1.3" + "@changesets/get-dependents-graph": "npm:^2.1.4" "@changesets/logger": "npm:^0.1.1" "@changesets/should-skip-package": "npm:^0.1.2" "@changesets/types": "npm:^6.1.0" "@manypkg/get-packages": "npm:^1.1.3" fs-extra: "npm:^7.0.1" micromatch: "npm:^4.0.8" - checksum: 10c0/68764135cbd014aca24b20429ffaf6f90e440286c7d233c33ddc968f0ab54b9e6e5dd5015a619dd0e0dd2eb172f028064a229fa610c260b779ff5315a840be1e + checksum: 10c0/1c0e7975aa719e2c87dfda3f5a1eb81b9f4852cdfb5b5c9d181fa2f8f485e92370b3bdfdb6f666432207dd75a3f79fa8fffe7847d48fb11308acb5ecf327bc12 languageName: node linkType: hard @@ -2083,15 +2083,15 @@ __metadata: languageName: node linkType: hard -"@changesets/get-dependents-graph@npm:^2.1.3": - version: 2.1.3 - resolution: "@changesets/get-dependents-graph@npm:2.1.3" +"@changesets/get-dependents-graph@npm:^2.1.4": + version: 2.1.4 + resolution: "@changesets/get-dependents-graph@npm:2.1.4" dependencies: "@changesets/types": "npm:^6.1.0" "@manypkg/get-packages": "npm:^1.1.3" picocolors: "npm:^1.1.0" semver: "npm:^7.5.3" - checksum: 10c0/b9d9992440b7e09dcaf22f57d28f1d8e0e31996e1bc44dbbfa1801e44f93fa49ebba6f9356c60f6ff0bd85cd0f0d0b8602f7e0f2addc5be647b686e6f8985f70 + checksum: 10c0/37b12ba42f16c458d0b574bcafa0247ff2b9a218686a64c86fc75bccc9ba3982f9c27206542941cf3a0563d9b199f40a830682b45e9fd902536de91344cbd0a2 languageName: node linkType: hard @@ -2105,17 +2105,17 @@ __metadata: languageName: node linkType: hard -"@changesets/get-release-plan@npm:^4.0.15": - version: 4.0.15 - resolution: "@changesets/get-release-plan@npm:4.0.15" +"@changesets/get-release-plan@npm:^4.0.16": + version: 4.0.16 + resolution: "@changesets/get-release-plan@npm:4.0.16" dependencies: - "@changesets/assemble-release-plan": "npm:^6.0.9" - "@changesets/config": "npm:^3.1.3" + "@changesets/assemble-release-plan": "npm:^6.0.10" + "@changesets/config": "npm:^3.1.4" "@changesets/pre": "npm:^2.0.2" "@changesets/read": "npm:^0.6.7" "@changesets/types": "npm:^6.1.0" "@manypkg/get-packages": "npm:^1.1.3" - checksum: 10c0/d059c18ef5aab1c1aa1dd4f68d74e2fc351d965e28a76ab7f7c63c3290787d645f887d89c50b92f9f6bb63148a5d17329cfbb9cdea8e02c669a47768ec3456bc + checksum: 10c0/4be4553e13fe331f6d5b2ed98fece21c8d2b38c04a0543f726a0398b7538ef8fd073d712c35ae4540ed4fc6f84f08de6335318bc09dd562b189fb6968d049e95 languageName: node linkType: hard @@ -2221,10 +2221,10 @@ __metadata: languageName: node linkType: hard -"@colordx/core@npm:^5.0.0": - version: 5.0.3 - resolution: "@colordx/core@npm:5.0.3" - checksum: 10c0/b938d1384acfaa31e6101a54192bb7e05703b940fc24139e38761df6e5498c63bb0bf252fa09afb9a147413c162a75097c685327199c75f1531b535ffb0f8cb6 +"@colordx/core@npm:^5.4.3": + version: 5.4.3 + resolution: "@colordx/core@npm:5.4.3" + checksum: 10c0/9fa1888b8794d6e80c9fb346bf18dc6de0a79834099559baab4854ed09c5684f1ec26f1d745c2702774dbb47ce936eeb0645da0f64cce43b45df68f7e8fa9ff2 languageName: node linkType: hard @@ -2330,6 +2330,16 @@ __metadata: languageName: node linkType: hard +"@emnapi/core@npm:1.10.0": + version: 1.10.0 + resolution: "@emnapi/core@npm:1.10.0" + dependencies: + "@emnapi/wasi-threads": "npm:1.2.1" + tslib: "npm:^2.4.0" + checksum: 10c0/f51d08227857b60632de7714d708124f0e100a1462dde6df8221760939aa3204a73193830371830fac0716f3ccd2129f2cac1b17cd7d7958bc4da9018a296edb + languageName: node + linkType: hard + "@emnapi/core@npm:1.9.2": version: 1.9.2 resolution: "@emnapi/core@npm:1.9.2" @@ -2340,6 +2350,15 @@ __metadata: languageName: node linkType: hard +"@emnapi/runtime@npm:1.10.0": + version: 1.10.0 + resolution: "@emnapi/runtime@npm:1.10.0" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/953f14991d1aefb92ee6f8eb27dea725e484791a53a0cb5f47d9e0087b9a2c929ff2e92adf95af15d6ad456db6300c6b761ebf72b50a875b874a83520b3ba093 + languageName: node + linkType: hard + "@emnapi/runtime@npm:1.9.2": version: 1.9.2 resolution: "@emnapi/runtime@npm:1.9.2" @@ -2815,10 +2834,10 @@ __metadata: languageName: node linkType: hard -"@faker-js/faker@npm:^10.3.0": - version: 10.3.0 - resolution: "@faker-js/faker@npm:10.3.0" - checksum: 10c0/5a4688f8a040366bda83e6c7f144f9db9bef1d44f7c5b43fcee15c8def07463fcdcb572458b0d14072aca58c14321f1195e11d8664ce094266e29eb4d2a70ac4 +"@faker-js/faker@npm:^10.4.0": + version: 10.4.0 + resolution: "@faker-js/faker@npm:10.4.0" + checksum: 10c0/29f15e46f91757d654f6f42ac8313ac4aebb6591e2ebae7fdd8d36d5017490327ea67be37cff06d18303db54abf468524bcc7e4b1de53be4eb884c1f71d571c1 languageName: node linkType: hard @@ -3547,9 +3566,9 @@ __metadata: languageName: node linkType: hard -"@lit/localize-tools@npm:^0.8.1": - version: 0.8.1 - resolution: "@lit/localize-tools@npm:0.8.1" +"@lit/localize-tools@npm:^0.8.2": + version: 0.8.2 + resolution: "@lit/localize-tools@npm:0.8.2" dependencies: "@lit/localize": "npm:^0.12.0" "@parse5/tools": "npm:^0.3.0" @@ -3564,7 +3583,7 @@ __metadata: typescript: "npm:~5.9.0" bin: lit-localize: bin/lit-localize.js - checksum: 10c0/ad5acd7818f65fb53de74f17e47818526ffd11aee66df9c5964157d6fe71c0395d3e1200d6b73485bd46d6e760c78e8343995685bc086bbf6b1745ea4135ea7d + checksum: 10c0/265939fcc318b8ec2687f1477ecdff92eed3a74c7a53a6a985785e7df2296d75268ffa6b2b20c237b2a7e807ab9bcfa75308af2f4822402b73f295e10cbdc1b3 languageName: node linkType: hard @@ -3936,26 +3955,26 @@ __metadata: languageName: node linkType: hard -"@napi-rs/wasm-runtime@npm:^1.1.1, @napi-rs/wasm-runtime@npm:^1.1.3": - version: 1.1.3 - resolution: "@napi-rs/wasm-runtime@npm:1.1.3" +"@napi-rs/wasm-runtime@npm:^1.1.1, @napi-rs/wasm-runtime@npm:^1.1.4": + version: 1.1.4 + resolution: "@napi-rs/wasm-runtime@npm:1.1.4" dependencies: "@tybys/wasm-util": "npm:^0.10.1" peerDependencies: "@emnapi/core": ^1.7.1 "@emnapi/runtime": ^1.7.1 - checksum: 10c0/745bb32a023b95095a18d93658bf4564403c2283ca0500a043afcf566ac6082bd0611792f14636276bab07dc2ce6d862591c8aabddae02ec697245b05bc6f144 + checksum: 10c0/2e88e1955258949ccf2d18c79975821ad38071b465ef126a5e14110977b97868867b016c1ad046e963cccc42c0bd9db6c8ff5fd1ebb61b87bb3487f339041658 languageName: node linkType: hard -"@ngtools/webpack@npm:21.2.6": - version: 21.2.6 - resolution: "@ngtools/webpack@npm:21.2.6" +"@ngtools/webpack@npm:21.2.12": + version: 21.2.12 + resolution: "@ngtools/webpack@npm:21.2.12" peerDependencies: "@angular/compiler-cli": ^21.0.0 typescript: ">=5.9 <6.0" webpack: ^5.54.0 - checksum: 10c0/0161945c2823962672ba3502f5a90aa329f102923e1d0200b627f0f2ac5370d94f81d4348534fe8431043c4f1bf38168b982faded3e086fe4214ef5ea45e50d1 + checksum: 10c0/aeb8906a75be9be6ef03b91919f62dabe81ede3ef8feee4da14f559e3b8ceeb413096a5f71f2a463a9c6e396ee22ccdbb8db2653eb2b9e83d8faf8b0be3315b8 languageName: node linkType: hard @@ -4121,6 +4140,150 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-android-arm-eabi@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-android-arm-eabi@npm:0.127.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@oxc-parser/binding-android-arm64@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-android-arm64@npm:0.127.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@oxc-parser/binding-darwin-arm64@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-darwin-arm64@npm:0.127.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@oxc-parser/binding-darwin-x64@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-darwin-x64@npm:0.127.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@oxc-parser/binding-freebsd-x64@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-freebsd-x64@npm:0.127.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@oxc-parser/binding-linux-arm-gnueabihf@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-linux-arm-gnueabihf@npm:0.127.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@oxc-parser/binding-linux-arm-musleabihf@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-linux-arm-musleabihf@npm:0.127.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@oxc-parser/binding-linux-arm64-gnu@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-linux-arm64-gnu@npm:0.127.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@oxc-parser/binding-linux-arm64-musl@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-linux-arm64-musl@npm:0.127.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@oxc-parser/binding-linux-ppc64-gnu@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-linux-ppc64-gnu@npm:0.127.0" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@oxc-parser/binding-linux-riscv64-gnu@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-linux-riscv64-gnu@npm:0.127.0" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@oxc-parser/binding-linux-riscv64-musl@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-linux-riscv64-musl@npm:0.127.0" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + +"@oxc-parser/binding-linux-s390x-gnu@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-linux-s390x-gnu@npm:0.127.0" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@oxc-parser/binding-linux-x64-gnu@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-linux-x64-gnu@npm:0.127.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@oxc-parser/binding-linux-x64-musl@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-linux-x64-musl@npm:0.127.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@oxc-parser/binding-openharmony-arm64@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-openharmony-arm64@npm:0.127.0" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@oxc-parser/binding-wasm32-wasi@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-wasm32-wasi@npm:0.127.0" + dependencies: + "@emnapi/core": "npm:1.9.2" + "@emnapi/runtime": "npm:1.9.2" + "@napi-rs/wasm-runtime": "npm:^1.1.4" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@oxc-parser/binding-win32-arm64-msvc@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-win32-arm64-msvc@npm:0.127.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@oxc-parser/binding-win32-ia32-msvc@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-win32-ia32-msvc@npm:0.127.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@oxc-parser/binding-win32-x64-msvc@npm:0.127.0": + version: 0.127.0 + resolution: "@oxc-parser/binding-win32-x64-msvc@npm:0.127.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@oxc-project/types@npm:=0.113.0": version: 0.113.0 resolution: "@oxc-project/types@npm:0.113.0" @@ -4128,10 +4291,17 @@ __metadata: languageName: node linkType: hard -"@oxc-project/types@npm:=0.124.0": - version: 0.124.0 - resolution: "@oxc-project/types@npm:0.124.0" - checksum: 10c0/9564ee3ce41f4b87802ffd0d62a7602d27f4503fbd39c1bedab98d54fde06e2ac254a8f85d8f679af1281a26e8fc7aa053fadbb3e09e786b38178eb38a8e2fb3 +"@oxc-project/types@npm:=0.132.0": + version: 0.132.0 + resolution: "@oxc-project/types@npm:0.132.0" + checksum: 10c0/d0ca5e98be0b873d69e4f0f743eb35026833603dac11db9d55f2b5438251b381b886dc556fe3175a17b673f8e2073c49bde88d7e6e702aa09298c22b8b5504e1 + languageName: node + linkType: hard + +"@oxc-project/types@npm:^0.127.0": + version: 0.127.0 + resolution: "@oxc-project/types@npm:0.127.0" + checksum: 10c0/52c0947ac64a9ca119fe971f947e784a35ecd14a072fa3f542a58a5f6c42010b53f2bf92731e39b9899b83c990a9517bbd29d1e5a5b7b489e52616685c6a9278 languageName: node linkType: hard @@ -4277,135 +4447,135 @@ __metadata: languageName: node linkType: hard -"@oxfmt/binding-android-arm-eabi@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-android-arm-eabi@npm:0.46.0" +"@oxfmt/binding-android-arm-eabi@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-android-arm-eabi@npm:0.52.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@oxfmt/binding-android-arm64@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-android-arm64@npm:0.46.0" +"@oxfmt/binding-android-arm64@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-android-arm64@npm:0.52.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@oxfmt/binding-darwin-arm64@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-darwin-arm64@npm:0.46.0" +"@oxfmt/binding-darwin-arm64@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-darwin-arm64@npm:0.52.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxfmt/binding-darwin-x64@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-darwin-x64@npm:0.46.0" +"@oxfmt/binding-darwin-x64@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-darwin-x64@npm:0.52.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxfmt/binding-freebsd-x64@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-freebsd-x64@npm:0.46.0" +"@oxfmt/binding-freebsd-x64@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-freebsd-x64@npm:0.52.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@oxfmt/binding-linux-arm-gnueabihf@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-linux-arm-gnueabihf@npm:0.46.0" +"@oxfmt/binding-linux-arm-gnueabihf@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-linux-arm-gnueabihf@npm:0.52.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxfmt/binding-linux-arm-musleabihf@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-linux-arm-musleabihf@npm:0.46.0" +"@oxfmt/binding-linux-arm-musleabihf@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-linux-arm-musleabihf@npm:0.52.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxfmt/binding-linux-arm64-gnu@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-linux-arm64-gnu@npm:0.46.0" +"@oxfmt/binding-linux-arm64-gnu@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-linux-arm64-gnu@npm:0.52.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@oxfmt/binding-linux-arm64-musl@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-linux-arm64-musl@npm:0.46.0" +"@oxfmt/binding-linux-arm64-musl@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-linux-arm64-musl@npm:0.52.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@oxfmt/binding-linux-ppc64-gnu@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-linux-ppc64-gnu@npm:0.46.0" +"@oxfmt/binding-linux-ppc64-gnu@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-linux-ppc64-gnu@npm:0.52.0" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@oxfmt/binding-linux-riscv64-gnu@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-linux-riscv64-gnu@npm:0.46.0" +"@oxfmt/binding-linux-riscv64-gnu@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-linux-riscv64-gnu@npm:0.52.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@oxfmt/binding-linux-riscv64-musl@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-linux-riscv64-musl@npm:0.46.0" +"@oxfmt/binding-linux-riscv64-musl@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-linux-riscv64-musl@npm:0.52.0" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@oxfmt/binding-linux-s390x-gnu@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-linux-s390x-gnu@npm:0.46.0" +"@oxfmt/binding-linux-s390x-gnu@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-linux-s390x-gnu@npm:0.52.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@oxfmt/binding-linux-x64-gnu@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-linux-x64-gnu@npm:0.46.0" +"@oxfmt/binding-linux-x64-gnu@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-linux-x64-gnu@npm:0.52.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@oxfmt/binding-linux-x64-musl@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-linux-x64-musl@npm:0.46.0" +"@oxfmt/binding-linux-x64-musl@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-linux-x64-musl@npm:0.52.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@oxfmt/binding-openharmony-arm64@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-openharmony-arm64@npm:0.46.0" +"@oxfmt/binding-openharmony-arm64@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-openharmony-arm64@npm:0.52.0" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@oxfmt/binding-win32-arm64-msvc@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-win32-arm64-msvc@npm:0.46.0" +"@oxfmt/binding-win32-arm64-msvc@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-win32-arm64-msvc@npm:0.52.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@oxfmt/binding-win32-ia32-msvc@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-win32-ia32-msvc@npm:0.46.0" +"@oxfmt/binding-win32-ia32-msvc@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-win32-ia32-msvc@npm:0.52.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@oxfmt/binding-win32-x64-msvc@npm:0.46.0": - version: 0.46.0 - resolution: "@oxfmt/binding-win32-x64-msvc@npm:0.46.0" +"@oxfmt/binding-win32-x64-msvc@npm:0.52.0": + version: 0.52.0 + resolution: "@oxfmt/binding-win32-x64-msvc@npm:0.52.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -4715,14 +4885,14 @@ __metadata: languageName: node linkType: hard -"@playwright/test@npm:^1.58.2": - version: 1.59.1 - resolution: "@playwright/test@npm:1.59.1" +"@playwright/test@npm:^1.60.0": + version: 1.60.0 + resolution: "@playwright/test@npm:1.60.0" dependencies: - playwright: "npm:1.59.1" + playwright: "npm:1.60.0" bin: playwright: cli.js - checksum: 10c0/8c2d94a860d3c254a0b114df2f888ad0a0e9310f45b6059bd5d4da196d965cadf6922267cef0881cfa9784d4bef6d78363d2c2d94caa64be67ff644c41162137 + checksum: 10c0/86b06e6437933e741c7cd43f362024e857e7bc28a55fcbb0553ef55e01a2a403c64f4786868de8af86a6e303fe99e98a18a42ba19489f43ae122e457f9e2d189 languageName: node linkType: hard @@ -4733,13 +4903,6 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-android-arm64@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.15" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@rolldown/binding-android-arm64@npm:1.0.0-rc.4": version: 1.0.0-rc.4 resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.4" @@ -4747,10 +4910,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.15" - conditions: os=darwin & cpu=arm64 +"@rolldown/binding-android-arm64@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-android-arm64@npm:1.0.2" + conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -4761,10 +4924,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-darwin-x64@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.15" - conditions: os=darwin & cpu=x64 +"@rolldown/binding-darwin-arm64@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-darwin-arm64@npm:1.0.2" + conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -4775,10 +4938,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.15" - conditions: os=freebsd & cpu=x64 +"@rolldown/binding-darwin-x64@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-darwin-x64@npm:1.0.2" + conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -4789,10 +4952,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.15" - conditions: os=linux & cpu=arm +"@rolldown/binding-freebsd-x64@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-freebsd-x64@npm:1.0.2" + conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -4803,10 +4966,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.15" - conditions: os=linux & cpu=arm64 & libc=glibc +"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.2" + conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -4817,10 +4980,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.15" - conditions: os=linux & cpu=arm64 & libc=musl +"@rolldown/binding-linux-arm64-gnu@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.2" + conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard @@ -4831,24 +4994,24 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.15" - conditions: os=linux & cpu=ppc64 & libc=glibc +"@rolldown/binding-linux-arm64-musl@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.2" + conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.15" - conditions: os=linux & cpu=s390x & libc=glibc +"@rolldown/binding-linux-ppc64-gnu@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.2" + conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.15" - conditions: os=linux & cpu=x64 & libc=glibc +"@rolldown/binding-linux-s390x-gnu@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.2" + conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard @@ -4859,10 +5022,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.15" - conditions: os=linux & cpu=x64 & libc=musl +"@rolldown/binding-linux-x64-gnu@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.2" + conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard @@ -4873,10 +5036,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.15" - conditions: os=openharmony & cpu=arm64 +"@rolldown/binding-linux-x64-musl@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.2" + conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard @@ -4887,14 +5050,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.15" - dependencies: - "@emnapi/core": "npm:1.9.2" - "@emnapi/runtime": "npm:1.9.2" - "@napi-rs/wasm-runtime": "npm:^1.1.3" - conditions: cpu=wasm32 +"@rolldown/binding-openharmony-arm64@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.2" + conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard @@ -4907,10 +5066,14 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.15" - conditions: os=win32 & cpu=arm64 +"@rolldown/binding-wasm32-wasi@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.2" + dependencies: + "@emnapi/core": "npm:1.10.0" + "@emnapi/runtime": "npm:1.10.0" + "@napi-rs/wasm-runtime": "npm:^1.1.4" + conditions: cpu=wasm32 languageName: node linkType: hard @@ -4921,10 +5084,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.15" - conditions: os=win32 & cpu=x64 +"@rolldown/binding-win32-arm64-msvc@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.2" + conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -4935,10 +5098,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/pluginutils@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "@rolldown/pluginutils@npm:1.0.0-rc.15" - checksum: 10c0/15eef6a65ee6b2d07405c16999c2333c40d8aeea60bbc35e04957992fe6477c7b278d3f02679688bb928ad2ef3fbd3a6149c116d7dc9928ebf8d1434a0591674 +"@rolldown/binding-win32-x64-msvc@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.2" + conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -4949,6 +5112,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/pluginutils@npm:^1.0.0": + version: 1.0.1 + resolution: "@rolldown/pluginutils@npm:1.0.1" + checksum: 10c0/99d9b06d90196823e4d8c841f258db7a16e5dbba5824a2962b05d907b79f1ba929d56f22dd744fd530936e568c865ee56a719dc31e57e13bc0a8eb4764a8d8dd + languageName: node + linkType: hard + "@rollup/plugin-json@npm:^6.1.0": version: 6.1.0 resolution: "@rollup/plugin-json@npm:6.1.0" @@ -5174,14 +5344,14 @@ __metadata: languageName: node linkType: hard -"@schematics/angular@npm:21.2.6": - version: 21.2.6 - resolution: "@schematics/angular@npm:21.2.6" +"@schematics/angular@npm:21.2.12": + version: 21.2.12 + resolution: "@schematics/angular@npm:21.2.12" dependencies: - "@angular-devkit/core": "npm:21.2.6" - "@angular-devkit/schematics": "npm:21.2.6" + "@angular-devkit/core": "npm:21.2.12" + "@angular-devkit/schematics": "npm:21.2.12" jsonc-parser: "npm:3.3.1" - checksum: 10c0/1f458793db0a5fe943950b80c3f646140f8279d69597a01203e2ec0b2a54767acbf224a7e79f0be4ace54f12b379a5c8481bc37d5433a0c244d420c134ea345a + checksum: 10c0/23177db584feec204ccbd72a7fb71d8272eaa5074c530fb1fed5b78acc9882c9569f68ff1a4906f8d972a236efd2f4a82222cf0791476e522cbf0ab4931d395f languageName: node linkType: hard @@ -5272,22 +5442,22 @@ __metadata: languageName: node linkType: hard -"@sinonjs/fake-timers@npm:^15.1.1": - version: 15.1.1 - resolution: "@sinonjs/fake-timers@npm:15.1.1" +"@sinonjs/fake-timers@npm:^15.3.2": + version: 15.4.0 + resolution: "@sinonjs/fake-timers@npm:15.4.0" dependencies: "@sinonjs/commons": "npm:^3.0.1" - checksum: 10c0/8eaaa1c9db91256dfe31f3503cdd844ea031ffd16276b3bcd95457432d666d6d027453af5f884e010dba4ebe264b50ac0aac049e192c5f370158da9b291206b9 + checksum: 10c0/de4522afe0699fa8d3ae9d1715cbaa4b47e518c707bb7988a9ec6c7c67557d9f6df451f6be0338598b984a86f65aab9fab38dd9ce75a3c0ffb801a9500d5b10d languageName: node linkType: hard -"@sinonjs/samsam@npm:^9.0.3": - version: 9.0.3 - resolution: "@sinonjs/samsam@npm:9.0.3" +"@sinonjs/samsam@npm:^10.0.2": + version: 10.0.2 + resolution: "@sinonjs/samsam@npm:10.0.2" dependencies: "@sinonjs/commons": "npm:^3.0.1" type-detect: "npm:^4.1.0" - checksum: 10c0/d8e82ee3b09aa7cef607f689d435c7146a0466c6b38fb7973bdf9a0dec84cb5cd0040475c8ace746ecbfd8b0e907756065485064fe0f8ed512e870dedc81b796 + checksum: 10c0/b590af28a093d30db9b84f32793b18388817c6555df2533666c43e257e8a7becf43d864489d87af1b6e18b3163cc8f92a5bb2b3bec62406e6661a21fea43275b languageName: node linkType: hard @@ -5305,19 +5475,19 @@ __metadata: version: 0.0.0-use.local resolution: "@sl-design-system/angular@workspace:packages/angular" dependencies: - "@angular-devkit/architect": "npm:^0.2102.6" - "@angular-devkit/build-angular": "npm:^21.2.6" - "@angular-devkit/core": "npm:^21.2.6" - "@angular/animations": "npm:^21.2.7" - "@angular/cli": "npm:^21.2.6" - "@angular/common": "npm:^21.2.7" - "@angular/compiler": "npm:^21.2.7" - "@angular/compiler-cli": "npm:^21.2.7" - "@angular/core": "npm:^21.2.7" - "@angular/forms": "npm:^21.2.7" - "@angular/platform-browser": "npm:^21.2.7" - "@angular/platform-browser-dynamic": "npm:^21.2.7" - "@angular/router": "npm:^21.2.7" + "@angular-devkit/architect": "npm:^0.2102.12" + "@angular-devkit/build-angular": "npm:^21.2.12" + "@angular-devkit/core": "npm:^21.2.12" + "@angular/animations": "npm:^21.2.14" + "@angular/cli": "npm:^21.2.12" + "@angular/common": "npm:^21.2.14" + "@angular/compiler": "npm:^21.2.14" + "@angular/compiler-cli": "npm:^21.2.14" + "@angular/core": "npm:^21.2.14" + "@angular/forms": "npm:^21.2.14" + "@angular/platform-browser": "npm:^21.2.14" + "@angular/platform-browser-dynamic": "npm:^21.2.14" + "@angular/router": "npm:^21.2.14" "@sl-design-system/checkbox": "npm:^2.1.9" "@sl-design-system/combobox": "npm:^0.1.8" "@sl-design-system/form": "npm:^1.4.0" @@ -5331,23 +5501,23 @@ __metadata: "@sl-design-system/text-field": "npm:^1.6.8" "@sl-design-system/time-field": "npm:^0.1.0" "@standard-schema/spec": "npm:^1.1.0" - "@storybook/addon-docs": "npm:^10.3.5" - "@storybook/angular": "npm:^10.3.5" + "@storybook/addon-docs": "npm:^10.4.1" + "@storybook/angular": "npm:^10.4.1" "@types/jasmine": "npm:~6.0.0" - baseline-browser-mapping: "npm:^2.10.9" - jasmine-core: "npm:~6.1.0" + baseline-browser-mapping: "npm:^2.10.32" + jasmine-core: "npm:~6.2.0" karma: "npm:~6.4.4" karma-chrome-launcher: "npm:~3.2.0" karma-coverage: "npm:~2.2.1" karma-jasmine: "npm:~5.1.0" karma-jasmine-html-reporter: "npm:~2.2.0" - ng-packagr: "npm:^21.2.2" + ng-packagr: "npm:^21.2.3" rxjs: "npm:~7.8.2" - storybook: "npm:^10.3.5" + storybook: "npm:^10.4.1" tslib: "npm:^2.8.1" - typescript: "npm:~6.0.2" + typescript: "npm:~6.0.3" wireit: "npm:^0.14.12" - zone.js: "npm:~0.16.1" + zone.js: "npm:~0.16.2" languageName: unknown linkType: soft @@ -5358,7 +5528,7 @@ __metadata: "@lit/localize": "npm:^0.12.2" "@open-wc/scoped-elements": "npm:^3.0.6" "@sl-design-system/shared": "npm:^0.12.0" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@lit/localize": ^0.12.1 "@open-wc/scoped-elements": ^3.0.6 @@ -5385,7 +5555,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sl-design-system/badge@workspace:packages/components/badge" dependencies: - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: lit: ^3.1.4 languageName: unknown @@ -5428,7 +5598,7 @@ __metadata: resolution: "@sl-design-system/button-bar@workspace:packages/components/button-bar" dependencies: "@sl-design-system/button": "npm:^2.0.0" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: lit: ^3.1.4 languageName: unknown @@ -5441,7 +5611,7 @@ __metadata: "@open-wc/scoped-elements": "npm:^3.0.6" "@sl-design-system/shared": "npm:^0.12.0" "@sl-design-system/tooltip": "npm:^2.0.0" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@open-wc/scoped-elements": ^3.0.6 lit: ^3.1.4 @@ -5458,7 +5628,7 @@ __metadata: "@sl-design-system/format-date": "npm:^0.1.6" "@sl-design-system/icon": "npm:^1.4.2" "@sl-design-system/tooltip": "npm:^2.0.0" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@lit/localize": ^0.12.1 "@open-wc/scoped-elements": ^3.0.6 @@ -5484,7 +5654,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sl-design-system/card@workspace:packages/components/card" dependencies: - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: lit: ^3.1.4 languageName: unknown @@ -5506,15 +5676,15 @@ __metadata: version: 0.0.0-use.local resolution: "@sl-design-system/chromatic@workspace:chromatic" dependencies: - "@storybook/addon-a11y": "npm:^10.3.5" - "@storybook/addon-docs": "npm:^10.3.5" - "@storybook/addon-themes": "npm:^10.3.5" - "@storybook/web-components-vite": "npm:^10.3.5" - lit: "npm:^3.3.2" - storybook: "npm:^10.3.5" - storybook-addon-pseudo-states: "npm:^10.3.5" + "@storybook/addon-a11y": "npm:^10.4.1" + "@storybook/addon-docs": "npm:^10.4.1" + "@storybook/addon-themes": "npm:^10.4.1" + "@storybook/web-components-vite": "npm:^10.4.1" + lit: "npm:^3.3.3" + storybook: "npm:^10.4.1" + storybook-addon-pseudo-states: "npm:^10.4.1" tslib: "npm:^2.8.1" - typescript: "npm:^6.0.2" + typescript: "npm:^6.0.3" wireit: "npm:^0.14.12" languageName: unknown linkType: soft @@ -5537,7 +5707,7 @@ __metadata: "@sl-design-system/listbox": "npm:^0.1.4" "@sl-design-system/tag": "npm:^0.1.12" "@sl-design-system/text-field": "npm:^1.6.8" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@open-wc/scoped-elements": ^3.0.6 lit: ^3.1.4 @@ -5564,7 +5734,7 @@ __metadata: "@sl-design-system/icon": "npm:^1.4.2" "@sl-design-system/shared": "npm:^0.12.0" "@sl-design-system/text-field": "npm:^1.6.9" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@open-wc/scoped-elements": ^3.0.6 lit: ^3.1.4 @@ -5581,7 +5751,7 @@ __metadata: "@sl-design-system/button-bar": "npm:^1.5.0" "@sl-design-system/icon": "npm:^1.4.2" "@sl-design-system/shared": "npm:^0.12.0" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@lit/localize": ^0.12.1 "@open-wc/scoped-elements": ^3.0.6 @@ -5596,7 +5766,7 @@ __metadata: "@open-wc/scoped-elements": "npm:^3.0.6" "@sl-design-system/button": "npm:^2.0.0" "@sl-design-system/button-bar": "npm:^1.5.0" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@open-wc/scoped-elements": ^3.0.6 lit: ^3.1.4 @@ -5681,8 +5851,8 @@ __metadata: eslint-plugin-storybook: "npm:^10.3.3" eslint-plugin-unused-imports: "npm:^4.1.4" eslint-plugin-wc: "npm:^3.0.1" - storybook: "npm:^10.3.5" - typescript: "npm:^6.0.2" + storybook: "npm:^10.4.1" + typescript: "npm:^6.0.3" typescript-eslint: "npm:^8.58.0" peerDependencies: eslint: ^9.17.0 @@ -5700,7 +5870,7 @@ __metadata: dependencies: eslint: "npm:^9.27.0" eslint-plugin-lit-a11y: "npm:^4.1.4" - mocha: "npm:11.7.5" + mocha: "npm:11.7.6" peerDependencies: eslint: ^9.25.1 eslint-plugin-lit-a11y: ^4.1.4 @@ -5722,7 +5892,7 @@ __metadata: "@typescript-eslint/eslint-plugin": "npm:^8.58.0" "@typescript-eslint/parser": "npm:^8.58.0" eslint: "npm:^9.27.0" - typescript: "npm:^6.0.2" + typescript: "npm:^6.0.3" languageName: unknown linkType: soft @@ -5779,7 +5949,7 @@ __metadata: "@sl-design-system/toggle-group": "npm:^0.0.15" "@sl-design-system/tool-bar": "npm:^0.2.4" "@sl-design-system/tooltip": "npm:^2.0.0" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@lit-labs/virtualizer": ^2.0.13 "@lit/localize": ^0.12.1 @@ -5792,7 +5962,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sl-design-system/icon@workspace:packages/components/icon" dependencies: - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: lit: ^3.1.4 languageName: unknown @@ -5807,7 +5977,7 @@ __metadata: "@sl-design-system/button": "npm:^2.0.1" "@sl-design-system/icon": "npm:^1.4.2" "@sl-design-system/popover": "npm:^1.2.7" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@lit/localize": ^0.12.1 "@open-wc/scoped-elements": ^3.0.6 @@ -5854,7 +6024,7 @@ __metadata: "@lit-labs/virtualizer": "npm:^2.1.1" "@open-wc/scoped-elements": "npm:^3.0.6" "@sl-design-system/icon": "npm:^1.4.0" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@lit-labs/virtualizer": ^2.0.13 "@open-wc/scoped-elements": ^3.0.6 @@ -5931,47 +6101,47 @@ __metadata: resolution: "@sl-design-system/monorepo@workspace:." dependencies: "@af-utils/scrollend-polyfill": "npm:^0.0.14" - "@axe-core/playwright": "npm:^4.11.1" - "@changesets/cli": "npm:^2.30.0" + "@axe-core/playwright": "npm:^4.11.3" + "@changesets/cli": "npm:^2.31.0" "@changesets/get-github-info": "npm:^0.8.0" "@custom-elements-manifest/analyzer": "npm:^0.11.0" - "@faker-js/faker": "npm:^10.3.0" - "@lit/localize-tools": "npm:^0.8.1" - "@playwright/test": "npm:^1.58.2" - "@storybook/addon-a11y": "npm:^10.3.5" - "@storybook/addon-docs": "npm:^10.3.5" - "@storybook/addon-vitest": "npm:^10.3.5" - "@storybook/web-components": "npm:^10.3.5" - "@storybook/web-components-vite": "npm:^10.3.5" + "@faker-js/faker": "npm:^10.4.0" + "@lit/localize-tools": "npm:^0.8.2" + "@playwright/test": "npm:^1.60.0" + "@storybook/addon-a11y": "npm:^10.4.1" + "@storybook/addon-docs": "npm:^10.4.1" + "@storybook/addon-vitest": "npm:^10.4.1" + "@storybook/web-components": "npm:^10.4.1" + "@storybook/web-components-vite": "npm:^10.4.1" "@types/chai-datetime": "npm:^1.0.0" "@types/chai-dom": "npm:^1.11.3" - "@types/sinon": "npm:^21.0.0" + "@types/sinon": "npm:^21.0.1" "@types/sinon-chai": "npm:^4.0.0" - "@vitest/browser-playwright": "npm:~4.1.4" - "@vitest/coverage-v8": "npm:~4.1.4" - "@vitest/ui": "npm:~4.1.4" + "@vitest/browser-playwright": "npm:~4.1.7" + "@vitest/coverage-v8": "npm:~4.1.7" + "@vitest/ui": "npm:~4.1.7" "@webcomponents/scoped-custom-element-registry": "npm:^0.0.10" axe-html-reporter: "npm:^2.2.11" chai: "npm:^6.2.2" chai-datetime: "npm:^1.8.1" chai-dom: "npm:^1.12.1" - chromatic: "npm:^15.3.0" + chromatic: "npm:^15.3.1" eslint: "npm:^9.27.0" husky: "npm:^9.1.7" invokers-polyfill: "npm:^1.0.3" - lint-staged: "npm:^16.4.0" - lit: "npm:^3.3.2" + lint-staged: "npm:^17.0.5" + lit: "npm:^3.3.3" mockdate: "npm:^3.0.5" - oxfmt: "npm:^0.46.0" - playwright: "npm:^1.59.1" - sinon: "npm:^21.0.3" + oxfmt: "npm:^0.52.0" + playwright: "npm:^1.60.0" + sinon: "npm:^21.1.2" sinon-chai: "npm:^4.0.1" - storybook: "npm:^10.3.5" + storybook: "npm:^10.4.1" storybook-addon-tag-badges: "npm:^3.1.0" stylelint: "npm:^16.19.1" - typescript: "npm:^6.0.2" - vite: "npm:^8.0.8" - vitest: "npm:~4.1.4" + typescript: "npm:^6.0.3" + vite: "npm:^8.0.14" + vitest: "npm:~4.1.7" wireit: "npm:^0.14.12" languageName: unknown linkType: soft @@ -6062,7 +6232,7 @@ __metadata: "@lit/localize": "npm:^0.12.2" "@open-wc/scoped-elements": "npm:^3.0.6" "@sl-design-system/icon": "npm:^1.4.1" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@lit/localize": ^0.12.1 "@open-wc/scoped-elements": ^3.0.6 @@ -6125,12 +6295,12 @@ __metadata: "@fortawesome/sharp-solid-svg-icons": "npm:^7.2.0" "@tokens-studio/sd-transforms": "npm:^2.0.1" "@tokens-studio/sdk": "npm:^3.0.0" - cssnano: "npm:^7.1.4" + cssnano: "npm:^7.1.9" esbuild: "npm:^0.25.12" fast-glob: "npm:^3.3.3" graphql: "npm:^16.13.1" - postcss: "npm:^8.5.8" - sass: "npm:^1.99.0" + postcss: "npm:^8.5.15" + sass: "npm:^1.100.0" style-dictionary: "npm:^5.1.1" languageName: unknown linkType: soft @@ -6139,7 +6309,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sl-design-system/scrollbar@workspace:packages/components/scrollbar" dependencies: - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: lit: ^3.1.4 languageName: unknown @@ -6167,7 +6337,7 @@ __metadata: "@sl-design-system/icon": "npm:^1.4.2" "@sl-design-system/listbox": "npm:^0.1.6" "@sl-design-system/shared": "npm:^0.12.0" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@lit/localize": ^0.12.1 "@open-wc/scoped-elements": ^3.0.6 @@ -6180,7 +6350,7 @@ __metadata: dependencies: "@floating-ui/dom": "npm:^1.7.6" "@open-wc/dedupe-mixin": "npm:^2.0.1" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@floating-ui/dom": ^1.6.5 "@open-wc/dedupe-mixin": ^2.0.1 @@ -6192,7 +6362,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sl-design-system/skeleton@workspace:packages/components/skeleton" dependencies: - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: lit: ^3.1.4 languageName: unknown @@ -6202,7 +6372,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sl-design-system/spinner@workspace:packages/components/spinner" dependencies: - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: lit: ^3.1.4 languageName: unknown @@ -6212,7 +6382,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sl-design-system/stylelint-config@workspace:tools/stylelint-config" dependencies: - postcss: "npm:^8.5.8" + postcss: "npm:^8.5.15" postcss-lit: "npm:^1.4.1" stylelint: "npm:^16.19.1" stylelint-config-standard: "npm:^38.0.0" @@ -6261,7 +6431,7 @@ __metadata: "@sl-design-system/icon": "npm:^1.4.2" "@sl-design-system/shared": "npm:^0.12.0" "@sl-design-system/tooltip": "npm:^2.0.0" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@lit/localize": ^0.12.1 "@open-wc/scoped-elements": ^3.0.6 @@ -6320,7 +6490,7 @@ __metadata: "@sl-design-system/icon": "npm:^1.4.2" "@sl-design-system/listbox": "npm:^0.1.4" "@sl-design-system/text-field": "npm:^1.6.9" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@open-wc/scoped-elements": ^3.0.6 lit: ^3.1.4 @@ -6335,7 +6505,7 @@ __metadata: "@sl-design-system/icon": "npm:^1.4.2" "@sl-design-system/shared": "npm:^0.12.0" "@sl-design-system/tooltip": "npm:^2.0.0" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@open-wc/scoped-elements": ^3.0.6 lit: ^3.1.4 @@ -6349,7 +6519,7 @@ __metadata: "@open-wc/scoped-elements": "npm:^3.0.6" "@sl-design-system/shared": "npm:^0.12.0" "@sl-design-system/toggle-button": "npm:^0.0.15" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: lit: ^3.1.4 languageName: unknown @@ -6372,7 +6542,7 @@ __metadata: "@sl-design-system/menu": "npm:^0.3.2" "@sl-design-system/toggle-button": "npm:^0.0.15" "@sl-design-system/toggle-group": "npm:^0.0.15" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@open-wc/scoped-elements": ^3.0.6 languageName: unknown @@ -6399,7 +6569,7 @@ __metadata: "@sl-design-system/skeleton": "npm:^1.0.1" "@sl-design-system/spinner": "npm:^2.0.1" "@sl-design-system/virtual-list": "npm:^0.0.5" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@open-wc/scoped-elements": ^3.0.6 lit: ^3.1.4 @@ -6412,7 +6582,7 @@ __metadata: dependencies: "@sl-design-system/shared": "npm:^0.12.0" "@tanstack/virtual-core": "npm:^3.13.23" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@tanstack/virtual-core": ^3.13.12 lit: ^3.1.4 @@ -6423,9 +6593,9 @@ __metadata: version: 0.0.0-use.local resolution: "@sl-design-system/vitest-browser-lit@workspace:tools/vitest-browser-lit" dependencies: - "@vitest/browser": "npm:^4.1.4" - lit: "npm:^3.3.2" - vitest: "npm:^4.1.4" + "@vitest/browser": "npm:^4.1.7" + lit: "npm:^3.3.3" + vitest: "npm:^4.1.7" peerDependencies: "@vitest/browser": ">=2.1.0" lit: ">3.0.0" @@ -6457,10 +6627,10 @@ __metadata: markdown-it: "npm:^14.1.0" markdown-it-anchor: "npm:^8.6.7" markdown-it-attrs: "npm:^4.1.6" - sass: "npm:^1.99.0" + sass: "npm:^1.100.0" slugify: "npm:^1.6.8" tiny-glob: "npm:^0.2.9" - typescript: "npm:^6.0.2" + typescript: "npm:^6.0.3" wireit: "npm:^0.14.12" languageName: unknown linkType: soft @@ -6479,57 +6649,61 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-a11y@npm:^10.3.5": - version: 10.3.5 - resolution: "@storybook/addon-a11y@npm:10.3.5" +"@storybook/addon-a11y@npm:^10.4.1": + version: 10.4.1 + resolution: "@storybook/addon-a11y@npm:10.4.1" dependencies: "@storybook/global": "npm:^5.0.0" axe-core: "npm:^4.2.0" peerDependencies: - storybook: ^10.3.5 - checksum: 10c0/e12228948d85cea75014f8d81c94cabf9ab73614d4d3ecae3064013d4dbc7d25f1e3a10306f613db0c508a098a376b1dc2113aa57afb3fd196b22ae613776943 + storybook: ^10.4.1 + checksum: 10c0/2a051276db77dbf196b0c509aae6e49e1d1a722db1ecd3a73fa60ff8c1d88e61900f9c31a66201cedab8dd555718fca407099b4f27662fd3d9404b2096770344 languageName: node linkType: hard -"@storybook/addon-docs@npm:^10.3.5": - version: 10.3.5 - resolution: "@storybook/addon-docs@npm:10.3.5" +"@storybook/addon-docs@npm:^10.4.1": + version: 10.4.1 + resolution: "@storybook/addon-docs@npm:10.4.1" dependencies: "@mdx-js/react": "npm:^3.0.0" - "@storybook/csf-plugin": "npm:10.3.5" - "@storybook/icons": "npm:^2.0.1" - "@storybook/react-dom-shim": "npm:10.3.5" + "@storybook/csf-plugin": "npm:10.4.1" + "@storybook/icons": "npm:^2.0.2" + "@storybook/react-dom-shim": "npm:10.4.1" react: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" react-dom: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^10.3.5 - checksum: 10c0/6f2d7aee8c565df7e16e9d69da1366fc02e9a399a2ad9c5d5b04790955f7b55a1418814cb83eaa7d7210c6496ff2541d2bd05225b36ac266d82b4d9f3caf40c7 + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.4.1 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/d3fa09989e969a6319bab2d9a883450c3339b87d40836e7e6c1ccd56f0e45d04456b9e6ed80ec8897fde0aa489e6a711fdf3d24f210eefa12983aae374575ae0 languageName: node linkType: hard -"@storybook/addon-themes@npm:^10.3.5": - version: 10.3.5 - resolution: "@storybook/addon-themes@npm:10.3.5" +"@storybook/addon-themes@npm:^10.4.1": + version: 10.4.1 + resolution: "@storybook/addon-themes@npm:10.4.1" dependencies: ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^10.3.5 - checksum: 10c0/fef56bb9c6d45fea53f4a5f0d209a7ba77a16f2b6665f38526e20bc50b755fbf3976f87848c85955e86828ec1a02762ac9b82c275f7f69b14b22b8fc8913eb60 + storybook: ^10.4.1 + checksum: 10c0/b5df1b11b1170c8207b9a0896db11a210d08ff004d47f054909348d3356965c3e801dc825c20e66d967488b85f4007259bc78da05e2fe1975802dd8e3792b9d4 languageName: node linkType: hard -"@storybook/addon-vitest@npm:^10.3.5": - version: 10.3.5 - resolution: "@storybook/addon-vitest@npm:10.3.5" +"@storybook/addon-vitest@npm:^10.4.1": + version: 10.4.1 + resolution: "@storybook/addon-vitest@npm:10.4.1" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^2.0.1" + "@storybook/icons": "npm:^2.0.2" peerDependencies: "@vitest/browser": ^3.0.0 || ^4.0.0 "@vitest/browser-playwright": ^4.0.0 "@vitest/runner": ^3.0.0 || ^4.0.0 - storybook: ^10.3.5 + storybook: ^10.4.1 vitest: ^3.0.0 || ^4.0.0 peerDependenciesMeta: "@vitest/browser": @@ -6540,15 +6714,15 @@ __metadata: optional: true vitest: optional: true - checksum: 10c0/13b5fda1e3843bf6b6f58926e6ddf6dac849f32c69b1bc962c1965d855ef690d2390185bc13bbc8594f84ec1aa980a4426f2a875eb379e6d3d839d8e36c1765b + checksum: 10c0/91f5b490da41ae432ed6b676c39580483066c283888686adfce553832342107e3bda123f1bf2c481e1103f9fbb53303264d190737df9cd24d3f761d0b2ffda8c languageName: node linkType: hard -"@storybook/angular@npm:^10.3.5": - version: 10.3.5 - resolution: "@storybook/angular@npm:10.3.5" +"@storybook/angular@npm:^10.4.1": + version: 10.4.1 + resolution: "@storybook/angular@npm:10.4.1" dependencies: - "@storybook/builder-webpack5": "npm:10.3.5" + "@storybook/builder-webpack5": "npm:10.4.1" "@storybook/global": "npm:^5.0.0" telejson: "npm:8.0.0" ts-dedent: "npm:^2.0.0" @@ -6567,7 +6741,7 @@ __metadata: "@angular/platform-browser": ">=18.0.0 < 22.0.0" "@angular/platform-browser-dynamic": ">=18.0.0 < 22.0.0" rxjs: ^6.5.3 || ^7.4.0 - storybook: ^10.3.5 + storybook: ^10.4.1 typescript: ^4.9.0 || ^5.0.0 zone.js: ">=0.14.0" peerDependenciesMeta: @@ -6577,28 +6751,28 @@ __metadata: optional: true zone.js: optional: true - checksum: 10c0/600c0564d96274dab00fddc4fb03674a05661c853a30964841263bc0b3ede4cf9f7b621297b17f1c1b3059e08ea1127917474e65c199cb58ad65f47a24e3fd48 + checksum: 10c0/c72ee301a9f55e6b38badccc62eb1d2fdc77cb3420742e845aa359a3cf1a41c22274d993d1a06fa9a94b3ffb4f66f53f3c9a5aad7bcc80e0cbed6d5440ebaaa8 languageName: node linkType: hard -"@storybook/builder-vite@npm:10.3.5": - version: 10.3.5 - resolution: "@storybook/builder-vite@npm:10.3.5" +"@storybook/builder-vite@npm:10.4.1": + version: 10.4.1 + resolution: "@storybook/builder-vite@npm:10.4.1" dependencies: - "@storybook/csf-plugin": "npm:10.3.5" + "@storybook/csf-plugin": "npm:10.4.1" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^10.3.5 + storybook: ^10.4.1 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 10c0/6ba4ea38539c25843cfb3e8eca6fa6c1fc81b7dafde923735dec6bd980c8d7b90173ef3139eaed27602f87319ef9d54a273b3ae22e10d08e1fcabc3c9a253ec3 + checksum: 10c0/bbf6fc1f1139255b0336a55c6ccaa6a2a70789bfde61ca27eedf6bd687a1dd479d75f693afdfa5bc1b9ef46090e64a29d93d210c941fe9862c72e3009e5151a6 languageName: node linkType: hard -"@storybook/builder-webpack5@npm:10.3.5": - version: 10.3.5 - resolution: "@storybook/builder-webpack5@npm:10.3.5" +"@storybook/builder-webpack5@npm:10.4.1": + version: 10.4.1 + resolution: "@storybook/builder-webpack5@npm:10.4.1" dependencies: - "@storybook/core-webpack": "npm:10.3.5" + "@storybook/core-webpack": "npm:10.4.1" case-sensitive-paths-webpack-plugin: "npm:^2.4.0" cjs-module-lexer: "npm:^1.2.3" css-loader: "npm:^7.1.2" @@ -6607,41 +6781,41 @@ __metadata: html-webpack-plugin: "npm:^5.5.0" magic-string: "npm:^0.30.5" style-loader: "npm:^4.0.0" - terser-webpack-plugin: "npm:^5.3.14" + terser-webpack-plugin: "npm:^5.3.17" ts-dedent: "npm:^2.0.0" webpack: "npm:5" webpack-dev-middleware: "npm:^6.1.2" webpack-hot-middleware: "npm:^2.25.1" webpack-virtual-modules: "npm:^0.6.0" peerDependencies: - storybook: ^10.3.5 + storybook: ^10.4.1 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/71d3c95acb79c05c14742a74845a3f2a13ef87477ff95a35665709ca7b4399edb79525637eb2b3bf0742d9e88cf46104a8a5ef4308e28b72bdbf91431a79839c + checksum: 10c0/1c3ed0b89aceed199417d4ba67b44a1866a60a60f6b2ec00d97313c04b119f06fce2ab7fb207cf117531303920cd0aa9c62f47eb4e1dbc424339ee04c8cba486 languageName: node linkType: hard -"@storybook/core-webpack@npm:10.3.5": - version: 10.3.5 - resolution: "@storybook/core-webpack@npm:10.3.5" +"@storybook/core-webpack@npm:10.4.1": + version: 10.4.1 + resolution: "@storybook/core-webpack@npm:10.4.1" dependencies: ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^10.3.5 - checksum: 10c0/384b453c1bdd49fe9c4eef6e8e783ae239cce1c9e4fa6e5f1a9cb46ab50868c3cdb8e66cf993a7cfd925154ff3d1322a0a4c23571fc98ca10f6f5d1272e33768 + storybook: ^10.4.1 + checksum: 10c0/012546136942ea4eb5b1e8a1c9244b84c092f1ec3a60fd134c7b0f80dd923a950b75d8fa574e79c2b64c6fda42d09fa4d6cdff876d987ec273d74fa14ea29ac0 languageName: node linkType: hard -"@storybook/csf-plugin@npm:10.3.5": - version: 10.3.5 - resolution: "@storybook/csf-plugin@npm:10.3.5" +"@storybook/csf-plugin@npm:10.4.1": + version: 10.4.1 + resolution: "@storybook/csf-plugin@npm:10.4.1" dependencies: unplugin: "npm:^2.3.5" peerDependencies: esbuild: "*" rollup: "*" - storybook: ^10.3.5 + storybook: ^10.4.1 vite: "*" webpack: "*" peerDependenciesMeta: @@ -6653,7 +6827,7 @@ __metadata: optional: true webpack: optional: true - checksum: 10c0/db9ee2b24f4affbed28ec162daa0223186b025e84374b93abe4b5cd0e38195b1bfa1e4553f2d9e99cac718a1f253035227c9153c04677fd3ad0022a39cfd6f5c + checksum: 10c0/4d23b69eb5107856b29e54d1b209392ff61e4ac37adc7def06e04a19e3279edfc36ebe38b8ac1841407406edae4caca3a1ddfa009e11314f3ec8ec3b01c1f847 languageName: node linkType: hard @@ -6664,50 +6838,58 @@ __metadata: languageName: node linkType: hard -"@storybook/icons@npm:^2.0.1": - version: 2.0.1 - resolution: "@storybook/icons@npm:2.0.1" +"@storybook/icons@npm:^2.0.2": + version: 2.0.2 + resolution: "@storybook/icons@npm:2.0.2" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10c0/df2bbf1a5b50f12ab1bf78cae6de4dbf7c49df0e3a5f845553b51b20adbe8386a09fd172ea60342379f9284bb528cba2d0e2659cae6eb8d015cf92c8b32f1222 + checksum: 10c0/072486356fc929ba5a1a225a8636f0e50b2019083e86e4d48d55aeeae4b40f17731cd1eea9cf1785c53e5fc4409fa93aeca15dccb96675c8e7ab536b18ba864c languageName: node linkType: hard -"@storybook/react-dom-shim@npm:10.3.5": - version: 10.3.5 - resolution: "@storybook/react-dom-shim@npm:10.3.5" +"@storybook/react-dom-shim@npm:10.4.1": + version: 10.4.1 + resolution: "@storybook/react-dom-shim@npm:10.4.1" peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + "@types/react-dom": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.5 - checksum: 10c0/53383a37a4507dbdb088bebb402f259126d678d63d4b83186b794192e8a8d6528852b223d8bd7f98645293e5f99cf5feb11bfa14e441a47de92d42d3aa04ecdb + storybook: ^10.4.1 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/1d39e86d861eef5577174721a938159e97f80c27a856af182157cf5d0936ca3a91eaf1e5bbbfd6560d1035db1ffebdd757d0f08fd1c2c21cf353a996ea04d6f0 languageName: node linkType: hard -"@storybook/web-components-vite@npm:^10.3.5": - version: 10.3.5 - resolution: "@storybook/web-components-vite@npm:10.3.5" +"@storybook/web-components-vite@npm:^10.4.1": + version: 10.4.1 + resolution: "@storybook/web-components-vite@npm:10.4.1" dependencies: - "@storybook/builder-vite": "npm:10.3.5" - "@storybook/web-components": "npm:10.3.5" + "@storybook/builder-vite": "npm:10.4.1" + "@storybook/web-components": "npm:10.4.1" peerDependencies: - storybook: ^10.3.5 - checksum: 10c0/ecfa5b1d3118294b693e4af8d913a762e03c6e1131cba08eb3f39ffac7a1c029ad2d54bad5167e937ed6198d794622a72205f3fd3a47094d294bd3dd0cdb296f + storybook: ^10.4.1 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10c0/c42b6371f8b57cfe7899d9298cf79cb1cbb6006f6049acc0e643a4083c0587493ce8bb8068c984865c10622a03aa2f02bc40305c7e6ee63abb0e9fca6dfad744 languageName: node linkType: hard -"@storybook/web-components@npm:10.3.5, @storybook/web-components@npm:^10.3.5": - version: 10.3.5 - resolution: "@storybook/web-components@npm:10.3.5" +"@storybook/web-components@npm:10.4.1, @storybook/web-components@npm:^10.4.1": + version: 10.4.1 + resolution: "@storybook/web-components@npm:10.4.1" dependencies: "@storybook/global": "npm:^5.0.0" tiny-invariant: "npm:^1.3.1" ts-dedent: "npm:^2.0.0" peerDependencies: lit: ^2.0.0 || ^3.0.0 - storybook: ^10.3.5 - checksum: 10c0/a2bc569935b432b29e091fb9380e539ff1b24ccdd6e7e782540103608d08dbe71d26e8b1dd603e22376e6ac5dd84f4aef0b414ea5f75d67eb1b6375ea05d6b93 + storybook: ^10.4.1 + checksum: 10c0/e68766caed2db31dfeab87f3975b4820ff505a5ed0d0e48ff078162b9e970bb06b7574105dd952921e9c043b586cad3f6295fdbffbd0c68dd33802d41eb75790 languageName: node linkType: hard @@ -7387,12 +7569,12 @@ __metadata: languageName: node linkType: hard -"@types/sinon@npm:*, @types/sinon@npm:^21.0.0": - version: 21.0.0 - resolution: "@types/sinon@npm:21.0.0" +"@types/sinon@npm:*, @types/sinon@npm:^21.0.1": + version: 21.0.1 + resolution: "@types/sinon@npm:21.0.1" dependencies: "@types/sinonjs__fake-timers": "npm:*" - checksum: 10c0/276d610b5f975eba875c207075d031389d9321d407ff2210106301471893e1198fe83798c507b241620f498735806ab729aaca59b51a340d31875e5d61e18d3a + checksum: 10c0/b6cdc8420de4a657b1643c4da6cdc6f418de1c633b8022503929fc11538b5a691d70d74a93f3b82472018a6c778c8d543d2c3e23a4b68f73c37233277c436f16 languageName: node linkType: hard @@ -7795,47 +7977,47 @@ __metadata: languageName: node linkType: hard -"@vitest/browser-playwright@npm:~4.1.4": - version: 4.1.4 - resolution: "@vitest/browser-playwright@npm:4.1.4" +"@vitest/browser-playwright@npm:~4.1.7": + version: 4.1.7 + resolution: "@vitest/browser-playwright@npm:4.1.7" dependencies: - "@vitest/browser": "npm:4.1.4" - "@vitest/mocker": "npm:4.1.4" + "@vitest/browser": "npm:4.1.7" + "@vitest/mocker": "npm:4.1.7" tinyrainbow: "npm:^3.1.0" peerDependencies: playwright: "*" - vitest: 4.1.4 + vitest: 4.1.7 peerDependenciesMeta: playwright: optional: false - checksum: 10c0/3f2d29631de796d041d43334552b13c2adf3e5ab04b9a60ba28e666b3b4a6628cc57e7a8b4949f4ed4a536a796908079297d9d33b4e90f5b13e2abb45f419c22 + checksum: 10c0/b5d5b843e4dac3ae622a2d5049160326205e52a74e314e06d7167162fe9bb5d71972d2b8c07d180b882a97b1af644cdeff1f3a97744a218ccddd440946c4b498 languageName: node linkType: hard -"@vitest/browser@npm:4.1.4, @vitest/browser@npm:^4.1.4": - version: 4.1.4 - resolution: "@vitest/browser@npm:4.1.4" +"@vitest/browser@npm:4.1.7, @vitest/browser@npm:^4.1.7": + version: 4.1.7 + resolution: "@vitest/browser@npm:4.1.7" dependencies: "@blazediff/core": "npm:1.9.1" - "@vitest/mocker": "npm:4.1.4" - "@vitest/utils": "npm:4.1.4" + "@vitest/mocker": "npm:4.1.7" + "@vitest/utils": "npm:4.1.7" magic-string: "npm:^0.30.21" pngjs: "npm:^7.0.0" sirv: "npm:^3.0.2" tinyrainbow: "npm:^3.1.0" ws: "npm:^8.19.0" peerDependencies: - vitest: 4.1.4 - checksum: 10c0/c219c685d5befc2372d7cc80ef4ff78c9e57cb4a1bb74ec25d76596c9ad130d2bf67bc05d7ed1a6a8e0da763cd1c523eb81c95cd623c20f163509d0b36dae1c7 + vitest: 4.1.7 + checksum: 10c0/7ba88939a899e763510661059a8c620f1f42cf8db05c9bfb955cfb8c3e096c22ab630b6e50150579988e1483acc99772feb3d4114dd651f14c0aa9a01aa60505 languageName: node linkType: hard -"@vitest/coverage-v8@npm:~4.1.4": - version: 4.1.4 - resolution: "@vitest/coverage-v8@npm:4.1.4" +"@vitest/coverage-v8@npm:~4.1.7": + version: 4.1.7 + resolution: "@vitest/coverage-v8@npm:4.1.7" dependencies: "@bcoe/v8-coverage": "npm:^1.0.2" - "@vitest/utils": "npm:4.1.4" + "@vitest/utils": "npm:4.1.7" ast-v8-to-istanbul: "npm:^1.0.0" istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-report: "npm:^3.0.1" @@ -7845,12 +8027,12 @@ __metadata: std-env: "npm:^4.0.0-rc.1" tinyrainbow: "npm:^3.1.0" peerDependencies: - "@vitest/browser": 4.1.4 - vitest: 4.1.4 + "@vitest/browser": 4.1.7 + vitest: 4.1.7 peerDependenciesMeta: "@vitest/browser": optional: true - checksum: 10c0/e128a70b15eeee55ad201b9f2a9d88f3a2ccd630c1518ec8ab1d80a6e7b557d23d426244ce09748c8553a53725137d92696bd1be3bd9349863dd375749988a4a + checksum: 10c0/288fa77cfec00d84528154be90727ee0a868b91a32847b57e078fa4f3061711a53036a68d78bb4ea15e5c65b4644af6d2b7ad28b68b9301e9145426cdc27c0cd languageName: node linkType: hard @@ -7867,25 +8049,25 @@ __metadata: languageName: node linkType: hard -"@vitest/expect@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/expect@npm:4.1.4" +"@vitest/expect@npm:4.1.7": + version: 4.1.7 + resolution: "@vitest/expect@npm:4.1.7" dependencies: "@standard-schema/spec": "npm:^1.1.0" "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:4.1.4" - "@vitest/utils": "npm:4.1.4" + "@vitest/spy": "npm:4.1.7" + "@vitest/utils": "npm:4.1.7" chai: "npm:^6.2.2" tinyrainbow: "npm:^3.1.0" - checksum: 10c0/99b53a931366ddc985f26528495ec991fa2ce64018b00a56f989c322553045c5adf17e091eb7a12d786246712f84d36fc88e9d26c852538ff4dd5a6f9cf98715 + checksum: 10c0/1a72387c6d3cac1e12cd4df382e666d96560b38001ea0133f1e0a22825f71ccf1640ccce13244296b0054c15cf04442f3adbd67dfc57fe542bd35a46cd805487 languageName: node linkType: hard -"@vitest/mocker@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/mocker@npm:4.1.4" +"@vitest/mocker@npm:4.1.7": + version: 4.1.7 + resolution: "@vitest/mocker@npm:4.1.7" dependencies: - "@vitest/spy": "npm:4.1.4" + "@vitest/spy": "npm:4.1.7" estree-walker: "npm:^3.0.3" magic-string: "npm:^0.30.21" peerDependencies: @@ -7896,7 +8078,7 @@ __metadata: optional: true vite: optional: true - checksum: 10c0/da61ee63743da4bc45df0488c994e284e7059a4005149195744705945d19aeb267c801b1f7d85e71b40f547ff2d5a195175c5d51e8455179c794ce67a019de87 + checksum: 10c0/e03dbbba435543e3cfa5e034ba8ade371de5e398255f75366ebc370ff8dd78d45f7d7cc9daa76eb1d399b31e659e47d3cbb710566e64ceeeba3f99b418e4b955 languageName: node linkType: hard @@ -7909,34 +8091,34 @@ __metadata: languageName: node linkType: hard -"@vitest/pretty-format@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/pretty-format@npm:4.1.4" +"@vitest/pretty-format@npm:4.1.7": + version: 4.1.7 + resolution: "@vitest/pretty-format@npm:4.1.7" dependencies: tinyrainbow: "npm:^3.1.0" - checksum: 10c0/14a25c5acd02b1d18f9fab01d884658edb9137008d01025273617fb000e36391e4fda1513e94a257f5e611fb09041a0c042d145a90d359c9e810c0044b12763e + checksum: 10c0/49ef801171708e3a92214e8720efbedbd6e0e6baf17971aaf4feb7422e5c9eba82262c24a9e6dd4d41a31fae77bd31d5b37cf140d13e0ac4ce29a7457bdc692f languageName: node linkType: hard -"@vitest/runner@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/runner@npm:4.1.4" +"@vitest/runner@npm:4.1.7": + version: 4.1.7 + resolution: "@vitest/runner@npm:4.1.7" dependencies: - "@vitest/utils": "npm:4.1.4" + "@vitest/utils": "npm:4.1.7" pathe: "npm:^2.0.3" - checksum: 10c0/a942ecf2e50e4c380f0d269f87272353dc40fe354357e1ecd0c6568fd37202bb86e33db676f4ad6cc5f1ab30937bba0b278d987729b21a0f22e9827f7f577da2 + checksum: 10c0/63474c6fc088d75b5d7fe735195504f923c694b83a22eb9caa53d6486c923974304c2e3ef4d5bcd808d88082174f38434be320fc4fe649a8cf33f0459a0576e3 languageName: node linkType: hard -"@vitest/snapshot@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/snapshot@npm:4.1.4" +"@vitest/snapshot@npm:4.1.7": + version: 4.1.7 + resolution: "@vitest/snapshot@npm:4.1.7" dependencies: - "@vitest/pretty-format": "npm:4.1.4" - "@vitest/utils": "npm:4.1.4" + "@vitest/pretty-format": "npm:4.1.7" + "@vitest/utils": "npm:4.1.7" magic-string: "npm:^0.30.21" pathe: "npm:^2.0.3" - checksum: 10c0/9221df7c097665a204c811184ac2f3b89638ecd115344e703e9c4361dabd2ba80be4710ed20d127817d34227a74f21b90725deaecd4632954b492ad258d4913f + checksum: 10c0/6fa49c4242a4acc0557ee6a20552db41f4f4c9d2d4c05993181c3f5f19e66579e08f63d34f792b79400547ab791ef500a9955b77390c381e45c3bb8e33717793 languageName: node linkType: hard @@ -7949,18 +8131,18 @@ __metadata: languageName: node linkType: hard -"@vitest/spy@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/spy@npm:4.1.4" - checksum: 10c0/1036591947668845e45515d5b66b2095071609c243d2c987d650c71d0a27418e5de75a8b1ad44b7f45c5d97e71176640f0f49da94b32fb3d11e87cdd009bed26 +"@vitest/spy@npm:4.1.7": + version: 4.1.7 + resolution: "@vitest/spy@npm:4.1.7" + checksum: 10c0/be2a95d5c5c438b57c9b33cef1289fb02659214754b5e946cb4b8183e2b5089e49e3fda6ca05981f3ea9872b207595db109e25072668c0a671203f69fddbbe99 languageName: node linkType: hard -"@vitest/ui@npm:~4.1.4": - version: 4.1.4 - resolution: "@vitest/ui@npm:4.1.4" +"@vitest/ui@npm:~4.1.7": + version: 4.1.7 + resolution: "@vitest/ui@npm:4.1.7" dependencies: - "@vitest/utils": "npm:4.1.4" + "@vitest/utils": "npm:4.1.7" fflate: "npm:^0.8.2" flatted: "npm:^3.4.2" pathe: "npm:^2.0.3" @@ -7968,8 +8150,8 @@ __metadata: tinyglobby: "npm:^0.2.15" tinyrainbow: "npm:^3.1.0" peerDependencies: - vitest: 4.1.4 - checksum: 10c0/2bd711f9f20b479e0b118508b1a1ea3b3dfe1ebd55905aed7a9324a1a59c07b3e7f82b58c94ffc7dbcb91bc66355883d932b1325bba7a96824c25d0e8b5c8ac7 + vitest: 4.1.7 + checksum: 10c0/7688c7dd1673d1de533417e8872e388f2c7bee3c9935bff7873fe6d8b7cfdf6ae3cce5b6c8f1a1e7a017d204db3970c9e73449af351721d5361dcade9b0753f3 languageName: node linkType: hard @@ -7984,14 +8166,14 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/utils@npm:4.1.4" +"@vitest/utils@npm:4.1.7": + version: 4.1.7 + resolution: "@vitest/utils@npm:4.1.7" dependencies: - "@vitest/pretty-format": "npm:4.1.4" + "@vitest/pretty-format": "npm:4.1.7" convert-source-map: "npm:^2.0.0" tinyrainbow: "npm:^3.1.0" - checksum: 10c0/7f81db08e5a8db1e83a37a8d64db011ae3a08b5bcc9aa220a6da428385acb75b11c77b169ab7a9f753529cc25ec11406cff6099b92711fda6291f844fb840a4e + checksum: 10c0/aa0079d8923506300527dc23ff68cf090ffcb2c6a9549e598ae22ba0eb8a6bb4448b10724b38bc6b077f9957333302a857d791ad2f7abd807bb6263c9a218833 languageName: node linkType: hard @@ -8633,10 +8815,10 @@ __metadata: languageName: node linkType: hard -"ansi-regex@npm:^6.0.1": - version: 6.0.1 - resolution: "ansi-regex@npm:6.0.1" - checksum: 10c0/cbe16dbd2c6b2735d1df7976a7070dd277326434f0212f43abf6d87674095d247968209babdaad31bb00882fa68807256ba9be340eec2f1004de14ca75f52a08 +"ansi-regex@npm:^6.2.2": + version: 6.2.2 + resolution: "ansi-regex@npm:6.2.2" + checksum: 10c0/05d4acb1d2f59ab2cf4b794339c7b168890d44dda4bf0ce01152a8da0213aca207802f930442ce8cd22d7a92f44907664aac6508904e75e038fa944d2601b30f languageName: node linkType: hard @@ -8649,7 +8831,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": +"ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1, ansi-styles@npm:^6.2.3": version: 6.2.3 resolution: "ansi-styles@npm:6.2.3" checksum: 10c0/23b8a4ce14e18fb854693b95351e286b771d23d8844057ed2e7d083cd3e708376c3323707ec6a24365f7d7eda3ca00327fe04092e29e551499ec4c8b7bfac868 @@ -9105,10 +9287,10 @@ __metadata: languageName: node linkType: hard -"axe-core@npm:^4.2.0, axe-core@npm:^4.3.3, axe-core@npm:~4.11.3": - version: 4.11.3 - resolution: "axe-core@npm:4.11.3" - checksum: 10c0/bc757775ef41396faf6470752a12e96f3972d0d97cae4ec28e99cec7bca2c5aaa6d040b97e7f0278e8d1ea354fa0b0bf7fcaa51775a725d7ed0a0834e7ea13d7 +"axe-core@npm:^4.2.0, axe-core@npm:^4.3.3, axe-core@npm:~4.11.4": + version: 4.11.4 + resolution: "axe-core@npm:4.11.4" + checksum: 10c0/c4aa83fc3eac5f7a0d0cb1a28f9d073acf0c06ce8daacc38608faa278c57ce084c028c850746b98817ae4c101c30c1a32e95ea34748c4b4c7419b9b81221ef84 languageName: node linkType: hard @@ -9324,12 +9506,12 @@ __metadata: languageName: node linkType: hard -"baseline-browser-mapping@npm:^2.10.9, baseline-browser-mapping@npm:^2.9.0": - version: 2.10.9 - resolution: "baseline-browser-mapping@npm:2.10.9" +"baseline-browser-mapping@npm:^2.10.12, baseline-browser-mapping@npm:^2.10.32": + version: 2.10.32 + resolution: "baseline-browser-mapping@npm:2.10.32" bin: baseline-browser-mapping: dist/cli.cjs - checksum: 10c0/5221d92d7deeae6f7c6ce90ace8d15ffad50095a92c6ddf0cf826f49717a1afb607de32482447a388df82f4ca89c7c26eaf76fe31f8a2727fe1c09912bcfe184 + checksum: 10c0/408c93245bdf1e92ab0f891ebf9283ec60dbabfaac81bdc9a20d371565a2a496b0fb8028f7d628c3f66f90ee142670a81575cf1cbd5229f7b4b0d350db911085 languageName: node linkType: hard @@ -9580,18 +9762,18 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.24.0, browserslist@npm:^4.24.5, browserslist@npm:^4.26.0, browserslist@npm:^4.28.1": - version: 4.28.1 - resolution: "browserslist@npm:4.28.1" +"browserslist@npm:^4.0.0, browserslist@npm:^4.24.0, browserslist@npm:^4.26.0, browserslist@npm:^4.28.1, browserslist@npm:^4.28.2": + version: 4.28.2 + resolution: "browserslist@npm:4.28.2" dependencies: - baseline-browser-mapping: "npm:^2.9.0" - caniuse-lite: "npm:^1.0.30001759" - electron-to-chromium: "npm:^1.5.263" - node-releases: "npm:^2.0.27" - update-browserslist-db: "npm:^1.2.0" + baseline-browser-mapping: "npm:^2.10.12" + caniuse-lite: "npm:^1.0.30001782" + electron-to-chromium: "npm:^1.5.328" + node-releases: "npm:^2.0.36" + update-browserslist-db: "npm:^1.2.3" bin: browserslist: cli.js - checksum: 10c0/545a5fa9d7234e3777a7177ec1e9134bb2ba60a69e6b95683f6982b1473aad347c77c1264ccf2ac5dea609a9731fbfbda6b85782bdca70f80f86e28a402504bd + checksum: 10c0/c0228b6330f785b7fa59d2d360124ec6d9322f96ed9f3ee1f873e33ecc9503a6f0ffc3b71191a28c4ff6e930b753b30043da1c33844a9548f3018d491f09ce60 languageName: node linkType: hard @@ -9806,10 +9988,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001759, caniuse-lite@npm:^1.0.30001774": - version: 1.0.30001777 - resolution: "caniuse-lite@npm:1.0.30001777" - checksum: 10c0/e35443fa7c470edc06e315297cca706790840e96983fff12dfe502a4b123d6e4a64b9b4e8e35fb2f5bb60c31b24fbda93d76b2f700ce183df474671236fa7a4a +"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001774, caniuse-lite@npm:^1.0.30001782": + version: 1.0.30001793 + resolution: "caniuse-lite@npm:1.0.30001793" + checksum: 10c0/bee8f8b55d1ccdb2076b7355c06fd01916952eadd76b828e4d5fb9ac62d17ec7db0e2b7c326b923478b93526ad1ff74f189cf40c06de0e4a5edbc677009b97fe languageName: node linkType: hard @@ -10060,9 +10242,9 @@ __metadata: languageName: node linkType: hard -"chromatic@npm:^15.3.0": - version: 15.3.0 - resolution: "chromatic@npm:15.3.0" +"chromatic@npm:^15.3.1": + version: 15.3.1 + resolution: "chromatic@npm:15.3.1" peerDependencies: "@chromatic-com/cypress": ^0.*.* || ^1.0.0 "@chromatic-com/playwright": ^0.*.* || ^1.0.0 @@ -10075,7 +10257,7 @@ __metadata: chroma: dist/bin.js chromatic: dist/bin.js chromatic-cli: dist/bin.js - checksum: 10c0/0145783d5a38c10e60a8d9d630dd971f5544353cd359b7b74b01904003a20720acabf0b370ff821dcf976ee5b077687430d810e2658b0fb01ae209edf50e9c29 + checksum: 10c0/9987fd93a8b61b1f2f4ea9544ac9a2e280904c5731c49394a4b505ddcf4e50093691e5aa2b944a6182bd52745c782e275fd283d1e5778d75f8bb5e70fb66768e languageName: node linkType: hard @@ -10153,13 +10335,13 @@ __metadata: languageName: node linkType: hard -"cli-truncate@npm:^5.0.0": - version: 5.1.0 - resolution: "cli-truncate@npm:5.1.0" +"cli-truncate@npm:^5.0.0, cli-truncate@npm:^5.2.0": + version: 5.2.0 + resolution: "cli-truncate@npm:5.2.0" dependencies: - slice-ansi: "npm:^7.1.0" - string-width: "npm:^8.0.0" - checksum: 10c0/388a4c9813372fb82ef3958af9bcf233419e80f4f435386cc83666ba85c9ccfdaa4dd6e47a9fde8f70b1e2b485cfc5da97bc899ce4f3b24ed04933a2f878f7d6 + slice-ansi: "npm:^8.0.0" + string-width: "npm:^8.2.0" + checksum: 10c0/0d4ec94702ca85b64522ac93633837fb5ea7db17b79b1322a60f6045e6ae2b8cd7bd4c1d19ac7d1f9e10e3bbda1112e172e439b68c02b785ee00da8d6a5c5471 languageName: node linkType: hard @@ -10368,7 +10550,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^14.0.0, commander@npm:^14.0.3": +"commander@npm:^14.0.0": version: 14.0.3 resolution: "commander@npm:14.0.3" checksum: 10c0/755652564bbf56ff2ff083313912b326450d3f8d8c85f4b71416539c9a05c3c67dbd206821ca72635bf6b160e2afdefcb458e86b317827d5cb333b69ce7f1a24 @@ -10842,64 +11024,64 @@ __metadata: languageName: node linkType: hard -"cssnano-preset-default@npm:^7.0.12": - version: 7.0.12 - resolution: "cssnano-preset-default@npm:7.0.12" +"cssnano-preset-default@npm:^7.0.17": + version: 7.0.17 + resolution: "cssnano-preset-default@npm:7.0.17" dependencies: - browserslist: "npm:^4.28.1" + browserslist: "npm:^4.28.2" css-declaration-sorter: "npm:^7.2.0" - cssnano-utils: "npm:^5.0.1" + cssnano-utils: "npm:^5.0.3" postcss-calc: "npm:^10.1.1" - postcss-colormin: "npm:^7.0.7" - postcss-convert-values: "npm:^7.0.9" - postcss-discard-comments: "npm:^7.0.6" - postcss-discard-duplicates: "npm:^7.0.2" - postcss-discard-empty: "npm:^7.0.1" - postcss-discard-overridden: "npm:^7.0.1" - postcss-merge-longhand: "npm:^7.0.5" - postcss-merge-rules: "npm:^7.0.8" - postcss-minify-font-values: "npm:^7.0.1" - postcss-minify-gradients: "npm:^7.0.2" - postcss-minify-params: "npm:^7.0.6" - postcss-minify-selectors: "npm:^7.0.6" - postcss-normalize-charset: "npm:^7.0.1" - postcss-normalize-display-values: "npm:^7.0.1" - postcss-normalize-positions: "npm:^7.0.1" - postcss-normalize-repeat-style: "npm:^7.0.1" - postcss-normalize-string: "npm:^7.0.1" - postcss-normalize-timing-functions: "npm:^7.0.1" - postcss-normalize-unicode: "npm:^7.0.6" - postcss-normalize-url: "npm:^7.0.1" - postcss-normalize-whitespace: "npm:^7.0.1" - postcss-ordered-values: "npm:^7.0.2" - postcss-reduce-initial: "npm:^7.0.6" - postcss-reduce-transforms: "npm:^7.0.1" - postcss-svgo: "npm:^7.1.1" - postcss-unique-selectors: "npm:^7.0.5" - peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/aee2b6ef0d3d76dfa21608fa55702fe02ff73af249ee68a265993e376caeba4bc1ab3654fead7b2b7da55a8ece7711a890c04fbc5616f2f233bcfc287c8f1908 - languageName: node - linkType: hard - -"cssnano-utils@npm:^5.0.1": - version: 5.0.1 - resolution: "cssnano-utils@npm:5.0.1" + postcss-colormin: "npm:^7.0.10" + postcss-convert-values: "npm:^7.0.12" + postcss-discard-comments: "npm:^7.0.8" + postcss-discard-duplicates: "npm:^7.0.4" + postcss-discard-empty: "npm:^7.0.3" + postcss-discard-overridden: "npm:^7.0.3" + postcss-merge-longhand: "npm:^7.0.7" + postcss-merge-rules: "npm:^7.0.11" + postcss-minify-font-values: "npm:^7.0.3" + postcss-minify-gradients: "npm:^7.0.5" + postcss-minify-params: "npm:^7.0.9" + postcss-minify-selectors: "npm:^7.1.2" + postcss-normalize-charset: "npm:^7.0.3" + postcss-normalize-display-values: "npm:^7.0.3" + postcss-normalize-positions: "npm:^7.0.4" + postcss-normalize-repeat-style: "npm:^7.0.4" + postcss-normalize-string: "npm:^7.0.3" + postcss-normalize-timing-functions: "npm:^7.0.3" + postcss-normalize-unicode: "npm:^7.0.9" + postcss-normalize-url: "npm:^7.0.3" + postcss-normalize-whitespace: "npm:^7.0.3" + postcss-ordered-values: "npm:^7.0.4" + postcss-reduce-initial: "npm:^7.0.9" + postcss-reduce-transforms: "npm:^7.0.3" + postcss-svgo: "npm:^7.1.3" + postcss-unique-selectors: "npm:^7.0.7" + peerDependencies: + postcss: ^8.5.13 + checksum: 10c0/92b69d1d4b85ead9dc4efaaa9c8be90d5da2125b7928b133f7b73cd8f631fd76b3e7e5cb779ca32c4d9ffa50602cb7e844e4139519e5deac4c7c60f4a0d58822 + languageName: node + linkType: hard + +"cssnano-utils@npm:^5.0.3": + version: 5.0.3 + resolution: "cssnano-utils@npm:5.0.3" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/e416e58587ccec4d904093a2834c66c44651578a58960019884add376d4f151c5b809674108088140dd57b0787cb7132a083d40ae33a72bf986d03c4b7b7c5f4 + postcss: ^8.5.13 + checksum: 10c0/ea78fb1e9aa1b149da01ec14f0e982173dfa00f72c9278f3bc247f9fb0ab4a1da3b2342d6e3aa4a800b9b5c404a67296140b730cbaa0e6a8d9a0ddc11cd2813d languageName: node linkType: hard -"cssnano@npm:^7.1.4": - version: 7.1.4 - resolution: "cssnano@npm:7.1.4" +"cssnano@npm:^7.1.9": + version: 7.1.9 + resolution: "cssnano@npm:7.1.9" dependencies: - cssnano-preset-default: "npm:^7.0.12" + cssnano-preset-default: "npm:^7.0.17" lilconfig: "npm:^3.1.3" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/de5f20dec1350c79f4eedc2b6b3b1e72403b2983e5dd93d3b735c39298276dc401b21b59a0fa7a2b8dfe0f8d95802cbdce805ec583e14a48593ed1173ea65107 + postcss: ^8.5.13 + checksum: 10c0/4c944405c4c319be63ea19ce488548487d8fbfba46ef6a6fb504b6ce62d4549d0137f99a8ade2a2843e192ae7981751f66b0dc982421d56390488c332baca5af languageName: node linkType: hard @@ -11295,10 +11477,10 @@ __metadata: languageName: node linkType: hard -"diff@npm:^8.0.3": - version: 8.0.3 - resolution: "diff@npm:8.0.3" - checksum: 10c0/d29321c70d3545fdcb56c5fdd76028c3f04c012462779e062303d4c3c531af80d2c360c26b871e6e2b9a971d2422d47e1779a859106c4cac4b5d2d143df70e20 +"diff@npm:^8.0.4": + version: 8.0.4 + resolution: "diff@npm:8.0.4" + checksum: 10c0/7ee5d03926db4039be7252ac3b0abaae1bd122a2ca971e5ca7270e444e36ff83dd906fad1a719740ca347e97ed5dc8f458a76a8391dbcd7aff363bdafb348a00 languageName: node linkType: hard @@ -11511,10 +11693,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.5.263": - version: 1.5.307 - resolution: "electron-to-chromium@npm:1.5.307" - checksum: 10c0/eb773a28af0dd7b3717b9bc2b31f332bcb42b43019866e039276db75c8c14063f96e29d19bea47231b4335a319d8518997b2d577dec6b5b237b768c7afdc5588 +"electron-to-chromium@npm:^1.5.328": + version: 1.5.361 + resolution: "electron-to-chromium@npm:1.5.361" + checksum: 10c0/5e6e9c0c12ab82366eddf575c8b9114d5af710abad3b592141c56cfa0169471f3633dcc793420c303fad68dff21c8fb724b43cde7e66be947983fa3d6d5a7359 languageName: node linkType: hard @@ -12491,10 +12673,10 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^5.0.1": - version: 5.0.1 - resolution: "eventemitter3@npm:5.0.1" - checksum: 10c0/4ba5c00c506e6c786b4d6262cfbce90ddc14c10d4667e5c83ae993c9de88aa856033994dd2b35b83e8dc1170e224e66a319fa80adc4c32adcd2379bbc75da814 +"eventemitter3@npm:^5.0.1, eventemitter3@npm:^5.0.4": + version: 5.0.4 + resolution: "eventemitter3@npm:5.0.4" + checksum: 10c0/575b8cac8d709e1473da46f8f15ef311b57ff7609445a7c71af5cd42598583eee6f098fa7a593e30f27e94b8865642baa0689e8fa97c016f742abdb3b1bf6d9a languageName: node linkType: hard @@ -13379,10 +13561,10 @@ __metadata: languageName: node linkType: hard -"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.0": - version: 1.4.0 - resolution: "get-east-asian-width@npm:1.4.0" - checksum: 10c0/4e481d418e5a32061c36fbb90d1b225a254cc5b2df5f0b25da215dcd335a3c111f0c2023ffda43140727a9cafb62dac41d022da82c08f31083ee89f714ee3b83 +"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.1, get-east-asian-width@npm:^1.5.0": + version: 1.6.0 + resolution: "get-east-asian-width@npm:1.6.0" + checksum: 10c0/7e72e9550fd49ca5b246f9af6bb2afc129c96412845ff6556b3274fd44817a381702ca17028efe9866b261a3d44254cbf21e6c90cf05b4b61675630af776d431 languageName: node linkType: hard @@ -14765,12 +14947,12 @@ __metadata: languageName: node linkType: hard -"is-fullwidth-code-point@npm:^5.0.0": - version: 5.0.0 - resolution: "is-fullwidth-code-point@npm:5.0.0" +"is-fullwidth-code-point@npm:^5.0.0, is-fullwidth-code-point@npm:^5.1.0": + version: 5.1.0 + resolution: "is-fullwidth-code-point@npm:5.1.0" dependencies: - get-east-asian-width: "npm:^1.0.0" - checksum: 10c0/cd591b27d43d76b05fa65ed03eddce57a16e1eca0b7797ff7255de97019bcaf0219acfc0c4f7af13319e13541f2a53c0ace476f442b13267b9a6a7568f2b65c8 + get-east-asian-width: "npm:^1.3.1" + checksum: 10c0/c1172c2e417fb73470c56c431851681591f6a17233603a9e6f94b7ba870b2e8a5266506490573b607fb1081318589372034aa436aec07b465c2029c0bc7f07a4 languageName: node linkType: hard @@ -15382,10 +15564,10 @@ __metadata: languageName: node linkType: hard -"jasmine-core@npm:~6.1.0": - version: 6.1.0 - resolution: "jasmine-core@npm:6.1.0" - checksum: 10c0/c2a6e7687e057f04c9b63b2a0dd2730745dbd9e6dc837ae4caf4afa04481060242a5805cc99fbd39235d07f08690b5d9df05f6d479caf0f43f86026d021d04e8 +"jasmine-core@npm:~6.2.0": + version: 6.2.0 + resolution: "jasmine-core@npm:6.2.0" + checksum: 10c0/4845ce6e93b5a48abbc0d37d748189ff826568e11bc594e527b173cd77dbd371e7a9f16c4a5e9bde7fb864ac60e1e3b21ddc930343f4d51d766128c55f4ceab6 languageName: node linkType: hard @@ -16140,19 +16322,21 @@ __metadata: languageName: node linkType: hard -"lint-staged@npm:^16.4.0": - version: 16.4.0 - resolution: "lint-staged@npm:16.4.0" +"lint-staged@npm:^17.0.5": + version: 17.0.5 + resolution: "lint-staged@npm:17.0.5" dependencies: - commander: "npm:^14.0.3" - listr2: "npm:^9.0.5" - picomatch: "npm:^4.0.3" + listr2: "npm:^10.2.1" + picomatch: "npm:^4.0.4" string-argv: "npm:^0.3.2" - tinyexec: "npm:^1.0.4" - yaml: "npm:^2.8.2" + tinyexec: "npm:^1.1.2" + yaml: "npm:^2.8.4" + dependenciesMeta: + yaml: + optional: true bin: lint-staged: bin/lint-staged.js - checksum: 10c0/67625a49a2a01368c7df2da7e553567a79c4b261d9faf3436e00fc3a2f9c4bbe7295909012c47b3d9029e269fd7d7469901a5120573527a032f15797aa497c26 + checksum: 10c0/6ecf2024744147ea768dbd550c0c47f04b295f07d30e5ecb5e375c18d8fc24d4bfa897042f4aabd7c3510054ad5bbbb51fa36e60e8dca853e31f9876ef9a3726 languageName: node linkType: hard @@ -16175,7 +16359,7 @@ __metadata: languageName: node linkType: hard -"listr2@npm:9.0.5, listr2@npm:^9.0.5": +"listr2@npm:9.0.5": version: 9.0.5 resolution: "listr2@npm:9.0.5" dependencies: @@ -16189,6 +16373,19 @@ __metadata: languageName: node linkType: hard +"listr2@npm:^10.2.1": + version: 10.2.1 + resolution: "listr2@npm:10.2.1" + dependencies: + cli-truncate: "npm:^5.2.0" + eventemitter3: "npm:^5.0.4" + log-update: "npm:^6.1.0" + rfdc: "npm:^1.4.1" + wrap-ansi: "npm:^10.0.0" + checksum: 10c0/a381a7aaef2e8625e6e882835ef446d14306c8fa371b56c4499cf23ece86f84922008af11962bfba5411b51589e02d280bea2b820451a2efad89ebf78bbe68a4 + languageName: node + linkType: hard + "lit-element@npm:^4.2.0": version: 4.2.0 resolution: "lit-element@npm:4.2.0" @@ -16209,14 +16406,14 @@ __metadata: languageName: node linkType: hard -"lit@npm:^3.0.0, lit@npm:^3.2.0, lit@npm:^3.3.2": - version: 3.3.2 - resolution: "lit@npm:3.3.2" +"lit@npm:^3.0.0, lit@npm:^3.2.0, lit@npm:^3.3.3": + version: 3.3.3 + resolution: "lit@npm:3.3.3" dependencies: "@lit/reactive-element": "npm:^2.1.0" lit-element: "npm:^4.2.0" lit-html: "npm:^3.3.0" - checksum: 10c0/50563fd9c7bf546f8fdc6a936321b5be581ce440a359b06048ae5d44c1ecf6c38c2ded708e97d36a1ce70da1a7ad569890e40e0fb5ed040ec42d5ed2365f468d + checksum: 10c0/11b6175ebffce92f4cf835ddd8a198d49707c27f34b0a7c538d2cb294f6c2fb7ff40aabd3589fcf0c2ed162404faaf2bb7c966fad9024bb9e02d013e2d5aa0b5 languageName: node linkType: hard @@ -17751,9 +17948,9 @@ __metadata: languageName: node linkType: hard -"mocha@npm:11.7.5": - version: 11.7.5 - resolution: "mocha@npm:11.7.5" +"mocha@npm:11.7.6": + version: 11.7.6 + resolution: "mocha@npm:11.7.6" dependencies: browser-stdout: "npm:^1.3.1" chokidar: "npm:^4.0.1" @@ -17779,7 +17976,7 @@ __metadata: bin: _mocha: bin/_mocha mocha: bin/mocha.js - checksum: 10c0/e6150cba85345aaabbc5b2e7341b1e6706b878f5a9782960cad57fd4cc458730a76d08c5f1a3e05d3ebb49cad93b455764bb00727bd148dcaf0c6dd4fa665b3d + checksum: 10c0/95b3eeb52eaf253167d335d1c1a46659f6f282dc112a74fd0ac46380637ae3ee52894319e00a46412a1e5a6e3056128153dc87c1fe36565c748b9fbe8d705ad0 languageName: node linkType: hard @@ -17932,12 +18129,12 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.11": - version: 3.3.11 - resolution: "nanoid@npm:3.3.11" +"nanoid@npm:^3.3.11, nanoid@npm:^3.3.12": + version: 3.3.12 + resolution: "nanoid@npm:3.3.12" bin: nanoid: bin/nanoid.cjs - checksum: 10c0/40e7f70b3d15f725ca072dfc4f74e81fcf1fbb02e491cf58ac0c79093adc9b0a73b152bcde57df4b79cd097e13023d7504acb38404a4da7bc1cd8e887b82fe0b + checksum: 10c0/ba142b7b39e11e80c16dd74b0365d407880c87c1cf7e1480956981ae940ee36060fa5b6f092cd1e315184dd19244c657bd017d03327bd3c62247d691c5e8edfb languageName: node linkType: hard @@ -18015,9 +18212,9 @@ __metadata: languageName: node linkType: hard -"ng-packagr@npm:^21.2.2": - version: 21.2.2 - resolution: "ng-packagr@npm:21.2.2" +"ng-packagr@npm:^21.2.3": + version: 21.2.3 + resolution: "ng-packagr@npm:21.2.3" dependencies: "@ampproject/remapping": "npm:^2.3.0" "@rollup/plugin-json": "npm:^6.1.0" @@ -18054,7 +18251,7 @@ __metadata: optional: true bin: ng-packagr: src/cli/main.js - checksum: 10c0/0c1d8dd1f9c80417b6120e2eae158d2e6713e63fbca28f8da48bbc94e101d276dd7d4e1e0e6b31e76689babe1d0487345e4dcf9fc3d44824c9474a72c523a896 + checksum: 10c0/6b587f18e4999c2bd651def518f2571d4044456c447cb0b7e5f1c318b0ebc94aa7fa59f4b833d72e405bdbbf6ee104fac28410242f722e2d5cc49966103c4fc2 languageName: node linkType: hard @@ -18179,10 +18376,10 @@ __metadata: languageName: node linkType: hard -"node-releases@npm:^2.0.27": - version: 2.0.27 - resolution: "node-releases@npm:2.0.27" - checksum: 10c0/f1e6583b7833ea81880627748d28a3a7ff5703d5409328c216ae57befbced10ce2c991bea86434e8ec39003bd017f70481e2e5f8c1f7e0a7663241f81d6e00e2 +"node-releases@npm:^2.0.36": + version: 2.0.46 + resolution: "node-releases@npm:2.0.46" + checksum: 10c0/04632591f97f15848adfb12b21fa013a6c19809afcf5db65fe88c95a36271c3f423e21110fd319ad5a9c5029ffe65eb81f3e4857e6af19622bc888d92a04ad22 languageName: node linkType: hard @@ -18679,7 +18876,77 @@ __metadata: languageName: node linkType: hard -"oxc-resolver@npm:^11.9.0": +"oxc-parser@npm:^0.127.0": + version: 0.127.0 + resolution: "oxc-parser@npm:0.127.0" + dependencies: + "@oxc-parser/binding-android-arm-eabi": "npm:0.127.0" + "@oxc-parser/binding-android-arm64": "npm:0.127.0" + "@oxc-parser/binding-darwin-arm64": "npm:0.127.0" + "@oxc-parser/binding-darwin-x64": "npm:0.127.0" + "@oxc-parser/binding-freebsd-x64": "npm:0.127.0" + "@oxc-parser/binding-linux-arm-gnueabihf": "npm:0.127.0" + "@oxc-parser/binding-linux-arm-musleabihf": "npm:0.127.0" + "@oxc-parser/binding-linux-arm64-gnu": "npm:0.127.0" + "@oxc-parser/binding-linux-arm64-musl": "npm:0.127.0" + "@oxc-parser/binding-linux-ppc64-gnu": "npm:0.127.0" + "@oxc-parser/binding-linux-riscv64-gnu": "npm:0.127.0" + "@oxc-parser/binding-linux-riscv64-musl": "npm:0.127.0" + "@oxc-parser/binding-linux-s390x-gnu": "npm:0.127.0" + "@oxc-parser/binding-linux-x64-gnu": "npm:0.127.0" + "@oxc-parser/binding-linux-x64-musl": "npm:0.127.0" + "@oxc-parser/binding-openharmony-arm64": "npm:0.127.0" + "@oxc-parser/binding-wasm32-wasi": "npm:0.127.0" + "@oxc-parser/binding-win32-arm64-msvc": "npm:0.127.0" + "@oxc-parser/binding-win32-ia32-msvc": "npm:0.127.0" + "@oxc-parser/binding-win32-x64-msvc": "npm:0.127.0" + "@oxc-project/types": "npm:^0.127.0" + dependenciesMeta: + "@oxc-parser/binding-android-arm-eabi": + optional: true + "@oxc-parser/binding-android-arm64": + optional: true + "@oxc-parser/binding-darwin-arm64": + optional: true + "@oxc-parser/binding-darwin-x64": + optional: true + "@oxc-parser/binding-freebsd-x64": + optional: true + "@oxc-parser/binding-linux-arm-gnueabihf": + optional: true + "@oxc-parser/binding-linux-arm-musleabihf": + optional: true + "@oxc-parser/binding-linux-arm64-gnu": + optional: true + "@oxc-parser/binding-linux-arm64-musl": + optional: true + "@oxc-parser/binding-linux-ppc64-gnu": + optional: true + "@oxc-parser/binding-linux-riscv64-gnu": + optional: true + "@oxc-parser/binding-linux-riscv64-musl": + optional: true + "@oxc-parser/binding-linux-s390x-gnu": + optional: true + "@oxc-parser/binding-linux-x64-gnu": + optional: true + "@oxc-parser/binding-linux-x64-musl": + optional: true + "@oxc-parser/binding-openharmony-arm64": + optional: true + "@oxc-parser/binding-wasm32-wasi": + optional: true + "@oxc-parser/binding-win32-arm64-msvc": + optional: true + "@oxc-parser/binding-win32-ia32-msvc": + optional: true + "@oxc-parser/binding-win32-x64-msvc": + optional: true + checksum: 10c0/9d109fb3a79c0862a36434cc01c8c0e8f6cf5f1efe9369e02d2183fd518479b10262cf092da2e7f8328befae446afa05ccf742ce12f8346d81429c8f2cdf1651 + languageName: node + linkType: hard + +"oxc-resolver@npm:^11.19.1, oxc-resolver@npm:^11.9.0": version: 11.19.1 resolution: "oxc-resolver@npm:11.19.1" dependencies: @@ -18748,30 +19015,33 @@ __metadata: languageName: node linkType: hard -"oxfmt@npm:^0.46.0": - version: 0.46.0 - resolution: "oxfmt@npm:0.46.0" - dependencies: - "@oxfmt/binding-android-arm-eabi": "npm:0.46.0" - "@oxfmt/binding-android-arm64": "npm:0.46.0" - "@oxfmt/binding-darwin-arm64": "npm:0.46.0" - "@oxfmt/binding-darwin-x64": "npm:0.46.0" - "@oxfmt/binding-freebsd-x64": "npm:0.46.0" - "@oxfmt/binding-linux-arm-gnueabihf": "npm:0.46.0" - "@oxfmt/binding-linux-arm-musleabihf": "npm:0.46.0" - "@oxfmt/binding-linux-arm64-gnu": "npm:0.46.0" - "@oxfmt/binding-linux-arm64-musl": "npm:0.46.0" - "@oxfmt/binding-linux-ppc64-gnu": "npm:0.46.0" - "@oxfmt/binding-linux-riscv64-gnu": "npm:0.46.0" - "@oxfmt/binding-linux-riscv64-musl": "npm:0.46.0" - "@oxfmt/binding-linux-s390x-gnu": "npm:0.46.0" - "@oxfmt/binding-linux-x64-gnu": "npm:0.46.0" - "@oxfmt/binding-linux-x64-musl": "npm:0.46.0" - "@oxfmt/binding-openharmony-arm64": "npm:0.46.0" - "@oxfmt/binding-win32-arm64-msvc": "npm:0.46.0" - "@oxfmt/binding-win32-ia32-msvc": "npm:0.46.0" - "@oxfmt/binding-win32-x64-msvc": "npm:0.46.0" +"oxfmt@npm:^0.52.0": + version: 0.52.0 + resolution: "oxfmt@npm:0.52.0" + dependencies: + "@oxfmt/binding-android-arm-eabi": "npm:0.52.0" + "@oxfmt/binding-android-arm64": "npm:0.52.0" + "@oxfmt/binding-darwin-arm64": "npm:0.52.0" + "@oxfmt/binding-darwin-x64": "npm:0.52.0" + "@oxfmt/binding-freebsd-x64": "npm:0.52.0" + "@oxfmt/binding-linux-arm-gnueabihf": "npm:0.52.0" + "@oxfmt/binding-linux-arm-musleabihf": "npm:0.52.0" + "@oxfmt/binding-linux-arm64-gnu": "npm:0.52.0" + "@oxfmt/binding-linux-arm64-musl": "npm:0.52.0" + "@oxfmt/binding-linux-ppc64-gnu": "npm:0.52.0" + "@oxfmt/binding-linux-riscv64-gnu": "npm:0.52.0" + "@oxfmt/binding-linux-riscv64-musl": "npm:0.52.0" + "@oxfmt/binding-linux-s390x-gnu": "npm:0.52.0" + "@oxfmt/binding-linux-x64-gnu": "npm:0.52.0" + "@oxfmt/binding-linux-x64-musl": "npm:0.52.0" + "@oxfmt/binding-openharmony-arm64": "npm:0.52.0" + "@oxfmt/binding-win32-arm64-msvc": "npm:0.52.0" + "@oxfmt/binding-win32-ia32-msvc": "npm:0.52.0" + "@oxfmt/binding-win32-x64-msvc": "npm:0.52.0" tinypool: "npm:2.1.0" + peerDependencies: + svelte: ^5.0.0 + vite-plus: "*" dependenciesMeta: "@oxfmt/binding-android-arm-eabi": optional: true @@ -18811,9 +19081,14 @@ __metadata: optional: true "@oxfmt/binding-win32-x64-msvc": optional: true + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true bin: oxfmt: bin/oxfmt - checksum: 10c0/bc258840802ac8f66a2a24fc7a5b87d668d0da41d3033811aa30583483b1f66299c3648e34edbe45b4c97d061cbbec208a439a498b1a2ccefd94d73b780fef46 + checksum: 10c0/a67a597202e29432f29049a6862feb927b6f996e5e909cb32acb2fe282d4b6d01d2e693c2b8a0ca6d016f236301cdd66c9250fd13af0876aee6c5b0eddba259f languageName: node linkType: hard @@ -19353,27 +19628,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.59.1": - version: 1.59.1 - resolution: "playwright-core@npm:1.59.1" +"playwright-core@npm:1.60.0": + version: 1.60.0 + resolution: "playwright-core@npm:1.60.0" bin: playwright-core: cli.js - checksum: 10c0/d41a74d9681ce3beb3d5239e9ed577710b4ad099a6ca2476219c6599d51e9cb4b80bd72ed82c528da6a5d929c18ae3b872cf02bb83f78fa1c2cb9199c501abee + checksum: 10c0/99ccd43923b6e9355e0723b7fe221e6326efd4687f8dafff951313662aea11db51f542a9c2122c704c445fb9baae1c9ec9fa6f895126bbddd9fe92313f6942c9 languageName: node linkType: hard -"playwright@npm:1.59.1, playwright@npm:^1.59.1": - version: 1.59.1 - resolution: "playwright@npm:1.59.1" +"playwright@npm:1.60.0, playwright@npm:^1.60.0": + version: 1.60.0 + resolution: "playwright@npm:1.60.0" dependencies: fsevents: "npm:2.3.2" - playwright-core: "npm:1.59.1" + playwright-core: "npm:1.60.0" dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 10c0/dfe38396e616e5c4f98825ce90037bb96e477c5a2bd9258a24854f8ce72a8a41427b19098863866f85aa0216e70287dd537c4438d761aca93995e31ae099c533 + checksum: 10c0/714ad76d85b4865d7e43c0012f9039800c1485373388973ed39d79339cee5ad467052d1e2f1eaeca107a1cb6e65342186a8578a4c3504853d84c3a691250d5db languageName: node linkType: hard @@ -19439,67 +19714,67 @@ __metadata: languageName: node linkType: hard -"postcss-colormin@npm:^7.0.7": - version: 7.0.7 - resolution: "postcss-colormin@npm:7.0.7" +"postcss-colormin@npm:^7.0.10": + version: 7.0.10 + resolution: "postcss-colormin@npm:7.0.10" dependencies: - "@colordx/core": "npm:^5.0.0" - browserslist: "npm:^4.28.1" + "@colordx/core": "npm:^5.4.3" + browserslist: "npm:^4.28.2" caniuse-api: "npm:^3.0.0" postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/94b05efc6dd31b88ff7f7cd564f09a7f558343cd31d72aa7172b648623aa1d420168f6e0c37e92bfc406dd51a7561fb75b93f97b6638f7deae1f6b09fb7f6c70 + postcss: ^8.5.13 + checksum: 10c0/2c3b508c72275d668dbda81e2fab994ef70b0a783286f07b5ce0eb325e6386dd4d9018ad35e93c37dc013a5fa50a6e33a26e59e2eb0f506a20300e5fa7e3df94 languageName: node linkType: hard -"postcss-convert-values@npm:^7.0.9": - version: 7.0.9 - resolution: "postcss-convert-values@npm:7.0.9" +"postcss-convert-values@npm:^7.0.12": + version: 7.0.12 + resolution: "postcss-convert-values@npm:7.0.12" dependencies: - browserslist: "npm:^4.28.1" + browserslist: "npm:^4.28.2" postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/b0b4ceaab3b8f9e6f69cfd749f10f537ced0f7525664b310722dc1275de527ce721938d30a8dfe5417d3d879347d647d735b83fc3da2a9292ed3573509c1c8b4 + postcss: ^8.5.13 + checksum: 10c0/b2c3438639cb3f512d98848578556b176f3bd1455f045bdfe0bed5da89b7c2b7461e70311dd476e72354ff78b3ecf520606a55a1254d4989225ae8337d825f50 languageName: node linkType: hard -"postcss-discard-comments@npm:^7.0.6": - version: 7.0.6 - resolution: "postcss-discard-comments@npm:7.0.6" +"postcss-discard-comments@npm:^7.0.8": + version: 7.0.8 + resolution: "postcss-discard-comments@npm:7.0.8" dependencies: postcss-selector-parser: "npm:^7.1.1" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/c299e68eb83a94f4efdfd8ee871de3c5eac481bc5f68557e354269ae4c0f3708eb24db6f952fef892348ecf1d9cc0bcda76ce2db9b9592eef116d92ba9da265a + postcss: ^8.5.13 + checksum: 10c0/872272f26f475d029afe9364124c9a56000220fbf1264f3524413a9a22f31025a55cbacca0fbbbd60c722866911e2d6cffb18498a14e2dd026526711152ab9d0 languageName: node linkType: hard -"postcss-discard-duplicates@npm:^7.0.2": - version: 7.0.2 - resolution: "postcss-discard-duplicates@npm:7.0.2" +"postcss-discard-duplicates@npm:^7.0.4": + version: 7.0.4 + resolution: "postcss-discard-duplicates@npm:7.0.4" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/83035b1158ee0f0c8c6441c9f0fcd3c83027b19c4b1d19802d140ba02535623520edb4d52db40d06881ad2b31a9d859445cf56aeaf0de5183c3edd22eaf7e023 + postcss: ^8.5.13 + checksum: 10c0/2e58e4f9a8ef9cc080f6712dc4ca6d699725d5db6d1eb613215dde6c569c647db92daf91d4c5c861d082ec88916104ffc49162df81a70a5193ec249326ccb1e6 languageName: node linkType: hard -"postcss-discard-empty@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-discard-empty@npm:7.0.1" +"postcss-discard-empty@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-discard-empty@npm:7.0.3" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/c11c5571f573a147db911d2d82b4102eff2930fa1d5cc63c25c2cbd9f496a91a7364075f322b61e0eb9c217fc86f06680deb0fb858a32e29148abd7cb2617f8f + postcss: ^8.5.13 + checksum: 10c0/3be7b5348e32d9f3dba38e6b6045773d1679492c50adbab93dfae59e8fb121f0884c080b3af04298e157fa89d7e387951094d6e648600557ff7920c60dd8048f languageName: node linkType: hard -"postcss-discard-overridden@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-discard-overridden@npm:7.0.1" +"postcss-discard-overridden@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-discard-overridden@npm:7.0.3" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/413c68411f1f3b9ee2a862eca4599f54e6b35a5556af12518032b4f6b3f47c57a6db1cc4565692fb8633b7a1fd26e096f5cd86e50aaf702375d621efbd819d05 + postcss: ^8.5.13 + checksum: 10c0/7797721b03b6b3a270f2b19eaafc9aca14e642cf1c37cf130f60392fa9c0357182bab968dc9006a155ce3c9ca5928aa4069082741cfb69dfca5a0e5827550004 languageName: node linkType: hard @@ -19568,78 +19843,80 @@ __metadata: languageName: node linkType: hard -"postcss-merge-longhand@npm:^7.0.5": - version: 7.0.5 - resolution: "postcss-merge-longhand@npm:7.0.5" +"postcss-merge-longhand@npm:^7.0.7": + version: 7.0.7 + resolution: "postcss-merge-longhand@npm:7.0.7" dependencies: postcss-value-parser: "npm:^4.2.0" - stylehacks: "npm:^7.0.5" + stylehacks: "npm:^7.0.11" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/148fe5fc33f967f6e579a184a4bb82c8e6ffb1d5f720a2c7aa85849a56ee8d23ce3f026d6f6b45a38f63f761fcfafe3b82ac54da7bf080fd58eb743be4c4ce46 + postcss: ^8.5.13 + checksum: 10c0/18808d22a37d1a801a62c89bf9238e36c678ed8f011cb01e57115579f05b7b0cf36ca96cbaca22c544848e7846159a301efb54fe52a0b2940c1a933e928b01f2 languageName: node linkType: hard -"postcss-merge-rules@npm:^7.0.8": - version: 7.0.8 - resolution: "postcss-merge-rules@npm:7.0.8" +"postcss-merge-rules@npm:^7.0.11": + version: 7.0.11 + resolution: "postcss-merge-rules@npm:7.0.11" dependencies: - browserslist: "npm:^4.28.1" + browserslist: "npm:^4.28.2" caniuse-api: "npm:^3.0.0" - cssnano-utils: "npm:^5.0.1" + cssnano-utils: "npm:^5.0.3" postcss-selector-parser: "npm:^7.1.1" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/4476886406c829e5fb0971cc51a8be565c5defe05f3111876b3921105cb5bb8293330b7ede0b3f07e8ccb721c70e1a5a419be23b3652f6519a1b8adafd25787c + postcss: ^8.5.13 + checksum: 10c0/b10d4490cd4ee32b5abaca4b0e32ceaecb868b318075741ceb161f38882422047d1e8f8618b3ed2c21e36a6848a544dfe6f0f440fb6cbc9760502b55acb72201 languageName: node linkType: hard -"postcss-minify-font-values@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-minify-font-values@npm:7.0.1" +"postcss-minify-font-values@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-minify-font-values@npm:7.0.3" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/2327863b0f4c025855ba9bb88951ce92985ce1c64bab24002b5d75f024268c396735af311db7342e8ca5ebc80c18c282d7cb63292c36a457348eda041c5fe197 + postcss: ^8.5.13 + checksum: 10c0/d4a1ff78684566ad7157498861d3d7ba711145fb77a5292baacbdf91762196bafafbffb9f2be14455cbaf3e989da27c193db8d3ed0a32f77f4ff36486377f8cd languageName: node linkType: hard -"postcss-minify-gradients@npm:^7.0.2": - version: 7.0.2 - resolution: "postcss-minify-gradients@npm:7.0.2" +"postcss-minify-gradients@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-minify-gradients@npm:7.0.5" dependencies: - "@colordx/core": "npm:^5.0.0" - cssnano-utils: "npm:^5.0.1" + "@colordx/core": "npm:^5.4.3" + cssnano-utils: "npm:^5.0.3" postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/d8b176de4e694d8c3fa00b800fe82cdb2297a3e5bad8d24d17773186a228bca82e2e02a14efeccb8091773e9e71df2c825f172f118c9952f882505219a942cf6 + postcss: ^8.5.13 + checksum: 10c0/a880cb142cc0da0946627a4992be9aa8c85d1b5c2c8864ab53b4078c9fea53224d50d14890856a2c3ff03b2f945e1638794cfd51928bbf1cf4f4d5b984e6bc13 languageName: node linkType: hard -"postcss-minify-params@npm:^7.0.6": - version: 7.0.6 - resolution: "postcss-minify-params@npm:7.0.6" +"postcss-minify-params@npm:^7.0.9": + version: 7.0.9 + resolution: "postcss-minify-params@npm:7.0.9" dependencies: - browserslist: "npm:^4.28.1" - cssnano-utils: "npm:^5.0.1" + browserslist: "npm:^4.28.2" + cssnano-utils: "npm:^5.0.3" postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/18a8e4f23a3a807f919e9dff103c5315337db5616145c7169b1a4caea50465f57f80c600aceab85d7d0ee2959bdee50fe863dc97d87573bee26cebd064006d79 + postcss: ^8.5.13 + checksum: 10c0/5507d7e33ac4d1d395ba6c5ef84332c21f542b64d7739412a9afb7f36607038f8758a437f6dd53e3e742e3e01256b51394b1cdd78cb98c7f6571641653064687 languageName: node linkType: hard -"postcss-minify-selectors@npm:^7.0.6": - version: 7.0.6 - resolution: "postcss-minify-selectors@npm:7.0.6" +"postcss-minify-selectors@npm:^7.1.2": + version: 7.1.2 + resolution: "postcss-minify-selectors@npm:7.1.2" dependencies: + browserslist: "npm:^4.28.1" + caniuse-api: "npm:^3.0.0" cssesc: "npm:^3.0.0" postcss-selector-parser: "npm:^7.1.1" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/1b598fc5f321e3ca4e73ae5bfc2735b0009eaf7c4b5c27eadb6e4ad7fd72508923dcccd4361fe179cb8d756da77abb017e56ad9878604e7f085a601c638f8918 + postcss: ^8.5.13 + checksum: 10c0/b7be4f2f6b84c8ab3d6ee7524821a8917f15f7c2e78241c1eefc72aba6998e328d8281c723e74a816e2334f8e0110d540b8f61c69d8cf620e20fb33597e52234 languageName: node linkType: hard @@ -19687,136 +19964,136 @@ __metadata: languageName: node linkType: hard -"postcss-normalize-charset@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-normalize-charset@npm:7.0.1" +"postcss-normalize-charset@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-normalize-charset@npm:7.0.3" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/e879ecbd8a2f40b427ac8800c34ad6670fa820838ad27950c34b628e9248ce763433045bb4254f65c02d74825f41377a9cf278f8cdcf7284acbd6a3b33af83fe + postcss: ^8.5.13 + checksum: 10c0/e6b1dfce826a1f0486a8c0c1c9b7a6a0097b92ff94bcfd42897618952454111ded275cdf49a162ef33afe9ba9f7751fa2c5776149f2a361b926433cc355bf914 languageName: node linkType: hard -"postcss-normalize-display-values@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-normalize-display-values@npm:7.0.1" +"postcss-normalize-display-values@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-normalize-display-values@npm:7.0.3" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/00d77846972e5261aebb38594f8999cfb84fe745ec9d3c2a4d8a91a1b6e703f02b0ccc9342e8fd4fa1f3e5e1f85d4aac2446dae898690ef41bc06de95008b975 + postcss: ^8.5.13 + checksum: 10c0/d6a5605828fa452f48173689384c133bd74a7291d5c95aa153bcfd89eb3c1774fa1402400d6c983af6f6e9aab36460b4161aca14925e5c70d00f1c4be28eac9d languageName: node linkType: hard -"postcss-normalize-positions@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-normalize-positions@npm:7.0.1" +"postcss-normalize-positions@npm:^7.0.4": + version: 7.0.4 + resolution: "postcss-normalize-positions@npm:7.0.4" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/00f43f9635905ae11ba04cec9272cfa783b7793058ea8e576cb3cf8ea59df6f7bbdc34fdcba82724aaf789ee1f0697266e7ce98818aeca640889d67906f87f9e + postcss: ^8.5.13 + checksum: 10c0/df03a71357926b063e8df781b5ee6c1b8c6559cf3d2f593f45072a57bef56a69c6ff56d771563123fd8a7c60188ce59b78887cc45f1edf9e88cafee997bf8937 languageName: node linkType: hard -"postcss-normalize-repeat-style@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-normalize-repeat-style@npm:7.0.1" +"postcss-normalize-repeat-style@npm:^7.0.4": + version: 7.0.4 + resolution: "postcss-normalize-repeat-style@npm:7.0.4" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/de4f1350ae979e34e29f7f9e1ade23dcdfdccb4c290889ab45d15935c3af8218858e9fe06fc4af3fe5dc0478d719c7ce7d0d995dd9f786c93d5d3eaa7187d6ed + postcss: ^8.5.13 + checksum: 10c0/68e92d0a7698834bd50262695f774fe7a74610a085c2daf9af89bd9a425c28745d428cccd217d1c3af5026fc2fda0792844451ab18fea3bbaeb2f5894a45af8a languageName: node linkType: hard -"postcss-normalize-string@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-normalize-string@npm:7.0.1" +"postcss-normalize-string@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-normalize-string@npm:7.0.3" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/da3bc2458529544abad32860cd835d27b010a7fb16b121f0b64f44775a332795de0cd1a0280a380f868e4958997bd13a0275aca8e404c835ce120cf8ab69f4db + postcss: ^8.5.13 + checksum: 10c0/8f550d6e0dac27780ecc2df1d99cfb09c7caa5f9aad6865c7b0acc09531838b0d170de02ccaac7f93a3befdfdea960e6804f478d99b5f66f8605ff60c21f6ccf languageName: node linkType: hard -"postcss-normalize-timing-functions@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-normalize-timing-functions@npm:7.0.1" +"postcss-normalize-timing-functions@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-normalize-timing-functions@npm:7.0.3" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/9389555176925bb31428220285b89b8cec2c2669f3ebb8f033463e7356cf1f54d0baaf71ddc097beb7adc418b9d2ea3cc628886fbf8e782c74ddaab4c2290749 + postcss: ^8.5.13 + checksum: 10c0/0774dd96fb6fcde65151c055c667ec56d2d9121510c2bf43fc34653fb062e1bb85eae166f6b0f84ce43004a6960be278b22487b3ba486df08883902270e157e2 languageName: node linkType: hard -"postcss-normalize-unicode@npm:^7.0.6": - version: 7.0.6 - resolution: "postcss-normalize-unicode@npm:7.0.6" +"postcss-normalize-unicode@npm:^7.0.9": + version: 7.0.9 + resolution: "postcss-normalize-unicode@npm:7.0.9" dependencies: - browserslist: "npm:^4.28.1" + browserslist: "npm:^4.28.2" postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/65aa5ac43535287179cf9434bf77c298f7b7d65aebcf050ffb36c340de85dc5bfc67888953e5ed707a069f68abdefe086b2c548fe6c1d7cb9ff63fa012a42581 + postcss: ^8.5.13 + checksum: 10c0/8fa72f4425759eca09d494d69ebe8537c84749dfd3f4222b874e03896a1d3c8712883131e69b30f4c6ebc0fc049c208268d53ff7d8362d4b0f5390f7d4cd3702 languageName: node linkType: hard -"postcss-normalize-url@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-normalize-url@npm:7.0.1" +"postcss-normalize-url@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-normalize-url@npm:7.0.3" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/d04ff170efcc77aef221f20f2a1a783c95564898321521a5940c17cf6cbdfd4f44b005efab77feebfae17873b17a30248c14c6f6166b4dfe382e524d6a3a935b + postcss: ^8.5.13 + checksum: 10c0/5d7f7ec7636a95d86814802469dab9a1c376347b7bdcf0855536a065f096dd13089a74e4b54701d2d029ddafa92846c6d66fe1c6c2d0250df13ab690f9f4b131 languageName: node linkType: hard -"postcss-normalize-whitespace@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-normalize-whitespace@npm:7.0.1" +"postcss-normalize-whitespace@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-normalize-whitespace@npm:7.0.3" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/efbdbe1d0bc1dfed08168f417968f112996c6985efe0ba48137a4811052a65b46ac702b74afbb3110a51515aff67ffe1e139ce9a723e8d8543977e4cc6269911 + postcss: ^8.5.13 + checksum: 10c0/8247e3068cca065113ebac191566a2447cc18b324e1c700e61521d291d8b9389a34caee49e2494110ae55509fac0c39d9321f425f74f5a6c4fe7f8971b9e0f51 languageName: node linkType: hard -"postcss-ordered-values@npm:^7.0.2": - version: 7.0.2 - resolution: "postcss-ordered-values@npm:7.0.2" +"postcss-ordered-values@npm:^7.0.4": + version: 7.0.4 + resolution: "postcss-ordered-values@npm:7.0.4" dependencies: - cssnano-utils: "npm:^5.0.1" + cssnano-utils: "npm:^5.0.3" postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/77e4daa70e120864aac5a0f5c71cc8b66408829eabe45203d4d86c93229425c26e030cf75d6f328432935c28a50c5294108aa2439fa8da256aa1852cc71c84f3 + postcss: ^8.5.13 + checksum: 10c0/f336df91931f8269c1da113c2742fa5f74e2e0c4994bc4829da9beffb7fde60d5f895f4276e66a241901be65587bd6dcf4ac2eacaf37fc6d611c7e470e7f5e8f languageName: node linkType: hard -"postcss-reduce-initial@npm:^7.0.6": - version: 7.0.6 - resolution: "postcss-reduce-initial@npm:7.0.6" +"postcss-reduce-initial@npm:^7.0.9": + version: 7.0.9 + resolution: "postcss-reduce-initial@npm:7.0.9" dependencies: - browserslist: "npm:^4.28.1" + browserslist: "npm:^4.28.2" caniuse-api: "npm:^3.0.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/1ae83ce61e34438ee9fb61082999cd3cc9673e8e5b5a929349b3df317177d6fabca9aac6708823fcab91dcc23e758d30cfd1b393973ca953e1d190a1ff4c976a + postcss: ^8.5.13 + checksum: 10c0/f548fd5be5da0dc656299386985fb32d9bfe654648dedddc290ade3378cca168a3c7ac129e296c91b28150634f1b8933c7d1209b8be3917479522c216d91331e languageName: node linkType: hard -"postcss-reduce-transforms@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-reduce-transforms@npm:7.0.1" +"postcss-reduce-transforms@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-reduce-transforms@npm:7.0.3" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/b379ea1d87ea27f331b472c8a21b4c6bb3c114ea573b66743f6fb4a52cab758c1930cd194df873d347901e347c47035e1353be6cf4250e469ec512f599385957 + postcss: ^8.5.13 + checksum: 10c0/414c7fc44273069fc8b6b51c68d8c0ba3003ac8f9cf3a075382e264781f79ad3228100d616339e6fa01249766e2c7eb79457812e84be931a26ac43929a8608b9 languageName: node linkType: hard @@ -19864,26 +20141,26 @@ __metadata: languageName: node linkType: hard -"postcss-svgo@npm:^7.1.1": - version: 7.1.1 - resolution: "postcss-svgo@npm:7.1.1" +"postcss-svgo@npm:^7.1.3": + version: 7.1.3 + resolution: "postcss-svgo@npm:7.1.3" dependencies: postcss-value-parser: "npm:^4.2.0" svgo: "npm:^4.0.1" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/bcdf06455f527b553727c99e4b5e2d17b7ecf009e7e32184993acea508b8afcb7d5f39bc2b257f4524dadc2ec406f905671915a9f8fd3f12ffd9fdfc000f45f9 + postcss: ^8.5.13 + checksum: 10c0/3df4771966dc11e1eb4171a152bd8219f246fc6b6a740139b437b84b12c14063064918e64dcc2e47161ca1a4a968787aeb28b9a26108bee86d981e7686a8f804 languageName: node linkType: hard -"postcss-unique-selectors@npm:^7.0.5": - version: 7.0.5 - resolution: "postcss-unique-selectors@npm:7.0.5" +"postcss-unique-selectors@npm:^7.0.7": + version: 7.0.7 + resolution: "postcss-unique-selectors@npm:7.0.7" dependencies: postcss-selector-parser: "npm:^7.1.1" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/29982666bc2cb6c889ecd8a3bc96b2a4329861174904873e54d74f8af5d99df273b2fd3964ae62d4290b64fd8e8c03df236a1ed0c5c7c5410f51cceab3621496 + postcss: ^8.5.13 + checksum: 10c0/9e71642b83d517472ffbdf2a89efbc5b5ea680d680973915003d04a0bc4dfc11bfbc4cf221fe439e8e2a1b14aac7cd256a6e900af5370429c8eebccd6abaaff6 languageName: node linkType: hard @@ -19901,25 +20178,25 @@ __metadata: languageName: node linkType: hard -"postcss@npm:8.5.6": - version: 8.5.6 - resolution: "postcss@npm:8.5.6" +"postcss@npm:8.5.12": + version: 8.5.12 + resolution: "postcss@npm:8.5.12" dependencies: nanoid: "npm:^3.3.11" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10c0/5127cc7c91ed7a133a1b7318012d8bfa112da9ef092dddf369ae699a1f10ebbd89b1b9f25f3228795b84585c72aabd5ced5fc11f2ba467eedf7b081a66fad024 + checksum: 10c0/5baebaf574c567bc1b3d61197f38af4ce5920b8f611c887fb6bc3dcc14af00253c169dbf19897bc889cce0b0d9818ab5eb4ea0caedf02b0bab10da8a43ce8c12 languageName: node linkType: hard -"postcss@npm:^8.2.14, postcss@npm:^8.4.40, postcss@npm:^8.4.47, postcss@npm:^8.4.49, postcss@npm:^8.5.3, postcss@npm:^8.5.6, postcss@npm:^8.5.8": - version: 8.5.8 - resolution: "postcss@npm:8.5.8" +"postcss@npm:^8.2.14, postcss@npm:^8.4.40, postcss@npm:^8.4.47, postcss@npm:^8.4.49, postcss@npm:^8.5.15, postcss@npm:^8.5.3, postcss@npm:^8.5.6": + version: 8.5.15 + resolution: "postcss@npm:8.5.15" dependencies: - nanoid: "npm:^3.3.11" + nanoid: "npm:^3.3.12" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10c0/dd918f7127ee7c60a0295bae2e72b3787892296e1d1c3c564d7a2a00c68d8df83cadc3178491259daa19ccc54804fb71ed8c937c6787e08d8bd4bedf8d17044c + checksum: 10c0/7f2e63ae22fbe43aace1bf652bd99da4e90737c64194d49e51ddc9cd0f9e51ff2861a7d734379b494deffa03a880a5c65eec70bc29ee9ebaa7136dde3eee8f31 languageName: node linkType: hard @@ -21031,27 +21308,25 @@ __metadata: languageName: node linkType: hard -"rolldown@npm:1.0.0-rc.15": - version: 1.0.0-rc.15 - resolution: "rolldown@npm:1.0.0-rc.15" - dependencies: - "@oxc-project/types": "npm:=0.124.0" - "@rolldown/binding-android-arm64": "npm:1.0.0-rc.15" - "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.15" - "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.15" - "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.15" - "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.15" - "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.15" - "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.15" - "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.15" - "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.15" - "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.15" - "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.15" - "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.15" - "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.15" - "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.15" - "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.15" - "@rolldown/pluginutils": "npm:1.0.0-rc.15" +"rolldown@npm:1.0.0-rc.4": + version: 1.0.0-rc.4 + resolution: "rolldown@npm:1.0.0-rc.4" + dependencies: + "@oxc-project/types": "npm:=0.113.0" + "@rolldown/binding-android-arm64": "npm:1.0.0-rc.4" + "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.4" + "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.4" + "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.4" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.4" + "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.4" + "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.4" + "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.4" + "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.4" + "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.4" + "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.4" + "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.4" + "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.4" + "@rolldown/pluginutils": "npm:1.0.0-rc.4" dependenciesMeta: "@rolldown/binding-android-arm64": optional: true @@ -21067,10 +21342,6 @@ __metadata: optional: true "@rolldown/binding-linux-arm64-musl": optional: true - "@rolldown/binding-linux-ppc64-gnu": - optional: true - "@rolldown/binding-linux-s390x-gnu": - optional: true "@rolldown/binding-linux-x64-gnu": optional: true "@rolldown/binding-linux-x64-musl": @@ -21085,29 +21356,31 @@ __metadata: optional: true bin: rolldown: bin/cli.mjs - checksum: 10c0/95df21125dafd2a0ce6ae9a89d926540e47900684023126c84632e18123371020da8f6b3235a188c45af0e4f9a5b963235de33bd9658ee5db9f3ff5862200eed + checksum: 10c0/1336aedae71b5885036aeff6bbe9d6c58af205f72e8686edb2a3b27e6b4eb056f2f528b50373ad91567696369501f5b18eb99a926343bdb3012b7495962ddcfb languageName: node linkType: hard -"rolldown@npm:1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "rolldown@npm:1.0.0-rc.4" - dependencies: - "@oxc-project/types": "npm:=0.113.0" - "@rolldown/binding-android-arm64": "npm:1.0.0-rc.4" - "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.4" - "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.4" - "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.4" - "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.4" - "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.4" - "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.4" - "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.4" - "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.4" - "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.4" - "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.4" - "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.4" - "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.4" - "@rolldown/pluginutils": "npm:1.0.0-rc.4" +"rolldown@npm:1.0.2": + version: 1.0.2 + resolution: "rolldown@npm:1.0.2" + dependencies: + "@oxc-project/types": "npm:=0.132.0" + "@rolldown/binding-android-arm64": "npm:1.0.2" + "@rolldown/binding-darwin-arm64": "npm:1.0.2" + "@rolldown/binding-darwin-x64": "npm:1.0.2" + "@rolldown/binding-freebsd-x64": "npm:1.0.2" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.2" + "@rolldown/binding-linux-arm64-gnu": "npm:1.0.2" + "@rolldown/binding-linux-arm64-musl": "npm:1.0.2" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.2" + "@rolldown/binding-linux-s390x-gnu": "npm:1.0.2" + "@rolldown/binding-linux-x64-gnu": "npm:1.0.2" + "@rolldown/binding-linux-x64-musl": "npm:1.0.2" + "@rolldown/binding-openharmony-arm64": "npm:1.0.2" + "@rolldown/binding-wasm32-wasi": "npm:1.0.2" + "@rolldown/binding-win32-arm64-msvc": "npm:1.0.2" + "@rolldown/binding-win32-x64-msvc": "npm:1.0.2" + "@rolldown/pluginutils": "npm:^1.0.0" dependenciesMeta: "@rolldown/binding-android-arm64": optional: true @@ -21123,6 +21396,10 @@ __metadata: optional: true "@rolldown/binding-linux-arm64-musl": optional: true + "@rolldown/binding-linux-ppc64-gnu": + optional: true + "@rolldown/binding-linux-s390x-gnu": + optional: true "@rolldown/binding-linux-x64-gnu": optional: true "@rolldown/binding-linux-x64-musl": @@ -21136,8 +21413,8 @@ __metadata: "@rolldown/binding-win32-x64-msvc": optional: true bin: - rolldown: bin/cli.mjs - checksum: 10c0/1336aedae71b5885036aeff6bbe9d6c58af205f72e8686edb2a3b27e6b4eb056f2f528b50373ad91567696369501f5b18eb99a926343bdb3012b7495962ddcfb + rolldown: ./bin/cli.mjs + checksum: 10c0/628327a6e3122c0b62880f1c87d54095394e5138a6af2e6e7b2f67ef4c4b11f1421db68c9a5bb4e1be161465a863ab4f68f15076ce895cd4bb3d0ba18a3b20b1 languageName: node linkType: hard @@ -21438,12 +21715,12 @@ __metadata: languageName: node linkType: hard -"sass@npm:^1.81.0, sass@npm:^1.99.0": - version: 1.99.0 - resolution: "sass@npm:1.99.0" +"sass@npm:^1.100.0, sass@npm:^1.81.0": + version: 1.100.0 + resolution: "sass@npm:1.100.0" dependencies: "@parcel/watcher": "npm:^2.4.1" - chokidar: "npm:^4.0.0" + chokidar: "npm:^5.0.0" immutable: "npm:^5.1.5" source-map-js: "npm:>=0.6.2 <2.0.0" dependenciesMeta: @@ -21451,7 +21728,7 @@ __metadata: optional: true bin: sass: sass.js - checksum: 10c0/83c54a8c6decb79fff50dd9500d7932cf1cb7c5d9be4bc42bd3d537402c37bbee062aea6efdbdf9fb0b8697b18177d60c72bf101872336b93b1c27a2dc3621e1 + checksum: 10c0/e2aab47c87b69d2d4f8e48fa665138548069f56a7fd0fc4e15c9bde888b715798e49d33436e873918a8849ca3cc6c141a68618f58e2f3b2e6ec179cc309ca622 languageName: node linkType: hard @@ -21904,16 +22181,15 @@ __metadata: languageName: node linkType: hard -"sinon@npm:^21.0.3": - version: 21.0.3 - resolution: "sinon@npm:21.0.3" +"sinon@npm:^21.1.2": + version: 21.1.2 + resolution: "sinon@npm:21.1.2" dependencies: "@sinonjs/commons": "npm:^3.0.1" - "@sinonjs/fake-timers": "npm:^15.1.1" - "@sinonjs/samsam": "npm:^9.0.3" - diff: "npm:^8.0.3" - supports-color: "npm:^7.2.0" - checksum: 10c0/06418f39b577d1ad85475f2b7715d4c954ff807fb0d2d3c87fd3540a46bb82e032945eb6e2fe49dc8c099c934086e717ff1779c5001a8032eb16420c09cdacd2 + "@sinonjs/fake-timers": "npm:^15.3.2" + "@sinonjs/samsam": "npm:^10.0.2" + diff: "npm:^8.0.4" + checksum: 10c0/d33aaf68f9bd78ad32f68ace36b7437e1e1f93ea341c4c2fd814935b3bf6788169ebd6258c8bce195d3adb13198814f28048470d80fa239cf2ffbf50dfd3082b languageName: node linkType: hard @@ -21970,6 +22246,16 @@ __metadata: languageName: node linkType: hard +"slice-ansi@npm:^8.0.0": + version: 8.0.0 + resolution: "slice-ansi@npm:8.0.0" + dependencies: + ansi-styles: "npm:^6.2.3" + is-fullwidth-code-point: "npm:^5.1.0" + checksum: 10c0/0ce4aa91febb7cea4a00c2c27bb820fa53b6d2862ce0f80f7120134719f7914fc416b0ed966cf35250a3169e152916392f35917a2d7cad0fcc5d8b841010fa9a + languageName: node + linkType: hard + "slugify@npm:^1.6.6, slugify@npm:^1.6.8": version: 1.6.8 resolution: "slugify@npm:1.6.8" @@ -22357,12 +22643,12 @@ __metadata: languageName: node linkType: hard -"storybook-addon-pseudo-states@npm:^10.3.5": - version: 10.3.5 - resolution: "storybook-addon-pseudo-states@npm:10.3.5" +"storybook-addon-pseudo-states@npm:^10.4.1": + version: 10.4.1 + resolution: "storybook-addon-pseudo-states@npm:10.4.1" peerDependencies: - storybook: ^10.3.5 - checksum: 10c0/19750dd0bd6c6fa44754214f6da2d9be66f60a5f2dcc95d71f26e11e97ccf0fd23e9c2577fa3e618fe5d380900f3f71ef90f7a64905ee1a7329fddc98a269bf2 + storybook: ^10.4.1 + checksum: 10c0/cb355ecc2c8b15e3e62efed7b89a9b7b439dca263e043106bf440aedb524f5c8cc477aa891b8cb47084828549382d64cf2a52afec10b457b53c31ed99c7e2fe4 languageName: node linkType: hard @@ -22377,12 +22663,12 @@ __metadata: languageName: node linkType: hard -"storybook@npm:^10.3.5": - version: 10.3.5 - resolution: "storybook@npm:10.3.5" +"storybook@npm:^10.4.1": + version: 10.4.1 + resolution: "storybook@npm:10.4.1" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^2.0.1" + "@storybook/icons": "npm:^2.0.2" "@testing-library/jest-dom": "npm:^6.9.1" "@testing-library/user-event": "npm:^14.6.1" "@vitest/expect": "npm:3.2.4" @@ -22390,18 +22676,26 @@ __metadata: "@webcontainer/env": "npm:^1.1.1" esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0" open: "npm:^10.2.0" + oxc-parser: "npm:^0.127.0" + oxc-resolver: "npm:^11.19.1" recast: "npm:^0.23.5" semver: "npm:^7.7.3" use-sync-external-store: "npm:^1.5.0" ws: "npm:^8.18.0" peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 prettier: ^2 || ^3 + vite-plus: ^0.1.15 peerDependenciesMeta: + "@types/react": + optional: true prettier: optional: true + vite-plus: + optional: true bin: storybook: ./dist/bin/dispatcher.js - checksum: 10c0/1443e4710b0bb972db7704d8445c039a6335afafebe853d2b0d89b161262050cd7ae5eda3811c771d54d832f0acc80cf2c231d24f73d1f547d020898394afde6 + checksum: 10c0/c63daf0c48d8379e665dcbfccdb6a3cfc9994a58e084ee56bbdf3371216f85dc4959766b6cd01db4a8ee9e3beda0ae6a420fbf28d802f6e62ee0e830692fc1a7 languageName: node linkType: hard @@ -22479,13 +22773,13 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^8.0.0, string-width@npm:^8.1.0": - version: 8.1.0 - resolution: "string-width@npm:8.1.0" +"string-width@npm:^8.1.0, string-width@npm:^8.2.0": + version: 8.2.1 + resolution: "string-width@npm:8.2.1" dependencies: - get-east-asian-width: "npm:^1.3.0" - strip-ansi: "npm:^7.1.0" - checksum: 10c0/749b5d0dab2532b4b6b801064230f4da850f57b3891287023117ab63a464ad79dd208f42f793458f48f3ad121fe2e1f01dd525ff27ead957ed9f205e27406593 + get-east-asian-width: "npm:^1.5.0" + strip-ansi: "npm:^7.1.2" + checksum: 10c0/d467b4eaf4c40a01bb438a2620e77badd2456ffd5131c9973abe4f3acf7c802d5b21f3b6a00a5e33a7fc28ca8f9c103226e01bac61e9f259659c6f46d78e353a languageName: node linkType: hard @@ -22585,12 +22879,12 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0": - version: 7.1.2 - resolution: "strip-ansi@npm:7.1.2" +"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0, strip-ansi@npm:^7.1.2": + version: 7.2.0 + resolution: "strip-ansi@npm:7.2.0" dependencies: - ansi-regex: "npm:^6.0.1" - checksum: 10c0/0d6d7a023de33368fd042aab0bf48f4f4077abdfd60e5393e73c7c411e85e1b3a83507c11af2e656188511475776215df9ca589b4da2295c9455cc399ce1858b + ansi-regex: "npm:^6.2.2" + checksum: 10c0/544d13b7582f8254811ea97db202f519e189e59d35740c46095897e254e4f1aa9fe1524a83ad6bc5ad67d4dd6c0281d2e0219ed62b880a6238a16a17d375f221 languageName: node linkType: hard @@ -22669,15 +22963,15 @@ __metadata: languageName: node linkType: hard -"stylehacks@npm:^7.0.5": - version: 7.0.5 - resolution: "stylehacks@npm:7.0.5" +"stylehacks@npm:^7.0.11": + version: 7.0.11 + resolution: "stylehacks@npm:7.0.11" dependencies: - browserslist: "npm:^4.24.5" - postcss-selector-parser: "npm:^7.1.0" + browserslist: "npm:^4.28.2" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: - postcss: ^8.4.32 - checksum: 10c0/66a15cbbac00b15ee68d01bdaf8b044c8e4e9e13fc27a6971d4ec39f09553769bf1e11245abe21393b8fead66255cf2e03d84265e3ee265bd6183eb499f8774a + postcss: ^8.5.13 + checksum: 10c0/6464a08f45ae720b6e6b6c4a04a0c45d6297b5dbcc3432afd79c2e40c868f3ae62046b0faddf712189ad0ea6f0262bb99b828f830e6c9225ee28a6c58f9e4f06 languageName: node linkType: hard @@ -22830,7 +23124,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0, supports-color@npm:^7.2.0": +"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" dependencies: @@ -23037,9 +23331,9 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:^5.3.14, terser-webpack-plugin@npm:^5.3.16": - version: 5.3.17 - resolution: "terser-webpack-plugin@npm:5.3.17" +"terser-webpack-plugin@npm:^5.3.16, terser-webpack-plugin@npm:^5.3.17": + version: 5.6.0 + resolution: "terser-webpack-plugin@npm:5.6.0" dependencies: "@jridgewell/trace-mapping": "npm:^0.3.25" jest-worker: "npm:^27.4.5" @@ -23048,13 +23342,31 @@ __metadata: peerDependencies: webpack: ^5.1.0 peerDependenciesMeta: + "@minify-html/node": + optional: true "@swc/core": optional: true + "@swc/css": + optional: true + "@swc/html": + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true esbuild: optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true uglify-js: optional: true - checksum: 10c0/bfe08fbb3e5e5a8b2525dcc1705c370ca67f218051f0df241f86531ab0f1a93d5b290176ba09cff28cd5f774836684a7e436421d0641c0f4dfd07110d8d907bf + checksum: 10c0/191882a727d571291df49b11bdcfa7459aa78e96c542a993d66f70df052404e3b30157708a80c2895bbe2de4860217c2addcf15d5b2321df8f0aa0de2191f64f languageName: node linkType: hard @@ -23128,14 +23440,14 @@ __metadata: languageName: node linkType: hard -"tinyexec@npm:^1.0.2, tinyexec@npm:^1.0.4": - version: 1.0.4 - resolution: "tinyexec@npm:1.0.4" - checksum: 10c0/d4a5bbcf6bdb23527a4b74c4aa566f41432167112fe76f420ec7e3a90a3ecfd3a7d944383e2719fc3987b69400f7b928daf08700d145fb527c2e80ec01e198bd +"tinyexec@npm:^1.0.2, tinyexec@npm:^1.1.2": + version: 1.2.2 + resolution: "tinyexec@npm:1.2.2" + checksum: 10c0/8bcb4969c572c21d570c033e29cb896e26d96e49e58f4fe07a532d3d65e10bdfae59733bf8a6a0fd9b611543c4ed3b890c939c3234489599296fb92515eb4625 languageName: node linkType: hard -"tinyglobby@npm:0.2.15, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": +"tinyglobby@npm:0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -23145,6 +23457,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.16": + version: 0.2.16 + resolution: "tinyglobby@npm:0.2.16" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.4" + checksum: 10c0/f2e09fd93dd95c41e522113b686ff6f7c13020962f8698a864a257f3d7737599afc47722b7ab726e12f8a813f779906187911ff8ee6701ede65072671a7e934b + languageName: node + linkType: hard + "tinypool@npm:2.1.0": version: 2.1.0 resolution: "tinypool@npm:2.1.0" @@ -23541,13 +23863,13 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^6.0.2, typescript@npm:~6.0.2": - version: 6.0.2 - resolution: "typescript@npm:6.0.2" +"typescript@npm:^6.0.3, typescript@npm:~6.0.3": + version: 6.0.3 + resolution: "typescript@npm:6.0.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/4b860b0bf87cc0fee0f66d8ef2640b5a8a8a8c74d1129adb82e389e5f97124383823c47946bef8a73ede371461143a3aa8544399d2133c7b2e4f07e81860af7f + checksum: 10c0/4a25ff5045b984370f48f196b3a0120779b1b343d40b9a68d114ea5e5fff099809b2bb777576991a63a5cd59cf7bffd96ff6fe10afcefbcb8bd6fb96ad4b6606 languageName: node linkType: hard @@ -23581,13 +23903,13 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^6.0.2#optional!builtin, typescript@patch:typescript@npm%3A~6.0.2#optional!builtin": - version: 6.0.2 - resolution: "typescript@patch:typescript@npm%3A6.0.2#optional!builtin::version=6.0.2&hash=5786d5" +"typescript@patch:typescript@npm%3A^6.0.3#optional!builtin, typescript@patch:typescript@npm%3A~6.0.3#optional!builtin": + version: 6.0.3 + resolution: "typescript@patch:typescript@npm%3A6.0.3#optional!builtin::version=6.0.3&hash=5786d5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/49f0b84fc6ca55653e77752b8a61beabc09ee3dae5d965c31596225aa6ef213c5727b1d2e895b900416dc603854ba0872ac4a812c2a4ed6793a601f9c675de02 + checksum: 10c0/2f25c74e65663c248fa1ade2b8459d9ce5372ff9dad07067310f132966ebec1d93f6c42f0baf77a6b6a7a91460463f708e6887013aaade22111037457c6b25df languageName: node linkType: hard @@ -23879,7 +24201,7 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.2.0": +"update-browserslist-db@npm:^1.2.3": version: 1.2.3 resolution: "update-browserslist-db@npm:1.2.3" dependencies: @@ -24054,9 +24376,9 @@ __metadata: languageName: node linkType: hard -"vite@npm:7.3.1": - version: 7.3.1 - resolution: "vite@npm:7.3.1" +"vite@npm:7.3.2": + version: 7.3.2 + resolution: "vite@npm:7.3.2" dependencies: esbuild: "npm:^0.27.0" fdir: "npm:^6.5.0" @@ -24105,23 +24427,23 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/5c7548f5f43a23533e53324304db4ad85f1896b1bfd3ee32ae9b866bac2933782c77b350eb2b52a02c625c8ad1ddd4c000df077419410650c982cd97fde8d014 + checksum: 10c0/74be36907e208916f18bfec81c8eba18b869f0a170f1ece0a4dcb14874d0f0e7c022fb6c2ad896e3ee6c973fe88f53ac23b4078879ada340d8b263260868b8d4 languageName: node linkType: hard -"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0, vite@npm:^8.0.8": - version: 8.0.8 - resolution: "vite@npm:8.0.8" +"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0, vite@npm:^8.0.14": + version: 8.0.14 + resolution: "vite@npm:8.0.14" dependencies: fsevents: "npm:~2.3.3" lightningcss: "npm:^1.32.0" picomatch: "npm:^4.0.4" - postcss: "npm:^8.5.8" - rolldown: "npm:1.0.0-rc.15" - tinyglobby: "npm:^0.2.15" + postcss: "npm:^8.5.15" + rolldown: "npm:1.0.2" + tinyglobby: "npm:^0.2.16" peerDependencies: "@types/node": ^20.19.0 || >=22.12.0 - "@vitejs/devtools": ^0.1.0 + "@vitejs/devtools": ^0.1.18 esbuild: ^0.27.0 || ^0.28.0 jiti: ">=1.21.0" less: ^4.0.0 @@ -24162,21 +24484,21 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/63474b399612ccf087d0aa025d7eb5c0d675012b6257b7f64332ff39579d4af4d5d7f0ac330906fc99b101abbf592c756adf143bb5748a02aec08f7d3639054d + checksum: 10c0/1ff99b4daadc64aed5f9e40387ecf39fd3bca45c1a5c4fa4aa82197de901930f0507af8d75c54715e2744c99575913947efb625653a78ef6df3997c5613970bd languageName: node linkType: hard -"vitest@npm:^4.1.4, vitest@npm:~4.1.4": - version: 4.1.4 - resolution: "vitest@npm:4.1.4" - dependencies: - "@vitest/expect": "npm:4.1.4" - "@vitest/mocker": "npm:4.1.4" - "@vitest/pretty-format": "npm:4.1.4" - "@vitest/runner": "npm:4.1.4" - "@vitest/snapshot": "npm:4.1.4" - "@vitest/spy": "npm:4.1.4" - "@vitest/utils": "npm:4.1.4" +"vitest@npm:^4.1.7, vitest@npm:~4.1.7": + version: 4.1.7 + resolution: "vitest@npm:4.1.7" + dependencies: + "@vitest/expect": "npm:4.1.7" + "@vitest/mocker": "npm:4.1.7" + "@vitest/pretty-format": "npm:4.1.7" + "@vitest/runner": "npm:4.1.7" + "@vitest/snapshot": "npm:4.1.7" + "@vitest/spy": "npm:4.1.7" + "@vitest/utils": "npm:4.1.7" es-module-lexer: "npm:^2.0.0" expect-type: "npm:^1.3.0" magic-string: "npm:^0.30.21" @@ -24194,12 +24516,12 @@ __metadata: "@edge-runtime/vm": "*" "@opentelemetry/api": ^1.9.0 "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.1.4 - "@vitest/browser-preview": 4.1.4 - "@vitest/browser-webdriverio": 4.1.4 - "@vitest/coverage-istanbul": 4.1.4 - "@vitest/coverage-v8": 4.1.4 - "@vitest/ui": 4.1.4 + "@vitest/browser-playwright": 4.1.7 + "@vitest/browser-preview": 4.1.7 + "@vitest/browser-webdriverio": 4.1.7 + "@vitest/coverage-istanbul": 4.1.7 + "@vitest/coverage-v8": 4.1.7 + "@vitest/ui": 4.1.7 happy-dom: "*" jsdom: "*" vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -24230,7 +24552,7 @@ __metadata: optional: false bin: vitest: vitest.mjs - checksum: 10c0/a85288778cf6a6f0222aaac547fc84f917565ba78d1e32df4693226ec93aa8675f549b246b70913e9f1d80a87830b39843f9bd96b39d270e599ff4f71def6260 + checksum: 10c0/5328eab211161bdb854159154b02d7b2beab0cf1e26a1c13f6a64b0f1402029d41f19987cf60684051c09a6925030285195ecbe57271c2033e1d4f7a666590d0 languageName: node linkType: hard @@ -24685,6 +25007,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^10.0.0": + version: 10.0.0 + resolution: "wrap-ansi@npm:10.0.0" + dependencies: + ansi-styles: "npm:^6.2.3" + string-width: "npm:^8.2.0" + strip-ansi: "npm:^7.1.2" + checksum: 10c0/6b163457630fe6d1c72aeed283a7410b2cc7487312df8b0ce96df3fbd64a2a7c948856ea97c25148c848627587c5c7945be474d8e723ab6011bb0756a53a9e89 + languageName: node + linkType: hard + "wrap-ansi@npm:^6.2.0": version: 6.2.0 resolution: "wrap-ansi@npm:6.2.0" @@ -24827,12 +25160,12 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.8.2": - version: 2.8.2 - resolution: "yaml@npm:2.8.2" +"yaml@npm:^2.8.4": + version: 2.9.0 + resolution: "yaml@npm:2.9.0" bin: yaml: bin.mjs - checksum: 10c0/703e4dc1e34b324aa66876d63618dcacb9ed49f7e7fe9b70f1e703645be8d640f68ab84f12b86df8ac960bac37acf5513e115de7c970940617ce0343c8c9cd96 + checksum: 10c0/f340718df45e97a9551b9bf9dac61c80050bc464513b710debfb5067c380c8472e3b67809cffacb4ab5ffb5e66ef9310816c88b05f371cec60abfedd8c88e0a2 languageName: node linkType: hard @@ -24980,10 +25313,10 @@ __metadata: languageName: node linkType: hard -"zone.js@npm:~0.16.1": - version: 0.16.1 - resolution: "zone.js@npm:0.16.1" - checksum: 10c0/6f3638310e89697b0a21a4e7282b2505f26fb32dabb6ca1f09b721279e8512436efff199db0c7e3c7b7d11400dd42c88fa5cffc85984db56b9ac497a0050fe11 +"zone.js@npm:~0.16.2": + version: 0.16.2 + resolution: "zone.js@npm:0.16.2" + checksum: 10c0/bc84f2257335881f274a4522d9a3a1b21ef02ce42bbc8322751cd4b40636bfe19af48eb08053f3864f30d78549e0893d6bb336bf7123fc06020da70e6d89db8e languageName: node linkType: hard From c023493827b5fcd16c9de35a0bf6e35a74f8ae43 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Tue, 26 May 2026 10:50:22 +0200 Subject: [PATCH 11/50] =?UTF-8?q?=F0=9F=93=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/breadcrumbs/src/breadcrumbs.ts | 30 ++++++------ .../ellipsize-text/src/ellipsize-text.scss | 4 ++ .../ellipsize-text/src/ellipsize-text.ts | 42 +++++------------ packages/components/grid/src/grid.spec.ts | 33 +++++-------- packages/components/tag/src/tag.ts | 38 ++++----------- packages/components/tooltip/src/tooltip.scss | 10 ++-- packages/components/tooltip/src/tooltip.ts | 46 ++++++++++++------- 7 files changed, 83 insertions(+), 120 deletions(-) diff --git a/packages/components/breadcrumbs/src/breadcrumbs.ts b/packages/components/breadcrumbs/src/breadcrumbs.ts index 73cd327267..625f593f62 100644 --- a/packages/components/breadcrumbs/src/breadcrumbs.ts +++ b/packages/components/breadcrumbs/src/breadcrumbs.ts @@ -210,8 +210,7 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { isMobile() || this.hideHomeLabel ? msg('Home', { id: 'sl.breadcrumbs.home' }) : undefined - )} - > + )}> ${isMobile() || this.hideHomeLabel ? '' @@ -230,8 +229,7 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { aria-label=${msg('More breadcrumbs', { id: 'sl.breadcrumbs.moreBreadcrumbs' })} fill="ghost" id="button" - variant=${ifDefined(this.inverted ? 'inverted' : undefined)} - > + variant=${ifDefined(this.inverted ? 'inverted' : undefined)}>
@@ -360,18 +358,18 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { if (link.hasAttribute('data-has-tooltip')) { return; } else { - const cleanup = Tooltip.lazy( - link, - tooltip => { - tooltip.position = 'bottom'; - tooltip.textContent = link.textContent?.trim() || ''; - requestAnimationFrame(() => { - tooltipsSlot.assign(...tooltipsSlot.assignedElements(), tooltip); - }); - }, - { context: this.shadowRoot! } - ); - this.#tooltipCleanupFunctions.set(link, cleanup); + // const cleanup = Tooltip.lazy( + // link, + // tooltip => { + // tooltip.position = 'bottom'; + // tooltip.textContent = link.textContent?.trim() || ''; + // requestAnimationFrame(() => { + // tooltipsSlot.assign(...tooltipsSlot.assignedElements(), tooltip); + // }); + // }, + // { context: this.shadowRoot! } + // ); + // this.#tooltipCleanupFunctions.set(link, cleanup); link.dataset['hasTooltip'] = 'true'; } } else if (link.hasAttribute('data-has-tooltip') && link.hasAttribute('aria-describedby')) { diff --git a/packages/components/ellipsize-text/src/ellipsize-text.scss b/packages/components/ellipsize-text/src/ellipsize-text.scss index 742f8350dd..2663f87b7b 100644 --- a/packages/components/ellipsize-text/src/ellipsize-text.scss +++ b/packages/components/ellipsize-text/src/ellipsize-text.scss @@ -1,5 +1,9 @@ :host { display: block; +} + +slot { + display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; diff --git a/packages/components/ellipsize-text/src/ellipsize-text.ts b/packages/components/ellipsize-text/src/ellipsize-text.ts index 1034c4e892..481ac33950 100644 --- a/packages/components/ellipsize-text/src/ellipsize-text.ts +++ b/packages/components/ellipsize-text/src/ellipsize-text.ts @@ -3,7 +3,8 @@ import { ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; import { Tooltip } from '@sl-design-system/tooltip'; -import { type CSSResultGroup, LitElement, type TemplateResult, html } from 'lit'; +import { type CSSResultGroup, LitElement, type TemplateResult, html, nothing } from 'lit'; +import { state } from 'lit/decorators.js'; import styles from './ellipsize-text.scss.js'; declare global { @@ -30,8 +31,8 @@ export class EllipsizeText extends ScopedElementsMixin(LitElement) { /** Observe size changes. */ #observer = new ResizeObserver(() => this.#onResize()); - /** The lazy tooltip. */ - #tooltip?: Tooltip | (() => void); + /** @internal Whether the tooltip is visible. */ + @state() tooltip?: boolean; override connectedCallback(): void { super.connectedCallback(); @@ -42,40 +43,19 @@ export class EllipsizeText extends ScopedElementsMixin(LitElement) { override disconnectedCallback(): void { this.#observer.disconnect(); - if (this.#tooltip instanceof Tooltip) { - this.#tooltip.remove(); - } else if (this.#tooltip) { - this.#tooltip(); - } - - this.#tooltip = undefined; - super.disconnectedCallback(); } override render(): TemplateResult { - return html``; + return html` + + ${this.tooltip + ? html`${this.textContent?.trim()}` + : nothing} + `; } #onResize(): void { - if (this.offsetWidth < this.scrollWidth) { - this.#tooltip ||= Tooltip.lazy( - this, - tooltip => { - this.#tooltip = tooltip; - tooltip.position = 'bottom'; - tooltip.textContent = this.textContent?.trim() || ''; - }, - { context: this.shadowRoot! } - ); - } else if (this.#tooltip instanceof Tooltip) { - this.removeAttribute('aria-describedby'); - - this.#tooltip.remove(); - this.#tooltip = undefined; - } else if (this.#tooltip) { - this.#tooltip(); - this.#tooltip = undefined; - } + this.tooltip = this.offsetWidth < this.scrollWidth; } } diff --git a/packages/components/grid/src/grid.spec.ts b/packages/components/grid/src/grid.spec.ts index 222e125121..71e64b1c8a 100644 --- a/packages/components/grid/src/grid.spec.ts +++ b/packages/components/grid/src/grid.spec.ts @@ -2,7 +2,7 @@ import '@sl-design-system/button/register.js'; import '@sl-design-system/menu/register.js'; import { isPopoverOpen } from '@sl-design-system/shared'; import { type ToolBar } from '@sl-design-system/tool-bar'; -import { Tooltip, tooltip } from '@sl-design-system/tooltip'; +import { Tooltip } from '@sl-design-system/tooltip'; import '@sl-design-system/tooltip/register.js'; import { fixture } from '@sl-design-system/vitest-browser-lit'; import { html } from 'lit'; @@ -55,8 +55,7 @@ describe('sl-grid', () => { .items=${[ { firstName: 'John', lastName: 'Doe' }, { firstName: 'Jane', lastName: 'Smith' } - ]} - > + ]}> @@ -191,12 +190,11 @@ describe('sl-grid', () => { it('should show a lazy tooltip on a bulk action button in the floating action bar', async () => { await mountMultipleSelectGrid(html` + tooltip="I am a tooltip" + variant="inverted"> Action 2 `); @@ -228,8 +226,7 @@ describe('sl-grid', () => { aria-describedby="bulk-action-tooltip" fill="outline" slot="bulk-actions" - variant="inverted" - > + variant="inverted"> Action 2 `); @@ -270,8 +267,7 @@ describe('sl-grid', () => { aria-describedby="bulk-action-tooltip" fill="outline" slot="bulk-actions" - variant="inverted" - > + variant="inverted"> Action 2 `); @@ -301,12 +297,11 @@ describe('sl-grid', () => { it('should switch between the cancel tooltip and a bulk action tooltip in the floating action bar', async () => { await mountMultipleSelectGrid(html` + tooltip="I am a tooltip" + variant="inverted"> Action 2 `); @@ -360,8 +355,7 @@ describe('sl-grid', () => { { firstName: 'Alice', lastName: 'Johnson' } ]} selects="single" - row-action="select" - > + row-action="select"> @@ -484,8 +478,7 @@ describe('sl-grid', () => { { firstName: 'John', lastName: 'Doe' }, { firstName: 'Jane', lastName: 'Smith' } ]} - row-action="activate" - > + row-action="activate"> @@ -553,8 +546,7 @@ describe('sl-grid', () => { { firstName: 'John', lastName: 'Doe' }, { firstName: 'Jane', lastName: 'Smith' } ]} - row-action="select" - > + row-action="select"> @@ -651,8 +643,7 @@ describe('sl-grid', () => { { firstName: 'Sophie', lastName: 'Müller', email: 'sophie.muller@school1.edu' }, { firstName: 'Luca', lastName: 'van Dijk', email: 'luca.vandijk@school4.edu' }, { firstName: 'Clara', lastName: 'de Vries', email: 'clara.devries@school4.edu' } - ]} - > + ]}> diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts index 76d5260b36..222a663a1c 100644 --- a/packages/components/tag/src/tag.ts +++ b/packages/components/tag/src/tag.ts @@ -60,9 +60,6 @@ export class Tag extends ScopedElementsMixin(LitElement) { /** Observe changes in size, so we can check whether we need to show tooltips for truncated links. */ #observer = new ResizeObserver(() => this.#onResize()); - /** Either an instanceof of Tooltip, or a cleanup function. */ - #tooltip?: Tooltip | (() => void); - /** * Whether the tag component is disabled, when set no interaction is possible. * @@ -70,6 +67,9 @@ export class Tag extends ScopedElementsMixin(LitElement) { */ @property({ type: Boolean, reflect: true }) disabled?: boolean; + /** @internal Whether the tooltip is visible. */ + @state() tooltip?: boolean; + /** @internal The label of the tag component. */ @state() label = ''; @@ -106,14 +106,6 @@ export class Tag extends ScopedElementsMixin(LitElement) { override disconnectedCallback(): void { this.#observer.disconnect(); - if (this.#tooltip instanceof Tooltip) { - this.#tooltip?.remove(); - } else if (this.#tooltip) { - this.#tooltip(); - } - - this.#tooltip = undefined; - super.disconnectedCallback(); } @@ -151,11 +143,12 @@ export class Tag extends ScopedElementsMixin(LitElement) { @click=${this.#onRemove} ?disabled=${this.disabled} aria-hidden="true" + id="button" part="button" - tabindex="-1" - > + tabindex="-1"> + ${this.tooltip ? html`${this.label}` : nothing} ` : nothing} `; @@ -183,26 +176,11 @@ export class Tag extends ScopedElementsMixin(LitElement) { #onResize(): void { const slot = this.renderRoot.querySelector('slot'); - if (slot && slot.clientWidth < slot.scrollWidth) { - this.#tooltip ||= Tooltip.lazy( - this, - tooltip => { - this.#tooltip = tooltip; - tooltip.textContent = this.label; - }, - { context: this.shadowRoot! } - ); - } else if (this.#tooltip instanceof Tooltip) { - this.#tooltip.remove(); - this.#tooltip = undefined; - } else if (this.#tooltip) { - this.#tooltip(); - this.#tooltip = undefined; - } + this.tooltip = !!slot && slot.clientWidth < slot.scrollWidth; // If the contents of the tag overflows, make sure it is keyboard focusable, // so the user can tab to it. - if (!this.disabled && (this.removable || this.#tooltip)) { + if (!this.disabled && (this.removable || this.tooltip)) { this.setAttribute('tabindex', '0'); } else if (!this.hasAttribute('aria-labelledby')) { this.removeAttribute('tabindex'); diff --git a/packages/components/tooltip/src/tooltip.scss b/packages/components/tooltip/src/tooltip.scss index efeb38d526..14b327c2f0 100644 --- a/packages/components/tooltip/src/tooltip.scss +++ b/packages/components/tooltip/src/tooltip.scss @@ -5,7 +5,7 @@ box-sizing: border-box; color: var(--sl-color-foreground-inverted-plain); font-weight: var(--sl-text-new-typeset-fontWeight-regular); - margin: var(--sl-size-150); + margin: var(--sl-size-100); opacity: 0; padding: var(--sl-size-100) var(--sl-size-150); position-area: top; @@ -15,19 +15,19 @@ @media (prefers-reduced-motion: no-preference) { transition-duration: 150ms; - transition-property: opacity; + transition-property: display, opacity; transition-timing-function: cubic-bezier(0.4, 0, 1, 1); } } :host(:popover-open) { + opacity: 1; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + @starting-style { display: block; opacity: 0; } - - opacity: 1; - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } [part='hover-bridge'] { diff --git a/packages/components/tooltip/src/tooltip.ts b/packages/components/tooltip/src/tooltip.ts index 27db72607b..025ce6c909 100644 --- a/packages/components/tooltip/src/tooltip.ts +++ b/packages/components/tooltip/src/tooltip.ts @@ -2,10 +2,13 @@ import { CSSResultGroup, LitElement, PropertyValues, TemplateResult, html } from import { property, state } from 'lit/decorators.js'; import styles from './tooltip.scss.js'; -let nextUniqueId = 0; +declare global { + interface HTMLElementTagNameMap { + 'sl-tooltip': Tooltip; + } +} -const SHOW_DELAY = 150, - HIDE_DELAY = 0; +let nextUniqueId = 0; /** * A tooltip component that can be used to display additional information about an element when the @@ -21,9 +24,18 @@ const SHOW_DELAY = 150, * @slot - The content of the tooltip. * * @csspart arrow - The arrow element that points to the anchor. - * @csspart safe-triangle - An invisible element used to extend the hover area of the tooltip. + * @csspart hover-bridge - An invisible element used to extend the hover area of the tooltip. */ export class Tooltip extends LitElement { + /** + * The delay in milliseconds before showing the tooltip when the mouse hovers over the anchor + * element. + */ + static hoverShowDelay: number = 150; + + /** The delay in milliseconds before hiding the tooltip when the mouse leaves the anchor element. */ + static hoverHideDelay: number = 0; + /** @internal */ static override styles: CSSResultGroup = styles; @@ -138,7 +150,7 @@ export class Tooltip extends LitElement { return html`
-
+
`; } @@ -191,7 +203,7 @@ export class Tooltip extends LitElement { this.#hoverTimeout = setTimeout(() => { this.showPopover(); - }, SHOW_DELAY); + }, Tooltip.hoverShowDelay); } }; @@ -209,14 +221,14 @@ export class Tooltip extends LitElement { if (!(anchorHovered || tooltipHovered)) { this.#hoverTimeout = setTimeout(() => { this.hidePopover(); - }, HIDE_DELAY); + }, Tooltip.hoverHideDelay); } } }; #onToggle = (event: ToggleEvent): void => { if (event.newState === 'open' && this.anchor) { - this.#positionHoverExtender(this.anchor); + this.#positionHoverBridge(this.anchor); } }; @@ -246,9 +258,9 @@ export class Tooltip extends LitElement { element[ariaProperty] = refs.filter((ref: Element) => ref !== this); } - #positionHoverExtender(anchor: Element): void { - const extender = this.renderRoot.querySelector('[part="hover-extender"]'); - if (!extender) { + #positionHoverBridge(anchor: Element): void { + const bridge = this.renderRoot.querySelector('[part="hover-bridge"]'); + if (!bridge) { return; } @@ -299,15 +311,15 @@ export class Tooltip extends LitElement { `${width}px ${t.bottom - top}px, ${width}px ${t.top - top}px)`; } else { // Tooltip and anchor overlap; no bridge needed. - extender.style.display = 'none'; + bridge.style.display = 'none'; return; } - extender.style.left = `${left}px`; - extender.style.top = `${top}px`; - extender.style.width = `${width}px`; - extender.style.height = `${height}px`; - extender.style.clipPath = polygon; + bridge.style.left = `${left}px`; + bridge.style.top = `${top}px`; + bridge.style.width = `${width}px`; + bridge.style.height = `${height}px`; + bridge.style.clipPath = polygon; } #updateAnchor(): void { From 1c5a95191c3cbe62dbb5fa2f9271f7f62ffd4494 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Tue, 26 May 2026 10:52:49 +0200 Subject: [PATCH 12/50] =?UTF-8?q?=F0=9F=8D=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/tooltip/src/edge-cases.stories.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/components/tooltip/src/edge-cases.stories.ts b/packages/components/tooltip/src/edge-cases.stories.ts index 1d790d4008..dada9a3f87 100644 --- a/packages/components/tooltip/src/edge-cases.stories.ts +++ b/packages/components/tooltip/src/edge-cases.stories.ts @@ -21,10 +21,10 @@ export const DisabledButtons = { render: () => html` Disabled attribute - Tooltip text + Tooltip text ARIA disabled - Tooltip text + Tooltip text ` }; @@ -42,7 +42,7 @@ export const MenuButton = { Delete... - Tooltip text + Tooltip text ` }; @@ -52,9 +52,9 @@ export const Nested = {

Card title

Hover me - Tooltip text + Tooltip text - Card tooltip + Card tooltip ` }; From d9520395a6833f2ca5aca5f383b836a4f52be8a8 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Tue, 26 May 2026 12:17:45 +0200 Subject: [PATCH 13/50] =?UTF-8?q?=F0=9F=8E=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/button/src/button.spec.ts | 56 ++++++++++++------- .../components/button/src/button.stories.ts | 3 +- packages/components/button/src/button.ts | 21 +++---- .../shared/src/mixins/forward-aria-mixin.ts | 9 ++- packages/components/tooltip/src/tooltip.ts | 19 ++++--- .../lib/rules/button-has-label.js | 5 +- .../tests/lib/rules/button-has-label.test.js | 6 ++ 7 files changed, 75 insertions(+), 44 deletions(-) diff --git a/packages/components/button/src/button.spec.ts b/packages/components/button/src/button.spec.ts index 83549c3738..e07447048a 100644 --- a/packages/components/button/src/button.spec.ts +++ b/packages/components/button/src/button.spec.ts @@ -134,10 +134,13 @@ describe('sl-button', () => { describe('icon only, directly in button', () => { beforeEach(async () => { el = await fixture(html` - + `); + + // Wait for the MutationObserver to detect the sl-icon and update the icon-only state. + await new Promise(resolve => setTimeout(resolve, 50)); }); it('should have the icon-only state', () => { @@ -155,10 +158,13 @@ describe('sl-button', () => { describe('icon only, wrapped in container', () => { beforeEach(async () => { el = await fixture(html` - + `); + + // Wait for the MutationObserver to detect the sl-icon and update the icon-only state. + await new Promise(resolve => setTimeout(resolve, 50)); }); it('should have the icon-only state', () => { @@ -669,23 +675,34 @@ describe('sl-button', () => { expect(el.renderRoot.querySelector('sl-tooltip')).to.have.text('My tooltip'); }); - it('should set aria-describedby on the inner button when a text button has a tooltip', async () => { + it('should set ariaDescribedByElements on the inner button when a text button has a tooltip', async () => { el = await fixture(html`Hello world`); button = el.renderRoot.querySelector('button')!; - expect(button).to.have.attribute('aria-describedby', 'tooltip'); - expect(button).not.to.have.attribute('aria-labelledby'); + const tooltipEl = el.renderRoot.querySelector('sl-tooltip')!; + await tooltipEl.updateComplete; + + expect(button.ariaDescribedByElements).to.include(tooltipEl); + expect(button.ariaLabelledByElements).not.to.include(tooltipEl); }); - it('should set aria-labelledby on the inner button when an icon-only button has a tooltip', async () => { - // eslint-disable-next-line slds/button-has-label - el = await fixture(html``); - el.tooltip = 'Mark as favorite'; + it('should set ariaLabelledByElements on the inner button when an icon-only button has a tooltip', async () => { + el = await fixture( + html`` + ); + + // The first render uses type="description" because icon-only is detected asynchronously via + // requestAnimationFrame. Wait for that rAF and the resulting re-render, which changes the + // tooltip type to "label" and updates its ARIA relations. + await new Promise(resolve => requestAnimationFrame(resolve)); await el.updateComplete; + button = el.renderRoot.querySelector('button')!; + const tooltipEl = el.renderRoot.querySelector('sl-tooltip')!; + await tooltipEl.updateComplete; - expect(button).to.have.attribute('aria-labelledby', 'tooltip'); - expect(button).not.to.have.attribute('aria-describedby'); + expect(button.ariaLabelledByElements).to.include(tooltipEl); + expect(button.ariaDescribedByElements ?? []).not.to.include(tooltipEl); }); it('should remove the tooltip when the tooltip property is unset', async () => { @@ -717,11 +734,11 @@ describe('sl-button', () => { expect(ariaDescElements).to.include(tooltipEl); }); - it('should include both the tooltip and aria-labelledby element in ariaLabelledByElements', async () => { + it('should include both the tooltip and aria-describedby element in ariaDescribedByElements', async () => { const wrapper = await fixture(html`
Favorite star - + Hello world
@@ -729,15 +746,12 @@ describe('sl-button', () => { el = wrapper.querySelector('sl-button')!; - const tooltipEl = el.renderRoot.querySelector('sl-tooltip')!, - labelEl = wrapper.querySelector('#icon-btn-label')!, - ariaLabelElements = getForwardedAriaProperty( - el, - 'ariaLabelledByElements' as keyof HTMLElement - ) as Element[]; + const button = el.renderRoot.querySelector('button')!, + tooltip = el.renderRoot.querySelector('sl-tooltip')!, + span = wrapper.querySelector('#icon-btn-label')!; - expect(ariaLabelElements).to.include(labelEl); - expect(ariaLabelElements).to.include(tooltipEl); + expect(button.ariaDescribedByElements).to.include(span); + expect(button.ariaDescribedByElements).to.include(tooltip); }); }); }); diff --git a/packages/components/button/src/button.stories.ts b/packages/components/button/src/button.stories.ts index b5ab017b64..2711b3266e 100644 --- a/packages/components/button/src/button.stories.ts +++ b/packages/components/button/src/button.stories.ts @@ -1,4 +1,3 @@ -/* eslint-disable slds/button-has-label */ import { faPlus, faUniversalAccess } from '@fortawesome/pro-regular-svg-icons'; import '@sl-design-system/avatar/register.js'; import '@sl-design-system/dialog/register.js'; @@ -154,7 +153,7 @@ export const IconOnly: Story = { return html`

This example shows an icon-only button. When using an icon-only button, it's important to - provide an accessible name using the aria-label attribute so that assistive + provide an accessible name using the tooltip property so that assistive technologies can convey the purpose of the button to users.

this.#onUpdate()); } override render(): TemplateResult { @@ -191,28 +191,25 @@ export class Button extends ForwardAriaMixin(ScopedElementsMixin(LitElement)) { } // If the button is icon only, the tooltip functions as the label, otherwise it functions as the description. - let ariaLabelledBy: string | undefined, ariaDescribedBy: string | undefined; + let ariaType: string | undefined; if (this.tooltip) { - if (this.internals.states.has('icon-only')) { - ariaLabelledBy = 'tooltip'; - } else { - ariaDescribedBy = 'tooltip'; - } + ariaType = this.internals.states.has('icon-only') ? 'label' : 'description'; } return html` - ${this.tooltip ? html`${this.tooltip}` : nothing} + ${this.tooltip + ? html`${this.tooltip}` + : nothing} `; } diff --git a/packages/components/shared/src/mixins/forward-aria-mixin.ts b/packages/components/shared/src/mixins/forward-aria-mixin.ts index 0fa2ba6dac..a6b8ac78b9 100644 --- a/packages/components/shared/src/mixins/forward-aria-mixin.ts +++ b/packages/components/shared/src/mixins/forward-aria-mixin.ts @@ -191,7 +191,14 @@ export function ForwardAriaMixin< .filter((el): el is HTMLElement => el !== null); if (elementsProp.endsWith('Elements')) { - (targetElement as unknown as Record)[elementsProp] = elements; + const elementsPropValue = + (targetElement as unknown as Record)[elementsProp] ?? []; + + // Make sure we don't override any existing references + (targetElement as unknown as Record)[elementsProp] = [ + ...elementsPropValue, + ...elements + ]; } else { (targetElement as unknown as Record)[elementsProp] = elements[0] ?? null; diff --git a/packages/components/tooltip/src/tooltip.ts b/packages/components/tooltip/src/tooltip.ts index 025ce6c909..db5ff88453 100644 --- a/packages/components/tooltip/src/tooltip.ts +++ b/packages/components/tooltip/src/tooltip.ts @@ -120,7 +120,7 @@ export class Tooltip extends LitElement { document.removeEventListener('keydown', this.#onKeydown); if (this.anchor) { - this.#removeAriaRelation(this.anchor); + this.#removeAriaRelation(this.anchor, this.type); } super.disconnectedCallback(); @@ -144,6 +144,11 @@ export class Tooltip extends LitElement { this.hidePopover(); } } + + if (changes.has('type') && this.anchor) { + this.#removeAriaRelation(this.anchor, changes.get('type')); + this.#addAriaRelation(this.anchor, this.type); + } } override render(): TemplateResult { @@ -242,8 +247,8 @@ export class Tooltip extends LitElement { return type === 'description' ? 'ariaDescribedByElements' : 'ariaLabelledByElements'; } - #addAriaRelation(element: Element): void { - const ariaProperty = this.#getAriaPropertyFromType(this.type); + #addAriaRelation(element: Element, type?: 'description' | 'label'): void { + const ariaProperty = this.#getAriaPropertyFromType(type); const refs = element[ariaProperty] ?? []; if (!refs.includes(this)) { @@ -251,8 +256,8 @@ export class Tooltip extends LitElement { } } - #removeAriaRelation(element: Element): void { - const ariaProperty = this.#getAriaPropertyFromType(this.type); + #removeAriaRelation(element: Element, type?: 'description' | 'label'): void { + const ariaProperty = this.#getAriaPropertyFromType(type); const refs = element[ariaProperty] ?? []; element[ariaProperty] = refs.filter((ref: Element) => ref !== this); @@ -343,7 +348,7 @@ export class Tooltip extends LitElement { const { signal } = this.#eventController; if (newAnchor) { - this.#addAriaRelation(newAnchor); + this.#addAriaRelation(newAnchor, this.type); newAnchor.addEventListener('blur', this.#onBlur, { capture: true, signal }); newAnchor.addEventListener('click', this.#onClick, { signal }); @@ -359,7 +364,7 @@ export class Tooltip extends LitElement { } if (oldAnchor) { - this.#removeAriaRelation(oldAnchor); + this.#removeAriaRelation(oldAnchor, this.type); oldAnchor.removeEventListener('blur', this.#onBlur, { capture: true }); oldAnchor.removeEventListener('click', this.#onClick); diff --git a/tools/eslint-plugin-slds/lib/rules/button-has-label.js b/tools/eslint-plugin-slds/lib/rules/button-has-label.js index 73334937df..75a76e5dca 100644 --- a/tools/eslint-plugin-slds/lib/rules/button-has-label.js +++ b/tools/eslint-plugin-slds/lib/rules/button-has-label.js @@ -43,7 +43,10 @@ export const buttonHasLabel = { if ( hasTextContent(element) || hasAccessibleName(element) || - hasTooltipWithAriaRelationLabel + hasTooltipWithAriaRelationLabel || + // The `tooltip` attribute on sl-button provides the accessible label for icon-only + // buttons at runtime, so it counts as an accessible name at lint time. + 'tooltip' in (element.attribs ?? {}) ) { return; } diff --git a/tools/eslint-plugin-slds/tests/lib/rules/button-has-label.test.js b/tools/eslint-plugin-slds/tests/lib/rules/button-has-label.test.js index be7d234588..c5fb2dd9ac 100644 --- a/tools/eslint-plugin-slds/tests/lib/rules/button-has-label.test.js +++ b/tools/eslint-plugin-slds/tests/lib/rules/button-has-label.test.js @@ -33,6 +33,12 @@ ruleTester.run('button-has-label', buttonHasLabel, { }, { code: "html`\n`;" + }, + // tooltip attribute on sl-button (new feature) + { code: "html``;" }, + { code: 'html``;' }, + { + code: 'html``;' } ], invalid: [ From 2ec2c5e014ff3ef4a3060c0d150bb332a69d4559 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Tue, 26 May 2026 12:40:03 +0200 Subject: [PATCH 14/50] =?UTF-8?q?=F0=9F=8D=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/button/src/button.spec.ts | 8 +- packages/components/button/src/button.ts | 9 +- .../menu/src/menu-button.stories.ts | 113 +++--------------- packages/components/menu/src/menu-button.ts | 11 +- 4 files changed, 36 insertions(+), 105 deletions(-) diff --git a/packages/components/button/src/button.spec.ts b/packages/components/button/src/button.spec.ts index e07447048a..b94fac503a 100644 --- a/packages/components/button/src/button.spec.ts +++ b/packages/components/button/src/button.spec.ts @@ -669,10 +669,16 @@ describe('sl-button', () => { expect(el.renderRoot.querySelector('sl-tooltip')).to.exist; }); + it('should have an sl-tooltip with a tooltip part when set', async () => { + el = await fixture(html`Hello world`); + + expect(el.renderRoot.querySelector('[part="tooltip"]')).to.exist; + }); + it('should set the tooltip text content', async () => { el = await fixture(html`Hello world`); - expect(el.renderRoot.querySelector('sl-tooltip')).to.have.text('My tooltip'); + expect(el.renderRoot.querySelector('sl-tooltip')).to.have.trimmed.text('My tooltip'); }); it('should set ariaDescribedByElements on the inner button when a text button has a tooltip', async () => { diff --git a/packages/components/button/src/button.ts b/packages/components/button/src/button.ts index 8157977a99..41ee50de9c 100644 --- a/packages/components/button/src/button.ts +++ b/packages/components/button/src/button.ts @@ -47,6 +47,7 @@ export type ButtonVariant = * @slot default - Text label of the button. Optionally an sl-icon can be added * * @csspart button - The internal <button> element. + * @csspart tooltip - The tooltip element that is shown when the tooltip attribute is set. */ export class Button extends ForwardAriaMixin(ScopedElementsMixin(LitElement)) { /** @internal */ @@ -191,7 +192,7 @@ export class Button extends ForwardAriaMixin(ScopedElementsMixin(LitElement)) { } // If the button is icon only, the tooltip functions as the label, otherwise it functions as the description. - let ariaType: string | undefined; + let ariaType: 'description' | 'label' | undefined; if (this.tooltip) { ariaType = this.internals.states.has('icon-only') ? 'label' : 'description'; } @@ -208,7 +209,11 @@ export class Button extends ForwardAriaMixin(ScopedElementsMixin(LitElement)) { ${this.tooltip - ? html`${this.tooltip}` + ? html` + + ${this.tooltip} + + ` : nothing} `; } diff --git a/packages/components/menu/src/menu-button.stories.ts b/packages/components/menu/src/menu-button.stories.ts index 8a91e74329..02c0cf15c2 100644 --- a/packages/components/menu/src/menu-button.stories.ts +++ b/packages/components/menu/src/menu-button.stories.ts @@ -27,8 +27,8 @@ type Props = Pick TemplateResult); justifySelf: string; - label?: string; menuItems?(): TemplateResult; + tooltip?: string; }; type Story = StoryObj; @@ -101,6 +101,9 @@ export default { control: 'inline-radio', options: ['md', 'lg'] }, + tooltip: { + control: 'text' + }, variant: { control: 'inline-radio', options: ['default', 'primary', 'info'] @@ -117,11 +120,11 @@ export default { disabled, fill, justifySelf, - label, menuItems, position, shape, size, + tooltip, variant }) => { return html` @@ -131,18 +134,20 @@ export default { height: calc(100dvh - 2rem); place-items: center; } + sl-menu-button::part(tooltip) { + max-inline-size: 200px; + } + tooltip=${ifDefined(tooltip)} + variant=${ifDefined(variant)}> ${typeof body === 'string' ? html`
${body}
` : body()} ${menuItems?.()}
@@ -153,7 +158,6 @@ export default { export const Basic: Story = { args: { body: () => html``, - label: 'Settings', menuItems: () => html` @@ -163,7 +167,8 @@ export const Basic: Story = { Delete... - ` + `, + tooltip: 'Settings' } }; @@ -188,7 +193,8 @@ export const IconAndText: Story = { Settings `, - label: undefined + tooltip: + 'I am a tooltip for an icon and text menu button, so I should be a description, not a label' } }; @@ -196,7 +202,7 @@ export const Text: Story = { args: { ...Basic.args, body: () => html`Settings`, - label: undefined + tooltip: undefined } }; @@ -290,95 +296,6 @@ export const Avatar: Story = { } }; -export const WithTooltips: Story = { - parameters: { - a11y: { - config: { - rules: [ - { - /** - * The rule is disabled for icon-only sl-menu-buttons because they use - * ariaLabelledByElements to set aria-labelledby across shadow DOM boundaries, which the - * a11y checker cannot detect. - */ - id: 'aria-command-name', - enabled: false, - selector: 'sl-menu-button >> sl-button[icon-only]' - } - ] - } - } - }, - render: () => html` - -

Menu buttons with tooltips connected via aria-labelledby

-
- - - - - Rename... - - - - Delete... - - - Settings - - - - - - Rename... - - - - Delete... - - - Edit -
- -

Menu buttons with tooltips connected via aria-describedby

-
- - - Settings - - - Rename... - - - - Delete... - - - Open settings menu - - - - Edit - - - Rename... - - - - Delete... - - - Open edit menu -
- ` -}; - export const All: Story = { render: () => html` -

This example shows a tool bar with icon only buttons / menu buttons with tooltips.

-
- - - - - Bold - - - - - Italic - - - - - Underline (disabled) - - - - - Underline - - - - - - Rename... - - - - Delete... - - - Settings - - - - - - Rename... - - - - Delete... - - - Edit - -
- -

This example shows a tool bar with icon only buttons / menu buttons with aria-label.

-
- - - - - - - - - - - - - - - - - - - - - - Rename... - - - - Delete... - - - - - - - - Rename... - - - - Delete... - - - -
- `; - } -}; - export const Combination: Story = { render: () => html` + tabindex=${ifDefined(tabIndex)}> ${subheading} ${badge?.() ?? nothing} ${fallback?.() ?? nothing} `; @@ -147,15 +150,13 @@ export const Colors: Story = { color=${color} display-name=${`${color.at(0)?.toUpperCase() + color.slice(1)} subtle`} shape=${ifDefined(shape)} - size=${ifDefined(size)} - > + size=${ifDefined(size)}> + size=${ifDefined(size)}> ` )} @@ -337,8 +338,7 @@ export const Sizes: Story = { picture-url=${ifDefined(pictureUrl)} shape=${ifDefined(shape)} size=${size} - ?vertical=${vertical} - > + ?vertical=${vertical}> ${subheading ? html`${subheading}` : nothing} ${badgeSizes[size] === 'sm' ? nothing : '2'} diff --git a/packages/components/avatar/src/avatar.ts b/packages/components/avatar/src/avatar.ts index a090645ccc..476cc4637f 100644 --- a/packages/components/avatar/src/avatar.ts +++ b/packages/components/avatar/src/avatar.ts @@ -43,19 +43,19 @@ export type AvatarSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'; * ```html * + * picture-url="http://sanomalearning.design/avatars/lynn.png"> * ``` * + * @slot - The subheading of the avatar. + * @slot badge - The badge to display on the avatar. + * @slot fallback - The fallback content to display when no picture is set. + * * @csspart avatar - The container for positioning the badge. * @csspart initials - The initials to display when no picture is set. * @csspart name - The display name, either a `` or `` if `href` is set. * @csspart picture - The element containing the image, initials or fallback content. + * @csspart tooltip - The tooltip that is shown when the display name overflows. * @csspart wrapper - The wrapper element around the image and name. - * - * @slot badge - The badge to display on the avatar. - * @slot default - The subheading of the avatar. - * @slot fallback - The fallback content to display when no picture is set. */ export class Avatar extends ScopedElementsMixin(LitElement) { /** @internal */ @@ -118,6 +118,9 @@ export class Avatar extends ScopedElementsMixin(LitElement) { /** The size of the avatar. */ @property({ reflect: true }) size: AvatarSize = 'md'; + /** @internal Whether the tooltip is visible. */ + @state() tooltip?: boolean; + /** If true, will display the name below the image. */ @property({ type: Boolean, reflect: true }) vertical?: boolean; @@ -148,9 +151,11 @@ export class Avatar extends ScopedElementsMixin(LitElement) { } override render(): TemplateResult { + const avatar = this.renderAvatar(); + return this.href - ? html`${this.renderAvatar()}` - : html`
${this.renderAvatar()}
`; + ? html`${avatar}` + : html`
${avatar}
`; } renderAvatar(): TemplateResult { @@ -164,8 +169,7 @@ export class Avatar extends ScopedElementsMixin(LitElement) { @error=${this.#onError} part="image" src=${this.pictureUrl} - alt=${ifDefined(this.imageOnly ? this.displayName : '')} - /> + alt=${ifDefined(this.imageOnly ? this.displayName : '')} /> ` : html` @@ -177,8 +181,10 @@ export class Avatar extends ScopedElementsMixin(LitElement) { ${this.imageOnly ? nothing : html` - ${this.displayName} - ${this.displayName} + ${this.tooltip + ? html`${this.displayName}` + : nothing} + ${this.displayName} `} `; @@ -218,16 +224,11 @@ export class Avatar extends ScopedElementsMixin(LitElement) { this.clipPath = undefined; } - // Check if the name overflows and if so, enable the tooltip const name = this.renderRoot.querySelector('[part="name"]'); - if ( - name && - (name?.offsetWidth < name.scrollWidth || name.offsetHeight + 4 < name.scrollHeight) - ) { - name.setAttribute('aria-describedby', 'avatar-tooltip'); - } else { - name?.removeAttribute('aria-describedby'); - } + + // Check if the name overflows and if so, enable the tooltip + this.tooltip = + !!name && (name.offsetWidth < name.scrollWidth || name.offsetHeight + 4 < name.scrollHeight); } #onSlotChange(event: Event & { target: HTMLSlotElement }): void { From 1bc52485b39b663ce43c36a477eaae394116ed48 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Tue, 26 May 2026 16:10:21 +0200 Subject: [PATCH 20/50] =?UTF-8?q?=F0=9F=9B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../checkbox/src/checkbox-group.stories.ts | 22 +++++++++- .../components/checkbox/src/checkbox-group.ts | 5 +-- .../components/checkbox/src/checkbox.scss | 2 + .../checkbox/src/checkbox.stories.ts | 3 +- packages/components/grid/src/grid.ts | 28 ++++-------- .../grid/src/stories/basics.stories.ts | 43 ++++++++----------- .../tooltip/src/edge-cases.stories.ts | 21 +++++++++ 7 files changed, 72 insertions(+), 52 deletions(-) diff --git a/packages/components/checkbox/src/checkbox-group.stories.ts b/packages/components/checkbox/src/checkbox-group.stories.ts index 8fc5f1e8ed..edb9289431 100644 --- a/packages/components/checkbox/src/checkbox-group.stories.ts +++ b/packages/components/checkbox/src/checkbox-group.stories.ts @@ -1,6 +1,7 @@ import '@sl-design-system/button/register.js'; import '@sl-design-system/button-bar/register.js'; import '@sl-design-system/form/register.js'; +import '@sl-design-system/tooltip/register.js'; import { type Meta, type StoryObj } from '@storybook/web-components-vite'; import { type TemplateResult, html } from 'lit'; import '../register.js'; @@ -61,8 +62,7 @@ export default { ?required=${required} .label=${label} .size=${size} - .value=${value} - > + .value=${value}> ${boxes?.() ?? html` Option 1 @@ -77,6 +77,11 @@ export default { Report validity + `; } } satisfies Meta; @@ -141,6 +146,19 @@ export const NoLabel: Story = { } }; +export const Tooltips: Story = { + args: { + boxes: () => html` + Option 1 + Tooltip for option 1 + Option 2 + Tooltip for option 2 + Option 3 + Tooltip for option 3 + ` + } +}; + export const CustomValidity: Story = { args: { hint: 'This story has both builtin validation (required) and custom validation. You need to select the middle option to make the field valid. The custom validation is done by listening to the sl-validate event and setting the custom validity on the checkbox group.', diff --git a/packages/components/checkbox/src/checkbox-group.ts b/packages/components/checkbox/src/checkbox-group.ts index 3d5b6db374..947540f8b1 100644 --- a/packages/components/checkbox/src/checkbox-group.ts +++ b/packages/components/checkbox/src/checkbox-group.ts @@ -75,7 +75,7 @@ export class CheckboxGroup extends FormControlMixin(LitElement) { readonly internals = this.attachInternals(); /** @internal The slotted checkboxes. */ - @queryAssignedElements() boxes?: Array>; + @queryAssignedElements({ selector: 'sl-checkbox' }) boxes?: Array>; /** @internal Emits when the component loses focus. */ @event({ name: 'sl-blur' }) blurEvent!: EventEmitter; @@ -178,8 +178,7 @@ export class CheckboxGroup extends FormControlMixin(LitElement) { @sl-change=${this.#stopEvent} @sl-focus=${this.#stopEvent} @sl-form-control=${this.#onFormControl} - @sl-validate=${this.#stopEvent} - > + @sl-validate=${this.#stopEvent}> `; } diff --git a/packages/components/checkbox/src/checkbox.scss b/packages/components/checkbox/src/checkbox.scss index 66f3303081..17a77f119a 100644 --- a/packages/components/checkbox/src/checkbox.scss +++ b/packages/components/checkbox/src/checkbox.scss @@ -74,6 +74,8 @@ } :host([no-label]) { + display: inline-flex; + [part='outer'] { padding-inline-start: var(--_padding-inline); } diff --git a/packages/components/checkbox/src/checkbox.stories.ts b/packages/components/checkbox/src/checkbox.stories.ts index fd7fe9a664..a430a8ee1a 100644 --- a/packages/components/checkbox/src/checkbox.stories.ts +++ b/packages/components/checkbox/src/checkbox.stories.ts @@ -242,7 +242,8 @@ export const NoVisibleLabel: StoryObj = { aria-label attribute. That attribute is automatically applied to the input element.

- + + Toggle me `; } }; diff --git a/packages/components/grid/src/grid.ts b/packages/components/grid/src/grid.ts index 7b0d8f00fa..bf1de1d764 100644 --- a/packages/components/grid/src/grid.ts +++ b/packages/components/grid/src/grid.ts @@ -1,4 +1,3 @@ -/* eslint-disable slds/button-has-label */ /* eslint-disable lit/prefer-static-styles */ import { localized, msg, str } from '@lit/localize'; import { @@ -27,7 +26,6 @@ import { type SlSelectEvent, type SlToggleEvent } from '@sl-design-system/shared import { Skeleton } from '@sl-design-system/skeleton'; import { ToggleGroup } from '@sl-design-system/toggle-group'; import { ToolBar } from '@sl-design-system/tool-bar'; -import { Tooltip } from '@sl-design-system/tooltip'; import { type CSSResultGroup, LitElement, @@ -130,8 +128,7 @@ export class Grid extends ScopedElementsMixin(LitElement) { 'sl-skeleton': Skeleton, 'sl-scrollbar': Scrollbar, 'sl-toggle-group': ToggleGroup, - 'sl-tool-bar': ToolBar, - 'sl-tooltip': Tooltip + 'sl-tool-bar': ToolBar }; } @@ -449,8 +446,7 @@ export class Grid extends ScopedElementsMixin(LitElement) { + style="display:none"> @@ -461,8 +457,7 @@ export class Grid extends ScopedElementsMixin(LitElement) { href="#table-end" class="skip-link-start" @click=${(e: Event & { target: HTMLSlotElement }) => this.#onSkipTo(e, 'end')} - @focus=${(e: Event & { target: HTMLSlotElement }) => this.#onSkipToFocus(e, 'top')} - > + @focus=${(e: Event & { target: HTMLSlotElement }) => this.#onSkipToFocus(e, 'top')}> ${msg('Skip to end of table', { id: 'sl.grid.skipToEndOfTable' })} ` @@ -474,8 +469,7 @@ export class Grid extends ScopedElementsMixin(LitElement) { @sl-filter-register=${this.#onFilterRegister} @sl-sorter-change=${this.#onSorterChange} @sl-sorter-register=${this.#onSorterRegister} - part="thead" - > + part="thead"> ${this.#headerRows.map(row => this.renderHeaderRow(row))} @@ -508,15 +502,11 @@ export class Grid extends ScopedElementsMixin(LitElement) { + tooltip=${msg('Cancel selection', { id: 'sl.grid.cancelSelection' })} + variant="inverted"> - - ${msg('Cancel selection', { id: 'sl.grid.cancelSelection' })} - ${!this.noSkipLinks @@ -618,8 +608,7 @@ export class Grid extends ScopedElementsMixin(LitElement) { @drop=${(event: DragEvent) => this.#onDrop(event, item)} aria-rowindex=${index} index=${index} - part=${parts.join(' ')} - > + part=${parts.join(' ')}> ${rows[rows.length - 1].map(col => col.renderData(item))} `; @@ -643,8 +632,7 @@ export class Grid extends ScopedElementsMixin(LitElement) { ?collapsed=${collapsed} ?drag-handle=${draggable} ?selectable=${selectable} - .selected=${item.selected ?? 'none'} - > + .selected=${item.selected ?? 'none'}> ${this.groupHeaderRenderer?.(item) ?? html` diff --git a/packages/components/grid/src/stories/basics.stories.ts b/packages/components/grid/src/stories/basics.stories.ts index 4ce1097286..2c9cad5674 100644 --- a/packages/components/grid/src/stories/basics.stories.ts +++ b/packages/components/grid/src/stories/basics.stories.ts @@ -47,13 +47,11 @@ export const Basic: Story = { header="Student" path="fullName" .renderer=${avatarRenderer} - .scopedElements=${{ 'sl-avatar': Avatar }} - > + .scopedElements=${{ 'sl-avatar': Avatar }}> + .scopedElements=${{ 'sl-format-date': FormatDate }}> `; @@ -92,8 +90,7 @@ export const EllipsizeText: Story = { style="max-inline-size: 500px" ellipsize-text column-divider - no-skip-links - > + no-skip-links> @@ -128,11 +125,10 @@ export const Header: Story = { path="firstName" .header=${() => html` First name - - Some information about the first name + + Some information about the first name `} - .scopedElements=${{ 'sl-icon': Icon, 'sl-tooltip': Tooltip }} - > + .scopedElements=${{ 'sl-icon': Icon, 'sl-tooltip': Tooltip }}> @@ -142,8 +138,7 @@ export const Header: Story = { City `} path="school.city" - .scopedElements=${{ 'sl-icon': Icon }} - > + .scopedElements=${{ 'sl-icon': Icon }}> html` @@ -160,8 +155,7 @@ export const Header: Story = { 'sl-icon': Icon, 'sl-menu-button': MenuButtonComponent, 'sl-menu-item': MenuItem - }} - > + }}> ` @@ -199,8 +193,7 @@ export const MenuButton: Story = { grow="3" header="Person" .renderer=${avatarRenderer} - .scopedElements=${{ 'sl-avatar': Avatar }} - > + .scopedElements=${{ 'sl-avatar': Avatar }}> + width="48"> `; } @@ -299,11 +291,12 @@ export const Skeleton: Story = {
+ variant="circle"> + style="block-size: 18px; inline-size: ${Math.max( + Math.random() * 100, + 30 + )}%">
`; } else { @@ -313,8 +306,7 @@ export const Skeleton: Story = { + size="sm"> `; } }; @@ -344,8 +336,7 @@ export const Skeleton: Story = { + .scopedElements=${{ 'sl-avatar': Avatar }}> `; diff --git a/packages/components/tooltip/src/edge-cases.stories.ts b/packages/components/tooltip/src/edge-cases.stories.ts index dada9a3f87..4ac235e81a 100644 --- a/packages/components/tooltip/src/edge-cases.stories.ts +++ b/packages/components/tooltip/src/edge-cases.stories.ts @@ -3,6 +3,7 @@ import { faGear as fasGear } from '@fortawesome/pro-solid-svg-icons'; import '@sl-design-system/button/register.js'; import '@sl-design-system/button-bar/register.js'; import '@sl-design-system/card/register.js'; +import '@sl-design-system/dialog/register.js'; import { Icon } from '@sl-design-system/icon'; import '@sl-design-system/icon/register.js'; import '@sl-design-system/menu/register.js'; @@ -17,6 +18,26 @@ export default { title: 'Overlay/Tooltip/Edge cases' }; +export const Dialog = { + render: () => html` + + + Open dialog + + +

Dialog title

+ Close +
+ ` +}; + export const DisabledButtons = { render: () => html` From 448df6295c62b76ba5a8d840718eeda4115a72a3 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 27 May 2026 10:36:03 +0200 Subject: [PATCH 21/50] =?UTF-8?q?=F0=9F=8D=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/tag/src/tag-list.scss | 4 + packages/components/tag/src/tag-list.ts | 28 +++--- packages/components/tag/src/tag.scss | 40 +++++--- packages/components/tag/src/tag.ts | 112 +++++++++++---------- packages/components/tooltip/src/tooltip.ts | 2 - 5 files changed, 100 insertions(+), 86 deletions(-) diff --git a/packages/components/tag/src/tag-list.scss b/packages/components/tag/src/tag-list.scss index 3073a21d93..656df24c3d 100644 --- a/packages/components/tag/src/tag-list.scss +++ b/packages/components/tag/src/tag-list.scss @@ -64,3 +64,7 @@ flex-wrap: wrap; gap: var(--sl-size-050); } + +sl-tag::part(tooltip) { + max-inline-size: 300px; +} diff --git a/packages/components/tag/src/tag-list.ts b/packages/components/tag/src/tag-list.ts index 915a30a1e8..b46cc94729 100644 --- a/packages/components/tag/src/tag-list.ts +++ b/packages/components/tag/src/tag-list.ts @@ -4,7 +4,6 @@ import { ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; import { RovingTabindexController } from '@sl-design-system/shared'; -import { Tooltip } from '@sl-design-system/tooltip'; import { type CSSResultGroup, LitElement, @@ -52,8 +51,7 @@ export class TagList extends ScopedElementsMixin(LitElement) { /** @internal */ static get scopedElements(): ScopedElementsMap { return { - 'sl-tag': Tag, - 'sl-tooltip': Tooltip + 'sl-tag': Tag }; } @@ -241,25 +239,29 @@ export class TagList extends ScopedElementsMixin(LitElement) { } override render(): TemplateResult { + let tooltip; + if (this.stacked) { + const label = msg('List of hidden elements', { id: 'sl.tag.listOfHiddenElements' }), + tags = this.tags + .filter(tag => tag.style.display === 'none') + .map(tag => tag.label) + .join(', '); + + tooltip = `${label}:${tags}`; + } + return html` ${this.stacked ? html`
+ tooltip=${ifDefined(tooltip)} + variant=${ifDefined(this.variant)}> +${this.stackSize} - - ${msg('List of hidden elements', { id: 'sl.tag.listOfHiddenElements' })}: - ${this.tags - .filter(tag => tag.style.display === 'none') - .map(tag => tag.label) - .join(', ')} -
` : nothing} diff --git a/packages/components/tag/src/tag.scss b/packages/components/tag/src/tag.scss index 8222ecf524..be6367aaa1 100644 --- a/packages/components/tag/src/tag.scss +++ b/packages/components/tag/src/tag.scss @@ -5,14 +5,9 @@ --_br-color: var(--sl-color-border-neutral-plain); align-items: center; - border: var(--sl-size-borderWidth-subtle) solid var(--_br-color); - border-radius: var(--sl-size-borderRadius-default); color: var(--sl-color-foreground-neutral-bold); display: inline-flex; max-inline-size: 100%; - outline: transparent solid var(--sl-size-borderWidth-focusRing); - outline-offset: var(--sl-size-outlineOffset-default); - overflow: hidden; vertical-align: middle; @media (prefers-reduced-motion: no-preference) { @@ -21,12 +16,8 @@ } } -:host([removable]) slot { - border-inline-end: var(--sl-size-borderWidth-default) solid var(--_br-color); -} - :host([size='lg']) { - slot { + [part='label'] { padding: calc(var(--sl-size-100) - var(--sl-size-borderWidth-default)) var(--sl-size-150); } @@ -49,26 +40,43 @@ color: var(--sl-color-foreground-disabled); pointer-events: none; - slot, - button { + button, + [part='label'] { background: var(--sl-color-background-disabled); } } -:host(:focus-visible) { - outline-color: var(--sl-color-border-focused); +:host(:focus-within) { position: relative; z-index: 1; // Make sure the focus ring is above other elements } -slot { +[part='container'] { + border: var(--sl-size-borderWidth-subtle) solid var(--_br-color); + border-radius: var(--sl-size-borderRadius-default); + box-sizing: border-box; + display: inline-flex; + max-inline-size: 100%; + outline: transparent solid var(--sl-size-borderWidth-focusRing); + outline-offset: var(--sl-size-outlineOffset-default); + overflow: hidden; + + &:focus-visible { + outline-color: var(--sl-color-border-focused); + } +} + +[part='label'] { background: var(--_bg-color); - display: block; flex: 1; overflow: hidden; padding: calc(var(--sl-size-025) - var(--sl-size-borderWidth-default)) var(--sl-size-100); text-overflow: ellipsis; white-space: nowrap; + + &:has(+ button) { + border-inline-end: var(--sl-size-borderWidth-default) solid var(--_br-color); + } } button { diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts index 222a663a1c..e9a983f6a2 100644 --- a/packages/components/tag/src/tag.ts +++ b/packages/components/tag/src/tag.ts @@ -6,15 +6,9 @@ import { import { Icon } from '@sl-design-system/icon'; import { EventEmitter, EventsController, event } from '@sl-design-system/shared'; import { Tooltip } from '@sl-design-system/tooltip'; -import { - type CSSResultGroup, - LitElement, - type PropertyValues, - type TemplateResult, - html, - nothing -} from 'lit'; +import { type CSSResultGroup, LitElement, type TemplateResult, html, nothing } from 'lit'; import { property, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; import styles from './tag.scss.js'; declare global { @@ -40,6 +34,11 @@ export type TagVariant = 'neutral' | 'info'; * ``` * * @slot default - The tag label. + * + * @csspart container - The component's container. + * @csspart label - The tag's label. + * @csspart button - The remove button. + * @csspart tooltip - The tooltip shown when the content is truncated. */ @localized() export class Tag extends ScopedElementsMixin(LitElement) { @@ -51,6 +50,12 @@ export class Tag extends ScopedElementsMixin(LitElement) { }; } + /** @internal */ + static override shadowRootOptions: ShadowRootInit = { + ...LitElement.shadowRootOptions, + delegatesFocus: true + }; + /** @internal */ static override styles: CSSResultGroup = styles; @@ -67,9 +72,6 @@ export class Tag extends ScopedElementsMixin(LitElement) { */ @property({ type: Boolean, reflect: true }) disabled?: boolean; - /** @internal Whether the tooltip is visible. */ - @state() tooltip?: boolean; - /** @internal The label of the tag component. */ @state() label = ''; @@ -90,6 +92,13 @@ export class Tag extends ScopedElementsMixin(LitElement) { */ @property({ reflect: true }) size?: TagSize; + /** + * The text to be shown in the tooltip. If the tooltip property isn't set explicitly to a string, + * the component itself will automatically determine when to show a tooltip based on the content's + * truncation. + */ + @property() tooltip?: boolean | string; + /** * The variant of the tag. * @@ -109,46 +118,43 @@ export class Tag extends ScopedElementsMixin(LitElement) { super.disconnectedCallback(); } - override updated(changes: PropertyValues): void { - super.updated(changes); - - if (changes.has('disabled') || changes.has('removable')) { - if (this.removable) { - this.setAttribute('tabindex', this.disabled ? '-1' : '0'); - } else if (this.disabled || changes.get('removable')) { - this.removeAttribute('tabindex'); - } - } + override render(): TemplateResult { + const focusable = !this.disabled && (this.removable || this.tooltip); - if (changes.has('removable')) { - if (this.removable) { - this.setAttribute( - 'aria-description', - msg('Press the delete or backspace key to remove this item', { - id: 'sl.tag.removalInstructions' - }) - ); - } else { - this.removeAttribute('aria-description'); - } + let description; + if (focusable && this.removable) { + description = msg('Press the delete or backspace key to remove this item', { + id: 'sl.tag.removalInstructions' + }); } - } - override render(): TemplateResult { return html` - - ${this.removable && !this.disabled +
+
+ +
+ ${this.removable && !this.disabled + ? html` + + ` + : nothing} +
+ ${this.tooltip ? html` - - ${this.tooltip ? html`${this.label}` : nothing} + + ${typeof this.tooltip === 'string' ? this.tooltip : this.label} + ` : nothing} `; @@ -174,17 +180,13 @@ export class Tag extends ScopedElementsMixin(LitElement) { } #onResize(): void { - const slot = this.renderRoot.querySelector('slot'); + if (typeof this.tooltip === 'string') { + return; + } - this.tooltip = !!slot && slot.clientWidth < slot.scrollWidth; + const label = this.renderRoot.querySelector('[part="label"]'); - // If the contents of the tag overflows, make sure it is keyboard focusable, - // so the user can tab to it. - if (!this.disabled && (this.removable || this.tooltip)) { - this.setAttribute('tabindex', '0'); - } else if (!this.hasAttribute('aria-labelledby')) { - this.removeAttribute('tabindex'); - } + this.tooltip = !!label && label.clientWidth < label.scrollWidth; } #onSlotChange(event: Event & { target: HTMLSlotElement }): void { diff --git a/packages/components/tooltip/src/tooltip.ts b/packages/components/tooltip/src/tooltip.ts index db5ff88453..18e9b44226 100644 --- a/packages/components/tooltip/src/tooltip.ts +++ b/packages/components/tooltip/src/tooltip.ts @@ -23,7 +23,6 @@ let nextUniqueId = 0; * * @slot - The content of the tooltip. * - * @csspart arrow - The arrow element that points to the anchor. * @csspart hover-bridge - An invisible element used to extend the hover area of the tooltip. */ export class Tooltip extends LitElement { @@ -154,7 +153,6 @@ export class Tooltip extends LitElement { override render(): TemplateResult { return html` -
`; } From d8c1e1c2d40536cdd50a221a1b686e0eab0ecab7 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 27 May 2026 14:17:26 +0200 Subject: [PATCH 22/50] =?UTF-8?q?=F0=9F=90=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .storybook/vitest.setup.ts | 8 - .../button-bar/src/button-bar.spec.ts | 10 +- .../components/button-bar/src/button-bar.ts | 17 +- .../components/calendar/src/calendar.spec.ts | 43 ++--- packages/components/calendar/src/calendar.ts | 23 ++- .../ellipsize-text/src/ellipsize-text.spec.ts | 32 +--- .../ellipsize-text/src/ellipsize-text.ts | 8 +- packages/components/grid/src/grid.spec.ts | 181 ------------------ .../components/menu/src/menu-button.spec.ts | 17 +- packages/components/menu/src/menu-button.ts | 59 ++++-- packages/components/tag/src/tag-list.scss | 38 +--- packages/components/tag/src/tag-list.spec.ts | 30 +-- packages/components/tag/src/tag-list.ts | 2 - packages/components/tag/src/tag.spec.ts | 81 ++++---- packages/components/tooltip/src/tooltip.ts | 2 +- vitest.config.ts | 3 +- 16 files changed, 172 insertions(+), 382 deletions(-) delete mode 100644 .storybook/vitest.setup.ts diff --git a/.storybook/vitest.setup.ts b/.storybook/vitest.setup.ts deleted file mode 100644 index b89010f028..0000000000 --- a/.storybook/vitest.setup.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview'; -import { setProjectAnnotations } from '@storybook/web-components-vite'; -import '../vitest.setup'; -import * as projectAnnotations from './preview'; - -// This is an important step to apply the right configuration when testing your stories. -// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations -setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]); diff --git a/packages/components/button-bar/src/button-bar.spec.ts b/packages/components/button-bar/src/button-bar.spec.ts index d70977c07c..a4d5cd61e7 100644 --- a/packages/components/button-bar/src/button-bar.spec.ts +++ b/packages/components/button-bar/src/button-bar.spec.ts @@ -101,7 +101,7 @@ describe('sl-button-bar', () => { `); // Give the buttons a chance to update - await el.updateComplete; + await new Promise(resolve => setTimeout(resolve, 50)); }); it('should have the icon-only state', () => { @@ -123,7 +123,7 @@ describe('sl-button-bar', () => { `); // Give the buttons a chance to update - await el.updateComplete; + await new Promise(resolve => setTimeout(resolve, 50)); }); it('should not have the icon-only state', () => { @@ -143,7 +143,7 @@ describe('sl-button-bar', () => { `); // Give the buttons a chance to update - await el.updateComplete; + await new Promise(resolve => setTimeout(resolve, 50)); }); it('should not match :state(icon-only)', () => { @@ -165,7 +165,7 @@ describe('sl-button-bar', () => { `); // Give the buttons a chance to update - await el.updateComplete; + await new Promise(resolve => setTimeout(resolve, 50)); }); it('should have the icon-only state', () => { @@ -188,7 +188,7 @@ describe('sl-button-bar', () => { el.appendChild(button); // Wait for the slot change and update - await new Promise(resolve => setTimeout(resolve)); + await new Promise(resolve => setTimeout(resolve, 50)); expect(el).not.to.match(':state(empty)'); }); diff --git a/packages/components/button-bar/src/button-bar.ts b/packages/components/button-bar/src/button-bar.ts index c411aca047..ac8e592d3d 100644 --- a/packages/components/button-bar/src/button-bar.ts +++ b/packages/components/button-bar/src/button-bar.ts @@ -107,10 +107,18 @@ export class ButtonBar extends LitElement { async #onMutate(): Promise { const buttons = (this.buttons ?? []).filter(el => el.tagName !== 'STYLE'); + if (buttons.length) { + this.#internals.states.delete('empty'); + this.#updateButtons(); + } else { + this.#internals.states.add('empty'); + } + const icons = await Promise.all( buttons.map(async el => { if (el instanceof ReactiveElement) { - await el.updateComplete; + // Give the button time to set the `icon-only` state + await new Promise(resolve => setTimeout(resolve)); } // Also check for the `icon-only` attribute for backward compatibility with older button versions @@ -128,13 +136,6 @@ export class ButtonBar extends LitElement { } else { this.#internals.states.delete('icon-only'); } - - if (buttons.length) { - this.#internals.states.delete('empty'); - this.#updateButtons(); - } else { - this.#internals.states.add('empty'); - } } #onSlotChange(event: Event & { target: HTMLSlotElement }): void { diff --git a/packages/components/calendar/src/calendar.spec.ts b/packages/components/calendar/src/calendar.spec.ts index 9eee3ba3a6..d828e1be83 100644 --- a/packages/components/calendar/src/calendar.spec.ts +++ b/packages/components/calendar/src/calendar.spec.ts @@ -596,8 +596,7 @@ describe('sl-calendar', () => { + max=${new Date(Date.UTC(2023, 11, 31)).toISOString()}> `); const helperText = el.renderRoot.querySelector('.helper-text'); @@ -610,8 +609,7 @@ describe('sl-calendar', () => { el = await fixture(html` + min=${new Date(Date.UTC(2023, 0, 1)).toISOString()}> `); const helperText = el.renderRoot.querySelector('.helper-text'); @@ -624,8 +622,7 @@ describe('sl-calendar', () => { el = await fixture(html` + max=${new Date(Date.UTC(2023, 11, 31)).toISOString()}> `); const helperText = el.renderRoot.querySelector('.helper-text'); @@ -639,8 +636,7 @@ describe('sl-calendar', () => { + max=${new Date(Date.UTC(2023, 5, 20)).toISOString()}> `); const helperText = el.renderRoot.querySelector('.helper-text'); @@ -654,8 +650,7 @@ describe('sl-calendar', () => { + max=${new Date(Date.UTC(2024, 11, 31)).toISOString()}> `); const helperText = el.renderRoot.querySelector('.helper-text'); @@ -669,8 +664,7 @@ describe('sl-calendar', () => { + max=${new Date(Date.UTC(2023, 11, 31)).toISOString()}> `); const monthView = el.renderRoot @@ -691,8 +685,7 @@ describe('sl-calendar', () => { + max=${new Date(Date.UTC(2023, 11, 31)).toISOString()}> `); // Switch to month mode @@ -721,8 +714,7 @@ describe('sl-calendar', () => { + max=${new Date(Date.UTC(2024, 11, 31)).toISOString()}> `); // Switch to year mode @@ -755,27 +747,24 @@ describe('sl-calendar', () => { show-today min=${new Date(Date.UTC(2023, 2, 1)).toISOString()} max=${new Date(Date.UTC(2023, 2, 31)).toISOString()} - .indicatorDates=${[{ date: indicatorDate, color: 'blue', label: 'Event' }]} - > + .indicatorDates=${[{ date: indicatorDate, color: 'blue', label: 'Event' }]}> `); const monthView = el.renderRoot .querySelector('sl-select-day') ?.renderRoot.querySelector('sl-month-view:not([inert])'), dayButton = monthView?.renderRoot.querySelector('button[tabindex="0"]'), - indicatorId = dayButton?.getAttribute('aria-describedby'), - indicatorTooltip = indicatorId - ? (monthView?.renderRoot as ShadowRoot)?.getElementById(indicatorId) - : null; - - dayButton?.focus(); - - const helperText = el.renderRoot.querySelector('.helper-text'); + indicatorTooltip = dayButton?.nextElementSibling, + helperText = el.renderRoot.querySelector('.helper-text'); expect(dayButton).to.exist; expect(indicatorTooltip).to.exist; - expect(dayButton?.ariaDescribedByElements).to.include(helperText); + expect(indicatorTooltip).to.have.trimmed.text('Event'); + + dayButton?.focus(); + expect(dayButton?.ariaDescribedByElements).to.include(indicatorTooltip); + expect(dayButton?.ariaDescribedByElements).to.include(helperText); }); }); }); diff --git a/packages/components/calendar/src/calendar.ts b/packages/components/calendar/src/calendar.ts index 5976dcbf6d..ddf3a54099 100644 --- a/packages/components/calendar/src/calendar.ts +++ b/packages/components/calendar/src/calendar.ts @@ -151,8 +151,7 @@ export class Calendar extends LocaleMixin(ScopedElementsMixin(LitElement)) { locale=${ifDefined(this.locale)} max=${ifDefined(this.max?.toISOString())} min=${ifDefined(this.min?.toISOString())} - style=${ifDefined(this.mode === 'day' ? undefined : 'visibility: hidden')} - > + style=${ifDefined(this.mode === 'day' ? undefined : 'visibility: hidden')}> ${choose(this.mode, [ [ 'month', @@ -165,8 +164,7 @@ export class Calendar extends LocaleMixin(ScopedElementsMixin(LitElement)) { .month=${this.month} locale=${ifDefined(this.locale)} max=${ifDefined(this.max?.toISOString())} - min=${ifDefined(this.min?.toISOString())} - > + min=${ifDefined(this.min?.toISOString())}> ` ], [ @@ -178,8 +176,7 @@ export class Calendar extends LocaleMixin(ScopedElementsMixin(LitElement)) { .selected=${this.selected} .year=${this.month} max=${ifDefined(this.max?.toISOString())} - min=${ifDefined(this.min?.toISOString())} - > + min=${ifDefined(this.min?.toISOString())}> ` ] ])} @@ -221,14 +218,24 @@ export class Calendar extends LocaleMixin(ScopedElementsMixin(LitElement)) { `; } - /** Sets `ariaDescribedByElements` on the button that receives focus inside the calendar grid. */ + /** + * Adds the helper-text to `ariaDescribedByElements` on the first button that receives focus + * inside the calendar grid. + * + * This is an accessibility hack necessary because of + * https://github.com/nvaccess/nvda/issues/13392. NVDA automatically switches to browse mode when + * detecting interactive elements inside grid cells. Because of that focus is not moved to another + * day and the ARIA descriptions aren't read. As a workaround, we're adding the helper text to the + * first button that receives focus, so NVDA will at least read the helper text once. Once this + * NVDA issue is resolved, we can remove this workaround and add the helper text to the min/max + * day as expected. + */ #onFocusIn(event: FocusEvent): void { if (!this.min && !this.max) { return; } const helperText = this.renderRoot.querySelector('.helper-text'); - if (!helperText) { return; } diff --git a/packages/components/ellipsize-text/src/ellipsize-text.spec.ts b/packages/components/ellipsize-text/src/ellipsize-text.spec.ts index 0439f6093e..57c0e77f9f 100644 --- a/packages/components/ellipsize-text/src/ellipsize-text.spec.ts +++ b/packages/components/ellipsize-text/src/ellipsize-text.spec.ts @@ -26,32 +26,20 @@ describe('sl-ellipsize-text', () => { }); it('should not have a tooltip by default', () => { - expect(el).not.to.have.attribute('aria-describedby'); + expect(el.renderRoot.querySelector('sl-tooltip')).not.to.exist; }); - describe('tooltip', () => { - beforeEach(async () => { - el.style.width = '100px'; + it('should have a tooltip when there is not enough space', async () => { + el.style.width = '100px'; - // Wait for the resize observer to trigger - await new Promise(resolve => setTimeout(resolve, 100)); + // Wait for the resize observer to trigger + await new Promise(resolve => setTimeout(resolve, 50)); - // Trigger a focus event to create the tooltip - el.dispatchEvent(new Event('focusin')); - }); + const slot = el.renderRoot.querySelector('slot'), + tooltip = el.renderRoot.querySelector('sl-tooltip'); - it('should have a tooltip when there is not enough space', () => { - const tooltip = el.nextElementSibling; - - expect(tooltip).to.exist; - expect(tooltip).to.match('sl-tooltip'); - expect(el).to.have.attribute('aria-describedby', tooltip?.id); - }); - - it('should remove the tooltip when the element is removed from the DOM', () => { - el.remove(); - - expect(document.querySelector('sl-tooltip')).not.to.exist; - }); + expect(tooltip).to.exist; + expect(tooltip).to.have.trimmed.text('This is a long text that should be truncated'); + expect(slot?.ariaDescribedByElements).to.include(tooltip); }); }); diff --git a/packages/components/ellipsize-text/src/ellipsize-text.ts b/packages/components/ellipsize-text/src/ellipsize-text.ts index 481ac33950..a11260c6b6 100644 --- a/packages/components/ellipsize-text/src/ellipsize-text.ts +++ b/packages/components/ellipsize-text/src/ellipsize-text.ts @@ -16,6 +16,8 @@ declare global { /** * Small utility component to add ellipsis to text that overflows its container. It also adds a * tooltip with the full text. + * + * @slot - The text to be truncated. */ export class EllipsizeText extends ScopedElementsMixin(LitElement) { /** @internal */ @@ -50,12 +52,14 @@ export class EllipsizeText extends ScopedElementsMixin(LitElement) { return html` ${this.tooltip - ? html`${this.textContent?.trim()}` + ? html`${this.textContent?.trim()}` : nothing} `; } #onResize(): void { - this.tooltip = this.offsetWidth < this.scrollWidth; + const slot = this.renderRoot.querySelector('slot'); + + this.tooltip = !!slot && slot.offsetWidth < slot.scrollWidth; } } diff --git a/packages/components/grid/src/grid.spec.ts b/packages/components/grid/src/grid.spec.ts index 71e64b1c8a..a3f2c3dbd5 100644 --- a/packages/components/grid/src/grid.spec.ts +++ b/packages/components/grid/src/grid.spec.ts @@ -1,14 +1,11 @@ import '@sl-design-system/button/register.js'; import '@sl-design-system/menu/register.js'; -import { isPopoverOpen } from '@sl-design-system/shared'; import { type ToolBar } from '@sl-design-system/tool-bar'; -import { Tooltip } from '@sl-design-system/tooltip'; import '@sl-design-system/tooltip/register.js'; import { fixture } from '@sl-design-system/vitest-browser-lit'; import { html } from 'lit'; import { type SinonSpy, spy } from 'sinon'; import { beforeEach, describe, expect, it } from 'vitest'; -import { userEvent } from 'vitest/browser'; import '../register.js'; import { type Grid, type SlActiveRowChangeEvent } from './grid.js'; import { waitForGridToRenderData } from './utils.js'; @@ -21,17 +18,6 @@ describe('sl-grid', () => { { firstName: 'John', lastName: 'Doe' }, { firstName: 'Jane', lastName: 'Smith' } ]; - const findTooltip = (id: string): HTMLElement | null => { - return ( - el.querySelector(`#${CSS.escape(id)}`) ?? - el.renderRoot.querySelector(`#${CSS.escape(id)}`) - ); - }; - - const findTooltipByText = (text: string): HTMLElement | null => - Array.from(el.querySelectorAll('sl-tooltip')) - .concat(Array.from(el.renderRoot.querySelectorAll('sl-tooltip'))) - .find(tooltipEl => tooltipEl.textContent?.includes(text)) ?? null; const mountMultipleSelectGrid = async (bulkActions?: unknown): Promise> => { el = await fixture(html` @@ -178,173 +164,6 @@ describe('sl-grid', () => { }); }); - describe('multiple select bulk actions', () => { - const openBulkActions = async (): Promise => { - el.renderRoot - .querySelector('tbody tr:first-of-type td[part~="selection"]') - ?.click(); - await el.updateComplete; - await new Promise(resolve => setTimeout(resolve, 50)); - }; - - it('should show a lazy tooltip on a bulk action button in the floating action bar', async () => { - await mountMultipleSelectGrid(html` - - Action 2 - - `); - - await openBulkActions(); - - const bulkActions = el.renderRoot.querySelector('[part="bulk-actions"]'), - button = el.querySelector('sl-button[slot="bulk-actions"]'); - - expect(bulkActions).to.exist; - expect(button).to.exist; - expect(isPopoverOpen(bulkActions!)).to.be.true; - - await userEvent.hover(button!); - await el.updateComplete; - await new Promise(resolve => setTimeout(resolve, Tooltip.hoverShowDelay + 50)); - - const lazyTooltip = findTooltipByText('I am a tooltip'); - - expect(lazyTooltip).to.exist; - expect(lazyTooltip?.tagName).to.equal('SL-TOOLTIP'); - expect(isPopoverOpen(lazyTooltip!)).to.be.true; - }); - - it('should keep showing an explicit tooltip for a bulk action button on repeated hover', async () => { - await mountMultipleSelectGrid(html` - Bulk action tooltip - - Action 2 - - `); - - await openBulkActions(); - - const button = el.querySelector('sl-button[slot="bulk-actions"]'), - explicitTooltip = findTooltip('bulk-action-tooltip'); - - expect(button).to.exist; - expect(explicitTooltip).to.exist; - - await userEvent.hover(button!); - await el.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await new Promise(resolve => setTimeout(resolve, Tooltip.hoverShowDelay + 10)); - - expect(isPopoverOpen(explicitTooltip!)).to.be.true; - - await userEvent.unhover(button!); - await el.updateComplete; - await new Promise(resolve => setTimeout(resolve, Tooltip.hoverHideDelay + 10)); - - expect(isPopoverOpen(explicitTooltip!)).to.be.false; - - await userEvent.hover(button!); - await el.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await new Promise(resolve => setTimeout(resolve, Tooltip.hoverShowDelay + 10)); - - expect(isPopoverOpen(explicitTooltip!)).to.be.true; - }); - - it('should show the explicit bulk action tooltip when hovering the sl-button proxy target', async () => { - await mountMultipleSelectGrid(html` - Bulk action tooltip - - Action 2 - - `); - - await openBulkActions(); - - const button = el.querySelector< - HTMLElement & { updateComplete?: Promise; renderRoot?: ShadowRoot } - >('sl-button[slot="bulk-actions"]'), - explicitTooltip = findTooltip('bulk-action-tooltip'); - - await button?.updateComplete; - - const proxyTarget = button?.renderRoot?.querySelector('button'); - - expect(proxyTarget).to.exist; - expect(explicitTooltip).to.exist; - - await userEvent.hover(proxyTarget!); - await el.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await new Promise(resolve => setTimeout(resolve, Tooltip.hoverShowDelay + 10)); - - expect(isPopoverOpen(explicitTooltip!)).to.be.true; - }); - - it('should switch between the cancel tooltip and a bulk action tooltip in the floating action bar', async () => { - await mountMultipleSelectGrid(html` - - Action 2 - - `); - - await openBulkActions(); - - const cancelButton = el.renderRoot.querySelector( - '[part="bulk-actions"] > sl-button:last-of-type' - ), - cancelTooltip = el.renderRoot.querySelector('#tooltip'), - bulkButton = el.querySelector< - HTMLElement & { updateComplete?: Promise; renderRoot?: ShadowRoot } - >('sl-button[slot="bulk-actions"]'); - - await bulkButton?.updateComplete; - - const bulkProxyTarget = bulkButton?.renderRoot?.querySelector('button'); - - expect(cancelButton).to.exist; - expect(cancelTooltip).to.exist; - expect(bulkButton).to.exist; - expect(bulkProxyTarget).to.exist; - - await userEvent.hover(cancelButton!); - await el.updateComplete; - await new Promise(resolve => setTimeout(resolve, Tooltip.hoverShowDelay + 50)); - - expect(isPopoverOpen(cancelTooltip!)).to.be.true; - - await userEvent.hover(bulkProxyTarget!); - await el.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); - await new Promise(resolve => setTimeout(resolve, Tooltip.hoverShowDelay + 50)); - - const bulkTooltip = findTooltipByText('I am a tooltip'); - - expect(bulkTooltip).to.exist; - expect(bulkTooltip?.tagName).to.equal('SL-TOOLTIP'); - expect(isPopoverOpen(cancelTooltip!)).to.be.false; - expect(isPopoverOpen(bulkTooltip!)).to.be.true; - }); - }); - describe('single select', () => { beforeEach(async () => { el = await fixture(html` diff --git a/packages/components/menu/src/menu-button.spec.ts b/packages/components/menu/src/menu-button.spec.ts index 28f7bade3c..6ca3e3ddec 100644 --- a/packages/components/menu/src/menu-button.spec.ts +++ b/packages/components/menu/src/menu-button.spec.ts @@ -130,10 +130,12 @@ describe('sl-menu-button', () => { }); describe('show', () => { - it('should show the menu when the button is clicked', () => { - button.click(); - + it('should toggle the menu when the button is clicked', async () => { + await userEvent.click(button); expect(menu).to.match(':popover-open'); + + await userEvent.click(button); + expect(menu).not.to.match(':popover-open'); }); it('should not show the menu when the button is clicked while disabled', async () => { @@ -145,7 +147,7 @@ describe('sl-menu-button', () => { expect(menu).not.to.match(':popover-open'); }); - it('should not move focus when the button is clicked without being focused', async () => { + it('should focus the first menu item when the button is clicked', async () => { // Ensure button is not focused document.body.focus(); @@ -153,7 +155,7 @@ describe('sl-menu-button', () => { await new Promise(resolve => setTimeout(resolve, 50)); expect(menu).to.match(':popover-open'); - expect(document.activeElement).not.to.equal(el.querySelector('sl-menu-item')); + expect(document.activeElement).to.equal(el.querySelector('sl-menu-item')); }); it('should focus the first menu item when opened with Enter while button is focused', async () => { @@ -269,7 +271,10 @@ describe('sl-menu-button', () => { expect(button.querySelector('.selected')).not.to.exist; }); - it('should mark the button as icon only', () => { + it('should mark the button as icon only', async () => { + // Wait for the MutationObserver to detect the sl-icon and update the icon-only state. + await new Promise(resolve => setTimeout(resolve, 50)); + expect(button).to.match(':state(icon-only)'); }); }); diff --git a/packages/components/menu/src/menu-button.ts b/packages/components/menu/src/menu-button.ts index 330e6267ad..84e392d1ff 100644 --- a/packages/components/menu/src/menu-button.ts +++ b/packages/components/menu/src/menu-button.ts @@ -68,8 +68,12 @@ export class MenuButton extends ForwardAriaMixin(ScopedElementsMixin(LitElement) } }); - /** The state of the menu popover. */ - #popoverState?: string; + /** + * Flag indicating whether the popover was just closed. We need to know this so we can properly + * handle button clicks that close the popover. If the popover was just closed, we don't want to + * show it again when the button click event fires. + */ + #popoverJustClosed = false; /** @internal The button. */ @query('sl-button') button!: Button; @@ -141,6 +145,7 @@ export class MenuButton extends ForwardAriaMixin(ScopedElementsMixin(LitElement) @@ -166,12 +172,22 @@ export class MenuButton extends ForwardAriaMixin(ScopedElementsMixin(LitElement) `; } + #onBeforeToggle(event: ToggleEvent): void { + if (event.newState === 'closed') { + this.#popoverJustClosed = true; + } + } + #onClick(): void { - if (this.#isDisabled()) { + if (this.#isDisabled() || this.#popoverJustClosed) { return; } this.menu.togglePopover(); + + if (this.menu.matches(':popover-open')) { + this.menu.focus(); + } } #onHostClick(event: Event): void { @@ -202,7 +218,7 @@ export class MenuButton extends ForwardAriaMixin(ScopedElementsMixin(LitElement) // Prevents the Escape key event from bubbling up, so that pressing 'Escape' inside the menu // does not close parent containers (such as dialogs). event.stopPropagation(); - } else if (this.#popoverState !== 'open' && event.key === 'ArrowDown') { + } else if (event.key === 'ArrowDown' && !this.menu.matches(':popover-open')) { this.menu.showPopover(); this.menu.focus(); } else { @@ -224,10 +240,25 @@ export class MenuButton extends ForwardAriaMixin(ScopedElementsMixin(LitElement) } } - #onMenuClick(event: Event & { target: HTMLElement }): void { + #onMenuClick(event: Event): void { + const menuItem = event.composedPath().find(el => el instanceof MenuItem); + // Only hide the menu if the user clicked on a menu item - if (event.composedPath().find(el => el instanceof MenuItem)) { - this.menu.hidePopover(); + if (menuItem) { + const focusVisible = menuItem.matches(':focus-visible'); + + // Pass the source, so we know if we need to focus the button in #onToggle + this.menu.togglePopover({ source: menuItem }); + + // Focus the button again after clicking a menu item + this.button.focus({ focusVisible }); + } + } + + #onPointerDown(event: PointerEvent): void { + if (this.menu.matches(':popover-open')) { + event.preventDefault(); + event.stopImmediatePropagation(); } } @@ -236,13 +267,13 @@ export class MenuButton extends ForwardAriaMixin(ScopedElementsMixin(LitElement) } #onToggle(event: ToggleEvent): void { - this.#popoverState = event.newState; + if (event.newState === 'closed') { + this.#popoverJustClosed = false; - if (event.newState === 'closed' && this.menu.matches(':focus-within')) { - this.button.focus(); - } else if (event.newState === 'open' && this.button.matches(':focus-within')) { - // If the menu is opening and the button is focused, move focus to the menu - this.menu.focus(); + // Only focus the button again if there is no source, aka Escape was pressed + if (!event.source && this.menu.matches(':focus-within')) { + this.button.focus(); + } } } diff --git a/packages/components/tag/src/tag-list.scss b/packages/components/tag/src/tag-list.scss index 656df24c3d..099e519839 100644 --- a/packages/components/tag/src/tag-list.scss +++ b/packages/components/tag/src/tag-list.scss @@ -1,9 +1,4 @@ :host { - --_border-color: var(--sl-color-background-secondary-subtle); - --_stack-margin: var(--sl-size-010); - --_stack-offset: calc(var(--sl-size-010) + var(--sl-size-025)); - --_stack-width: var(--sl-size-050); - display: flex; gap: var(--sl-size-050); } @@ -21,10 +16,6 @@ } } -:host([variant='info']) { - --_border-color: var(--sl-color-background-info-subtle); -} - ::slotted(*) { flex-shrink: 0; } @@ -32,31 +23,10 @@ .stack { display: flex; position: relative; - - &.double { - padding-inline-start: var(--_stack-offset); - } - - &.triple { - padding-inline-start: calc(var(--_stack-offset) * 2); - } } -.double::before, -.triple::before, -.triple::after { - border-color: var(--_border-color); - border-inline-start: calc(var(--_stack-offset) - var(--_stack-margin)) solid var(--_border-color); - border-radius: var(--sl-size-borderRadius-full); - content: ''; - display: flex; - inline-size: var(--_stack-width); - inset: 0 auto 0 0; - position: absolute; -} - -.triple::before { - inset-inline-start: var(--_stack-offset); +sl-tag::part(tooltip) { + max-inline-size: calc(5 * var(--sl-size-800)); } .list { @@ -64,7 +34,3 @@ flex-wrap: wrap; gap: var(--sl-size-050); } - -sl-tag::part(tooltip) { - max-inline-size: 300px; -} diff --git a/packages/components/tag/src/tag-list.spec.ts b/packages/components/tag/src/tag-list.spec.ts index 18ff6af8fb..1f0208fe4f 100644 --- a/packages/components/tag/src/tag-list.spec.ts +++ b/packages/components/tag/src/tag-list.spec.ts @@ -121,20 +121,16 @@ describe('sl-tag-list', () => { it('should have a tooltip for the stack', () => { const tag = el.renderRoot.querySelector('sl-tag'), - tooltip = el.renderRoot.querySelector('sl-tooltip'); + container = tag?.renderRoot.querySelector('[part="container"]'), + tooltip = tag?.renderRoot.querySelector('sl-tooltip'), + text = tooltip?.textContent?.trim() ?? ''; + expect(container?.ariaDescribedByElements).to.include(tooltip); expect(tooltip).to.exist; - expect(tooltip?.id).to.equal(tag?.getAttribute('aria-labelledby')); - - const tagContent = tooltip?.textContent?.trim(); - - expect(tagContent).to.exist; - expect(tagContent?.includes('List of hidden elements:')).to.be.true; - expect( - tagContent?.includes( - 'My label 1, My label 2, My label 3, My label 4, My label 5, My label 6, My label 7' - ) - ).to.be.true; + expect(text).to.include('List of hidden elements:'); + expect(text).to.include( + 'My label 1, My label 2, My label 3, My label 4, My label 5, My label 6, My label 7' + ); }); it('should have a stack with a tag, which contains the stack size', () => { @@ -211,16 +207,6 @@ describe('sl-tag-list', () => { expect(tag).to.have.trimmed.text('+6'); }); - it('should clear legacy stack decoration classes', async () => { - const stack = el.renderRoot.querySelector('.stack') as HTMLElement; - stack.classList.add('double', 'triple'); - - await triggerVisibilityUpdate(); - - expect(stack).not.to.have.class('double'); - expect(stack).not.to.have.class('triple'); - }); - it('should stop observing the previous stack element when stacked mode is disabled', async () => { const stack = el.renderRoot.querySelector('.stack') as HTMLElement; const unobserveSpy = vi.spyOn(ResizeObserver.prototype, 'unobserve'); diff --git a/packages/components/tag/src/tag-list.ts b/packages/components/tag/src/tag-list.ts index b46cc94729..b633ff5bad 100644 --- a/packages/components/tag/src/tag-list.ts +++ b/packages/components/tag/src/tag-list.ts @@ -483,8 +483,6 @@ export class TagList extends ScopedElementsMixin(LitElement) { 0 ); this.stack.style.display = this.stackSize === 0 ? 'none' : ''; - // Ensure legacy decoration classes are not kept on existing elements (e.g. after HMR). - this.stack.classList.remove('double', 'triple'); const stackTag = this.stack.querySelector('sl-tag'); diff --git a/packages/components/tag/src/tag.spec.ts b/packages/components/tag/src/tag.spec.ts index ba411ed18b..035e148ea8 100644 --- a/packages/components/tag/src/tag.spec.ts +++ b/packages/components/tag/src/tag.spec.ts @@ -50,15 +50,45 @@ describe('sl-tag', () => { expect(el.renderRoot.querySelector('button')).to.exist; }); - it('should not have a tabindex', () => { - expect(el).not.to.have.attribute('tabindex'); + it('should not have a tooltip', () => { + expect(el.tooltip).to.be.undefined; + expect(el.renderRoot.querySelector('sl-tooltip')).not.to.exist; }); - it('should not have a tooltip', async () => { - el.focus(); + it('should have a tooltip when set', async () => { + el.tooltip = 'Tooltip text'; await el.updateComplete; - expect(el).not.to.have.attribute('aria-describedby'); + const container = el.renderRoot.querySelector('[part="container"]'), + tooltip = el.renderRoot.querySelector('sl-tooltip'); + + expect(container?.ariaDescribedByElements).to.include(tooltip); + expect(tooltip).to.exist; + expect(tooltip).to.have.trimmed.text('Tooltip text'); + }); + + it('should not be focusable', () => { + const container = el.renderRoot.querySelector('[part="container"]'); + + expect(container).not.to.have.attribute('tabindex'); + }); + + it('should be focusable when removable', async () => { + el.removable = true; + await el.updateComplete; + + const container = el.renderRoot.querySelector('[part="container"]'); + + expect(container).to.have.attribute('tabindex', '0'); + }); + + it('should be focusable when there is a tooltip', async () => { + el.tooltip = 'Tooltip text'; + await el.updateComplete; + + const container = el.renderRoot.querySelector('[part="container"]'); + + expect(container).to.have.attribute('tabindex', '0'); }); }); @@ -68,27 +98,14 @@ describe('sl-tag', () => { }); it('should have an ARIA description indicating how to remove the tag', () => { - expect(el).to.have.attribute( + const container = el.renderRoot.querySelector('[part="container"]'); + + expect(container).to.have.attribute( 'aria-description', 'Press the delete or backspace key to remove this item' ); }); - it('should have a tabindex of 0', () => { - expect(el).to.have.attribute('tabindex', '0'); - }); - - it('should have a tabindex of -1 when disabled', async () => { - el.disabled = true; - await el.updateComplete; - - expect(el).to.have.attribute('tabindex', '-1'); - }); - - it('should have a button', () => { - expect(el.renderRoot.querySelector('button')).to.exist; - }); - it('should hide the button for ARIA', () => { expect(el.renderRoot.querySelector('button')).to.have.attribute('aria-hidden', 'true'); }); @@ -103,15 +120,6 @@ describe('sl-tag', () => { expect(el).to.exist; }); - it('should be removed when the button is clicked using the keyboard', async () => { - const onRemove = spy(el, 'remove'); - - el.renderRoot.querySelector('button')?.focus(); - await userEvent.keyboard('{Enter}'); - - expect(onRemove).to.have.been.calledOnce; - }); - it('should be removed when the backspace key is pressed', async () => { const onRemove = spy(el, 'remove'); @@ -130,12 +138,11 @@ describe('sl-tag', () => { expect(onRemove).to.have.been.calledOnce; }); - it('should emit an sl-remove event when a remove button is clicked', async () => { + it('should emit an sl-remove event when a remove button is clicked', () => { const onRemove = spy(); el.addEventListener('sl-remove', onRemove); el.renderRoot.querySelector('button')?.click(); - await el.updateComplete; expect(onRemove).to.have.been.calledOnce; }); @@ -151,13 +158,11 @@ describe('sl-tag', () => { await new Promise(resolve => setTimeout(resolve, 50)); }); - it('should have a tooltip when the label is too long', async () => { - el.focus(); - await el.updateComplete; - - expect(el).to.have.attribute('aria-describedby'); + it('should have a tooltip when the label is too long', () => { + const container = el.renderRoot.querySelector('[part="container"]'), + tooltip = el.renderRoot.querySelector('sl-tooltip'); - const tooltip = document.getElementById(el.getAttribute('aria-describedby')!); + expect(container?.ariaDescribedByElements).to.include(tooltip); expect(tooltip).to.exist; expect(tooltip).to.have.trimmed.text('My label is very long'); }); diff --git a/packages/components/tooltip/src/tooltip.ts b/packages/components/tooltip/src/tooltip.ts index 18e9b44226..99a73fdd92 100644 --- a/packages/components/tooltip/src/tooltip.ts +++ b/packages/components/tooltip/src/tooltip.ts @@ -189,7 +189,7 @@ export class Tooltip extends LitElement { }; #onFocus = (): void => { - if (this.#hasTrigger('focus')) { + if (this.#hasTrigger('focus') && this.anchor?.matches(':focus-visible')) { this.showPopover(); } }; diff --git a/vitest.config.ts b/vitest.config.ts index 18ed741ae6..b0e41802c8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -20,8 +20,7 @@ export default defineConfig({ headless: true, provider: playwright(), instances: [{ browser: 'chromium' }] - }, - setupFiles: ['.storybook/vitest.setup.ts'] + } } }, { From a2605091145c96ae7519394231bd3bc7240f97de Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 27 May 2026 14:58:24 +0200 Subject: [PATCH 23/50] =?UTF-8?q?=E2=9D=84=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/tooltip/src/tooltip.spec.ts | 493 ++++++++++++++++++ packages/components/tooltip/src/tooltip.ts | 12 + 2 files changed, 505 insertions(+) create mode 100644 packages/components/tooltip/src/tooltip.spec.ts diff --git a/packages/components/tooltip/src/tooltip.spec.ts b/packages/components/tooltip/src/tooltip.spec.ts new file mode 100644 index 0000000000..601525f482 --- /dev/null +++ b/packages/components/tooltip/src/tooltip.spec.ts @@ -0,0 +1,493 @@ +import { fixture } from '@sl-design-system/vitest-browser-lit'; +import { html } from 'lit'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { userEvent } from 'vitest/browser'; +import '../register.js'; +import { Tooltip } from './tooltip.js'; + +describe('sl-tooltip', () => { + let el: HTMLElement, anchor: HTMLElement, tooltip: Tooltip; + + const waitFor = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + + describe('defaults', () => { + beforeEach(async () => { + el = await fixture(html` +
+ + Tooltip text +
+ `); + anchor = el.querySelector('#btn')!; + tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; + }); + + it('should have aria-hidden="true"', () => { + expect(tooltip).to.have.attribute('aria-hidden', 'true'); + }); + + it('should have popover="manual"', () => { + expect(tooltip).to.have.attribute('popover', 'manual'); + }); + + it('should have role="tooltip"', () => { + expect(tooltip).to.have.attribute('role', 'tooltip'); + }); + + it('should have an auto-generated id', () => { + expect(tooltip.id).to.match(/^sl-tooltip-\d+$/); + }); + + it('should not be open by default', () => { + expect(tooltip.matches(':popover-open')).to.be.false; + }); + + it('should have a hover-bridge part element', () => { + expect(tooltip.renderRoot.querySelector('[part="hover-bridge"]')).to.exist; + }); + + it('should default trigger to "focus hover"', () => { + expect(tooltip.trigger).to.equal('focus hover'); + }); + + it('should assign slotted text to the default slot', () => { + const text = tooltip.renderRoot + .querySelector('slot') + ?.assignedNodes({ flatten: true }) + .filter(node => node.nodeType === Node.TEXT_NODE) + .map(node => node.textContent) + .join(); + + expect(text).to.equal('Tooltip text'); + }); + }); + + describe('anchor binding', () => { + beforeEach(async () => { + el = await fixture(html` +
+ + Tip +
+ `); + anchor = el.querySelector('#anchor')!; + tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; + }); + + it('should bind to the anchor element referenced by the "for" attribute', () => { + expect(tooltip.anchor).to.equal(anchor); + }); + + it('should set ariaLabelledByElements on the anchor by default', () => { + expect(anchor.ariaLabelledByElements).to.include(tooltip); + }); + + it('should set anchor-name on the anchor', () => { + expect(anchor.style.anchorName).to.equal(`--${tooltip.id}`); + }); + + it('should set position-anchor on the tooltip', () => { + expect(tooltip.style.positionAnchor).to.equal(`--${tooltip.id}`); + }); + + it('should clear the anchor when "for" is removed', async () => { + tooltip.removeAttribute('for'); + await tooltip.updateComplete; + + expect(tooltip.anchor).to.be.undefined; + }); + + it('should clear the ARIA relation when "for" is removed', async () => { + tooltip.removeAttribute('for'); + await tooltip.updateComplete; + + expect(anchor.ariaLabelledByElements ?? []).not.to.include(tooltip); + }); + + it('should clear anchor-name and position-anchor when "for" is removed', async () => { + tooltip.removeAttribute('for'); + await tooltip.updateComplete; + + expect(anchor.style.anchorName).to.equal(''); + expect(tooltip.style.positionAnchor).to.equal(''); + }); + + it('should not overwrite an existing anchor-name on the anchor', async () => { + const newEl = await fixture(html` +
+ + Tip +
+ `); + const preNamed = newEl.querySelector('#pre-named')!; + const tip = newEl.querySelector('sl-tooltip')!; + await tip.updateComplete; + + expect(preNamed.style.anchorName).to.equal('--my-anchor'); + expect(tip.style.positionAnchor).to.equal('--my-anchor'); + }); + + it('should remove the ARIA relation when disconnected', async () => { + tooltip.remove(); + await tooltip.updateComplete; + + expect(anchor.ariaLabelledByElements ?? []).not.to.include(tooltip); + }); + }); + + describe('type', () => { + beforeEach(async () => { + el = await fixture(html` +
+ + Tip +
+ `); + anchor = el.querySelector('#t-anchor')!; + tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; + }); + + it('should use ariaLabelledByElements when type is undefined (default)', () => { + expect(anchor.ariaLabelledByElements).to.include(tooltip); + expect(anchor.ariaDescribedByElements ?? []).not.to.include(tooltip); + }); + + it('should use ariaLabelledByElements when type is "label"', async () => { + tooltip.type = 'label'; + await tooltip.updateComplete; + + expect(anchor.ariaLabelledByElements).to.include(tooltip); + expect(anchor.ariaDescribedByElements ?? []).not.to.include(tooltip); + }); + + it('should use ariaDescribedByElements when type is "description"', async () => { + tooltip.type = 'description'; + await tooltip.updateComplete; + + expect(anchor.ariaDescribedByElements).to.include(tooltip); + expect(anchor.ariaLabelledByElements ?? []).not.to.include(tooltip); + }); + + it('should switch ARIA relation when type changes from label to description', async () => { + tooltip.type = 'label'; + await tooltip.updateComplete; + + tooltip.type = 'description'; + await tooltip.updateComplete; + + expect(anchor.ariaDescribedByElements).to.include(tooltip); + expect(anchor.ariaLabelledByElements ?? []).not.to.include(tooltip); + }); + + it('should switch ARIA relation when type changes from description to label', async () => { + tooltip.type = 'description'; + await tooltip.updateComplete; + + tooltip.type = 'label'; + await tooltip.updateComplete; + + expect(anchor.ariaLabelledByElements).to.include(tooltip); + expect(anchor.ariaDescribedByElements ?? []).not.to.include(tooltip); + }); + }); + + describe('disabled', () => { + beforeEach(async () => { + el = await fixture(html` +
+ + Tip +
+ `); + anchor = el.querySelector('#d-anchor')!; + tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; + }); + + it('should not open when disabled and mouseover is dispatched', async () => { + anchor.dispatchEvent(new Event('mouseover', { bubbles: true })); + await waitFor(Tooltip.hoverShowDelay + 10); + + expect(tooltip.matches(':popover-open')).to.be.false; + }); + + it('should close when disabled while open', async () => { + tooltip.disabled = false; + await tooltip.updateComplete; + + tooltip.showPopover(); + expect(tooltip.matches(':popover-open')).to.be.true; + + tooltip.disabled = true; + await tooltip.updateComplete; + + expect(tooltip.matches(':popover-open')).to.be.false; + }); + }); + + describe('open property', () => { + beforeEach(async () => { + el = await fixture(html` +
+ + Tip +
+ `); + anchor = el.querySelector('#o-anchor')!; + tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; + }); + + it('should show the tooltip when open is set to true', async () => { + tooltip.open = true; + await tooltip.updateComplete; + + expect(tooltip.matches(':popover-open')).to.be.true; + }); + + it('should hide the tooltip when open is set to false after being shown', async () => { + tooltip.open = true; + await tooltip.updateComplete; + + tooltip.open = false; + await tooltip.updateComplete; + + expect(tooltip.matches(':popover-open')).to.be.false; + }); + }); + + describe('hover trigger', () => { + beforeEach(async () => { + el = await fixture(html` +
+ + Tip +
+ `); + anchor = el.querySelector('#h-anchor')!; + tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; + }); + + it('should show the tooltip after hovering the anchor', async () => { + anchor.dispatchEvent(new Event('mouseover', { bubbles: true })); + await waitFor(Tooltip.hoverShowDelay + 10); + + expect(tooltip.matches(':popover-open')).to.be.true; + }); + + it('should not show the tooltip before the hover delay elapses', async () => { + anchor.dispatchEvent(new Event('mouseover', { bubbles: true })); + await waitFor(Math.max(0, Tooltip.hoverShowDelay - 10)); + + expect(tooltip.matches(':popover-open')).to.be.false; + }); + + it('should hide the tooltip when mousing out of the anchor', async () => { + anchor.dispatchEvent(new Event('mouseover', { bubbles: true })); + await waitFor(Tooltip.hoverShowDelay + 10); + + anchor.dispatchEvent(new Event('mouseout', { bubbles: true })); + await tooltip.updateComplete; + await waitFor(Tooltip.hoverHideDelay + 10); + + expect(tooltip.matches(':popover-open')).to.be.false; + }); + + it('should not show when trigger does not include hover', async () => { + tooltip.trigger = 'focus'; + await tooltip.updateComplete; + + anchor.dispatchEvent(new Event('mouseover', { bubbles: true })); + await waitFor(Tooltip.hoverShowDelay + 10); + + expect(tooltip.matches(':popover-open')).to.be.false; + }); + }); + + describe('focus trigger', () => { + beforeEach(async () => { + el = await fixture(html` +
+ + Tip +
+ `); + anchor = el.querySelector('#f-anchor')!; + tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; + }); + + it('should show the tooltip on focus (when :focus-visible)', async () => { + await userEvent.tab(); + await tooltip.updateComplete; + + expect(tooltip.matches(':popover-open')).to.be.true; + }); + + it('should hide the tooltip on blur', async () => { + await userEvent.tab(); + await tooltip.updateComplete; + + anchor.blur(); + await tooltip.updateComplete; + + expect(tooltip.matches(':popover-open')).to.be.false; + }); + + it('should not show when trigger does not include focus', async () => { + tooltip.trigger = 'hover'; + await tooltip.updateComplete; + + await userEvent.tab(); + await tooltip.updateComplete; + + expect(tooltip.matches(':popover-open')).to.be.false; + }); + }); + + describe('click trigger', () => { + beforeEach(async () => { + el = await fixture(html` +
+ + Tip +
+ `); + anchor = el.querySelector('#c-anchor')!; + tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; + }); + + it('should show the tooltip on click', async () => { + anchor.dispatchEvent(new Event('click', { bubbles: true })); + await tooltip.updateComplete; + + expect(tooltip.matches(':popover-open')).to.be.true; + }); + + it('should hide the tooltip on a second click', async () => { + anchor.dispatchEvent(new Event('click', { bubbles: true })); + await tooltip.updateComplete; + + anchor.dispatchEvent(new Event('click', { bubbles: true })); + await tooltip.updateComplete; + + expect(tooltip.matches(':popover-open')).to.be.false; + }); + + it('should hide the tooltip when clicking the anchor while a hover-triggered open is active', async () => { + // trigger=click only, so hover does nothing; click toggles + tooltip.showPopover(); + anchor.dispatchEvent(new Event('click', { bubbles: true })); + await tooltip.updateComplete; + + expect(tooltip.matches(':popover-open')).to.be.false; + }); + }); + + describe('manual trigger', () => { + beforeEach(async () => { + el = await fixture(html` +
+ + Tip +
+ `); + anchor = el.querySelector('#m-anchor')!; + tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; + }); + + it('should not show on hover', async () => { + anchor.dispatchEvent(new Event('mouseover', { bubbles: true })); + await waitFor(Tooltip.hoverShowDelay + 10); + + expect(tooltip.matches(':popover-open')).to.be.false; + }); + + it('should not show on focus', async () => { + await userEvent.tab(); + await tooltip.updateComplete; + + expect(tooltip.matches(':popover-open')).to.be.false; + }); + + it('should show when showPopover is called programmatically', () => { + tooltip.showPopover(); + + expect(tooltip.matches(':popover-open')).to.be.true; + }); + }); + + describe('keyboard', () => { + beforeEach(async () => { + el = await fixture(html` +
+ + Tip +
+ `); + anchor = el.querySelector('#k-anchor')!; + tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; + + tooltip.showPopover(); + }); + + it('should hide the tooltip when pressing Escape', async () => { + await userEvent.keyboard('{Escape}'); + await tooltip.updateComplete; + + expect(tooltip.matches(':popover-open')).to.be.false; + }); + }); + + describe('delay (fake timers)', () => { + beforeEach(async () => { + vi.useFakeTimers(); + + el = await fixture(html` +
+ + Tip +
+ `); + anchor = el.querySelector('#delay-anchor')!; + tooltip = el.querySelector('sl-delay-anchor')! ?? el.querySelector('sl-tooltip')!; + tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('should stay closed before the hover show delay elapses', async () => { + anchor.dispatchEvent(new Event('mouseover', { bubbles: true })); + await vi.advanceTimersByTimeAsync(Math.max(0, Tooltip.hoverShowDelay - 10)); + + expect(tooltip.matches(':popover-open')).to.be.false; + }); + + it('should open after the hover show delay elapses', async () => { + anchor.dispatchEvent(new Event('mouseover', { bubbles: true })); + await vi.advanceTimersByTimeAsync(Tooltip.hoverShowDelay + 10); + + expect(tooltip.matches(':popover-open')).to.be.true; + }); + + it('should cancel pending show when mouse leaves before delay elapses', async () => { + anchor.dispatchEvent(new Event('mouseover', { bubbles: true })); + await vi.advanceTimersByTimeAsync(Tooltip.hoverShowDelay - 10); + + anchor.dispatchEvent(new Event('mouseout', { bubbles: true })); + await vi.advanceTimersByTimeAsync(Tooltip.hoverShowDelay + 100); + + expect(tooltip.matches(':popover-open')).to.be.false; + }); + }); +}); diff --git a/packages/components/tooltip/src/tooltip.ts b/packages/components/tooltip/src/tooltip.ts index 99a73fdd92..10790a5536 100644 --- a/packages/components/tooltip/src/tooltip.ts +++ b/packages/components/tooltip/src/tooltip.ts @@ -327,6 +327,18 @@ export class Tooltip extends LitElement { #updateAnchor(): void { if (!this.for) { + const oldAnchor = this.anchor; + if (oldAnchor) { + this.#removeAriaRelation(oldAnchor, this.type); + oldAnchor.removeEventListener('blur', this.#onBlur, { capture: true }); + oldAnchor.removeEventListener('click', this.#onClick); + oldAnchor.removeEventListener('focus', this.#onFocus, { capture: true }); + oldAnchor.removeEventListener('mouseover', this.#onMouseOver); + oldAnchor.removeEventListener('mouseout', this.#onMouseOut); + oldAnchor.style.anchorName = ''; + this.style.positionAnchor = ''; + } + this.anchor = undefined; return; } From 5236177d1524c795ba818d0924e7112d3e98f51e Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 27 May 2026 15:38:29 +0200 Subject: [PATCH 24/50] =?UTF-8?q?=F0=9F=94=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/button/src/button.stories.ts | 33 +++++++++-------- packages/components/tooltip/src/tooltip.ts | 37 ++++++++++--------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/packages/components/button/src/button.stories.ts b/packages/components/button/src/button.stories.ts index 2711b3266e..2f21810707 100644 --- a/packages/components/button/src/button.stories.ts +++ b/packages/components/button/src/button.stories.ts @@ -148,6 +148,18 @@ export const Disabled: Story = { } }; +export const DoubleLabel: Story = { + render: () => html` +

+ This button has two labels: this text and the tooltip set via the + tooltip property. +

+ + + + ` +}; + export const IconOnly: Story = { render: ({ fill, shape, size, variant }) => { return html` @@ -224,31 +236,31 @@ export const All: Story = { Small Button - + Button - + Medium Button - + Button - + Large Button - + Button - + @@ -513,12 +525,3 @@ export const All: Story = { `; } }; - -export const DoubleLabel: Story = { - render: () => html` -

This is the button's label as well.

- - - - ` -}; diff --git a/packages/components/tooltip/src/tooltip.ts b/packages/components/tooltip/src/tooltip.ts index 10790a5536..d35a38c482 100644 --- a/packages/components/tooltip/src/tooltip.ts +++ b/packages/components/tooltip/src/tooltip.ts @@ -323,20 +323,31 @@ export class Tooltip extends LitElement { bridge.style.width = `${width}px`; bridge.style.height = `${height}px`; bridge.style.clipPath = polygon; + bridge.style.display = ''; + } + + #cleanupAnchor(anchor: HTMLElement, type: 'description' | 'label' | undefined): void { + this.#removeAriaRelation(anchor, type); + + anchor.removeEventListener('blur', this.#onBlur, { capture: true }); + anchor.removeEventListener('click', this.#onClick); + anchor.removeEventListener('focus', this.#onFocus, { capture: true }); + anchor.removeEventListener('mouseover', this.#onMouseOver); + anchor.removeEventListener('mouseout', this.#onMouseOut); + + // Only clear the anchorName if it was set by us. + if (anchor.style.anchorName === this.style.positionAnchor) { + anchor.style.anchorName = ''; + } + + this.style.positionAnchor = ''; } #updateAnchor(): void { if (!this.for) { const oldAnchor = this.anchor; if (oldAnchor) { - this.#removeAriaRelation(oldAnchor, this.type); - oldAnchor.removeEventListener('blur', this.#onBlur, { capture: true }); - oldAnchor.removeEventListener('click', this.#onClick); - oldAnchor.removeEventListener('focus', this.#onFocus, { capture: true }); - oldAnchor.removeEventListener('mouseover', this.#onMouseOver); - oldAnchor.removeEventListener('mouseout', this.#onMouseOut); - oldAnchor.style.anchorName = ''; - this.style.positionAnchor = ''; + this.#cleanupAnchor(oldAnchor, this.type); } this.anchor = undefined; @@ -374,15 +385,7 @@ export class Tooltip extends LitElement { } if (oldAnchor) { - this.#removeAriaRelation(oldAnchor, this.type); - - oldAnchor.removeEventListener('blur', this.#onBlur, { capture: true }); - oldAnchor.removeEventListener('click', this.#onClick); - oldAnchor.removeEventListener('focus', this.#onFocus, { capture: true }); - oldAnchor.removeEventListener('mouseover', this.#onMouseOver); - oldAnchor.removeEventListener('mouseout', this.#onMouseOut); - oldAnchor.style.anchorName = ''; - this.style.positionAnchor = ''; + this.#cleanupAnchor(oldAnchor, this.type); } this.anchor = newAnchor; From 0cbe05c67f97457ee8a74fa08d1a1fb43cfd6ece Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 27 May 2026 15:50:05 +0200 Subject: [PATCH 25/50] =?UTF-8?q?=F0=9F=98=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/button/src/button.spec.ts | 21 ------------------- .../lib/rules/button-has-label.js | 8 ++++--- .../tests/lib/rules/button-has-label.test.js | 8 +++++++ 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/packages/components/button/src/button.spec.ts b/packages/components/button/src/button.spec.ts index b94fac503a..c397876bc9 100644 --- a/packages/components/button/src/button.spec.ts +++ b/packages/components/button/src/button.spec.ts @@ -719,27 +719,6 @@ describe('sl-button', () => { expect(el.renderRoot.querySelector('sl-tooltip')).to.be.null; }); - it('should include both the tooltip and aria-describedby element in ariaDescribedByElements', async () => { - const wrapper = await fixture(html` -
- Additional description - Click me -
- `); - - el = wrapper.querySelector('sl-button')!; - - const tooltipEl = el.renderRoot.querySelector('sl-tooltip')!, - descEl = wrapper.querySelector('#btn-desc')!, - ariaDescElements = getForwardedAriaProperty( - el, - 'ariaDescribedByElements' as keyof HTMLElement - ) as Element[]; - - expect(ariaDescElements).to.include(descEl); - expect(ariaDescElements).to.include(tooltipEl); - }); - it('should include both the tooltip and aria-describedby element in ariaDescribedByElements', async () => { const wrapper = await fixture(html`
diff --git a/tools/eslint-plugin-slds/lib/rules/button-has-label.js b/tools/eslint-plugin-slds/lib/rules/button-has-label.js index 75a76e5dca..810b5f9e33 100644 --- a/tools/eslint-plugin-slds/lib/rules/button-has-label.js +++ b/tools/eslint-plugin-slds/lib/rules/button-has-label.js @@ -40,13 +40,15 @@ export const buttonHasLabel = { return; } + // The `tooltip` attribute on sl-button provides the accessible label for icon-only + // buttons at runtime, so it counts as an accessible name at lint time. + const hasTooltipAttribute = ((element.attribs ?? {})['tooltip'] ?? '').trim() !== ''; + if ( hasTextContent(element) || hasAccessibleName(element) || hasTooltipWithAriaRelationLabel || - // The `tooltip` attribute on sl-button provides the accessible label for icon-only - // buttons at runtime, so it counts as an accessible name at lint time. - 'tooltip' in (element.attribs ?? {}) + hasTooltipAttribute ) { return; } diff --git a/tools/eslint-plugin-slds/tests/lib/rules/button-has-label.test.js b/tools/eslint-plugin-slds/tests/lib/rules/button-has-label.test.js index c5fb2dd9ac..364859aefb 100644 --- a/tools/eslint-plugin-slds/tests/lib/rules/button-has-label.test.js +++ b/tools/eslint-plugin-slds/tests/lib/rules/button-has-label.test.js @@ -81,6 +81,14 @@ ruleTester.run('button-has-label', buttonHasLabel, { { code: "html``;", errors: [{ messageId: 'mustBeAriaRelationLabel' }] + }, + { + code: "html``;", + errors: [{ messageId: 'missingText' }] + }, + { + code: 'html``;', + errors: [{ messageId: 'missingText' }] } ] }); From e6eb7d3de4fc9fd79bb679f351d51c1e7f75240b Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 27 May 2026 15:54:25 +0200 Subject: [PATCH 26/50] =?UTF-8?q?=F0=9F=8C=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/checkbox/src/checkbox.stories.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/components/checkbox/src/checkbox.stories.ts b/packages/components/checkbox/src/checkbox.stories.ts index a430a8ee1a..3b07f9f764 100644 --- a/packages/components/checkbox/src/checkbox.stories.ts +++ b/packages/components/checkbox/src/checkbox.stories.ts @@ -237,11 +237,7 @@ export const Indeterminate: StoryObj = { export const NoVisibleLabel: StoryObj = { render: () => { return html` -

- This checkbox has no internal or external label. It only has an - aria-label attribute. That attribute is automatically applied to the - input element. -

+

This checkbox has no label. It uses a tooltip as the label.

Toggle me `; From 36c87aea2608a2e2aa0e7d66a52e56f8c26fc873 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Thu, 28 May 2026 09:42:13 +0200 Subject: [PATCH 27/50] =?UTF-8?q?=F0=9F=8D=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/tag/src/tag-list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/tag/src/tag-list.ts b/packages/components/tag/src/tag-list.ts index b633ff5bad..5053f616e3 100644 --- a/packages/components/tag/src/tag-list.ts +++ b/packages/components/tag/src/tag-list.ts @@ -247,7 +247,7 @@ export class TagList extends ScopedElementsMixin(LitElement) { .map(tag => tag.label) .join(', '); - tooltip = `${label}:${tags}`; + tooltip = `${label}: ${tags}`; } return html` From 5d7c66490590e4ce56123c646304dedbf2d8e870 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Mon, 1 Jun 2026 08:54:54 +0200 Subject: [PATCH 28/50] =?UTF-8?q?=F0=9F=9A=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../breadcrumbs/src/breadcrumbs.stories.ts | 15 +- .../components/breadcrumbs/src/breadcrumbs.ts | 293 +++++++++--------- 2 files changed, 159 insertions(+), 149 deletions(-) diff --git a/packages/components/breadcrumbs/src/breadcrumbs.stories.ts b/packages/components/breadcrumbs/src/breadcrumbs.stories.ts index 8adeed6771..a1c577e6f7 100644 --- a/packages/components/breadcrumbs/src/breadcrumbs.stories.ts +++ b/packages/components/breadcrumbs/src/breadcrumbs.stories.ts @@ -130,13 +130,16 @@ export const CustomHome: Story = { export const Overflow: Story = { args: { breadcrumbs: () => html` + + Commodo nisi ut mollit adipisicing esse fugiat Lorem irure do. + Adipisicing sint excepteur officia voluptate. - Nostrud ad fugiat amet officia anim qui sit tempor veniam magna. - Lorem adipisicing do duis sunt laboris magna officia irure fugiat. + + Nostrud ad fugiat amet officia anim qui sit tempor veniam magna. + + + Lorem adipisicing do duis sunt laboris magna officia irure fugiat. + ` } }; diff --git a/packages/components/breadcrumbs/src/breadcrumbs.ts b/packages/components/breadcrumbs/src/breadcrumbs.ts index 625f593f62..22e9dcad2c 100644 --- a/packages/components/breadcrumbs/src/breadcrumbs.ts +++ b/packages/components/breadcrumbs/src/breadcrumbs.ts @@ -27,10 +27,8 @@ declare global { } export interface Breadcrumb { - collapsed?: boolean; - label: string; - tooltip?: Tooltip | (() => void); - url?: string; + element: Element; + tooltip: Tooltip; } /** @@ -54,11 +52,6 @@ const isMobile = (): boolean => matchMedia('(width <= 600px)').matches; */ @localized() export class Breadcrumbs extends ScopedElementsMixin(LitElement) { - static override shadowRootOptions: ShadowRootInit = { - ...LitElement.shadowRootOptions, - slotAssignment: 'manual' - }; - /** * When true, doesn't show a home label in the first breadcrumb next to the home icon. * @@ -93,14 +86,20 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { }; } + /** @internal */ + static override shadowRootOptions: ShadowRootInit = { + ...LitElement.shadowRootOptions, + slotAssignment: 'manual' + }; + /** @internal */ static override styles: CSSResultGroup = styles; - /** @internal Because of the manual slot assignment we need to observe mutations */ + /** Because of the manual slot assignment we need to observe mutations */ #mutationObserver = new MutationObserver(() => this.#onMutation()); /** Observe changes in size, so we can check whether we need to show tooltips for truncated links. */ - #observer = new ResizeObserver(() => this.#update()); + #resizeObserver = new ResizeObserver(() => this.#onResize()); /** Map to keep track of cleanup functions for tooltips associated with breadcrumb links. */ #tooltipCleanupFunctions = new Map void>(); @@ -109,10 +108,10 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { #updateScheduled = false; /** @internal The slotted breadcrumbs. */ - @state() breadcrumbLinks: HTMLElement[] = []; + @state() breadcrumbs: Breadcrumb[] = []; /** @internal The slotted custom home link, if any. */ - @state() customHomeLink: HTMLElement | undefined = undefined; + @state() customHomeLink?: Element; /** @internal The threshold for when breadcrumbs should be collapsed into a menu. */ @state() collapseThreshold = COLLAPSE_THRESHOLD; @@ -163,14 +162,12 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { this.setAttribute('role', 'navigation'); - this.#observer.observe(this); - this.#mutationObserver.observe(this, { - childList: true - }); + this.#mutationObserver.observe(this, { childList: true }); + this.#resizeObserver.observe(this); } override disconnectedCallback(): void { - this.#observer.disconnect(); + this.#resizeObserver.disconnect(); this.#mutationObserver.disconnect(); // Call cleanup functions to remove event listeners before removing tooltips @@ -178,22 +175,22 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { this.#tooltipCleanupFunctions.clear(); // Clean up any tooltips projected into or assigned to the "tooltips" slot to avoid memory leaks. - const tooltipsSlot = this.renderRoot?.querySelector('slot[name="tooltips"]'); - - if (tooltipsSlot) { - tooltipsSlot.assignedElements({ flatten: true }).forEach(tooltip => { - tooltip.remove(); - }); - } + this.renderRoot + ?.querySelector('slot[name="tooltips"]') + ?.assignedElements({ flatten: true }) + .forEach(tooltip => tooltip.remove()); // Also remove any light DOM elements explicitly using slot="tooltips" for backwards compatibility. - this.querySelectorAll('[slot="tooltips"]').forEach(tooltip => { - tooltip.remove(); - }); + this.querySelectorAll('[slot="tooltips"]').forEach(tooltip => tooltip.remove()); super.disconnectedCallback(); } + override firstUpdated(): void { + // Don't trigger a lifecycle loop by calling this in a rAF callback + requestAnimationFrame(() => this.#onMutation()); + } + override render(): TemplateResult { return html` @@ -221,7 +218,7 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { `} - ${this.breadcrumbLinks.length > this.collapseThreshold + ${this.breadcrumbs.length > this.collapseThreshold ? html`
  • - ${this.breadcrumbLinks + ${this.breadcrumbs .slice(0, -this.collapseThreshold) .map( (_, index) => @@ -244,102 +241,37 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { ` : nothing} - ${this.breadcrumbLinks - .slice(Math.max(0, this.breadcrumbLinks.length - this.collapseThreshold)) - .map( - (_, index, array) => html` -
  • - ${index < array.length - 1 - ? html`` - : nothing} - ` - )} + ${this.breadcrumbs.slice(Math.max(0, this.breadcrumbs.length - this.collapseThreshold)).map( + (_, index, array) => html` +
  • + ${index < array.length - 1 + ? html`` + : nothing} + ` + )} `; } - override firstUpdated(): void { - // Process initial light DOM children after first render - this.#processChildren(); - // Perform slot assignments after first render - this.#assignSlots(); - } - #onClick = (): void => { this.renderRoot.querySelector('sl-popover')?.togglePopover(); }; - #processChildren(): void { - const children = Array.from(this.children); - - // Filter for elements without a slot attribute (default slot content) - this.breadcrumbLinks = children - .filter(el => !el.hasAttribute('slot') && !(el instanceof Tooltip)) - .map(el => el as HTMLElement); - this.customHomeLink = children.find(el => el.getAttribute('slot') === 'home') as - | HTMLElement - | undefined; - } - - #assignSlots(): void { - requestAnimationFrame(() => { - if (this.customHomeLink) { - const slot = this.renderRoot.querySelector('slot[name="home"]') as HTMLSlotElement; - slot?.assign(this.customHomeLink); - } - // Assign breadcrumb links to either the menu area based on the collapse threshold - this.breadcrumbLinks.slice(0, -this.collapseThreshold).forEach((link, index) => { - const slot = this.renderRoot.querySelector( - `slot[name="breadcrumb-menu-${index}"]` - ) as HTMLSlotElement; - link.removeAttribute('aria-current'); - if (link.hasAttribute('data-has-tooltip') && link.hasAttribute('aria-describedby')) { - // Note: No need to call cleanup() here - it was already called when the tooltip was created - this.#tooltipCleanupFunctions.delete(link); - - const tooltipsSlot = this.renderRoot.querySelector( - 'slot[name="tooltips"]' - ) as HTMLSlotElement; - const tooltip = tooltipsSlot - .assignedElements() - .find(el => el.id === link.getAttribute('aria-describedby')) as Tooltip | undefined; - tooltip?.remove(); - link.removeAttribute('data-has-tooltip'); - link.removeAttribute('aria-describedby'); - } - slot?.assign(link); - }); - - // Assign the remaining breadcrumb links to the main breadcrumb area and set aria-current on the last one - this.breadcrumbLinks - .slice(Math.max(0, this.breadcrumbLinks.length - this.collapseThreshold)) - .forEach((link, index) => { - const slot = this.renderRoot.querySelector( - `slot[name="breadcrumb-${index}"]` - ) as HTMLSlotElement; - link.removeAttribute('aria-current'); - this.#setTooltip(link); - slot?.assign(link); - }); - - const lastLink = this.breadcrumbLinks[this.breadcrumbLinks.length - 1]; - if (lastLink) { - lastLink.setAttribute('aria-current', 'page'); - } - }); - } - #onMutation = (): void => { + // Process light DOM children this.#processChildren(); + + // Perform slot assignments this.#assignSlots(); }; - #update(): void { + #onResize(): void { if (this.#updateScheduled) { return; } this.#updateScheduled = true; + requestAnimationFrame(() => { this.#updateScheduled = false; this.collapseThreshold = isMobile() ? MOBILE_COLLAPSE_THRESHOLD : COLLAPSE_THRESHOLD; @@ -347,44 +279,119 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { }); } - #setTooltip(link: HTMLElement): void { - const tooltipsSlot = this.renderRoot.querySelector('slot[name="tooltips"]') as HTMLSlotElement; + #processChildren(): void { + const children = Array.from(this.children); + + this.breadcrumbs = children + .filter(el => !el.hasAttribute('slot') && !(el instanceof Tooltip)) + .map((el, index) => { + // Make sure the breadcrumb has a DOM id we can reference + el.id ||= `sl-breadcrumb-${index}`; + + // Use an existing tooltip, or create a new one + let tooltip = el.ariaLabelledByElements?.at(0) as Tooltip; + if (!tooltip) { + tooltip = this.shadowRoot!.createElement('sl-tooltip'); + tooltip.for = el.id; + tooltip.textContent = el.textContent?.trim() || ''; + el.after(tooltip); + } - if (!tooltipsSlot) { - return; + return { element: el, tooltip }; + }); + + this.customHomeLink = children.find(el => el.getAttribute('slot') === 'home'); + } + + #assignSlots(): void { + if (this.customHomeLink) { + this.renderRoot + .querySelector('slot[name="home"]') + ?.assign(this.customHomeLink); } - if (link.offsetWidth < link.scrollWidth) { - if (link.hasAttribute('data-has-tooltip')) { - return; - } else { - // const cleanup = Tooltip.lazy( - // link, - // tooltip => { - // tooltip.position = 'bottom'; - // tooltip.textContent = link.textContent?.trim() || ''; - // requestAnimationFrame(() => { - // tooltipsSlot.assign(...tooltipsSlot.assignedElements(), tooltip); - // }); - // }, - // { context: this.shadowRoot! } - // ); - // this.#tooltipCleanupFunctions.set(link, cleanup); - link.dataset['hasTooltip'] = 'true'; - } - } else if (link.hasAttribute('data-has-tooltip') && link.hasAttribute('aria-describedby')) { - const cleanup = this.#tooltipCleanupFunctions.get(link); - if (cleanup) { - cleanup(); + const tooltips = + this.renderRoot + .querySelector('slot[name="tooltips"]') + ?.assignedElements({ flatten: true }) ?? []; + + // Assign breadcrumb links to the menu area based on the collapse threshold + this.breadcrumbs.slice(0, -this.collapseThreshold).forEach((crumb, index) => { + const link = crumb.element; + + link.removeAttribute('aria-current'); + + if (link.hasAttribute('data-has-tooltip') && link.hasAttribute('aria-describedby')) { + // Note: No need to call cleanup() here - it was already called when the tooltip was created + // this.#tooltipCleanupFunctions.delete(link); + + tooltips.find(el => el.id === link.getAttribute('aria-describedby'))?.remove(); + + link.removeAttribute('data-has-tooltip'); + link.removeAttribute('aria-describedby'); } - this.#tooltipCleanupFunctions.delete(link); - - const tooltip = tooltipsSlot - .assignedElements() - .find(el => el.id === link.getAttribute('aria-describedby')) as Tooltip | undefined; - tooltip?.remove(); - link.removeAttribute('data-has-tooltip'); - link.removeAttribute('aria-describedby'); - } + + this.renderRoot + .querySelector(`slot[name="breadcrumb-menu-${index}"]`) + ?.assign(link); + }); + + // Assign the remaining breadcrumbs to the main breadcrumb area + this.breadcrumbs + .slice(Math.max(0, this.breadcrumbs.length - this.collapseThreshold)) + .forEach((crumb, index) => { + const link = crumb.element; + + link.removeAttribute('aria-current'); + // this.#setTooltip(link); + + this.renderRoot + .querySelector(`slot[name="breadcrumb-${index}"]`) + ?.assign(link, crumb.tooltip); + }); + + // Set aria-current on the last breadcrumb + this.breadcrumbs.at(-1)?.element.setAttribute('aria-current', 'page'); } + + // #setTooltip(link: HTMLElement): void { + // const tooltipsSlot = this.renderRoot.querySelector('slot[name="tooltips"]') as HTMLSlotElement; + + // if (!tooltipsSlot) { + // return; + // } + + // if (link.offsetWidth < link.scrollWidth) { + // if (link.hasAttribute('data-has-tooltip')) { + // return; + // } else { + // // const cleanup = Tooltip.lazy( + // // link, + // // tooltip => { + // // tooltip.position = 'bottom'; + // // tooltip.textContent = link.textContent?.trim() || ''; + // // requestAnimationFrame(() => { + // // tooltipsSlot.assign(...tooltipsSlot.assignedElements(), tooltip); + // // }); + // // }, + // // { context: this.shadowRoot! } + // // ); + // // this.#tooltipCleanupFunctions.set(link, cleanup); + // link.dataset['hasTooltip'] = 'true'; + // } + // } else if (link.hasAttribute('data-has-tooltip') && link.hasAttribute('aria-describedby')) { + // const cleanup = this.#tooltipCleanupFunctions.get(link); + // if (cleanup) { + // cleanup(); + // } + // this.#tooltipCleanupFunctions.delete(link); + + // const tooltip = tooltipsSlot + // .assignedElements() + // .find(el => el.id === link.getAttribute('aria-describedby')) as Tooltip | undefined; + // tooltip?.remove(); + // link.removeAttribute('data-has-tooltip'); + // link.removeAttribute('aria-describedby'); + // } + // } } From 2cc6659d3002a35fceb4714d1751f1a00dbd125c Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Mon, 1 Jun 2026 14:31:48 +0200 Subject: [PATCH 29/50] =?UTF-8?q?=F0=9F=98=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../breadcrumbs/src/breadcrumbs.spec.ts | 220 ++++-------------- .../components/breadcrumbs/src/breadcrumbs.ts | 184 ++++++--------- 2 files changed, 107 insertions(+), 297 deletions(-) diff --git a/packages/components/breadcrumbs/src/breadcrumbs.spec.ts b/packages/components/breadcrumbs/src/breadcrumbs.spec.ts index 64e08d0c28..e25f6e85ed 100644 --- a/packages/components/breadcrumbs/src/breadcrumbs.spec.ts +++ b/packages/components/breadcrumbs/src/breadcrumbs.spec.ts @@ -17,6 +17,9 @@ describe('sl-breadcrumbs', () => { Developers `); + + // Wait for the component to process slot assignments + await new Promise(resolve => setTimeout(resolve, 50)); }); it('should have a navigation role', () => { @@ -27,6 +30,25 @@ describe('sl-breadcrumbs', () => { expect(el).to.have.attribute('aria-label', 'Breadcrumb trail'); }); + it('should have a home label', () => { + const homeLink = el.renderRoot.querySelector('li.home a')!; + + expect(homeLink).to.have.trimmed.text('Home'); + expect(homeLink).not.to.have.attribute('aria-label'); + expect(homeLink.querySelector('sl-icon')).to.have.attribute('name', 'home-blank'); + }); + + it('should hide the home label when hideHomeLabel is set', async () => { + el.hideHomeLabel = true; + await el.updateComplete; + + const homeLink = el.renderRoot.querySelector('li.home a')!; + + expect(homeLink).to.have.trimmed.text(''); + expect(homeLink).to.have.attribute('aria-label', 'Home'); + expect(homeLink.querySelector('sl-icon')).to.have.attribute('name', 'home-blank'); + }); + it('should not be inverted', () => { expect(el).not.to.have.attribute('inverted'); expect(el.inverted).to.be.undefined; @@ -109,7 +131,7 @@ describe('sl-breadcrumbs', () => { }); }); - describe('static no home default', () => { + describe('static noHome default', () => { beforeEach(async () => { Breadcrumbs.noHome = true; @@ -129,7 +151,7 @@ describe('sl-breadcrumbs', () => { }); }); - describe('static home url default', () => { + describe('static homeUrl default', () => { beforeEach(async () => { Breadcrumbs.homeUrl = '/custom-home'; @@ -144,68 +166,11 @@ describe('sl-breadcrumbs', () => { afterEach(() => (Breadcrumbs.homeUrl = '/')); - it('should not have a home link', () => { + it('should have a different home link', () => { expect(el.renderRoot.querySelector('li.home a')).to.have.attribute('href', '/custom-home'); }); }); - describe('hideHomeLabel', () => { - beforeEach(async () => { - el = await fixture(html` - - Docs - Getting Started - Developers - - `); - }); - - it('should show the home label by default', () => { - const homeLink = el.renderRoot.querySelector('li.home a')!; - - expect(homeLink).to.have.trimmed.text('Home'); - expect(homeLink.querySelector('sl-icon')).to.have.attribute('name', 'home-blank'); - expect(homeLink).not.to.have.attribute('aria-label'); - }); - - it('should hide the home label when hideHomeLabel is set', async () => { - el.hideHomeLabel = true; - await el.updateComplete; - - const homeLink = el.renderRoot.querySelector('li.home a')!; - - expect(homeLink).to.have.trimmed.text(''); - expect(homeLink).to.have.attribute('aria-label', 'Home'); - expect(homeLink.querySelector('sl-icon')).to.have.attribute('name', 'home-blank'); - }); - - it('should hide the home label when the attribute is set', async () => { - el.setAttribute('hide-home-label', ''); - await el.updateComplete; - - const homeLink = el.renderRoot.querySelector('li.home a')!; - - expect(homeLink).to.have.trimmed.text(''); - expect(homeLink).to.have.attribute('aria-label', 'Home'); - }); - - it('should show the home label again when hideHomeLabel is unset', async () => { - el.hideHomeLabel = true; - await el.updateComplete; - - let homeLink = el.renderRoot.querySelector('li.home a')!; - expect(homeLink).to.have.trimmed.text(''); - - el.hideHomeLabel = false; - await el.updateComplete; - - homeLink = el.renderRoot.querySelector('li.home a')!; - - expect(homeLink).to.have.trimmed.text('Home'); - expect(homeLink).not.to.have.attribute('aria-label'); - }); - }); - describe('static hideHomeLabel default', () => { beforeEach(async () => { Breadcrumbs.hideHomeLabel = true; @@ -243,9 +208,8 @@ describe('sl-breadcrumbs', () => { `); - // Wait for requestAnimationFrame - await new Promise(resolve => requestAnimationFrame(resolve)); - await el.updateComplete; + // Wait for the component to process slot assignments + await new Promise(resolve => setTimeout(resolve, 50)); }); it('should have all links with separators in the DOM', () => { @@ -306,7 +270,7 @@ describe('sl-breadcrumbs', () => { `); - // Give the resize observer time to process + // Wait for the component to process slot assignments await new Promise(resolve => setTimeout(resolve, 50)); }); @@ -359,10 +323,7 @@ describe('sl-breadcrumbs', () => { `); - // Wait for requestAnimationFrame to process slot assignments - await new Promise(resolve => requestAnimationFrame(resolve)); - await el.updateComplete; - // Extra wait for mutation observer + // Wait for the component to process slot assignments await new Promise(resolve => setTimeout(resolve, 50)); }); @@ -397,8 +358,8 @@ describe('sl-breadcrumbs', () => { `); - await new Promise(resolve => requestAnimationFrame(resolve)); - await el.updateComplete; + // Wait for the component to process slot assignments + await new Promise(resolve => setTimeout(resolve, 50)); }); it('should update when links are added dynamically', async () => { @@ -490,8 +451,8 @@ describe('sl-breadcrumbs', () => { `); - await new Promise(resolve => requestAnimationFrame(resolve)); - await el.updateComplete; + // Wait for the component to process slot assignments + await new Promise(resolve => setTimeout(resolve, 50)); // Should not show collapse button const button = el.renderRoot.querySelector('sl-button'), @@ -513,9 +474,7 @@ describe('sl-breadcrumbs', () => { `); - await new Promise(resolve => requestAnimationFrame(resolve)); - await el.updateComplete; - // Wait for resize observer + // Wait for the component to process slot assignments await new Promise(resolve => setTimeout(resolve, 50)); // Should not show collapse button with exactly threshold (3) links - need MORE than threshold @@ -541,8 +500,8 @@ describe('sl-breadcrumbs', () => { `); - await new Promise(resolve => requestAnimationFrame(resolve)); - await el.updateComplete; + // Wait for the component to process slot assignments + await new Promise(resolve => setTimeout(resolve, 50)); // Should process all elements, not just anchors const slots = Array.from( @@ -557,8 +516,8 @@ describe('sl-breadcrumbs', () => { it('should handle empty breadcrumbs', async () => { el = await fixture(html``); - await new Promise(resolve => requestAnimationFrame(resolve)); - await el.updateComplete; + // Wait for the component to process slot assignments + await new Promise(resolve => setTimeout(resolve, 50)); // Should still render home link const homeLink = el.renderRoot.querySelector('li.home a'); @@ -579,8 +538,8 @@ describe('sl-breadcrumbs', () => { `); - await new Promise(resolve => requestAnimationFrame(resolve)); - await el.updateComplete; + // Wait for the component to process slot assignments + await new Promise(resolve => setTimeout(resolve, 50)); // Should not render home section at all when noHome is true const homeListItem = el.renderRoot.querySelector('li.home'); @@ -588,103 +547,4 @@ describe('sl-breadcrumbs', () => { expect(homeListItem).not.to.exist; }); }); - - describe('observers', () => { - it('should disconnect observers on disconnectedCallback', async () => { - el = await fixture(html` - - Docs - - `); - - await el.updateComplete; - - // Remove from DOM - el.remove(); - - // The component should clean up observers (no error should be thrown) - expect(el.isConnected).to.be.false; - }); - }); - - describe('tooltips for truncated links', () => { - beforeEach(async () => { - el = await fixture(html` - - Very Long Breadcrumb Link That Will Be Truncated - Another Very Long Breadcrumb Link - Short - - `); - - // Wait for requestAnimationFrame to process slot assignments - await new Promise(resolve => requestAnimationFrame(resolve)); - await el.updateComplete; - - // Give the resize observer time to process and check for truncation - await new Promise(resolve => setTimeout(resolve, 100)); - await el.updateComplete; - }); - - it('should mark truncated links with data-has-tooltip attribute', () => { - const slots = Array.from( - el.renderRoot.querySelectorAll( - 'slot[name^="breadcrumb-"]:not([name*="menu"])' - ) - ), - visibleLinks = slots.map(slot => slot.assignedElements()[0]) as HTMLElement[], - truncatedLinks = visibleLinks.filter(link => link.hasAttribute('data-has-tooltip')); - - // At least the long links should be marked for tooltips - expect(truncatedLinks.length).to.be.greaterThan(0); - }); - - it('should not mark non-truncated links for tooltips', () => { - const slots = Array.from( - el.renderRoot.querySelectorAll( - 'slot[name^="breadcrumb-"]:not([name*="menu"])' - ) - ), - visibleLinks = slots.map(slot => slot.assignedElements()[0]) as HTMLElement[]; - - // Find the "Short" link - it should not have any tooltip attributes initially - const shortLink = visibleLinks.find(link => link.textContent?.trim() === 'Short'); - - if (shortLink) { - // Check if link is actually not truncated (offsetWidth >= scrollWidth) - const isTruncated = shortLink.offsetWidth < shortLink.scrollWidth; - - if (!isTruncated) { - expect(shortLink).not.to.have.attribute('data-has-tooltip'); - } - } - }); - - it('should detect truncation based on offsetWidth vs scrollWidth', () => { - const slots = Array.from( - el.renderRoot.querySelectorAll( - 'slot[name^="breadcrumb-"]:not([name*="menu"])' - ) - ), - visibleLinks = slots.map(slot => slot.assignedElements()[0]) as HTMLElement[]; - - visibleLinks.forEach(link => { - const isTruncated = link.offsetWidth < link.scrollWidth, - hasTooltipMarker = link.hasAttribute('data-has-tooltip'); - - // If marked for tooltip, it should be truncated - if (hasTooltipMarker) { - expect(isTruncated).to.be.true; - } - }); - }); - }); }); diff --git a/packages/components/breadcrumbs/src/breadcrumbs.ts b/packages/components/breadcrumbs/src/breadcrumbs.ts index 22e9dcad2c..293da3a6a7 100644 --- a/packages/components/breadcrumbs/src/breadcrumbs.ts +++ b/packages/components/breadcrumbs/src/breadcrumbs.ts @@ -27,10 +27,12 @@ declare global { } export interface Breadcrumb { - element: Element; + element: HTMLElement; tooltip: Tooltip; } +let nextUniqueId = 0; + /** * If there are more than 3 items, hide all items except the last 3 items. Note that we cannot use * CSS custom properties for styling, so the value needs to be hardcoded there. It doesn't make @@ -95,18 +97,15 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { /** @internal */ static override styles: CSSResultGroup = styles; + /** Timeout ID for debouncing slot assignment during resize events. */ + #assignSlotsTimeoutId?: ReturnType; + /** Because of the manual slot assignment we need to observe mutations */ #mutationObserver = new MutationObserver(() => this.#onMutation()); /** Observe changes in size, so we can check whether we need to show tooltips for truncated links. */ #resizeObserver = new ResizeObserver(() => this.#onResize()); - /** Map to keep track of cleanup functions for tooltips associated with breadcrumb links. */ - #tooltipCleanupFunctions = new Map void>(); - - /** Flag to prevent multiple simultaneous updates. */ - #updateScheduled = false; - /** @internal The slotted breadcrumbs. */ @state() breadcrumbs: Breadcrumb[] = []; @@ -167,22 +166,14 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { } override disconnectedCallback(): void { + if (this.#assignSlotsTimeoutId) { + clearTimeout(this.#assignSlotsTimeoutId); + this.#assignSlotsTimeoutId = undefined; + } + this.#resizeObserver.disconnect(); this.#mutationObserver.disconnect(); - // Call cleanup functions to remove event listeners before removing tooltips - this.#tooltipCleanupFunctions.forEach(cleanup => cleanup()); - this.#tooltipCleanupFunctions.clear(); - - // Clean up any tooltips projected into or assigned to the "tooltips" slot to avoid memory leaks. - this.renderRoot - ?.querySelector('slot[name="tooltips"]') - ?.assignedElements({ flatten: true }) - .forEach(tooltip => tooltip.remove()); - - // Also remove any light DOM elements explicitly using slot="tooltips" for backwards compatibility. - this.querySelectorAll('[slot="tooltips"]').forEach(tooltip => tooltip.remove()); - super.disconnectedCallback(); } @@ -258,49 +249,40 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { }; #onMutation = (): void => { + // Stop observing while we process the children + this.#mutationObserver.disconnect(); + // Process light DOM children this.#processChildren(); - // Perform slot assignments - this.#assignSlots(); + // Start observing again to catch any changes to the light DOM + this.#mutationObserver.observe(this, { childList: true }); + + // Give the DOM changes from #processChildren() a chance to apply before we perform + // slot assignments, otherwise the breadcrumb elements won't have time to render + // and offsetWidth and scrollWidth will be 0. + requestAnimationFrame(() => this.#assignSlots()); }; #onResize(): void { - if (this.#updateScheduled) { - return; + if (this.#assignSlotsTimeoutId) { + clearTimeout(this.#assignSlotsTimeoutId); } - this.#updateScheduled = true; - - requestAnimationFrame(() => { - this.#updateScheduled = false; - this.collapseThreshold = isMobile() ? MOBILE_COLLAPSE_THRESHOLD : COLLAPSE_THRESHOLD; - this.#onMutation(); - }); - } + // Debounce the slot assignment to prevent excessive calculations during resize events + this.#assignSlotsTimeoutId = setTimeout(() => { + const newCollapseThreshold = isMobile() ? MOBILE_COLLAPSE_THRESHOLD : COLLAPSE_THRESHOLD; - #processChildren(): void { - const children = Array.from(this.children); - - this.breadcrumbs = children - .filter(el => !el.hasAttribute('slot') && !(el instanceof Tooltip)) - .map((el, index) => { - // Make sure the breadcrumb has a DOM id we can reference - el.id ||= `sl-breadcrumb-${index}`; - - // Use an existing tooltip, or create a new one - let tooltip = el.ariaLabelledByElements?.at(0) as Tooltip; - if (!tooltip) { - tooltip = this.shadowRoot!.createElement('sl-tooltip'); - tooltip.for = el.id; - tooltip.textContent = el.textContent?.trim() || ''; - el.after(tooltip); - } - - return { element: el, tooltip }; - }); + // If the collapse threshold has changed, then we need to reprocess the children + if (newCollapseThreshold !== this.collapseThreshold) { + this.collapseThreshold = newCollapseThreshold; + this.#onMutation(); + } else { + this.#assignSlots(); + } - this.customHomeLink = children.find(el => el.getAttribute('slot') === 'home'); + this.#assignSlotsTimeoutId = undefined; + }, 20); } #assignSlots(): void { @@ -310,88 +292,56 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { ?.assign(this.customHomeLink); } - const tooltips = - this.renderRoot - .querySelector('slot[name="tooltips"]') - ?.assignedElements({ flatten: true }) ?? []; - // Assign breadcrumb links to the menu area based on the collapse threshold this.breadcrumbs.slice(0, -this.collapseThreshold).forEach((crumb, index) => { - const link = crumb.element; - - link.removeAttribute('aria-current'); - - if (link.hasAttribute('data-has-tooltip') && link.hasAttribute('aria-describedby')) { - // Note: No need to call cleanup() here - it was already called when the tooltip was created - // this.#tooltipCleanupFunctions.delete(link); - - tooltips.find(el => el.id === link.getAttribute('aria-describedby'))?.remove(); - - link.removeAttribute('data-has-tooltip'); - link.removeAttribute('aria-describedby'); - } + crumb.element.removeAttribute('aria-current'); + crumb.tooltip.disabled = true; this.renderRoot .querySelector(`slot[name="breadcrumb-menu-${index}"]`) - ?.assign(link); + ?.assign(crumb.element, crumb.tooltip); }); // Assign the remaining breadcrumbs to the main breadcrumb area this.breadcrumbs .slice(Math.max(0, this.breadcrumbs.length - this.collapseThreshold)) .forEach((crumb, index) => { - const link = crumb.element; - - link.removeAttribute('aria-current'); - // this.#setTooltip(link); + crumb.element.removeAttribute('aria-current'); + crumb.tooltip.disabled = crumb.element.offsetWidth >= crumb.element.scrollWidth; this.renderRoot .querySelector(`slot[name="breadcrumb-${index}"]`) - ?.assign(link, crumb.tooltip); + ?.assign(crumb.element, crumb.tooltip); }); // Set aria-current on the last breadcrumb this.breadcrumbs.at(-1)?.element.setAttribute('aria-current', 'page'); } - // #setTooltip(link: HTMLElement): void { - // const tooltipsSlot = this.renderRoot.querySelector('slot[name="tooltips"]') as HTMLSlotElement; - - // if (!tooltipsSlot) { - // return; - // } - - // if (link.offsetWidth < link.scrollWidth) { - // if (link.hasAttribute('data-has-tooltip')) { - // return; - // } else { - // // const cleanup = Tooltip.lazy( - // // link, - // // tooltip => { - // // tooltip.position = 'bottom'; - // // tooltip.textContent = link.textContent?.trim() || ''; - // // requestAnimationFrame(() => { - // // tooltipsSlot.assign(...tooltipsSlot.assignedElements(), tooltip); - // // }); - // // }, - // // { context: this.shadowRoot! } - // // ); - // // this.#tooltipCleanupFunctions.set(link, cleanup); - // link.dataset['hasTooltip'] = 'true'; - // } - // } else if (link.hasAttribute('data-has-tooltip') && link.hasAttribute('aria-describedby')) { - // const cleanup = this.#tooltipCleanupFunctions.get(link); - // if (cleanup) { - // cleanup(); - // } - // this.#tooltipCleanupFunctions.delete(link); - - // const tooltip = tooltipsSlot - // .assignedElements() - // .find(el => el.id === link.getAttribute('aria-describedby')) as Tooltip | undefined; - // tooltip?.remove(); - // link.removeAttribute('data-has-tooltip'); - // link.removeAttribute('aria-describedby'); - // } - // } + #processChildren(): void { + const children = Array.from(this.children); + + this.breadcrumbs = children + .filter( + (el): el is HTMLElement => + el instanceof HTMLElement && !(el instanceof Tooltip) && !el.hasAttribute('slot') + ) + .map(el => { + // Make sure the breadcrumb has a unique DOM id we can reference + el.id ||= `sl-breadcrumb-${nextUniqueId++}`; + + // Use an existing tooltip, or create a new one + let tooltip = el.ariaLabelledByElements?.at(0) as Tooltip; + if (!tooltip) { + tooltip = this.shadowRoot!.createElement('sl-tooltip'); + tooltip.for = el.id; + tooltip.textContent = el.textContent?.trim() || ''; + el.after(tooltip); + } + + return { element: el, tooltip }; + }); + + this.customHomeLink = children.find(el => el.getAttribute('slot') === 'home'); + } } From f23ed3216c45ead4424e38b9d25de58c4a6e6a8c Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Mon, 1 Jun 2026 14:39:49 +0200 Subject: [PATCH 30/50] =?UTF-8?q?=F0=9F=8D=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/tame-insects-tan.md | 7 +++++++ packages/components/tooltip/src/tooltip.scss | 1 + 2 files changed, 8 insertions(+) create mode 100644 .changeset/tame-insects-tan.md diff --git a/.changeset/tame-insects-tan.md b/.changeset/tame-insects-tan.md new file mode 100644 index 0000000000..52d50355d3 --- /dev/null +++ b/.changeset/tame-insects-tan.md @@ -0,0 +1,7 @@ +--- +'@sl-design-system/breadcrumbs': minor +--- + +Use the new tooltip implementation + +The breadcrumbs component has been updated to use the simplified tooltip implementation. Tooltips for truncated breadcrumb links are now managed using the new `` `for` attribute approach, removing the need for manual cleanup functions and reducing internal complexity. diff --git a/packages/components/tooltip/src/tooltip.scss b/packages/components/tooltip/src/tooltip.scss index 14b327c2f0..87bc1881a2 100644 --- a/packages/components/tooltip/src/tooltip.scss +++ b/packages/components/tooltip/src/tooltip.scss @@ -14,6 +14,7 @@ text-wrap: pretty; @media (prefers-reduced-motion: no-preference) { + transition-behavior: allow-discrete; transition-duration: 150ms; transition-property: display, opacity; transition-timing-function: cubic-bezier(0.4, 0, 1, 1); From dbf9e6ef5ab072325f72c586e935c20c18a939f5 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Mon, 1 Jun 2026 14:41:47 +0200 Subject: [PATCH 31/50] =?UTF-8?q?=F0=9F=9A=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/breadcrumbs/src/breadcrumbs.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/components/breadcrumbs/src/breadcrumbs.ts b/packages/components/breadcrumbs/src/breadcrumbs.ts index 293da3a6a7..4d1dc971d2 100644 --- a/packages/components/breadcrumbs/src/breadcrumbs.ts +++ b/packages/components/breadcrumbs/src/breadcrumbs.ts @@ -331,7 +331,9 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { el.id ||= `sl-breadcrumb-${nextUniqueId++}`; // Use an existing tooltip, or create a new one - let tooltip = el.ariaLabelledByElements?.at(0) as Tooltip; + let tooltip = el.ariaLabelledByElements?.find( + (el): el is Tooltip => el instanceof Tooltip && el.for === el.id + ); if (!tooltip) { tooltip = this.shadowRoot!.createElement('sl-tooltip'); tooltip.for = el.id; From ed764d3a4ed77b4e041a3c167c7cdff62e6fad89 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Mon, 1 Jun 2026 15:05:40 +0200 Subject: [PATCH 32/50] =?UTF-8?q?=F0=9F=9B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/afraid-parents-nail.md | 17 +++++++---------- .changeset/green-ends-wink.md | 5 +++++ .changeset/light-pears-fail.md | 5 +++++ .changeset/public-buckets-cheat.md | 8 ++++++++ .changeset/social-cows-pull.md | 7 +++++++ .changeset/violet-baboons-crash.md | 24 ++++++++++++++++++++++++ 6 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 .changeset/green-ends-wink.md create mode 100644 .changeset/light-pears-fail.md create mode 100644 .changeset/public-buckets-cheat.md create mode 100644 .changeset/social-cows-pull.md create mode 100644 .changeset/violet-baboons-crash.md diff --git a/.changeset/afraid-parents-nail.md b/.changeset/afraid-parents-nail.md index 608780b8d4..0e8650c6ad 100644 --- a/.changeset/afraid-parents-nail.md +++ b/.changeset/afraid-parents-nail.md @@ -1,20 +1,17 @@ --- '@sl-design-system/button': minor +'@sl-design-system/menu': minor +'@sl-design-system/tag': minor +'@sl-design-system/toggle-button': minor --- -Add `tooltip` property to button +Add `tooltip` property -Previously, adding a tooltip to a button required adding a sibling `` element manually and wiring up the correct `aria-describedby` or `aria-labelledby` relationship by hand. This was especially cumbersome for icon-only buttons, where the tooltip doubles as the accessible label. +Previously, adding a tooltip to any kind of component required adding a sibling `` element manually and wiring up the correct `aria-describedby` or `aria-labelledby` relationship by hand. This was especially cumbersome for icon-only buttons, where the tooltip doubles as the accessible label. -The new `tooltip` property improves the Developer Experience by letting you attach a tooltip directly on the button: +The new `tooltip` property improves the Developer Experience by letting you set a tooltip directly on the component. -```html - - - -``` - -The button handles all the accessibility wiring automatically: +For buttons, it handles all the accessibility wiring automatically: - For **icon-only buttons** the tooltip text acts as the accessible label (`aria-labelledby`). - For **text buttons** the tooltip text acts as an accessible description (`aria-describedby`). diff --git a/.changeset/green-ends-wink.md b/.changeset/green-ends-wink.md new file mode 100644 index 0000000000..c2ad56f4da --- /dev/null +++ b/.changeset/green-ends-wink.md @@ -0,0 +1,5 @@ +--- +'@sl-design-system/shared': minor +--- + +When forwarding ARIA labels or descriptions, the `ForwardAriaMixin` no longer overrides any existing `ariaDescribedByElements` or `ariaLabelledByElements` values. This allows components that use the mixin to maintain their own ARIA relationships without interference, while still forwarding any additional ARIA attributes as needed. diff --git a/.changeset/light-pears-fail.md b/.changeset/light-pears-fail.md new file mode 100644 index 0000000000..a0b6fe6406 --- /dev/null +++ b/.changeset/light-pears-fail.md @@ -0,0 +1,5 @@ +--- +'@sl-design-system/button-bar': patch +--- + +Fix not giving buttons enough time to set the icon-only state diff --git a/.changeset/public-buckets-cheat.md b/.changeset/public-buckets-cheat.md new file mode 100644 index 0000000000..b447f82993 --- /dev/null +++ b/.changeset/public-buckets-cheat.md @@ -0,0 +1,8 @@ +--- +'@sl-design-system/avatar': minor +'@sl-design-system/calendar': minor +'@sl-design-system/ellipsize-text': minor +'@sl-design-system/grid': minor +--- + +Use the new tooltip implementation diff --git a/.changeset/social-cows-pull.md b/.changeset/social-cows-pull.md new file mode 100644 index 0000000000..8e1e63c2c3 --- /dev/null +++ b/.changeset/social-cows-pull.md @@ -0,0 +1,7 @@ +--- +'@sl-design-system/menu': patch +--- + +Fix clicking a second time did not dismiss the menu + +When a user clicked the menu button to open the menu and then clicked it again to close it, the menu would immediately reopen. This happened because the button click fired after the popover's `toggle` event, causing the button's click handler to call `togglePopover()` again on an already-closed menu. The fix tracks a `#popoverJustClosed` flag via the `beforetoggle` event and skips the click handler when the flag is set. diff --git a/.changeset/violet-baboons-crash.md b/.changeset/violet-baboons-crash.md new file mode 100644 index 0000000000..1b842f5d38 --- /dev/null +++ b/.changeset/violet-baboons-crash.md @@ -0,0 +1,24 @@ +--- +'@sl-design-system/tooltip': major +--- + +Rewrite of the tooltip component: the tooltip was the component responsible for the most bug reports and had a complex internal implementation. This rewrite significantly simplifies the component. + +The complex internal positioning logic, `AnchorController`, `EventsController`, and `lazy()` static method have been removed in favour of native browser `popover` and CSS Anchor Positioning APIs. + +> [!NOTE] +> CSS Anchor Positioning is not yet supported in all browsers. You may need to include the [CSS Anchor Positioning polyfill](https://anchor-positioning.oddbird.net/) in your application. + +#### Breaking changes + +- The `TooltipOptions` interface and `Tooltip.lazy()` static method have been removed. Use the `for` attribute to link a tooltip to its anchor instead. +- The `position`, `offset`, `maxWidth`, `arrowPadding`, and `viewportMargin` properties/statics have been removed. +- `hoverShowDelay` changed from `500ms` to `150ms` and `hoverHideDelay` changed from `200ms` to `0ms`. + +#### New API + +- `for` — links the tooltip to an anchor element by ID +- `type` — controls the ARIA relationship: `'label'` (`aria-labelledby`) or `'description'` (`aria-describedby`, default) +- `trigger` — space-separated list of triggers: `'focus'`, `'hover'`, and/or `'click'` (default: `'focus hover'`) +- `disabled` — prevents the tooltip from showing +- `open` — reflects the current open state From 933aaa2bc870f000f0d5ec557a25e325b00756b6 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 3 Jun 2026 08:58:34 +0200 Subject: [PATCH 33/50] =?UTF-8?q?=F0=9F=8C=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tooltip/src/edge-cases.stories.ts | 18 +++-- .../components/tooltip/src/tooltip.spec.ts | 70 +++++++++---------- .../components/tooltip/src/tooltip.stories.ts | 24 +++++-- packages/components/tooltip/src/tooltip.ts | 20 ++++-- 4 files changed, 80 insertions(+), 52 deletions(-) diff --git a/packages/components/tooltip/src/edge-cases.stories.ts b/packages/components/tooltip/src/edge-cases.stories.ts index 4ac235e81a..4ebc060081 100644 --- a/packages/components/tooltip/src/edge-cases.stories.ts +++ b/packages/components/tooltip/src/edge-cases.stories.ts @@ -41,18 +41,23 @@ export const Dialog = { export const DisabledButtons = { render: () => html` - Disabled attribute - Tooltip text - - ARIA disabled - Tooltip text + + Disabled attribute + + + ARIA disabled + ` }; export const MenuButton = { render: () => html` - + @@ -63,7 +68,6 @@ export const MenuButton = { Delete... - Tooltip text ` }; diff --git a/packages/components/tooltip/src/tooltip.spec.ts b/packages/components/tooltip/src/tooltip.spec.ts index 601525f482..27a45ee46f 100644 --- a/packages/components/tooltip/src/tooltip.spec.ts +++ b/packages/components/tooltip/src/tooltip.spec.ts @@ -18,8 +18,10 @@ describe('sl-tooltip', () => { Tooltip text
    `); + anchor = el.querySelector('#btn')!; tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; }); @@ -68,7 +70,7 @@ describe('sl-tooltip', () => { el = await fixture(html`
    - Tip + Tip
    `); anchor = el.querySelector('#anchor')!; @@ -80,8 +82,8 @@ describe('sl-tooltip', () => { expect(tooltip.anchor).to.equal(anchor); }); - it('should set ariaLabelledByElements on the anchor by default', () => { - expect(anchor.ariaLabelledByElements).to.include(tooltip); + it('should set ariaDescribedByElements on the anchor', () => { + expect(anchor.ariaDescribedByElements).to.include(tooltip); }); it('should set anchor-name on the anchor', () => { @@ -103,7 +105,7 @@ describe('sl-tooltip', () => { tooltip.removeAttribute('for'); await tooltip.updateComplete; - expect(anchor.ariaLabelledByElements ?? []).not.to.include(tooltip); + expect(anchor.ariaDescribedByElements ?? []).not.to.include(tooltip); }); it('should clear anchor-name and position-anchor when "for" is removed', async () => { @@ -133,7 +135,7 @@ describe('sl-tooltip', () => { tooltip.remove(); await tooltip.updateComplete; - expect(anchor.ariaLabelledByElements ?? []).not.to.include(tooltip); + expect(anchor.ariaDescribedByElements ?? []).not.to.include(tooltip); }); }); @@ -145,12 +147,14 @@ describe('sl-tooltip', () => { Tip `); + anchor = el.querySelector('#t-anchor')!; tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; }); - it('should use ariaLabelledByElements when type is undefined (default)', () => { + it('should use ariaLabelledByElements by default', () => { expect(anchor.ariaLabelledByElements).to.include(tooltip); expect(anchor.ariaDescribedByElements ?? []).not.to.include(tooltip); }); @@ -170,28 +174,6 @@ describe('sl-tooltip', () => { expect(anchor.ariaDescribedByElements).to.include(tooltip); expect(anchor.ariaLabelledByElements ?? []).not.to.include(tooltip); }); - - it('should switch ARIA relation when type changes from label to description', async () => { - tooltip.type = 'label'; - await tooltip.updateComplete; - - tooltip.type = 'description'; - await tooltip.updateComplete; - - expect(anchor.ariaDescribedByElements).to.include(tooltip); - expect(anchor.ariaLabelledByElements ?? []).not.to.include(tooltip); - }); - - it('should switch ARIA relation when type changes from description to label', async () => { - tooltip.type = 'description'; - await tooltip.updateComplete; - - tooltip.type = 'label'; - await tooltip.updateComplete; - - expect(anchor.ariaLabelledByElements).to.include(tooltip); - expect(anchor.ariaDescribedByElements ?? []).not.to.include(tooltip); - }); }); describe('disabled', () => { @@ -199,11 +181,13 @@ describe('sl-tooltip', () => { el = await fixture(html`
    - Tip + Tip
    `); + anchor = el.querySelector('#d-anchor')!; tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; }); @@ -226,6 +210,20 @@ describe('sl-tooltip', () => { expect(tooltip.matches(':popover-open')).to.be.false; }); + + it('should add/remove ariaDescribedByElements reference when enabled/disabled', async () => { + expect(anchor.ariaDescribedByElements ?? []).not.to.include(tooltip); + + tooltip.disabled = false; + await tooltip.updateComplete; + + expect(anchor.ariaDescribedByElements ?? []).to.include(tooltip); + + tooltip.disabled = true; + await tooltip.updateComplete; + + expect(anchor.ariaDescribedByElements ?? []).not.to.include(tooltip); + }); }); describe('open property', () => { @@ -233,7 +231,7 @@ describe('sl-tooltip', () => { el = await fixture(html`
    - Tip + Tip
    `); anchor = el.querySelector('#o-anchor')!; @@ -264,7 +262,7 @@ describe('sl-tooltip', () => { el = await fixture(html`
    - Tip + Tip
    `); anchor = el.querySelector('#h-anchor')!; @@ -313,7 +311,7 @@ describe('sl-tooltip', () => { el = await fixture(html`
    - Tip + Tip
    `); anchor = el.querySelector('#f-anchor')!; @@ -354,7 +352,7 @@ describe('sl-tooltip', () => { el = await fixture(html`
    - Tip + Tip
    `); anchor = el.querySelector('#c-anchor')!; @@ -394,7 +392,7 @@ describe('sl-tooltip', () => { el = await fixture(html`
    - Tip + Tip
    `); anchor = el.querySelector('#m-anchor')!; @@ -428,7 +426,7 @@ describe('sl-tooltip', () => { el = await fixture(html`
    - Tip + Tip
    `); anchor = el.querySelector('#k-anchor')!; @@ -453,7 +451,7 @@ describe('sl-tooltip', () => { el = await fixture(html`
    - Tip + Tip
    `); anchor = el.querySelector('#delay-anchor')!; diff --git a/packages/components/tooltip/src/tooltip.stories.ts b/packages/components/tooltip/src/tooltip.stories.ts index 27a2817f30..8f44906203 100644 --- a/packages/components/tooltip/src/tooltip.stories.ts +++ b/packages/components/tooltip/src/tooltip.stories.ts @@ -5,7 +5,7 @@ import { ifDefined } from 'lit/directives/if-defined.js'; import '../register.js'; import { Tooltip } from './tooltip.js'; -type Props = Pick & { +type Props = Pick & { maxWidth: number; position: string; showHoverBridge: boolean; @@ -45,12 +45,27 @@ export default { trigger: { control: 'inline-check', options: ['click', 'hover', 'focus', 'manual'] + }, + type: { + control: 'inline-radio', + options: ['description', 'label'] } }, args: { - text: 'Tooltip text' + text: 'Tooltip text', + type: 'description' }, - render: ({ disabled, maxWidth, open, position, showHoverBridge, text, tooltip, trigger }) => html` + render: ({ + disabled, + maxWidth, + open, + position, + showHoverBridge, + text, + tooltip, + trigger, + type + }) => html` Anchor ${tooltip ? tooltip() @@ -59,7 +74,8 @@ export default { ?disabled=${disabled} ?open=${open} for="button" - trigger=${ifDefined(trigger?.join(' ') || undefined)}> + trigger=${ifDefined(trigger?.join(' ') || undefined)} + type=${ifDefined(type)}> ${text}
    `} diff --git a/packages/components/tooltip/src/tooltip.ts b/packages/components/tooltip/src/tooltip.ts index d35a38c482..8ff993f1ae 100644 --- a/packages/components/tooltip/src/tooltip.ts +++ b/packages/components/tooltip/src/tooltip.ts @@ -132,10 +132,6 @@ export class Tooltip extends LitElement { this.#updateAnchor(); } - if (changes.has('disabled') && this.disabled) { - this.hidePopover(); - } - if (changes.has('open')) { if (this.open) { this.showPopover(); @@ -146,7 +142,21 @@ export class Tooltip extends LitElement { if (changes.has('type') && this.anchor) { this.#removeAriaRelation(this.anchor, changes.get('type')); - this.#addAriaRelation(this.anchor, this.type); + if (!this.disabled) { + this.#addAriaRelation(this.anchor, this.type); + } + } + + if (changes.has('disabled')) { + if (this.disabled) { + this.hidePopover(); + + if (this.anchor) { + this.#removeAriaRelation(this.anchor, this.type); + } + } else if (this.anchor) { + this.#addAriaRelation(this.anchor, this.type); + } } } From ee21384d9be2245fd2177838bf5cf9292c0763a2 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 3 Jun 2026 12:58:03 +0200 Subject: [PATCH 34/50] =?UTF-8?q?=F0=9F=8E=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/button/src/button.ts | 12 +- .../src/mixins/forward-aria-mixin.spec.ts | 38 ++++ .../shared/src/mixins/forward-aria-mixin.ts | 31 +++ .../toggle-button/src/toggle-button.scss | 214 ++++++++++-------- .../toggle-button/src/toggle-button.spec.ts | 128 ++++++----- .../toggle-button/src/toggle-button.ts | 161 ++++++++----- .../toggle-group/src/toggle-group.scss | 14 +- .../toggle-group/src/toggle-group.spec.ts | 33 +-- .../toggle-group/src/toggle-group.ts | 34 ++- 9 files changed, 406 insertions(+), 259 deletions(-) diff --git a/packages/components/button/src/button.ts b/packages/components/button/src/button.ts index 41ee50de9c..175f6d8055 100644 --- a/packages/components/button/src/button.ts +++ b/packages/components/button/src/button.ts @@ -40,9 +40,7 @@ export type ButtonVariant = /** * A single, simple button, with optionally an icon. * - * ```html - * Foo - * ``` + * @customElement sl-button * * @slot default - Text label of the button. Optionally an sl-icon can be added * @@ -110,14 +108,14 @@ export class Button extends ForwardAriaMixin(ScopedElementsMixin(LitElement)) { /** * The fill of the button. * - * @default solid + * @default 'solid' */ @property({ reflect: true }) fill?: ButtonFill; /** * The shape of the button. * - * @default square + * @default 'square' */ @property({ reflect: true }) shape?: ButtonShape; @@ -147,14 +145,14 @@ export class Button extends ForwardAriaMixin(ScopedElementsMixin(LitElement)) { * The type of the button. Can be used to mimic the functionality of submit and reset buttons in * native HTML buttons. * - * @default button + * @default 'button' */ @property() type?: ButtonType; /** * The variant of the button. * - * @default secondary + * @default 'secondary' */ @property({ reflect: true }) variant?: ButtonVariant; diff --git a/packages/components/shared/src/mixins/forward-aria-mixin.spec.ts b/packages/components/shared/src/mixins/forward-aria-mixin.spec.ts index 42ce664fa6..cdddec0967 100644 --- a/packages/components/shared/src/mixins/forward-aria-mixin.spec.ts +++ b/packages/components/shared/src/mixins/forward-aria-mixin.spec.ts @@ -444,6 +444,44 @@ describe('ForwardAriaMixin', () => { }); }); + describe('removeAttribute', () => { + it('should remove a plain attribute from the proxy target', () => { + el.setAttribute('aria-label', 'Test label'); + el.removeAttribute('aria-label'); + + expect(button).not.to.have.attribute('aria-label'); + }); + + it('should remove aria-disabled from the proxy target', () => { + el.setAttribute('aria-disabled', 'true'); + el.removeAttribute('aria-disabled'); + + expect(button).not.to.have.attribute('aria-disabled'); + }); + + it('should clear element references from the proxy target', () => { + const label = document.createElement('span'); + label.id = 'remove-label'; + el.parentElement!.prepend(label); + + el.setAttribute('aria-labelledby', 'remove-label'); + el.removeAttribute('aria-labelledby'); + + expect(button.ariaLabelledByElements).to.be.null; + + label.remove(); + }); + + it('should not affect the proxy for attributes not in the observed list', () => { + button.setAttribute('aria-hidden', 'true'); + el.removeAttribute('aria-hidden'); + + expect(button).to.have.attribute('aria-hidden', 'true'); + + button.removeAttribute('aria-hidden'); + }); + }); + describe('no observedAttributes specified', () => { let defaultEl: InstanceType, defaultButton: HTMLButtonElement; diff --git a/packages/components/shared/src/mixins/forward-aria-mixin.ts b/packages/components/shared/src/mixins/forward-aria-mixin.ts index e6e34d040d..ddc093eb4c 100644 --- a/packages/components/shared/src/mixins/forward-aria-mixin.ts +++ b/packages/components/shared/src/mixins/forward-aria-mixin.ts @@ -164,6 +164,37 @@ export function ForwardAriaMixin< } } + override removeAttribute(name: string): void { + if (observedAttributes ? observedAttributes.includes(name) : name.startsWith('aria-')) { + // Always remove from pending so a queued forward doesn't re-add it. + this.#pendingAttributes.delete(name); + + // If the attribute is still present on the host, this call came from + // #forwardAttributes cleaning up after forwarding — the proxy was just + // set correctly, so don't touch it. Only clear the proxy when the + // attribute is already absent (i.e. an explicit external removal call). + if (!this.hasAttribute(name)) { + const target = targetElements.get(this); + if (target) { + if (name === 'aria-disabled') { + setAriaDisabled(target, null); + ariaDisabledStorage.set(this, null); + } else { + const elementsProp = ELEMENT_REFERENCES[name]; + if (elementsProp) { + (target as unknown as Record)[elementsProp] = + null; + } else { + target.removeAttribute(name); + } + } + } + } + } + + super.removeAttribute(name); + } + #forwardAttributes(): void { const targetElement = targetElements.get(this); diff --git a/packages/components/toggle-button/src/toggle-button.scss b/packages/components/toggle-button/src/toggle-button.scss index babd619305..b208af0760 100644 --- a/packages/components/toggle-button/src/toggle-button.scss +++ b/packages/components/toggle-button/src/toggle-button.scss @@ -1,124 +1,44 @@ :host { - --_bg-color: var(--sl-color-background-neutral-bold); - --_bg-mix-color: var(--sl-color-background-neutral-interactive-bold); - --_bg-opacity: var(--sl-opacity-interactive-plain-idle); --_transition-duration: var(--sl-animation-button-duration); --_transition-easing: var(--sl-animation-button-easing); // these are overwritten in toggle group + --_border-compensation: var(--_toggle-group-border, var(--_border-width)); --_border-width: var(--sl-size-borderWidth-none); --_button-border-radius: var(--sl-size-borderRadius-default); --_group-compensation-start: 0px; --_group-compensation-end: 0px; - --_border-compensation: var(--_toggle-group-border, var(--_border-width)); - background: color-mix( - in srgb, - var(--_bg-color), - var(--_bg-mix-color) calc(100% * var(--_bg-opacity)) - ); - border: var(--_border-width) solid var(--sl-color-border-plain); - border-radius: var(--_button-border-radius); - color: var(--sl-color-foreground-neutral-bold); - cursor: pointer; display: inline-flex; - outline: transparent solid var(--sl-size-borderWidth-focusRing); - outline-offset: var(--sl-size-outlineOffset-default); + flex-shrink: 0; vertical-align: middle; } -:host(:hover) { - --_bg-opacity: var(--sl-opacity-interactive-plain-hover); -} - -:host(:active) { - --_bg-opacity: var(--sl-opacity-interactive-plain-active); -} - -:host(:focus-visible) { - outline-color: var(--sl-color-border-focused); -} - -// selectors based on attributes - -:host([shape='pill']) { - --_button-border-radius: var(--sl-size-borderRadius-full); -} - :host([fill='outline']) { --_border-width: var(--sl-size-borderWidth-action); - --_bg-color: transparent; - --_bg-mix-color: var(--sl-color-background-neutral-interactive-plain); -} -:host([pressed]) { - --_bg-color: var(--sl-color-background-selected-bold); - --_bg-mix-color: var(--sl-color-background-selected-interactive-bold); - - border-color: var(--sl-color-border-selected); - color: var(--sl-color-foreground-selected-onBold); -} - -:host(:is([disabled], [aria-disabled='true'])), -:host(:is([disabled], [aria-disabled='true'])[pressed]) { - --_bg-color: transparent; - --_bg-mix-color: transparent; - - border-color: var(--sl-color-border-disabled); - color: var(--sl-color-foreground-disabled); - cursor: default; - pointer-events: none; + button { + --_bg-color: transparent; + --_bg-mix-color: var(--sl-color-background-neutral-interactive-plain); + } } -:host(:is([disabled], [aria-disabled='true'])[pressed]) { - --_bg-color: var(--sl-color-background-disabled); -} - -:host(:where(:active, :focus-visible, :hover)) { - transition-duration: var(--_transition-duration); - transition-property: border-color, color, outline-color; - transition-timing-function: var(--_transition-easing); -} - -:host([pressed]) slot[name='default'], -:host(:not([pressed])) slot[name='pressed'] { - display: none; +:host([shape='pill']) { + --_button-border-radius: var(--sl-size-borderRadius-full); } -:host([error]), -:host([error][pressed]) { +:host(:state(error)) button { --_bg-color: var(--sl-color-background-accent-red-bold); --_bg-mix-color: var(--sl-color-background-accent-red-interactive-bold); color: var(--sl-color-foreground-selected-onBold); } -[part='wrapper'] { - --_check-compensation: 0px; - - align-items: center; - display: flex; - gap: var(--sl-size-075); - justify-content: center; - min-block-size: 1lh; - padding: calc(var(--sl-size-075) - var(--_border-compensation)) - calc( - var(--sl-size-200) + var(--_check-compensation) - var(--_border-compensation) + - var(--_group-compensation-end) - ) - calc(var(--sl-size-075) - var(--_border-compensation)) - calc( - var(--sl-size-200) + var(--_check-compensation) - var(--_border-compensation) + - var(--_group-compensation-start) - ); - transition: background var(--_transition-duration) var(--_transition-easing); -} - -:host([text-only]) [part='wrapper'] { +:host(:state(text-only)) button { --_check-compensation: calc((var(--sl-size-075) + var(--sl-size-new-icon-md)) / 2); } -:host([size='sm']) [part='wrapper'] { +:host(:where([size='sm'])) button { --_check-compensation: calc((var(--sl-size-075) + var(--sl-size-new-icon-xs)) / 2); padding: calc(var(--sl-size-025) - var(--_border-compensation)) @@ -133,11 +53,11 @@ ); } -:host([text-only][size='sm']) [part='wrapper'] { +:host(:state(text-only):where([size='sm'])) button { --_check-compensation: calc((var(--sl-size-075) + var(--sl-size-new-icon-xs)) / 2); } -:host([size='lg']) [part='wrapper'] { +:host(:where([size='lg'])) button { padding: calc(var(--sl-size-125) - var(--_border-compensation)) calc( var(--sl-size-300) + var(--_check-compensation) - var(--_border-compensation) + @@ -150,12 +70,13 @@ ); } -:host([pressed][size='sm']) [part='wrapper'], -:host([pressed]) [part='wrapper'] { +:host(:state(pressed)) button { + --_bg-color: var(--sl-color-background-selected-bold); + --_bg-mix-color: var(--sl-color-background-selected-interactive-bold); --_check-compensation: 0px; } -:host([icon-only]) [part='wrapper'] { +:host(:state(icon-only)) button { --_check-compensation: 0px; aspect-ratio: 1; @@ -166,7 +87,7 @@ calc(var(--sl-size-125) - var(--_border-compensation) + var(--_group-compensation-start)); } -:host([icon-only][size='sm']) [part='wrapper'] { +:host(:state(icon-only)[size='sm']) button { line-height: var(--sl-size-new-icon-xs); padding: calc(var(--sl-size-075) - var(--_border-compensation)) calc(var(--sl-size-075) - var(--_border-compensation) + var(--_group-compensation-end)) @@ -174,13 +95,110 @@ calc(var(--sl-size-075) - var(--_border-compensation) + var(--_group-compensation-start)); } -:host([icon-only][size='lg']) [part='wrapper'] { +:host(:state(icon-only)[size='lg']) button { padding: calc(var(--sl-size-200) - var(--_border-compensation)) calc(var(--sl-size-200) - var(--_border-compensation) + var(--_group-compensation-end)) calc(var(--sl-size-200) - var(--_border-compensation)) calc(var(--sl-size-200) - var(--_border-compensation) + var(--_group-compensation-start)); } +button { + --_bg-color: var(--sl-color-background-neutral-bold); + --_bg-mix-color: var(--sl-color-background-neutral-interactive-bold); + --_bg-opacity: var(--sl-opacity-interactive-plain-idle); + --_check-compensation: 0px; + + align-items: center; + appearance: none; + background: color-mix( + in srgb, + var(--_bg-color), + var(--_bg-mix-color) calc(100% * var(--_bg-opacity)) + ); + border: var(--_border-width) solid var(--sl-color-border-plain); + border-radius: var(--_button-border-radius); + box-sizing: content-box; + color: var(--sl-color-foreground-neutral-bold); + cursor: pointer; + display: inline-flex; + flex: 1 1 auto; + font: inherit; + gap: var(--sl-size-075); + justify-content: center; + margin: 0; + min-block-size: 1lh; + min-inline-size: 0; + outline: transparent solid var(--sl-size-borderWidth-focusRing); + outline-offset: var(--sl-size-outlineOffset-default); + padding: calc(var(--sl-size-075) - var(--_border-compensation)) + calc( + var(--sl-size-200) + var(--_check-compensation) - var(--_border-compensation) + + var(--_group-compensation-end) + ) + calc(var(--sl-size-075) - var(--_border-compensation)) + calc( + var(--sl-size-200) + var(--_check-compensation) - var(--_border-compensation) + + var(--_group-compensation-start) + ); + user-select: none; + + &:focus-visible { + outline-color: var(--sl-color-border-focused); + z-index: 1; + } + + &:hover { + --_bg-opacity: var(--sl-opacity-interactive-plain-hover); + } + + &:active { + --_bg-opacity: var(--sl-opacity-interactive-plain-active); + } + + &:disabled { + pointer-events: none; + } + + &[aria-pressed='true'] { + border-color: var(--sl-color-border-selected); + color: var(--sl-color-foreground-selected-onBold); + + slot[name='default'] { + display: none; + } + + slot[name='pressed'] { + display: contents; + } + } + + &:is(:disabled, [aria-disabled]) { + --_bg-color: transparent; + --_bg-mix-color: transparent; + + border-color: var(--sl-color-border-disabled); + color: var(--sl-color-foreground-disabled); + cursor: default; + + &[aria-pressed='true'] { + // --_bg-color: var(--sl-color-background-disabled); + --_bg-color: var(--sl-color-background-neutral-bold); + } + } +} + +@media (prefers-reduced-motion: no-preference) { + button:where(:active, :focus-visible, :hover) { + transition-duration: var(--_transition-duration); + transition-property: background, border-color, color, outline-color; + transition-timing-function: var(--_transition-easing); + } +} + +slot[name='pressed'] { + display: none; +} + sl-icon, ::slotted(sl-icon) { fill: currentcolor; diff --git a/packages/components/toggle-button/src/toggle-button.spec.ts b/packages/components/toggle-button/src/toggle-button.spec.ts index 7db9774cbe..6c0dc157a1 100644 --- a/packages/components/toggle-button/src/toggle-button.spec.ts +++ b/packages/components/toggle-button/src/toggle-button.spec.ts @@ -9,7 +9,7 @@ import '../register.js'; import { type ToggleButton } from './toggle-button.js'; describe('sl-toggle-button', () => { - let el: ToggleButton; + let el: ToggleButton, button: HTMLButtonElement; describe('defaults', () => { beforeEach(async () => { @@ -19,10 +19,12 @@ describe('sl-toggle-button', () => { `); + button = el.renderRoot.querySelector('button')!; }); - it('should have a button role', () => { - expect(el.role).to.equal('button'); + it('should have a native button in the shadow DOM', () => { + expect(button).to.exist; + expect(button).to.have.attribute('type', 'button'); }); it('should not have an explicit size', () => { @@ -38,9 +40,8 @@ describe('sl-toggle-button', () => { }); it('should not be pressed', () => { - expect(el).to.have.attribute('aria-pressed', 'false'); - expect(el).not.to.have.attribute('pressed'); - expect(el.pressed).to.be.false; + expect(button).to.have.attribute('aria-pressed', 'false'); + expect(el.pressed).not.to.be.true; }); it('should not be disabled', () => { @@ -49,23 +50,28 @@ describe('sl-toggle-button', () => { }); it('should be marked as icon only', () => { - expect(el).to.have.attribute('icon-only'); + expect(el).to.match(':state(icon-only)'); + }); + + it('should delegate focus to the inner button', () => { + el.focus(); + + expect(document.activeElement).to.equal(el); + expect(el.shadowRoot?.activeElement).to.equal(button); }); it('should toggle the pressed state when clicked', async () => { - el.click(); + await userEvent.click(button); await el.updateComplete; - expect(el).to.have.attribute('aria-pressed', 'true'); - expect(el).to.have.attribute('pressed'); + expect(button).to.have.attribute('aria-pressed', 'true'); expect(el.pressed).to.be.true; - el.click(); + await userEvent.click(button); await el.updateComplete; - expect(el).to.have.attribute('aria-pressed', 'false'); - expect(el).not.to.have.attribute('pressed'); - expect(el.pressed).to.be.false; + expect(button).to.have.attribute('aria-pressed', 'false'); + expect(el.pressed).not.to.be.true; }); it('should toggle the pressed state when pressing enter', async () => { @@ -73,17 +79,15 @@ describe('sl-toggle-button', () => { await userEvent.keyboard('{Enter}'); await el.updateComplete; - expect(el).to.have.attribute('aria-pressed', 'true'); - expect(el).to.have.attribute('pressed'); + expect(button).to.have.attribute('aria-pressed', 'true'); expect(el.pressed).to.be.true; el.focus(); await userEvent.keyboard('{Enter}'); await el.updateComplete; - expect(el).to.have.attribute('aria-pressed', 'false'); - expect(el).not.to.have.attribute('pressed'); - expect(el.pressed).to.be.false; + expect(button).to.have.attribute('aria-pressed', 'false'); + expect(el.pressed).not.to.be.true; }); it('should toggle the pressed state when pressing space', async () => { @@ -91,17 +95,15 @@ describe('sl-toggle-button', () => { await userEvent.keyboard('{Space}'); await el.updateComplete; - expect(el).to.have.attribute('aria-pressed', 'true'); - expect(el).to.have.attribute('pressed'); + expect(button).to.have.attribute('aria-pressed', 'true'); expect(el.pressed).to.be.true; el.focus(); await userEvent.keyboard('{Space}'); await el.updateComplete; - expect(el).to.have.attribute('aria-pressed', 'false'); - expect(el).not.to.have.attribute('pressed'); - expect(el.pressed).to.be.false; + expect(button).to.have.attribute('aria-pressed', 'false'); + expect(el.pressed).not.to.be.true; }); it('should emit an sl-toggle event on click', async () => { @@ -111,7 +113,7 @@ describe('sl-toggle-button', () => { onToggle(event.detail); }); - el.click(); + await userEvent.click(button); await el.updateComplete; expect(onToggle).to.have.been.calledOnce; @@ -157,16 +159,16 @@ describe('sl-toggle-button', () => { `); + button = el.renderRoot.querySelector('button')!; }); it('should not toggle the pressed state when clicked', async () => { - el.click(); + button.click(); await el.updateComplete; - expect(el).to.have.attribute('aria-pressed', 'false'); - expect(el).not.to.have.attribute('pressed'); - expect(el.pressed).to.be.false; + expect(button).to.have.attribute('aria-pressed', 'false'); + expect(el.pressed).not.to.be.true; }); it('should not toggle the pressed state when pressing enter', async () => { @@ -175,9 +177,8 @@ describe('sl-toggle-button', () => { await userEvent.keyboard('{Enter}'); await el.updateComplete; - expect(el).to.have.attribute('aria-pressed', 'false'); - expect(el).not.to.have.attribute('pressed'); - expect(el.pressed).to.be.false; + expect(button).to.have.attribute('aria-pressed', 'false'); + expect(el.pressed).not.to.be.true; }); it('should not toggle the pressed state when pressing space', async () => { @@ -186,16 +187,15 @@ describe('sl-toggle-button', () => { await userEvent.keyboard('{Space}'); await el.updateComplete; - expect(el).to.have.attribute('aria-pressed', 'false'); - expect(el).not.to.have.attribute('pressed'); - expect(el.pressed).to.be.false; + expect(button).to.have.attribute('aria-pressed', 'false'); + expect(el.pressed).not.to.be.true; }); it('should not emit an sl-toggle event when clicked', async () => { const onToggle = spy(); el.addEventListener('sl-toggle', onToggle); - el.click(); + button.click(); await el.updateComplete; expect(onToggle).not.to.have.been.called; @@ -205,22 +205,22 @@ describe('sl-toggle-button', () => { const onClick = spy(); el.addEventListener('click', onClick); - el.click(); + button.click(); await el.updateComplete; expect(onClick).not.to.have.been.called; }); - it('should set aria-disabled', () => { - expect(el).to.have.attribute('aria-disabled', 'true'); + it('should disable the inner button', () => { + expect(button).to.have.attribute('disabled'); }); - it('should remove aria-disabled when re-enabled', async () => { + it('should remove disabled from the inner button when re-enabled', async () => { el.disabled = false; await el.updateComplete; expect(el).not.to.have.attribute('disabled'); - expect(el).not.to.have.attribute('aria-disabled'); + expect(button).not.to.have.attribute('disabled'); }); it('should preserve a consumer-provided aria-disabled value when re-enabled', async () => { @@ -230,12 +230,13 @@ describe('sl-toggle-button', () => { `); + button = el.renderRoot.querySelector('button')!; el.disabled = false; await el.updateComplete; expect(el).not.to.have.attribute('disabled'); - expect(el).to.have.attribute('aria-disabled', 'true'); + expect(button).to.have.attribute('aria-disabled', 'true'); }); }); @@ -247,6 +248,15 @@ describe('sl-toggle-button', () => { `); + button = el.renderRoot.querySelector('button')!; + }); + + it('should proxy the aria-disabled attribute to the inner button', () => { + expect(button).to.have.attribute('aria-disabled', 'true'); + + el.removeAttribute('aria-disabled'); + + expect(button).not.to.have.attribute('aria-disabled'); }); it('should be focusable', () => { @@ -256,12 +266,11 @@ describe('sl-toggle-button', () => { }); it('should not toggle the pressed state when clicked', async () => { - el.click(); + button.click(); await el.updateComplete; - expect(el).to.have.attribute('aria-pressed', 'false'); - expect(el).not.to.have.attribute('pressed'); - expect(el.pressed).to.be.false; + expect(button).to.have.attribute('aria-pressed', 'false'); + expect(el.pressed).not.to.be.true; }); it('should not toggle the pressed state when pressing enter', async () => { @@ -270,16 +279,15 @@ describe('sl-toggle-button', () => { await userEvent.keyboard('{Enter}'); await el.updateComplete; - expect(el).to.have.attribute('aria-pressed', 'false'); - expect(el).not.to.have.attribute('pressed'); - expect(el.pressed).to.be.false; + expect(button).to.have.attribute('aria-pressed', 'false'); + expect(el.pressed).not.to.be.true; }); it('should not emit an sl-toggle event when clicked', async () => { const onToggle = spy(); el.addEventListener('sl-toggle', onToggle); - el.click(); + button.click(); await el.updateComplete; expect(onToggle).not.to.have.been.called; @@ -289,7 +297,7 @@ describe('sl-toggle-button', () => { const onClick = spy(); el.addEventListener('click', onClick); - el.click(); + button.click(); await el.updateComplete; expect(onClick).not.to.have.been.called; @@ -304,10 +312,11 @@ describe('sl-toggle-button', () => { `); + button = el.renderRoot.querySelector('button')!; }); it('should have an aria-pressed attribute set to true', () => { - expect(el).to.have.attribute('aria-pressed', 'true'); + expect(button).to.have.attribute('aria-pressed', 'true'); }); it('should have a true pressed state when the attribute it set', () => { @@ -391,14 +400,17 @@ describe('sl-toggle-button', () => { `); - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => requestAnimationFrame(resolve)); + await el.updateComplete; - const tooltip = el.renderRoot.querySelector('sl-tooltip'), - wrapper = el.renderRoot.querySelector('#wrapper'); + const tooltip = el.renderRoot.querySelector('sl-tooltip')!, + innerButton = el.renderRoot.querySelector('button'); + + await tooltip.updateComplete; expect(tooltip).to.exist; expect(tooltip).to.have.trimmed.text('Settings'); - expect(wrapper?.ariaLabelledByElements).to.include(tooltip); + expect(innerButton?.ariaLabelledByElements).to.include(tooltip); }); it('should use the tooltip as the description for the text button', async () => { @@ -407,11 +419,11 @@ describe('sl-toggle-button', () => { await new Promise(resolve => setTimeout(resolve, 50)); const tooltip = el.renderRoot.querySelector('sl-tooltip'), - wrapper = el.renderRoot.querySelector('#wrapper'); + innerButton = el.renderRoot.querySelector('button'); expect(tooltip).to.exist; expect(tooltip).to.have.trimmed.text('Tooltip'); - expect(wrapper?.ariaDescribedByElements).to.include(tooltip); + expect(innerButton?.ariaDescribedByElements).to.include(tooltip); }); }); }); diff --git a/packages/components/toggle-button/src/toggle-button.ts b/packages/components/toggle-button/src/toggle-button.ts index 75331c4b20..80dc5a5670 100644 --- a/packages/components/toggle-button/src/toggle-button.ts +++ b/packages/components/toggle-button/src/toggle-button.ts @@ -2,10 +2,10 @@ import { type ScopedElementsMap, ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; -import { ButtonShape } from '@sl-design-system/button'; import { Icon } from '@sl-design-system/icon'; -import { type EventEmitter, EventsController, event } from '@sl-design-system/shared'; +import { type EventEmitter, event } from '@sl-design-system/shared'; import { type SlToggleEvent } from '@sl-design-system/shared/events.js'; +import { ForwardAriaMixin } from '@sl-design-system/shared/mixins.js'; import { Tooltip } from '@sl-design-system/tooltip'; import { type CSSResultGroup, @@ -15,7 +15,7 @@ import { html, nothing } from 'lit'; -import { property, state } from 'lit/decorators.js'; +import { property, query, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import styles from './toggle-button.scss.js'; @@ -26,22 +26,26 @@ declare global { } export type ToggleButtonFill = 'outline' | 'solid'; +export type ToggleButtonShape = 'square' | 'pill'; export type ToggleButtonSize = 'sm' | 'md' | 'lg'; /** - * Lets the user toggle between two states. + * A button that lets the user toggle between two states. * - * ```html - * - * - * - * - * ``` + * @customElement sl-toggle-button * * @slot default - The icon shown in the default state of the button * @slot pressed - The icon shown in the pressed state of the button + * + * @csspart button - The internal <button> element. + * @csspart tooltip - The tooltip element that is shown when the tooltip attribute is set. + * + * @cssstate error - Set when there is an error with the toggle button, for example when there are no icons in an icon-only toggle button. + * @cssstate pressed - Set when the toggle button is in the pressed state. + * @cssstate icon-only - Set when the toggle button has icons and no text. + * @cssstate text-only - Set when the toggle button has text and no icons. */ -export class ToggleButton extends ScopedElementsMixin(LitElement) { +export class ToggleButton extends ForwardAriaMixin(ScopedElementsMixin(LitElement)) { /** @internal */ static get scopedElements(): ScopedElementsMap { return { @@ -50,44 +54,64 @@ export class ToggleButton extends ScopedElementsMixin(LitElement) { }; } + /** @internal */ + static override shadowRootOptions: ShadowRootInit = { + ...LitElement.shadowRootOptions, + delegatesFocus: true + }; + /** @internal */ static override styles: CSSResultGroup = styles; - // eslint-disable-next-line no-unused-private-class-members - #events = new EventsController(this, { - click: this.#onClick, - keydown: this.#onKeydown - }); + /** @internal The button element. */ + @query('button') button!: HTMLButtonElement; /** @internal The default (non-pressed) icon. */ @state() defaultIcon?: Icon; - /** Whether the button is disabled; when set no interaction is possible. */ + /** + * Whether the button is disabled; when set no interaction is possible. + * + * @default false + */ @property({ type: Boolean, reflect: true }) disabled?: boolean; - /** The variant of the toggle-button. */ + /** + * The variant of the toggle-button. + * + * @default 'solid' + */ @property({ reflect: true }) fill?: ToggleButtonFill; /** @internal True when the user has slotted text in the button. */ @state() hasText?: boolean; - /** @internal Used for setting the tooltip on the button. */ - @property({ reflect: true, attribute: 'aria-label' }) label?: string; + /** @internal */ + readonly internals = this.attachInternals(); /** - * The pressed state of the button. Set the default value, so the `aria-pressed` attribute is - * added to the element. + * The pressed state of the button. + * + * @default false */ - @property({ type: Boolean, reflect: true }) pressed = false; + @property({ type: Boolean }) pressed?: boolean; /** @internal The pressed icon. */ @state() pressedIcon?: Icon; - /** The size of the button. */ - @property({ reflect: true }) size?: ToggleButtonSize; + /** + * The shape of the button. + * + * @default 'square' + */ + @property({ reflect: true }) shape?: ToggleButtonShape; - /** The shape of the button. */ - @property({ reflect: true }) shape?: ButtonShape; + /** + * The size of the button. + * + * @default 'md' + */ + @property({ reflect: true }) size?: ToggleButtonSize; /** @internal Emits when the button has been toggled. */ @event({ name: 'sl-toggle' }) toggleEvent!: EventEmitter>; @@ -95,38 +119,30 @@ export class ToggleButton extends ScopedElementsMixin(LitElement) { /** The tooltip text for the button. */ @property() tooltip?: string; - override connectedCallback(): void { - super.connectedCallback(); - - this.setAttribute('role', 'button'); - - if (!this.hasAttribute('tabindex')) { - this.tabIndex = 0; - } - } - override firstUpdated(changes: PropertyValues): void { super.firstUpdated(changes); + this.setProxyTarget(this.button); + if (import.meta.env?.DEV) { // Wait for the slotchange events to fire before checking for errors requestAnimationFrame(() => { - this.removeAttribute('error'); + this.internals.states.delete('error'); if (this.parentElement?.tagName !== 'SL-TOGGLE-GROUP' && !this.hasText) { if (!this.defaultIcon) { console.error( 'There needs to be an sl-icon in the "default" slot for the component to work' ); - this.setAttribute('error', ''); + this.internals.states.add('error'); } else if (!this.pressedIcon) { console.error( 'There needs to be an sl-icon in the "pressed" slot for the component to work' ); - this.setAttribute('error', ''); + this.internals.states.add('error'); } else if (this.defaultIcon.name === this.pressedIcon.name) { console.error('Do not use the same icon for both states of the toggle button.'); - this.setAttribute('error', ''); + this.internals.states.add('error'); } } }); @@ -137,43 +153,75 @@ export class ToggleButton extends ScopedElementsMixin(LitElement) { super.updated(changes); if (changes.has('defaultIcon') || changes.has('hasText') || changes.has('pressedIcon')) { - this.toggleAttribute('icon-only', this.#isIconOnly()); - this.toggleAttribute('text-only', !!this.hasText && !this.defaultIcon && !this.pressedIcon); + const iconOnly = !this.hasText && (!!this.defaultIcon || !!this.pressedIcon), + textOnly = !!this.hasText && !this.defaultIcon && !this.pressedIcon, + hasIconOnly = this.internals.states.has('icon-only'); + + if (iconOnly) { + this.internals.states.add('icon-only'); + this.internals.states.delete('text-only'); + } else if (textOnly) { + this.internals.states.delete('icon-only'); + this.internals.states.add('text-only'); + } else { + this.internals.states.delete('icon-only'); + this.internals.states.delete('text-only'); + } + + // Trigger an update when the icon-only state changes + if (hasIconOnly !== iconOnly) { + this.requestUpdate(); + } } if (changes.has('defaultIcon') || changes.has('pressedIcon')) { [this.defaultIcon, this.pressedIcon].filter(Boolean).forEach(icon => { + // Map the button size to the appropriate icon size: xs for sm, otherwise md icon!.size = this.size === 'sm' ? 'xs' : 'md'; }); } if (changes.has('pressed')) { - this.setAttribute('aria-pressed', (this.pressed ?? false).toString()); + if (this.pressed) { + this.internals.states.add('pressed'); + } else { + this.internals.states.delete('pressed'); + } } } override render(): TemplateResult { let ariaType: 'description' | 'label' | undefined; if (this.tooltip) { - ariaType = this.#isIconOnly() ? 'label' : 'description'; + ariaType = this.internals.states.has('icon-only') ? 'label' : 'description'; } return html` -
    +
    + ${this.tooltip - ? html`${this.tooltip}` + ? html` + + ${this.tooltip} + + ` : nothing} `; } #onClick(event: Event): void { - if (this.disabled || this.ariaDisabled === 'true') { + if (this.disabled || this.button.ariaDisabled === 'true') { event.preventDefault(); event.stopImmediatePropagation(); @@ -188,17 +236,11 @@ export class ToggleButton extends ScopedElementsMixin(LitElement) { if (event.target.matches('[name="default"]')) { this.defaultIcon = event.target .assignedElements({ flatten: true }) - .find((element): element is Icon => element instanceof Icon); + .find(element => element instanceof Icon); } else { this.pressedIcon = event.target .assignedElements({ flatten: true }) - .find((element): element is Icon => element instanceof Icon); - } - } - - #onKeydown(event: KeyboardEvent): void { - if (['Enter', ' '].includes(event.key)) { - this.#onClick(event); + .find(element => element instanceof Icon); } } @@ -207,9 +249,4 @@ export class ToggleButton extends ScopedElementsMixin(LitElement) { .assignedNodes({ flatten: true }) .filter(node => node.textContent && node.textContent.trim().length > 0).length; } - - /** Returns true if the button only contains icons and no text. */ - #isIconOnly(): boolean { - return !this.hasText && (!!this.defaultIcon || !!this.pressedIcon); - } } diff --git a/packages/components/toggle-group/src/toggle-group.scss b/packages/components/toggle-group/src/toggle-group.scss index 026142de1b..7efabb9adf 100644 --- a/packages/components/toggle-group/src/toggle-group.scss +++ b/packages/components/toggle-group/src/toggle-group.scss @@ -17,7 +17,7 @@ --_border-color: var(--sl-color-border-disabled); - ::slotted(sl-toggle-button[pressed]) { + ::slotted(sl-toggle-button:state(pressed)) { --_bg-color: var(--sl-color-background-neutral-bold); } } @@ -81,20 +81,16 @@ } ::slotted(sl-toggle-button:first-of-type) { - --_button-border-radius: calc(var(--_border-radius) - var(--sl-size-borderWidth-action)); + --_button-border-radius: calc(var(--_border-radius) - var(--sl-size-borderWidth-action)) 0 0 + calc(var(--_border-radius) - var(--sl-size-borderWidth-action)); --_group-compensation-start: var(--_pill-compensation); - - border-end-end-radius: 0; - border-start-end-radius: 0; } ::slotted(sl-toggle-button:last-of-type) { - --_button-border-radius: calc(var(--_border-radius) - var(--sl-size-borderWidth-action)); + --_button-border-radius: 0 calc(var(--_border-radius) - var(--sl-size-borderWidth-action)) + calc(var(--_border-radius) - var(--sl-size-borderWidth-action)) 0; --_group-compensation-end: var(--_pill-compensation); - border-end-start-radius: 0; - border-start-start-radius: 0; - &::after { display: none; } diff --git a/packages/components/toggle-group/src/toggle-group.spec.ts b/packages/components/toggle-group/src/toggle-group.spec.ts index 19da3c8aa7..81ef708325 100644 --- a/packages/components/toggle-group/src/toggle-group.spec.ts +++ b/packages/components/toggle-group/src/toggle-group.spec.ts @@ -3,6 +3,7 @@ import '@sl-design-system/toggle-button/register.js'; import { fixture } from '@sl-design-system/vitest-browser-lit'; import { html } from 'lit'; import { beforeEach, describe, expect, it } from 'vitest'; +import { userEvent } from 'vitest/browser'; import '../register.js'; import { ToggleGroup } from './toggle-group.js'; @@ -78,19 +79,19 @@ describe('sl-toggle-group', () => { it('should only allow one button to be pressed at a time', async () => { const buttons = Array.from(el.querySelectorAll('sl-toggle-button')); - buttons[0].click(); + await userEvent.click(buttons[0]); await el.updateComplete; - expect(buttons[0].pressed).to.be.true; - expect(buttons[1].pressed).to.be.false; - expect(buttons[2].pressed).to.be.false; + expect(buttons[0]).to.match(':state(pressed)'); + expect(buttons[1]).not.to.match(':state(pressed)'); + expect(buttons[2]).not.to.match(':state(pressed)'); - buttons[1].click(); + await userEvent.click(buttons[1]); await el.updateComplete; - expect(buttons[0].pressed).to.be.false; - expect(buttons[1].pressed).to.be.true; - expect(buttons[2].pressed).to.be.false; + expect(buttons[0]).not.to.match(':state(pressed)'); + expect(buttons[1]).to.match(':state(pressed)'); + expect(buttons[2]).not.to.match(':state(pressed)'); }); }); @@ -121,19 +122,19 @@ describe('sl-toggle-group', () => { it('should allow multiple buttons to be pressed at the same time', async () => { const buttons = Array.from(el.querySelectorAll('sl-toggle-button')); - buttons[0].click(); + await userEvent.click(buttons[0]); await el.updateComplete; - expect(buttons[0].pressed).to.be.true; - expect(buttons[1].pressed).to.be.false; - expect(buttons[2].pressed).to.be.false; + expect(buttons[0]).to.match(':state(pressed)'); + expect(buttons[1]).not.to.match(':state(pressed)'); + expect(buttons[2]).not.to.match(':state(pressed)'); - buttons[1].click(); + await userEvent.click(buttons[1]); await el.updateComplete; - expect(buttons[0].pressed).to.be.true; - expect(buttons[1].pressed).to.be.true; - expect(buttons[2].pressed).to.be.false; + expect(buttons[0]).to.match(':state(pressed)'); + expect(buttons[1]).to.match(':state(pressed)'); + expect(buttons[2]).not.to.match(':state(pressed)'); }); }); }); diff --git a/packages/components/toggle-group/src/toggle-group.ts b/packages/components/toggle-group/src/toggle-group.ts index c4f435c356..4db5a0c514 100644 --- a/packages/components/toggle-group/src/toggle-group.ts +++ b/packages/components/toggle-group/src/toggle-group.ts @@ -38,7 +38,7 @@ export class ToggleGroup extends LitElement { this.renderRoot .querySelector('slot') ?.assignedElements({ flatten: true }) - .filter((element): element is ToggleButton => element instanceof ToggleButton) ?? [] + .filter(element => element instanceof ToggleButton) ?? [] ); } @@ -50,7 +50,11 @@ export class ToggleGroup extends LitElement { isFocusableElement: (el: ToggleButton) => !el.disabled }); - /** If set, will disable all buttons in the group. */ + /** + * If set, will disable all buttons in the group. + * + * @default false + */ @property({ type: Boolean, reflect: true }) disabled?: boolean; /** @@ -60,16 +64,30 @@ export class ToggleGroup extends LitElement { * When set to true multiple buttons can be active at the same time. In this case the group does * nothing when a button is toggled. Use this mode if you want to handle the toggling of buttons * yourself. + * + * @default false */ @property({ type: Boolean }) multiple?: boolean; - /** Determines the size of all buttons in the group. */ + /** + * Determines the size of all buttons in the group. + * + * @default 'md' + */ @property({ reflect: true }) size?: ToggleGroupSize; - /** The shaoe of the group. */ + /** + * The shape of the group. + * + * @default 'square' + */ @property({ reflect: true }) shape?: ToggleGroupShape; - /** The variant of the toggle-group. */ + /** + * The variant of the toggle-group. + * + * @default 'solid' + */ @property({ reflect: true }) fill?: ToggleGroupFill; override connectedCallback(): void { @@ -93,14 +111,11 @@ export class ToggleGroup extends LitElement { #onSlotChange(): void { this.#rovingTabindexController.clearElementCache(); - this.#updateButtonProperties(); } #onToggle(event: SlToggleEvent): void { - if (this.multiple) { - return; - } else if (event.detail) { + if (!this.multiple && event.detail) { this.#buttons .filter(button => button !== event.target) .forEach(button => (button.pressed = false)); @@ -112,6 +127,7 @@ export class ToggleGroup extends LitElement { if (typeof this.disabled === 'boolean') { button.disabled = this.disabled; } + button.fill = this.fill; if (this.size) { From 0a0c0a66be7dc30c8a4b94d0866b4077289dbfb0 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 3 Jun 2026 13:00:22 +0200 Subject: [PATCH 35/50] =?UTF-8?q?=E2=AD=90=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/violet-baboons-crash.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/violet-baboons-crash.md b/.changeset/violet-baboons-crash.md index 1b842f5d38..ddd8b7e233 100644 --- a/.changeset/violet-baboons-crash.md +++ b/.changeset/violet-baboons-crash.md @@ -18,7 +18,7 @@ The complex internal positioning logic, `AnchorController`, `EventsController`, #### New API - `for` — links the tooltip to an anchor element by ID -- `type` — controls the ARIA relationship: `'label'` (`aria-labelledby`) or `'description'` (`aria-describedby`, default) +- `type` — controls the ARIA relationship: `'label'` (`ariaLabelledByElements`, default) or `'description'` (`ariaDescribedByElements`) - `trigger` — space-separated list of triggers: `'focus'`, `'hover'`, and/or `'click'` (default: `'focus hover'`) - `disabled` — prevents the tooltip from showing - `open` — reflects the current open state From 15244c2b0aea4ba3aa121a664a727ed07820f224 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 3 Jun 2026 13:11:00 +0200 Subject: [PATCH 36/50] =?UTF-8?q?=F0=9F=A6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/breadcrumbs/src/breadcrumbs.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/components/breadcrumbs/src/breadcrumbs.spec.ts b/packages/components/breadcrumbs/src/breadcrumbs.spec.ts index e25f6e85ed..4876da06d7 100644 --- a/packages/components/breadcrumbs/src/breadcrumbs.spec.ts +++ b/packages/components/breadcrumbs/src/breadcrumbs.spec.ts @@ -287,7 +287,9 @@ describe('sl-breadcrumbs', () => { 'slot[name^="breadcrumb-"]:not([name*="menu"])' ) ); - const visibleLinks = slots.map(slot => slot.assignedElements()[0]); + const visibleLinks = slots.map(slot => + slot.assignedElements().find(el => el instanceof HTMLAnchorElement) + ); expect(visibleLinks).to.have.length(2); expect(visibleLinks[0]).to.have.trimmed.text('5'); @@ -299,7 +301,9 @@ describe('sl-breadcrumbs', () => { menuSlots = Array.from( el.renderRoot.querySelectorAll('slot[name^="breadcrumb-menu-"]') ), - menuItems = menuSlots.map(slot => slot.assignedElements()[0]); + menuItems = menuSlots.map(slot => + slot.assignedElements().find(el => el instanceof HTMLAnchorElement) + ); expect(button).to.exist; expect(button).to.have.attribute('fill', 'ghost'); From 76bfd444dded31e93c1f96d47231b548c073f27b Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 3 Jun 2026 13:16:43 +0200 Subject: [PATCH 37/50] =?UTF-8?q?=F0=9F=90=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/breadcrumbs/src/breadcrumbs.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/components/breadcrumbs/src/breadcrumbs.spec.ts b/packages/components/breadcrumbs/src/breadcrumbs.spec.ts index 4876da06d7..33b1657beb 100644 --- a/packages/components/breadcrumbs/src/breadcrumbs.spec.ts +++ b/packages/components/breadcrumbs/src/breadcrumbs.spec.ts @@ -287,10 +287,14 @@ describe('sl-breadcrumbs', () => { 'slot[name^="breadcrumb-"]:not([name*="menu"])' ) ); + console.log('slots', slots); + const visibleLinks = slots.map(slot => slot.assignedElements().find(el => el instanceof HTMLAnchorElement) ); + console.log('visibleLinks', visibleLinks); + expect(visibleLinks).to.have.length(2); expect(visibleLinks[0]).to.have.trimmed.text('5'); expect(visibleLinks[1]).to.have.trimmed.text('6'); From 63a0b59a94359d2bde49eb2fc4fb1cea0929ba11 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 3 Jun 2026 13:24:27 +0200 Subject: [PATCH 38/50] =?UTF-8?q?=F0=9F=8F=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../breadcrumbs/src/breadcrumbs.spec.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/components/breadcrumbs/src/breadcrumbs.spec.ts b/packages/components/breadcrumbs/src/breadcrumbs.spec.ts index 33b1657beb..74558aa503 100644 --- a/packages/components/breadcrumbs/src/breadcrumbs.spec.ts +++ b/packages/components/breadcrumbs/src/breadcrumbs.spec.ts @@ -300,18 +300,23 @@ describe('sl-breadcrumbs', () => { expect(visibleLinks[1]).to.have.trimmed.text('6'); }); + it('should have an expand button to show the rest of the breadcrumbs', () => { + const button = el.renderRoot.querySelector('sl-button'); + + expect(button).to.exist; + expect(button).to.have.attribute('fill', 'ghost'); + expect(button?.querySelector('sl-icon')).to.have.attribute('name', 'ellipsis'); + }); + it('should show all hidden links in the popover', () => { - const button = el.renderRoot.querySelector('sl-button'), - menuSlots = Array.from( + const menuSlots = Array.from( el.renderRoot.querySelectorAll('slot[name^="breadcrumb-menu-"]') ), menuItems = menuSlots.map(slot => slot.assignedElements().find(el => el instanceof HTMLAnchorElement) ); - expect(button).to.exist; - expect(button).to.have.attribute('fill', 'ghost'); - expect(button?.querySelector('sl-icon')).to.have.attribute('name', 'ellipsis'); + console.log('menuItems', menuItems); expect(menuItems).to.have.length(4); expect(menuItems[0]).to.have.text('1'); From b8009a81856d39f9a030e381b5e6d45668c1ecd5 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 3 Jun 2026 13:30:43 +0200 Subject: [PATCH 39/50] =?UTF-8?q?=F0=9F=90=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../breadcrumbs/src/breadcrumbs.spec.ts | 24 +++++++++---------- .../components/breadcrumbs/src/breadcrumbs.ts | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/components/breadcrumbs/src/breadcrumbs.spec.ts b/packages/components/breadcrumbs/src/breadcrumbs.spec.ts index 74558aa503..0da6a53618 100644 --- a/packages/components/breadcrumbs/src/breadcrumbs.spec.ts +++ b/packages/components/breadcrumbs/src/breadcrumbs.spec.ts @@ -19,7 +19,7 @@ describe('sl-breadcrumbs', () => { `); // Wait for the component to process slot assignments - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); }); it('should have a navigation role', () => { @@ -209,7 +209,7 @@ describe('sl-breadcrumbs', () => { `); // Wait for the component to process slot assignments - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); }); it('should have all links with separators in the DOM', () => { @@ -271,7 +271,7 @@ describe('sl-breadcrumbs', () => { `); // Wait for the component to process slot assignments - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); }); it('should only show the home icon', () => { @@ -337,7 +337,7 @@ describe('sl-breadcrumbs', () => { `); // Wait for the component to process slot assignments - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); }); it('should render the custom home link', () => { @@ -372,7 +372,7 @@ describe('sl-breadcrumbs', () => { `); // Wait for the component to process slot assignments - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); }); it('should update when links are added dynamically', async () => { @@ -382,7 +382,7 @@ describe('sl-breadcrumbs', () => { el.appendChild(newLink); // Wait for mutation observer - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => requestAnimationFrame(resolve)); const links = Array.from(el.querySelectorAll('a')), @@ -398,7 +398,7 @@ describe('sl-breadcrumbs', () => { lastLink.remove(); // Wait for mutation observer - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => requestAnimationFrame(resolve)); const remainingLinks = Array.from(el.querySelectorAll('a')), @@ -465,7 +465,7 @@ describe('sl-breadcrumbs', () => { `); // Wait for the component to process slot assignments - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); // Should not show collapse button const button = el.renderRoot.querySelector('sl-button'), @@ -488,7 +488,7 @@ describe('sl-breadcrumbs', () => { `); // Wait for the component to process slot assignments - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); // Should not show collapse button with exactly threshold (3) links - need MORE than threshold const button = el.renderRoot.querySelector('sl-button'); @@ -514,7 +514,7 @@ describe('sl-breadcrumbs', () => { `); // Wait for the component to process slot assignments - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); // Should process all elements, not just anchors const slots = Array.from( @@ -530,7 +530,7 @@ describe('sl-breadcrumbs', () => { el = await fixture(html``); // Wait for the component to process slot assignments - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); // Should still render home link const homeLink = el.renderRoot.querySelector('li.home a'); @@ -552,7 +552,7 @@ describe('sl-breadcrumbs', () => { `); // Wait for the component to process slot assignments - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); // Should not render home section at all when noHome is true const homeListItem = el.renderRoot.querySelector('li.home'); diff --git a/packages/components/breadcrumbs/src/breadcrumbs.ts b/packages/components/breadcrumbs/src/breadcrumbs.ts index 4d1dc971d2..320e926ae8 100644 --- a/packages/components/breadcrumbs/src/breadcrumbs.ts +++ b/packages/components/breadcrumbs/src/breadcrumbs.ts @@ -282,7 +282,7 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { } this.#assignSlotsTimeoutId = undefined; - }, 20); + }, 50); } #assignSlots(): void { From a944d652ba7a8cb25d60531c6b63dac682267937 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 3 Jun 2026 13:31:17 +0200 Subject: [PATCH 40/50] =?UTF-8?q?=F0=9F=8E=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/breadcrumbs/src/breadcrumbs.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/breadcrumbs/src/breadcrumbs.spec.ts b/packages/components/breadcrumbs/src/breadcrumbs.spec.ts index 0da6a53618..30f0a4f352 100644 --- a/packages/components/breadcrumbs/src/breadcrumbs.spec.ts +++ b/packages/components/breadcrumbs/src/breadcrumbs.spec.ts @@ -421,8 +421,8 @@ describe('sl-breadcrumbs', () => { `); - await new Promise(resolve => requestAnimationFrame(resolve)); - await el.updateComplete; + // Wait for the component to process slot assignments + await new Promise(resolve => setTimeout(resolve, 100)); }); it('should toggle popover when button is clicked', async () => { From 52100b137f1b91713c76660eb498b80082825051 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 3 Jun 2026 13:39:49 +0200 Subject: [PATCH 41/50] =?UTF-8?q?=F0=9F=90=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../breadcrumbs/src/breadcrumbs.spec.ts | 5 ----- .../components/breadcrumbs/src/breadcrumbs.ts | 16 ++++++++-------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/components/breadcrumbs/src/breadcrumbs.spec.ts b/packages/components/breadcrumbs/src/breadcrumbs.spec.ts index 30f0a4f352..36f5d35c97 100644 --- a/packages/components/breadcrumbs/src/breadcrumbs.spec.ts +++ b/packages/components/breadcrumbs/src/breadcrumbs.spec.ts @@ -287,14 +287,11 @@ describe('sl-breadcrumbs', () => { 'slot[name^="breadcrumb-"]:not([name*="menu"])' ) ); - console.log('slots', slots); const visibleLinks = slots.map(slot => slot.assignedElements().find(el => el instanceof HTMLAnchorElement) ); - console.log('visibleLinks', visibleLinks); - expect(visibleLinks).to.have.length(2); expect(visibleLinks[0]).to.have.trimmed.text('5'); expect(visibleLinks[1]).to.have.trimmed.text('6'); @@ -316,8 +313,6 @@ describe('sl-breadcrumbs', () => { slot.assignedElements().find(el => el instanceof HTMLAnchorElement) ); - console.log('menuItems', menuItems); - expect(menuItems).to.have.length(4); expect(menuItems[0]).to.have.text('1'); expect(menuItems[1]).to.have.text('2'); diff --git a/packages/components/breadcrumbs/src/breadcrumbs.ts b/packages/components/breadcrumbs/src/breadcrumbs.ts index 320e926ae8..bdb3b599f2 100644 --- a/packages/components/breadcrumbs/src/breadcrumbs.ts +++ b/packages/components/breadcrumbs/src/breadcrumbs.ts @@ -326,22 +326,22 @@ export class Breadcrumbs extends ScopedElementsMixin(LitElement) { (el): el is HTMLElement => el instanceof HTMLElement && !(el instanceof Tooltip) && !el.hasAttribute('slot') ) - .map(el => { + .map(crumb => { // Make sure the breadcrumb has a unique DOM id we can reference - el.id ||= `sl-breadcrumb-${nextUniqueId++}`; + crumb.id ||= `sl-breadcrumb-${nextUniqueId++}`; // Use an existing tooltip, or create a new one - let tooltip = el.ariaLabelledByElements?.find( - (el): el is Tooltip => el instanceof Tooltip && el.for === el.id + let tooltip = crumb.ariaLabelledByElements?.find( + (el): el is Tooltip => el instanceof Tooltip && el.for === crumb.id ); if (!tooltip) { tooltip = this.shadowRoot!.createElement('sl-tooltip'); - tooltip.for = el.id; - tooltip.textContent = el.textContent?.trim() || ''; - el.after(tooltip); + tooltip.for = crumb.id; + tooltip.textContent = crumb.textContent?.trim() || ''; + crumb.after(tooltip); } - return { element: el, tooltip }; + return { element: crumb, tooltip }; }); this.customHomeLink = children.find(el => el.getAttribute('slot') === 'home'); From ddd256af3ed5b19d21373e49849c0d9ae112d753 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 3 Jun 2026 13:56:23 +0200 Subject: [PATCH 42/50] =?UTF-8?q?=F0=9F=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/angular/src/tooltip.directive.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/angular/src/tooltip.directive.ts b/packages/angular/src/tooltip.directive.ts index c283ba0dd7..8562104e1b 100644 --- a/packages/angular/src/tooltip.directive.ts +++ b/packages/angular/src/tooltip.directive.ts @@ -14,7 +14,7 @@ import '@sl-design-system/tooltip/register.js'; standalone: true }) export class TooltipDirective implements AfterViewInit, OnChanges, OnDestroy { - private tooltip?: Tooltip | (() => void); + private tooltip?: Tooltip; @Input() slTooltip = ''; @@ -27,19 +27,15 @@ export class TooltipDirective implements AfterViewInit, OnChanges, OnDestroy { } ngAfterViewInit(): void { - this.tooltip = Tooltip.lazy(this.elRef.nativeElement, tooltip => { - this.tooltip = tooltip; - tooltip.textContent = this.slTooltip; - }); + this.tooltip = document.createElement('sl-tooltip'); + this.tooltip.for = + this.elRef.nativeElement.id ||= `sl-tooltip-${Math.random().toString(36).slice(2)}`; + this.tooltip.textContent = this.slTooltip; + this.elRef.nativeElement.after(this.tooltip); } ngOnDestroy(): void { - if (this.tooltip instanceof Tooltip) { - this.tooltip?.remove(); - this.tooltip = undefined; - } else if (this.tooltip) { - this.tooltip(); - this.tooltip = undefined; - } + this.tooltip?.remove(); + this.tooltip = undefined; } } From 876ce64af7dfeaa9e6f6b5ee8d6a642542fbba15 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 3 Jun 2026 13:56:56 +0200 Subject: [PATCH 43/50] =?UTF-8?q?=F0=9F=8E=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/nasty-clowns-spend.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/nasty-clowns-spend.md diff --git a/.changeset/nasty-clowns-spend.md b/.changeset/nasty-clowns-spend.md new file mode 100644 index 0000000000..cebed11101 --- /dev/null +++ b/.changeset/nasty-clowns-spend.md @@ -0,0 +1,5 @@ +--- +'@sl-design-system/angular': minor +--- + +Use the new tooltip implementation From 7db9e8599f9256a2f5402f89cb4777ea96bb9dfc Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 3 Jun 2026 13:58:01 +0200 Subject: [PATCH 44/50] =?UTF-8?q?=F0=9F=8E=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/nasty-clowns-spend.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/nasty-clowns-spend.md b/.changeset/nasty-clowns-spend.md index cebed11101..891fe8fc0b 100644 --- a/.changeset/nasty-clowns-spend.md +++ b/.changeset/nasty-clowns-spend.md @@ -3,3 +3,5 @@ --- Use the new tooltip implementation + +The tooltip directive has been updated to use the new tooltip implementation. This means that the tooltip is now created as a separate element and positioned using the `for` attribute. The old implementation, which used a lazy loader, has been removed. The API of the tooltip directive remains the same, but the DOM structure may have changed. From 56de8070c107fe03a3d3e3cc78db707ce036100f Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Wed, 3 Jun 2026 14:13:27 +0200 Subject: [PATCH 45/50] =?UTF-8?q?=F0=9F=90=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/clean-chicken-look.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .changeset/clean-chicken-look.md diff --git a/.changeset/clean-chicken-look.md b/.changeset/clean-chicken-look.md new file mode 100644 index 0000000000..359eff8f19 --- /dev/null +++ b/.changeset/clean-chicken-look.md @@ -0,0 +1,19 @@ +--- +'@sl-design-system/toggle-button': minor +--- + +Refactor toggle button to use an internal ` +
    Tooltip
    + `; + } + } + ); + + return html``; + } +}; diff --git a/yarn.lock b/yarn.lock index 15515ea57e..7379b6fdbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4123,6 +4123,18 @@ __metadata: languageName: node linkType: hard +"@oddbird/css-anchor-positioning@npm:^0.10.0-alpha": + version: 0.10.0-alpha + resolution: "@oddbird/css-anchor-positioning@npm:0.10.0-alpha" + dependencies: + "@floating-ui/dom": "npm:^1.7.6" + "@types/css-tree": "npm:^2.3.11" + css-tree: "npm:^3.2.1" + nanoid: "npm:^5.1.11" + checksum: 10c0/ac57007611732e572883afe6122316c0c8c38fac51095f232b1e338f3b8b599d082c8e595353815618106ec3dce98b6db11e21997d38329fc596df5bc1ac1472 + languageName: node + linkType: hard + "@open-wc/dedupe-mixin@npm:^2.0.0, @open-wc/dedupe-mixin@npm:^2.0.1": version: 2.0.1 resolution: "@open-wc/dedupe-mixin@npm:2.0.1" @@ -6107,6 +6119,7 @@ __metadata: "@custom-elements-manifest/analyzer": "npm:^0.11.0" "@faker-js/faker": "npm:^10.4.0" "@lit/localize-tools": "npm:^0.8.2" + "@oddbird/css-anchor-positioning": "npm:^0.10.0-alpha" "@playwright/test": "npm:^1.60.0" "@storybook/addon-a11y": "npm:^10.4.1" "@storybook/addon-docs": "npm:^10.4.1" @@ -7209,6 +7222,13 @@ __metadata: languageName: node linkType: hard +"@types/css-tree@npm:^2.3.11": + version: 2.3.11 + resolution: "@types/css-tree@npm:2.3.11" + checksum: 10c0/8f8706b3a94eabe1218da47ca067e337da37ae9ec5bedd1d695747fac4c9fc5c8fe66ba90024f8f29e590e63e9eb0121c143d7788687ff8167cd3457d1979ac1 + languageName: node + linkType: hard + "@types/debug@npm:^4.0.0": version: 4.1.9 resolution: "@types/debug@npm:4.1.9" @@ -10984,6 +11004,16 @@ __metadata: languageName: node linkType: hard +"css-tree@npm:^3.2.1": + version: 3.2.1 + resolution: "css-tree@npm:3.2.1" + dependencies: + mdn-data: "npm:2.27.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/1f65e9ccaa56112a4706d6f003dd43d777f0dbcf848e66fd320f823192533581f8dd58daa906cb80622658332d50284d6be13b87a6ab4556cbbfe9ef535bbf7e + languageName: node + linkType: hard + "css-tree@npm:~2.2.0": version: 2.2.1 resolution: "css-tree@npm:2.2.1" @@ -17159,6 +17189,13 @@ __metadata: languageName: node linkType: hard +"mdn-data@npm:2.27.1": + version: 2.27.1 + resolution: "mdn-data@npm:2.27.1" + checksum: 10c0/eb8abf5d22e4d1e090346f5e81b67d23cef14c83940e445da5c44541ad874dc8fb9f6ca236e8258c3a489d9fb5884188a4d7d58773adb9089ac2c0b966796393 + languageName: node + linkType: hard + "mdn-data@npm:^2.21.0": version: 2.21.0 resolution: "mdn-data@npm:2.21.0" @@ -18138,6 +18175,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^5.1.11": + version: 5.1.11 + resolution: "nanoid@npm:5.1.11" + bin: + nanoid: bin/nanoid.js + checksum: 10c0/91580d18c29263ac0e871734f0d86e7f906f523f974d3c30fc65354ccf387ccffd606c2a6c28acc2977a3950146347e790ce9e3f514133a48995af5ccdb308ce + languageName: node + linkType: hard + "nanomatch@npm:^1.2.9": version: 1.2.13 resolution: "nanomatch@npm:1.2.13"