From 9c22bef8a31a9788df90249c20689cdef9179d62 Mon Sep 17 00:00:00 2001 From: Jeri Peier Date: Mon, 18 Nov 2024 13:28:58 +0100 Subject: [PATCH 01/14] test: use promise instead of waitForCondition for events (#3204) --- .../timetable-row/timetable-row.spec.ts | 4 +- src/elements/accordion/accordion.spec.ts | 18 ++-- src/elements/alert/alert/alert.spec.ts | 8 +- .../autocomplete-grid-button.spec.ts | 4 +- .../autocomplete-grid.spec.ts | 56 ++++++------- .../autocomplete/autocomplete.spec.ts | 42 +++++----- .../breadcrumb-group/breadcrumb-group.spec.ts | 4 +- .../breadcrumb/breadcrumb/breadcrumb.spec.ts | 4 +- .../button/button-link/button-link.spec.ts | 4 +- .../button-static/button-static.spec.ts | 4 +- src/elements/button/button/button.spec.ts | 4 +- src/elements/calendar/calendar.spec.ts | 2 +- .../card/card-button/card-button.spec.ts | 4 +- src/elements/card/card-link/card-link.spec.ts | 4 +- .../checkbox/common/checkbox-common.spec.ts | 2 +- src/elements/core/testing/event-spy.ts | 62 +++++++++++++- .../datepicker-next-day.spec.ts | 6 +- .../datepicker-previous-day.spec.ts | 6 +- .../datepicker-toggle.spec.ts | 21 +++-- .../datepicker/datepicker/datepicker.spec.ts | 26 +++--- src/elements/dialog/dialog/dialog.spec.ts | 44 +++++----- .../expansion-panel/expansion-panel.spec.ts | 14 ++-- .../flip-card/flip-card/flip-card.spec.ts | 8 +- .../header-button/header-button.spec.ts | 4 +- .../header/header-link/header-link.spec.ts | 4 +- src/elements/header/header/header.spec.ts | 6 +- .../link/link-button/link-button.spec.ts | 4 +- .../link/link-static/link-static.spec.ts | 4 +- src/elements/link/link/link.spec.ts | 4 +- .../menu/menu-button/menu-button.spec.ts | 4 +- src/elements/menu/menu-link/menu-link.spec.ts | 4 +- src/elements/menu/menu/menu.spec.ts | 48 +++++------ .../navigation-button.spec.ts | 4 +- .../navigation-link/navigation-link.spec.ts | 4 +- .../navigation/navigation/navigation.spec.ts | 48 +++++------ .../notification/notification.spec.ts | 12 +-- src/elements/overlay/overlay.spec.ts | 34 ++++---- .../paginator/paginator/paginator.spec.ts | 6 +- .../popover-trigger/popover-trigger.spec.ts | 10 +-- src/elements/popover/popover/popover.spec.ts | 82 +++++++++---------- .../radio-button-group.spec.ts | 6 +- .../radio-button-panel.spec.ts | 12 +-- .../radio-button/radio-button.spec.ts | 12 +-- src/elements/select/select.spec.ts | 44 +++++----- .../selection-expansion-panel.spec.ts | 22 ++--- src/elements/stepper/stepper/stepper.spec.ts | 16 ++-- src/elements/tabs/tab-group/tab-group.spec.ts | 4 +- src/elements/tag/tag-group/tag-group.spec.ts | 22 ++--- src/elements/toast/toast.spec.ts | 26 +++--- .../toggle-check/toggle-check.spec.ts | 2 +- .../toggle-option/toggle-option.spec.ts | 8 +- src/elements/toggle/toggle/toggle.spec.ts | 18 ++-- .../train/train-wagon/train-wagon.spec.ts | 4 +- 53 files changed, 443 insertions(+), 386 deletions(-) diff --git a/src/elements-experimental/timetable-row/timetable-row.spec.ts b/src/elements-experimental/timetable-row/timetable-row.spec.ts index 42b00eeda1..5c6f2913a7 100644 --- a/src/elements-experimental/timetable-row/timetable-row.spec.ts +++ b/src/elements-experimental/timetable-row/timetable-row.spec.ts @@ -1,7 +1,7 @@ import { assert, expect } from '@open-wc/testing'; import type { SbbCardElement } from '@sbb-esta/lyne-elements/card.js'; import { fixture } from '@sbb-esta/lyne-elements/core/testing/private.js'; -import { EventSpy, waitForCondition } from '@sbb-esta/lyne-elements/core/testing.js'; +import { EventSpy } from '@sbb-esta/lyne-elements/core/testing.js'; import { html } from 'lit/static-html.js'; import type { ITripItem, Notice, PtSituation } from '../core/timetable/timetable-properties.js'; @@ -32,7 +32,7 @@ describe(`sbb-timetable-row`, () => { const changeSpy = new EventSpy('click'); card.click(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); }); }); diff --git a/src/elements/accordion/accordion.spec.ts b/src/elements/accordion/accordion.spec.ts index efbf029edf..043872e4a3 100644 --- a/src/elements/accordion/accordion.spec.ts +++ b/src/elements/accordion/accordion.spec.ts @@ -2,7 +2,7 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { fixture } from '../core/testing/private.js'; -import { waitForCondition, waitForLitRender, EventSpy } from '../core/testing.js'; +import { EventSpy, waitForLitRender } from '../core/testing.js'; import { SbbExpansionPanelElement, type SbbExpansionPanelHeaderElement, @@ -139,21 +139,21 @@ describe(`sbb-accordion`, () => { } headerTwo.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); expect(panelOne.expanded).to.be.equal(false); expect(panelTwo.expanded).to.be.equal(true); expect(panelThree.expanded).to.be.equal(false); headerOne.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 2); + await willOpenEventSpy.calledTimes(2); expect(willOpenEventSpy.count).to.be.equal(2); expect(panelOne.expanded).to.be.equal(true); expect(panelTwo.expanded).to.be.equal(false); expect(panelThree.expanded).to.be.equal(false); headerThree.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 3); + await willOpenEventSpy.calledTimes(3); expect(willOpenEventSpy.count).to.be.equal(3); expect(panelOne.expanded).to.be.equal(false); expect(panelTwo.expanded).to.be.equal(false); @@ -182,21 +182,21 @@ describe(`sbb-accordion`, () => { } headerTwo.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); expect(panelOne.expanded).to.be.equal(false); expect(panelTwo.expanded).to.be.equal(true); expect(panelThree.expanded).to.be.equal(false); headerOne.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 2); + await willOpenEventSpy.calledTimes(2); expect(willOpenEventSpy.count).to.be.equal(2); expect(panelOne.expanded).to.be.equal(true); expect(panelTwo.expanded).to.be.equal(true); expect(panelThree.expanded).to.be.equal(false); headerThree.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 3); + await willOpenEventSpy.calledTimes(3); expect(willOpenEventSpy.count).to.be.equal(3); expect(panelOne.expanded).to.be.equal(true); expect(panelTwo.expanded).to.be.equal(true); @@ -224,12 +224,12 @@ describe(`sbb-accordion`, () => { const willOpenEventSpy = new EventSpy(SbbExpansionPanelElement.events.willOpen); headerTwo.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); expect(panelTwo.expanded).to.be.equal(true); headerThree.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 2); + await willOpenEventSpy.calledTimes(2); expect(willOpenEventSpy.count).to.be.equal(2); expect(panelThree.expanded).to.be.equal(true); diff --git a/src/elements/alert/alert/alert.spec.ts b/src/elements/alert/alert/alert.spec.ts index 4a9b0f41bf..11d25b5470 100644 --- a/src/elements/alert/alert/alert.spec.ts +++ b/src/elements/alert/alert/alert.spec.ts @@ -2,7 +2,7 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { waitForCondition, EventSpy } from '../../core/testing.js'; +import { EventSpy } from '../../core/testing.js'; import { SbbAlertElement } from './alert.js'; @@ -25,9 +25,9 @@ describe(`sbb-alert`, () => { html`Interruption`, ); - await waitForCondition(() => willOpenSpy.events.length === 1); + await willOpenSpy.calledOnce(); expect(willOpenSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenSpy.events.length === 1); + await didOpenSpy.calledOnce(); expect(didOpenSpy.count).to.be.equal(1); alert.requestDismissal(); @@ -35,7 +35,7 @@ describe(`sbb-alert`, () => { alert.close(); - await waitForCondition(() => didCloseSpy.events.length === 1); + await didCloseSpy.calledOnce(); expect(willCloseSpy.count).to.be.equal(1); expect(didCloseSpy.count).to.be.equal(1); }); diff --git a/src/elements/autocomplete-grid/autocomplete-grid-button/autocomplete-grid-button.spec.ts b/src/elements/autocomplete-grid/autocomplete-grid-button/autocomplete-grid-button.spec.ts index 6863dad599..48bfd38fcd 100644 --- a/src/elements/autocomplete-grid/autocomplete-grid-button/autocomplete-grid-button.spec.ts +++ b/src/elements/autocomplete-grid/autocomplete-grid-button/autocomplete-grid-button.spec.ts @@ -2,7 +2,7 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbAutocompleteGridButtonElement } from './autocomplete-grid-button.js'; @@ -24,7 +24,7 @@ describe(`sbb-autocomplete-grid-button`, () => { const clickSpy = new EventSpy('click'); element.click(); - await waitForCondition(() => clickSpy.events.length === 1); + await clickSpy.calledOnce(); expect(clickSpy.count).to.be.equal(1); }); diff --git a/src/elements/autocomplete-grid/autocomplete-grid/autocomplete-grid.spec.ts b/src/elements/autocomplete-grid/autocomplete-grid/autocomplete-grid.spec.ts index cb73c476dd..b3830f75a1 100644 --- a/src/elements/autocomplete-grid/autocomplete-grid/autocomplete-grid.spec.ts +++ b/src/elements/autocomplete-grid/autocomplete-grid/autocomplete-grid.spec.ts @@ -4,7 +4,7 @@ import { html } from 'lit/static-html.js'; import { isSafari } from '../../core/dom.js'; import { fixture, tabKey } from '../../core/testing/private.js'; -import { describeIf, EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { describeIf, EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbFormFieldElement } from '../../form-field.js'; import type { SbbAutocompleteGridButtonElement } from '../autocomplete-grid-button.js'; import { SbbAutocompleteGridOptionElement } from '../autocomplete-grid-option.js'; @@ -100,47 +100,47 @@ describe(`sbb-autocomplete-grid`, () => { const didCloseEventSpy = new EventSpy(SbbAutocompleteGridElement.events.didClose); input.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); expect(input).to.have.attribute('aria-expanded', 'true'); await sendKeys({ press: 'Escape' }); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); expect(input).to.have.attribute('aria-expanded', 'false'); await sendKeys({ press: 'ArrowDown' }); - await waitForCondition(() => willOpenEventSpy.events.length === 2); + await willOpenEventSpy.calledTimes(2); expect(willOpenEventSpy.count).to.be.equal(2); - await waitForCondition(() => didOpenEventSpy.events.length === 2); + await didOpenEventSpy.calledTimes(2); expect(didOpenEventSpy.count).to.be.equal(2); expect(input).to.have.attribute('aria-expanded', 'true'); await sendKeys({ press: tabKey }); - await waitForCondition(() => willCloseEventSpy.events.length === 2); + await willCloseEventSpy.calledTimes(2); expect(willCloseEventSpy.count).to.be.equal(2); - await waitForCondition(() => didCloseEventSpy.events.length === 2); + await didCloseEventSpy.calledTimes(2); expect(didCloseEventSpy.count).to.be.equal(2); expect(input).to.have.attribute('aria-expanded', 'false'); input.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 3); + await willOpenEventSpy.calledTimes(3); expect(willOpenEventSpy.count).to.be.equal(3); - await waitForCondition(() => didOpenEventSpy.events.length === 3); + await didOpenEventSpy.calledTimes(3); expect(didOpenEventSpy.count).to.be.equal(3); expect(input).to.have.attribute('aria-expanded', 'true'); // Simulate backdrop click - sendMouse({ type: 'click', position: [formField.offsetWidth + 25, 25] }); + await sendMouse({ type: 'click', position: [formField.offsetWidth + 25, 25] }); - await waitForCondition(() => willCloseEventSpy.events.length === 3); + await willCloseEventSpy.calledTimes(3); expect(willCloseEventSpy.count).to.be.equal(3); - await waitForCondition(() => didCloseEventSpy.events.length === 3); + await didCloseEventSpy.calledTimes(3); expect(didCloseEventSpy.count).to.be.equal(3); expect(input).to.have.attribute('aria-expanded', 'false'); }); @@ -153,9 +153,9 @@ describe(`sbb-autocomplete-grid`, () => { ); input.focus(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await sendKeys({ press: 'ArrowDown' }); @@ -173,16 +173,16 @@ describe(`sbb-autocomplete-grid`, () => { const clickSpy = new EventSpy('click'); input.focus(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); const buttonOne = element.querySelector('#button-1') as SbbAutocompleteGridButtonElement; buttonOne.click(); await waitForLitRender(element); - await waitForCondition(() => clickSpy.events.length === 1); + await clickSpy.calledOnce(); expect(clickSpy.count).to.be.equal(1); expect( (clickSpy.firstEvent!.target as SbbAutocompleteGridButtonElement).option!.textContent, @@ -201,7 +201,7 @@ describe(`sbb-autocomplete-grid`, () => { const buttonThree = element.querySelector('#button-3'); input.focus(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await sendKeys({ press: 'ArrowDown' }); @@ -246,7 +246,7 @@ describe(`sbb-autocomplete-grid`, () => { const optTwo = element.querySelector('#option-2'); input.focus(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await sendKeys({ press: 'ArrowDown' }); @@ -259,7 +259,7 @@ describe(`sbb-autocomplete-grid`, () => { expect(input).to.have.attribute('aria-activedescendant', 'option-2'); await sendKeys({ press: 'Enter' }); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); expect(optTwo).not.to.have.attribute('data-active'); @@ -277,7 +277,7 @@ describe(`sbb-autocomplete-grid`, () => { const buttonTwo = element.querySelector('#button-2'); input.focus(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await sendKeys({ press: 'ArrowDown' }); @@ -289,7 +289,7 @@ describe(`sbb-autocomplete-grid`, () => { expect(buttonOne).to.have.attribute('data-focus-visible'); expect(input).to.have.attribute('aria-activedescendant', 'button-1'); await sendKeys({ press: 'Enter' }); - await waitForCondition(() => clickSpy.events.length === 1); + await clickSpy.calledOnce(); expect(clickSpy.count).to.be.equal(1); await sendKeys({ press: 'ArrowDown' }); @@ -300,7 +300,7 @@ describe(`sbb-autocomplete-grid`, () => { expect(buttonTwo).to.have.attribute('data-focus-visible'); expect(input).to.have.attribute('aria-activedescendant', 'button-2'); await sendKeys({ press: 'Enter' }); - await waitForCondition(() => clickSpy.events.length === 2); + await clickSpy.calledTimes(2); expect(clickSpy.count).to.be.equal(2); }); @@ -344,7 +344,7 @@ describe(`sbb-autocomplete-grid`, () => { ); element.open(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -356,7 +356,7 @@ describe(`sbb-autocomplete-grid`, () => { const willCloseEventSpy = new EventSpy(SbbAutocompleteGridElement.events.willClose); element.open(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); await waitForLitRender(element); element.addEventListener(SbbAutocompleteGridElement.events.willClose, (ev) => @@ -364,7 +364,7 @@ describe(`sbb-autocomplete-grid`, () => { ); element.close(); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'opened'); diff --git a/src/elements/autocomplete/autocomplete.spec.ts b/src/elements/autocomplete/autocomplete.spec.ts index 4053e3d5db..2dbb837143 100644 --- a/src/elements/autocomplete/autocomplete.spec.ts +++ b/src/elements/autocomplete/autocomplete.spec.ts @@ -4,7 +4,7 @@ import { html } from 'lit/static-html.js'; import { isSafari } from '../core/dom.js'; import { fixture, tabKey } from '../core/testing/private.js'; -import { describeIf, EventSpy, waitForCondition, waitForLitRender } from '../core/testing.js'; +import { describeIf, EventSpy, waitForLitRender } from '../core/testing.js'; import { SbbFormFieldElement } from '../form-field.js'; import { SbbOptionElement } from '../option.js'; @@ -70,47 +70,47 @@ describe(`sbb-autocomplete`, () => { const didCloseEventSpy = new EventSpy(SbbAutocompleteElement.events.didClose); input.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); expect(input).to.have.attribute('aria-expanded', 'true'); await sendKeys({ press: 'Escape' }); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); expect(input).to.have.attribute('aria-expanded', 'false'); await sendKeys({ press: 'ArrowDown' }); - await waitForCondition(() => willOpenEventSpy.events.length === 2); + await willOpenEventSpy.calledTimes(2); expect(willOpenEventSpy.count).to.be.equal(2); - await waitForCondition(() => didOpenEventSpy.events.length === 2); + await didOpenEventSpy.calledTimes(2); expect(didOpenEventSpy.count).to.be.equal(2); expect(input).to.have.attribute('aria-expanded', 'true'); await sendKeys({ press: tabKey }); - await waitForCondition(() => willCloseEventSpy.events.length === 2); + await willCloseEventSpy.calledTimes(2); expect(willCloseEventSpy.count).to.be.equal(2); - await waitForCondition(() => didCloseEventSpy.events.length === 2); + await didCloseEventSpy.calledTimes(2); expect(didCloseEventSpy.count).to.be.equal(2); expect(input).to.have.attribute('aria-expanded', 'false'); input.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 3); + await willOpenEventSpy.calledTimes(3); expect(willOpenEventSpy.count).to.be.equal(3); - await waitForCondition(() => didOpenEventSpy.events.length === 3); + await didOpenEventSpy.calledTimes(3); expect(didOpenEventSpy.count).to.be.equal(3); expect(input).to.have.attribute('aria-expanded', 'true'); // Simulate backdrop click - sendMouse({ type: 'click', position: [formField.offsetWidth + 25, 25] }); + await sendMouse({ type: 'click', position: [formField.offsetWidth + 25, 25] }); - await waitForCondition(() => willCloseEventSpy.events.length === 3); + await willCloseEventSpy.calledTimes(3); expect(willCloseEventSpy.count).to.be.equal(3); - await waitForCondition(() => didCloseEventSpy.events.length === 3); + await didCloseEventSpy.calledTimes(3); expect(didCloseEventSpy.count).to.be.equal(3); expect(input).to.have.attribute('aria-expanded', 'false'); }); @@ -121,9 +121,9 @@ describe(`sbb-autocomplete`, () => { const optionSelectedEventSpy = new EventSpy(SbbOptionElement.events.optionSelected); input.focus(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await sendKeys({ press: 'ArrowDown' }); @@ -143,7 +143,7 @@ describe(`sbb-autocomplete`, () => { const optTwo = element.querySelector('#option-2'); input.focus(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await sendKeys({ press: 'ArrowDown' }); @@ -156,7 +156,7 @@ describe(`sbb-autocomplete`, () => { expect(input).to.have.attribute('aria-activedescendant', 'option-2'); await sendKeys({ press: 'Enter' }); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); expect(optTwo).not.to.have.attribute('data-active'); @@ -204,7 +204,7 @@ describe(`sbb-autocomplete`, () => { element.addEventListener(SbbAutocompleteElement.events.willOpen, (ev) => ev.preventDefault()); element.open(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -216,13 +216,13 @@ describe(`sbb-autocomplete`, () => { const willCloseEventSpy = new EventSpy(SbbAutocompleteElement.events.willClose); element.open(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); await waitForLitRender(element); element.addEventListener(SbbAutocompleteElement.events.willClose, (ev) => ev.preventDefault()); element.close(); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'opened'); diff --git a/src/elements/breadcrumb/breadcrumb-group/breadcrumb-group.spec.ts b/src/elements/breadcrumb/breadcrumb-group/breadcrumb-group.spec.ts index b2f41fbe03..eafb9a0a70 100644 --- a/src/elements/breadcrumb/breadcrumb-group/breadcrumb-group.spec.ts +++ b/src/elements/breadcrumb/breadcrumb-group/breadcrumb-group.spec.ts @@ -3,7 +3,7 @@ import { sendKeys, setViewport } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import type { SbbBreadcrumbElement } from '../breadcrumb.js'; import { SbbBreadcrumbGroupElement } from './breadcrumb-group.js'; @@ -149,7 +149,7 @@ describe(`sbb-breadcrumb-group`, () => { const changeSpy = new EventSpy('click', ellipsisButton); ellipsisButton.click(); await waitForLitRender(ellipsisListItemElement); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); ellipsisListItemElement = element.shadowRoot!.querySelector( '#sbb-breadcrumb-group-ellipsis', diff --git a/src/elements/breadcrumb/breadcrumb/breadcrumb.spec.ts b/src/elements/breadcrumb/breadcrumb/breadcrumb.spec.ts index f8428057b2..5591ea7f1a 100644 --- a/src/elements/breadcrumb/breadcrumb/breadcrumb.spec.ts +++ b/src/elements/breadcrumb/breadcrumb/breadcrumb.spec.ts @@ -2,7 +2,7 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { waitForCondition, EventSpy, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbBreadcrumbElement } from './breadcrumb.js'; @@ -21,7 +21,7 @@ describe(`sbb-breadcrumb`, () => { const changeSpy = new EventSpy('click'); element.click(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); }); diff --git a/src/elements/button/button-link/button-link.spec.ts b/src/elements/button/button-link/button-link.spec.ts index 14ca9bebea..4daf35d9d6 100644 --- a/src/elements/button/button-link/button-link.spec.ts +++ b/src/elements/button/button-link/button-link.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbButtonLinkElement } from './button-link.js'; @@ -25,7 +25,7 @@ describe(`sbb-button-link`, () => { const clickSpy = new EventSpy('click'); element.click(); - await waitForCondition(() => clickSpy.events.length === 1); + await clickSpy.calledOnce(); expect(clickSpy.count).to.be.equal(1); }); diff --git a/src/elements/button/button-static/button-static.spec.ts b/src/elements/button/button-static/button-static.spec.ts index a9a674a3d6..8470aaf1e6 100644 --- a/src/elements/button/button-static/button-static.spec.ts +++ b/src/elements/button/button-static/button-static.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { buttonIconTestTemplate, buttonSpaceIconTestTemplate, @@ -30,7 +30,7 @@ describe(`sbb-button-static`, () => { const clickSpy = new EventSpy('click'); element.click(); - await waitForCondition(() => clickSpy.events.length === 1); + await clickSpy.calledOnce(); expect(clickSpy.count).to.be.equal(1); }); diff --git a/src/elements/button/button/button.spec.ts b/src/elements/button/button/button.spec.ts index 08f5f5f76c..3b9ff85a4a 100644 --- a/src/elements/button/button/button.spec.ts +++ b/src/elements/button/button/button.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { buttonIconTestTemplate, buttonSpaceIconTestTemplate, @@ -29,7 +29,7 @@ describe(`sbb-button`, () => { const clickSpy = new EventSpy('click'); element.click(); - await waitForCondition(() => clickSpy.events.length === 1); + await clickSpy.calledOnce(); expect(clickSpy.count).to.be.equal(1); }); diff --git a/src/elements/calendar/calendar.spec.ts b/src/elements/calendar/calendar.spec.ts index 568df96ff5..642bf653e6 100644 --- a/src/elements/calendar/calendar.spec.ts +++ b/src/elements/calendar/calendar.spec.ts @@ -126,7 +126,7 @@ describe(`sbb-calendar`, () => { ) as HTMLElement; expect(newSelectedDate).not.to.have.class('sbb-calendar__selected'); newSelectedDate.click(); - await waitForCondition(() => selectedSpy.events.length === 1); + await selectedSpy.calledOnce(); expect(selectedDate).not.to.have.class('sbb-calendar__selected'); expect(newSelectedDate).to.have.class('sbb-calendar__selected'); diff --git a/src/elements/card/card-button/card-button.spec.ts b/src/elements/card/card-button/card-button.spec.ts index c9349032a2..7fe08631b8 100644 --- a/src/elements/card/card-button/card-button.spec.ts +++ b/src/elements/card/card-button/card-button.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import type { SbbCardElement } from '../card.js'; import type { SbbCardButtonElement } from './card-button.js'; @@ -138,7 +138,7 @@ describe(`sbb-card-button`, () => { action.click(); - await waitForCondition(() => clickSpy.events.length === 1); + await clickSpy.calledOnce(); expect(clickSpy.count).to.be.equal(1); }); diff --git a/src/elements/card/card-link/card-link.spec.ts b/src/elements/card/card-link/card-link.spec.ts index a40cbb56e8..b6e4445a26 100644 --- a/src/elements/card/card-link/card-link.spec.ts +++ b/src/elements/card/card-link/card-link.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import type { SbbCardElement } from '../card.js'; import type { SbbCardLinkElement } from './card-link.js'; @@ -148,7 +148,7 @@ describe(`sbb-card-link`, () => { action.click(); - await waitForCondition(() => clickSpy.events.length === 1); + await clickSpy.calledOnce(); expect(clickSpy.count).to.be.equal(1); }); diff --git a/src/elements/checkbox/common/checkbox-common.spec.ts b/src/elements/checkbox/common/checkbox-common.spec.ts index 97fdf927fe..f28e10ec79 100644 --- a/src/elements/checkbox/common/checkbox-common.spec.ts +++ b/src/elements/checkbox/common/checkbox-common.spec.ts @@ -51,7 +51,7 @@ describe(`checkbox common behaviors`, () => { element.focus(); await sendKeys({ press: 'Space' }); - await waitForCondition(() => changeSpy.count === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.greaterThan(0); }); }); diff --git a/src/elements/core/testing/event-spy.ts b/src/elements/core/testing/event-spy.ts index ddc3d02fbb..3544611bab 100644 --- a/src/elements/core/testing/event-spy.ts +++ b/src/elements/core/testing/event-spy.ts @@ -1,19 +1,23 @@ +type PromiseWithExecutor = { + promise: Promise; + resolve: (value: T) => void; + reject: (reason?: any) => void; +}; + /** * This class exists to facilitate the test migration from stencil to lit. * It mimics the API that stencil provided to test events. - * - * TODO: Document me */ export class EventSpy { - private _count: number = 0; public get count(): number { return this._count; } + private _count: number = 0; - private _events: T[] = []; public get events(): T[] { return this._events; } + private _events: T[] = []; public get firstEvent(): T | null { return this.events.length ? this.events[0] : null; @@ -23,6 +27,8 @@ export class EventSpy { return this.events.length ? this.events[this.events.length - 1] : null; } + private _promiseEventMap = new Map>(); + public constructor( private _event: string, private readonly _target: Node | null = null, @@ -30,13 +36,61 @@ export class EventSpy { if (!this._target) { this._target = document; } + this._listenForEvent(); } + public calledOnce(timeout = 1000): Promise { + return this.calledTimes(1, timeout); + } + + public calledTimes(count: number, timeout = 1000): Promise { + if (this.count > count) { + return Promise.reject( + `Event has been emitted more than expected (expected ${count}, actual ${this.count}`, + ); + } else if (this.count === count) { + return Promise.resolve(this.events[count - 1]); + } else if (this._promiseEventMap.has(count)) { + return this._promiseEventMap.get(count)!.promise; + } else { + let resolve: (value: T) => void; + let reject: (reason?: any) => void; + const promise = new Promise((resolveFunction, rejectFunction) => { + resolve = resolveFunction; + reject = rejectFunction; + }); + + const promiseWithExecutor = { + promise, + resolve: resolve!, + reject: reject!, + }; + + this._promiseEventMap.set(count, promiseWithExecutor); + + return this._wrapPromiseWithTimeout(promiseWithExecutor, count, timeout); + } + } + + private _wrapPromiseWithTimeout( + promiseWithExecutor: PromiseWithExecutor, + count: number, + timeout: number, + ): Promise { + const clearTimeoutId = setTimeout( + () => promiseWithExecutor.reject(`awaiting calledTimes(${count}) results in timeout`), + timeout, + ); + promiseWithExecutor.promise.then(() => clearTimeout(clearTimeoutId)); + return promiseWithExecutor.promise; + } + private _listenForEvent(): void { this._target?.addEventListener(this._event, (ev) => { this._events.push(ev as T); this._count++; + this._promiseEventMap.get(this.count)?.resolve(ev as T); }); } } diff --git a/src/elements/datepicker/datepicker-next-day/datepicker-next-day.spec.ts b/src/elements/datepicker/datepicker-next-day/datepicker-next-day.spec.ts index 3b09717cc9..c08601f0d0 100644 --- a/src/elements/datepicker/datepicker-next-day/datepicker-next-day.spec.ts +++ b/src/elements/datepicker/datepicker-next-day/datepicker-next-day.spec.ts @@ -2,7 +2,7 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import type { SbbFormFieldElement } from '../../form-field.js'; import type { SbbDatepickerElement } from '../datepicker.js'; @@ -44,7 +44,7 @@ describe(`sbb-datepicker-next-day`, () => { await element.click(); - await waitForCondition(() => changeSpy.events.length >= 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); expect(blurSpy.count).to.be.equal(1); @@ -136,7 +136,7 @@ describe(`sbb-datepicker-next-day`, () => { const changeSpy = new EventSpy('change', input); const blurSpy = new EventSpy('blur', input); element.click(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); expect(blurSpy.count).to.be.equal(1); expect(input.value).to.be.equal('Su, 22.01.2023'); diff --git a/src/elements/datepicker/datepicker-previous-day/datepicker-previous-day.spec.ts b/src/elements/datepicker/datepicker-previous-day/datepicker-previous-day.spec.ts index 03eb216f2a..053d9f9f23 100644 --- a/src/elements/datepicker/datepicker-previous-day/datepicker-previous-day.spec.ts +++ b/src/elements/datepicker/datepicker-previous-day/datepicker-previous-day.spec.ts @@ -2,7 +2,7 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import type { SbbFormFieldElement } from '../../form-field.js'; import type { SbbDatepickerElement } from '../datepicker.js'; @@ -40,7 +40,7 @@ describe(`sbb-datepicker-previous-day`, () => { const changeSpy = new EventSpy('change', input); const blurSpy = new EventSpy('blur', input); element.click(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); expect(blurSpy.count).to.be.equal(1); @@ -134,7 +134,7 @@ describe(`sbb-datepicker-previous-day`, () => { const changeSpy = new EventSpy('change', input); const blurSpy = new EventSpy('blur', input); element.click(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); expect(blurSpy.count).to.be.equal(1); expect(input.value).to.be.equal('Th, 19.01.2023'); diff --git a/src/elements/datepicker/datepicker-toggle/datepicker-toggle.spec.ts b/src/elements/datepicker/datepicker-toggle/datepicker-toggle.spec.ts index b1fdec4be5..1043fc5bb5 100644 --- a/src/elements/datepicker/datepicker-toggle/datepicker-toggle.spec.ts +++ b/src/elements/datepicker/datepicker-toggle/datepicker-toggle.spec.ts @@ -1,4 +1,4 @@ -import { assert, expect } from '@open-wc/testing'; +import { assert, aTimeout, expect } from '@open-wc/testing'; import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; @@ -50,7 +50,7 @@ describe(`sbb-datepicker-toggle`, () => { expect(popover).to.have.attribute('data-state', 'closed'); popoverTrigger.click(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(popover).to.have.attribute('data-state', 'opened'); }); @@ -77,7 +77,7 @@ describe(`sbb-datepicker-toggle`, () => { element.open(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(popover).to.have.attribute('data-state', 'opened'); }); @@ -186,7 +186,7 @@ describe(`sbb-datepicker-toggle`, () => { const popoverTrigger: SbbMiniButtonElement = element.shadowRoot!.querySelector('sbb-mini-button')!; popoverTrigger.click(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(popover).to.have.attribute('data-state', 'opened'); const calendar: SbbCalendarElement = @@ -228,7 +228,10 @@ describe(`sbb-datepicker-toggle`, () => { // Open calendar datepickerToggle.open(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); + + // We have to wait another tick + await aTimeout(0); // Year view should be active const calendar = datepickerToggle.shadowRoot!.querySelector('sbb-calendar')!; @@ -251,11 +254,11 @@ describe(`sbb-datepicker-toggle`, () => { // Expect selected date and closed calendar expect(defaultDateAdapter.toIso8601(calendar.selected!)).to.be.equal('2020-05-05'); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); // Open again datepickerToggle.open(); - await waitForCondition(() => didOpenEventSpy.events.length === 2); + await didOpenEventSpy.calledTimes(2); // Should open with year view again expect(calendar.shadowRoot!.querySelector('.sbb-calendar__table-year-view')!).not.to.be.null; @@ -265,7 +268,7 @@ describe(`sbb-datepicker-toggle`, () => { // Close again await sendKeys({ press: 'Escape' }); - await waitForCondition(() => didCloseEventSpy.events.length === 2); + await didCloseEventSpy.calledTimes(2); // Changing to month view datepickerToggle.view = 'month'; @@ -273,7 +276,7 @@ describe(`sbb-datepicker-toggle`, () => { // Open again datepickerToggle.open(); - await waitForCondition(() => didOpenEventSpy.events.length === 3); + await didOpenEventSpy.calledTimes(3); // Month view should be active and correct year preselected expect(calendar.shadowRoot!.querySelector('.sbb-calendar__table-month-view')!).not.to.be.null; diff --git a/src/elements/datepicker/datepicker/datepicker.spec.ts b/src/elements/datepicker/datepicker/datepicker.spec.ts index 9fa5201ecf..fb79348164 100644 --- a/src/elements/datepicker/datepicker/datepicker.spec.ts +++ b/src/elements/datepicker/datepicker/datepicker.spec.ts @@ -8,7 +8,7 @@ import { NativeDateAdapter } from '../../core/datetime.js'; import { findInput } from '../../core/dom.js'; import { i18nDateChangedTo } from '../../core/i18n.js'; import { fixture, tabKey, typeInElement } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import type { SbbFormFieldElement } from '../../form-field.js'; import type { SbbDatepickerNextDayElement } from '../datepicker-next-day.js'; import type { SbbDatepickerPreviousDayElement } from '../datepicker-previous-day.js'; @@ -93,7 +93,7 @@ describe(`sbb-datepicker`, () => { const changeSpy = new EventSpy('change', element); typeInElement(input, '20/01/2023'); button.focus(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(input.value).to.be.equal('Fr, 20.01.2023'); expect(changeSpy.count).to.be.equal(1); }); @@ -102,7 +102,7 @@ describe(`sbb-datepicker`, () => { const changeSpy = new EventSpy('change', element); typeInElement(input, '20/01/12'); button.focus(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(input.value).to.be.equal('Fr, 20.01.2012'); expect(changeSpy.count).to.be.equal(1); }); @@ -111,7 +111,7 @@ describe(`sbb-datepicker`, () => { const changeSpy = new EventSpy('change', element); typeInElement(input, '20/01/99'); button.focus(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(input.value).to.be.equal('We, 20.01.1999'); expect(changeSpy.count).to.be.equal(1); }); @@ -120,7 +120,7 @@ describe(`sbb-datepicker`, () => { const changeSpy = new EventSpy('change', element); typeInElement(input, '20..2012'); button.focus(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(input).to.have.attribute('data-sbb-invalid'); expect(changeSpy.count).to.be.equal(1); }); @@ -129,7 +129,7 @@ describe(`sbb-datepicker`, () => { const changeSpy = new EventSpy('change', element); typeInElement(input, '20.05.'); button.focus(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(input).to.have.attribute('data-sbb-invalid'); expect(changeSpy.count).to.be.equal(1); }); @@ -138,7 +138,7 @@ describe(`sbb-datepicker`, () => { const changeSpy = new EventSpy('change', element); typeInElement(input, '20.00.2012'); button.focus(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(input).to.have.attribute('data-sbb-invalid'); expect(changeSpy.count).to.be.equal(1); }); @@ -147,7 +147,7 @@ describe(`sbb-datepicker`, () => { const changeSpy = new EventSpy('change', element); typeInElement(input, '00.05.2020'); button.focus(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(input).to.have.attribute('data-sbb-invalid'); expect(changeSpy.count).to.be.equal(1); }); @@ -163,11 +163,11 @@ describe(`sbb-datepicker`, () => { it('renders and emits event when input parameter changes', async () => { const datePickerUpdatedSpy = new EventSpy('datePickerUpdated'); element.wide = true; - await waitForCondition(() => datePickerUpdatedSpy.events.length === 1); + await datePickerUpdatedSpy.calledOnce(); expect(datePickerUpdatedSpy.count).to.be.equal(1); element.dateFilter = () => false; await waitForLitRender(element); - await waitForCondition(() => datePickerUpdatedSpy.events.length === 2); + await datePickerUpdatedSpy.calledTimes(2); expect(datePickerUpdatedSpy.count).to.be.equal(2); }); @@ -192,7 +192,7 @@ describe(`sbb-datepicker`, () => { }; await waitForLitRender(element); typeInElement(input, '7.8', { key: 'Enter', keyCode: 13 }); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); await waitForLitRender(element); expect(input.value).to.be.equal('Mo, 07.08'); expect(changeSpy.count).to.be.equal(1); @@ -206,7 +206,7 @@ describe(`sbb-datepicker`, () => { await waitForLitRender(element); // Then validation event should emit with false - await waitForCondition(() => validationChangeSpy.events.length === 1); + await validationChangeSpy.calledOnce(); expect((validationChangeSpy.lastEvent as CustomEvent).detail['valid']).to.be.equal(false); expect(input).to.have.attribute('data-sbb-invalid'); @@ -226,7 +226,7 @@ describe(`sbb-datepicker`, () => { input.blur(); // Then validation event should be emitted with true - await waitForCondition(() => validationChangeSpy.events.length === 1); + await validationChangeSpy.calledOnce(); expect((validationChangeSpy.lastEvent as CustomEvent).detail['valid']).to.be.equal(true); expect(input).not.to.have.attribute('data-sbb-invalid'); }); diff --git a/src/elements/dialog/dialog/dialog.spec.ts b/src/elements/dialog/dialog/dialog.spec.ts index 230d73127d..94c0e7df32 100644 --- a/src/elements/dialog/dialog/dialog.spec.ts +++ b/src/elements/dialog/dialog/dialog.spec.ts @@ -20,11 +20,11 @@ async function openDialog(element: SbbDialogElement): Promise { element.open(); await waitForLitRender(element); - await waitForCondition(() => willOpen.events.length === 1); + await willOpen.calledOnce(); expect(willOpen.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didOpen.events.length === 1); + await didOpen.calledOnce(); expect(didOpen.count).to.be.equal(1); await waitForLitRender(element); @@ -63,7 +63,7 @@ describe('sbb-dialog', () => { element.open(); await waitForLitRender(element); - await waitForCondition(() => willOpen.events.length === 1); + await willOpen.calledOnce(); expect(willOpen.count).to.be.equal(1); await waitForLitRender(element); @@ -82,11 +82,11 @@ describe('sbb-dialog', () => { element.close(); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 1); + await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didClose.events.length === 1); + await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); await waitForLitRender(element); @@ -105,7 +105,7 @@ describe('sbb-dialog', () => { element.close(); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 1); + await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); await waitForLitRender(element); @@ -124,11 +124,11 @@ describe('sbb-dialog', () => { element.dispatchEvent(new CustomEvent('pointerup')); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 1); + await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didClose.events.length === 1); + await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); await waitForLitRender(element); @@ -214,11 +214,11 @@ describe('sbb-dialog', () => { closeButton.click(); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 1); + await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didClose.events.length === 1); + await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); await waitForLitRender(element); @@ -237,11 +237,11 @@ describe('sbb-dialog', () => { await sendKeys({ press: 'Escape' }); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 1); + await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didClose.events.length === 1); + await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); await waitForLitRender(element); @@ -275,11 +275,11 @@ describe('sbb-dialog', () => { stackedDialog.open(); await waitForLitRender(element); - await waitForCondition(() => willOpen.events.length === 2); + await willOpen.calledTimes(2); expect(willOpen.count).to.be.equal(2); await waitForLitRender(element); - await waitForCondition(() => didOpen.events.length === 2); + await didOpen.calledTimes(2); expect(didOpen.count).to.be.equal(2); await waitForLitRender(element); @@ -291,11 +291,11 @@ describe('sbb-dialog', () => { await sendKeys({ press: 'Escape' }); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 1); + await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didClose.events.length === 1); + await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); await waitForLitRender(element); @@ -308,11 +308,11 @@ describe('sbb-dialog', () => { await sendKeys({ press: 'Escape' }); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 2); + await willClose.calledTimes(2); expect(willClose.count).to.be.equal(2); await waitForLitRender(element); - await waitForCondition(() => didClose.events.length === 2); + await didClose.calledTimes(2); expect(didClose.count).to.be.equal(2); await waitForLitRender(element); @@ -344,11 +344,11 @@ describe('sbb-dialog', () => { innerElement.open(); await waitForLitRender(element); - await waitForCondition(() => willOpen.events.length === 2); + await willOpen.calledTimes(2); expect(willOpen.count).to.be.equal(2); await waitForLitRender(element); - await waitForCondition(() => didOpen.events.length === 2); + await didOpen.calledTimes(2); expect(didOpen.count).to.be.equal(2); await waitForLitRender(element); @@ -359,11 +359,11 @@ describe('sbb-dialog', () => { innerElement.dispatchEvent(new CustomEvent('pointerup')); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 1); + await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didClose.events.length === 1); + await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); await waitForLitRender(element); diff --git a/src/elements/expansion-panel/expansion-panel/expansion-panel.spec.ts b/src/elements/expansion-panel/expansion-panel/expansion-panel.spec.ts index 2ce8516076..4a38141810 100644 --- a/src/elements/expansion-panel/expansion-panel/expansion-panel.spec.ts +++ b/src/elements/expansion-panel/expansion-panel/expansion-panel.spec.ts @@ -2,7 +2,7 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { waitForCondition, waitForLitRender, EventSpy } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import type { SbbExpansionPanelContentElement } from '../expansion-panel-content.js'; import '../expansion-panel-content.js'; import { SbbExpansionPanelHeaderElement } from '../expansion-panel-header.js'; @@ -73,27 +73,27 @@ describe(`sbb-expansion-panel`, () => { await waitForLitRender(element); header.click(); - await waitForCondition(() => toggleExpandedEventSpy.events.length === 1); + await toggleExpandedEventSpy.calledOnce(); expect(toggleExpandedEventSpy.count).to.be.equal(1); await waitForLitRender(element); expect(element.expanded).to.be.equal(true); expect(header.getAttribute('aria-expanded')).to.be.equal('true'); expect(content.getAttribute('aria-hidden')).to.be.equal('false'); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); header.click(); - await waitForCondition(() => toggleExpandedEventSpy.events.length === 2); + await toggleExpandedEventSpy.calledTimes(2); expect(toggleExpandedEventSpy.count).to.be.equal(2); await waitForLitRender(element); expect(element.expanded).to.be.equal(false); expect(header.getAttribute('aria-expanded')).to.be.equal('false'); expect(content.getAttribute('aria-hidden')).to.be.equal('true'); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); }); diff --git a/src/elements/flip-card/flip-card/flip-card.spec.ts b/src/elements/flip-card/flip-card/flip-card.spec.ts index 12d8ccc81c..4a10c8e962 100644 --- a/src/elements/flip-card/flip-card/flip-card.spec.ts +++ b/src/elements/flip-card/flip-card/flip-card.spec.ts @@ -2,7 +2,7 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import type { SbbFlipCardDetailsElement } from '../flip-card-details.js'; import type { SbbFlipCardSummaryElement } from '../flip-card-summary.js'; @@ -45,7 +45,7 @@ describe('sbb-flip-card', () => { element.shadowRoot?.querySelector('button')!.click(); await waitForLitRender(element); - await waitForCondition(() => flipSpy.events.length === 1); + await flipSpy.calledOnce(); expect(flipSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-flipped'); @@ -67,7 +67,7 @@ describe('sbb-flip-card', () => { element.click(); await waitForLitRender(element); - await waitForCondition(() => flipSpy.events.length === 1); + await flipSpy.calledOnce(); expect(flipSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-flipped'); @@ -89,7 +89,7 @@ describe('sbb-flip-card', () => { element.toggle(); await waitForLitRender(element); - await waitForCondition(() => flipSpy.events.length === 1); + await flipSpy.calledOnce(); expect(flipSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-flipped'); diff --git a/src/elements/header/header-button/header-button.spec.ts b/src/elements/header/header-button/header-button.spec.ts index 2efe4e25df..e77ae0afa7 100644 --- a/src/elements/header/header-button/header-button.spec.ts +++ b/src/elements/header/header-button/header-button.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbHeaderButtonElement } from './header-button.js'; @@ -23,7 +23,7 @@ describe(`sbb-header-button`, () => { const clickSpy = new EventSpy('click'); element.click(); - await waitForCondition(() => clickSpy.events.length === 1); + await clickSpy.calledOnce(); expect(clickSpy.count).to.be.equal(1); }); diff --git a/src/elements/header/header-link/header-link.spec.ts b/src/elements/header/header-link/header-link.spec.ts index 0d1bd2eea0..4fa3c226de 100644 --- a/src/elements/header/header-link/header-link.spec.ts +++ b/src/elements/header/header-link/header-link.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbHeaderLinkElement } from './header-link.js'; @@ -23,7 +23,7 @@ describe(`sbb-header-link`, () => { const clickSpy = new EventSpy('click'); element.click(); - await waitForCondition(() => clickSpy.events.length === 1); + await clickSpy.calledOnce(); expect(clickSpy.count).to.be.equal(1); }); diff --git a/src/elements/header/header/header.spec.ts b/src/elements/header/header/header.spec.ts index 1aab90d985..e9ecd32435 100644 --- a/src/elements/header/header/header.spec.ts +++ b/src/elements/header/header/header.spec.ts @@ -3,7 +3,7 @@ import { sendKeys, setViewport } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture, tabKey } from '../../core/testing/private.js'; -import { EventSpy, mockScrollTo, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, mockScrollTo, waitForLitRender } from '../../core/testing.js'; import { SbbMenuElement } from '../../menu.js'; import type { SbbHeaderButtonElement } from '../header-button.js'; @@ -166,10 +166,10 @@ describe(`sbb-header`, () => { const menuTrigger = root.querySelector('sbb-header-button')!; menuTrigger.click(); await waitForLitRender(element); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); const menuId = menuTrigger.getAttribute('aria-controls'); diff --git a/src/elements/link/link-button/link-button.spec.ts b/src/elements/link/link-button/link-button.spec.ts index 6fc4ef97f4..b36bc0c0d5 100644 --- a/src/elements/link/link-button/link-button.spec.ts +++ b/src/elements/link/link-button/link-button.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbLinkButtonElement } from './link-button.js'; @@ -23,7 +23,7 @@ describe(`sbb-link-button`, () => { const changeSpy = new EventSpy('click'); await element.click(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); }); diff --git a/src/elements/link/link-static/link-static.spec.ts b/src/elements/link/link-static/link-static.spec.ts index 3dbf42f023..2142c8acdb 100644 --- a/src/elements/link/link-static/link-static.spec.ts +++ b/src/elements/link/link-static/link-static.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbLinkStaticElement } from './link-static.js'; @@ -23,7 +23,7 @@ describe(`sbb-link-static`, () => { const changeSpy = new EventSpy('click'); await element.click(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); }); diff --git a/src/elements/link/link/link.spec.ts b/src/elements/link/link/link.spec.ts index 8aa1a9241d..d016a85c7e 100644 --- a/src/elements/link/link/link.spec.ts +++ b/src/elements/link/link/link.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbLinkElement } from './link.js'; @@ -23,7 +23,7 @@ describe(`sbb-link`, () => { const changeSpy = new EventSpy('click'); await element.click(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); }); diff --git a/src/elements/menu/menu-button/menu-button.spec.ts b/src/elements/menu/menu-button/menu-button.spec.ts index d3153ea924..ebd020dcd1 100644 --- a/src/elements/menu/menu-button/menu-button.spec.ts +++ b/src/elements/menu/menu-button/menu-button.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbMenuButtonElement } from './menu-button.js'; @@ -23,7 +23,7 @@ describe(`sbb-menu-button`, () => { const changeSpy = new EventSpy('click'); element.click(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); }); diff --git a/src/elements/menu/menu-link/menu-link.spec.ts b/src/elements/menu/menu-link/menu-link.spec.ts index cd6ef01178..e90196abbb 100644 --- a/src/elements/menu/menu-link/menu-link.spec.ts +++ b/src/elements/menu/menu-link/menu-link.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbMenuLinkElement } from './menu-link.js'; @@ -25,7 +25,7 @@ describe(`sbb-menu-link`, () => { const changeSpy = new EventSpy('click'); element.click(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); }); diff --git a/src/elements/menu/menu/menu.spec.ts b/src/elements/menu/menu/menu.spec.ts index 4ff53ed69b..dac178930a 100644 --- a/src/elements/menu/menu/menu.spec.ts +++ b/src/elements/menu/menu/menu.spec.ts @@ -4,7 +4,7 @@ import { html } from 'lit/static-html.js'; import type { SbbButtonElement } from '../../button.js'; import { fixture, tabKey } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbMenuElement } from './menu.js'; @@ -48,11 +48,11 @@ describe(`sbb-menu`, () => { trigger.click(); await waitForLitRender(element); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -68,11 +68,11 @@ describe(`sbb-menu`, () => { trigger.click(); await waitForLitRender(element); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -84,11 +84,11 @@ describe(`sbb-menu`, () => { await sendKeys({ press: 'Escape' }); await waitForLitRender(element); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -105,11 +105,11 @@ describe(`sbb-menu`, () => { trigger.click(); await waitForLitRender(element); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -118,11 +118,11 @@ describe(`sbb-menu`, () => { menuAction.click(); await waitForLitRender(element); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -139,11 +139,11 @@ describe(`sbb-menu`, () => { trigger.click(); await waitForLitRender(element); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -153,11 +153,11 @@ describe(`sbb-menu`, () => { menuLink.click(); await waitForLitRender(element); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -173,11 +173,11 @@ describe(`sbb-menu`, () => { trigger.click(); await waitForLitRender(element); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -208,11 +208,11 @@ describe(`sbb-menu`, () => { trigger.click(); await waitForLitRender(element); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -234,11 +234,11 @@ describe(`sbb-menu`, () => { await sendKeys({ press: 'Enter' }); await waitForLitRender(element); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -254,7 +254,7 @@ describe(`sbb-menu`, () => { element.addEventListener(SbbMenuElement.events.willOpen, (ev) => ev.preventDefault()); element.open(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -266,13 +266,13 @@ describe(`sbb-menu`, () => { const willCloseEventSpy = new EventSpy(SbbMenuElement.events.willClose); element.open(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); await waitForLitRender(element); element.addEventListener(SbbMenuElement.events.willClose, (ev) => ev.preventDefault()); element.close(); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'opened'); diff --git a/src/elements/navigation/navigation-button/navigation-button.spec.ts b/src/elements/navigation/navigation-button/navigation-button.spec.ts index 9f7561c93c..5dec6b87a7 100644 --- a/src/elements/navigation/navigation-button/navigation-button.spec.ts +++ b/src/elements/navigation/navigation-button/navigation-button.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { waitForCondition, waitForLitRender, EventSpy } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbNavigationButtonElement } from './navigation-button.js'; @@ -24,7 +24,7 @@ describe(`sbb-navigation-button`, () => { it('dispatches event on click', async () => { const changeSpy = new EventSpy('click'); element.click(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); }); diff --git a/src/elements/navigation/navigation-link/navigation-link.spec.ts b/src/elements/navigation/navigation-link/navigation-link.spec.ts index 4a90deda60..923c0b375a 100644 --- a/src/elements/navigation/navigation-link/navigation-link.spec.ts +++ b/src/elements/navigation/navigation-link/navigation-link.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { waitForCondition, waitForLitRender, EventSpy } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbNavigationLinkElement } from './navigation-link.js'; @@ -24,7 +24,7 @@ describe(`sbb-navigation-link`, () => { it('dispatches event on click', async () => { const changeSpy = new EventSpy('click'); element.click(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); }); diff --git a/src/elements/navigation/navigation/navigation.spec.ts b/src/elements/navigation/navigation/navigation.spec.ts index 33b2973f7a..40b2f44152 100644 --- a/src/elements/navigation/navigation/navigation.spec.ts +++ b/src/elements/navigation/navigation/navigation.spec.ts @@ -50,7 +50,7 @@ describe(`sbb-navigation`, () => { element.open(); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -87,7 +87,7 @@ describe(`sbb-navigation`, () => { element.open(); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -133,7 +133,7 @@ describe(`sbb-navigation`, () => { element.open(); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -175,7 +175,7 @@ describe(`sbb-navigation`, () => { element.open(); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -200,7 +200,7 @@ describe(`sbb-navigation`, () => { element.close(); await waitForLitRender(element); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -209,7 +209,7 @@ describe(`sbb-navigation`, () => { element.open(); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 2); + await didOpenEventSpy.calledTimes(2); expect(didOpenEventSpy.count).to.be.equal(2); await waitForLitRender(element); @@ -228,7 +228,7 @@ describe(`sbb-navigation`, () => { element.open(); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -237,7 +237,7 @@ describe(`sbb-navigation`, () => { element.close(); await waitForLitRender(element); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -253,7 +253,7 @@ describe(`sbb-navigation`, () => { element.open(); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -262,7 +262,7 @@ describe(`sbb-navigation`, () => { closeButton.click(); await waitForLitRender(element); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -276,7 +276,7 @@ describe(`sbb-navigation`, () => { element.open(); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -288,7 +288,7 @@ describe(`sbb-navigation`, () => { await sendKeys({ press: 'Escape' }); await waitForLitRender(element); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -312,7 +312,7 @@ describe(`sbb-navigation`, () => { action.click(); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -322,7 +322,7 @@ describe(`sbb-navigation`, () => { closeEl.click(); await waitForLitRender(element); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -340,7 +340,7 @@ describe(`sbb-navigation`, () => { element.open(); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -369,7 +369,7 @@ describe(`sbb-navigation`, () => { element.open(); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -408,7 +408,7 @@ describe(`sbb-navigation`, () => { await waitForLitRender(element); await nextFrame(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); await waitForCondition(() => section.getAttribute('data-state') === 'opened'); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -418,7 +418,7 @@ describe(`sbb-navigation`, () => { closeButton.click(); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); await waitForCondition(() => section.getAttribute('data-state') === 'closed'); expect(didCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -441,7 +441,7 @@ describe(`sbb-navigation`, () => { action.click(); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -454,7 +454,7 @@ describe(`sbb-navigation`, () => { await sendKeys({ press: 'Escape' }); await waitForLitRender(element); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -478,7 +478,7 @@ describe(`sbb-navigation`, () => { action.click(); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -499,7 +499,7 @@ describe(`sbb-navigation`, () => { element.addEventListener(SbbNavigationElement.events.willOpen, (ev) => ev.preventDefault()); element.open(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -511,13 +511,13 @@ describe(`sbb-navigation`, () => { const willCloseEventSpy = new EventSpy(SbbNavigationElement.events.willClose); element.open(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); await waitForLitRender(element); element.addEventListener(SbbNavigationElement.events.willClose, (ev) => ev.preventDefault()); element.close(); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'opened'); diff --git a/src/elements/notification/notification.spec.ts b/src/elements/notification/notification.spec.ts index 2d411c8957..b2af8a186d 100644 --- a/src/elements/notification/notification.spec.ts +++ b/src/elements/notification/notification.spec.ts @@ -1,9 +1,9 @@ -import { aTimeout, assert, expect } from '@open-wc/testing'; +import { assert, aTimeout, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import type { SbbSecondaryButtonElement } from '../button.js'; import { fixture } from '../core/testing/private.js'; -import { waitForCondition, EventSpy, waitForLitRender } from '../core/testing.js'; +import { EventSpy, waitForCondition, waitForLitRender } from '../core/testing.js'; import { SbbNotificationElement } from './notification.js'; @@ -36,11 +36,11 @@ describe(`sbb-notification`, () => { element.close(); await waitForLitRender(element); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -65,11 +65,11 @@ describe(`sbb-notification`, () => { closeButton.click(); await waitForLitRender(element); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); diff --git a/src/elements/overlay/overlay.spec.ts b/src/elements/overlay/overlay.spec.ts index f9e26b0d8e..1b68c922f7 100644 --- a/src/elements/overlay/overlay.spec.ts +++ b/src/elements/overlay/overlay.spec.ts @@ -18,11 +18,11 @@ async function openOverlay(element: SbbOverlayElement): Promise { element.open(); await waitForLitRender(element); - await waitForCondition(() => willOpen.events.length === 1); + await willOpen.calledOnce(); expect(willOpen.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didOpen.events.length === 1); + await didOpen.calledOnce(); expect(didOpen.count).to.be.equal(1); await waitForLitRender(element); @@ -59,7 +59,7 @@ describe('sbb-overlay', () => { element.open(); await waitForLitRender(element); - await waitForCondition(() => willOpen.events.length === 1); + await willOpen.calledOnce(); expect(willOpen.count).to.be.equal(1); await waitForLitRender(element); @@ -78,11 +78,11 @@ describe('sbb-overlay', () => { element.close(); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 1); + await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didClose.events.length === 1); + await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); await waitForLitRender(element); @@ -101,7 +101,7 @@ describe('sbb-overlay', () => { element.close(); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 1); + await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); await waitForLitRender(element); @@ -119,11 +119,11 @@ describe('sbb-overlay', () => { closeButton.click(); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 1); + await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didClose.events.length === 1); + await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); await waitForLitRender(element); @@ -151,7 +151,7 @@ describe('sbb-overlay', () => { closeButton.click(); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 1); + await willClose.calledOnce(); expect(willClose.firstEvent?.detail.returnValue).to.be.deep.equal(form); }); @@ -168,11 +168,11 @@ describe('sbb-overlay', () => { await sendKeys({ press: 'Escape' }); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 1); + await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didClose.events.length === 1); + await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); await waitForLitRender(element); @@ -202,11 +202,11 @@ describe('sbb-overlay', () => { stackedOverlay.open(); await waitForLitRender(element); - await waitForCondition(() => willOpen.events.length === 2); + await willOpen.calledTimes(2); expect(willOpen.count).to.be.equal(2); await waitForLitRender(element); - await waitForCondition(() => didOpen.events.length === 2); + await didOpen.calledTimes(2); expect(didOpen.count).to.be.equal(2); await waitForLitRender(element); @@ -218,11 +218,11 @@ describe('sbb-overlay', () => { await sendKeys({ press: 'Escape' }); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 1); + await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didClose.events.length === 1); + await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); await waitForLitRender(element); @@ -235,11 +235,11 @@ describe('sbb-overlay', () => { await sendKeys({ press: 'Escape' }); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 2); + await willClose.calledTimes(2); expect(willClose.count).to.be.equal(2); await waitForLitRender(element); - await waitForCondition(() => didClose.events.length === 2); + await didClose.calledTimes(2); expect(didClose.count).to.be.equal(2); await waitForLitRender(element); diff --git a/src/elements/paginator/paginator/paginator.spec.ts b/src/elements/paginator/paginator/paginator.spec.ts index 6680e9ffb7..d258b6fcac 100644 --- a/src/elements/paginator/paginator/paginator.spec.ts +++ b/src/elements/paginator/paginator/paginator.spec.ts @@ -5,7 +5,7 @@ import { spy } from 'sinon'; import type { SbbMiniButtonElement } from '../../button/mini-button.js'; import { fixture, tabKey } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import type { SbbOptionElement } from '../../option.js'; import { SbbSelectElement } from '../../select.js'; @@ -110,9 +110,9 @@ describe('sbb-paginator', () => { const willOpen = new EventSpy(SbbSelectElement.events.willOpen); const didOpen = new EventSpy(SbbSelectElement.events.didOpen); select.click(); - await waitForCondition(() => willOpen.events.length === 1); + await willOpen.calledOnce(); expect(willOpen.count).to.be.equal(1); - await waitForCondition(() => didOpen.events.length === 1); + await didOpen.calledOnce(); expect(didOpen.count).to.be.equal(1); await waitForLitRender(element); diff --git a/src/elements/popover/popover-trigger/popover-trigger.spec.ts b/src/elements/popover/popover-trigger/popover-trigger.spec.ts index e4e4422386..cc0887663a 100644 --- a/src/elements/popover/popover-trigger/popover-trigger.spec.ts +++ b/src/elements/popover/popover-trigger/popover-trigger.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { waitForCondition, EventSpy, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; import { SbbPopoverElement } from '../popover.js'; import { SbbPopoverTriggerElement } from './popover-trigger.js'; @@ -38,10 +38,10 @@ describe(`sbb-popover-trigger`, () => { element.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); expect(popover).to.have.attribute('data-state', 'opened'); }); @@ -62,7 +62,7 @@ describe(`sbb-popover-trigger`, () => { const focusSpy = new EventSpy('focus', element); element.focus(); - await waitForCondition(() => focusSpy.events.length === 1); + await focusSpy.calledOnce(); expect(focusSpy.count).to.be.equal(1); await sendKeys({ press: 'Enter' }); @@ -77,7 +77,7 @@ describe(`sbb-popover-trigger`, () => { popover.hoverTrigger = true; element.focus(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); await sendKeys({ press: 'Enter' }); diff --git a/src/elements/popover/popover/popover.spec.ts b/src/elements/popover/popover/popover.spec.ts index 7d192756d8..b4c87586c9 100644 --- a/src/elements/popover/popover/popover.spec.ts +++ b/src/elements/popover/popover/popover.spec.ts @@ -44,10 +44,10 @@ describe(`sbb-popover`, () => { element.open(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-state', 'opened'); @@ -59,10 +59,10 @@ describe(`sbb-popover`, () => { trigger.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-state', 'opened'); @@ -76,19 +76,19 @@ describe(`sbb-popover`, () => { element.open(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-state', 'opened'); element.close(); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-state', 'closed'); }); @@ -102,19 +102,19 @@ describe(`sbb-popover`, () => { element.open(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-state', 'opened'); closeButton!.click(); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-state', 'closed'); @@ -131,10 +131,10 @@ describe(`sbb-popover`, () => { trigger.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-state', 'opened'); @@ -142,10 +142,10 @@ describe(`sbb-popover`, () => { popoverLink.click(); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-state', 'closed'); @@ -161,10 +161,10 @@ describe(`sbb-popover`, () => { trigger.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-state', 'opened'); @@ -190,13 +190,13 @@ describe(`sbb-popover`, () => { element.open(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); // Simulate backdrop click window.dispatchEvent(new MouseEvent('mousedown', { buttons: 1, clientX: 1 })); window.dispatchEvent(new PointerEvent('pointerup')); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(trigger).to.have.attribute('data-focus-origin', 'mouse'); expect(document.activeElement).to.be.equal(trigger); @@ -211,7 +211,7 @@ describe(`sbb-popover`, () => { element.open(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); const interactiveElementPosition = interactiveBackgroundElement.getBoundingClientRect(); await sendMouse({ @@ -221,7 +221,7 @@ describe(`sbb-popover`, () => { Math.round(interactiveElementPosition.y + interactiveElementPosition.height / 2), ], }); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(document.activeElement).to.be.equal(interactiveBackgroundElement); }); @@ -233,7 +233,7 @@ describe(`sbb-popover`, () => { trigger.click(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); expect(popoverLink).not.to.be.null; @@ -241,7 +241,7 @@ describe(`sbb-popover`, () => { popoverLink.focus(); await sendKeys({ press: 'Enter' }); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); expect(trigger).to.have.attribute('data-focus-origin', 'keyboard'); @@ -255,10 +255,10 @@ describe(`sbb-popover`, () => { await sendKeys({ press: tabKey }); await sendKeys({ press: 'Enter' }); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-state', 'opened'); @@ -278,14 +278,14 @@ describe(`sbb-popover`, () => { element.open(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); closeButton.focus(); await sendKeys({ press: 'Enter' }); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); expect(trigger).to.have.attribute('data-focus-origin', 'keyboard'); @@ -300,10 +300,10 @@ describe(`sbb-popover`, () => { trigger.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-state', 'opened'); @@ -311,10 +311,10 @@ describe(`sbb-popover`, () => { await sendKeys({ press: tabKey }); await sendKeys({ press: 'Escape' }); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-state', 'closed'); @@ -328,7 +328,7 @@ describe(`sbb-popover`, () => { element.addEventListener(SbbPopoverElement.events.willOpen, (ev) => ev.preventDefault()); element.open(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -340,13 +340,13 @@ describe(`sbb-popover`, () => { const willCloseEventSpy = new EventSpy(SbbPopoverElement.events.willClose); element.open(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); await waitForLitRender(element); element.addEventListener(SbbPopoverElement.events.willClose, (ev) => ev.preventDefault()); element.close(); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'opened'); @@ -476,10 +476,10 @@ describe(`sbb-popover`, () => { trigger.focus(); await sendKeys({ press: 'Space' }); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-state', 'opened'); @@ -491,17 +491,17 @@ describe(`sbb-popover`, () => { await sendKeys({ press: 'Space' }); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); expect(element).to.have.attribute('data-state', 'closed'); - await waitForCondition(() => willOpenEventSpy.events.length === 2); + await willOpenEventSpy.calledTimes(2); expect(willOpenEventSpy.count).to.be.equal(2); - await waitForCondition(() => didOpenEventSpy.events.length === 2); + await didOpenEventSpy.calledTimes(2); expect(didOpenEventSpy.count).to.be.equal(2); expect(secondElement).to.have.attribute('data-state', 'opened'); }); diff --git a/src/elements/radio-button/radio-button-group/radio-button-group.spec.ts b/src/elements/radio-button/radio-button-group/radio-button-group.spec.ts index a1e2899478..03c5d6756f 100644 --- a/src/elements/radio-button/radio-button-group/radio-button-group.spec.ts +++ b/src/elements/radio-button/radio-button-group/radio-button-group.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html, unsafeStatic } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { waitForCondition, waitForLitRender, EventSpy } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import type { SbbRadioButtonPanelElement } from '../radio-button-panel.js'; import type { SbbRadioButtonElement } from '../radio-button.js'; @@ -88,9 +88,9 @@ import { SbbRadioButtonGroupElement } from './radio-button-group.js'; const inputSpy = new EventSpy('input'); checkedRadio.click(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); - await waitForCondition(() => inputSpy.events.length === 1); + await inputSpy.calledOnce(); expect(inputSpy.count).to.be.equal(1); firstRadio.click(); diff --git a/src/elements/radio-button/radio-button-panel/radio-button-panel.spec.ts b/src/elements/radio-button/radio-button-panel/radio-button-panel.spec.ts index f502726e1f..ab9446623c 100644 --- a/src/elements/radio-button/radio-button-panel/radio-button-panel.spec.ts +++ b/src/elements/radio-button/radio-button-panel/radio-button-panel.spec.ts @@ -2,7 +2,7 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { waitForCondition, waitForLitRender, EventSpy } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbRadioButtonPanelElement } from './radio-button-panel.js'; @@ -31,7 +31,7 @@ describe(`sbb-radio-button`, () => { await waitForLitRender(element); expect(element).to.have.attribute('checked'); - await waitForCondition(() => stateChange.events.length === 1); + await stateChange.calledOnce(); expect(stateChange.count).to.be.equal(1); }); @@ -41,13 +41,13 @@ describe(`sbb-radio-button`, () => { element.click(); await waitForLitRender(element); expect(element).to.have.attribute('checked'); - await waitForCondition(() => stateChange.events.length === 1); + await stateChange.calledOnce(); expect(stateChange.count).to.be.equal(1); element.click(); await waitForLitRender(element); expect(element).to.have.attribute('checked'); - await waitForCondition(() => stateChange.events.length === 1); + await stateChange.calledOnce(); expect(stateChange.count).to.be.equal(1); }); @@ -58,13 +58,13 @@ describe(`sbb-radio-button`, () => { element.click(); await waitForLitRender(element); expect(element).to.have.attribute('checked'); - await waitForCondition(() => stateChange.events.length === 1); + await stateChange.calledOnce(); expect(stateChange.count).to.be.equal(1); element.click(); await waitForLitRender(element); expect(element).not.to.have.attribute('checked'); - await waitForCondition(() => stateChange.events.length === 2); + await stateChange.calledTimes(2); expect(stateChange.count).to.be.equal(2); }); }); diff --git a/src/elements/radio-button/radio-button/radio-button.spec.ts b/src/elements/radio-button/radio-button/radio-button.spec.ts index 6a7e47b9e6..026282240e 100644 --- a/src/elements/radio-button/radio-button/radio-button.spec.ts +++ b/src/elements/radio-button/radio-button/radio-button.spec.ts @@ -2,7 +2,7 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { waitForCondition, waitForLitRender, EventSpy } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbRadioButtonElement } from './radio-button.js'; @@ -48,7 +48,7 @@ describe(`sbb-radio-button`, () => { await waitForLitRender(element); expect(element).to.have.attribute('checked'); - await waitForCondition(() => stateChange.events.length === 1); + await stateChange.calledOnce(); expect(stateChange.count).to.be.equal(1); }); @@ -58,13 +58,13 @@ describe(`sbb-radio-button`, () => { element.click(); await waitForLitRender(element); expect(element).to.have.attribute('checked'); - await waitForCondition(() => stateChange.events.length === 1); + await stateChange.calledOnce(); expect(stateChange.count).to.be.equal(1); element.click(); await waitForLitRender(element); expect(element).to.have.attribute('checked'); - await waitForCondition(() => stateChange.events.length === 1); + await stateChange.calledOnce(); expect(stateChange.count).to.be.equal(1); }); @@ -75,13 +75,13 @@ describe(`sbb-radio-button`, () => { element.click(); await waitForLitRender(element); expect(element).to.have.attribute('checked'); - await waitForCondition(() => stateChange.events.length === 1); + await stateChange.calledOnce(); expect(stateChange.count).to.be.equal(1); element.click(); await waitForLitRender(element); expect(element).not.to.have.attribute('checked'); - await waitForCondition(() => stateChange.events.length === 2); + await stateChange.calledTimes(2); expect(stateChange.count).to.be.equal(2); }); diff --git a/src/elements/select/select.spec.ts b/src/elements/select/select.spec.ts index 2ff7c5e25c..fcffce68b5 100644 --- a/src/elements/select/select.spec.ts +++ b/src/elements/select/select.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture, tabKey } from '../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../core/testing.js'; +import { EventSpy, waitForLitRender } from '../core/testing.js'; import { SbbOptionElement } from '../option.js'; import { SbbSelectElement } from './select.js'; @@ -50,9 +50,9 @@ describe(`sbb-select`, () => { const didClose = new EventSpy(SbbSelectElement.events.didClose); element.dispatchEvent(new CustomEvent('click')); await waitForLitRender(element); - await waitForCondition(() => willOpen.events.length === 1); + await willOpen.calledOnce(); expect(willOpen.count).to.be.equal(1); - await waitForCondition(() => didOpen.events.length === 1); + await didOpen.calledOnce(); expect(didOpen.count).to.be.equal(1); await waitForLitRender(element); @@ -61,9 +61,9 @@ describe(`sbb-select`, () => { element.dispatchEvent(new CustomEvent('click')); await waitForLitRender(element); - await waitForCondition(() => willClose.events.length === 1); + await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); - await waitForCondition(() => didClose.events.length === 1); + await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); await waitForLitRender(element); @@ -166,9 +166,9 @@ describe(`sbb-select`, () => { const didOpen = new EventSpy(SbbSelectElement.events.didOpen); element.click(); - await waitForCondition(() => willOpen.events.length === 1); + await willOpen.calledOnce(); expect(willOpen.count).to.be.equal(1); - await waitForCondition(() => didOpen.events.length === 1); + await didOpen.calledOnce(); expect(didOpen.count).to.be.equal(1); await waitForLitRender(element); @@ -192,9 +192,9 @@ describe(`sbb-select`, () => { expect(selectionChange.count).to.be.equal(1); expect(optionSelected.count).to.be.equal(1); - await waitForCondition(() => willClose.events.length === 1); + await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); - await waitForCondition(() => didClose.events.length === 1); + await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); await waitForLitRender(element); @@ -210,9 +210,9 @@ describe(`sbb-select`, () => { const didOpen = new EventSpy(SbbSelectElement.events.didOpen); element.dispatchEvent(new CustomEvent('click')); - await waitForCondition(() => willOpen.events.length === 1); + await willOpen.calledOnce(); expect(willOpen.count).to.be.equal(1); - await waitForCondition(() => didOpen.events.length === 1); + await didOpen.calledOnce(); expect(didOpen.count).to.be.equal(1); await waitForLitRender(element); expect(firstOption).not.to.have.attribute('data-active'); @@ -251,23 +251,23 @@ describe(`sbb-select`, () => { focusableElement.focus(); await sendKeys({ press: 'Enter' }); await waitForLitRender(element); - await waitForCondition(() => didOpen.events.length === 1); + await didOpen.calledOnce(); expect(didOpen.count).to.be.equal(1); focusableElement.focus(); await sendKeys({ press: 'Escape' }); await waitForLitRender(element); - await waitForCondition(() => didClose.events.length === 1); + await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); focusableElement.focus(); await sendKeys({ press: 'ArrowDown' }); - await waitForCondition(() => didOpen.events.length === 2); + await didOpen.calledTimes(2); expect(didOpen.count).to.be.equal(2); focusableElement.focus(); await sendKeys({ press: tabKey }); - await waitForCondition(() => didClose.events.length === 2); + await didClose.calledTimes(2); expect(didClose.count).to.be.equal(2); focusableElement.focus(); @@ -291,7 +291,7 @@ describe(`sbb-select`, () => { const didOpen = new EventSpy(SbbSelectElement.events.didOpen); focusableElement.focus(); await sendKeys({ press: ' ' }); - await waitForCondition(() => didOpen.events.length === 1); + await didOpen.calledOnce(); expect(didOpen.count).to.be.equal(1); expect(firstOption).not.to.have.attribute('data-active'); expect(firstOption).not.to.have.attribute('selected'); @@ -333,7 +333,7 @@ describe(`sbb-select`, () => { const didClose = new EventSpy(SbbSelectElement.events.didClose); focusableElement.focus(); await sendKeys({ press: 'ArrowUp' }); - await waitForCondition(() => didOpen.events.length === 1); + await didOpen.calledOnce(); expect(didOpen.count).to.be.equal(1); expect(secondOption).not.to.have.attribute('data-active'); @@ -348,13 +348,13 @@ describe(`sbb-select`, () => { expect(displayValue).to.have.trimmed.text('Second'); await sendKeys({ press: 'Escape' }); - await waitForCondition(() => didClose.events.length === 1); + await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); element.focus(); await sendKeys({ press: 'ArrowDown' }); await waitForLitRender(element); - await waitForCondition(() => didOpen.events.length === 2); + await didOpen.calledTimes(2); expect(didOpen.count).to.be.equal(2); expect(secondOption).not.to.have.attribute('data-active'); expect(secondOption).to.have.attribute('selected'); @@ -377,7 +377,7 @@ describe(`sbb-select`, () => { element.addEventListener(SbbSelectElement.events.willOpen, (ev) => ev.preventDefault()); element.open(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -389,13 +389,13 @@ describe(`sbb-select`, () => { const willCloseEventSpy = new EventSpy(SbbSelectElement.events.willClose); element.open(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); await waitForLitRender(element); element.addEventListener(SbbSelectElement.events.willClose, (ev) => ev.preventDefault()); element.close(); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'opened'); diff --git a/src/elements/selection-expansion-panel/selection-expansion-panel.spec.ts b/src/elements/selection-expansion-panel/selection-expansion-panel.spec.ts index 9a252f79bb..91d85e3590 100644 --- a/src/elements/selection-expansion-panel/selection-expansion-panel.spec.ts +++ b/src/elements/selection-expansion-panel/selection-expansion-panel.spec.ts @@ -176,8 +176,8 @@ describe(`sbb-selection-expansion-panel`, () => { secondInput.click(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); + await didOpenEventSpy.calledOnce(); await waitForLitRender(wrapper); expect(willOpenEventSpy.count).to.be.equal(1); @@ -375,7 +375,7 @@ describe(`sbb-selection-expansion-panel`, () => { .then(() => Promise.reject('accidentally passed')) .catch((error) => expect(error).to.include('timeout')); - await waitForCondition(() => didOpenEventSpy.count === 1); + await didOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); expect(didOpenEventSpy.count).to.be.equal(1); expect(mainRadioButton1Label.name.trim()).to.be.equal('Main Option 1 , expanded'); @@ -386,8 +386,8 @@ describe(`sbb-selection-expansion-panel`, () => { // Activate main option 2 mainRadioButton2.click(); - await waitForCondition(() => didOpenEventSpy.count === 2); - await waitForCondition(() => didCloseEventSpy.count === 1); + await didOpenEventSpy.calledTimes(2); + await didCloseEventSpy.calledOnce(); const mainRadioButton1LabelSecondRender = (await a11ySnapshot({ selector: 'sbb-radio-button-panel[value="main1"]', @@ -440,7 +440,7 @@ describe(`sbb-selection-expansion-panel`, () => { 'sbb-radio-button[value="sub1"]', )!; - await waitForCondition(() => didOpenEventSpy.count === 1); + await didOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); expect(didOpenEventSpy.count).to.be.equal(1); expect(panel1).to.have.attribute('data-state', 'opened'); @@ -451,8 +451,8 @@ describe(`sbb-selection-expansion-panel`, () => { main2.checked = true; - await waitForCondition(() => didOpenEventSpy.count === 2); - await waitForCondition(() => didCloseEventSpy.count === 1); + await didOpenEventSpy.calledTimes(2); + await didCloseEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(2); expect(didOpenEventSpy.count).to.be.equal(2); @@ -559,7 +559,7 @@ describe(`sbb-selection-expansion-panel`, () => { it('selects input on click and shows related content', async () => { await waitForLitRender(wrapper); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); expect(didOpenEventSpy.count).to.be.equal(1); @@ -570,7 +570,7 @@ describe(`sbb-selection-expansion-panel`, () => { secondInput.click(); await waitForLitRender(wrapper); - await waitForCondition(() => didOpenEventSpy.events.length === 2); + await didOpenEventSpy.calledTimes(2); expect(willOpenEventSpy.count).to.be.equal(2); expect(didOpenEventSpy.count).to.be.equal(2); @@ -587,7 +587,7 @@ describe(`sbb-selection-expansion-panel`, () => { firstInput.click(); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); expect(didCloseEventSpy.count).to.be.equal(1); expect(firstInput.checked).to.be.false; diff --git a/src/elements/stepper/stepper/stepper.spec.ts b/src/elements/stepper/stepper/stepper.spec.ts index c418c2647b..da8212488f 100644 --- a/src/elements/stepper/stepper/stepper.spec.ts +++ b/src/elements/stepper/stepper/stepper.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture, tabKey } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbStepElement } from '../step/step.js'; import type { SbbStepLabelElement } from '../step-label.js'; @@ -65,7 +65,7 @@ describe('sbb-stepper', () => { stepLabelThree.click(); await waitForLitRender(element); - await waitForCondition(() => validate.events.length === 1); + await validate.calledOnce(); expect(validate.count).to.be.equal(1); expect(stepLabelThree).to.have.attribute('data-selected'); expect(stepLabelThree.step).to.have.attribute('data-selected'); @@ -81,7 +81,7 @@ describe('sbb-stepper', () => { element.selected = stepLabelThree.step!; await waitForLitRender(element); - await waitForCondition(() => validate.events.length === 1); + await validate.calledOnce(); expect(validate.count).to.be.equal(1); expect(stepLabelThree).to.have.attribute('data-selected'); expect(stepLabelThree.step).to.have.attribute('data-selected'); @@ -97,7 +97,7 @@ describe('sbb-stepper', () => { element.selectedIndex = 2; await waitForLitRender(element); - await waitForCondition(() => validate.events.length === 1); + await validate.calledOnce(); expect(validate.count).to.be.equal(1); expect(stepLabelThree).to.have.attribute('data-selected'); expect(stepLabelThree.step).to.have.attribute('data-selected'); @@ -115,7 +115,7 @@ describe('sbb-stepper', () => { stepperNext.click(); await waitForLitRender(element); - await waitForCondition(() => validate.events.length === 1); + await validate.calledOnce(); expect(validate.count).to.be.equal(1); expect(stepLabelTwo).to.have.attribute('data-selected'); expect(stepLabelTwo.step).to.have.attribute('data-selected'); @@ -137,7 +137,7 @@ describe('sbb-stepper', () => { stepperNext.click(); await waitForLitRender(element); - await waitForCondition(() => validate.events.length === 1); + await validate.calledOnce(); expect(validate.count).to.be.equal(1); expect(stepLabelTwo).to.have.attribute('data-selected'); expect(stepLabelTwo.step).to.have.attribute('data-selected'); @@ -181,7 +181,7 @@ describe('sbb-stepper', () => { stepperNext.click(); await waitForLitRender(element); - await waitForCondition(() => validate.events.length === 1); + await validate.calledOnce(); expect(validate.count).to.be.equal(1); expect(stepLabelTwo).to.have.attribute('data-selected'); expect(stepLabelTwo.step).to.have.attribute('data-selected'); @@ -198,7 +198,7 @@ describe('sbb-stepper', () => { stepLabelThree.click(); await waitForLitRender(element); - await waitForCondition(() => validate.events.length === 1); + await validate.calledOnce(); expect(validate.count).to.be.equal(1); expect(stepLabelThree).not.to.have.attribute('data-selected'); expect(stepLabelThree.step).not.to.have.attribute('data-selected'); diff --git a/src/elements/tabs/tab-group/tab-group.spec.ts b/src/elements/tabs/tab-group/tab-group.spec.ts index 8e707e546e..deadfabdc5 100644 --- a/src/elements/tabs/tab-group/tab-group.spec.ts +++ b/src/elements/tabs/tab-group/tab-group.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture, tabKey } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import type { SbbTabLabelElement } from '../tab-label.js'; import type { SbbTabElement } from '../tab.js'; @@ -102,7 +102,7 @@ describe(`sbb-tab-group`, () => { const changeSpy = new EventSpy(SbbTabGroupElement.events.didChange); tab.click(); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); }); diff --git a/src/elements/tag/tag-group/tag-group.spec.ts b/src/elements/tag/tag-group/tag-group.spec.ts index 85e6c54e4d..0088c1117a 100644 --- a/src/elements/tag/tag-group/tag-group.spec.ts +++ b/src/elements/tag/tag-group/tag-group.spec.ts @@ -2,7 +2,7 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { waitForCondition, waitForLitRender, EventSpy } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import type { SbbTagElement } from '../tag.js'; import { SbbTagGroupElement } from './tag-group.js'; @@ -46,9 +46,9 @@ describe(`sbb-tag-group`, () => { expect(tag1).to.have.attribute('checked'); expect(tag1.checked).to.be.equal(true); - await waitForCondition(() => inputSpy.events.length === 1); + await inputSpy.calledOnce(); expect(inputSpy.count).to.be.equal(1); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); expect(element.value).to.be.eql(['tag1']); }); @@ -111,9 +111,9 @@ describe(`sbb-tag-group`, () => { expect(tag2).not.to.have.attribute('checked'); expect(tag2.checked).to.be.equal(false); - await waitForCondition(() => inputSpy.events.length === 1); + await inputSpy.calledOnce(); expect(inputSpy.count).to.be.equal(1); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); expect(element.value).to.be.an('array').that.is.empty; }); @@ -279,9 +279,9 @@ describe(`sbb-tag-group`, () => { expect(tag1).to.have.attribute('checked'); expect(tag1.checked).to.be.equal(true); - await waitForCondition(() => inputSpy.events.length === 1); + await inputSpy.calledOnce(); expect(inputSpy.count).to.be.equal(1); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); expect(element.value).to.be.equal('tag1'); }); @@ -394,10 +394,10 @@ describe(`sbb-tag-group`, () => { expect(tag2).not.to.have.attribute('checked'); expect(tag2.checked).to.be.equal(false); - await waitForCondition(() => inputSpy.events.length === 1); + await inputSpy.calledOnce(); expect(inputSpy.count).to.be.equal(1); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); expect(element.value).to.be.equal('tag3'); @@ -419,10 +419,10 @@ describe(`sbb-tag-group`, () => { expect(tag2).not.to.have.attribute('checked'); expect(tag2.checked).to.be.equal(false); - await waitForCondition(() => inputSpy.events.length === 1); + await inputSpy.calledOnce(); expect(inputSpy.count).to.be.equal(1); - await waitForCondition(() => changeSpy.events.length === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.equal(1); expect(element.value).to.be.equal('tag1'); diff --git a/src/elements/toast/toast.spec.ts b/src/elements/toast/toast.spec.ts index 5880112f76..9ff9f98434 100644 --- a/src/elements/toast/toast.spec.ts +++ b/src/elements/toast/toast.spec.ts @@ -35,11 +35,11 @@ describe(`sbb-toast`, () => { element.open(); await waitForLitRender(element); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); expect(element.getAttribute('data-state')).to.be.equal('opened'); @@ -47,11 +47,11 @@ describe(`sbb-toast`, () => { // Will wait for timeout and then close itself await waitForLitRender(element); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -68,18 +68,18 @@ describe(`sbb-toast`, () => { element.open(); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); const dismissBtn = element.shadowRoot!.querySelector('sbb-transparent-button')!; dismissBtn.click(); await waitForLitRender(element); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -100,16 +100,16 @@ describe(`sbb-toast`, () => { element.open(); await waitForLitRender(element); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); actionBtn.click(); await waitForLitRender(element); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); - await waitForCondition(() => didCloseEventSpy.events.length === 1); + await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -173,7 +173,7 @@ describe(`sbb-toast`, () => { element.addEventListener(SbbToastElement.events.willOpen, (ev) => ev.preventDefault()); element.open(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); + await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); await waitForLitRender(element); @@ -185,13 +185,13 @@ describe(`sbb-toast`, () => { const willCloseEventSpy = new EventSpy(SbbToastElement.events.willClose); element.open(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); + await didOpenEventSpy.calledOnce(); await waitForLitRender(element); element.addEventListener(SbbToastElement.events.willClose, (ev) => ev.preventDefault()); element.close(); - await waitForCondition(() => willCloseEventSpy.events.length === 1); + await willCloseEventSpy.calledOnce(); await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'opened'); diff --git a/src/elements/toggle-check/toggle-check.spec.ts b/src/elements/toggle-check/toggle-check.spec.ts index be48ab31da..8b0df658aa 100644 --- a/src/elements/toggle-check/toggle-check.spec.ts +++ b/src/elements/toggle-check/toggle-check.spec.ts @@ -49,7 +49,7 @@ describe(`sbb-toggle-check`, () => { element.focus(); await sendKeys({ press: 'Space' }); - await waitForCondition(() => changeSpy.count === 1); + await changeSpy.calledOnce(); expect(changeSpy.count).to.be.greaterThan(0); }); }); diff --git a/src/elements/toggle/toggle-option/toggle-option.spec.ts b/src/elements/toggle/toggle-option/toggle-option.spec.ts index 3b7f6a1751..13d71853d0 100644 --- a/src/elements/toggle/toggle-option/toggle-option.spec.ts +++ b/src/elements/toggle/toggle-option/toggle-option.spec.ts @@ -2,7 +2,7 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import { SbbToggleOptionElement } from './toggle-option.js'; @@ -24,7 +24,7 @@ describe(`sbb-toggle-option`, () => { await waitForLitRender(element); expect(element).to.have.attribute('checked'); - await waitForCondition(() => onInput.events.length === 1); + await onInput.calledOnce(); expect(onInput.count).to.be.equal(1); }); @@ -35,14 +35,14 @@ describe(`sbb-toggle-option`, () => { await waitForLitRender(element); expect(element).to.have.attribute('checked'); - await waitForCondition(() => onInput.events.length === 1); + await onInput.calledOnce(); expect(onInput.count).to.be.equal(1); element.click(); await waitForLitRender(element); expect(element).to.have.attribute('checked'); - await waitForCondition(() => onInput.events.length === 1); + await onInput.calledOnce(); expect(onInput.count).to.be.equal(1); }); }); diff --git a/src/elements/toggle/toggle/toggle.spec.ts b/src/elements/toggle/toggle/toggle.spec.ts index 1507f98de9..9e0033c346 100644 --- a/src/elements/toggle/toggle/toggle.spec.ts +++ b/src/elements/toggle/toggle/toggle.spec.ts @@ -3,7 +3,7 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import type { SbbToggleOptionElement } from '../toggle-option.js'; import { SbbToggleElement } from './toggle.js'; @@ -239,8 +239,8 @@ describe(`sbb-toggle`, () => { secondOption.click(); await waitForLitRender(firstOption); - await waitForCondition(() => changeSpy.events.length === 1); - await waitForCondition(() => inputSpy.events.length === 1); + await changeSpy.calledOnce(); + await inputSpy.calledOnce(); expect(valueInEvent).to.equal('Value two'); // Checking value in events of EventSpy is too late to check the real use case, @@ -253,8 +253,8 @@ describe(`sbb-toggle`, () => { firstOption.click(); await waitForLitRender(firstOption); - await waitForCondition(() => changeSpy.events.length === 2); - await waitForCondition(() => inputSpy.events.length === 2); + await changeSpy.calledTimes(2); + await inputSpy.calledTimes(2); expect(firstOption).to.have.attribute('checked'); expect(valueInEvent).to.equal('Value one'); @@ -287,8 +287,8 @@ describe(`sbb-toggle`, () => { await waitForLitRender(element); expect(secondOption).to.have.attribute('checked'); - await waitForCondition(() => changeSpy.events.length === 1); - await waitForCondition(() => inputSpy.events.length === 1); + await changeSpy.calledOnce(); + await inputSpy.calledOnce(); firstOption.click(); await waitForLitRender(firstOption); @@ -306,8 +306,8 @@ describe(`sbb-toggle`, () => { await waitForLitRender(element); expect(secondOption).to.have.attribute('checked'); - await waitForCondition(() => changeSpy.events.length === 1); - await waitForCondition(() => inputSpy.events.length === 1); + await changeSpy.calledOnce(); + await inputSpy.calledOnce(); firstOption.click(); await waitForLitRender(firstOption); diff --git a/src/elements/train/train-wagon/train-wagon.spec.ts b/src/elements/train/train-wagon/train-wagon.spec.ts index 7cd964caf7..74c5ca9afc 100644 --- a/src/elements/train/train-wagon/train-wagon.spec.ts +++ b/src/elements/train/train-wagon/train-wagon.spec.ts @@ -2,7 +2,7 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; import type { SbbIconElement } from '../../icon.js'; import { SbbTrainWagonElement } from './train-wagon.js'; @@ -71,7 +71,7 @@ describe(`sbb-train-wagon`, () => { const sectorChangeSpy = new EventSpy(SbbTrainWagonElement.events.sectorChange); element.sector = 'B'; - await waitForCondition(() => sectorChangeSpy.events.length === 1); + await sectorChangeSpy.calledOnce(); expect(sectorChangeSpy.count).to.be.greaterThan(0); }); From 39fa5656742dd6f75c9be68994dcd19806142121 Mon Sep 17 00:00:00 2001 From: Lukas Spirig Date: Mon, 18 Nov 2024 13:32:51 +0100 Subject: [PATCH 02/14] refactor: add SbbMediaMatcherController (#3205) This adds the controller `SbbMediaMatcherController` which allows tracking media query matches. --- src/elements/core/controllers.ts | 1 + .../controllers/media-matchers-controller.ts | 95 +++++++++++++++++++ .../expansion-panel-header.ts | 14 ++- src/elements/menu/menu/menu.ts | 22 ++++- src/elements/popover/popover/popover.ts | 24 ++--- .../timetable-occupancy-icon.ts | 22 +++-- 6 files changed, 148 insertions(+), 30 deletions(-) create mode 100644 src/elements/core/controllers/media-matchers-controller.ts diff --git a/src/elements/core/controllers.ts b/src/elements/core/controllers.ts index 378101653a..4b1b37b710 100644 --- a/src/elements/core/controllers.ts +++ b/src/elements/core/controllers.ts @@ -1,4 +1,5 @@ export * from './controllers/connected-abort-controller.js'; export * from './controllers/inert-controller.js'; export * from './controllers/language-controller.js'; +export * from './controllers/media-matchers-controller.js'; export * from './controllers/slot-state-controller.js'; diff --git a/src/elements/core/controllers/media-matchers-controller.ts b/src/elements/core/controllers/media-matchers-controller.ts new file mode 100644 index 0000000000..f683f88c86 --- /dev/null +++ b/src/elements/core/controllers/media-matchers-controller.ts @@ -0,0 +1,95 @@ +import { SbbBreakpointMediumMin, SbbBreakpointSmallMax } from '@sbb-esta/lyne-design-tokens'; +import { isServer, type ReactiveController, type ReactiveControllerHost } from 'lit'; + +/* eslint-disable @typescript-eslint/naming-convention */ +export const SbbMediaQueryForcedColors = '(forced-colors: active)'; +export const SbbMediaQueryHover = '(any-hover: hover)'; +export const SbbMediaQueryPointerCoarse = '(pointer: coarse)'; +export const SbbMediaQueryBreakpointMediumAndAbove = `(min-width: ${SbbBreakpointMediumMin})`; +export const SbbMediaQueryBreakpointSmallAndBelow = `(max-width: ${SbbBreakpointSmallMax})`; +/* eslint-enable @typescript-eslint/naming-convention */ + +/** + * A callback, which is invoked when the associated media query match + * status changes. + */ +export type SbbMediaMatcherHandler = (matches: boolean) => void; + +interface MediaQueryEntry { + mediaQueryList: MediaQueryList; + eventHandler: (event: MediaQueryListEvent) => void; + handlers: Set; +} + +/** + * We want to cache MediaQueryList instances and corresponding + * event handlers, as a multitude of event handlers on global objects + * can degrade performance with time. + */ +const mediaQueryRegistry = new Map(); + +/** + * This controller allows listening to media query changes. + * + * @example + * new SbbMediaMatcherController(this, { + * [SbbForcedColorsQuery]: (matches) => doSomething(matches), + * }) + */ +export class SbbMediaMatcherController implements ReactiveController { + public constructor( + host: ReactiveControllerHost, + private _queries: Record, + ) { + host.addController(this); + } + + /** + * Returns whether the given query matches. Returns null with SSR. + * @param query The query to check against. + * @returns Whether the query matches or null with SSR. + */ + public matches(query: string): boolean | null { + if (isServer) { + return null; + } + const mediaQuery = mediaQueryRegistry.get(query); + if (mediaQuery) { + return mediaQuery.mediaQueryList.matches; + } else { + return matchMedia(query).matches; + } + } + + public hostConnected(): void { + if (isServer) { + return; + } + + for (const [query, handler] of Object.entries(this._queries)) { + const mediaQuery = mediaQueryRegistry.get(query); + if (mediaQuery) { + mediaQuery.handlers.add(handler); + } else { + const mediaQueryList = matchMedia(query); + const handlers = new Set([handler]); + const eventHandler = (e: MediaQueryListEvent): void => + handlers.forEach((h) => h(e.matches)); + mediaQueryList.addEventListener('change', eventHandler); + mediaQueryRegistry.set(query, { mediaQueryList, handlers, eventHandler }); + } + } + } + + public hostDisconnected(): void { + for (const [query, handler] of Object.entries(this._queries)) { + const mediaQuery = mediaQueryRegistry.get(query); + if (mediaQuery) { + mediaQuery.handlers.delete(handler); + if (!mediaQuery.handlers.size) { + mediaQueryRegistry.delete(query); + } + } + } + } +} diff --git a/src/elements/expansion-panel/expansion-panel-header/expansion-panel-header.ts b/src/elements/expansion-panel/expansion-panel-header/expansion-panel-header.ts index 60ad3728d2..4a7e710ec8 100644 --- a/src/elements/expansion-panel/expansion-panel-header/expansion-panel-header.ts +++ b/src/elements/expansion-panel/expansion-panel-header/expansion-panel-header.ts @@ -2,7 +2,12 @@ import { type CSSResultGroup, html, nothing, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators.js'; import { SbbButtonBaseElement } from '../../core/base-elements.js'; -import { SbbConnectedAbortController, SbbSlotStateController } from '../../core/controllers.js'; +import { + SbbMediaQueryHover, + SbbConnectedAbortController, + SbbMediaMatcherController, + SbbSlotStateController, +} from '../../core/controllers.js'; import { hostAttributes } from '../../core/decorators.js'; import { EventEmitter } from '../../core/eventing.js'; import { SbbDisabledTabIndexActionMixin } from '../../core/mixins.js'; @@ -41,6 +46,11 @@ class SbbExpansionPanelHeaderElement extends SbbDisabledTabIndexActionMixin( ); private _abort = new SbbConnectedAbortController(this); private _namedSlots = new SbbSlotStateController(this, () => this._setDataIconAttribute()); + private _mediaMatcher = new SbbMediaMatcherController(this, { + [SbbMediaQueryHover]: (m) => (this._isHover = m), + }); + + private _isHover: boolean = this._mediaMatcher.matches(SbbMediaQueryHover) ?? false; public override connectedCallback(): void { super.connectedCallback(); @@ -59,7 +69,7 @@ class SbbExpansionPanelHeaderElement extends SbbDisabledTabIndexActionMixin( private _onMouseMovement(toggleDataAttribute: boolean): void { const parent: SbbExpansionPanelElement = this.closest('sbb-expansion-panel')!; // The `sbb.hover-mq` logic has been removed from scss, but it must be replicated to have the correct behavior on mobile. - if (!toggleDataAttribute || (parent && window.matchMedia('(any-hover: hover)').matches)) { + if (!toggleDataAttribute || (parent && this._isHover)) { parent.toggleAttribute('data-toggle-hover', toggleDataAttribute); } } diff --git a/src/elements/menu/menu/menu.ts b/src/elements/menu/menu/menu.ts index 99aea42982..8819469dcf 100644 --- a/src/elements/menu/menu/menu.ts +++ b/src/elements/menu/menu/menu.ts @@ -11,9 +11,14 @@ import { setModalityOnNextFocus, } from '../../core/a11y.js'; import { SbbOpenCloseBaseElement } from '../../core/base-elements.js'; -import { SbbConnectedAbortController, SbbInertController } from '../../core/controllers.js'; +import { + SbbMediaQueryBreakpointSmallAndBelow, + SbbConnectedAbortController, + SbbInertController, + SbbMediaMatcherController, +} from '../../core/controllers.js'; import { forceType } from '../../core/decorators.js'; -import { findReferencedElement, isBreakpoint, SbbScrollHandler } from '../../core/dom.js'; +import { findReferencedElement, SbbScrollHandler } from '../../core/dom.js'; import { SbbNamedSlotListMixin } from '../../core/mixins.js'; import { getElementPosition, @@ -93,6 +98,15 @@ class SbbMenuElement extends SbbNamedSlotListMixin< private _focusHandler = new SbbFocusHandler(); private _scrollHandler = new SbbScrollHandler(); private _inertController = new SbbInertController(this); + private _mediaMatcher = new SbbMediaMatcherController(this, { + [SbbMediaQueryBreakpointSmallAndBelow]: (matches) => { + if (matches && (this.state === 'opening' || this.state === 'opened')) { + this._scrollHandler.disableScroll(); + } else { + this._scrollHandler.enableScroll(); + } + }, + }); /** * Opens the menu on trigger click. @@ -111,7 +125,7 @@ class SbbMenuElement extends SbbNamedSlotListMixin< this._triggerElement?.setAttribute('aria-expanded', 'true'); // Starting from breakpoint medium, disable scroll - if (!isBreakpoint('medium')) { + if (this._mediaMatcher.matches(SbbMediaQueryBreakpointSmallAndBelow)) { this._scrollHandler.disableScroll(); } } @@ -338,7 +352,7 @@ class SbbMenuElement extends SbbNamedSlotListMixin< private _setMenuPosition(): void { // Starting from breakpoint medium if ( - !isBreakpoint('medium') || + (this._mediaMatcher.matches(SbbMediaQueryBreakpointSmallAndBelow) ?? true) || !this._menu || !this._triggerElement || this.state === 'closing' diff --git a/src/elements/popover/popover/popover.ts b/src/elements/popover/popover/popover.ts index 66a7594edd..5e8e7726d1 100644 --- a/src/elements/popover/popover/popover.ts +++ b/src/elements/popover/popover/popover.ts @@ -10,7 +10,7 @@ import { setModalityOnNextFocus, } from '../../core/a11y.js'; import { SbbOpenCloseBaseElement } from '../../core/base-elements.js'; -import { SbbLanguageController } from '../../core/controllers.js'; +import { SbbMediaQueryPointerCoarse, SbbLanguageController } from '../../core/controllers.js'; import { forceType } from '../../core/decorators.js'; import { findReferencedElement } from '../../core/dom.js'; import { composedPathHasAttribute, EventEmitter } from '../../core/eventing.js'; @@ -34,6 +34,7 @@ const HORIZONTAL_OFFSET = 32; let nextId = 0; const popoversRef = new Set(); +const pointerCoarse = isServer ? false : matchMedia(SbbMediaQueryPointerCoarse).matches; /** * It displays contextual information within a popover. @@ -234,18 +235,13 @@ class SbbPopoverElement extends SbbHydrationMixin(SbbOpenCloseBaseElement) { // Check whether the trigger can be hovered. Some devices might interpret the media query (hover: hover) differently, // and not respect the fallback mechanism on the click. Therefore, the following is preferred to identify // all non-touchscreen devices. - this._hoverTrigger = this.hoverTrigger && !window.matchMedia('(pointer: coarse)').matches; + this._hoverTrigger = this.hoverTrigger && !pointerCoarse; this._popoverController?.abort(); - this._popoverController = new AbortController(); + const { signal } = (this._popoverController = new AbortController()); if (this._hoverTrigger) { - this._triggerElement.addEventListener('mouseenter', this._onTriggerMouseEnter, { - signal: this._popoverController.signal, - }); - - this._triggerElement.addEventListener('mouseleave', this._onTriggerMouseLeave, { - signal: this._popoverController.signal, - }); + this._triggerElement.addEventListener('mouseenter', this._onTriggerMouseEnter, { signal }); + this._triggerElement.addEventListener('mouseleave', this._onTriggerMouseLeave, { signal }); this._triggerElement.addEventListener( 'keydown', @@ -254,9 +250,7 @@ class SbbPopoverElement extends SbbHydrationMixin(SbbOpenCloseBaseElement) { this.open(); } }, - { - signal: this._popoverController.signal, - }, + { signal }, ); } else { this._triggerElement.addEventListener( @@ -266,9 +260,7 @@ class SbbPopoverElement extends SbbHydrationMixin(SbbOpenCloseBaseElement) { this.open(); } }, - { - signal: this._popoverController.signal, - }, + { signal }, ); } } diff --git a/src/elements/timetable-occupancy-icon/timetable-occupancy-icon.ts b/src/elements/timetable-occupancy-icon/timetable-occupancy-icon.ts index e77a7f2309..82d6fe9bad 100644 --- a/src/elements/timetable-occupancy-icon/timetable-occupancy-icon.ts +++ b/src/elements/timetable-occupancy-icon/timetable-occupancy-icon.ts @@ -1,7 +1,11 @@ import type { CSSResultGroup, PropertyValues } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import { SbbConnectedAbortController, SbbLanguageController } from '../core/controllers.js'; +import { + SbbMediaQueryForcedColors, + SbbLanguageController, + SbbMediaMatcherController, +} from '../core/controllers.js'; import { setOrRemoveAttribute } from '../core/dom.js'; import { i18nOccupancy } from '../core/i18n.js'; import type { SbbOccupancy } from '../core/interfaces.js'; @@ -21,8 +25,15 @@ class SbbTimetableOccupancyIconElement extends SbbNegativeMixin(SbbIconBase) { /** Wagon occupancy. */ @property() public accessor occupancy: SbbOccupancy = 'none'; - private _abort = new SbbConnectedAbortController(this); private _language = new SbbLanguageController(this).withHandler(() => this._setAriaLabel()); + private _mediaMatcher = new SbbMediaMatcherController(this, { + [SbbMediaQueryForcedColors]: (matches) => { + this._forcedColors = matches; + this._setNameAndAriaLabel(); + }, + }); + + private _forcedColors: boolean = this._mediaMatcher.matches(SbbMediaQueryForcedColors) ?? false; private async _setNameAndAriaLabel(): Promise { if (!this.occupancy) { @@ -30,7 +41,7 @@ class SbbTimetableOccupancyIconElement extends SbbNegativeMixin(SbbIconBase) { } let icon = `utilization-${this.occupancy}`; - if (globalThis.window?.matchMedia('(forced-colors: active)').matches) { + if (this._forcedColors) { icon += '-high-contrast'; } else if (this.negative) { icon += '-negative'; @@ -53,11 +64,6 @@ class SbbTimetableOccupancyIconElement extends SbbNegativeMixin(SbbIconBase) { public override connectedCallback(): void { super.connectedCallback(); - window - .matchMedia('(forced-colors: active)') - .addEventListener('change', () => this._setNameAndAriaLabel(), { - signal: this._abort.signal, - }); this._setNameAndAriaLabel(); } From dba0322d6e9be579d993f4eb138f0deb2c624adc Mon Sep 17 00:00:00 2001 From: Jeri Peier Date: Mon, 18 Nov 2024 15:07:46 +0100 Subject: [PATCH 03/14] test: remove obsolete waitForEvent() method (#3210) BREAKING CHANGE: waitForEvent() method was removed in favor of using EventSpy class --- src/elements/core/testing.ts | 1 - src/elements/core/testing/wait-for-event.ts | 19 ------------------- .../navigation/navigation/navigation.spec.ts | 6 ++++-- 3 files changed, 4 insertions(+), 22 deletions(-) delete mode 100644 src/elements/core/testing/wait-for-event.ts diff --git a/src/elements/core/testing.ts b/src/elements/core/testing.ts index 11960275c9..701f53e83d 100644 --- a/src/elements/core/testing.ts +++ b/src/elements/core/testing.ts @@ -2,6 +2,5 @@ export * from './testing/event-spy.js'; export * from './testing/mocha-extensions.js'; export * from './testing/scroll.js'; export * from './testing/wait-for-condition.js'; -export * from './testing/wait-for-event.js'; export * from './testing/wait-for-image-ready.js'; export * from './testing/wait-for-render.js'; diff --git a/src/elements/core/testing/wait-for-event.ts b/src/elements/core/testing/wait-for-event.ts deleted file mode 100644 index c40dc872f4..0000000000 --- a/src/elements/core/testing/wait-for-event.ts +++ /dev/null @@ -1,19 +0,0 @@ -export function waitForEvent( - element: HTMLElement, - eventName: string, - timeout = 1000, -): Promise { - return new Promise((resolve, reject) => { - const signal = AbortSignal.timeout(timeout); - const timeoutReached = (): void => reject(`Timeout of ${timeout} reached`); - signal.addEventListener('abort', timeoutReached); - element.addEventListener( - eventName, - () => { - signal.removeEventListener('abort', timeoutReached); - resolve(); - }, - { passive: true, signal }, - ); - }); -} diff --git a/src/elements/navigation/navigation/navigation.spec.ts b/src/elements/navigation/navigation/navigation.spec.ts index 40b2f44152..d0340af8db 100644 --- a/src/elements/navigation/navigation/navigation.spec.ts +++ b/src/elements/navigation/navigation/navigation.spec.ts @@ -5,7 +5,7 @@ import { html } from 'lit/static-html.js'; import type { SbbButtonElement } from '../../button.js'; import { pageScrollDisabled } from '../../core/dom.js'; import { fixture, tabKey } from '../../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForEvent, waitForLitRender } from '../../core/testing.js'; +import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; import type { SbbNavigationButtonElement } from '../navigation-button.js'; import type { SbbNavigationSectionElement } from '../navigation-section.js'; @@ -524,8 +524,10 @@ describe(`sbb-navigation`, () => { }); it('should re-enable scrolling when removed from the DOM', async () => { + const didOpenEventSpy = new EventSpy(SbbNavigationElement.events.didOpen); + element.open(); - await waitForEvent(element, SbbNavigationElement.events.didOpen); + await didOpenEventSpy.calledOnce(); expect(pageScrollDisabled()).to.be.true; From f98732bd0552ba3dee9450255560650a283b3a87 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:09:14 +0000 Subject: [PATCH 04/14] chore(deps): update dependency husky to v9.1.7 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 723a88abe3..3c00c9bb89 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "eslint-plugin-yml": "1.15.0", "glob": "11.0.0", "globals": "15.12.0", - "husky": "9.1.6", + "husky": "9.1.7", "lint-staged": "15.2.10", "lit-analyzer": "2.0.3", "madge": "8.0.0", diff --git a/yarn.lock b/yarn.lock index 3ff8887201..5b72d36ec1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5004,10 +5004,10 @@ human-signals@^5.0.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== -husky@9.1.6: - version "9.1.6" - resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.6.tgz#e23aa996b6203ab33534bdc82306b0cf2cb07d6c" - integrity sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A== +husky@9.1.7: + version "9.1.7" + resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.7.tgz#d46a38035d101b46a70456a850ff4201344c0b2d" + integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA== iconv-lite@0.4.24: version "0.4.24" From 828236f4d28255e97f26e4c3e2d13550add6a483 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 20:00:57 +0000 Subject: [PATCH 05/14] chore(deps): update dependency typescript-eslint to v8.15.0 --- package.json | 2 +- yarn.lock | 95 ++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 3c00c9bb89..9aa66c91f2 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "ts-lit-plugin": "2.0.2", "tslib": "2.8.1", "typescript": "5.6.3", - "typescript-eslint": "8.14.0", + "typescript-eslint": "8.15.0", "urlpattern-polyfill": "10.0.0", "vite": "5.4.11", "vite-plugin-dts": "4.3.0" diff --git a/yarn.lock b/yarn.lock index 5b72d36ec1..efcfe5a79e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2041,6 +2041,21 @@ natural-compare "^1.4.0" ts-api-utils "^1.3.0" +"@typescript-eslint/eslint-plugin@8.15.0": + version "8.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz#c95c6521e70c8b095a684d884d96c0c1c63747d2" + integrity sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.15.0" + "@typescript-eslint/type-utils" "8.15.0" + "@typescript-eslint/utils" "8.15.0" + "@typescript-eslint/visitor-keys" "8.15.0" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + ts-api-utils "^1.3.0" + "@typescript-eslint/parser@8.14.0": version "8.14.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.14.0.tgz#0a7e9dbc11bc07716ab2d7b1226217e9f6b51fc8" @@ -2052,6 +2067,17 @@ "@typescript-eslint/visitor-keys" "8.14.0" debug "^4.3.4" +"@typescript-eslint/parser@8.15.0": + version "8.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.15.0.tgz#92610da2b3af702cfbc02a46e2a2daa6260a9045" + integrity sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A== + dependencies: + "@typescript-eslint/scope-manager" "8.15.0" + "@typescript-eslint/types" "8.15.0" + "@typescript-eslint/typescript-estree" "8.15.0" + "@typescript-eslint/visitor-keys" "8.15.0" + debug "^4.3.4" + "@typescript-eslint/scope-manager@8.14.0": version "8.14.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz#01f37c147a735cd78f0ff355e033b9457da1f373" @@ -2060,6 +2086,14 @@ "@typescript-eslint/types" "8.14.0" "@typescript-eslint/visitor-keys" "8.14.0" +"@typescript-eslint/scope-manager@8.15.0": + version "8.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz#28a1a0f13038f382424f45a988961acaca38f7c6" + integrity sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA== + dependencies: + "@typescript-eslint/types" "8.15.0" + "@typescript-eslint/visitor-keys" "8.15.0" + "@typescript-eslint/type-utils@8.14.0": version "8.14.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.14.0.tgz#455c6af30c336b24a1af28bc4f81b8dd5d74d94d" @@ -2070,6 +2104,16 @@ debug "^4.3.4" ts-api-utils "^1.3.0" +"@typescript-eslint/type-utils@8.15.0": + version "8.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz#a6da0f93aef879a68cc66c73fe42256cb7426c72" + integrity sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw== + dependencies: + "@typescript-eslint/typescript-estree" "8.15.0" + "@typescript-eslint/utils" "8.15.0" + debug "^4.3.4" + ts-api-utils "^1.3.0" + "@typescript-eslint/types@7.18.0": version "7.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.18.0.tgz#b90a57ccdea71797ffffa0321e744f379ec838c9" @@ -2080,6 +2124,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.14.0.tgz#0d33d8d0b08479c424e7d654855fddf2c71e4021" integrity sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g== +"@typescript-eslint/types@8.15.0": + version "8.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.15.0.tgz#4958edf3d83e97f77005f794452e595aaf6430fc" + integrity sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ== + "@typescript-eslint/typescript-estree@8.14.0": version "8.14.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz#a7a3a5a53a6c09313e12fb4531d4ff582ee3c312" @@ -2094,6 +2143,20 @@ semver "^7.6.0" ts-api-utils "^1.3.0" +"@typescript-eslint/typescript-estree@8.15.0": + version "8.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz#915c94e387892b114a2a2cc0df2d7f19412c8ba7" + integrity sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg== + dependencies: + "@typescript-eslint/types" "8.15.0" + "@typescript-eslint/visitor-keys" "8.15.0" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" + "@typescript-eslint/typescript-estree@^7.6.0": version "7.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz#b5868d486c51ce8f312309ba79bdb9f331b37931" @@ -2118,6 +2181,16 @@ "@typescript-eslint/types" "8.14.0" "@typescript-eslint/typescript-estree" "8.14.0" +"@typescript-eslint/utils@8.15.0": + version "8.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.15.0.tgz#ac04679ad19252776b38b81954b8e5a65567cef6" + integrity sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "8.15.0" + "@typescript-eslint/types" "8.15.0" + "@typescript-eslint/typescript-estree" "8.15.0" + "@typescript-eslint/visitor-keys@7.18.0": version "7.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz#0564629b6124d67607378d0f0332a0495b25e7d7" @@ -2134,6 +2207,14 @@ "@typescript-eslint/types" "8.14.0" eslint-visitor-keys "^3.4.3" +"@typescript-eslint/visitor-keys@8.15.0": + version "8.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz#9ea5a85eb25401d2aa74ec8a478af4e97899ea12" + integrity sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q== + dependencies: + "@typescript-eslint/types" "8.15.0" + eslint-visitor-keys "^4.2.0" + "@vitest/expect@2.0.5": version "2.0.5" resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.0.5.tgz#f3745a6a2c18acbea4d39f5935e913f40d26fa86" @@ -8562,14 +8643,14 @@ typed-query-selector@^2.12.0: resolved "https://registry.yarnpkg.com/typed-query-selector/-/typed-query-selector-2.12.0.tgz#92b65dbc0a42655fccf4aeb1a08b1dddce8af5f2" integrity sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg== -typescript-eslint@8.14.0: - version "8.14.0" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.14.0.tgz#2435c0628e90303544fdd63ae311e9bf6d149a5d" - integrity sha512-K8fBJHxVL3kxMmwByvz8hNdBJ8a0YqKzKDX6jRlrjMuNXyd5T2V02HIq37+OiWXvUUOXgOOGiSSOh26Mh8pC3w== +typescript-eslint@8.15.0: + version "8.15.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.15.0.tgz#c8a2a0d183c3eb48ae176aa078c1b9daa584cf9d" + integrity sha512-wY4FRGl0ZI+ZU4Jo/yjdBu0lVTSML58pu6PgGtJmCufvzfV565pUF6iACQt092uFOd49iLOTX/sEVmHtbSrS+w== dependencies: - "@typescript-eslint/eslint-plugin" "8.14.0" - "@typescript-eslint/parser" "8.14.0" - "@typescript-eslint/utils" "8.14.0" + "@typescript-eslint/eslint-plugin" "8.15.0" + "@typescript-eslint/parser" "8.15.0" + "@typescript-eslint/utils" "8.15.0" typescript@5.4.2: version "5.4.2" From 212992e971f80f52a0ce98a0ae975281b55751f3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 08:55:24 +0100 Subject: [PATCH 06/14] chore(deps): update eslint (main) (#3207) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 6 +-- yarn.lock | 122 ++++++++++++++++++--------------------------------- 2 files changed, 45 insertions(+), 83 deletions(-) diff --git a/package.json b/package.json index 9aa66c91f2..265b258844 100644 --- a/package.json +++ b/package.json @@ -101,8 +101,8 @@ "@types/mocha": "10.0.9", "@types/node": "20.17.6", "@types/react": "18.3.12", - "@typescript-eslint/eslint-plugin": "8.14.0", - "@typescript-eslint/parser": "8.14.0", + "@typescript-eslint/eslint-plugin": "8.15.0", + "@typescript-eslint/parser": "8.15.0", "@web/test-runner": "0.19.0", "@web/test-runner-commands": "0.9.0", "@web/test-runner-playwright": "0.11.0", @@ -111,7 +111,7 @@ "custom-elements-manifest": "2.1.0", "date-fns": "4.1.0", "esbuild": "0.24.0", - "eslint": "9.14.0", + "eslint": "9.15.0", "eslint-config-prettier": "9.1.0", "eslint-import-resolver-typescript": "3.6.3", "eslint-plugin-import-x": "4.4.2", diff --git a/yarn.lock b/yarn.lock index efcfe5a79e..dfd56106c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -519,21 +519,21 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== -"@eslint/config-array@^0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.18.0.tgz#37d8fe656e0d5e3dbaea7758ea56540867fd074d" - integrity sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw== +"@eslint/config-array@^0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.0.tgz#3251a528998de914d59bb21ba4c11767cf1b3519" + integrity sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ== dependencies: "@eslint/object-schema" "^2.1.4" debug "^4.3.1" minimatch "^3.1.2" -"@eslint/core@^0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.7.0.tgz#a1bb4b6a4e742a5ff1894b7ee76fbf884ec72bd3" - integrity sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw== +"@eslint/core@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.9.0.tgz#168ee076f94b152c01ca416c3e5cf82290ab4fcd" + integrity sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg== -"@eslint/eslintrc@3.2.0", "@eslint/eslintrc@^3.1.0": +"@eslint/eslintrc@3.2.0", "@eslint/eslintrc@^3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.2.0.tgz#57470ac4e2e283a6bf76044d63281196e370542c" integrity sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w== @@ -548,11 +548,6 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.14.0": - version "9.14.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.14.0.tgz#2347a871042ebd11a00fd8c2d3d56a265ee6857e" - integrity sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg== - "@eslint/js@9.15.0": version "9.15.0" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.15.0.tgz#df0e24fe869143b59731942128c19938fdbadfb5" @@ -563,7 +558,7 @@ resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.4.tgz#9e69f8bb4031e11df79e03db09f9dbbae1740843" integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ== -"@eslint/plugin-kit@^0.2.0": +"@eslint/plugin-kit@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz#812980a6a41ecf3a8341719f92a6d1e784a2e0e8" integrity sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA== @@ -610,7 +605,7 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a" integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== -"@humanwhocodes/retry@^0.4.0": +"@humanwhocodes/retry@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.1.tgz#9a96ce501bc62df46c4031fbd970e3cc6b10f07b" integrity sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA== @@ -2026,21 +2021,6 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.14.0.tgz#7dc0e419c87beadc8f554bf5a42e5009ed3748dc" - integrity sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w== - dependencies: - "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.14.0" - "@typescript-eslint/type-utils" "8.14.0" - "@typescript-eslint/utils" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" - graphemer "^1.4.0" - ignore "^5.3.1" - natural-compare "^1.4.0" - ts-api-utils "^1.3.0" - "@typescript-eslint/eslint-plugin@8.15.0": version "8.15.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz#c95c6521e70c8b095a684d884d96c0c1c63747d2" @@ -2056,17 +2036,6 @@ natural-compare "^1.4.0" ts-api-utils "^1.3.0" -"@typescript-eslint/parser@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.14.0.tgz#0a7e9dbc11bc07716ab2d7b1226217e9f6b51fc8" - integrity sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA== - dependencies: - "@typescript-eslint/scope-manager" "8.14.0" - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/typescript-estree" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" - debug "^4.3.4" - "@typescript-eslint/parser@8.15.0": version "8.15.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.15.0.tgz#92610da2b3af702cfbc02a46e2a2daa6260a9045" @@ -2094,16 +2063,6 @@ "@typescript-eslint/types" "8.15.0" "@typescript-eslint/visitor-keys" "8.15.0" -"@typescript-eslint/type-utils@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.14.0.tgz#455c6af30c336b24a1af28bc4f81b8dd5d74d94d" - integrity sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ== - dependencies: - "@typescript-eslint/typescript-estree" "8.14.0" - "@typescript-eslint/utils" "8.14.0" - debug "^4.3.4" - ts-api-utils "^1.3.0" - "@typescript-eslint/type-utils@8.15.0": version "8.15.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz#a6da0f93aef879a68cc66c73fe42256cb7426c72" @@ -2171,16 +2130,6 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@8.14.0", "@typescript-eslint/utils@^8.1.0", "@typescript-eslint/utils@^8.8.1": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.14.0.tgz#ac2506875e03aba24e602364e43b2dfa45529dbd" - integrity sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.14.0" - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/typescript-estree" "8.14.0" - "@typescript-eslint/utils@8.15.0": version "8.15.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.15.0.tgz#ac04679ad19252776b38b81954b8e5a65567cef6" @@ -2191,6 +2140,16 @@ "@typescript-eslint/types" "8.15.0" "@typescript-eslint/typescript-estree" "8.15.0" +"@typescript-eslint/utils@^8.1.0", "@typescript-eslint/utils@^8.8.1": + version "8.14.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.14.0.tgz#ac2506875e03aba24e602364e43b2dfa45529dbd" + integrity sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "8.14.0" + "@typescript-eslint/types" "8.14.0" + "@typescript-eslint/typescript-estree" "8.14.0" + "@typescript-eslint/visitor-keys@7.18.0": version "7.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz#0564629b6124d67607378d0f0332a0495b25e7d7" @@ -3549,7 +3508,7 @@ cosmiconfig@^9.0.0: js-yaml "^4.1.0" parse-json "^5.2.0" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.3: version "7.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82" integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug== @@ -3558,6 +3517,15 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +cross-spawn@^7.0.5: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + css-functions-list@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.3.tgz#95652b0c24f0f59b291a9fc386041a19d4f40dbe" @@ -4360,26 +4328,26 @@ eslint-visitor-keys@^4.2.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== -eslint@9.14.0: - version "9.14.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.14.0.tgz#534180a97c00af08bcf2b60b0ebf0c4d6c1b2c95" - integrity sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g== +eslint@9.15.0: + version "9.15.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.15.0.tgz#77c684a4e980e82135ebff8ee8f0a9106ce6b8a6" + integrity sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.12.1" - "@eslint/config-array" "^0.18.0" - "@eslint/core" "^0.7.0" - "@eslint/eslintrc" "^3.1.0" - "@eslint/js" "9.14.0" - "@eslint/plugin-kit" "^0.2.0" + "@eslint/config-array" "^0.19.0" + "@eslint/core" "^0.9.0" + "@eslint/eslintrc" "^3.2.0" + "@eslint/js" "9.15.0" + "@eslint/plugin-kit" "^0.2.3" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" - "@humanwhocodes/retry" "^0.4.0" + "@humanwhocodes/retry" "^0.4.1" "@types/estree" "^1.0.6" "@types/json-schema" "^7.0.15" ajv "^6.12.4" chalk "^4.0.0" - cross-spawn "^7.0.2" + cross-spawn "^7.0.5" debug "^4.3.2" escape-string-regexp "^4.0.0" eslint-scope "^8.2.0" @@ -4399,7 +4367,6 @@ eslint@9.14.0: minimatch "^3.1.2" natural-compare "^1.4.0" optionator "^0.9.3" - text-table "^0.2.0" espree@^10.0.1, espree@^10.3.0: version "10.3.0" @@ -8435,11 +8402,6 @@ text-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-2.4.0.tgz#a1cfcc50cf34da41bfd047cc744f804d1680ea34" integrity sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g== -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - "through@>=2.2.7 <3", through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" From e113c6a5405a5caa3b79cdc3aa5e8c889943b63a Mon Sep 17 00:00:00 2001 From: Tommaso Menga Date: Tue, 19 Nov 2024 12:07:00 +0100 Subject: [PATCH 07/14] feat(sbb-radio-button, sbb-radio-button-panel): implement native form support (#3160) BREAKING CHANGE: Removed `SbbRadioButtonGroupEventDetail` from the `change`, `input` and `didChange` events of the `sbb-radio-button-group`. As an alternative to `event.detail.value` use `radioButtonGroup.value` --- .../autocomplete-grid.snapshot.spec.snap.js | 56 +- src/elements/core/mixins.ts | 1 + .../core/mixins/form-associated-mixin.ts | 2 +- .../form-associated-radio-button-mixin.ts | 315 +++++++++ .../common/radio-button-common.scss | 4 +- .../common/radio-button-common.spec.ts | 635 ++++++++++++++++++ .../common/radio-button-common.ts | 130 ++-- .../radio-button-group.snapshot.spec.snap.js | 65 +- .../radio-button-group.snapshot.spec.ts | 9 +- .../radio-button-group.spec.ts | 331 +++++---- .../radio-button-group.stories.ts | 8 + .../radio-button-group/radio-button-group.ts | 219 ++---- .../radio-button/radio-button-group/readme.md | 32 +- .../radio-button-panel.snapshot.spec.snap.js | 298 +++++++- .../radio-button-panel.snapshot.spec.ts | 25 +- .../radio-button-panel.spec.ts | 11 +- .../radio-button-panel.ssr.spec.ts | 29 +- .../radio-button-panel.stories.ts | 43 +- .../radio-button-panel/radio-button-panel.ts | 48 +- .../radio-button/radio-button-panel/readme.md | 51 +- .../radio-button.snapshot.spec.snap.js | 148 +++- .../radio-button.snapshot.spec.ts | 17 +- .../radio-button/radio-button.spec.ts | 157 +---- .../radio-button/radio-button.ssr.spec.ts | 29 +- .../radio-button/radio-button.stories.ts | 48 +- .../radio-button/radio-button/radio-button.ts | 17 +- .../radio-button/radio-button/readme.md | 50 +- .../selection-expansion-panel.spec.ts | 1 + .../selection-expansion-panel.stories.ts | 12 +- 29 files changed, 2077 insertions(+), 714 deletions(-) create mode 100644 src/elements/core/mixins/form-associated-radio-button-mixin.ts create mode 100644 src/elements/radio-button/common/radio-button-common.spec.ts diff --git a/src/elements/autocomplete-grid/autocomplete-grid/__snapshots__/autocomplete-grid.snapshot.spec.snap.js b/src/elements/autocomplete-grid/autocomplete-grid/__snapshots__/autocomplete-grid.snapshot.spec.snap.js index e98ede7d13..2b26399813 100644 --- a/src/elements/autocomplete-grid/autocomplete-grid/__snapshots__/autocomplete-grid.snapshot.spec.snap.js +++ b/src/elements/autocomplete-grid/autocomplete-grid/__snapshots__/autocomplete-grid.snapshot.spec.snap.js @@ -1,12 +1,8 @@ /* @web/test-runner snapshot v1 */ export const snapshots = {}; -snapshots["sbb-autocomplete-grid Safari DOM"] = -` +snapshots["sbb-autocomplete-grid Chrome-Firefox DOM"] = +` `; -/* end snapshot sbb-autocomplete-grid Safari DOM */ +/* end snapshot sbb-autocomplete-grid Chrome-Firefox DOM */ -snapshots["sbb-autocomplete-grid Safari Shadow DOM"] = +snapshots["sbb-autocomplete-grid Chrome-Firefox Shadow DOM"] = `
@@ -79,7 +75,11 @@ snapshots["sbb-autocomplete-grid Safari Shadow DOM"] =
-
+
@@ -87,10 +87,14 @@ snapshots["sbb-autocomplete-grid Safari Shadow DOM"] =
`; -/* end snapshot sbb-autocomplete-grid Safari Shadow DOM */ +/* end snapshot sbb-autocomplete-grid Chrome-Firefox Shadow DOM */ -snapshots["sbb-autocomplete-grid Chrome-Firefox DOM"] = -` +snapshots["sbb-autocomplete-grid Safari DOM"] = +` `; -/* end snapshot sbb-autocomplete-grid Chrome-Firefox DOM */ +/* end snapshot sbb-autocomplete-grid Safari DOM */ -snapshots["sbb-autocomplete-grid Chrome-Firefox Shadow DOM"] = +snapshots["sbb-autocomplete-grid Safari Shadow DOM"] = `
@@ -163,11 +167,7 @@ snapshots["sbb-autocomplete-grid Chrome-Firefox Shadow DOM"] =
-
+
@@ -175,16 +175,16 @@ snapshots["sbb-autocomplete-grid Chrome-Firefox Shadow DOM"] =
`; -/* end snapshot sbb-autocomplete-grid Chrome-Firefox Shadow DOM */ +/* end snapshot sbb-autocomplete-grid Safari Shadow DOM */ -snapshots["sbb-autocomplete-grid Chrome-Firefox A11y tree Chrome"] = +snapshots["sbb-autocomplete-grid Chrome-Firefox A11y tree Firefox"] = `

{ - "role": "WebArea", + "role": "document", "name": "", "children": [ { - "role": "text", + "role": "statictext", "name": "​" }, { @@ -197,16 +197,16 @@ snapshots["sbb-autocomplete-grid Chrome-Firefox A11y tree Chrome"] = }

`; -/* end snapshot sbb-autocomplete-grid Chrome-Firefox A11y tree Chrome */ +/* end snapshot sbb-autocomplete-grid Chrome-Firefox A11y tree Firefox */ -snapshots["sbb-autocomplete-grid Chrome-Firefox A11y tree Firefox"] = +snapshots["sbb-autocomplete-grid Chrome-Firefox A11y tree Chrome"] = `

{ - "role": "document", + "role": "WebArea", "name": "", "children": [ { - "role": "statictext", + "role": "text", "name": "​" }, { @@ -219,5 +219,5 @@ snapshots["sbb-autocomplete-grid Chrome-Firefox A11y tree Firefox"] = }

`; -/* end snapshot sbb-autocomplete-grid Chrome-Firefox A11y tree Firefox */ +/* end snapshot sbb-autocomplete-grid Chrome-Firefox A11y tree Chrome */ diff --git a/src/elements/core/mixins.ts b/src/elements/core/mixins.ts index dfb85b5223..3524825a79 100644 --- a/src/elements/core/mixins.ts +++ b/src/elements/core/mixins.ts @@ -2,6 +2,7 @@ export * from './mixins/constructor.js'; export * from './mixins/disabled-mixin.js'; export * from './mixins/form-associated-checkbox-mixin.js'; export * from './mixins/form-associated-mixin.js'; +export * from './mixins/form-associated-radio-button-mixin.js'; export * from './mixins/hydration-mixin.js'; export * from './mixins/named-slot-list-mixin.js'; export * from './mixins/negative-mixin.js'; diff --git a/src/elements/core/mixins/form-associated-mixin.ts b/src/elements/core/mixins/form-associated-mixin.ts index 1dd4c97fc0..24573f5e53 100644 --- a/src/elements/core/mixins/form-associated-mixin.ts +++ b/src/elements/core/mixins/form-associated-mixin.ts @@ -3,7 +3,7 @@ import { property, state } from 'lit/decorators.js'; import type { AbstractConstructor } from './constructor.js'; -export declare abstract class SbbFormAssociatedMixinType { +export declare abstract class SbbFormAssociatedMixinType extends LitElement { public get form(): HTMLFormElement | null; public get name(): string; public set name(value: string); diff --git a/src/elements/core/mixins/form-associated-radio-button-mixin.ts b/src/elements/core/mixins/form-associated-radio-button-mixin.ts new file mode 100644 index 0000000000..3fd0d1d3ee --- /dev/null +++ b/src/elements/core/mixins/form-associated-radio-button-mixin.ts @@ -0,0 +1,315 @@ +import type { LitElement, PropertyValues } from 'lit'; +import { property } from 'lit/decorators.js'; + +import { getNextElementIndex, interactivityChecker, isArrowKeyPressed } from '../a11y.js'; +import { SbbConnectedAbortController } from '../controllers.js'; +import { forceType } from '../decorators.js'; + +import type { Constructor } from './constructor.js'; +import { SbbDisabledMixin, type SbbDisabledMixinType } from './disabled-mixin.js'; +import { + type FormRestoreReason, + type FormRestoreState, + SbbFormAssociatedMixin, + type SbbFormAssociatedMixinType, +} from './form-associated-mixin.js'; +import { SbbRequiredMixin, type SbbRequiredMixinType } from './required-mixin.js'; + +/** + * A static registry that holds a collection of grouped `radio-buttons`. + * Groups of radio buttons are local to the form they belong (or the `renderRoot` if they're not part of any form) + * Multiple groups of radio with the same name can coexist (as long as they belong to a different form / renderRoot) + * It is mainly used to support the standalone groups of radios. + * @internal + */ +export const radioButtonRegistry = new WeakMap< + Node, + Map> +>(); + +export declare class SbbFormAssociatedRadioButtonMixinType + extends SbbFormAssociatedMixinType + implements Partial, Partial +{ + public checked: boolean; + public disabled: boolean; + public required: boolean; + + protected associatedRadioButtons?: Set; + protected abort: SbbConnectedAbortController; + + public formResetCallback(): void; + public formStateRestoreCallback(state: FormRestoreState | null, reason: FormRestoreReason): void; + + protected isDisabledExternally(): boolean; + protected isRequiredExternally(): boolean; + protected withUserInteraction?(): void; + protected updateFormValue(): void; + protected updateFocusableRadios(): void; + protected emitChangeEvents(): void; + protected navigateByKeyboard(radio: SbbFormAssociatedRadioButtonMixinType): Promise; +} + +/** + * The SbbFormAssociatedRadioButtonMixin enables native form support for radio controls. + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export const SbbFormAssociatedRadioButtonMixin = >( + superClass: T, +): Constructor & T => { + class SbbFormAssociatedRadioButtonElement + extends SbbDisabledMixin(SbbRequiredMixin(SbbFormAssociatedMixin(superClass))) + implements Partial + { + /** + * Whether the radio button is checked. + */ + @forceType() + @property({ type: Boolean }) + public accessor checked: boolean = false; + + protected abort = new SbbConnectedAbortController(this); + + /** + * Set of radio buttons that belongs to the same group of `this`. + * Assume them ordered in DOM order + */ + protected associatedRadioButtons?: Set; + private _radioButtonGroupsMap?: Map>; + private _didLoad: boolean = false; + + protected constructor() { + super(); + /** @internal */ + this.internals.role = 'radio'; + } + + public override connectedCallback(): void { + super.connectedCallback(); + this._connectToRegistry(); + this.addEventListener('keydown', (e) => this._handleArrowKeyDown(e), { + signal: this.abort.signal, + }); + } + + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this._disconnectFromRegistry(); + } + + /** + * Is called whenever the form is being reset. + * @internal + */ + public override formResetCallback(): void { + this.checked = this.hasAttribute('checked'); + } + + /** + * Called when the browser is trying to restore element’s state to state in which case + * reason is “restore”, or when the browser is trying to fulfill autofill on behalf of + * user in which case reason is “autocomplete”. + * @internal + */ + public override formStateRestoreCallback( + state: FormRestoreState | null, + _reason: FormRestoreReason, + ): void { + if (state) { + this.checked = state === this.value; + } + } + + protected override willUpdate(changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); + + // On 'name' change, move 'this' to the new registry + if (changedProperties.has('name')) { + this._disconnectFromRegistry(); + this._connectToRegistry(); + if (this.checked) { + this._deselectGroupedRadios(); + } + this.updateFocusableRadios(); + } + + if (changedProperties.has('checked')) { + this.toggleAttribute('data-checked', this.checked); + this.internals.ariaChecked = this.checked.toString(); + this.updateFormValueOnCheckedChange(); + if (this.checked) { + this._deselectGroupedRadios(); + } + this.updateFocusableRadios(); + } + + if (changedProperties.has('disabled')) { + this.updateFocusableRadios(); + } + } + + protected override firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + this._didLoad = true; + this.updateFocusableRadios(); + } + + /** + * Called on `value` change + * If 'checked', update the value. Otherwise, do nothing. + */ + protected override updateFormValue(): void { + if (this.checked) { + this.internals.setFormValue(this.value); + } + } + + /** + * Called on `checked` change + * If 'checked', update the value. Otherwise, reset it. + */ + protected updateFormValueOnCheckedChange(): void { + this.internals.setFormValue(this.checked ? this.value : null); + } + + /** + * Only a single radio should be focusable in the group. Defined as: + * - the checked radio; + * - the first non-disabled radio in DOM order; + */ + protected updateFocusableRadios(): void { + if (!this._didLoad) { + return; + } + const radios = this._interactableGroupedRadios(); + const checkedIndex = radios.findIndex((r) => r.checked && !r.disabled && !r.formDisabled); + const focusableIndex = + checkedIndex !== -1 + ? checkedIndex + : radios.findIndex((r) => !r.disabled && !r.formDisabled); // Get the first focusable radio + + if (focusableIndex !== -1) { + radios[focusableIndex].tabIndex = 0; + radios.splice(focusableIndex, 1); + } + + // Reset tabIndex on other radios + radios.forEach((r) => r.removeAttribute('tabindex')); + } + + protected async navigateByKeyboard(next: SbbFormAssociatedRadioButtonElement): Promise { + next.checked = true; + this.emitChangeEvents(); + + await next.updateComplete; + next.focus(); + } + + protected emitChangeEvents(): void { + // Manually dispatch events to simulate a user interaction + this.dispatchEvent( + new InputEvent('input', { bubbles: true, cancelable: true, composed: true }), + ); + this.dispatchEvent(new Event('change', { bubbles: true })); + } + + /** + * Add `this` to the radioButton registry + */ + private _connectToRegistry(): void { + if (!this.name) { + return; + } + + const root = this.form ?? this.getRootNode(); + this._radioButtonGroupsMap = radioButtonRegistry.get(root); + + // Initialize the 'root' map entry + if (!this._radioButtonGroupsMap) { + this._radioButtonGroupsMap = new Map(); + radioButtonRegistry.set(root, this._radioButtonGroupsMap); + } + + this.associatedRadioButtons = this._radioButtonGroupsMap.get( + this.name, + ) as unknown as Set; + + // Initialize the group set + if (!this.associatedRadioButtons) { + this.associatedRadioButtons = new Set(); + this._radioButtonGroupsMap.set( + this.name, + this.associatedRadioButtons as unknown as Set, + ); + } + + // Insert the new radio into the set and sort following the DOM order. + // Since the order of a 'Set' is the insert order, we have to empty it and re-insert radios in order + const entries = Array.from(this.associatedRadioButtons); + this.associatedRadioButtons.clear(); + + // Find `this` position and insert it + const index = entries.findIndex( + (r) => this.compareDocumentPosition(r) & Node.DOCUMENT_POSITION_FOLLOWING, + ); + if (index !== -1) { + entries.splice(index, 0, this); + } else { + entries.push(this); + } + + // Repopulate the Set + entries.forEach((r) => this.associatedRadioButtons!.add(r)); + } + + /** + * Remove `this` from the radioButton registry and, if the group is empty, delete the entry from the groups Map + */ + private _disconnectFromRegistry(): void { + this.associatedRadioButtons?.delete(this); + + if (this.associatedRadioButtons?.size === 0) { + this._radioButtonGroupsMap?.delete(this.name); + } + + this.associatedRadioButtons = undefined; + this._radioButtonGroupsMap = undefined; + } + + /** + * Return a list of 'interactable' grouped radios, ordered in DOM order + */ + private _interactableGroupedRadios(): SbbFormAssociatedRadioButtonElement[] { + return Array.from(this.associatedRadioButtons ?? []).filter((el) => + interactivityChecker.isVisible(el), + ); + } + + /** + * Deselect other radio of the same group + */ + private _deselectGroupedRadios(): void { + Array.from(this.associatedRadioButtons ?? []) + .filter((r) => r !== this) + .forEach((r) => (r.checked = false)); + } + + private async _handleArrowKeyDown(evt: KeyboardEvent): Promise { + if (!isArrowKeyPressed(evt)) { + return; + } + evt.preventDefault(); + + const enabledRadios = this._interactableGroupedRadios().filter( + (r) => !r.disabled && !r.formDisabled, + ); + const current: number = enabledRadios.indexOf(this); + const nextIndex: number = getNextElementIndex(evt, current, enabledRadios.length); + + await this.navigateByKeyboard(enabledRadios[nextIndex]); + } + } + + return SbbFormAssociatedRadioButtonElement as unknown as Constructor & + T; +}; diff --git a/src/elements/radio-button/common/radio-button-common.scss b/src/elements/radio-button/common/radio-button-common.scss index f90731b6a5..faa7ba162f 100644 --- a/src/elements/radio-button/common/radio-button-common.scss +++ b/src/elements/radio-button/common/radio-button-common.scss @@ -30,7 +30,7 @@ } } -:host([checked]) { +:host([data-checked]) { --sbb-radio-button-inner-circle-color: var(--sbb-color-red); --sbb-radio-button-background-fake-border-width: calc( (var(--sbb-radio-button-dimension) - var(--sbb-radio-button-inner-circle-dimension)) / 2 @@ -43,7 +43,7 @@ } // Disabled definitions have to be after checked definitions -:host([disabled]) { +:host(:disabled) { --sbb-radio-button-label-color: var(--sbb-color-granite); --sbb-radio-button-background-color: var(--sbb-color-milk); --sbb-radio-button-border-style: dashed; diff --git a/src/elements/radio-button/common/radio-button-common.spec.ts b/src/elements/radio-button/common/radio-button-common.spec.ts new file mode 100644 index 0000000000..fedd394ef3 --- /dev/null +++ b/src/elements/radio-button/common/radio-button-common.spec.ts @@ -0,0 +1,635 @@ +import { expect } from '@open-wc/testing'; +import { a11ySnapshot, sendKeys } from '@web/test-runner-commands'; +import { html, unsafeStatic } from 'lit/static-html.js'; +import type { Context } from 'mocha'; + +import { isChromium, isWebkit } from '../../core/dom.js'; +import { radioButtonRegistry } from '../../core/mixins.js'; +import { fixture } from '../../core/testing/private.js'; +import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import type { SbbRadioButtonPanelElement } from '../radio-button-panel.js'; +import type { SbbRadioButtonElement } from '../radio-button.js'; + +import '../radio-button.js'; +import '../radio-button-panel.js'; + +interface CheckboxAccessibilitySnapshot { + checked: boolean; + role: string; + disabled: boolean; + required: boolean; +} + +describe(`radio-button common behaviors`, () => { + ['sbb-radio-button', 'sbb-radio-button-panel'].forEach((selector) => { + const tagSingle = unsafeStatic(selector); + + describe(`${selector} general`, () => { + let element: SbbRadioButtonElement | SbbRadioButtonPanelElement; + + beforeEach(async () => { + /* eslint-disable lit/binding-positions */ + element = await fixture(html`<${tagSingle} name="name" value="value">Label`); + await waitForLitRender(element); + }); + + describe('events', () => { + it('emit event on click', async () => { + expect(element).not.to.have.attribute('checked'); + const changeSpy = new EventSpy('change'); + + element.click(); + await waitForLitRender(element); + + expect(changeSpy.count).to.be.greaterThan(0); + expect(element).not.to.have.attribute('checked'); + expect(element.checked).to.equal(true); + }); + + it('emit event on keypress', async () => { + const changeSpy = new EventSpy('change'); + + element.focus(); + await sendKeys({ press: 'Space' }); + + await waitForCondition(() => changeSpy.count === 1); + expect(changeSpy.count).to.be.greaterThan(0); + }); + }); + + it('should prevent scrolling on space bar press', async () => { + const root = await fixture( + html`
+
+ <${tagSingle} name="name2"> +
+
`, + ); + element = root.querySelector(selector)!; + + expect(element.checked).to.be.false; + expect(root.scrollTop).to.be.equal(0); + + element.focus(); + await sendKeys({ press: ' ' }); + await waitForLitRender(element); + + await waitForCondition(() => element.checked); + expect(root.scrollTop).to.be.equal(0); + }); + + it('should reflect aria-required false', async () => { + const snapshot = (await a11ySnapshot({ + selector: selector, + })) as unknown as CheckboxAccessibilitySnapshot; + + expect(snapshot.required).to.be.undefined; + }); + + it('should reflect accessibility tree setting required attribute to true', async function (this: Context) { + // On Firefox sometimes a11ySnapshot fails. Retrying three times should stabilize the build. + this.retries(3); + + element.toggleAttribute('required', true); + await waitForLitRender(element); + + const snapshot = (await a11ySnapshot({ + selector: selector, + })) as unknown as CheckboxAccessibilitySnapshot; + + // TODO: Recheck if it is working in Chromium + if (!isChromium) { + expect(snapshot.required).to.be.true; + } + }); + + it('should reflect accessibility tree setting required attribute to false', async function (this: Context) { + // On Firefox sometimes a11ySnapshot fails. Retrying three times should stabilize the build. + this.retries(3); + + element.toggleAttribute('required', true); + await waitForLitRender(element); + + element.removeAttribute('required'); + await waitForLitRender(element); + + const snapshot = (await a11ySnapshot({ + selector: selector, + })) as unknown as CheckboxAccessibilitySnapshot; + + expect(snapshot.required).not.to.be.ok; + }); + + it('should reflect accessibility tree setting required property to true', async function (this: Context) { + // On Firefox sometimes a11ySnapshot fails. Retrying three times should stabilize the build. + this.retries(3); + + element.required = true; + await waitForLitRender(element); + + const snapshot = (await a11ySnapshot({ + selector: selector, + })) as unknown as CheckboxAccessibilitySnapshot; + + // TODO: Recheck if it is working in Chromium + if (!isChromium) { + expect(snapshot.required).to.be.true; + } + }); + + it('should reflect accessibility tree setting required property to false', async function (this: Context) { + // On Firefox sometimes a11ySnapshot fails. Retrying three times should stabilize the build. + this.retries(3); + + element.required = true; + await waitForLitRender(element); + + element.required = false; + await waitForLitRender(element); + + const snapshot = (await a11ySnapshot({ + selector: selector, + })) as unknown as CheckboxAccessibilitySnapshot; + + expect(snapshot.required).not.to.be.ok; + }); + + it('should restore form state on formStateRestoreCallback()', async () => { + // Mimic tab restoration. Does not test the full cycle as we can not set the browser in the required state. + element.formStateRestoreCallback('value', 'restore'); + await waitForLitRender(element); + + expect(element.checked).to.be.true; + + element.formStateRestoreCallback('anotherValue', 'restore'); + await waitForLitRender(element); + + expect(element.checked).to.be.false; + }); + + it('should ignore interaction when disabled', async () => { + const inputSpy = new EventSpy('input', element); + const changeSpy = new EventSpy('change', element); + element.disabled = true; + await waitForLitRender(element); + + element.focus(); + element.click(); + await sendKeys({ press: ' ' }); + + expect(inputSpy.count).to.be.equal(0); + expect(changeSpy.count).to.be.equal(0); + expect(element.checked).to.be.false; + }); + }); + }); + + describe('compare to native', () => { + let elements: (SbbRadioButtonElement | SbbRadioButtonPanelElement)[], + nativeElements: HTMLInputElement[], + form: HTMLFormElement, + fieldset: HTMLFieldSetElement, + nativeFieldset: HTMLFieldSetElement, + inputSpy: EventSpy, + changeSpy: EventSpy, + nativeInputSpy: EventSpy, + nativeChangeSpy: EventSpy; + + const compareToNative = async (): Promise => { + elements.forEach((radio, i) => { + expect(radio.checked, `radio ${radio.value} checked`).to.be.equal( + nativeElements[i].checked, + ); + }); + + // Events + expect(inputSpy.count, `'input' event`).to.be.equal(nativeInputSpy.count); + expect(changeSpy.count, `'change' event`).to.be.equal(nativeChangeSpy.count); + + // Form state should always correspond to checked property. + const formData = new FormData(form); + expect(formData.get('sbb-group-1'), 'form data - group 1').to.be.equal( + formData.get('native-group-1'), + ); + expect(formData.get('sbb-group-2'), 'form data - group 2').to.be.equal( + formData.get('native-group-2'), + ); + }; + + ['sbb-radio-button', 'sbb-radio-button-panel'].forEach((selector) => { + describe(selector, () => { + const tagSingle = unsafeStatic(selector); + + describe('general behavior', () => { + beforeEach(async () => { + form = await fixture(html` +
+
+ <${tagSingle} value="1" name="sbb-group-1">1 + <${tagSingle} value="2" name="sbb-group-1">2 + <${tagSingle} value="3" name="sbb-group-1">3 + + <${tagSingle} value="4" name="sbb-group-2">1 + <${tagSingle} value="5" name="sbb-group-2">2 +
+
+ + + + + + +
+
`); + + elements = Array.from(form.querySelectorAll(selector)); + nativeElements = Array.from(form.querySelectorAll('input')); + fieldset = form.querySelector('#sbb-set')!; + nativeFieldset = form.querySelector('#native-set')!; + + inputSpy = new EventSpy('input', fieldset); + changeSpy = new EventSpy('change', fieldset); + nativeInputSpy = new EventSpy('input', nativeFieldset); + nativeChangeSpy = new EventSpy('change', nativeFieldset); + + await waitForLitRender(form); + }); + + it('should find connected form', () => { + expect(elements[0].form).to.be.equal(form); + }); + + it('first elements of groups should be focusable', async () => { + expect(elements[0].tabIndex).to.be.equal(0); + expect(elements[1].tabIndex).to.be.equal(-1); + expect(elements[2].tabIndex).to.be.equal(-1); + expect(elements[3].tabIndex).to.be.equal(0); + expect(elements[4].tabIndex).to.be.equal(-1); + await compareToNative(); + }); + + it('should select on click', async () => { + elements[1].click(); + await waitForLitRender(form); + expect(document.activeElement === elements[1]).to.be.true; + + nativeElements[1].click(); + await waitForLitRender(form); + + expect(elements[0].tabIndex).to.be.equal(-1); + expect(elements[1].tabIndex).to.be.equal(0); + expect(elements[1].checked).to.be.true; + await compareToNative(); + }); + + it('should reflect state after programmatic change', async () => { + elements[1].checked = true; + nativeElements[1].checked = true; + await waitForLitRender(form); + + expect(elements[0].tabIndex).to.be.equal(-1); + expect(elements[1].tabIndex).to.be.equal(0); + await compareToNative(); + }); + + it('should reset on form reset', async () => { + elements[1].checked = true; + nativeElements[1].checked = true; + await waitForLitRender(form); + + form.reset(); + await waitForLitRender(form); + + expect(elements[0].tabIndex).to.be.equal(0); + expect(elements[1].tabIndex).to.be.equal(-1); + expect(elements[1].checked).to.be.false; + await compareToNative(); + }); + + it('should restore default on form reset', async () => { + elements[1].toggleAttribute('checked', true); + nativeElements[1].toggleAttribute('checked', true); + await waitForLitRender(form); + + elements[0].click(); + nativeElements[0].click(); + await waitForLitRender(form); + + form.reset(); + await waitForLitRender(form); + + expect(elements[0].tabIndex).to.be.equal(-1); + expect(elements[1].tabIndex).to.be.equal(0); + expect(elements[0].checked).to.be.false; + expect(elements[1].checked).to.be.true; + await compareToNative(); + }); + + it('should restore on form restore', async () => { + // Mimic tab restoration. Does not test the full cycle as we can not set the browser in the required state. + elements[0].formStateRestoreCallback('2', 'restore'); + elements[1].formStateRestoreCallback('2', 'restore'); + await waitForLitRender(form); + + expect(elements[0].checked).to.be.false; + expect(elements[1].checked).to.be.true; + expect(elements[0].tabIndex).to.be.equal(-1); + expect(elements[1].tabIndex).to.be.equal(0); + }); + + it('should handle adding a new radio to the group', async () => { + elements[0].checked = true; + await waitForLitRender(form); + + // Create and add a new checked radio to the group + const newRadio = document.createElement(selector) as + | SbbRadioButtonElement + | SbbRadioButtonPanelElement; + newRadio.setAttribute('name', 'sbb-group-1'); + newRadio.setAttribute('value', '4'); + newRadio.toggleAttribute('checked', true); + fieldset.appendChild(newRadio); + + await waitForLitRender(form); + + expect(elements[0].checked).to.be.false; + expect(newRadio.checked).to.be.true; + expect(elements[0].tabIndex).to.be.equal(-1); + expect(newRadio.tabIndex).to.be.equal(0); + }); + + it('should handle moving a radio between the groups', async () => { + elements[0].checked = true; + nativeElements[0].checked = true; + elements[3].checked = true; + nativeElements[3].checked = true; + + await waitForLitRender(form); + + elements[3].name = elements[0].name; + nativeElements[3].name = nativeElements[0].name; + + await waitForLitRender(form); + + // When moving a checked radio to a group, it has priority and becomes the new checked + expect(elements[0].checked).to.be.false; + expect(elements[3].checked).to.be.true; + expect(elements[0].tabIndex).to.be.equal(-1); + expect(elements[3].tabIndex).to.be.equal(0); + await compareToNative(); + }); + + describe('keyboard interaction', () => { + it('should select on space key', async () => { + elements[0].focus(); + await sendKeys({ press: 'Space' }); + await waitForLitRender(form); + + expect(elements[0].checked).to.be.true; + }); + + it('should select and wrap on arrow keys', async () => { + elements[1].checked = true; + await waitForLitRender(form); + elements[1].focus(); + + await sendKeys({ press: 'ArrowRight' }); + await waitForLitRender(form); + + expect(elements[1].checked).to.be.false; + expect(elements[2].checked).to.be.true; + expect(document.activeElement === elements[2]).to.be.true; + + await sendKeys({ press: 'ArrowDown' }); + await waitForLitRender(form); + + expect(elements[2].checked).to.be.false; + expect(elements[0].checked).to.be.true; + expect(document.activeElement === elements[0]).to.be.true; + + await sendKeys({ press: 'ArrowLeft' }); + await waitForLitRender(form); + + expect(elements[0].checked).to.be.false; + expect(elements[2].checked).to.be.true; + expect(document.activeElement === elements[2]).to.be.true; + + await sendKeys({ press: 'ArrowUp' }); + await waitForLitRender(form); + + expect(elements[2].checked).to.be.false; + expect(elements[1].checked).to.be.true; + expect(document.activeElement === elements[1]).to.be.true; + + // Execute same steps on native and compare the outcome + nativeElements[1].focus(); + await sendKeys({ press: 'ArrowRight' }); + await sendKeys({ press: 'ArrowDown' }); + await sendKeys({ press: 'ArrowLeft' }); + await sendKeys({ press: 'ArrowUp' }); + + // On webkit, native radios do not wrap + if (!isWebkit) { + await compareToNative(); + } + }); + + it('should handle keyboard interaction outside of a form', async () => { + // Move the radios outside the form + form.parentElement!.append(fieldset); + await waitForLitRender(fieldset); + + elements[0].focus(); + await sendKeys({ press: 'ArrowDown' }); + await waitForLitRender(fieldset); + expect(elements[0].checked).to.be.false; + expect(elements[1].checked).to.be.true; + expect(document.activeElement === elements[1]).to.be.true; + + await sendKeys({ press: 'ArrowDown' }); + await sendKeys({ press: 'ArrowDown' }); + await waitForLitRender(fieldset); + expect(elements[0].checked).to.be.true; + expect(document.activeElement === elements[0]).to.be.true; + }); + + it('should skip disabled elements on arrow keys', async () => { + elements[1].disabled = true; + await waitForLitRender(form); + + elements[0].focus(); + await sendKeys({ press: 'ArrowRight' }); + await waitForLitRender(form); + + expect(elements[0].checked).to.be.false; + expect(elements[2].checked).to.be.true; + expect(document.activeElement === elements[2]).to.be.true; + + await sendKeys({ press: 'ArrowLeft' }); + await waitForLitRender(form); + + expect(elements[2].checked).to.be.false; + expect(elements[0].checked).to.be.true; + expect(document.activeElement === elements[0]).to.be.true; + }); + + it('should skip non-visible elements on arrow keys', async () => { + elements[1].style.display = 'none'; + await waitForLitRender(form); + + elements[0].focus(); + await sendKeys({ press: 'ArrowRight' }); + await waitForLitRender(form); + + expect(elements[0].checked).to.be.false; + expect(elements[2].checked).to.be.true; + expect(document.activeElement === elements[2]).to.be.true; + + await sendKeys({ press: 'ArrowLeft' }); + await waitForLitRender(form); + + expect(elements[2].checked).to.be.false; + expect(elements[0].checked).to.be.true; + expect(document.activeElement === elements[0]).to.be.true; + }); + }); + + describe('disabled state', () => { + it('should result :disabled', async () => { + elements[0].disabled = true; + await waitForLitRender(form); + + expect(elements[0]).to.match(':disabled'); + expect(elements[1].tabIndex).to.be.equal(0); + }); + + it('should result :disabled if a fieldSet is', async () => { + fieldset.toggleAttribute('disabled', true); + await waitForLitRender(form); + + expect(elements[0]).to.match(':disabled'); + }); + + it('should do nothing when clicked', async () => { + elements[0].disabled = true; + await waitForLitRender(form); + + elements[0].click(); + await waitForLitRender(form); + + expect(elements[0].checked).to.be.false; + }); + + it('should update tabindex when the first element is disabled', async () => { + expect(elements[0].tabIndex).to.be.equal(0); + elements[0].disabled = true; + await waitForLitRender(form); + + expect(elements[0].tabIndex).to.be.equal(-1); + expect(elements[1].tabIndex).to.be.equal(0); + }); + }); + }); + + describe('multiple groups with the same name', () => { + let root: HTMLElement; + let form2: HTMLFormElement; + + beforeEach(async () => { + root = await fixture(html` +
+
+ <${tagSingle} value="1" name="sbb-group-1">1 + <${tagSingle} value="2" name="sbb-group-1">2 + <${tagSingle} value="3" name="sbb-group-1">3 +
+
+ <${tagSingle} value="1" name="sbb-group-1">1 + <${tagSingle} value="2" name="sbb-group-1">2 + <${tagSingle} value="3" name="sbb-group-1">3 +
+
+ `); + + form = root.querySelector('form#main')!; + form2 = root.querySelector('form#secondary')!; + elements = Array.from(root.querySelectorAll(selector)); + await waitForLitRender(root); + }); + + it('groups should be independent', async () => { + expect(elements[0].tabIndex).to.be.equal(0); + expect(elements[3].tabIndex).to.be.equal(0); + + // Check the first element of each group + elements[0].click(); + elements[3].click(); + await waitForLitRender(root); + + expect(elements[0].tabIndex).to.be.equal(0); + expect(elements[0].checked).to.be.true; + expect(elements[3].tabIndex).to.be.equal(0); + expect(elements[3].checked).to.be.true; + }); + + it('groups should be independent when keyboard navigated', async () => { + elements[0].focus(); + + await sendKeys({ press: 'ArrowUp' }); + await waitForLitRender(root); + + expect(elements[2].tabIndex).to.be.equal(0); + expect(elements[2].checked).to.be.true; + expect(elements[5].tabIndex).to.be.equal(-1); + expect(elements[5].checked).to.be.false; + }); + + describe('radioButtonRegistry', () => { + it('should be in the correct state', async () => { + const group1Set = radioButtonRegistry.get(form)!.get('sbb-group-1')!; + const group2Set = radioButtonRegistry.get(form2)!.get('sbb-group-1')!; + const group1Radios = Array.from(form.querySelectorAll(selector)); + const group2Radios = Array.from(form2.querySelectorAll(selector)); + + // Assert the order is the correct + expect(group1Set.size).to.be.equal(3); + expect(group2Set.size).to.be.equal(3); + + expect(Array.from(group1Set)).contains.members(group1Radios); + expect(Array.from(group2Set)).contains.members(group2Radios); + }); + + it('should be sorted in DOM order', async () => { + const group1Set = radioButtonRegistry.get(form)!.get('sbb-group-1')!; + let group1Radios = Array.from(form.querySelectorAll(selector)); + + // Assert the order is the correct + expect(group1Set.size).to.be.equal(3); + Array.from(group1Set).forEach((r, i) => expect(group1Radios[i] === r).to.be.true); + + // Move the first radio to the last position + form.append(group1Radios[0]); + group1Radios = Array.from(form.querySelectorAll(selector)); + + // Assert the order is the correct + expect(group1Set.size).to.be.equal(3); + Array.from(group1Set).forEach((r, i) => expect(group1Radios[i] === r).to.be.true); + }); + + it('should remove empty entries from the registry', async () => { + const group2Set = radioButtonRegistry.get(form2)!.get('sbb-group-1')!; + + // Remove the second radio group from the DOM + form2.remove(); + await waitForLitRender(root); + + expect(group2Set.size).to.be.equal(0); + expect(radioButtonRegistry.get(form)!.get('sbb-group-1')?.size).to.be.equal(3); + expect(radioButtonRegistry.get(form2)!.get('sbb-group-1')).to.be.undefined; + }); + }); + }); + }); + }); + }); +}); diff --git a/src/elements/radio-button/common/radio-button-common.ts b/src/elements/radio-button/common/radio-button-common.ts index 2ce2dc0c0a..7e487a354c 100644 --- a/src/elements/radio-button/common/radio-button-common.ts +++ b/src/elements/radio-button/common/radio-button-common.ts @@ -1,16 +1,19 @@ import type { LitElement, PropertyValues } from 'lit'; import { property } from 'lit/decorators.js'; -import { SbbConnectedAbortController } from '../../core/controllers.js'; -import { forceType, hostAttributes } from '../../core/decorators.js'; -import { setOrRemoveAttribute } from '../../core/dom.js'; -import { EventEmitter, HandlerRepository, formElementHandlerAspect } from '../../core/eventing.js'; +import { setModalityOnNextFocus } from '../../core/a11y.js'; +import { EventEmitter } from '../../core/eventing.js'; import type { SbbCheckedStateChange, SbbDisabledStateChange, SbbStateChange, } from '../../core/interfaces.js'; -import type { AbstractConstructor } from '../../core/mixins.js'; +import { + type AbstractConstructor, + type Constructor, + SbbFormAssociatedRadioButtonMixin, + type SbbFormAssociatedRadioButtonMixinType, +} from '../../core/mixins.js'; import type { SbbRadioButtonGroupElement } from '../radio-button-group.js'; export type SbbRadioButtonSize = 'xs' | 's' | 'm'; @@ -20,32 +23,24 @@ export type SbbRadioButtonStateChange = Extract< SbbDisabledStateChange | SbbCheckedStateChange >; -export declare class SbbRadioButtonCommonElementMixinType { +export declare class SbbRadioButtonCommonElementMixinType extends SbbFormAssociatedRadioButtonMixinType { public get allowEmptySelection(): boolean; public set allowEmptySelection(boolean); - public accessor value: string; - public get disabled(): boolean; - public set disabled(boolean); - public get required(): boolean; - public set required(boolean); public get group(): SbbRadioButtonGroupElement | null; - public get checked(): boolean; - public set checked(boolean); public select(): void; } // eslint-disable-next-line @typescript-eslint/naming-convention -export const SbbRadioButtonCommonElementMixin = >( +export const SbbRadioButtonCommonElementMixin = >( superClass: T, ): AbstractConstructor & T => { - @hostAttributes({ - role: 'radio', - }) abstract class SbbRadioButtonCommonElement - extends superClass + extends SbbFormAssociatedRadioButtonMixin(superClass) implements Partial { public static readonly events = { + change: 'change', + input: 'input', stateChange: 'stateChange', } as const; @@ -61,37 +56,6 @@ export const SbbRadioButtonCommonElementMixin = this._handleClick(e), { signal }); this.addEventListener('keydown', (e) => this._handleKeyDown(e), { signal }); - this._handlerRepository.connect(); // We need to call requestUpdate to update the reflected attributes ['disabled', 'required', 'size'].forEach((p) => this.requestUpdate(p)); } - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._handlerRepository.disconnect(); + /** + * Set the radio-button as 'checked'; if 'allowEmptySelection', toggle the checked property. + * In both cases it emits the change events. + */ + public select(): void { + if (this.disabled || this.formDisabled) { + return; + } + + if (this.allowEmptySelection) { + this.checked = !this.checked; + this.emitChangeEvents(); + } else if (!this.checked) { + this.checked = true; + this.emitChangeEvents(); + } } protected override willUpdate(changedProperties: PropertyValues): void { super.willUpdate(changedProperties); if (changedProperties.has('checked')) { - this.setAttribute('aria-checked', `${this.checked}`); if (this.checked !== changedProperties.get('checked')!) { this._stateChange.emit({ type: 'checked', checked: this.checked }); } } if (changedProperties.has('disabled')) { - setOrRemoveAttribute(this, 'aria-disabled', this.disabled ? 'true' : null); if (this.disabled !== changedProperties.get('disabled')!) { this._stateChange.emit({ type: 'disabled', disabled: this.disabled }); } } - if (changedProperties.has('required')) { - this.setAttribute('aria-required', `${this.required}`); - } } - private _handleClick(event: Event): void { + protected override isDisabledExternally(): boolean { + return this.group?.disabled ?? false; + } + + protected override isRequiredExternally(): boolean { + return this.group?.required ?? false; + } + + private async _handleClick(event: Event): Promise { event.preventDefault(); this.select(); + + /** + * Since only a single radio of a group is focusable at any time, it is possible that the one clicked does not have 'tabindex=0'. + * To cover that, we await the next render (which will make the 'checked' radio focusable) and focus the clicked radio + */ + await this.updateComplete; // Wait for 'tabindex' to be updated + setModalityOnNextFocus(this); + this.focus(); } private _handleKeyDown(evt: KeyboardEvent): void { if (evt.code === 'Space') { + evt.preventDefault(); this.select(); } } diff --git a/src/elements/radio-button/radio-button-group/__snapshots__/radio-button-group.snapshot.spec.snap.js b/src/elements/radio-button/radio-button-group/__snapshots__/radio-button-group.snapshot.spec.snap.js index db299bfefc..0d3d1e1313 100644 --- a/src/elements/radio-button/radio-button-group/__snapshots__/radio-button-group.snapshot.spec.snap.js +++ b/src/elements/radio-button/radio-button-group/__snapshots__/radio-button-group.snapshot.spec.snap.js @@ -3,9 +3,38 @@ export const snapshots = {}; snapshots["sbb-radio-button-group renders DOM"] = ` + + 1 + + + 2 + + + 3 + `; /* end snapshot sbb-radio-button-group renders DOM */ @@ -26,7 +55,24 @@ snapshots["sbb-radio-button-group renders A11y tree Chrome"] = `

{ "role": "WebArea", - "name": "" + "name": "", + "children": [ + { + "role": "radio", + "name": "1", + "checked": false + }, + { + "role": "radio", + "name": "2", + "checked": true + }, + { + "role": "radio", + "name": "3", + "checked": false + } + ] }

`; @@ -36,7 +82,22 @@ snapshots["sbb-radio-button-group renders A11y tree Firefox"] = `

{ "role": "document", - "name": "" + "name": "", + "children": [ + { + "role": "radio", + "name": "1" + }, + { + "role": "radio", + "name": "2", + "checked": true + }, + { + "role": "radio", + "name": "3" + } + ] }

`; diff --git a/src/elements/radio-button/radio-button-group/radio-button-group.snapshot.spec.ts b/src/elements/radio-button/radio-button-group/radio-button-group.snapshot.spec.ts index 5f7031c296..71e331e700 100644 --- a/src/elements/radio-button/radio-button-group/radio-button-group.snapshot.spec.ts +++ b/src/elements/radio-button/radio-button-group/radio-button-group.snapshot.spec.ts @@ -6,13 +6,20 @@ import { fixture, testA11yTreeSnapshot } from '../../core/testing/private.js'; import type { SbbRadioButtonGroupElement } from './radio-button-group.js'; import './radio-button-group.js'; +import '../radio-button.js'; describe(`sbb-radio-button-group`, () => { let element: SbbRadioButtonGroupElement; describe('renders', () => { beforeEach(async () => { - element = await fixture(html``); + element = await fixture( + html` + 1 + 2 + 3 + `, + ); }); it('DOM', async () => { diff --git a/src/elements/radio-button/radio-button-group/radio-button-group.spec.ts b/src/elements/radio-button/radio-button-group/radio-button-group.spec.ts index 03c5d6756f..1b56ef0898 100644 --- a/src/elements/radio-button/radio-button-group/radio-button-group.spec.ts +++ b/src/elements/radio-button/radio-button-group/radio-button-group.spec.ts @@ -1,5 +1,4 @@ import { assert, expect } from '@open-wc/testing'; -import { sendKeys } from '@web/test-runner-commands'; import { html, unsafeStatic } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; @@ -7,261 +6,249 @@ import { EventSpy, waitForLitRender } from '../../core/testing.js'; import type { SbbRadioButtonPanelElement } from '../radio-button-panel.js'; import type { SbbRadioButtonElement } from '../radio-button.js'; +import { SbbRadioButtonGroupElement } from './radio-button-group.js'; + import '../radio-button.js'; import '../radio-button-panel.js'; -import { SbbRadioButtonGroupElement } from './radio-button-group.js'; - ['sbb-radio-button', 'sbb-radio-button-panel'].forEach((selector) => { const tagSingle = unsafeStatic(selector); describe(`sbb-radio-button-group with ${selector}`, () => { let element: SbbRadioButtonGroupElement; - describe('events', () => { + describe('general behavior', () => { + let radios: (SbbRadioButtonElement | SbbRadioButtonPanelElement)[]; + beforeEach(async () => { /* eslint-disable lit/binding-positions */ - element = await fixture( - html` - - <${tagSingle} id="sbb-radio-1" value="Value one">Value one - <${tagSingle} id="sbb-radio-2" value="Value two">Value two - <${tagSingle} id="sbb-radio-3" value="Value three" disabled - >Value three - <${tagSingle} id="sbb-radio-4" value="Value four">Value four - - `, - ); + element = await fixture(html` + + <${tagSingle} id="sbb-radio-1" value="Value one">Value one + <${tagSingle} id="sbb-radio-2" value="Value two">Value two + <${tagSingle} id="sbb-radio-3" value="Value three" disabled>Value three + + `); + radios = Array.from(element.querySelectorAll(selector)); + + await waitForLitRender(element); }); it('renders', () => { assert.instanceOf(element, SbbRadioButtonGroupElement); }); - it('selects radio on click', async () => { - const firstRadio = element.querySelector('#sbb-radio-1') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; - const radio = element.querySelector('#sbb-radio-2') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; - - expect(firstRadio).to.have.attribute('checked'); + it('should set name on each radios', async () => { + const name = element.name; + radios.forEach((r) => expect(r.name).to.equal(name)); + }); - radio.click(); + it('should update the name of each radios', async () => { + element.name = 'group-1'; await waitForLitRender(element); - expect(radio).to.have.attribute('checked'); - expect(firstRadio).not.to.have.attribute('checked'); + radios.forEach((r) => expect(r.name).to.equal('group-1')); }); - it('renders', () => { - assert.instanceOf(element, SbbRadioButtonGroupElement); - }); + it('selects radio on click', async () => { + const radio = radios[0]; + expect(element.value).to.null; - describe('events', () => { - it('selects radio on click', async () => { - const firstRadio = element.querySelector('#sbb-radio-1') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; - const radio = element.querySelector('#sbb-radio-2') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; + radio.click(); + await waitForLitRender(element); - expect(firstRadio).to.have.attribute('checked'); + expect(radio.checked).to.be.true; + expect(element.value).to.be.equal(radio.value); + }); - radio.click(); - await waitForLitRender(element); + it('selects radio when value is set', async () => { + const radio = radios[0]; + expect(element.value).to.null; + element.value = radio.value; - expect(radio).to.have.attribute('checked'); - expect(firstRadio).not.to.have.attribute('checked'); - }); + await waitForLitRender(element); - it('dispatches event on radio change', async () => { - const firstRadio = element.querySelector('#sbb-radio-1') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; - const checkedRadio = element.querySelector('#sbb-radio-2') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; - const changeSpy = new EventSpy('change'); - const inputSpy = new EventSpy('input'); + expect(radio.checked).to.be.true; + expect(element.value).to.be.equal(radio.value); + }); - checkedRadio.click(); - await changeSpy.calledOnce(); - expect(changeSpy.count).to.be.equal(1); - await inputSpy.calledOnce(); - expect(inputSpy.count).to.be.equal(1); + it('should ignore disabled radios', async () => { + const radio = radios[0]; + radio.checked = true; + radio.disabled = true; + await waitForLitRender(element); - firstRadio.click(); - await waitForLitRender(element); - expect(firstRadio).to.have.attribute('checked'); - }); + expect(element.value).to.be.null; + }); - it('does not select disabled radio on click', async () => { - const firstRadio = element.querySelector('#sbb-radio-1') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; - const disabledRadio = element.querySelector('#sbb-radio-3') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; + it('should update disabled on children', async () => { + element.disabled = true; + await waitForLitRender(element); - disabledRadio.click(); - await waitForLitRender(element); + radios.forEach((r) => expect(r.disabled).to.be.true); - expect(disabledRadio).not.to.have.attribute('checked'); - expect(firstRadio).to.have.attribute('checked'); - }); + element.disabled = false; + await waitForLitRender(element); + expect(radios[0].disabled).to.be.false; + expect(radios[1].disabled).to.be.false; + expect(radios[2].disabled).to.be.true; + }); - it('should update tabIndex on disabled child change', async () => { - const firstRadio = element.querySelector('#sbb-radio-1') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; + it('should update required on children', async () => { + element.required = true; + await waitForLitRender(element); - const secondRadio = element.querySelector('#sbb-radio-2') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; + radios.forEach((r) => expect(r.required).to.be.true); - expect(firstRadio.tabIndex).to.be.equal(0); - expect(secondRadio.tabIndex).to.be.equal(-1); + element.required = false; + await waitForLitRender(element); + radios.forEach((r) => expect(r.required).to.be.false); + }); - firstRadio.disabled = true; - await waitForLitRender(element); + it('should update size on children', async () => { + element.size = 's'; + await waitForLitRender(element); - expect(firstRadio.tabIndex).to.be.equal(-1); - expect(secondRadio.tabIndex).to.be.equal(0); - }); + radios.forEach((r) => expect(r.size).to.be.equal('s')); + }); - it('preserves radio button disabled state after being disabled from group', async () => { - const firstRadio = element.querySelector('#sbb-radio-1') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; - const secondRadio = element.querySelector('#sbb-radio-2') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; - const disabledRadio = element.querySelector('#sbb-radio-3') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; - - element.disabled = true; - await waitForLitRender(element); + it('preserves radio button disabled state after being disabled from group', async () => { + const firstRadio = radios[0]; + const disabledRadio = radios[2]; - disabledRadio.click(); - await waitForLitRender(element); - expect(disabledRadio).not.to.have.attribute('checked'); - expect(firstRadio).to.have.attribute('checked'); + element.disabled = true; + await waitForLitRender(element); + radios.forEach((r) => expect(r.disabled).to.be.true); - secondRadio.click(); - await waitForLitRender(element); - expect(secondRadio).not.to.have.attribute('checked'); + element.disabled = false; + await waitForLitRender(element); + expect(firstRadio.disabled).to.be.false; + expect(disabledRadio.disabled).to.be.true; + }); - element.disabled = false; - await waitForLitRender(element); + describe('events', () => { + it('dispatches event on radio change', async () => { + const radio = radios[1]; + const changeSpy = new EventSpy('change'); + const inputSpy = new EventSpy('input'); - disabledRadio.click(); + radio.click(); await waitForLitRender(element); - expect(disabledRadio).not.to.have.attribute('checked'); - expect(firstRadio).to.have.attribute('checked'); - }); - - it('selects radio on left arrow key pressed', async () => { - const firstRadio = element.querySelector('#sbb-radio-1') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; - firstRadio.focus(); - await waitForLitRender(element); + await changeSpy.calledOnce(); + await inputSpy.calledOnce(); - await sendKeys({ press: 'ArrowLeft' }); - await waitForLitRender(element); + const changeEvent = changeSpy.lastEvent!; + expect(changeSpy.count).to.be.equal(1); + expect(changeEvent.target === radio).to.be.true; - const radio = element.querySelector('#sbb-radio-4'); - expect(radio).to.have.attribute('checked'); + const inputEvent = changeSpy.lastEvent!; + expect(inputSpy.count).to.be.equal(1); + expect(inputEvent.target === radio).to.be.true; - firstRadio.click(); + // A click on a checked radio should not emit any event + radio.click(); await waitForLitRender(element); - expect(firstRadio).to.have.attribute('checked'); + expect(changeSpy.count).to.be.equal(1); + expect(inputSpy.count).to.be.equal(1); }); - it('selects radio on right arrow key pressed', async () => { - const firstRadio = element.querySelector('#sbb-radio-1') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; - - firstRadio.focus(); - await sendKeys({ press: 'ArrowRight' }); - - await waitForLitRender(element); - const radio = element.querySelector('#sbb-radio-2'); - - expect(radio).to.have.attribute('checked'); + it('does not select disabled radio on click', async () => { + const changeSpy = new EventSpy('change'); + const inputSpy = new EventSpy('input'); + const disabledRadio = radios[2]; - firstRadio.click(); + disabledRadio.click(); await waitForLitRender(element); - expect(firstRadio).to.have.attribute('checked'); + expect(disabledRadio.checked).to.be.false; + expect(changeSpy.count).to.be.equal(0); + expect(inputSpy.count).to.be.equal(0); }); + }); + }); - it('wraps around on arrow key navigation', async () => { - const firstRadio = element.querySelector('#sbb-radio-1') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; - const secondRadio = element.querySelector('#sbb-radio-2') as - | SbbRadioButtonElement - | SbbRadioButtonPanelElement; - - secondRadio.click(); - await waitForLitRender(element); - expect(secondRadio).to.have.attribute('checked'); - - secondRadio.focus(); - await waitForLitRender(element); - - await sendKeys({ press: 'ArrowRight' }); - await waitForLitRender(element); - - await sendKeys({ press: 'ArrowRight' }); - await waitForLitRender(element); + describe('with init properties', () => { + let radios: (SbbRadioButtonElement | SbbRadioButtonPanelElement)[]; - const radio = element.querySelector('#sbb-radio-1'); - expect(radio).to.have.attribute('checked'); + beforeEach(async () => { + element = await fixture(html` + + <${tagSingle} id="sbb-radio-1" value="Value one">Value one + <${tagSingle} id="sbb-radio-2" value="Value two">Value two + + `); + radios = Array.from(element.querySelectorAll(selector)); + await waitForLitRender(element); + }); - firstRadio.click(); - await waitForLitRender(element); + it('should correctly set the init state', async () => { + radios.forEach((r) => expect(r.name).to.be.equal('group-2')); + expect(radios[0].checked).to.be.false; + expect(radios[0].tabIndex).to.be.equal(-1); - expect(firstRadio).to.have.attribute('checked'); - }); + expect(radios[1].checked).to.be.true; + expect(radios[1].tabIndex).to.be.equal(0); }); }); - describe('initialization', () => { + describe('value preservation', () => { beforeEach(async () => { element = await fixture(html` +

Other content

`); }); - it('should preserve value when no radios were slotted but slotchange was triggered', () => { + it('should preserve value when no radios match the group value', () => { expect(element.value).to.equal('Value one'); }); it('should restore value when radios are slotted', async () => { + const radioTwo = document.createElement('sbb-radio-button'); + radioTwo.value = 'Value two'; + element.appendChild(radioTwo); + await waitForLitRender(element); + expect(element.value).to.equal('Value one'); + + const radioOne = document.createElement('sbb-radio-button'); + radioOne.value = 'Value one'; + element.appendChild(radioOne); + await waitForLitRender(element); + + expect(element.value).to.equal('Value one'); + expect(radioOne.checked).to.be.true; + }); + + it('checked radios should have priority over group value', async () => { const radioOne = document.createElement('sbb-radio-button'); radioOne.value = 'Value one'; const radioTwo = document.createElement('sbb-radio-button'); radioTwo.value = 'Value two'; + radioTwo.checked = true; element.appendChild(radioTwo); element.appendChild(radioOne); await waitForLitRender(element); - expect(element.value).to.equal('Value one'); - expect(radioOne).to.have.attribute('checked'); + expect(element.value).to.equal('Value two'); + expect(radioOne.checked).to.be.false; + expect(radioTwo.checked).to.be.true; + }); + + it('user interaction should have priority over group value', async () => { + const radioOne = element.querySelector( + 'sbb-radio-button[value="42"]', + )!; + radioOne.click(); + + await waitForLitRender(element); + + expect(element.value).to.equal('42'); }); }); }); diff --git a/src/elements/radio-button/radio-button-group/radio-button-group.stories.ts b/src/elements/radio-button/radio-button-group/radio-button-group.stories.ts index 8c4335990d..862200a5c8 100644 --- a/src/elements/radio-button/radio-button-group/radio-button-group.stories.ts +++ b/src/elements/radio-button/radio-button-group/radio-button-group.stories.ts @@ -39,6 +39,12 @@ const value: InputType = { }, }; +const name: InputType = { + control: { + type: 'text', + }, +}; + const required: InputType = { control: { type: 'boolean', @@ -86,6 +92,7 @@ const ariaLabel: InputType = { const defaultArgTypes: ArgTypes = { value, + name, required, disabled, 'allow-empty-selection': allowEmptySelection, @@ -97,6 +104,7 @@ const defaultArgTypes: ArgTypes = { const defaultArgs: Args = { value: 'Value two', + name: undefined, required: false, disabled: false, 'allow-empty-selection': false, diff --git a/src/elements/radio-button/radio-button-group/radio-button-group.ts b/src/elements/radio-button/radio-button-group/radio-button-group.ts index 5f239ed78e..871ec403a1 100644 --- a/src/elements/radio-button/radio-button-group/radio-button-group.ts +++ b/src/elements/radio-button/radio-button-group/radio-button-group.ts @@ -2,31 +2,25 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { LitElement, html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import { getNextElementIndex, isArrowKeyPressed } from '../../core/a11y.js'; import { SbbConnectedAbortController } from '../../core/controllers.js'; import { forceType, hostAttributes, slotState } from '../../core/decorators.js'; import { EventEmitter } from '../../core/eventing.js'; -import type { SbbHorizontalFrom, SbbOrientation, SbbStateChange } from '../../core/interfaces.js'; +import type { SbbHorizontalFrom, SbbOrientation } from '../../core/interfaces.js'; import { SbbDisabledMixin } from '../../core/mixins.js'; -import type { SbbRadioButtonStateChange, SbbRadioButtonSize } from '../common.js'; +import type { SbbRadioButtonSize } from '../common.js'; import type { SbbRadioButtonPanelElement } from '../radio-button-panel.js'; import type { SbbRadioButtonElement } from '../radio-button.js'; import style from './radio-button-group.scss?lit&inline'; -export type SbbRadioButtonGroupEventDetail = { - value: any | null; - radioButton: SbbRadioButtonElement | SbbRadioButtonPanelElement; -}; +let nextId = 0; /** * It can be used as a container for one or more `sbb-radio-button`. * * @slot - Use the unnamed slot to add `sbb-radio-button` elements to the `sbb-radio-button-group`. * @slot error - Use this to provide a `sbb-form-error` to show an error message. - * @event {CustomEvent} didChange - Deprecated. Only used for React. Will probably be removed once React 19 is available. Emits whenever the `sbb-radio-group` value changes. - * @event {CustomEvent} change - Emits whenever the `sbb-radio-group` value changes. - * @event {CustomEvent} input - Emits whenever the `sbb-radio-group` value changes. + * @event {CustomEvent} didChange - Deprecated. Only used for React. Will probably be removed once React 19 is available. Emits whenever the `sbb-radio-group` value changes. */ export @customElement('sbb-radio-button-group') @@ -59,7 +53,28 @@ class SbbRadioButtonGroupElement extends SbbDisabledMixin(LitElement) { /** * The value of the radio group. */ - @property() public accessor value: any | null; + @property() + public set value(val: any | null) { + this._fallbackValue = val; + if (!this._didLoad) { + return; + } + if (!val) { + this.radioButtons.forEach((r) => (r.checked = false)); + return; + } + const toCheck = this.radioButtons.find((r) => r.value === val); + if (toCheck) { + toCheck.checked = true; + } + } + public get value(): any | null { + return this.radioButtons.find((r) => r.checked && !r.disabled)?.value ?? this._fallbackValue; + } + /** + * Used to preserve the `value` in case the radios are not yet 'loaded' + */ + private _fallbackValue: any | null = null; /** * Size variant. @@ -78,6 +93,10 @@ class SbbRadioButtonGroupElement extends SbbDisabledMixin(LitElement) { @property({ reflect: true }) public accessor orientation: SbbOrientation = 'horizontal'; + @forceType() + @property() + public accessor name: string = `sbb-radio-button-group-${++nextId}`; + /** * List of contained radio buttons. */ @@ -90,13 +109,6 @@ class SbbRadioButtonGroupElement extends SbbDisabledMixin(LitElement) { ).filter((el) => el.closest?.('sbb-radio-button-group') === this); } - private get _enabledRadios(): (SbbRadioButtonElement | SbbRadioButtonPanelElement)[] | undefined { - if (!this.disabled) { - return this.radioButtons.filter((r) => !r.disabled); - } - } - - private _hasSelectionExpansionPanelElement: boolean = false; private _didLoad = false; private _abort = new SbbConnectedAbortController(this); @@ -104,62 +116,29 @@ class SbbRadioButtonGroupElement extends SbbDisabledMixin(LitElement) { * Emits whenever the `sbb-radio-group` value changes. * @deprecated only used for React. Will probably be removed once React 19 is available. */ - private _didChange: EventEmitter = new EventEmitter( + private _didChange: EventEmitter = new EventEmitter( this, SbbRadioButtonGroupElement.events.didChange, ); - /** - * Emits whenever the `sbb-radio-group` value changes. - */ - private _change: EventEmitter = new EventEmitter( - this, - SbbRadioButtonGroupElement.events.change, - ); - - /** - * Emits whenever the `sbb-radio-group` value changes. - */ - private _input: EventEmitter = new EventEmitter( - this, - SbbRadioButtonGroupElement.events.input, - ); - public override connectedCallback(): void { super.connectedCallback(); const signal = this._abort.signal; - this.addEventListener( - 'stateChange', - (e: CustomEvent) => - this._onRadioButtonChange(e as CustomEvent), - { - signal, - passive: true, - }, - ); - this.addEventListener('keydown', (e) => this._handleKeyDown(e), { signal }); - this._hasSelectionExpansionPanelElement = !!this.querySelector?.( - 'sbb-selection-expansion-panel', - ); this.toggleAttribute( 'data-has-panel', !!this.querySelector?.('sbb-selection-expansion-panel, sbb-radio-button-panel'), ); - this._updateRadios(this.value); + + this.addEventListener('change', (e: Event) => this._onRadioChange(e), { + signal, + }); } public override willUpdate(changedProperties: PropertyValues): void { super.willUpdate(changedProperties); - if (changedProperties.has('value')) { - this._valueChanged(this.value); - } if (changedProperties.has('disabled')) { - for (const radio of this.radioButtons) { - radio.tabIndex = this._getRadioTabIndex(radio); - radio.requestUpdate?.('disabled'); - } - this._setFocusableRadio(); + this.radioButtons.forEach((r) => r.requestUpdate?.('disabled')); } if (changedProperties.has('required')) { this.radioButtons.forEach((r) => r.requestUpdate?.('required')); @@ -167,124 +146,58 @@ class SbbRadioButtonGroupElement extends SbbDisabledMixin(LitElement) { if (changedProperties.has('size')) { this.radioButtons.forEach((r) => r.requestUpdate?.('size')); } - } - - private _valueChanged(value: any | undefined): void { - for (const radio of this.radioButtons) { - radio.checked = radio.value === value; - radio.tabIndex = this._getRadioTabIndex(radio); + if (changedProperties.has('name')) { + this._updateRadiosName(); } - this._setFocusableRadio(); } - protected override firstUpdated(changedProperties: PropertyValues): void { + protected override async firstUpdated(changedProperties: PropertyValues): Promise { super.firstUpdated(changedProperties); - this._didLoad = true; - this._updateRadios(this.value); - } - public override disconnectedCallback(): void { - super.disconnectedCallback(); + await this.updateComplete; + this._updateRadioState(); } - private _onRadioButtonChange(event: CustomEvent): void { - event.stopPropagation(); + private _onRadioChange(event: Event): void { + const target = event.target! as SbbRadioButtonElement | SbbRadioButtonPanelElement; - if (!this._didLoad) { + // Only filter radio-buttons event + if (target.localName !== 'sbb-radio-button' && target.localName !== 'sbb-radio-button-panel') { return; } - if (event.detail.type === 'disabled') { - this._updateRadios(this.value); - } else if (event.detail.type === 'checked') { - const radioButton = event.target as SbbRadioButtonElement; - - if (event.detail.checked) { - this.value = radioButton.value; - this._emitChange(radioButton, this.value); - } else if (this.allowEmptySelection) { - this.value = this.radioButtons.find((radio) => radio.checked)?.value; - if (!this.value) { - this._emitChange(radioButton); - } - } - } - } - - private _emitChange(radioButton: SbbRadioButtonElement, value?: string): void { - this._change.emit({ value, radioButton }); - this._input.emit({ value, radioButton }); - this._didChange.emit({ value, radioButton }); - } - - private _updateRadios(initValue?: string): void { - if (!this._didLoad) { - return; - } - - const radioButtons = this.radioButtons; - - this.value = initValue ?? radioButtons.find((radio) => radio.checked)?.value ?? this.value; - - for (const radio of radioButtons) { - radio.checked = radio.value === this.value; - radio.tabIndex = this._getRadioTabIndex(radio); - } - - this._setFocusableRadio(); - } - - private _setFocusableRadio(): void { - const checked = this.radioButtons.find((radio) => radio.checked && !radio.disabled); - - const enabledRadios = this._enabledRadios; - if (!checked && enabledRadios?.length) { - enabledRadios[0].tabIndex = 0; - } + this._fallbackValue = null; // Since the user interacted, the fallbackValue logic does not apply anymore + this._didChange.emit(); } - private _getRadioTabIndex(radio: SbbRadioButtonElement | SbbRadioButtonPanelElement): number { - const isEnabled = !radio.disabled && !this.disabled; - - return (radio.checked || this._hasSelectionExpansionPanelElement) && isEnabled ? 0 : -1; + /** + * Proxy 'name' to child radio-buttons + */ + private _updateRadiosName(): void { + this.radioButtons.forEach((r) => (r.name = this.name)); } - private _handleKeyDown(evt: KeyboardEvent): void { - const enabledRadios = this._enabledRadios; - - if ( - !enabledRadios || - !enabledRadios.length || - // don't trap nested handling - ((evt.target as HTMLElement) !== this && - (evt.target as HTMLElement).parentElement !== this && - (evt.target as HTMLElement).parentElement?.localName !== 'sbb-selection-expansion-panel') - ) { - return; - } - - if (!isArrowKeyPressed(evt)) { - return; - } - - const current: number = enabledRadios.findIndex( - (e: SbbRadioButtonElement | SbbRadioButtonPanelElement) => e === evt.target, - ); - const nextIndex: number = getNextElementIndex(evt, current, enabledRadios.length); - - if (!this._hasSelectionExpansionPanelElement) { - enabledRadios[nextIndex].select(); + /** + * Re-trigger the setter and update the checked state of the radios. + * Mainly used to cover cases where the setter is called before the radios are loaded + */ + private _updateRadioState(): void { + if (this._fallbackValue) { + // eslint-disable-next-line no-self-assign + this.value = this.value; } - - enabledRadios[nextIndex].focus(); - evt.preventDefault(); } protected override render(): TemplateResult { return html`
- this._updateRadios()}> + { + this._updateRadiosName(); + this._updateRadioState(); + }} + >
diff --git a/src/elements/radio-button/radio-button-group/readme.md b/src/elements/radio-button/radio-button-group/readme.md index 0f0d708890..c2aeeda198 100644 --- a/src/elements/radio-button/radio-button-group/readme.md +++ b/src/elements/radio-button/radio-button-group/readme.md @@ -1,10 +1,11 @@ The `sbb-radio-button-group` is a component which can be used as a wrapper for a collection of either [sbb-radio-button](/docs/elements-sbb-radio-button-sbb-radio-button--docs)s, [sbb-radio-button-panel](/docs/elements-sbb-radio-button-sbb-radio-button-panel--docs)s, or [sbb-selection-expansion-panel](/docs/elements-sbb-selection-expansion-panel--docs)s. +Individual radio-buttons inside of a radio-group will inherit the `name` of the group. ```html - + Option one Option two Option three @@ -69,24 +70,23 @@ In order to ensure readability for screen-readers, please provide an `aria-label ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| --------------------- | ----------------------- | ------- | --------------------------------------------------------- | -------------- | --------------------------------------------------------- | -| `allowEmptySelection` | `allow-empty-selection` | public | `boolean` | `false` | Whether the radios can be deselected. | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `horizontalFrom` | `horizontal-from` | public | `SbbHorizontalFrom \| null` | `null` | Overrides the behaviour of `orientation` property. | -| `orientation` | `orientation` | public | `SbbOrientation` | `'horizontal'` | Radio group's orientation, either horizontal or vertical. | -| `radioButtons` | - | public | `(SbbRadioButtonElement \| SbbRadioButtonPanelElement)[]` | | List of contained radio buttons. | -| `required` | `required` | public | `boolean` | `false` | Whether the radio group is required. | -| `size` | `size` | public | `SbbRadioButtonSize` | `'m'` | Size variant. | -| `value` | `value` | public | `any \| null` | | The value of the radio group. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ----------------------- | ------- | --------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------- | +| `allowEmptySelection` | `allow-empty-selection` | public | `boolean` | `false` | Whether the radios can be deselected. | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `horizontalFrom` | `horizontal-from` | public | `SbbHorizontalFrom \| null` | `null` | Overrides the behaviour of `orientation` property. | +| `name` | `name` | public | `string` | `` `sbb-radio-button-group-${++nextId}` `` | | +| `orientation` | `orientation` | public | `SbbOrientation` | `'horizontal'` | Radio group's orientation, either horizontal or vertical. | +| `radioButtons` | - | public | `(SbbRadioButtonElement \| SbbRadioButtonPanelElement)[]` | | List of contained radio buttons. | +| `required` | `required` | public | `boolean` | `false` | Whether the radio group is required. | +| `size` | `size` | public | `SbbRadioButtonSize` | `'m'` | Size variant. | +| `value` | `value` | public | `any \| null` | | The value of the radio group. | ## Events -| Name | Type | Description | Inherited From | -| ----------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------- | -| `change` | `CustomEvent` | Emits whenever the `sbb-radio-group` value changes. | | -| `didChange` | `CustomEvent` | Deprecated. Only used for React. Will probably be removed once React 19 is available. Emits whenever the `sbb-radio-group` value changes. | | -| `input` | `CustomEvent` | Emits whenever the `sbb-radio-group` value changes. | | +| Name | Type | Description | Inherited From | +| ----------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------- | +| `didChange` | `CustomEvent` | Deprecated. Only used for React. Will probably be removed once React 19 is available. Emits whenever the `sbb-radio-group` value changes. | | ## Slots diff --git a/src/elements/radio-button/radio-button-panel/__snapshots__/radio-button-panel.snapshot.spec.snap.js b/src/elements/radio-button/radio-button-panel/__snapshots__/radio-button-panel.snapshot.spec.snap.js index 94bc56a5db..f5b0760c69 100644 --- a/src/elements/radio-button/radio-button-panel/__snapshots__/radio-button-panel.snapshot.spec.snap.js +++ b/src/elements/radio-button/radio-button-panel/__snapshots__/radio-button-panel.snapshot.spec.snap.js @@ -3,11 +3,10 @@ export const snapshots = {}; snapshots["sbb-radio-button-panel should render unchecked DOM"] = ` Label @@ -28,13 +27,6 @@ snapshots["sbb-radio-button-panel should render unchecked Shadow DOM"] =
- @@ -50,13 +42,12 @@ snapshots["sbb-radio-button-panel should render unchecked Shadow DOM"] = snapshots["sbb-radio-button-panel should render checked DOM"] = ` Label @@ -77,14 +68,6 @@ snapshots["sbb-radio-button-panel should render checked Shadow DOM"] =
- @@ -98,7 +81,7 @@ snapshots["sbb-radio-button-panel should render checked Shadow DOM"] = `; /* end snapshot sbb-radio-button-panel should render checked Shadow DOM */ -snapshots["sbb-radio-button-panel Unchecked - A11y tree Chrome"] = +snapshots["sbb-radio-button-panel should render unchecked A11y tree Chrome"] = `

{ "role": "WebArea", @@ -106,16 +89,32 @@ snapshots["sbb-radio-button-panel Unchecked - A11y tree Chrome"] = "children": [ { "role": "radio", - "name": "Label", + "name": "Label Suffix Subtext", "checked": false } ] }

`; -/* end snapshot sbb-radio-button-panel Unchecked - A11y tree Chrome */ +/* end snapshot sbb-radio-button-panel should render unchecked A11y tree Chrome */ + +snapshots["sbb-radio-button-panel should render unchecked A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "", + "children": [ + { + "role": "radio", + "name": "Label Suffix Subtext" + } + ] +} +

+`; +/* end snapshot sbb-radio-button-panel should render unchecked A11y tree Firefox */ -snapshots["sbb-radio-button-panel Checked - A11y tree Chrome"] = +snapshots["sbb-radio-button-panel should render checked A11y tree Chrome"] = `

{ "role": "WebArea", @@ -123,16 +122,16 @@ snapshots["sbb-radio-button-panel Checked - A11y tree Chrome"] = "children": [ { "role": "radio", - "name": "Label", + "name": "Label Suffix Subtext", "checked": true } ] }

`; -/* end snapshot sbb-radio-button-panel Checked - A11y tree Chrome */ +/* end snapshot sbb-radio-button-panel should render checked A11y tree Chrome */ -snapshots["sbb-radio-button-panel Unchecked - A11y tree Firefox"] = +snapshots["sbb-radio-button-panel should render checked A11y tree Firefox"] = `

{ "role": "document", @@ -140,15 +139,34 @@ snapshots["sbb-radio-button-panel Unchecked - A11y tree Firefox"] = "children": [ { "role": "radio", - "name": "Label" + "name": "Label Suffix Subtext", + "checked": true } ] }

`; -/* end snapshot sbb-radio-button-panel Unchecked - A11y tree Firefox */ +/* end snapshot sbb-radio-button-panel should render checked A11y tree Firefox */ -snapshots["sbb-radio-button-panel Checked - A11y tree Firefox"] = +snapshots["sbb-radio-button-panel Disabled - A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "", + "children": [ + { + "role": "radio", + "name": "Label", + "disabled": true, + "checked": false + } + ] +} +

+`; +/* end snapshot sbb-radio-button-panel Disabled - A11y tree Chrome */ + +snapshots["sbb-radio-button-panel Disabled - A11y tree Firefox"] = `

{ "role": "document", @@ -157,11 +175,227 @@ snapshots["sbb-radio-button-panel Checked - A11y tree Firefox"] = { "role": "radio", "name": "Label", + "disabled": true + } + ] +} +

+`; +/* end snapshot sbb-radio-button-panel Disabled - A11y tree Firefox */ + +snapshots["sbb-radio-button-panel renders DOM"] = +` + Label + + Subtext + + + Suffix + + +`; +/* end snapshot sbb-radio-button-panel renders DOM */ + +snapshots["sbb-radio-button-panel renders Shadow DOM"] = +` +`; +/* end snapshot sbb-radio-button-panel renders Shadow DOM */ + +snapshots["sbb-radio-button-panel renders checked DOM"] = +` + Label + + Subtext + + + Suffix + + +`; +/* end snapshot sbb-radio-button-panel renders checked DOM */ + +snapshots["sbb-radio-button-panel renders checked Shadow DOM"] = +` +`; +/* end snapshot sbb-radio-button-panel renders checked Shadow DOM */ + +snapshots["sbb-radio-button-panel renders A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "", + "children": [ + { + "role": "radio", + "name": "Label Suffix Subtext", + "checked": false + } + ] +} +

+`; +/* end snapshot sbb-radio-button-panel renders A11y tree Chrome */ + +snapshots["sbb-radio-button-panel renders A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "", + "children": [ + { + "role": "radio", + "name": "Label Suffix Subtext" + } + ] +} +

+`; +/* end snapshot sbb-radio-button-panel renders A11y tree Firefox */ + +snapshots["sbb-radio-button-panel renders checked A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "", + "children": [ + { + "role": "radio", + "name": "Label Suffix Subtext", + "checked": true + } + ] +} +

+`; +/* end snapshot sbb-radio-button-panel renders checked A11y tree Chrome */ + +snapshots["sbb-radio-button-panel renders checked A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "", + "children": [ + { + "role": "radio", + "name": "Label Suffix Subtext", "checked": true } ] }

`; -/* end snapshot sbb-radio-button-panel Checked - A11y tree Firefox */ +/* end snapshot sbb-radio-button-panel renders checked A11y tree Firefox */ + +snapshots["sbb-radio-button-panel renders disabled - A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "", + "children": [ + { + "role": "radio", + "name": "Label", + "disabled": true, + "checked": false + } + ] +} +

+`; +/* end snapshot sbb-radio-button-panel renders disabled - A11y tree Chrome */ + +snapshots["sbb-radio-button-panel renders disabled - A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "", + "children": [ + { + "role": "radio", + "name": "Label", + "disabled": true + } + ] +} +

+`; +/* end snapshot sbb-radio-button-panel renders disabled - A11y tree Firefox */ + +snapshots["sbb-radio-button-panel renders required - A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "", + "children": [ + { + "role": "radio", + "name": "", + "checked": false + } + ] +} +

+`; +/* end snapshot sbb-radio-button-panel renders required - A11y tree Chrome */ + +snapshots["sbb-radio-button-panel renders required - A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "", + "children": [ + { + "role": "radio", + "name": "", + "required": true + } + ] +} +

+`; +/* end snapshot sbb-radio-button-panel renders required - A11y tree Firefox */ diff --git a/src/elements/radio-button/radio-button-panel/radio-button-panel.snapshot.spec.ts b/src/elements/radio-button/radio-button-panel/radio-button-panel.snapshot.spec.ts index afd8ec850d..3b11770541 100644 --- a/src/elements/radio-button/radio-button-panel/radio-button-panel.snapshot.spec.ts +++ b/src/elements/radio-button/radio-button-panel/radio-button-panel.snapshot.spec.ts @@ -8,10 +8,10 @@ import { SbbRadioButtonPanelElement } from './radio-button-panel.js'; describe('sbb-radio-button-panel', () => { let element: SbbRadioButtonPanelElement; - describe('should render unchecked', async () => { + describe('renders', async () => { beforeEach(async () => { element = (await fixture( - html` + html` Label Subtext Suffix @@ -27,12 +27,14 @@ describe('sbb-radio-button-panel', () => { it('Shadow DOM', async () => { await expect(element).shadowDom.to.be.equalSnapshot(); }); + + testA11yTreeSnapshot(); }); - describe('should render checked', async () => { + describe('renders checked', async () => { beforeEach(async () => { element = await fixture( - html` + html` Label Subtext Suffix @@ -47,15 +49,20 @@ describe('sbb-radio-button-panel', () => { it('Shadow DOM', async () => { await expect(element).shadowDom.to.be.equalSnapshot(); }); + + testA11yTreeSnapshot(); }); testA11yTreeSnapshot( - html`Label`, - 'Unchecked - A11y tree', + html`Label`, + 'renders disabled - A11y tree', ); - testA11yTreeSnapshot( - html`Label`, - 'Checked - A11y tree', + html``, + 'renders required - A11y tree', ); }); diff --git a/src/elements/radio-button/radio-button-panel/radio-button-panel.spec.ts b/src/elements/radio-button/radio-button-panel/radio-button-panel.spec.ts index ab9446623c..9020f899ca 100644 --- a/src/elements/radio-button/radio-button-panel/radio-button-panel.spec.ts +++ b/src/elements/radio-button/radio-button-panel/radio-button-panel.spec.ts @@ -30,7 +30,8 @@ describe(`sbb-radio-button`, () => { element.click(); await waitForLitRender(element); - expect(element).to.have.attribute('checked'); + expect(element.checked).to.be.true; + expect(element).to.have.attribute('data-checked'); await stateChange.calledOnce(); expect(stateChange.count).to.be.equal(1); }); @@ -40,13 +41,13 @@ describe(`sbb-radio-button`, () => { element.click(); await waitForLitRender(element); - expect(element).to.have.attribute('checked'); + expect(element.checked).to.be.true; await stateChange.calledOnce(); expect(stateChange.count).to.be.equal(1); element.click(); await waitForLitRender(element); - expect(element).to.have.attribute('checked'); + expect(element.checked).to.be.true; await stateChange.calledOnce(); expect(stateChange.count).to.be.equal(1); }); @@ -57,13 +58,13 @@ describe(`sbb-radio-button`, () => { element.allowEmptySelection = true; element.click(); await waitForLitRender(element); - expect(element).to.have.attribute('checked'); + expect(element.checked).to.be.true; await stateChange.calledOnce(); expect(stateChange.count).to.be.equal(1); element.click(); await waitForLitRender(element); - expect(element).not.to.have.attribute('checked'); + expect(element.checked).to.be.false; await stateChange.calledTimes(2); expect(stateChange.count).to.be.equal(2); }); diff --git a/src/elements/radio-button/radio-button-panel/radio-button-panel.ssr.spec.ts b/src/elements/radio-button/radio-button-panel/radio-button-panel.ssr.spec.ts index 3cb06155fa..edaf29b1ec 100644 --- a/src/elements/radio-button/radio-button-panel/radio-button-panel.ssr.spec.ts +++ b/src/elements/radio-button/radio-button-panel/radio-button-panel.ssr.spec.ts @@ -6,18 +6,37 @@ import { ssrHydratedFixture } from '../../core/testing/private.js'; import { SbbRadioButtonPanelElement } from './radio-button-panel.js'; describe(`sbb-radio-button-panel ssr`, () => { - let root: SbbRadioButtonPanelElement; - - beforeEach(async () => { - root = await ssrHydratedFixture( + it('renders', async () => { + const root = await ssrHydratedFixture( html`Value label`, { modules: ['./radio-button-panel.js'], }, ); + assert.instanceOf(root, SbbRadioButtonPanelElement); }); - it('renders', () => { + it('renders checked', async () => { + const root = await ssrHydratedFixture( + html`Value label`, + { + modules: ['./radio-button-panel.js'], + }, + ); + assert.instanceOf(root, SbbRadioButtonPanelElement); + }); + + it('renders standalone group', async () => { + const root = await ssrHydratedFixture( + html` + Value 1 + Value 2 + Value 3 + `, + { + modules: ['./radio-button-panel.js'], + }, + ); assert.instanceOf(root, SbbRadioButtonPanelElement); }); }); diff --git a/src/elements/radio-button/radio-button-panel/radio-button-panel.stories.ts b/src/elements/radio-button/radio-button-panel/radio-button-panel.stories.ts index 965a05747d..4860798f6b 100644 --- a/src/elements/radio-button/radio-button-panel/radio-button-panel.stories.ts +++ b/src/elements/radio-button/radio-button-panel/radio-button-panel.stories.ts @@ -1,12 +1,15 @@ +import { withActions } from '@storybook/addon-actions/decorator'; import type { InputType } from '@storybook/types'; -import type { Args, ArgTypes, Meta, StoryObj } from '@storybook/web-components'; -import { html, type TemplateResult } from 'lit'; +import type { Args, ArgTypes, Decorator, Meta, StoryObj } from '@storybook/web-components'; +import { html, nothing, type TemplateResult } from 'lit'; +import { repeat } from 'lit/directives/repeat.js'; import { sbbSpread } from '../../../storybook/helpers/spread.js'; import readme from './readme.md?raw'; import '../../icon.js'; +import '../../title.js'; import '../../card/card-badge.js'; import '../radio-button-panel.js'; @@ -85,7 +88,7 @@ const defaultArgs: Args = { const cardBadge = (): TemplateResult => html`%`; const DefaultTemplate = ({ labelBoldClass, ...args }: Args): TemplateResult => - html`${labelBoldClass ? html`Label` : 'Label'} Subtext @@ -103,6 +106,30 @@ const DefaultTemplate = ({ labelBoldClass, ...args }: Args): TemplateResult => ${cardBadge()} `; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const StandaloneTemplate = ({ value, ...args }: Args): TemplateResult => html` +
+ Group 1 + ${repeat( + new Array(3), + (_, i) => + html`
+ ${DefaultTemplate({ ...args, value: `value-${i + 1}`, name: `group-1` })} +
`, + )} +
+ + Group 2 + ${repeat( + new Array(4), + (_, i) => + html`
+ ${DefaultTemplate({ ...args, value: `value-${i + 1}`, name: `group-2` })} +
`, + )} + +`; + export const Default: StoryObj = { render: DefaultTemplate, argTypes: defaultArgTypes, @@ -151,8 +178,18 @@ export const CheckedBold: StoryObj = { args: { ...defaultArgs, checked: true, labelBoldClass: true }, }; +export const StandaloneGroup: StoryObj = { + render: StandaloneTemplate, + argTypes: defaultArgTypes, + args: { ...defaultArgs }, +}; + const meta: Meta = { parameters: { + decorators: [withActions as Decorator], + actions: { + handles: ['change', 'input'], + }, docs: { extractComponentDescription: () => readme, }, diff --git a/src/elements/radio-button/radio-button-panel/radio-button-panel.ts b/src/elements/radio-button/radio-button-panel/radio-button-panel.ts index d4b8946a4a..a68c565acf 100644 --- a/src/elements/radio-button/radio-button-panel/radio-button-panel.ts +++ b/src/elements/radio-button/radio-button-panel/radio-button-panel.ts @@ -11,6 +11,7 @@ import { customElement, property } from 'lit/decorators.js'; import { getOverride, slotState } from '../../core/decorators.js'; import { panelCommonStyle, + type SbbFormAssociatedRadioButtonMixinType, SbbPanelMixin, type SbbPanelSize, SbbUpdateSchedulerMixin, @@ -27,6 +28,9 @@ import '../../screen-reader-only.js'; * @slot subtext - Slot used to render a subtext under the label. * @slot suffix - Slot used to render additional content after the label. * @slot badge - Use this slot to provide a `sbb-card-badge` (optional). + * @event {Event} change - Fired on change. + * @event {InputEvent} input - Fired on input. + * @overrideType value - string | null */ export @customElement('sbb-radio-button-panel') @@ -39,6 +43,8 @@ class SbbRadioButtonPanelElement extends SbbPanelMixin( // FIXME using ...super.events requires: https://github.com/sbb-design-systems/lyne-components/issues/2600 public static readonly events = { stateChange: 'stateChange', + change: 'change', + input: 'input', panelConnected: 'panelConnected', } as const; @@ -47,6 +53,13 @@ class SbbRadioButtonPanelElement extends SbbPanelMixin( @getOverride((i, v) => (i.group?.size ? (i.group.size === 'xs' ? 's' : i.group.size) : v)) public accessor size: SbbPanelSize = 'm'; + private _hasSelectionExpansionPanelElement: boolean = false; + + public override connectedCallback(): void { + super.connectedCallback(); + this._hasSelectionExpansionPanelElement = !!this.closest?.('sbb-selection-expansion-panel'); + } + protected override async willUpdate(changedProperties: PropertyValues): Promise { super.willUpdate(changedProperties); @@ -55,6 +68,31 @@ class SbbRadioButtonPanelElement extends SbbPanelMixin( } } + /** + * As an exception, panels with an expansion-panel attached are always focusable + */ + protected override updateFocusableRadios(): void { + super.updateFocusableRadios(); + const radios = Array.from(this.associatedRadioButtons ?? []) as SbbRadioButtonPanelElement[]; + + radios + .filter((r) => !r.disabled && r._hasSelectionExpansionPanelElement) + .forEach((r) => (r.tabIndex = 0)); + } + + /** + * As an exception, radio-panels with an expansion-panel attached are not checked automatically when navigating by keyboard + */ + protected override async navigateByKeyboard( + next: SbbFormAssociatedRadioButtonMixinType, + ): Promise { + if (!this._hasSelectionExpansionPanelElement) { + await super.navigateByKeyboard(next); + } else { + next.focus(); + } + } + protected override render(): TemplateResult { return html`