diff --git a/.changeset/afraid-parents-nail.md b/.changeset/afraid-parents-nail.md new file mode 100644 index 0000000000..0e8650c6ad --- /dev/null +++ b/.changeset/afraid-parents-nail.md @@ -0,0 +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 + +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 set a tooltip directly on the component. + +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/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 ` + ${this.tooltip + ? html` + + ${this.tooltip} + + ` + : nothing} `; } @@ -234,10 +260,17 @@ export class Button extends ForwardAriaMixin(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'); } + + // Trigger an update when the icon-only state changes + if (hasIconOnly !== iconOnly) { + this.requestUpdate(); + } } } diff --git a/packages/components/calendar/package.json b/packages/components/calendar/package.json index cf7c9c4d63..51f8ce4618 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/calendar/src/calendar.spec.ts b/packages/components/calendar/src/calendar.spec.ts index 5d5a6d5cbe..d828e1be83 100644 --- a/packages/components/calendar/src/calendar.spec.ts +++ b/packages/components/calendar/src/calendar.spec.ts @@ -754,19 +754,17 @@ describe('sl-calendar', () => { .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 bce12fb65f..ddf3a54099 100644 --- a/packages/components/calendar/src/calendar.ts +++ b/packages/components/calendar/src/calendar.ts @@ -218,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/calendar/src/month-view.spec.ts b/packages/components/calendar/src/month-view.spec.ts index 9b6bf63b4c..d81f195fe7 100644 --- a/packages/components/calendar/src/month-view.spec.ts +++ b/packages/components/calendar/src/month-view.spec.ts @@ -500,11 +500,11 @@ describe('sl-month-view', () => { tooltip = button?.nextElementSibling; expect(button).to.exist; - expect(button).to.have.attribute('aria-describedby', tooltip?.id); + expect(button?.ariaDescribedByElements).to.have.length(1); + expect(button?.ariaDescribedByElements).to.include(tooltip); expect(tooltip).to.match('sl-tooltip'); - expect(tooltip).to.have.attribute('id'); - expect(tooltip?.textContent?.trim()).to.equal('Special day'); + expect(tooltip).to.have.trimmed.text('Special day'); }); it('should only show one tooltip at a time and maintain ARIA stability', async () => { @@ -525,8 +525,8 @@ describe('sl-month-view', () => { const tooltips = el.renderRoot.querySelectorAll('sl-tooltip'); expect(tooltips).to.have.length(2); - expect(button13).to.have.attribute('aria-describedby', tooltips[0].id); - expect(button14).to.have.attribute('aria-describedby', tooltips[1].id); + expect(button13?.ariaDescribedByElements).to.include(tooltips[0]); + expect(button14?.ariaDescribedByElements).to.include(tooltips[1]); expect(Array.from(tooltips).every(t => !isPopoverOpen(t))).to.be.true; // 1. Hover first button @@ -552,8 +552,8 @@ describe('sl-month-view', () => { expect(Array.from(tooltips).every(t => !isPopoverOpen(t))).to.be.true; // 4. ARIA stability check (even when closed) - expect(button13).to.have.attribute('aria-describedby', tooltips[0].id); - expect(button14).to.have.attribute('aria-describedby', tooltips[1].id); + expect(button13?.ariaDescribedByElements).to.include(tooltips[0]); + expect(button14?.ariaDescribedByElements).to.include(tooltips[1]); }); it('should render no tooltip when no color or label provided', async () => { diff --git a/packages/components/calendar/src/month-view.ts b/packages/components/calendar/src/month-view.ts index c09681f7f4..d21c35c1c3 100644 --- a/packages/components/calendar/src/month-view.ts +++ b/packages/components/calendar/src/month-view.ts @@ -354,17 +354,15 @@ export class MonthView extends LocaleMixin(ScopedElementsMixin(LitElement)) { @keydown=${(event: KeyboardEvent) => this.#onKeydown(event, day)} ?autofocus=${autofocus} aria-current=${ifDefined(parts.includes('today') ? 'date' : undefined)} - aria-describedby=${ifDefined( - day.indicator?.label ? `indicator-${day.date.toISOString()}` : undefined - )} aria-label=${this.getDayLabel(day)} aria-pressed=${selected.toString()} + id=${day.date.toISOString()} part=${parts.join(' ')}> ${day.date.getDate()} ${day.indicator?.label ? html` - + ${day.indicator.label} ` 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/checkbox/src/checkbox-group.stories.ts b/packages/components/checkbox/src/checkbox-group.stories.ts index 88cb8c9fc9..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'; @@ -76,6 +77,11 @@ export default { Report validity + `; } } satisfies Meta; @@ -140,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 8679d48d6a..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; 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..3b07f9f764 100644 --- a/packages/components/checkbox/src/checkbox.stories.ts +++ b/packages/components/checkbox/src/checkbox.stories.ts @@ -237,12 +237,9 @@ 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 `; } }; diff --git a/packages/components/combobox/CHANGELOG.md b/packages/components/combobox/CHANGELOG.md index d5f11aba5f..0e43be126a 100644 --- a/packages/components/combobox/CHANGELOG.md +++ b/packages/components/combobox/CHANGELOG.md @@ -20,11 +20,9 @@ ### Patch Changes - [#3211](https://github.com/sl-design-system/components/pull/3211) [`20a1178`](https://github.com/sl-design-system/components/commit/20a1178f0f1548bd083df7d337ecba443daf579f) - Functional changes: - - The popover opens when you click in the combobox, no longer when you enter the combobox with keyboard navigation. Accessibility improvements: - - Forward ARIA attributes (`aria-label`, `aria-describedby`, `aria-labelledby`) from host element to the input element for proper screen reader support - Automatically associate label with input via `aria-labelledby` when a label is present @@ -175,7 +173,6 @@ ``` You can customize the rendering of each option by using: - - `optionLabelPath` to specify the path to the label in each option object - `optionValuePath` to specify the path to the value in each option object @@ -185,7 +182,6 @@ the options in both scenarios by using the `sl-option { ... }` selector. - [#1642](https://github.com/sl-design-system/components/pull/1642) [`cef2371`](https://github.com/sl-design-system/components/commit/cef2371d5868439edbba8156bf38c167b72f0f39) - Various combobox fixes: - - Add `aria-owns` for linking the input to the listbox - Add `aria-posinset` and `aria-setsize` to the listbox options for virtual lists - Add focus style to tags @@ -208,7 +204,6 @@ ### Patch Changes - [#1599](https://github.com/sl-design-system/components/pull/1599) [`4714b36`](https://github.com/sl-design-system/components/commit/4714b36f1387d4d1731a310b621caf5a33be105b) - Various a11y related fixes/improvements: - - The label was associated with the `` element instead of the `` element - `aria-selected="false"` was missing on the non-selected options - `aria-multiselectable="true"` was missing on the listbox when the multiple property is set diff --git a/packages/components/combobox/package.json b/packages/components/combobox/package.json index 08008570ce..4bc30c2d49 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 aa832f7ffa..7698023686 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/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.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 1034c4e892..a11260c6b6 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 { @@ -15,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 */ @@ -30,8 +33,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 +45,21 @@ 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'); + const slot = this.renderRoot.querySelector('slot'); - this.#tooltip.remove(); - this.#tooltip = undefined; - } else if (this.#tooltip) { - this.#tooltip(); - this.#tooltip = undefined; - } + this.tooltip = !!slot && slot.offsetWidth < slot.scrollWidth; } } diff --git a/packages/components/form/CHANGELOG.md b/packages/components/form/CHANGELOG.md index 5aaff3415d..8300faeb7f 100644 --- a/packages/components/form/CHANGELOG.md +++ b/packages/components/form/CHANGELOG.md @@ -16,7 +16,6 @@ ### Minor Changes - [#3248](https://github.com/sl-design-system/components/pull/3248) [`fc60898`](https://github.com/sl-design-system/components/commit/fc60898ea3c7b5b234a13c6bf157e89528f3a11f) - Standardized warning and error icons: - - Changed `warning` icons from `octagon-exclamation-solid` to `triangle-exclamation-solid` in Callout, Inline message, and Progress bar. - Changed `circle-exclamation-solid` to `triangle-exclamation-solid` in validation messages in the Form field. - Changed `error/danger` icons from `diamond-exclamation-solid` or `octagon-exclamation-solid` to the new `octagon-xmark-solid` icon in Callout, Inline message, and Progress bar. Make sure to update your theme if you update any of these components. diff --git a/packages/components/grid/CHANGELOG.md b/packages/components/grid/CHANGELOG.md index 2b05594182..f3e17de222 100644 --- a/packages/components/grid/CHANGELOG.md +++ b/packages/components/grid/CHANGELOG.md @@ -201,7 +201,6 @@ ### Minor Changes - [#2034](https://github.com/sl-design-system/components/pull/2034) [`1072075`](https://github.com/sl-design-system/components/commit/1072075e3f1b5f0bf8b07dc1f89fd39b9f7103d0) - Big improvements: - - New visual styles throughout all components - Refactored to use the new `ListDataSource` class and view model types - Removed `SelectionController` since that logic is now part of `ListDataSource` @@ -224,7 +223,6 @@ This change removes the `activatable-row` property and instead leaves it to the user to set the `activeRow` property on the `sl-grid` component. The examples in Storybook now also use a button with an avatar to activate the row, which is more accessible than using a checkbox. This fixes the issue we had before where we could not find a solution how to make the row activatable with the keyboard, while also keeping the checkbox for selection. Now, the row can be activated with the keyboard by focusing the button and pressing Enter or Space. And at the same time, the checkbox can still be used for selection. - [#2024](https://github.com/sl-design-system/components/pull/2024) [`a343e29`](https://github.com/sl-design-system/components/commit/a343e298d6b65966e04b3fbfc3598305a29bf1cc) - Grid improvements: - - Add "Cancel selection" button to the bulk action toolbar - Add `column` argument to `GridColumnHeaderRenderer` type - Fix missing aria-label on a selection column checkbox @@ -248,7 +246,6 @@ ### Patch Changes - [#2077](https://github.com/sl-design-system/components/pull/2077) [`778e8a1`](https://github.com/sl-design-system/components/commit/778e8a1ae5dc7908e5c000a620b8143883c75a91) - - Adds option to have no skip table links - - Fixes issue for Safari (an other browsers that don't support native anchor positioning) where to "Skip to start of table" link was positioned incorrectly - [#2072](https://github.com/sl-design-system/components/pull/2072) [`77b348d`](https://github.com/sl-design-system/components/commit/77b348d19a4869f9242d8ea1c70d32d1e6d04212) - Fix regression with basic drag and drop of rows within grid @@ -349,7 +346,6 @@ - [#1791](https://github.com/sl-design-system/components/pull/1791) [`133b883`](https://github.com/sl-design-system/components/commit/133b883234d911dabe37bd3c8acef26afea20fe9) - Replace `--sl-size-borderWidth-subtle` with `--sl-size-borderWidth-default` - [#1653](https://github.com/sl-design-system/components/pull/1653) [`f15d75c`](https://github.com/sl-design-system/components/commit/f15d75c6c3765b797f0bed57c5d1f2855cab4f7e) - Improve horizontal scrolling experience: - - Add shadows to the left and right of the grid when it is scrollable - Add the new `` when the grid is horizontally scrollable - Make sure the scrollbar is always visible (sticky at the bottom of the grid) @@ -375,7 +371,6 @@ - [#1609](https://github.com/sl-design-system/components/pull/1609) [`515e2fb`](https://github.com/sl-design-system/components/commit/515e2fbbda7ecee92392b8ddf9f98c335fe32cf6) - Added tokens for grid - [#1616](https://github.com/sl-design-system/components/pull/1616) [`b1e3b74`](https://github.com/sl-design-system/components/commit/b1e3b741e78400e3755ddaa0c5c4fdeed2e3f960) - Improved accessibilty of the table; - - Added aria-rowindex and aria-rowcount; - Improved keyboardnavigation, including skip table links - Changed the way selecting works; active row by clicking on the entire row and selecting a row by checking the checkbox @@ -392,7 +387,6 @@ - [#1693](https://github.com/sl-design-system/components/pull/1693) [`4e57f9c`](https://github.com/sl-design-system/components/commit/4e57f9c60835a07db45f74fde73a3bf13b6abe51) - Refactor existing data sources into list specific datasources, clearing the way to add `TreeDataSource` in the `@sl-design-system/tree` package. - - The base `DataSource` class has support for sorting and filtering - Grouping and pagination has been moved to the `ListDataSource` class - `ArrayDataSource` and `FetchDataSource` have been renamed to `ArrayListDataSource` and `FetchListDataSource` respectively diff --git a/packages/components/grid/package.json b/packages/components/grid/package.json index 202ec017f8..bec07d4eed 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/grid/src/grid.spec.ts b/packages/components/grid/src/grid.spec.ts index c376c62c68..4cb860d8f8 100644 --- a/packages/components/grid/src/grid.spec.ts +++ b/packages/components/grid/src/grid.spec.ts @@ -1,15 +1,12 @@ import '@sl-design-system/button/register.js'; import { ArrayListDataSource } from '@sl-design-system/data-source'; 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 '@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'; @@ -23,17 +20,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` @@ -227,173 +213,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/grid/src/grid.ts b/packages/components/grid/src/grid.ts index 89498877b3..a7f8dd524d 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 }; } @@ -505,14 +502,11 @@ export class Grid extends ScopedElementsMixin(LitElement) { - - ${msg('Cancel selection', { id: 'sl.grid.cancelSelection' })} - ${!this.noSkipLinks diff --git a/packages/components/grid/src/stories/basics.stories.ts b/packages/components/grid/src/stories/basics.stories.ts index 4a53e42b63..b114a48ddc 100644 --- a/packages/components/grid/src/stories/basics.stories.ts +++ b/packages/components/grid/src/stories/basics.stories.ts @@ -125,8 +125,8 @@ 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 }}> 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/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/CHANGELOG.md b/packages/components/listbox/CHANGELOG.md index 44194657af..4cac633f1b 100644 --- a/packages/components/listbox/CHANGELOG.md +++ b/packages/components/listbox/CHANGELOG.md @@ -62,7 +62,6 @@ ### Patch Changes - [#1632](https://github.com/sl-design-system/components/pull/1632) [`e68df34`](https://github.com/sl-design-system/components/commit/e68df344917a8d0bdc6a4c92f59079a247c6e7a9) - Add ability to render grouped items using lit-virtualizer: - - New `optionGroupPath` property to specify the path to the group name in the option object - New `` component to render the group header - Add `items` property for advanced customization of how options are rendered (used in combobox) diff --git a/packages/components/listbox/package.json b/packages/components/listbox/package.json index d922dacdb2..b0d5c40099 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/menu/CHANGELOG.md b/packages/components/menu/CHANGELOG.md index 250413032b..8ff8ddb3af 100644 --- a/packages/components/menu/CHANGELOG.md +++ b/packages/components/menu/CHANGELOG.md @@ -45,7 +45,6 @@ - [#3001](https://github.com/sl-design-system/components/pull/3001) [`a7ac909`](https://github.com/sl-design-system/components/commit/a7ac90987881881bd0cb916c583e68c785b52622) - Improves accessibility of menu-groups with headers - [#3022](https://github.com/sl-design-system/components/pull/3022) [`a4a0c23`](https://github.com/sl-design-system/components/commit/a4a0c23a5341a2026c23e6e7fdf05cfdd44dc16c) - - `MenuButton` and `Button` now correctly block click/keyboard activation when `aria-disabled` is set, but remain focusable for improved accessibility and tooltip support. - - Standalone `MenuButton` continues to use native `disabled` to remain non-focusable, while support for `aria-disabled` focusability has been improved. - [#3034](https://github.com/sl-design-system/components/pull/3034) [`fd4a0d7`](https://github.com/sl-design-system/components/commit/fd4a0d79b4c0d9a1438b437bc7a1122f03d08c11) - Changed styling so the hover and active state have an indicator. This makes it more accessible because we don't rely on only a subtle change in the background color. @@ -154,7 +153,6 @@ ### Patch Changes - [#1777](https://github.com/sl-design-system/components/pull/1777) [`67f5b81`](https://github.com/sl-design-system/components/commit/67f5b810558d124289f26e3cc3fb2c59da97bb5f) - Fixed several accessibility issues; - - Improved VoiceOver in support Chrome - Fixed keyboard navigation in submenu - Applied new tokens to menu and menu item 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.stories.ts b/packages/components/menu/src/menu-button.stories.ts index 41b448fe54..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,16 +134,19 @@ export default { height: calc(100dvh - 2rem); place-items: center; } + sl-menu-button::part(tooltip) { + max-inline-size: 200px; + } ${typeof body === 'string' ? html`
${body}
` : body()} ${menuItems?.()} @@ -152,7 +158,6 @@ export default { export const Basic: Story = { args: { body: () => html``, - label: 'Settings', menuItems: () => html` @@ -162,7 +167,8 @@ export const Basic: Story = { Delete... - ` + `, + tooltip: 'Settings' } }; @@ -187,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' } }; @@ -195,7 +202,7 @@ export const Text: Story = { args: { ...Basic.args, body: () => html`Settings`, - label: undefined + tooltip: undefined } }; @@ -289,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` + + Open dialog + + +

Dialog title

+ Close +
+ ` +}; + +export const DisabledButtons = { + render: () => html` + + + Disabled attribute + + + ARIA disabled + + + ` +}; + +export const MenuButton = { + render: () => html` + + + + + Rename... + + + + Delete... + + + ` +}; + +export const Nested = { + render: () => html` + +

Card title

+ + Hover me + Tooltip text + +
+ Card tooltip + ` +}; 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; + let el: HTMLElement, anchor: HTMLElement, 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 +
+ + Tooltip text
`); - 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; + anchor = el.querySelector('#btn')!; + tooltip = el.querySelector('sl-tooltip')!; - 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 have aria-hidden="true"', () => { + expect(tooltip).to.have.attribute('aria-hidden', 'true'); }); - 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 have popover="manual"', () => { + expect(tooltip).to.have.attribute('popover', 'manual'); }); - 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 have role="tooltip"', () => { + expect(tooltip).to.have.attribute('role', 'tooltip'); }); - 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 have an auto-generated id', () => { + expect(tooltip.id).to.match(/^sl-tooltip-\d+$/); }); - 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 not be open by default', () => { + expect(tooltip.matches(':popover-open')).to.be.false; }); - it('should stay open on focusout when the anchor remains hovered', async () => { - await tooltip.updateComplete; + it('should have a hover-bridge part element', () => { + expect(tooltip.renderRoot.querySelector('[part="hover-bridge"]')).to.exist; + }); - 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(); + it('should default trigger to "focus hover"', () => { + expect(tooltip.trigger).to.equal('focus hover'); + }); - expect(tooltip.matches(':popover-open')).to.be.true; + 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(); - 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(); - } + expect(text).to.equal('Tooltip text'); }); + }); - it('should restore the tooltip to the focused shared anchor after unhovering another shared anchor', async () => { + describe('anchor binding', () => { + beforeEach(async () => { el = await fixture(html` -
- - - Shared tooltip +
+ + Tip
`); - - 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)); + anchor = el.querySelector('#anchor')!; + tooltip = el.querySelector('sl-tooltip')!; 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 bind to the anchor element referenced by the "for" attribute', () => { + expect(tooltip.anchor).to.equal(anchor); }); - 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 set ariaDescribedByElements on the anchor', () => { + expect(anchor.ariaDescribedByElements).to.include(tooltip); }); - it('should be positioned at the top by default', () => { - expect(tooltip.position).to.equal('top'); + it('should set anchor-name on the anchor', () => { + expect(anchor.style.anchorName).to.equal(`--${tooltip.id}`); }); - 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 set position-anchor on the tooltip', () => { + expect(tooltip.style.positionAnchor).to.equal(`--${tooltip.id}`); }); - it('should not have a maxWidth by default', async () => { - tooltip.setAttribute('max-width', '150'); + it('should clear the anchor when "for" is removed', async () => { + tooltip.removeAttribute('for'); await tooltip.updateComplete; - await showTooltip(); - - expect(getComputedStyle(tooltip).maxInlineSize).to.equal('150px'); + expect(tooltip.anchor).to.be.undefined; }); - it('should show the tooltip on sl-close dispatched outside the tooltip root while anchor keeps focus', async () => { - button?.focus(); + it('should clear the ARIA relation when "for" is removed', async () => { + tooltip.removeAttribute('for'); 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; + expect(anchor.ariaDescribedByElements ?? []).not.to.include(tooltip); }); - 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)); + it('should clear anchor-name and position-anchor when "for" is removed', async () => { + tooltip.removeAttribute('for'); 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; + expect(anchor.style.anchorName).to.equal(''); + expect(tooltip.style.positionAnchor).to.equal(''); }); - }); - describe('linked via aria-labelledby', () => { - beforeEach(async () => { - el = await fixture(html` -
- Button element - Message with lots of long text, that exceeds 150px easily + it('should not overwrite an existing anchor-name on the anchor', async () => { + const newEl = await fixture(html` +
+ + Tip
`); - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; + 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 show the tooltip on pointerover', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); + it('should remove the ARIA relation when disconnected', async () => { + tooltip.remove(); await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - expect(tooltip.matches(':popover-open')).to.be.true; + expect(anchor.ariaDescribedByElements ?? []).not.to.include(tooltip); }); }); - describe('multiple ids', () => { + describe('type', () => { beforeEach(async () => { el = await fixture(html` -
- Other element - Button - Tooltip message +
+ + Tip
`); - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - }); + anchor = el.querySelector('#t-anchor')!; + tooltip = el.querySelector('sl-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; + it('should use ariaLabelledByElements by default', () => { + expect(anchor.ariaLabelledByElements).to.include(tooltip); + expect(anchor.ariaDescribedByElements ?? []).not.to.include(tooltip); }); - }); - describe('multiple ids with aria-labelledby', () => { - beforeEach(async () => { - el = await fixture(html` -
- Other label - Button with multiple label ids - - Tooltip label -
- `); + it('should use ariaLabelledByElements when type is "label"', async () => { + tooltip.type = 'label'; + await tooltip.updateComplete; - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; + expect(anchor.ariaLabelledByElements).to.include(tooltip); + expect(anchor.ariaDescribedByElements ?? []).not.to.include(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 })); + it('should use ariaDescribedByElements when type is "description"', async () => { + tooltip.type = 'description'; await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - expect(tooltip.matches(':popover-open')).to.be.true; + expect(anchor.ariaDescribedByElements).to.include(tooltip); + expect(anchor.ariaLabelledByElements ?? []).not.to.include(tooltip); }); }); - describe('ElementInternals ariaDescribedByElements', () => { + describe('disabled', () => { beforeEach(async () => { el = await fixture(html` -
- Button - Tooltip via ElementInternals +
+ + Tip
`); - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; + anchor = el.querySelector('#d-anchor')!; + tooltip = el.querySelector('sl-tooltip')!; - // Manually set ariaDescribedByElements - if (button.internals) { - button.internals.ariaDescribedByElements = [tooltip]; - } + await tooltip.updateComplete; }); - it('should show tooltip when referenced via ElementInternals ariaDescribedByElements', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - 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.true; + expect(tooltip.matches(':popover-open')).to.be.false; }); - it('should hide tooltip on pointerout when referenced via ElementInternals', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); + it('should close when disabled while open', async () => { + tooltip.disabled = false; await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); + tooltip.showPopover(); expect(tooltip.matches(':popover-open')).to.be.true; - button?.dispatchEvent(new Event('pointerout', { bubbles: true })); + tooltip.disabled = 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(); + it('should add/remove ariaDescribedByElements reference when enabled/disabled', async () => { + expect(anchor.ariaDescribedByElements ?? []).not.to.include(tooltip); + + tooltip.disabled = false; await tooltip.updateComplete; - await new Promise(resolve => requestAnimationFrame(resolve)); + + expect(anchor.ariaDescribedByElements ?? []).to.include(tooltip); + + tooltip.disabled = true; await tooltip.updateComplete; - expect(tooltip.matches(':popover-open')).to.be.true; + expect(anchor.ariaDescribedByElements ?? []).not.to.include(tooltip); }); }); - describe('ElementInternals ariaLabelledByElements', () => { + describe('open property', () => { beforeEach(async () => { el = await fixture(html` -
- Button - Tooltip label via ElementInternals +
+ + Tip
`); - - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; - - // Manually set ariaLabelledByElements - if (button.internals) { - button.internals.ariaLabelledByElements = [tooltip]; - } + anchor = el.querySelector('#o-anchor')!; + tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; }); - it('should show tooltip when referenced via ElementInternals ariaLabelledByElements', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); + it('should show the tooltip when open is set to true', async () => { + tooltip.open = 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 })); + it('should hide the tooltip when open is set to false after being shown', async () => { + tooltip.open = true; await tooltip.updateComplete; - await waitFor(Tooltip.hoverShowDelay + 10); - - expect(tooltip.matches(':popover-open')).to.be.true; - button?.dispatchEvent(new Event('pointerout', { bubbles: true })); + tooltip.open = false; await tooltip.updateComplete; - await waitFor(Tooltip.hoverHideDelay + 10); expect(tooltip.matches(':popover-open')).to.be.false; }); }); - describe('ElementInternals with multiple elements', () => { - let otherElement: HTMLElement; - + describe('hover trigger', () => { beforeEach(async () => { el = await fixture(html` -
- Other element - Button - Tooltip message +
+ + Tip
`); - - 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]; - } + anchor = el.querySelector('#h-anchor')!; + tooltip = el.querySelector('sl-tooltip')!; + await tooltip.updateComplete; }); - it('should show tooltip when it is one of multiple elements in ariaDescribedByElements', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - 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; }); - }); - - 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); + 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.true; + expect(tooltip.matches(':popover-open')).to.be.false; }); - it('should hide tooltip on pointerout when referenced via Element ariaDescribedByElements', async () => { - button?.dispatchEvent(new Event('pointerover', { bubbles: true })); - await tooltip.updateComplete; + it('should hide the tooltip when mousing out of the anchor', async () => { + anchor.dispatchEvent(new Event('mouseover', { bubbles: true })); await waitFor(Tooltip.hoverShowDelay + 10); - expect(tooltip.matches(':popover-open')).to.be.true; - - button?.dispatchEvent(new Event('pointerout', { bubbles: true })); + 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 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 })); + it('should not show when trigger does not include hover', async () => { + tooltip.trigger = 'focus'; 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; + anchor.dispatchEvent(new Event('mouseover', { bubbles: true })); 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; - + describe('focus trigger', () => { beforeEach(async () => { el = await fixture(html` -
- - Settings - Rename... - Delete... - - Open settings menu +
+ + Tip
`); - - 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 + anchor = el.querySelector('#f-anchor')!; + tooltip = el.querySelector('sl-tooltip')!; 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 })); + it('should show the tooltip on focus (when :focus-visible)', async () => { + await userEvent.tab(); 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(); + it('should hide the tooltip on blur', async () => { + await userEvent.tab(); 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 })); + anchor.blur(); 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(); + it('should not show when trigger does not include focus', async () => { + tooltip.trigger = 'hover'; await tooltip.updateComplete; - expect(menu.matches(':popover-open')).to.be.true; - - const menuItem = el.querySelector('sl-menu-item') as HTMLElement; - - menuItem.focus(); + await userEvent.tab(); 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; - + describe('click trigger', () => { beforeEach(async () => { el = await fixture(html` -
- - Anchor - - - Tooltip via assigned slot +
+ + Tip
`); - - 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; - + anchor = el.querySelector('#c-anchor')!; + tooltip = el.querySelector('sl-tooltip')!; 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)); + it('should show the tooltip on click', async () => { + anchor.dispatchEvent(new Event('click', { bubbles: true })); 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; + it('should hide the tooltip on a second click', async () => { + anchor.dispatchEvent(new Event('click', { bubbles: true })); 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 })); + anchor.dispatchEvent(new Event('click', { bubbles: 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)); + expect(tooltip.matches(':popover-open')).to.be.false; }); - 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 })); + 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; - 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; - + describe('manual trigger', () => { beforeEach(async () => { el = await fixture(html` -
- - - - External description - Tooltip via assigned slot +
+ + Tip
`); - - 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; - + anchor = el.querySelector('#m-anchor')!; + tooltip = el.querySelector('sl-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; + 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.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)); + expect(tooltip.matches(':popover-open')).to.be.false; }); - it('should sync and clear the tooltip slot when reparenting is skipped', async () => { - slottedAnchor.dispatchEvent(new Event('pointerover', { bubbles: true, composed: true })); + it('should not show on focus', async () => { + await userEvent.tab(); 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)); + expect(tooltip.matches(':popover-open')).to.be.false; }); - 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); + it('should show when showPopover is called programmatically', () => { + tooltip.showPopover(); 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; - + describe('keyboard', () => { beforeEach(async () => { el = await fixture(html` -
- Button +
+ + Tip
`); - - 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 })); - + anchor = el.querySelector('#k-anchor')!; + tooltip = el.querySelector('sl-tooltip')!; 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; + tooltip.showPopover(); }); - 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); + it('should hide the tooltip when pressing Escape', async () => { + await userEvent.keyboard('{Escape}'); 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)); + it('should stop propagation of the Escape keydown event', async () => { + const onKeydown = spy(); - button.dispatchEvent(new Event('pointerover', { bubbles: true })); - button.dispatchEvent(new Event('pointerover', { bubbles: true })); // second should be ignored + anchor.addEventListener('keydown', onKeydown); + anchor.focus(); - await tooltip.updateComplete; + await userEvent.keyboard('{Escape}'); - expect(el.querySelectorAll('sl-tooltip')).to.have.lengthOf(1); - expect(tooltip).to.exist; + expect(onKeydown).to.not.have.been.called; }); }); - describe('delay semantics', () => { - beforeEach(() => { + describe('delay (fake timers)', () => { + beforeEach(async () => { vi.useFakeTimers(); - }); - beforeEach(async () => { el = await fixture(html` -
- Button - Tooltip +
+ + Tip
`); - button = el.querySelector('sl-button') as Button; - tooltip = el.querySelector('sl-tooltip') as Tooltip; + 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 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; + 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)); - await vi.advanceTimersByTimeAsync(Tooltip.hoverShowDelay - preShowWait + 10); - expect(tooltip.matches(':popover-open')).to.be.true; + expect(tooltip.matches(':popover-open')).to.be.false; }); - it('should stay open before the fixed hover hide delay elapses', async () => { - button.dispatchEvent(new Event('pointerover', { bubbles: true })); + 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; - button.dispatchEvent(new Event('pointerout', { bubbles: true })); - await vi.advanceTimersByTimeAsync(Math.max(0, Tooltip.hoverHideDelay - 50)); 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); - 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 f5ee57a2a3..0c6b47966e 100644 --- a/packages/components/tooltip/src/tooltip.stories.ts +++ b/packages/components/tooltip/src/tooltip.stories.ts @@ -1,324 +1,161 @@ 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 { LitElement, TemplateResult, css, 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: { + disabled: { + control: 'boolean' + }, + maxWidth: { + control: 'number' + }, + open: { + control: 'boolean' + }, + position: { control: 'inline-radio', - options: ['start', 'center', 'end'] + options: ['top', 'right', 'bottom', 'left'] + }, + showHoverBridge: { + control: 'boolean' + }, + text: { + control: 'text' }, - example: { + tooltip: { table: { disable: true } }, - justifySelf: { - control: 'inline-radio', - options: ['start', 'center', 'end'] + trigger: { + control: 'inline-check', + options: ['click', 'hover', 'focus', 'manual'] }, - position: { - control: 'select', - options: [ - 'top', - 'top-start', - 'top-end', - 'right', - 'right-start', - 'right-end', - 'bottom', - 'bottom-start', - 'bottom-end', - 'left', - 'left-start', - 'left-end' - ] + type: { + control: 'inline-radio', + options: ['description', 'label'] } }, - render: props => { - const { alignSelf, example, justifySelf, message, position, maxWidth } = props; - - return html` - - ${example - ? example?.(props) - : html` - - Button - - ${message} - `} - `; - } + args: { + text: 'Tooltip text', + type: 'description' + }, + render: ({ + disabled, + maxWidth, + open, + position, + showHoverBridge, + text, + tooltip, + trigger, + type + }) => 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 - - ` + text: 'Click again to dismiss', + trigger: ['click'] } }; -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. -

- -
- - - -
- `; +export const Disabled = { + args: { + disabled: true } }; -export const Disabled: Story = { +export const HoverBridge = { args: { - example: ({ alignSelf, justifySelf, message }) => html` -
- Disabled button - Disabled (ARIA only) button -
- ` + 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 Shared: Story = { +export const All = { args: { - example: ({ alignSelf, justifySelf, message }) => html` - - We - all - share - the - same - tooltip - - ${message} + tooltip: () => html` + Top + Right + Bottom + Left ` - }, - parameters: { - // Notifies Chromatic to pause the animations at the first frame for this specific story. - chromatic: { pauseAnimationAtEnd: false, prefersReducedMotion: 'reduce' } } }; -export const NestedChildren: Story = { - 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} - ` - } -}; - -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 - `; + return html``; } }; diff --git a/packages/components/tooltip/src/tooltip.ts b/packages/components/tooltip/src/tooltip.ts index 7e093cda5f..ce47b14182 100644 --- a/packages/components/tooltip/src/tooltip.ts +++ b/packages/components/tooltip/src/tooltip.ts @@ -1,1387 +1,406 @@ -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; /** - * 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 - The content of the tooltip. * - * @slot default - The slot for the tooltip content. + * @csspart hover-bridge - 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; + /** + * The delay in milliseconds before showing the tooltip when the mouse hovers over the anchor + * element. + */ + static hoverShowDelay: number = 150; - /** @internal The default offset of the tooltip to its anchor. */ - static offset = 12; + /** 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; - /** @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); + /** Controller for managing event listeners. */ + #eventController = new AbortController(); - callback(tooltip); + /** Timeout ID for the hover delay. */ + #hoverTimeout?: ReturnType; - tooltip.anchorElement = target as HTMLElement; + /** @internal The element this tooltip is anchored to. */ + @state() anchor?: HTMLElement | null; - // 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(); - - /** Whether the current open state was triggered by focus-based interaction. */ - #openedByFocus = false; - - /** The root where the tooltip was originally connected before any runtime reparenting. */ - #originalRoot?: Node; - - /** 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); - } - 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.id) { + this.id = `sl-tooltip-${nextUniqueId++}`; } - 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; + if (this.#eventController.signal.aborted) { + this.#eventController = new AbortController(); } - if (changes.has('offset')) { - this.#anchor.offset = this.offset ?? Tooltip.offset; - } - - if (changes.has('position')) { - this.#anchor.position = this.position; - } - } - - #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; - } + const { signal } = this.#eventController; - if (event.type === 'pointerout' && !isPopoverOpen(this)) { - return; - } + this.addEventListener('beforetoggle', this.#onBeforeToggle, { signal }); + this.addEventListener('mouseout', this.#onMouseOut, { signal }); + this.addEventListener('toggle', this.#onToggle, { signal }); - // 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; + // 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(); } + } - 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; - } - - // 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; - } - - 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; - } + override disconnectedCallback() { + clearTimeout(this.#hoverTimeout); - // 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') - ); + this.#eventController.abort(); - 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, { capture: true }); - #onKeydown(event: KeyboardEvent): void { - if (isPopoverOpen(this) && event.key === 'Escape') { - this.#hideTooltip(); - return; + if (this.anchor) { + this.#removeAriaRelation(this.anchor, this.type); } - // `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; - - if (!anchorElement) { - return; - } - - const normalizedAnchorElement = this.#normalizeAnchorElement(anchorElement); + override willUpdate(changes: PropertyValues): void { + super.willUpdate(changes); - // 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('anchor') || changes.has('for')) { + this.#updateAnchor(); } - // 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; - } - - if ( - !this.#matchesAnchor(normalizedAnchorElement) && - !this.#stableAnchors.has(normalizedAnchorElement) - ) { - 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); - } - }); + if (changes.has('type') && this.anchor) { + this.#removeAriaRelation(this.anchor, changes.get('type')); + if (!this.disabled) { + this.#addAriaRelation(this.anchor, this.type); } } - }; - - /** - * 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`; - } - - 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; + if (changes.has('disabled')) { + if (this.disabled) { + this.hidePopover(); - 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; + if (this.anchor) { + this.#removeAriaRelation(this.anchor, this.type); + } + } else if (this.anchor) { + this.#addAriaRelation(this.anchor, this.type); } - - 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; - } + override render(): TemplateResult { + return html` + +
+ `; + } - if (current.parentElement) { - current = current.parentElement; - continue; + #onBeforeToggle = (event: ToggleEvent): void => { + if (event.newState === 'open') { + if (this.disabled) { + event.preventDefault(); + return; } - const shadowRoot = this.#getShadowRoot(current.getRootNode()); - current = shadowRoot?.host ?? null; + document.addEventListener('keydown', this.#onKeydown, { capture: true }); + } else { + document.removeEventListener('keydown', this.#onKeydown, { capture: true }); } - - 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'); + #onBlur = (): void => { + if (this.#hasTrigger('focus')) { + this.hidePopover(); } - - if (event.type === 'focusin' || event.type === 'sl-close') { - return element.matches(':focus-within'); - } - - 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; - } - } + #onClick = (): void => { + if (this.#hasTrigger('click')) { + if (this.matches(':popover-open')) { + this.hidePopover(); + } else { + this.showPopover(); } + } else { + this.hidePopover(); } - - 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; + #onFocus = (): void => { + if (this.#hasTrigger('focus')) { + this.showPopover(); } - - return this.#findAnchorFromElement(activeElement); }; - #findKnownFocusedAnchor = (): HTMLElement | undefined => - Array.from(this.#knownAnchors).find( - anchor => anchor.isConnected && anchor.matches(':focus-within') - ); + #onKeydown = (event: KeyboardEvent): void => { + if (event.key === 'Escape') { + event.preventDefault(); + event.stopPropagation(); - /** - * 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.hidePopover(); } - - this.#getKnownAnchors(); }; - /** - * 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)); - } - } - } - }; - - /** - * 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(); + #onMouseOver = (): void => { + if (this.#hasTrigger('hover')) { + clearTimeout(this.#hoverTimeout); - 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); - } + this.#hoverTimeout = setTimeout(() => { + this.showPopover(); + }, Tooltip.hoverShowDelay); } }; - #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; - } - - const shadowRoot = this.#getShadowRoot(root); - if (!shadowRoot) { - break; + #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; } - 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(); + clearTimeout(this.#hoverTimeout); - 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); + if (!(anchorHovered || tooltipHovered)) { + this.#hoverTimeout = setTimeout(() => { + this.hidePopover(); + }, Tooltip.hoverHideDelay); } } - - return knownAnchors; }; - #getShadowRoot = (node: Node | null | undefined): ShadowRoot | undefined => { - if (node instanceof ShadowRoot) { - return node; + #onToggle = (event: ToggleEvent): void => { + if (event.newState === 'open' && this.anchor) { + this.#positionHoverBridge(this.anchor); } - - // 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; - } - - return undefined; }; - #moveToAnchorRoot = (anchorElement: HTMLElement, anchorRoot?: ShadowRoot): boolean => { - const root = anchorRoot ?? this.#findAssignedSlotRoot(anchorElement, []); + #hasTrigger(trigger: string): boolean { + return this.trigger.split(' ').includes(trigger); + } - if (root && this.getRootNode() !== root) { - root.append(this); - return true; - } + #getAriaPropertyFromType( + type?: 'description' | 'label' + ): 'ariaDescribedByElements' | 'ariaLabelledByElements' { + return type === 'description' ? 'ariaDescribedByElements' : 'ariaLabelledByElements'; + } - return false; - }; + #addAriaRelation(element: Element, type?: 'description' | 'label'): void { + const ariaProperty = this.#getAriaPropertyFromType(type); - #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; + const refs = element[ariaProperty] ?? []; + if (!refs.includes(this)) { + element[ariaProperty] = [...refs, this]; } + } - return !hasExplicitRelation; - }; + #removeAriaRelation(element: Element, type?: 'description' | 'label'): void { + const ariaProperty = this.#getAriaPropertyFromType(type); + + const refs = element[ariaProperty] ?? []; + element[ariaProperty] = refs.filter((ref: Element) => ref !== this); + } - #syncSlotWithAnchor = (anchorElement: HTMLElement, isInAnchorRoot: boolean): void => { - if (isInAnchorRoot) { - this.removeAttribute('slot'); + #positionHoverBridge(anchor: Element): void { + const bridge = this.renderRoot.querySelector('[part="hover-bridge"]'); + if (!bridge) { 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; + // Tooltip and anchor overlap; no bridge needed. + bridge.style.display = 'none'; + return; } - return ( - this.#hasAnyExplicitRelation(proxyTarget) || - this.#hasAnyReflectedRelation(anchorElement) || - this.#hasAnyReflectedRelation(proxyTarget) || - this.#hasAnyReflectedRelation(internals) - ); - }; + bridge.style.left = `${left}px`; + bridge.style.top = `${top}px`; + bridge.style.width = `${width}px`; + bridge.style.height = `${height}px`; + bridge.style.clipPath = polygon; + bridge.style.display = ''; + } - /** - * 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]); + #cleanupAnchor(anchor: HTMLElement, type: 'description' | 'label' | undefined): void { + this.#removeAriaRelation(anchor, type); - for (const knownAnchor of this.#getKnownAnchors()) { - if (this.#matchesAnchor(knownAnchor)) { - anchors.add(this.#normalizeAnchorElement(knownAnchor)); - } - } + 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); - for (const root of this.#getAnchorSearchRoots()) { - for (const ariaAnchor of this.#getAriaAnchors(root)) { - if (this.#matchesAnchor(ariaAnchor)) { - anchors.add(this.#normalizeAnchorElement(ariaAnchor)); - } - } + // Only clear the anchorName if it was set by us. + if (anchor.style.anchorName === this.style.positionAnchor) { + anchor.style.anchorName = ''; } - return anchors; - }; + this.style.positionAnchor = ''; + } - /** - * 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)); - } - } + #updateAnchor(): void { + if (!this.for) { + const oldAnchor = this.anchor; + if (oldAnchor) { + this.#cleanupAnchor(oldAnchor, this.type); } - } - - 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); - } - - return proxyTarget instanceof Element && proxyTarget !== anchorElement - ? [proxyTarget] - : [anchorElement]; - }; - #setReflectedRelation = ( - target: - | { - ariaDescribedByElements?: readonly Element[] | null; - ariaLabelledByElements?: readonly Element[] | null; - } - | null - | undefined, - relation: 'description' | 'label' - ): void => { - if (!target) { + 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, this.type); - 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.#cleanupAnchor(oldAnchor, this.type); } - // 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; - } - } - - // 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/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/packages/themes/bingel-dc/CHANGELOG.md b/packages/themes/bingel-dc/CHANGELOG.md index 8e00e925d3..b2ad320c2c 100644 --- a/packages/themes/bingel-dc/CHANGELOG.md +++ b/packages/themes/bingel-dc/CHANGELOG.md @@ -23,11 +23,9 @@ ### Minor Changes - [#3020](https://github.com/sl-design-system/components/pull/3020) [`738e4a7`](https://github.com/sl-design-system/components/commit/738e4a77005043de2f9977fab9fb04d4fce6369d) - Added - - new elevation.surface.raised.primary token Fixed - - form input interactive backgrounds (color.background.input.plain.interactive.plain) from plain to bold variants across all themes (e.g., accent.grey.interactive.bold) for better state contrast. - [#2970](https://github.com/sl-design-system/components/pull/2970) [`e92ebb1`](https://github.com/sl-design-system/components/commit/e92ebb16c596919aaa301be2604ab5f3539738a9) - Caret icons have been updated to implement the new alignment strategy used in Font Awesome 7 @@ -147,11 +145,9 @@ - [#1710](https://github.com/sl-design-system/components/pull/1710) [`40cc538`](https://github.com/sl-design-system/components/commit/40cc538648e6ed5ac453fbe708bae8761caaab5e) - Overhaul of how (custom) icons are maintained in figma and exported to be used in the packages. The following icons have changed: - - `circle` has been renamed to `circle-solid` The following icons have been added: - - `badge-available` - `badge-away` - `badge-donotdisturb` @@ -160,7 +156,6 @@ - `info` The following items have been removed (mainly in cleaning up, they were never meant to be there) - - `svg-sort` - `svg-sort-down` - `svg-sort-up` @@ -204,7 +199,6 @@ ### Patch Changes - [#1454](https://github.com/sl-design-system/components/pull/1454) [`af62ce4`](https://github.com/sl-design-system/components/commit/af62ce4d0e65b1363b9cede48642bc22d1fc9365) - - Improve toggle button and group tokens - - Add a `check-solid` icon for use in the `toggle-button` component - [#1414](https://github.com/sl-design-system/components/pull/1414) [`ff1618c`](https://github.com/sl-design-system/components/commit/ff1618cdfa4d0060465d993f656345ba1044f88c) - Update icons to the latest fontawesome release (6.6.0) @@ -219,7 +213,6 @@ ### Patch Changes - [#1389](https://github.com/sl-design-system/components/pull/1389) [`f03971b`](https://github.com/sl-design-system/components/commit/f03971b7b338a4248df292060b91b6b903b6c8ed) - Minor style fixes: - - Fix the title and subtitle text being cutoff for certain characters due not enough line-height - Use a different color for the subtitle text @@ -265,7 +258,6 @@ - [#1251](https://github.com/sl-design-system/components/pull/1251) [`a3da76c`](https://github.com/sl-design-system/components/commit/a3da76c7df521c2241b565dc22025715f1231e9c) - New search icon - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - - Enhanced the color contrast of buttons when used on slightly darker backgrounds across all themes. - - Enhanced the color contrast of inline messages to match our buttons. - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - Fix missing triangle-exclamation-solid icon @@ -282,7 +274,6 @@ - [#1242](https://github.com/sl-design-system/components/pull/1242) [`ab122ec`](https://github.com/sl-design-system/components/commit/ab122ec672a515ae2ca7dce88c7344c1b209d538) - Fix missing `calc()` functions in theme parts. - [#1225](https://github.com/sl-design-system/components/pull/1225) [`ad297ab`](https://github.com/sl-design-system/components/commit/ad297ab817ab998253b9c2a90033c72dcc686893) - Updated/added tokens: - - Button bar available in all themes - Fixed accordion border - Button fixes diff --git a/packages/themes/bingel-int/CHANGELOG.md b/packages/themes/bingel-int/CHANGELOG.md index e0dedde146..0a9adbbc36 100644 --- a/packages/themes/bingel-int/CHANGELOG.md +++ b/packages/themes/bingel-int/CHANGELOG.md @@ -23,11 +23,9 @@ ### Minor Changes - [#3020](https://github.com/sl-design-system/components/pull/3020) [`738e4a7`](https://github.com/sl-design-system/components/commit/738e4a77005043de2f9977fab9fb04d4fce6369d) - Added - - new elevation.surface.raised.primary token Fixed - - form input interactive backgrounds (color.background.input.plain.interactive.plain) from plain to bold variants across all themes (e.g., accent.grey.interactive.bold) for better state contrast. - [#2970](https://github.com/sl-design-system/components/pull/2970) [`e92ebb1`](https://github.com/sl-design-system/components/commit/e92ebb16c596919aaa301be2604ab5f3539738a9) - Caret icons have been updated to implement the new alignment strategy used in Font Awesome 7 @@ -147,11 +145,9 @@ - [#1710](https://github.com/sl-design-system/components/pull/1710) [`40cc538`](https://github.com/sl-design-system/components/commit/40cc538648e6ed5ac453fbe708bae8761caaab5e) - Overhaul of how (custom) icons are maintained in figma and exported to be used in the packages. The following icons have changed: - - `circle` has been renamed to `circle-solid` The following icons have been added: - - `badge-available` - `badge-away` - `badge-donotdisturb` @@ -160,7 +156,6 @@ - `info` The following items have been removed (mainly in cleaning up, they were never meant to be there) - - `svg-sort` - `svg-sort-down` - `svg-sort-up` @@ -202,7 +197,6 @@ ### Patch Changes - [#1454](https://github.com/sl-design-system/components/pull/1454) [`af62ce4`](https://github.com/sl-design-system/components/commit/af62ce4d0e65b1363b9cede48642bc22d1fc9365) - - Improve toggle button and group tokens - - Add a `check-solid` icon for use in the `toggle-button` component - [#1414](https://github.com/sl-design-system/components/pull/1414) [`ff1618c`](https://github.com/sl-design-system/components/commit/ff1618cdfa4d0060465d993f656345ba1044f88c) - Update icons to the latest fontawesome release (6.6.0) @@ -217,7 +211,6 @@ ### Patch Changes - [#1389](https://github.com/sl-design-system/components/pull/1389) [`f03971b`](https://github.com/sl-design-system/components/commit/f03971b7b338a4248df292060b91b6b903b6c8ed) - Minor style fixes: - - Fix the title and subtitle text being cutoff for certain characters due not enough line-height - Use a different color for the subtitle text @@ -263,7 +256,6 @@ - [#1251](https://github.com/sl-design-system/components/pull/1251) [`a3da76c`](https://github.com/sl-design-system/components/commit/a3da76c7df521c2241b565dc22025715f1231e9c) - New search icon - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - - Enhanced the color contrast of buttons when used on slightly darker backgrounds across all themes. - - Enhanced the color contrast of inline messages to match our buttons. - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - Fix missing triangle-exclamation-solid icon @@ -280,7 +272,6 @@ - [#1242](https://github.com/sl-design-system/components/pull/1242) [`ab122ec`](https://github.com/sl-design-system/components/commit/ab122ec672a515ae2ca7dce88c7344c1b209d538) - Fix missing `calc()` functions in theme parts. - [#1225](https://github.com/sl-design-system/components/pull/1225) [`ad297ab`](https://github.com/sl-design-system/components/commit/ad297ab817ab998253b9c2a90033c72dcc686893) - Updated/added tokens: - - Button bar available in all themes - Fixed accordion border - Button fixes diff --git a/packages/themes/clickedu/CHANGELOG.md b/packages/themes/clickedu/CHANGELOG.md index 5c2260a3be..b65ed17eb6 100644 --- a/packages/themes/clickedu/CHANGELOG.md +++ b/packages/themes/clickedu/CHANGELOG.md @@ -27,15 +27,12 @@ ### Patch Changes - [#3020](https://github.com/sl-design-system/components/pull/3020) [`738e4a7`](https://github.com/sl-design-system/components/commit/738e4a77005043de2f9977fab9fb04d4fce6369d) - Added - - new elevation.surface.raised.primary token Fixed - - form input interactive backgrounds (color.background.input.plain.interactive.plain) from plain to bold variants across all themes (e.g., accent.grey.interactive.bold) for better state contrast. - [#3020](https://github.com/sl-design-system/components/pull/3020) [`738e4a7`](https://github.com/sl-design-system/components/commit/738e4a77005043de2f9977fab9fb04d4fce6369d) - - adjusted color.border.accent.grey.bold value from {grey.200} to {grey.300} for increased border visibility. - - updated color.blanket.plain transparency from {opacity.moderate} to {opacity.subtle} to align with updated design requirements for dialog overlays. - [#2982](https://github.com/sl-design-system/components/pull/2982) [`5784b96`](https://github.com/sl-design-system/components/commit/5784b9682e183391f842b10f1d194ceb137606f0) - Updated global.css so we have the opportunity to overwrite the link color from a component. @@ -153,11 +150,9 @@ - [#1710](https://github.com/sl-design-system/components/pull/1710) [`40cc538`](https://github.com/sl-design-system/components/commit/40cc538648e6ed5ac453fbe708bae8761caaab5e) - Overhaul of how (custom) icons are maintained in figma and exported to be used in the packages. The following icons have changed: - - `circle` has been renamed to `circle-solid` The following icons have been added: - - `badge-available` - `badge-away` - `badge-donotdisturb` @@ -166,7 +161,6 @@ - `info` The following items have been removed (mainly in cleaning up, they were never meant to be there) - - `svg-sort` - `svg-sort-down` - `svg-sort-up` @@ -216,7 +210,6 @@ - [#1473](https://github.com/sl-design-system/components/pull/1473) [`04b9ce2`](https://github.com/sl-design-system/components/commit/04b9ce28077255bfc516bce7b62bb7e4642060b3) - Fix/1470 space grotesk to open sans clickedu ds - [#1454](https://github.com/sl-design-system/components/pull/1454) [`af62ce4`](https://github.com/sl-design-system/components/commit/af62ce4d0e65b1363b9cede48642bc22d1fc9365) - - Improve toggle button and group tokens - - Add a `check-solid` icon for use in the `toggle-button` component - [#1414](https://github.com/sl-design-system/components/pull/1414) [`ff1618c`](https://github.com/sl-design-system/components/commit/ff1618cdfa4d0060465d993f656345ba1044f88c) - Update icons to the latest fontawesome release (6.6.0) @@ -231,7 +224,6 @@ ### Patch Changes - [#1389](https://github.com/sl-design-system/components/pull/1389) [`f03971b`](https://github.com/sl-design-system/components/commit/f03971b7b338a4248df292060b91b6b903b6c8ed) - Minor style fixes: - - Fix the title and subtitle text being cutoff for certain characters due not enough line-height - Use a different color for the subtitle text @@ -271,7 +263,6 @@ - [#1251](https://github.com/sl-design-system/components/pull/1251) [`a3da76c`](https://github.com/sl-design-system/components/commit/a3da76c7df521c2241b565dc22025715f1231e9c) - New search icon - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - - Enhanced the color contrast of buttons when used on slightly darker backgrounds across all themes. - - Enhanced the color contrast of inline messages to match our buttons. - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - Fix missing triangle-exclamation-solid icon diff --git a/packages/themes/editorial-suite/CHANGELOG.md b/packages/themes/editorial-suite/CHANGELOG.md index 2cd57c8e10..b02f6027cc 100644 --- a/packages/themes/editorial-suite/CHANGELOG.md +++ b/packages/themes/editorial-suite/CHANGELOG.md @@ -23,11 +23,9 @@ ### Minor Changes - [#3020](https://github.com/sl-design-system/components/pull/3020) [`738e4a7`](https://github.com/sl-design-system/components/commit/738e4a77005043de2f9977fab9fb04d4fce6369d) - Added - - new elevation.surface.raised.primary token Fixed - - form input interactive backgrounds (color.background.input.plain.interactive.plain) from plain to bold variants across all themes (e.g., accent.grey.interactive.bold) for better state contrast. - [#2970](https://github.com/sl-design-system/components/pull/2970) [`e92ebb1`](https://github.com/sl-design-system/components/commit/e92ebb16c596919aaa301be2604ab5f3539738a9) - Caret icons have been updated to implement the new alignment strategy used in Font Awesome 7 @@ -149,11 +147,9 @@ - [#1710](https://github.com/sl-design-system/components/pull/1710) [`40cc538`](https://github.com/sl-design-system/components/commit/40cc538648e6ed5ac453fbe708bae8761caaab5e) - Overhaul of how (custom) icons are maintained in figma and exported to be used in the packages. The following icons have changed: - - `circle` has been renamed to `circle-solid` The following icons have been added: - - `badge-available` - `badge-away` - `badge-donotdisturb` @@ -162,7 +158,6 @@ - `info` The following items have been removed (mainly in cleaning up, they were never meant to be there) - - `svg-sort` - `svg-sort-down` - `svg-sort-up` @@ -210,7 +205,6 @@ ### Patch Changes - [#1454](https://github.com/sl-design-system/components/pull/1454) [`af62ce4`](https://github.com/sl-design-system/components/commit/af62ce4d0e65b1363b9cede48642bc22d1fc9365) - - Improve toggle button and group tokens - - Add a `check-solid` icon for use in the `toggle-button` component - [#1414](https://github.com/sl-design-system/components/pull/1414) [`ff1618c`](https://github.com/sl-design-system/components/commit/ff1618cdfa4d0060465d993f656345ba1044f88c) - Update icons to the latest fontawesome release (6.6.0) @@ -225,7 +219,6 @@ ### Patch Changes - [#1389](https://github.com/sl-design-system/components/pull/1389) [`f03971b`](https://github.com/sl-design-system/components/commit/f03971b7b338a4248df292060b91b6b903b6c8ed) - Minor style fixes: - - Fix the title and subtitle text being cutoff for certain characters due not enough line-height - Use a different color for the subtitle text @@ -265,7 +258,6 @@ - [#1251](https://github.com/sl-design-system/components/pull/1251) [`a3da76c`](https://github.com/sl-design-system/components/commit/a3da76c7df521c2241b565dc22025715f1231e9c) - New search icon - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - - Enhanced the color contrast of buttons when used on slightly darker backgrounds across all themes. - - Enhanced the color contrast of inline messages to match our buttons. - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - Fix missing triangle-exclamation-solid icon diff --git a/packages/themes/itslearning/CHANGELOG.md b/packages/themes/itslearning/CHANGELOG.md index 7598fb5af0..1fe806c54b 100644 --- a/packages/themes/itslearning/CHANGELOG.md +++ b/packages/themes/itslearning/CHANGELOG.md @@ -23,11 +23,9 @@ ### Minor Changes - [#3020](https://github.com/sl-design-system/components/pull/3020) [`738e4a7`](https://github.com/sl-design-system/components/commit/738e4a77005043de2f9977fab9fb04d4fce6369d) - Added - - new elevation.surface.raised.primary token Fixed - - form input interactive backgrounds (color.background.input.plain.interactive.plain) from plain to bold variants across all themes (e.g., accent.grey.interactive.bold) for better state contrast. - [#2970](https://github.com/sl-design-system/components/pull/2970) [`e92ebb1`](https://github.com/sl-design-system/components/commit/e92ebb16c596919aaa301be2604ab5f3539738a9) - Caret icons have been updated to implement the new alignment strategy used in Font Awesome 7 @@ -147,11 +145,9 @@ - [#1710](https://github.com/sl-design-system/components/pull/1710) [`40cc538`](https://github.com/sl-design-system/components/commit/40cc538648e6ed5ac453fbe708bae8761caaab5e) - Overhaul of how (custom) icons are maintained in figma and exported to be used in the packages. The following icons have changed: - - `circle` has been renamed to `circle-solid` The following icons have been added: - - `badge-available` - `badge-away` - `badge-donotdisturb` @@ -160,7 +156,6 @@ - `info` The following items have been removed (mainly in cleaning up, they were never meant to be there) - - `svg-sort` - `svg-sort-down` - `svg-sort-up` @@ -208,7 +203,6 @@ ### Patch Changes - [#1454](https://github.com/sl-design-system/components/pull/1454) [`af62ce4`](https://github.com/sl-design-system/components/commit/af62ce4d0e65b1363b9cede48642bc22d1fc9365) - - Improve toggle button and group tokens - - Add a `check-solid` icon for use in the `toggle-button` component - [#1414](https://github.com/sl-design-system/components/pull/1414) [`ff1618c`](https://github.com/sl-design-system/components/commit/ff1618cdfa4d0060465d993f656345ba1044f88c) - Update icons to the latest fontawesome release (6.6.0) @@ -223,7 +217,6 @@ ### Patch Changes - [#1389](https://github.com/sl-design-system/components/pull/1389) [`f03971b`](https://github.com/sl-design-system/components/commit/f03971b7b338a4248df292060b91b6b903b6c8ed) - Minor style fixes: - - Fix the title and subtitle text being cutoff for certain characters due not enough line-height - Use a different color for the subtitle text @@ -263,7 +256,6 @@ - [#1251](https://github.com/sl-design-system/components/pull/1251) [`a3da76c`](https://github.com/sl-design-system/components/commit/a3da76c7df521c2241b565dc22025715f1231e9c) - New search icon - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - - Enhanced the color contrast of buttons when used on slightly darker backgrounds across all themes. - - Enhanced the color contrast of inline messages to match our buttons. - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - Fix missing triangle-exclamation-solid icon @@ -280,7 +272,6 @@ - [#1242](https://github.com/sl-design-system/components/pull/1242) [`ab122ec`](https://github.com/sl-design-system/components/commit/ab122ec672a515ae2ca7dce88c7344c1b209d538) - Fix missing `calc()` functions in theme parts. - [#1225](https://github.com/sl-design-system/components/pull/1225) [`ad297ab`](https://github.com/sl-design-system/components/commit/ad297ab817ab998253b9c2a90033c72dcc686893) - Updated/added tokens: - - Button bar available in all themes - Fixed accordion border - Button fixes diff --git a/packages/themes/kampus/CHANGELOG.md b/packages/themes/kampus/CHANGELOG.md index 7502b9eb45..fa59b313ae 100644 --- a/packages/themes/kampus/CHANGELOG.md +++ b/packages/themes/kampus/CHANGELOG.md @@ -23,11 +23,9 @@ ### Minor Changes - [#3020](https://github.com/sl-design-system/components/pull/3020) [`738e4a7`](https://github.com/sl-design-system/components/commit/738e4a77005043de2f9977fab9fb04d4fce6369d) - Added - - new elevation.surface.raised.primary token Fixed - - form input interactive backgrounds (color.background.input.plain.interactive.plain) from plain to bold variants across all themes (e.g., accent.grey.interactive.bold) for better state contrast. - [#2970](https://github.com/sl-design-system/components/pull/2970) [`e92ebb1`](https://github.com/sl-design-system/components/commit/e92ebb16c596919aaa301be2604ab5f3539738a9) - Caret icons have been updated to implement the new alignment strategy used in Font Awesome 7 @@ -147,11 +145,9 @@ - [#1710](https://github.com/sl-design-system/components/pull/1710) [`40cc538`](https://github.com/sl-design-system/components/commit/40cc538648e6ed5ac453fbe708bae8761caaab5e) - Overhaul of how (custom) icons are maintained in figma and exported to be used in the packages. The following icons have changed: - - `circle` has been renamed to `circle-solid` The following icons have been added: - - `badge-available` - `badge-away` - `badge-donotdisturb` @@ -160,7 +156,6 @@ - `info` The following items have been removed (mainly in cleaning up, they were never meant to be there) - - `svg-sort` - `svg-sort-down` - `svg-sort-up` @@ -210,7 +205,6 @@ ### Patch Changes - [#1454](https://github.com/sl-design-system/components/pull/1454) [`af62ce4`](https://github.com/sl-design-system/components/commit/af62ce4d0e65b1363b9cede48642bc22d1fc9365) - - Improve toggle button and group tokens - - Add a `check-solid` icon for use in the `toggle-button` component - [#1414](https://github.com/sl-design-system/components/pull/1414) [`ff1618c`](https://github.com/sl-design-system/components/commit/ff1618cdfa4d0060465d993f656345ba1044f88c) - Update icons to the latest fontawesome release (6.6.0) @@ -225,7 +219,6 @@ ### Patch Changes - [#1389](https://github.com/sl-design-system/components/pull/1389) [`f03971b`](https://github.com/sl-design-system/components/commit/f03971b7b338a4248df292060b91b6b903b6c8ed) - Minor style fixes: - - Fix the title and subtitle text being cutoff for certain characters due not enough line-height - Use a different color for the subtitle text @@ -265,7 +258,6 @@ - [#1251](https://github.com/sl-design-system/components/pull/1251) [`a3da76c`](https://github.com/sl-design-system/components/commit/a3da76c7df521c2241b565dc22025715f1231e9c) - New search icon - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - - Enhanced the color contrast of buttons when used on slightly darker backgrounds across all themes. - - Enhanced the color contrast of inline messages to match our buttons. - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - Fix missing triangle-exclamation-solid icon @@ -282,7 +274,6 @@ - [#1242](https://github.com/sl-design-system/components/pull/1242) [`ab122ec`](https://github.com/sl-design-system/components/commit/ab122ec672a515ae2ca7dce88c7344c1b209d538) - Fix missing `calc()` functions in theme parts. - [#1225](https://github.com/sl-design-system/components/pull/1225) [`ad297ab`](https://github.com/sl-design-system/components/commit/ad297ab817ab998253b9c2a90033c72dcc686893) - Updated/added tokens: - - Button bar available in all themes - Fixed accordion border - Button fixes diff --git a/packages/themes/magister/CHANGELOG.md b/packages/themes/magister/CHANGELOG.md index dbd1205008..54dda88c82 100644 --- a/packages/themes/magister/CHANGELOG.md +++ b/packages/themes/magister/CHANGELOG.md @@ -23,11 +23,9 @@ ### Minor Changes - [#3020](https://github.com/sl-design-system/components/pull/3020) [`738e4a7`](https://github.com/sl-design-system/components/commit/738e4a77005043de2f9977fab9fb04d4fce6369d) - Added - - new elevation.surface.raised.primary token Fixed - - form input interactive backgrounds (color.background.input.plain.interactive.plain) from plain to bold variants across all themes (e.g., accent.grey.interactive.bold) for better state contrast. - [#2970](https://github.com/sl-design-system/components/pull/2970) [`e92ebb1`](https://github.com/sl-design-system/components/commit/e92ebb16c596919aaa301be2604ab5f3539738a9) - Caret icons have been updated to implement the new alignment strategy used in Font Awesome 7 @@ -164,11 +162,9 @@ - [#1710](https://github.com/sl-design-system/components/pull/1710) [`40cc538`](https://github.com/sl-design-system/components/commit/40cc538648e6ed5ac453fbe708bae8761caaab5e) - Overhaul of how (custom) icons are maintained in figma and exported to be used in the packages. The following icons have changed: - - `circle` has been renamed to `circle-solid` The following icons have been added: - - `badge-available` - `badge-away` - `badge-donotdisturb` @@ -177,7 +173,6 @@ - `info` The following items have been removed (mainly in cleaning up, they were never meant to be there) - - `svg-sort` - `svg-sort-down` - `svg-sort-up` @@ -235,7 +230,6 @@ ### Patch Changes - [#1454](https://github.com/sl-design-system/components/pull/1454) [`af62ce4`](https://github.com/sl-design-system/components/commit/af62ce4d0e65b1363b9cede48642bc22d1fc9365) - - Improve toggle button and group tokens - - Add a `check-solid` icon for use in the `toggle-button` component - [#1414](https://github.com/sl-design-system/components/pull/1414) [`ff1618c`](https://github.com/sl-design-system/components/commit/ff1618cdfa4d0060465d993f656345ba1044f88c) - Update icons to the latest fontawesome release (6.6.0) @@ -250,7 +244,6 @@ ### Patch Changes - [#1389](https://github.com/sl-design-system/components/pull/1389) [`f03971b`](https://github.com/sl-design-system/components/commit/f03971b7b338a4248df292060b91b6b903b6c8ed) - Minor style fixes: - - Fix the title and subtitle text being cutoff for certain characters due not enough line-height - Use a different color for the subtitle text @@ -296,7 +289,6 @@ - [#1251](https://github.com/sl-design-system/components/pull/1251) [`a3da76c`](https://github.com/sl-design-system/components/commit/a3da76c7df521c2241b565dc22025715f1231e9c) - New search icon - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - - Enhanced the color contrast of buttons when used on slightly darker backgrounds across all themes. - - Enhanced the color contrast of inline messages to match our buttons. - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - Fix missing triangle-exclamation-solid icon @@ -313,7 +305,6 @@ - [#1242](https://github.com/sl-design-system/components/pull/1242) [`ab122ec`](https://github.com/sl-design-system/components/commit/ab122ec672a515ae2ca7dce88c7344c1b209d538) - Fix missing `calc()` functions in theme parts. - [#1225](https://github.com/sl-design-system/components/pull/1225) [`ad297ab`](https://github.com/sl-design-system/components/commit/ad297ab817ab998253b9c2a90033c72dcc686893) - Updated/added tokens: - - Button bar available in all themes - Fixed accordion border - Button fixes diff --git a/packages/themes/max/CHANGELOG.md b/packages/themes/max/CHANGELOG.md index 783beba8be..ceee0f1aad 100644 --- a/packages/themes/max/CHANGELOG.md +++ b/packages/themes/max/CHANGELOG.md @@ -23,11 +23,9 @@ ### Minor Changes - [#3020](https://github.com/sl-design-system/components/pull/3020) [`738e4a7`](https://github.com/sl-design-system/components/commit/738e4a77005043de2f9977fab9fb04d4fce6369d) - Added - - new elevation.surface.raised.primary token Fixed - - form input interactive backgrounds (color.background.input.plain.interactive.plain) from plain to bold variants across all themes (e.g., accent.grey.interactive.bold) for better state contrast. - [#2970](https://github.com/sl-design-system/components/pull/2970) [`e92ebb1`](https://github.com/sl-design-system/components/commit/e92ebb16c596919aaa301be2604ab5f3539738a9) - Caret icons have been updated to implement the new alignment strategy used in Font Awesome 7 @@ -149,11 +147,9 @@ - [#1710](https://github.com/sl-design-system/components/pull/1710) [`40cc538`](https://github.com/sl-design-system/components/commit/40cc538648e6ed5ac453fbe708bae8761caaab5e) - Overhaul of how (custom) icons are maintained in figma and exported to be used in the packages. The following icons have changed: - - `circle` has been renamed to `circle-solid` The following icons have been added: - - `badge-available` - `badge-away` - `badge-donotdisturb` @@ -162,7 +158,6 @@ - `info` The following items have been removed (mainly in cleaning up, they were never meant to be there) - - `svg-sort` - `svg-sort-down` - `svg-sort-up` @@ -210,7 +205,6 @@ ### Patch Changes - [#1454](https://github.com/sl-design-system/components/pull/1454) [`af62ce4`](https://github.com/sl-design-system/components/commit/af62ce4d0e65b1363b9cede48642bc22d1fc9365) - - Improve toggle button and group tokens - - Add a `check-solid` icon for use in the `toggle-button` component - [#1414](https://github.com/sl-design-system/components/pull/1414) [`ff1618c`](https://github.com/sl-design-system/components/commit/ff1618cdfa4d0060465d993f656345ba1044f88c) - Update icons to the latest fontawesome release (6.6.0) @@ -225,7 +219,6 @@ ### Patch Changes - [#1389](https://github.com/sl-design-system/components/pull/1389) [`f03971b`](https://github.com/sl-design-system/components/commit/f03971b7b338a4248df292060b91b6b903b6c8ed) - Minor style fixes: - - Fix the title and subtitle text being cutoff for certain characters due not enough line-height - Use a different color for the subtitle text @@ -265,7 +258,6 @@ - [#1251](https://github.com/sl-design-system/components/pull/1251) [`a3da76c`](https://github.com/sl-design-system/components/commit/a3da76c7df521c2241b565dc22025715f1231e9c) - New search icon - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - - Enhanced the color contrast of buttons when used on slightly darker backgrounds across all themes. - - Enhanced the color contrast of inline messages to match our buttons. - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - Fix missing triangle-exclamation-solid icon @@ -282,7 +274,6 @@ - [#1242](https://github.com/sl-design-system/components/pull/1242) [`ab122ec`](https://github.com/sl-design-system/components/commit/ab122ec672a515ae2ca7dce88c7344c1b209d538) - Fix missing `calc()` functions in theme parts. - [#1225](https://github.com/sl-design-system/components/pull/1225) [`ad297ab`](https://github.com/sl-design-system/components/commit/ad297ab817ab998253b9c2a90033c72dcc686893) - Updated/added tokens: - - Button bar available in all themes - Fixed accordion border - Button fixes diff --git a/packages/themes/my-digital-book/CHANGELOG.md b/packages/themes/my-digital-book/CHANGELOG.md index 1f1a9ba848..623d8f3017 100644 --- a/packages/themes/my-digital-book/CHANGELOG.md +++ b/packages/themes/my-digital-book/CHANGELOG.md @@ -23,11 +23,9 @@ ### Minor Changes - [#3020](https://github.com/sl-design-system/components/pull/3020) [`738e4a7`](https://github.com/sl-design-system/components/commit/738e4a77005043de2f9977fab9fb04d4fce6369d) - Added - - new elevation.surface.raised.primary token Fixed - - form input interactive backgrounds (color.background.input.plain.interactive.plain) from plain to bold variants across all themes (e.g., accent.grey.interactive.bold) for better state contrast. - [#2970](https://github.com/sl-design-system/components/pull/2970) [`e92ebb1`](https://github.com/sl-design-system/components/commit/e92ebb16c596919aaa301be2604ab5f3539738a9) - Caret icons have been updated to implement the new alignment strategy used in Font Awesome 7 @@ -147,11 +145,9 @@ - [#1710](https://github.com/sl-design-system/components/pull/1710) [`40cc538`](https://github.com/sl-design-system/components/commit/40cc538648e6ed5ac453fbe708bae8761caaab5e) - Overhaul of how (custom) icons are maintained in figma and exported to be used in the packages. The following icons have changed: - - `circle` has been renamed to `circle-solid` The following icons have been added: - - `badge-available` - `badge-away` - `badge-donotdisturb` @@ -160,7 +156,6 @@ - `info` The following items have been removed (mainly in cleaning up, they were never meant to be there) - - `svg-sort` - `svg-sort-down` - `svg-sort-up` @@ -208,7 +203,6 @@ ### Patch Changes - [#1454](https://github.com/sl-design-system/components/pull/1454) [`af62ce4`](https://github.com/sl-design-system/components/commit/af62ce4d0e65b1363b9cede48642bc22d1fc9365) - - Improve toggle button and group tokens - - Add a `check-solid` icon for use in the `toggle-button` component - [#1414](https://github.com/sl-design-system/components/pull/1414) [`ff1618c`](https://github.com/sl-design-system/components/commit/ff1618cdfa4d0060465d993f656345ba1044f88c) - Update icons to the latest fontawesome release (6.6.0) @@ -223,7 +217,6 @@ ### Patch Changes - [#1389](https://github.com/sl-design-system/components/pull/1389) [`f03971b`](https://github.com/sl-design-system/components/commit/f03971b7b338a4248df292060b91b6b903b6c8ed) - Minor style fixes: - - Fix the title and subtitle text being cutoff for certain characters due not enough line-height - Use a different color for the subtitle text @@ -263,7 +256,6 @@ - [#1251](https://github.com/sl-design-system/components/pull/1251) [`a3da76c`](https://github.com/sl-design-system/components/commit/a3da76c7df521c2241b565dc22025715f1231e9c) - New search icon - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - - Enhanced the color contrast of buttons when used on slightly darker backgrounds across all themes. - - Enhanced the color contrast of inline messages to match our buttons. - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - Fix missing triangle-exclamation-solid icon diff --git a/packages/themes/myvanin_onhold/CHANGELOG.md b/packages/themes/myvanin_onhold/CHANGELOG.md index e02970a6e1..76e2819738 100644 --- a/packages/themes/myvanin_onhold/CHANGELOG.md +++ b/packages/themes/myvanin_onhold/CHANGELOG.md @@ -105,11 +105,9 @@ - [#1710](https://github.com/sl-design-system/components/pull/1710) [`40cc538`](https://github.com/sl-design-system/components/commit/40cc538648e6ed5ac453fbe708bae8761caaab5e) - Overhaul of how (custom) icons are maintained in figma and exported to be used in the packages. The following icons have changed: - - `circle` has been renamed to `circle-solid` The following icons have been added: - - `badge-available` - `badge-away` - `badge-donotdisturb` @@ -118,7 +116,6 @@ - `info` The following items have been removed (mainly in cleaning up, they were never meant to be there) - - `svg-sort` - `svg-sort-down` - `svg-sort-up` @@ -180,7 +177,6 @@ - [#1251](https://github.com/sl-design-system/components/pull/1251) [`a3da76c`](https://github.com/sl-design-system/components/commit/a3da76c7df521c2241b565dc22025715f1231e9c) - New search icon - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - - Enhanced the color contrast of buttons when used on slightly darker backgrounds across all themes. - - Enhanced the color contrast of inline messages to match our buttons. - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - Fix missing triangle-exclamation-solid icon diff --git a/packages/themes/neon/CHANGELOG.md b/packages/themes/neon/CHANGELOG.md index a7f725243d..f87baff427 100644 --- a/packages/themes/neon/CHANGELOG.md +++ b/packages/themes/neon/CHANGELOG.md @@ -23,11 +23,9 @@ ### Minor Changes - [#3020](https://github.com/sl-design-system/components/pull/3020) [`738e4a7`](https://github.com/sl-design-system/components/commit/738e4a77005043de2f9977fab9fb04d4fce6369d) - Added - - new elevation.surface.raised.primary token Fixed - - form input interactive backgrounds (color.background.input.plain.interactive.plain) from plain to bold variants across all themes (e.g., accent.grey.interactive.bold) for better state contrast. - [#2970](https://github.com/sl-design-system/components/pull/2970) [`e92ebb1`](https://github.com/sl-design-system/components/commit/e92ebb16c596919aaa301be2604ab5f3539738a9) - Caret icons have been updated to implement the new alignment strategy used in Font Awesome 7 @@ -149,11 +147,9 @@ - [#1710](https://github.com/sl-design-system/components/pull/1710) [`40cc538`](https://github.com/sl-design-system/components/commit/40cc538648e6ed5ac453fbe708bae8761caaab5e) - Overhaul of how (custom) icons are maintained in figma and exported to be used in the packages. The following icons have changed: - - `circle` has been renamed to `circle-solid` The following icons have been added: - - `badge-available` - `badge-away` - `badge-donotdisturb` @@ -162,7 +158,6 @@ - `info` The following items have been removed (mainly in cleaning up, they were never meant to be there) - - `svg-sort` - `svg-sort-down` - `svg-sort-up` @@ -210,7 +205,6 @@ ### Patch Changes - [#1454](https://github.com/sl-design-system/components/pull/1454) [`af62ce4`](https://github.com/sl-design-system/components/commit/af62ce4d0e65b1363b9cede48642bc22d1fc9365) - - Improve toggle button and group tokens - - Add a `check-solid` icon for use in the `toggle-button` component - [#1414](https://github.com/sl-design-system/components/pull/1414) [`ff1618c`](https://github.com/sl-design-system/components/commit/ff1618cdfa4d0060465d993f656345ba1044f88c) - Update icons to the latest fontawesome release (6.6.0) @@ -225,7 +219,6 @@ ### Patch Changes - [#1389](https://github.com/sl-design-system/components/pull/1389) [`f03971b`](https://github.com/sl-design-system/components/commit/f03971b7b338a4248df292060b91b6b903b6c8ed) - Minor style fixes: - - Fix the title and subtitle text being cutoff for certain characters due not enough line-height - Use a different color for the subtitle text @@ -267,7 +260,6 @@ - [#1251](https://github.com/sl-design-system/components/pull/1251) [`a3da76c`](https://github.com/sl-design-system/components/commit/a3da76c7df521c2241b565dc22025715f1231e9c) - New search icon - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - - Enhanced the color contrast of buttons when used on slightly darker backgrounds across all themes. - - Enhanced the color contrast of inline messages to match our buttons. - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - Fix missing triangle-exclamation-solid icon @@ -284,7 +276,6 @@ - [#1242](https://github.com/sl-design-system/components/pull/1242) [`ab122ec`](https://github.com/sl-design-system/components/commit/ab122ec672a515ae2ca7dce88c7344c1b209d538) - Fix missing `calc()` functions in theme parts. - [#1225](https://github.com/sl-design-system/components/pull/1225) [`ad297ab`](https://github.com/sl-design-system/components/commit/ad297ab817ab998253b9c2a90033c72dcc686893) - Updated/added tokens: - - Button bar available in all themes - Fixed accordion border - Button fixes diff --git a/packages/themes/sanoma-learning/CHANGELOG.md b/packages/themes/sanoma-learning/CHANGELOG.md index af53b3b3cc..d459c3d991 100644 --- a/packages/themes/sanoma-learning/CHANGELOG.md +++ b/packages/themes/sanoma-learning/CHANGELOG.md @@ -23,11 +23,9 @@ ### Minor Changes - [#3020](https://github.com/sl-design-system/components/pull/3020) [`738e4a7`](https://github.com/sl-design-system/components/commit/738e4a77005043de2f9977fab9fb04d4fce6369d) - Added - - new elevation.surface.raised.primary token Fixed - - form input interactive backgrounds (color.background.input.plain.interactive.plain) from plain to bold variants across all themes (e.g., accent.grey.interactive.bold) for better state contrast. - [#2970](https://github.com/sl-design-system/components/pull/2970) [`e92ebb1`](https://github.com/sl-design-system/components/commit/e92ebb16c596919aaa301be2604ab5f3539738a9) - Caret icons have been updated to implement the new alignment strategy used in Font Awesome 7 @@ -155,11 +153,9 @@ - [#1710](https://github.com/sl-design-system/components/pull/1710) [`40cc538`](https://github.com/sl-design-system/components/commit/40cc538648e6ed5ac453fbe708bae8761caaab5e) - Overhaul of how (custom) icons are maintained in figma and exported to be used in the packages. The following icons have changed: - - `circle` has been renamed to `circle-solid` The following icons have been added: - - `badge-available` - `badge-away` - `badge-donotdisturb` @@ -168,7 +164,6 @@ - `info` The following items have been removed (mainly in cleaning up, they were never meant to be there) - - `svg-sort` - `svg-sort-down` - `svg-sort-up` @@ -220,7 +215,6 @@ - [#1422](https://github.com/sl-design-system/components/pull/1422) [`2833861`](https://github.com/sl-design-system/components/commit/28338611d0fccf175c3e22eb268fc5892522dc78) - Added tag tokens - [#1454](https://github.com/sl-design-system/components/pull/1454) [`af62ce4`](https://github.com/sl-design-system/components/commit/af62ce4d0e65b1363b9cede48642bc22d1fc9365) - - Improve toggle button and group tokens - - Add a `check-solid` icon for use in the `toggle-button` component - [#1414](https://github.com/sl-design-system/components/pull/1414) [`ff1618c`](https://github.com/sl-design-system/components/commit/ff1618cdfa4d0060465d993f656345ba1044f88c) - Update icons to the latest fontawesome release (6.6.0) @@ -233,7 +227,6 @@ ### Patch Changes - [#1389](https://github.com/sl-design-system/components/pull/1389) [`f03971b`](https://github.com/sl-design-system/components/commit/f03971b7b338a4248df292060b91b6b903b6c8ed) - Minor style fixes: - - Fix the title and subtitle text being cutoff for certain characters due not enough line-height - Use a different color for the subtitle text @@ -277,7 +270,6 @@ - [#1251](https://github.com/sl-design-system/components/pull/1251) [`a3da76c`](https://github.com/sl-design-system/components/commit/a3da76c7df521c2241b565dc22025715f1231e9c) - New search icon - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - - Enhanced the color contrast of buttons when used on slightly darker backgrounds across all themes. - - Enhanced the color contrast of inline messages to match our buttons. - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - Fix missing triangle-exclamation-solid icon diff --git a/packages/themes/teas/CHANGELOG.md b/packages/themes/teas/CHANGELOG.md index 112b72d888..0f8e98f2df 100644 --- a/packages/themes/teas/CHANGELOG.md +++ b/packages/themes/teas/CHANGELOG.md @@ -23,11 +23,9 @@ ### Minor Changes - [#3020](https://github.com/sl-design-system/components/pull/3020) [`738e4a7`](https://github.com/sl-design-system/components/commit/738e4a77005043de2f9977fab9fb04d4fce6369d) - Added - - new elevation.surface.raised.primary token Fixed - - form input interactive backgrounds (color.background.input.plain.interactive.plain) from plain to bold variants across all themes (e.g., accent.grey.interactive.bold) for better state contrast. - [#2970](https://github.com/sl-design-system/components/pull/2970) [`e92ebb1`](https://github.com/sl-design-system/components/commit/e92ebb16c596919aaa301be2604ab5f3539738a9) - Caret icons have been updated to implement the new alignment strategy used in Font Awesome 7 @@ -147,11 +145,9 @@ - [#1710](https://github.com/sl-design-system/components/pull/1710) [`40cc538`](https://github.com/sl-design-system/components/commit/40cc538648e6ed5ac453fbe708bae8761caaab5e) - Overhaul of how (custom) icons are maintained in figma and exported to be used in the packages. The following icons have changed: - - `circle` has been renamed to `circle-solid` The following icons have been added: - - `badge-available` - `badge-away` - `badge-donotdisturb` @@ -160,7 +156,6 @@ - `info` The following items have been removed (mainly in cleaning up, they were never meant to be there) - - `svg-sort` - `svg-sort-down` - `svg-sort-up` @@ -212,7 +207,6 @@ - [#1469](https://github.com/sl-design-system/components/pull/1469) [`7e31a0d`](https://github.com/sl-design-system/components/commit/7e31a0d63766e7efbdadffdd17b53bcdd1951515) - updated color default ghost button in TEAS - [#1454](https://github.com/sl-design-system/components/pull/1454) [`af62ce4`](https://github.com/sl-design-system/components/commit/af62ce4d0e65b1363b9cede48642bc22d1fc9365) - - Improve toggle button and group tokens - - Add a `check-solid` icon for use in the `toggle-button` component - [#1414](https://github.com/sl-design-system/components/pull/1414) [`ff1618c`](https://github.com/sl-design-system/components/commit/ff1618cdfa4d0060465d993f656345ba1044f88c) - Update icons to the latest fontawesome release (6.6.0) @@ -227,7 +221,6 @@ ### Patch Changes - [#1389](https://github.com/sl-design-system/components/pull/1389) [`f03971b`](https://github.com/sl-design-system/components/commit/f03971b7b338a4248df292060b91b6b903b6c8ed) - Minor style fixes: - - Fix the title and subtitle text being cutoff for certain characters due not enough line-height - Use a different color for the subtitle text @@ -267,7 +260,6 @@ - [#1251](https://github.com/sl-design-system/components/pull/1251) [`a3da76c`](https://github.com/sl-design-system/components/commit/a3da76c7df521c2241b565dc22025715f1231e9c) - New search icon - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - - Enhanced the color contrast of buttons when used on slightly darker backgrounds across all themes. - - Enhanced the color contrast of inline messages to match our buttons. - [#1234](https://github.com/sl-design-system/components/pull/1234) [`fe047da`](https://github.com/sl-design-system/components/commit/fe047da265a3d657d74ee26df95ebd73f2d7ef7f) - Fix missing triangle-exclamation-solid icon @@ -284,7 +276,6 @@ - [#1242](https://github.com/sl-design-system/components/pull/1242) [`ab122ec`](https://github.com/sl-design-system/components/commit/ab122ec672a515ae2ca7dce88c7344c1b209d538) - Fix missing `calc()` functions in theme parts. - [#1225](https://github.com/sl-design-system/components/pull/1225) [`ad297ab`](https://github.com/sl-design-system/components/commit/ad297ab817ab998253b9c2a90033c72dcc686893) - Updated/added tokens: - - Button bar available in all themes - Fixed accordion border - Button fixes diff --git a/packages/themes/tig/CHANGELOG.md b/packages/themes/tig/CHANGELOG.md index 3c481f33b8..296085f74e 100644 --- a/packages/themes/tig/CHANGELOG.md +++ b/packages/themes/tig/CHANGELOG.md @@ -23,11 +23,9 @@ ### Minor Changes - [#3020](https://github.com/sl-design-system/components/pull/3020) [`738e4a7`](https://github.com/sl-design-system/components/commit/738e4a77005043de2f9977fab9fb04d4fce6369d) - Added - - new elevation.surface.raised.primary token Fixed - - form input interactive backgrounds (color.background.input.plain.interactive.plain) from plain to bold variants across all themes (e.g., accent.grey.interactive.bold) for better state contrast. - [#2970](https://github.com/sl-design-system/components/pull/2970) [`e92ebb1`](https://github.com/sl-design-system/components/commit/e92ebb16c596919aaa301be2604ab5f3539738a9) - Caret icons have been updated to implement the new alignment strategy used in Font Awesome 7 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..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,10 +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 + hasTooltipWithAriaRelationLabel || + 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 be7d234588..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 @@ -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: [ @@ -75,6 +81,14 @@ ruleTester.run('button-has-label', buttonHasLabel, { { code: "html``;", errors: [{ messageId: 'mustBeAriaRelationLabel' }] + }, + { + code: "html``;", + errors: [{ messageId: 'missingText' }] + }, + { + code: 'html``;', + errors: [{ messageId: 'missingText' }] } ] }); diff --git a/tools/vitest-browser-lit/package.json b/tools/vitest-browser-lit/package.json index 5878209351..627a519e1f 100644 --- a/tools/vitest-browser-lit/package.json +++ b/tools/vitest-browser-lit/package.json @@ -29,7 +29,7 @@ }, "devDependencies": { "@vitest/browser": "^4.1.8", - "lit": "^3.3.2", + "lit": "^3.3.3", "vitest": "^4.1.8" } } 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'] + } } }, { diff --git a/yarn.lock b/yarn.lock index ffce67dc3e..dae48a42fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3566,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" @@ -3583,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 @@ -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" @@ -4305,272 +4317,277 @@ __metadata: languageName: node linkType: hard -"@oxc-resolver/binding-android-arm-eabi@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-android-arm-eabi@npm:11.20.0" +"@oxc-resolver/binding-android-arm-eabi@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-android-arm-eabi@npm:11.19.1" conditions: os=android & cpu=arm languageName: node linkType: hard -"@oxc-resolver/binding-android-arm64@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-android-arm64@npm:11.20.0" +"@oxc-resolver/binding-android-arm64@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-android-arm64@npm:11.19.1" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@oxc-resolver/binding-darwin-arm64@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-darwin-arm64@npm:11.20.0" +"@oxc-resolver/binding-darwin-arm64@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-darwin-arm64@npm:11.19.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxc-resolver/binding-darwin-x64@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-darwin-x64@npm:11.20.0" +"@oxc-resolver/binding-darwin-x64@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-darwin-x64@npm:11.19.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxc-resolver/binding-freebsd-x64@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-freebsd-x64@npm:11.20.0" +"@oxc-resolver/binding-freebsd-x64@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-freebsd-x64@npm:11.19.1" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.20.0" +"@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.19.1" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxc-resolver/binding-linux-arm-musleabihf@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-linux-arm-musleabihf@npm:11.20.0" +"@oxc-resolver/binding-linux-arm-musleabihf@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-arm-musleabihf@npm:11.19.1" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxc-resolver/binding-linux-arm64-gnu@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-linux-arm64-gnu@npm:11.20.0" +"@oxc-resolver/binding-linux-arm64-gnu@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-arm64-gnu@npm:11.19.1" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@oxc-resolver/binding-linux-arm64-musl@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-linux-arm64-musl@npm:11.20.0" +"@oxc-resolver/binding-linux-arm64-musl@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-arm64-musl@npm:11.19.1" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@oxc-resolver/binding-linux-ppc64-gnu@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-linux-ppc64-gnu@npm:11.20.0" +"@oxc-resolver/binding-linux-ppc64-gnu@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-ppc64-gnu@npm:11.19.1" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@oxc-resolver/binding-linux-riscv64-gnu@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-linux-riscv64-gnu@npm:11.20.0" +"@oxc-resolver/binding-linux-riscv64-gnu@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-riscv64-gnu@npm:11.19.1" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@oxc-resolver/binding-linux-riscv64-musl@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-linux-riscv64-musl@npm:11.20.0" +"@oxc-resolver/binding-linux-riscv64-musl@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-riscv64-musl@npm:11.19.1" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@oxc-resolver/binding-linux-s390x-gnu@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-linux-s390x-gnu@npm:11.20.0" +"@oxc-resolver/binding-linux-s390x-gnu@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-s390x-gnu@npm:11.19.1" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@oxc-resolver/binding-linux-x64-gnu@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-linux-x64-gnu@npm:11.20.0" +"@oxc-resolver/binding-linux-x64-gnu@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-x64-gnu@npm:11.19.1" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@oxc-resolver/binding-linux-x64-musl@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-linux-x64-musl@npm:11.20.0" +"@oxc-resolver/binding-linux-x64-musl@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-x64-musl@npm:11.19.1" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@oxc-resolver/binding-openharmony-arm64@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-openharmony-arm64@npm:11.20.0" +"@oxc-resolver/binding-openharmony-arm64@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-openharmony-arm64@npm:11.19.1" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@oxc-resolver/binding-wasm32-wasi@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-wasm32-wasi@npm:11.20.0" +"@oxc-resolver/binding-wasm32-wasi@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-wasm32-wasi@npm:11.19.1" dependencies: - "@emnapi/core": "npm:1.10.0" - "@emnapi/runtime": "npm:1.10.0" - "@napi-rs/wasm-runtime": "npm:^1.1.4" + "@napi-rs/wasm-runtime": "npm:^1.1.1" conditions: cpu=wasm32 languageName: node linkType: hard -"@oxc-resolver/binding-win32-arm64-msvc@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-win32-arm64-msvc@npm:11.20.0" +"@oxc-resolver/binding-win32-arm64-msvc@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-win32-arm64-msvc@npm:11.19.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@oxc-resolver/binding-win32-x64-msvc@npm:11.20.0": - version: 11.20.0 - resolution: "@oxc-resolver/binding-win32-x64-msvc@npm:11.20.0" +"@oxc-resolver/binding-win32-ia32-msvc@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-win32-ia32-msvc@npm:11.19.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@oxc-resolver/binding-win32-x64-msvc@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-win32-x64-msvc@npm:11.19.1" conditions: os=win32 & cpu=x64 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 @@ -5500,7 +5517,7 @@ __metadata: "@storybook/angular": "npm:^10.4.1" "@types/jasmine": "npm:~6.0.0" baseline-browser-mapping: "npm:^2.10.33" - jasmine-core: "npm:~6.1.0" + jasmine-core: "npm:~6.2.0" karma: "npm:~6.4.4" karma-chrome-launcher: "npm:~3.2.0" karma-coverage: "npm:~2.2.1" @@ -5523,7 +5540,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 @@ -5550,7 +5567,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 @@ -5593,7 +5610,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 @@ -5603,7 +5620,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.1" + "@sl-design-system/tooltip": "npm:^2.0.0" + lit: "npm:^3.3.3" + peerDependencies: + "@open-wc/scoped-elements": ^3.0.6 + lit: ^3.1.4 languageName: unknown linkType: soft @@ -5617,7 +5640,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 @@ -5643,7 +5666,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 @@ -5669,7 +5692,7 @@ __metadata: "@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.2" + lit: "npm:^3.3.3" storybook: "npm:^10.4.1" storybook-addon-pseudo-states: "npm:^10.4.1" tslib: "npm:^2.8.1" @@ -5696,7 +5719,7 @@ __metadata: "@sl-design-system/listbox": "npm:^0.1.7" "@sl-design-system/tag": "npm:^0.1.12" "@sl-design-system/text-field": "npm:^1.6.10" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@open-wc/scoped-elements": ^3.0.6 lit: ^3.1.4 @@ -5723,7 +5746,7 @@ __metadata: "@sl-design-system/icon": "npm:^1.4.2" "@sl-design-system/shared": "npm:^0.12.1" "@sl-design-system/text-field": "npm:^1.6.10" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" peerDependencies: "@open-wc/scoped-elements": ^3.0.6 lit: ^3.1.4 @@ -5740,7 +5763,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 @@ -5755,7 +5778,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 @@ -5938,7 +5961,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 @@ -5951,7 +5974,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 @@ -5966,7 +5989,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 @@ -6013,7 +6036,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 @@ -6095,7 +6118,8 @@ __metadata: "@changesets/get-github-info": "npm:^0.8.0" "@custom-elements-manifest/analyzer": "npm:^0.11.0" "@faker-js/faker": "npm:^10.4.0" - "@lit/localize-tools": "npm:^0.8.1" + "@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" @@ -6118,10 +6142,10 @@ __metadata: 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" + oxfmt: "npm:^0.52.0" playwright: "npm:^1.60.0" sinon: "npm:^21.1.2" sinon-chai: "npm:^4.0.1" @@ -6221,7 +6245,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 @@ -6306,7 +6330,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 @@ -6334,7 +6358,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 @@ -6347,7 +6371,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 @@ -6359,7 +6383,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 @@ -6369,7 +6393,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 @@ -6428,7 +6452,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 @@ -6487,7 +6511,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 @@ -6502,7 +6526,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 @@ -6516,7 +6540,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 @@ -6539,7 +6563,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 @@ -6566,7 +6590,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 @@ -6579,7 +6603,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 @@ -6591,7 +6615,7 @@ __metadata: resolution: "@sl-design-system/vitest-browser-lit@workspace:tools/vitest-browser-lit" dependencies: "@vitest/browser": "npm:^4.1.8" - lit: "npm:^3.3.2" + lit: "npm:^3.3.3" vitest: "npm:^4.1.8" peerDependencies: "@vitest/browser": ">=2.1.0" @@ -7206,6 +7230,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" @@ -8812,10 +8843,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 @@ -8828,7 +8859,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 @@ -10332,13 +10363,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 @@ -10547,7 +10578,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 @@ -10981,6 +11012,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" @@ -11691,9 +11732,9 @@ __metadata: linkType: hard "electron-to-chromium@npm:^1.5.328": - version: 1.5.364 - resolution: "electron-to-chromium@npm:1.5.364" - checksum: 10c0/0371e925fdd112751e091455809ded5f572098ef6acd50fc5a84e932ed6ba8fa71aa63575e736db30f2374ed792a772ff14cb3f9263c407b4bd5bc1861a87953 + version: 1.5.361 + resolution: "electron-to-chromium@npm:1.5.361" + checksum: 10c0/5e6e9c0c12ab82366eddf575c8b9114d5af710abad3b592141c56cfa0169471f3633dcc793420c303fad68dff21c8fb724b43cde7e66be947983fa3d6d5a7359 languageName: node linkType: hard @@ -12670,10 +12711,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 @@ -13558,10 +13599,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 @@ -14944,12 +14985,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 @@ -15561,10 +15602,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 @@ -16319,19 +16360,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 @@ -16354,7 +16397,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: @@ -16368,6 +16411,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" @@ -16388,14 +16444,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 @@ -17141,6 +17197,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" @@ -18120,6 +18183,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" @@ -18929,28 +19001,29 @@ __metadata: linkType: hard "oxc-resolver@npm:^11.19.1, oxc-resolver@npm:^11.9.0": - version: 11.20.0 - resolution: "oxc-resolver@npm:11.20.0" - dependencies: - "@oxc-resolver/binding-android-arm-eabi": "npm:11.20.0" - "@oxc-resolver/binding-android-arm64": "npm:11.20.0" - "@oxc-resolver/binding-darwin-arm64": "npm:11.20.0" - "@oxc-resolver/binding-darwin-x64": "npm:11.20.0" - "@oxc-resolver/binding-freebsd-x64": "npm:11.20.0" - "@oxc-resolver/binding-linux-arm-gnueabihf": "npm:11.20.0" - "@oxc-resolver/binding-linux-arm-musleabihf": "npm:11.20.0" - "@oxc-resolver/binding-linux-arm64-gnu": "npm:11.20.0" - "@oxc-resolver/binding-linux-arm64-musl": "npm:11.20.0" - "@oxc-resolver/binding-linux-ppc64-gnu": "npm:11.20.0" - "@oxc-resolver/binding-linux-riscv64-gnu": "npm:11.20.0" - "@oxc-resolver/binding-linux-riscv64-musl": "npm:11.20.0" - "@oxc-resolver/binding-linux-s390x-gnu": "npm:11.20.0" - "@oxc-resolver/binding-linux-x64-gnu": "npm:11.20.0" - "@oxc-resolver/binding-linux-x64-musl": "npm:11.20.0" - "@oxc-resolver/binding-openharmony-arm64": "npm:11.20.0" - "@oxc-resolver/binding-wasm32-wasi": "npm:11.20.0" - "@oxc-resolver/binding-win32-arm64-msvc": "npm:11.20.0" - "@oxc-resolver/binding-win32-x64-msvc": "npm:11.20.0" + version: 11.19.1 + resolution: "oxc-resolver@npm:11.19.1" + dependencies: + "@oxc-resolver/binding-android-arm-eabi": "npm:11.19.1" + "@oxc-resolver/binding-android-arm64": "npm:11.19.1" + "@oxc-resolver/binding-darwin-arm64": "npm:11.19.1" + "@oxc-resolver/binding-darwin-x64": "npm:11.19.1" + "@oxc-resolver/binding-freebsd-x64": "npm:11.19.1" + "@oxc-resolver/binding-linux-arm-gnueabihf": "npm:11.19.1" + "@oxc-resolver/binding-linux-arm-musleabihf": "npm:11.19.1" + "@oxc-resolver/binding-linux-arm64-gnu": "npm:11.19.1" + "@oxc-resolver/binding-linux-arm64-musl": "npm:11.19.1" + "@oxc-resolver/binding-linux-ppc64-gnu": "npm:11.19.1" + "@oxc-resolver/binding-linux-riscv64-gnu": "npm:11.19.1" + "@oxc-resolver/binding-linux-riscv64-musl": "npm:11.19.1" + "@oxc-resolver/binding-linux-s390x-gnu": "npm:11.19.1" + "@oxc-resolver/binding-linux-x64-gnu": "npm:11.19.1" + "@oxc-resolver/binding-linux-x64-musl": "npm:11.19.1" + "@oxc-resolver/binding-openharmony-arm64": "npm:11.19.1" + "@oxc-resolver/binding-wasm32-wasi": "npm:11.19.1" + "@oxc-resolver/binding-win32-arm64-msvc": "npm:11.19.1" + "@oxc-resolver/binding-win32-ia32-msvc": "npm:11.19.1" + "@oxc-resolver/binding-win32-x64-msvc": "npm:11.19.1" dependenciesMeta: "@oxc-resolver/binding-android-arm-eabi": optional: true @@ -18988,36 +19061,41 @@ __metadata: optional: true "@oxc-resolver/binding-win32-arm64-msvc": optional: true + "@oxc-resolver/binding-win32-ia32-msvc": + optional: true "@oxc-resolver/binding-win32-x64-msvc": optional: true - checksum: 10c0/4a0b0fae7a8b8a25d5173aaeb7348c53d5f60d5bb37a66c3177dcf416319d1f7065b625afa37b924a288dec5b401ba92b003cbce26b91e6631bd8690052c80de - 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" + checksum: 10c0/8ac4eaffa9c0bcbb9f4f4a2b43786457ec5a68684d8776cb78b5a15ce3d1a79d3e67262aa3c635f98a0c1cd6cd56a31fcb05bffb9a286100056e4ab06b928833 + languageName: node + linkType: hard + +"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 @@ -19057,9 +19135,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 @@ -22217,6 +22300,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" @@ -22734,13 +22827,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 @@ -22840,12 +22933,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 @@ -23293,8 +23386,8 @@ __metadata: linkType: hard "terser-webpack-plugin@npm:^5.3.16, terser-webpack-plugin@npm:^5.3.17": - version: 5.6.1 - resolution: "terser-webpack-plugin@npm:5.6.1" + 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" @@ -23327,7 +23420,7 @@ __metadata: optional: true uglify-js: optional: true - checksum: 10c0/841674dab9b58ee242a64a548f2797f83eca3debc010afb2aa1a4be7149e7195a6a546a746324803ba05f72d0c9dc4357e34f17e4b6d6c054bd49a0913c413b9 + checksum: 10c0/191882a727d571291df49b11bdcfa7459aa78e96c542a993d66f70df052404e3b30157708a80c2895bbe2de4860217c2addcf15d5b2321df8f0aa0de2191f64f languageName: node linkType: hard @@ -23401,10 +23494,10 @@ __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 @@ -24968,6 +25061,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" @@ -25110,12 +25214,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