From 4756494e1c6d9e0cf50fda197d0aa8b8e4f195f9 Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Tue, 23 Jan 2024 09:34:09 +0100 Subject: [PATCH 01/20] chore(selection-panel): set chromatic min-height --- .../selection-panel.stories.ts | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/components/selection-panel/selection-panel.stories.ts b/src/components/selection-panel/selection-panel.stories.ts index 83eaae6ef0..aa00ff2002 100644 --- a/src/components/selection-panel/selection-panel.stories.ts +++ b/src/components/selection-panel/selection-panel.stories.ts @@ -149,7 +149,11 @@ const WithCheckboxGroupTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + ${cardBadge()} Value one ${suffixAndSubtext()} @@ -180,6 +184,7 @@ const WithRadioButtonGroupTemplate = ({ orientation="vertical" horizontal-from="large" ?allow-empty-selection=${allowEmptySelection} + style="${isChromatic() ? 'min-height: 750px;' : ''}" > ${cardBadge()} @@ -210,7 +215,11 @@ const TicketsOptionsExampleTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + ${cardBadge()} Saving ${suffixAndSubtext()} @@ -338,7 +347,11 @@ const NestedCheckboxTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + Main Option 1 @@ -370,6 +383,7 @@ const WithCheckboxesErrorMessageTemplate = ({ { const checkboxGroup = event.currentTarget as HTMLElement; const hasChecked = Array.from(checkboxGroup.querySelectorAll('sbb-checkbox')).some( @@ -719,7 +733,9 @@ export const NestedCheckboxes: StoryObj = { const meta: Meta = { decorators: [ - (story) => html`
${story()}
`, + (story) => html` +
${story()}
+ `, withActions as Decorator, ], parameters: { From d56d30bb320b911e6ef2d33b7b65c531f199fc29 Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Tue, 23 Jan 2024 09:52:40 +0100 Subject: [PATCH 02/20] chore(selection-panel): set chromatic min-height --- src/components/selection-panel/selection-panel.stories.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/selection-panel/selection-panel.stories.ts b/src/components/selection-panel/selection-panel.stories.ts index aa00ff2002..0540241621 100644 --- a/src/components/selection-panel/selection-panel.stories.ts +++ b/src/components/selection-panel/selection-panel.stories.ts @@ -733,9 +733,7 @@ export const NestedCheckboxes: StoryObj = { const meta: Meta = { decorators: [ - (story) => html` -
${story()}
- `, + (story) => html`
${story()}
`, withActions as Decorator, ], parameters: { From b9398286670e51b4f26b98abe1ed1d8586039d0c Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Mon, 22 Jan 2024 11:48:45 +0100 Subject: [PATCH 03/20] refactor(selection-panel): improved open/close flow and animation --- src/components/checkbox/checkbox/checkbox.ts | 2 +- .../radio-button/radio-button/radio-button.ts | 2 +- .../selection-panel/selection-panel.e2e.ts | 113 ++++++++-------- .../selection-panel/selection-panel.scss | 62 +++++++-- .../selection-panel/selection-panel.ts | 127 ++++++++---------- 5 files changed, 169 insertions(+), 137 deletions(-) diff --git a/src/components/checkbox/checkbox/checkbox.ts b/src/components/checkbox/checkbox/checkbox.ts index d614c5178e..77d1623f2b 100644 --- a/src/components/checkbox/checkbox/checkbox.ts +++ b/src/components/checkbox/checkbox/checkbox.ts @@ -183,7 +183,6 @@ export class SbbCheckboxElement extends UpdateScheduler(LitElement) { this.addEventListener('click', (e) => this._handleClick(e), { signal }); this.addEventListener('keyup', (e) => this._handleKeyup(e), { signal }); this._handlerRepository.connect(); - this._checkboxLoaded.emit(); // We need to call requestUpdate to update the reflected attributes ['disabled', 'required', 'size'].forEach((p) => this.requestUpdate(p)); @@ -202,6 +201,7 @@ export class SbbCheckboxElement extends UpdateScheduler(LitElement) { // We need to wait for the selection-panel to be fully initialized this.startUpdate(); setTimeout(() => { + this._checkboxLoaded.emit(); this._isSelectionPanelInput && this._updateExpandedLabel(); this.completeUpdate(); }); diff --git a/src/components/radio-button/radio-button/radio-button.ts b/src/components/radio-button/radio-button/radio-button.ts index 6271aa54c1..d00225ebd1 100644 --- a/src/components/radio-button/radio-button/radio-button.ts +++ b/src/components/radio-button/radio-button/radio-button.ts @@ -197,7 +197,6 @@ export class SbbRadioButtonElement extends UpdateScheduler(LitElement) { this.addEventListener('click', (e) => this._handleClick(e), { signal }); this.addEventListener('keydown', (e) => this._handleKeyDown(e), { signal }); this._handlerRepository.connect(); - this._radioButtonLoaded.emit(); // We need to call requestUpdate to update the reflected attributes ['disabled', 'required', 'size'].forEach((p) => this.requestUpdate(p)); @@ -216,6 +215,7 @@ export class SbbRadioButtonElement extends UpdateScheduler(LitElement) { // We need to wait for the selection-panel to be fully initialized this.startUpdate(); setTimeout(() => { + this._radioButtonLoaded.emit(); this._isSelectionPanelInput && this._updateExpandedLabel(); this.completeUpdate(); }); diff --git a/src/components/selection-panel/selection-panel.e2e.ts b/src/components/selection-panel/selection-panel.e2e.ts index b51b11b369..aef720dc42 100644 --- a/src/components/selection-panel/selection-panel.e2e.ts +++ b/src/components/selection-panel/selection-panel.e2e.ts @@ -57,25 +57,19 @@ describe('sbb-selection-panel', () => { /* eslint-enable lit/binding-positions */ }; - const forceOpenTest = async ( - wrapper: SbbRadioButtonGroupElement | SbbCheckboxGroupElement, - secondInput: SbbRadioButtonElement | SbbCheckboxElement, - secondContent: HTMLDivElement, - ): Promise => { + const forceOpenTest = async (wrapper: SbbRadioButtonGroupElement | SbbCheckboxGroupElement, secondInput: SbbRadioButtonElement | SbbCheckboxElement): Promise => { elements.forEach((e) => (e.forceOpen = true)); await waitForLitRender(wrapper); - elements.forEach((e) => { - const panel = e.shadowRoot!.querySelector('.sbb-selection-panel__content--wrapper'); - expect(panel).to.have.attribute('data-expanded', ''); - }); + for (const el of elements) { + await waitForCondition(() => el.getAttribute('data-state') === 'opened'); + expect(el).to.have.attribute('data-state', 'opened'); + } expect(secondInput).not.to.have.attribute('checked'); - expect(secondContent).to.have.attribute('data-expanded'); secondInput.click(); await waitForLitRender(wrapper); expect(secondInput).to.have.attribute('checked'); - expect(secondContent).to.have.attribute('data-expanded'); }; const preservesDisabled = async ( @@ -127,54 +121,61 @@ describe('sbb-selection-panel', () => { let wrapper: SbbRadioButtonGroupElement; let firstPanel: SbbSelectionPanelElement; let firstInput: SbbRadioButtonElement; - let firstContent: HTMLDivElement; let secondPanel: SbbSelectionPanelElement; let secondInput: SbbRadioButtonElement; - let secondContent: HTMLDivElement; let disabledInput: SbbRadioButtonElement; + let willOpenEventSpy, didOpenEventSpy: EventSpy; beforeEach(async () => { + willOpenEventSpy = new EventSpy(SbbSelectionPanelElement.events.willOpen); + didOpenEventSpy = new EventSpy(SbbSelectionPanelElement.events.didOpen); + await fixture(getPageContent('radio-button')); elements = Array.from(document.querySelectorAll('sbb-selection-panel')); wrapper = document.querySelector('sbb-radio-button-group')!; firstPanel = document.querySelector('#sbb-selection-panel-1')!; firstInput = document.querySelector('#sbb-input-1')!; - firstContent = firstPanel.shadowRoot!.querySelector( - '.sbb-selection-panel__content--wrapper', - )!; secondPanel = document.querySelector('#sbb-selection-panel-2')!; secondInput = document.querySelector('#sbb-input-2')!; - secondContent = secondPanel.shadowRoot!.querySelector( - '.sbb-selection-panel__content--wrapper', - )!; disabledInput = document.querySelector('#sbb-input-3')!; }); it('renders', () => { elements.forEach((e) => assert.instanceOf(e, SbbSelectionPanelElement)); + assert.instanceOf(firstPanel, SbbSelectionPanelElement); + assert.instanceOf(firstInput, SbbRadioButtonElement); + assert.instanceOf(secondPanel, SbbSelectionPanelElement); + assert.instanceOf(secondInput, SbbRadioButtonElement); }); it('selects input on click and shows related content', async () => { - assert.instanceOf(firstPanel, SbbSelectionPanelElement); - assert.instanceOf(firstInput, SbbRadioButtonElement); + willOpenEventSpy = new EventSpy(SbbSelectionPanelElement.events.willOpen); + didOpenEventSpy = new EventSpy(SbbSelectionPanelElement.events.didOpen); + + await waitForLitRender(wrapper); + expect(firstInput).not.to.have.attribute('checked'); - expect(firstContent).not.to.have.attribute('data-expanded'); + expect(firstPanel).to.have.attribute('data-state', 'closed'); - assert.instanceOf(secondPanel, SbbSelectionPanelElement); - assert.instanceOf(secondInput, SbbRadioButtonElement); expect(secondInput).not.to.have.attribute('checked'); - expect(secondContent).not.to.have.attribute('data-expanded'); + expect(secondPanel).to.have.attribute('data-state', 'closed'); secondInput.click(); + + await waitForCondition(() => willOpenEventSpy.events.length === 1); + await waitForCondition(() => didOpenEventSpy.events.length === 1); await waitForLitRender(wrapper); + + expect(willOpenEventSpy.count).to.be.equal(1); + expect(didOpenEventSpy.count).to.be.equal(1); expect(firstInput).not.to.have.attribute('checked'); - expect(firstContent).not.to.have.attribute('data-expanded'); + expect(firstPanel).to.have.attribute('data-state', 'closed'); expect(secondInput).to.have.attribute('checked'); - expect(secondContent).to.have.attribute('data-expanded', ''); + expect(secondPanel).to.have.attribute('data-state', 'opened'); }); it('always displays related content with forceOpen', async () => { - await forceOpenTest(wrapper, secondInput, secondContent); + await forceOpenTest(wrapper, secondInput); }); it('dispatches event on input change', async () => { @@ -456,71 +457,75 @@ describe('sbb-selection-panel', () => { let wrapper: SbbCheckboxGroupElement; let firstPanel: SbbSelectionPanelElement; let firstInput: SbbCheckboxElement; - let firstContent: HTMLDivElement; let secondPanel: SbbSelectionPanelElement; let secondInput: SbbCheckboxElement; - let secondContent: HTMLDivElement; let disabledInput: SbbCheckboxElement; + let willOpenEventSpy, didOpenEventSpy, willCloseEventSpy, didCloseEventSpy: EventSpy; beforeEach(async () => { + willOpenEventSpy = new EventSpy(SbbSelectionPanelElement.events.willOpen); + didOpenEventSpy = new EventSpy(SbbSelectionPanelElement.events.didOpen); + willCloseEventSpy = new EventSpy(SbbSelectionPanelElement.events.willClose); + didCloseEventSpy = new EventSpy(SbbSelectionPanelElement.events.didClose); + await fixture(getPageContent('checkbox')); elements = Array.from(document.querySelectorAll('sbb-selection-panel')); wrapper = document.querySelector('sbb-checkbox-group')!; firstPanel = document.querySelector('#sbb-selection-panel-1')!; firstInput = document.querySelector('#sbb-input-1')!; - firstContent = firstPanel.shadowRoot!.querySelector( - '.sbb-selection-panel__content--wrapper', - )!; secondPanel = document.querySelector('#sbb-selection-panel-2')!; secondInput = document.querySelector('#sbb-input-2')!; - secondContent = secondPanel.shadowRoot!.querySelector( - '.sbb-selection-panel__content--wrapper', - )!; disabledInput = document.querySelector('#sbb-input-3')!; }); it('renders', () => { elements.forEach((e) => assert.instanceOf(e, SbbSelectionPanelElement)); + + assert.instanceOf(firstPanel, SbbSelectionPanelElement); + assert.instanceOf(firstInput, SbbCheckboxElement); + assert.instanceOf(secondPanel, SbbSelectionPanelElement); + assert.instanceOf(secondInput, SbbCheckboxElement); }); it('selects input on click and shows related content', async () => { await waitForLitRender(wrapper); + await waitForCondition(() => didOpenEventSpy.events.length === 1); - assert.instanceOf(firstPanel, SbbSelectionPanelElement); - assert.instanceOf(firstInput, SbbCheckboxElement); - - // TODO fix: should be 'opened', actual is 'close'. - // we have to rethink the open/close flow to make it work - //expect(firstPanel).to.have.attribute('data-state', 'opened'); + expect(willOpenEventSpy.count).to.be.equal(1); + expect(didOpenEventSpy.count).to.be.equal(1); + expect(firstPanel).to.have.attribute('data-state', 'opened'); expect(firstInput).to.have.attribute('checked'); - expect(firstContent).to.have.attribute('data-expanded', ''); - - assert.instanceOf(secondPanel, SbbSelectionPanelElement); - assert.instanceOf(secondInput, SbbCheckboxElement); - expect(firstPanel).to.have.attribute('data-state', 'closed'); + expect(secondPanel).to.have.attribute('data-state', 'closed'); expect(secondInput).not.to.have.attribute('checked'); - expect(secondContent).not.to.have.attribute('data-expanded'); secondInput.click(); await waitForLitRender(wrapper); + await waitForCondition(() => didOpenEventSpy.events.length === 2); + + expect(willOpenEventSpy.count).to.be.equal(2); + expect(didOpenEventSpy.count).to.be.equal(2); expect(firstInput).to.have.attribute('checked'); - expect(firstContent).to.have.attribute('data-expanded', ''); + expect(firstPanel).to.have.attribute('data-state', 'opened'); expect(secondInput).to.have.attribute('checked'); - expect(secondContent).to.have.attribute('data-expanded', ''); + expect(secondPanel).to.have.attribute('data-state', 'opened'); }); it('deselects input on click and hides related content', async () => { + await waitForCondition(() => firstPanel.getAttribute('data-state') === 'opened'); expect(firstInput).to.have.attribute('checked'); - expect(firstContent).to.have.attribute('data-expanded'); + expect(firstPanel).to.have.attribute('data-state', 'opened'); firstInput.click(); - await waitForLitRender(wrapper); + + await waitForCondition(() => didCloseEventSpy.events.length === 1); + expect(willCloseEventSpy.count).to.be.equal(1); + expect(didCloseEventSpy.count).to.be.equal(1); expect(firstInput).not.to.have.attribute('checked'); - expect(firstContent).not.to.have.attribute('data-expanded'); + expect(firstPanel).to.have.attribute('data-state', 'closed'); }); it('always displays related content with forceOpen', async () => { - await forceOpenTest(wrapper, secondInput, secondContent); + await forceOpenTest(wrapper, secondInput); }); it('dispatches event on input change', async () => { diff --git a/src/components/selection-panel/selection-panel.scss b/src/components/selection-panel/selection-panel.scss index dc0ef39b06..5bad5a110a 100644 --- a/src/components/selection-panel/selection-panel.scss +++ b/src/components/selection-panel/selection-panel.scss @@ -14,15 +14,7 @@ --sbb-selection-panel-input-padding: var(--sbb-spacing-responsive-xs) var(--sbb-spacing-responsive-xxs); --sbb-selection-panel-content-visibility: hidden; - --sbb-selection-panel-content-grid-template-rows: 0fr; - --sbb-selection-panel-content-opacity: 0; --sbb-selection-panel-content-padding-inline: var(--sbb-spacing-responsive-xxs); - --sbb-selection-panel-content-transition: grid-template-rows - var(--sbb-selection-panel-animation-duration) var(--sbb-animation-easing), - opacity var(--sbb-selection-panel-animation-duration) var(--sbb-animation-easing); - - // As the selection panel has always a white/milk background, we have to fix the focus outline color - // to default color for cases where the selection panel is used in a negative context. --sbb-focus-outline-color: var(--sbb-focus-outline-color-default); display: contents; @@ -53,8 +45,7 @@ --sbb-selection-panel-animation-duration: 0.1ms; } -:host([data-slot-names~='content'][force-open]), -:host([data-slot-names~='content'][data-checked]) { +:host([data-slot-names~='content']:where([data-state='opening'], [data-state='opened'])) { --sbb-selection-panel-input-padding: var(--sbb-spacing-responsive-xs) var(--sbb-spacing-responsive-xxs) var(--sbb-spacing-responsive-xxs) var(--sbb-spacing-responsive-xxs); @@ -93,10 +84,23 @@ .sbb-selection-panel__content--wrapper { display: grid; - grid-template-rows: var(--sbb-selection-panel-content-grid-template-rows); visibility: var(--sbb-selection-panel-content-visibility); - opacity: var(--sbb-selection-panel-content-opacity); - transition: var(--sbb-selection-panel-content-transition); + grid-template-rows: 0fr; + opacity: 0; + + :host(:where([data-state='opening'], [data-state='opened'])) & { + animation-name: open, open-opacity; + animation-fill-mode: forwards; + animation-duration: var(--sbb-selection-panel-animation-duration); + animation-timing-function: var(--sbb-animation-easing); + animation-delay: 0s, var(--sbb-selection-panel-animation-duration); + } + + :host(:where([data-state='closing'])) & { + animation-name: close; + animation-duration: var(--sbb-selection-panel-animation-duration); + animation-timing-function: var(--sbb-animation-easing); + } :host(:not([data-slot-names~='content'])) & { display: none; @@ -126,3 +130,35 @@ sbb-divider { property: padding; } } + +@keyframes open { + from { + grid-template-rows: 0fr; + } + + to { + grid-template-rows: 1fr; + } +} + +@keyframes open-opacity { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes close { + from { + grid-template-rows: 1fr; + opacity: 1; + } + + to { + grid-template-rows: 0fr; + opacity: 0; + } +} diff --git a/src/components/selection-panel/selection-panel.ts b/src/components/selection-panel/selection-panel.ts index 3337957462..9b96b66f65 100644 --- a/src/components/selection-panel/selection-panel.ts +++ b/src/components/selection-panel/selection-panel.ts @@ -1,4 +1,4 @@ -import type { CSSResultGroup, TemplateResult } from 'lit'; +import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { ref } from 'lit/directives/ref.js'; @@ -38,7 +38,7 @@ export class SbbSelectionPanelElement extends LitElement { @property() public color: 'white' | 'milk' = 'white'; /** Whether the content section is always visible. */ - @property({ attribute: 'force-open', reflect: true, type: Boolean }) public forceOpen = false; + @property({ attribute: 'force-open', type: Boolean }) public forceOpen = false; /** Whether the unselected panel has a border. */ @property({ reflect: true, type: Boolean }) public borderless = false; @@ -48,7 +48,7 @@ export class SbbSelectionPanelElement extends LitElement { public disableAnimation = false; /** The state of the selection panel. */ - @state() private _state?: 'closed' | 'opening' | 'opened' | 'closing'; + @state() private _state: 'closed' | 'opening' | 'opened' | 'closing' = 'closed'; /** Whether the selection panel is checked. */ @state() private _checked = false; @@ -91,99 +91,91 @@ export class SbbSelectionPanelElement extends LitElement { ); private _contentElement?: HTMLElement; - private _didLoad = false; private _abort = new ConnectedAbortController(this); - private _namedSlots = new NamedSlotStateController(this); /** * Whether it has an expandable content * @internal */ public get hasContent(): boolean { - return this._namedSlots.slots.has('content'); + // We cannot use the NamedSlots because it's too slow to initialize + return this.querySelectorAll?.('[slot="content"').length > 0; } - private get _input(): SbbCheckboxElement | SbbRadioButtonElement { - return this.querySelector('sbb-checkbox, sbb-radio-button') as - | SbbCheckboxElement - | SbbRadioButtonElement; + public constructor() { + super(); + new NamedSlotStateController(this); } - private _onInputChange( - event: CustomEvent, - ): void { - if (!this._state || !this._didLoad) { - return; + public override connectedCallback(): void { + super.connectedCallback(); + const signal = this._abort.signal; + this.addEventListener('stateChange', this._onInputStateChange.bind(this), { signal }); + this.addEventListener('checkboxLoaded', this._initFromInput.bind(this), { signal }); + this.addEventListener('radioButtonLoaded', this._initFromInput.bind(this), { signal }); + } + + protected override willUpdate(changedProperties: PropertyValues): void { + if (changedProperties.has('forceOpen')) { + this._updateState(); } + } - if (event.detail.type === 'disabled') { - this._disabled = event.detail.disabled; - return; - } else if (event.detail.type !== 'checked') { + private _updateState(): void { + if (!this.hasContent) { return; } - this._checked = event.detail.checked; + this.forceOpen || this._checked ? this._open() : this._close(); + } - if (!this._namedSlots.slots.has('content') || this.forceOpen) { + private _open(): void { + if (this._state !== 'closed' && this._state !== 'closing') { return; } - if (this._checked) { - this._state = 'opening'; - this._willOpen.emit(); - } else { - this._state = 'closing'; - this._willClose.emit(); - } + this._state = 'opening'; + this._willOpen.emit(); } - public override connectedCallback(): void { - super.connectedCallback(); - const signal = this._abort.signal; - this.addEventListener( - 'stateChange', - (e: CustomEvent) => - this._onInputChange(e as CustomEvent), - { signal, passive: true }, - ); - this.addEventListener('checkboxLoaded', () => this._updateSelectionPanel(), { signal }); - this.addEventListener('radioButtonLoaded', () => this._updateSelectionPanel(), { signal }); - } + private _close(): void { + if (this._state !== 'opened' && this._state !== 'opening') { + return; + } - protected override firstUpdated(): void { - this._didLoad = true; + this._state = 'closing'; + this._willClose.emit(); } - private _updateSelectionPanel(): void { - this._checked = this._input?.checked; - this._state = - this.forceOpen || (this._namedSlots.slots.has('content') && this._checked) - ? 'opened' - : 'closed'; - this._disabled = this._input?.disabled; + private _initFromInput(event: Event): void { + const input = event.target as SbbCheckboxElement | SbbRadioButtonElement; + this._checked = input.checked; + this._disabled = input.disabled; + this._updateState(); } - private _onTransitionEnd(event: TransitionEvent): void { - if (event.target !== this._contentElement || event.propertyName !== 'opacity') { + private _onInputStateChange( + event: CustomEvent, + ): void { + if (event.detail.type === 'disabled') { + this._disabled = event.detail.disabled; + return; + } else if (event.detail.type !== 'checked') { return; } - if (this._checked) { - this._handleOpening(); - } else { - this._handleClosing(); - } - } - - private _handleOpening(): void { - this._state = 'opened'; - this._didOpen.emit(); + this._checked = event.detail.checked; + this._updateState(); } - private _handleClosing(): void { - this._state = 'closed'; - this._didClose.emit(); + private _onAnimationEnd(event: AnimationEvent): void { + if (event.animationName === 'open-opacity' && this._state === 'opening') { + this._state = 'opened'; + this._didOpen.emit(); + } else if (event.animationName === 'close' && this._state === 'closing') { + this._state = 'closed'; + this._didClose.emit(); + } } protected override render(): TemplateResult { @@ -202,10 +194,9 @@ export class SbbSelectionPanelElement extends LitElement {
this._onTransitionEnd(event)} - ${ref((el?: Element) => { - this._contentElement = el as HTMLElement; + @animationend=${(event: AnimationEvent) => this._onAnimationEnd(event)} + ${ref((el: HTMLElement) => { + this._contentElement = el; if (this._contentElement) { this._contentElement.inert = !this._checked && !this.forceOpen; } From 218b13a500221f9bb91b654683cafb6a0ba9df19 Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Mon, 22 Jan 2024 12:03:35 +0100 Subject: [PATCH 04/20] refactor(selection-panel): removed ref usage --- src/components/selection-panel/selection-panel.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/selection-panel/selection-panel.ts b/src/components/selection-panel/selection-panel.ts index 9b96b66f65..bba57f5c8d 100644 --- a/src/components/selection-panel/selection-panel.ts +++ b/src/components/selection-panel/selection-panel.ts @@ -1,7 +1,6 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; -import { ref } from 'lit/directives/ref.js'; import type { SbbCheckboxElement, SbbCheckboxStateChange } from '../checkbox'; import { NamedSlotStateController } from '../core/common-behaviors'; @@ -90,7 +89,6 @@ export class SbbSelectionPanelElement extends LitElement { { bubbles: true, composed: true }, ); - private _contentElement?: HTMLElement; private _abort = new ConnectedAbortController(this); /** @@ -194,13 +192,8 @@ export class SbbSelectionPanelElement extends LitElement {
this._onAnimationEnd(event)} - ${ref((el: HTMLElement) => { - this._contentElement = el; - if (this._contentElement) { - this._contentElement.inert = !this._checked && !this.forceOpen; - } - })} >
From 656a783596d87a13e41008290bc38be894f04677 Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Mon, 22 Jan 2024 14:03:44 +0100 Subject: [PATCH 05/20] refactor(selection-panel): now only reacts to main input changes --- src/components/checkbox/checkbox/checkbox.ts | 8 +++++++- .../radio-button/radio-button/radio-button.ts | 5 +++++ src/components/selection-panel/selection-panel.ts | 13 ++++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/components/checkbox/checkbox/checkbox.ts b/src/components/checkbox/checkbox/checkbox.ts index 77d1623f2b..c55ad8804d 100644 --- a/src/components/checkbox/checkbox/checkbox.ts +++ b/src/components/checkbox/checkbox/checkbox.ts @@ -111,7 +111,13 @@ export class SbbCheckboxElement extends UpdateScheduler(LitElement) { } private _size: SbbCheckboxSize = 'm'; - /** Whether the input is the main input of a selection panel. */ + /** + * Whether the input is the main input of a selection panel. + * @internal + */ + public get isSelectionPanelInput(): boolean { + return this._isSelectionPanelInput; + } @state() private _isSelectionPanelInput = false; /** The label describing whether the selection panel is expanded (for screen readers only). */ diff --git a/src/components/radio-button/radio-button/radio-button.ts b/src/components/radio-button/radio-button/radio-button.ts index d00225ebd1..42c77dda9f 100644 --- a/src/components/radio-button/radio-button/radio-button.ts +++ b/src/components/radio-button/radio-button/radio-button.ts @@ -115,7 +115,11 @@ export class SbbRadioButtonElement extends UpdateScheduler(LitElement) { /** * Whether the input is the main input of a selection panel. + * @internal */ + public get isSelectionPanelInput(): boolean { + return this._isSelectionPanelInput; + } @state() private _isSelectionPanelInput = false; /** @@ -150,6 +154,7 @@ export class SbbRadioButtonElement extends UpdateScheduler(LitElement) { private _handleCheckedChange(currentValue: boolean, previousValue: boolean): void { if (currentValue !== previousValue) { + console.log('checked change'); this._stateChange.emit({ type: 'checked', checked: currentValue }); this._isSelectionPanelInput && this._updateExpandedLabel(); } diff --git a/src/components/selection-panel/selection-panel.ts b/src/components/selection-panel/selection-panel.ts index bba57f5c8d..389505baba 100644 --- a/src/components/selection-panel/selection-panel.ts +++ b/src/components/selection-panel/selection-panel.ts @@ -147,6 +147,11 @@ export class SbbSelectionPanelElement extends LitElement { private _initFromInput(event: Event): void { const input = event.target as SbbCheckboxElement | SbbRadioButtonElement; + + if (!input.isSelectionPanelInput) { + return; + } + this._checked = input.checked; this._disabled = input.disabled; this._updateState(); @@ -155,6 +160,12 @@ export class SbbSelectionPanelElement extends LitElement { private _onInputStateChange( event: CustomEvent, ): void { + const input = event.target as SbbCheckboxElement | SbbRadioButtonElement; + + if (!input.isSelectionPanelInput) { + return; + } + if (event.detail.type === 'disabled') { this._disabled = event.detail.disabled; return; @@ -192,7 +203,7 @@ export class SbbSelectionPanelElement extends LitElement {
this._onAnimationEnd(event)} >
From 94d5d4b75a8b20d863c87432d53b0ae73d674bba Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Mon, 22 Jan 2024 16:15:41 +0100 Subject: [PATCH 06/20] chore(selection-panel): chromatic fix --- src/components/selection-panel/selection-panel.stories.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/selection-panel/selection-panel.stories.ts b/src/components/selection-panel/selection-panel.stories.ts index 0540241621..47ebeefcd4 100644 --- a/src/components/selection-panel/selection-panel.stories.ts +++ b/src/components/selection-panel/selection-panel.stories.ts @@ -733,7 +733,9 @@ export const NestedCheckboxes: StoryObj = { const meta: Meta = { decorators: [ - (story) => html`
${story()}
`, + (story) => html` +
${story()}
+ `, withActions as Decorator, ], parameters: { From dbe7fdb7e9381ecb2616a3c518da3d967d6c52a7 Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Mon, 22 Jan 2024 17:13:25 +0100 Subject: [PATCH 07/20] test(selection-panel): enhance robusteness --- .../radio-button/radio-button/radio-button.ts | 1 - .../selection-panel/selection-panel.e2e.ts | 49 +++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/components/radio-button/radio-button/radio-button.ts b/src/components/radio-button/radio-button/radio-button.ts index 42c77dda9f..8c14be8b3f 100644 --- a/src/components/radio-button/radio-button/radio-button.ts +++ b/src/components/radio-button/radio-button/radio-button.ts @@ -154,7 +154,6 @@ export class SbbRadioButtonElement extends UpdateScheduler(LitElement) { private _handleCheckedChange(currentValue: boolean, previousValue: boolean): void { if (currentValue !== previousValue) { - console.log('checked change'); this._stateChange.emit({ type: 'checked', checked: currentValue }); this._isSelectionPanelInput && this._updateExpandedLabel(); } diff --git a/src/components/selection-panel/selection-panel.e2e.ts b/src/components/selection-panel/selection-panel.e2e.ts index aef720dc42..01f44932b4 100644 --- a/src/components/selection-panel/selection-panel.e2e.ts +++ b/src/components/selection-panel/selection-panel.e2e.ts @@ -277,6 +277,11 @@ describe('sbb-selection-panel', () => { document.querySelector('#input-no-content-2')!; const fourthInputNoContent: SbbRadioButtonElement = document.querySelector('#input-no-content-4')!; + const firstPanel = document.querySelector('#no-content-1')!; + const secondPanel = document.querySelector('#no-content-2')!; + + expect(firstPanel).to.have.attribute('data-state', 'closed'); + expect(secondPanel).to.have.attribute('data-state', 'closed'); await sendKeys({ down: 'Tab' }); await waitForLitRender(wrapperNoContent); @@ -310,11 +315,19 @@ describe('sbb-selection-panel', () => { describe('with nested radio buttons', () => { let nestedElement: SbbRadioButtonGroupElement; + let panel1: SbbSelectionPanelElement; + let panel2: SbbSelectionPanelElement; + let willOpenEventSpy, didOpenEventSpy, willCloseEventSpy, didCloseEventSpy: EventSpy; beforeEach(async () => { + willOpenEventSpy = new EventSpy(SbbSelectionPanelElement.events.willOpen); + didOpenEventSpy = new EventSpy(SbbSelectionPanelElement.events.didOpen); + willCloseEventSpy = new EventSpy(SbbSelectionPanelElement.events.willClose); + didCloseEventSpy = new EventSpy(SbbSelectionPanelElement.events.didClose); + nestedElement = await fixture(html` - + Main Option 1 Suboption 1 @@ -322,7 +335,7 @@ describe('sbb-selection-panel', () => { - + Main Option 2 Suboption 3 @@ -331,6 +344,8 @@ describe('sbb-selection-panel', () => { `); + panel1 = nestedElement.querySelector('#panel1')!; + panel2 = nestedElement.querySelector('#panel2')!; await waitForLitRender(nestedElement); }); @@ -351,18 +366,30 @@ describe('sbb-selection-panel', () => { .querySelector("sbb-radio-button[value='sub1']")! .shadowRoot!.querySelector('.sbb-radio-button__expanded-label'); + await waitForCondition(() => didOpenEventSpy.count === 1); + expect(willOpenEventSpy.count).to.be.equal(1); + expect(didOpenEventSpy.count).to.be.equal(1); expect(mainRadioButton1Label.textContent!.trim()).to.be.equal(', expanded'); expect(mainRadioButton2Label.textContent!.trim()).to.be.equal(', collapsed'); expect(subRadioButton1).to.be.null; + expect(panel1).to.have.attribute('data-state', 'opened'); + expect(panel2).to.have.attribute('data-state', 'closed'); // Activate main option 2 mainRadioButton2.click(); - await waitForLitRender(nestedElement); + await waitForCondition(() => didOpenEventSpy.count === 2); + await waitForCondition(() => didCloseEventSpy.count === 1); + expect(willOpenEventSpy.count).to.be.equal(2); + expect(didOpenEventSpy.count).to.be.equal(2); + expect(willCloseEventSpy.count).to.be.equal(1); + expect(didCloseEventSpy.count).to.be.equal(1); expect(mainRadioButton1Label.textContent!.trim()).to.be.equal(', collapsed'); expect(mainRadioButton2Label.textContent!.trim()).to.be.equal(', expanded'); expect(subRadioButton1).to.be.null; + expect(panel1).to.have.attribute('data-state', 'closed'); + expect(panel2).to.have.attribute('data-state', 'opened'); }); it('should mark only outer group children as disabled', async () => { @@ -393,13 +420,27 @@ describe('sbb-selection-panel', () => { 'sbb-radio-button[value="sub1"]', )!; + await waitForCondition(() => didOpenEventSpy.count === 1); + expect(willOpenEventSpy.count).to.be.equal(1); + expect(didOpenEventSpy.count).to.be.equal(1); + expect(panel1).to.have.attribute('data-state', 'opened'); + expect(panel2).to.have.attribute('data-state', 'closed'); expect(main1).to.have.attribute('checked'); expect(main2).not.to.have.attribute('checked'); expect(sub1).to.have.attribute('checked'); main2.checked = true; - await waitForLitRender(nestedElement); + await waitForCondition(() => didOpenEventSpy.count === 2); + await waitForCondition(() => didCloseEventSpy.count === 1); + + expect(willOpenEventSpy.count).to.be.equal(2); + expect(didOpenEventSpy.count).to.be.equal(2); + expect(willCloseEventSpy.count).to.be.equal(1); + expect(didCloseEventSpy.count).to.be.equal(1); + + expect(panel1).to.have.attribute('data-state', 'closed'); + expect(panel2).to.have.attribute('data-state', 'opened'); expect(main1).not.to.have.attribute('checked'); expect(main2).to.have.attribute('checked'); expect(sub1).to.have.attribute('checked'); From bed60e350eec87495d3f4fd62c7aeb3bcc994285 Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Mon, 22 Jan 2024 17:40:37 +0100 Subject: [PATCH 08/20] refactor(selection-panel): fix ts --- .../selection-panel/selection-panel.e2e.ts | 18 ++++++++++++++---- .../selection-panel/selection-panel.ts | 8 +++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/components/selection-panel/selection-panel.e2e.ts b/src/components/selection-panel/selection-panel.e2e.ts index 01f44932b4..c30b5bd6c6 100644 --- a/src/components/selection-panel/selection-panel.e2e.ts +++ b/src/components/selection-panel/selection-panel.e2e.ts @@ -57,7 +57,10 @@ describe('sbb-selection-panel', () => { /* eslint-enable lit/binding-positions */ }; - const forceOpenTest = async (wrapper: SbbRadioButtonGroupElement | SbbCheckboxGroupElement, secondInput: SbbRadioButtonElement | SbbCheckboxElement): Promise => { + const forceOpenTest = async ( + wrapper: SbbRadioButtonGroupElement | SbbCheckboxGroupElement, + secondInput: SbbRadioButtonElement | SbbCheckboxElement, + ): Promise => { elements.forEach((e) => (e.forceOpen = true)); await waitForLitRender(wrapper); @@ -124,7 +127,8 @@ describe('sbb-selection-panel', () => { let secondPanel: SbbSelectionPanelElement; let secondInput: SbbRadioButtonElement; let disabledInput: SbbRadioButtonElement; - let willOpenEventSpy, didOpenEventSpy: EventSpy; + let willOpenEventSpy: EventSpy; + let didOpenEventSpy: EventSpy; beforeEach(async () => { willOpenEventSpy = new EventSpy(SbbSelectionPanelElement.events.willOpen); @@ -317,7 +321,10 @@ describe('sbb-selection-panel', () => { let nestedElement: SbbRadioButtonGroupElement; let panel1: SbbSelectionPanelElement; let panel2: SbbSelectionPanelElement; - let willOpenEventSpy, didOpenEventSpy, willCloseEventSpy, didCloseEventSpy: EventSpy; + let willOpenEventSpy: EventSpy; + let didOpenEventSpy: EventSpy; + let willCloseEventSpy: EventSpy; + let didCloseEventSpy: EventSpy; beforeEach(async () => { willOpenEventSpy = new EventSpy(SbbSelectionPanelElement.events.willOpen); @@ -501,7 +508,10 @@ describe('sbb-selection-panel', () => { let secondPanel: SbbSelectionPanelElement; let secondInput: SbbCheckboxElement; let disabledInput: SbbCheckboxElement; - let willOpenEventSpy, didOpenEventSpy, willCloseEventSpy, didCloseEventSpy: EventSpy; + let willOpenEventSpy: EventSpy; + let didOpenEventSpy: EventSpy; + let willCloseEventSpy: EventSpy; + let didCloseEventSpy: EventSpy; beforeEach(async () => { willOpenEventSpy = new EventSpy(SbbSelectionPanelElement.events.willOpen); diff --git a/src/components/selection-panel/selection-panel.ts b/src/components/selection-panel/selection-panel.ts index 389505baba..1c646ebb82 100644 --- a/src/components/selection-panel/selection-panel.ts +++ b/src/components/selection-panel/selection-panel.ts @@ -2,12 +2,12 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; -import type { SbbCheckboxElement, SbbCheckboxStateChange } from '../checkbox'; +import type { SbbCheckboxElement } from '../checkbox'; import { NamedSlotStateController } from '../core/common-behaviors'; import { setAttribute } from '../core/dom'; import { EventEmitter, ConnectedAbortController } from '../core/eventing'; import type { SbbStateChange } from '../core/interfaces'; -import type { SbbRadioButtonElement, SbbRadioButtonStateChange } from '../radio-button'; +import type { SbbRadioButtonElement } from '../radio-button'; import style from './selection-panel.scss?lit&inline'; import '../divider'; @@ -157,9 +157,7 @@ export class SbbSelectionPanelElement extends LitElement { this._updateState(); } - private _onInputStateChange( - event: CustomEvent, - ): void { + private _onInputStateChange(event: CustomEvent): void { const input = event.target as SbbCheckboxElement | SbbRadioButtonElement; if (!input.isSelectionPanelInput) { From 0bbe0aa0ad337d8122ebcb1d1694b15d18fcf5b6 Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Mon, 22 Jan 2024 17:41:37 +0100 Subject: [PATCH 09/20] chore(selection-panel): set chromatic min-height --- src/components/selection-panel/selection-panel.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/selection-panel/selection-panel.stories.ts b/src/components/selection-panel/selection-panel.stories.ts index 47ebeefcd4..cd8b02fcbd 100644 --- a/src/components/selection-panel/selection-panel.stories.ts +++ b/src/components/selection-panel/selection-panel.stories.ts @@ -734,7 +734,7 @@ export const NestedCheckboxes: StoryObj = { const meta: Meta = { decorators: [ (story) => html` -
${story()}
+
${story()}
`, withActions as Decorator, ], From 8b259cadcbeb24b1ce8236453de9276dacdb7dbe Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Tue, 23 Jan 2024 09:34:09 +0100 Subject: [PATCH 10/20] chore(selection-panel): set chromatic min-height --- src/components/selection-panel/selection-panel.stories.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/selection-panel/selection-panel.stories.ts b/src/components/selection-panel/selection-panel.stories.ts index cd8b02fcbd..0540241621 100644 --- a/src/components/selection-panel/selection-panel.stories.ts +++ b/src/components/selection-panel/selection-panel.stories.ts @@ -733,9 +733,7 @@ export const NestedCheckboxes: StoryObj = { const meta: Meta = { decorators: [ - (story) => html` -
${story()}
- `, + (story) => html`
${story()}
`, withActions as Decorator, ], parameters: { From 626d133ea3ba182a526a24c1389c9dae73b94a08 Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Tue, 23 Jan 2024 11:00:35 +0100 Subject: [PATCH 11/20] chore(selection-panel): set chromatic min-height --- .../selection-panel/selection-panel.stories.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/selection-panel/selection-panel.stories.ts b/src/components/selection-panel/selection-panel.stories.ts index 0540241621..8104bd52f0 100644 --- a/src/components/selection-panel/selection-panel.stories.ts +++ b/src/components/selection-panel/selection-panel.stories.ts @@ -3,7 +3,7 @@ import type { InputType } from '@storybook/types'; import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/web-components'; import isChromatic from 'chromatic'; import type { TemplateResult } from 'lit'; -import { html } from 'lit'; +import { html, nothing } from 'lit'; import type { StyleInfo } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js'; @@ -152,7 +152,7 @@ const WithCheckboxGroupTemplate = ({ ${cardBadge()} @@ -184,7 +184,7 @@ const WithRadioButtonGroupTemplate = ({ orientation="vertical" horizontal-from="large" ?allow-empty-selection=${allowEmptySelection} - style="${isChromatic() ? 'min-height: 750px;' : ''}" + style="${isChromatic() ? 'min-height: 750px;' : nothing}" > ${cardBadge()} @@ -218,7 +218,7 @@ const TicketsOptionsExampleTemplate = ({ ${cardBadge()} @@ -350,7 +350,7 @@ const NestedCheckboxTemplate = ({ Main Option 1 @@ -383,7 +383,7 @@ const WithCheckboxesErrorMessageTemplate = ({ { const checkboxGroup = event.currentTarget as HTMLElement; const hasChecked = Array.from(checkboxGroup.querySelectorAll('sbb-checkbox')).some( From 9c2f17b8b4b556db508478cced84255b543eb943 Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Tue, 23 Jan 2024 12:05:18 +0100 Subject: [PATCH 12/20] chore(selection-panel): set chromatic min-height pt.2 --- src/components/selection-panel/selection-panel.stories.ts | 7 ++++++- src/components/selection-panel/selection-panel.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/selection-panel/selection-panel.stories.ts b/src/components/selection-panel/selection-panel.stories.ts index 8104bd52f0..31292f1db8 100644 --- a/src/components/selection-panel/selection-panel.stories.ts +++ b/src/components/selection-panel/selection-panel.stories.ts @@ -433,6 +433,7 @@ const WithRadiosErrorMessageTemplate = ({ horizontal-from="large" allow-empty-selection id="sbb-radio-group" + style="${isChromatic() ? 'min-height: 800px;' : nothing}" @change=${(event: CustomEvent) => { if (event.detail.value) { sbbFormError.remove(); @@ -494,7 +495,11 @@ const WithNoContentGroupTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + ${cardBadge()} diff --git a/src/components/selection-panel/selection-panel.ts b/src/components/selection-panel/selection-panel.ts index 1c646ebb82..e228d96330 100644 --- a/src/components/selection-panel/selection-panel.ts +++ b/src/components/selection-panel/selection-panel.ts @@ -97,7 +97,7 @@ export class SbbSelectionPanelElement extends LitElement { */ public get hasContent(): boolean { // We cannot use the NamedSlots because it's too slow to initialize - return this.querySelectorAll?.('[slot="content"').length > 0; + return this.querySelectorAll?.('[slot="content"]').length > 0; } public constructor() { From f40ab0957ee9405f8cbc0031937a84f9901cf0be Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Tue, 23 Jan 2024 13:54:26 +0100 Subject: [PATCH 13/20] chore(selection-panel): set chromatic min-height pt.3 --- .../selection-panel/selection-panel.stories.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/selection-panel/selection-panel.stories.ts b/src/components/selection-panel/selection-panel.stories.ts index 31292f1db8..98966a92b4 100644 --- a/src/components/selection-panel/selection-panel.stories.ts +++ b/src/components/selection-panel/selection-panel.stories.ts @@ -121,7 +121,7 @@ const WithCheckboxTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + ${cardBadge()} Value one ${suffixAndSubtext()} @@ -135,7 +135,7 @@ const WithRadioButtonTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + ${cardBadge()} Value one ${suffixAndSubtext()} @@ -319,7 +319,11 @@ const NestedRadioTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + Main Option 1 From 8f6f54620557d2c20351cef2eea484b239f3f888 Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Tue, 23 Jan 2024 14:26:14 +0100 Subject: [PATCH 14/20] chore(selection-panel): set chromatic min-height pt.4 --- .../selection-panel/selection-panel.stories.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/selection-panel/selection-panel.stories.ts b/src/components/selection-panel/selection-panel.stories.ts index 98966a92b4..692053f23c 100644 --- a/src/components/selection-panel/selection-panel.stories.ts +++ b/src/components/selection-panel/selection-panel.stories.ts @@ -121,7 +121,10 @@ const WithCheckboxTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + ${cardBadge()} Value one ${suffixAndSubtext()} @@ -135,7 +138,10 @@ const WithRadioButtonTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + ${cardBadge()} Value one ${suffixAndSubtext()} From c95ce9ce1e131ce610d99e3647fe2dad1aaf7fee Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Tue, 23 Jan 2024 16:30:54 +0100 Subject: [PATCH 15/20] feat(chromatic-stories): implemented 'fixedHeight' chromatic param --- scripts/chromatic-stories-generator.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/chromatic-stories-generator.ts b/scripts/chromatic-stories-generator.ts index bb7704c3db..b35f9aaa35 100644 --- a/scripts/chromatic-stories-generator.ts +++ b/scripts/chromatic-stories-generator.ts @@ -63,7 +63,7 @@ async function generateChromaticStory( return 'no params found'; } - const { disableSnapshot, ...chromaticParameters } = parameters?.chromatic ?? {}; + const { disableSnapshot, fixedHeight, ...chromaticParameters } = parameters?.chromatic ?? {}; if (!parameters) { return 'no params found'; } else if (disableSnapshot !== undefined) { @@ -74,14 +74,23 @@ async function generateChromaticStory( const relativeImport = basename(storyFile).replace(/\.ts$/, ''); const chromaticImport = relative(dirname(targetStoryFile), chromaticFile).replace(/\.ts$/, ''); + /** + * TODO Document fixedHeight + */ + const fixedHeightStyle = fixedHeight ? `style="min-height: ${fixedHeight}"` : ''; + const chromaticConfig = Object.entries(chromaticParameters) .map(([key, value]) => `${key}: ${JSON.stringify(value)}, `) .join(''); const storyFileContent = `import type { Meta, StoryObj } from '@storybook/web-components'; import config, * as stories from './${relativeImport}'; import { combineStories } from '${chromaticImport}'; +import { html } from 'lit'; const meta: Meta = { + decorators: [ + (story) => html\`
\${story()}
\`, + ], parameters: { backgrounds: { disable: true, From b1b05cda735bcfc1239bb71f75224e14cbf06d73 Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Tue, 23 Jan 2024 16:31:24 +0100 Subject: [PATCH 16/20] chore(selection-panel): set chromatic min-height pt.5 --- .../selection-panel.stories.ts | 45 ++++--------------- 1 file changed, 8 insertions(+), 37 deletions(-) diff --git a/src/components/selection-panel/selection-panel.stories.ts b/src/components/selection-panel/selection-panel.stories.ts index 692053f23c..ed62eaca93 100644 --- a/src/components/selection-panel/selection-panel.stories.ts +++ b/src/components/selection-panel/selection-panel.stories.ts @@ -121,10 +121,7 @@ const WithCheckboxTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + ${cardBadge()} Value one ${suffixAndSubtext()} @@ -138,10 +135,7 @@ const WithRadioButtonTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + ${cardBadge()} Value one ${suffixAndSubtext()} @@ -155,11 +149,7 @@ const WithCheckboxGroupTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + ${cardBadge()} Value one ${suffixAndSubtext()} @@ -190,7 +180,6 @@ const WithRadioButtonGroupTemplate = ({ orientation="vertical" horizontal-from="large" ?allow-empty-selection=${allowEmptySelection} - style="${isChromatic() ? 'min-height: 750px;' : nothing}" > ${cardBadge()} @@ -221,11 +210,7 @@ const TicketsOptionsExampleTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + ${cardBadge()} Saving ${suffixAndSubtext()} @@ -325,11 +310,7 @@ const NestedRadioTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + Main Option 1 @@ -357,11 +338,7 @@ const NestedCheckboxTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + Main Option 1 @@ -393,7 +370,6 @@ const WithCheckboxesErrorMessageTemplate = ({ { const checkboxGroup = event.currentTarget as HTMLElement; const hasChecked = Array.from(checkboxGroup.querySelectorAll('sbb-checkbox')).some( @@ -443,7 +419,6 @@ const WithRadiosErrorMessageTemplate = ({ horizontal-from="large" allow-empty-selection id="sbb-radio-group" - style="${isChromatic() ? 'min-height: 800px;' : nothing}" @change=${(event: CustomEvent) => { if (event.detail.value) { sbbFormError.remove(); @@ -505,11 +480,7 @@ const WithNoContentGroupTemplate = ({ disabledInput, ...args }: Args): TemplateResult => html` - + ${cardBadge()} @@ -752,7 +723,7 @@ const meta: Meta = { withActions as Decorator, ], parameters: { - chromatic: { delay: 9000 }, + chromatic: { delay: 9000, fixedHeight: '17000px' }, actions: { handles: [ SbbSelectionPanelElement.events.didOpen, From 5ceddca97ad412ff156b8472d6e5b341d9b3eb9c Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Tue, 23 Jan 2024 16:48:47 +0100 Subject: [PATCH 17/20] feat(chromatic-stories): implemented 'fixedHeight' chromatic param --- scripts/chromatic-stories-generator.ts | 14 +++++++++++++- .../selection-panel/selection-panel.stories.ts | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/chromatic-stories-generator.ts b/scripts/chromatic-stories-generator.ts index b35f9aaa35..fe57e043e9 100644 --- a/scripts/chromatic-stories-generator.ts +++ b/scripts/chromatic-stories-generator.ts @@ -75,7 +75,19 @@ async function generateChromaticStory( const chromaticImport = relative(dirname(targetStoryFile), chromaticFile).replace(/\.ts$/, ''); /** - * TODO Document fixedHeight + * The `fixedHeight` param forces the height of the snapshot on chromatic. + * It might be useful in cases where some content is cut off at the end of a snapshot + * The max fixedHeight we can use is 17'000 (17k * 1440 =~ 25kk) + * Now, the max snapshot size is 25'000'000px. + * + * Example: + * ``` + * ... + * parameters: { + * chromatic: { fixedHeight: '17000px', ... }, + * ... + * } + * ``` */ const fixedHeightStyle = fixedHeight ? `style="min-height: ${fixedHeight}"` : ''; diff --git a/src/components/selection-panel/selection-panel.stories.ts b/src/components/selection-panel/selection-panel.stories.ts index ed62eaca93..4d13efc67f 100644 --- a/src/components/selection-panel/selection-panel.stories.ts +++ b/src/components/selection-panel/selection-panel.stories.ts @@ -3,7 +3,7 @@ import type { InputType } from '@storybook/types'; import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/web-components'; import isChromatic from 'chromatic'; import type { TemplateResult } from 'lit'; -import { html, nothing } from 'lit'; +import { html } from 'lit'; import type { StyleInfo } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js'; From 59ce0bc64e5e0e69b555b9e86bc76190beea96dd Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Tue, 23 Jan 2024 17:06:47 +0100 Subject: [PATCH 18/20] chore(selection-panel, notification): set chromatic min-height --- src/components/notification/notification.stories.ts | 5 ++--- src/components/selection-panel/selection-panel.stories.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/notification/notification.stories.ts b/src/components/notification/notification.stories.ts index 78e0232bc2..99b950265e 100644 --- a/src/components/notification/notification.stories.ts +++ b/src/components/notification/notification.stories.ts @@ -237,9 +237,7 @@ const meta: Meta = { decorators: [ (story, context) => html`
${trigger(context.args)}
@@ -250,6 +248,7 @@ const meta: Meta = { withActions as Decorator, ], parameters: { + chromatic: { fixedHeight: '7500px' }, actions: { handles: [ SbbNotificationElement.events.didOpen, diff --git a/src/components/selection-panel/selection-panel.stories.ts b/src/components/selection-panel/selection-panel.stories.ts index 4d13efc67f..21adc171a8 100644 --- a/src/components/selection-panel/selection-panel.stories.ts +++ b/src/components/selection-panel/selection-panel.stories.ts @@ -723,7 +723,7 @@ const meta: Meta = { withActions as Decorator, ], parameters: { - chromatic: { delay: 9000, fixedHeight: '17000px' }, + chromatic: { delay: 9000, fixedHeight: '14500px' }, actions: { handles: [ SbbSelectionPanelElement.events.didOpen, From c7141c57887af397fcd65f1af5f57ec72576fcf5 Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Thu, 25 Jan 2024 11:27:38 +0100 Subject: [PATCH 19/20] refactor(selection-panel): disabled open/close animation before initialization --- src/components/checkbox/checkbox/checkbox.ts | 2 +- .../radio-button/radio-button/radio-button.ts | 2 +- .../selection-panel/selection-panel.scss | 38 +++++++++++++------ .../selection-panel/selection-panel.ts | 24 ++++++------ 4 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/components/checkbox/checkbox/checkbox.ts b/src/components/checkbox/checkbox/checkbox.ts index c55ad8804d..24c11e84d5 100644 --- a/src/components/checkbox/checkbox/checkbox.ts +++ b/src/components/checkbox/checkbox/checkbox.ts @@ -189,6 +189,7 @@ export class SbbCheckboxElement extends UpdateScheduler(LitElement) { this.addEventListener('click', (e) => this._handleClick(e), { signal }); this.addEventListener('keyup', (e) => this._handleKeyup(e), { signal }); this._handlerRepository.connect(); + this._checkboxLoaded.emit(); // We need to call requestUpdate to update the reflected attributes ['disabled', 'required', 'size'].forEach((p) => this.requestUpdate(p)); @@ -207,7 +208,6 @@ export class SbbCheckboxElement extends UpdateScheduler(LitElement) { // We need to wait for the selection-panel to be fully initialized this.startUpdate(); setTimeout(() => { - this._checkboxLoaded.emit(); this._isSelectionPanelInput && this._updateExpandedLabel(); this.completeUpdate(); }); diff --git a/src/components/radio-button/radio-button/radio-button.ts b/src/components/radio-button/radio-button/radio-button.ts index 8c14be8b3f..57223f5090 100644 --- a/src/components/radio-button/radio-button/radio-button.ts +++ b/src/components/radio-button/radio-button/radio-button.ts @@ -201,6 +201,7 @@ export class SbbRadioButtonElement extends UpdateScheduler(LitElement) { this.addEventListener('click', (e) => this._handleClick(e), { signal }); this.addEventListener('keydown', (e) => this._handleKeyDown(e), { signal }); this._handlerRepository.connect(); + this._radioButtonLoaded.emit(); // We need to call requestUpdate to update the reflected attributes ['disabled', 'required', 'size'].forEach((p) => this.requestUpdate(p)); @@ -219,7 +220,6 @@ export class SbbRadioButtonElement extends UpdateScheduler(LitElement) { // We need to wait for the selection-panel to be fully initialized this.startUpdate(); setTimeout(() => { - this._radioButtonLoaded.emit(); this._isSelectionPanelInput && this._updateExpandedLabel(); this.completeUpdate(); }); diff --git a/src/components/selection-panel/selection-panel.scss b/src/components/selection-panel/selection-panel.scss index 5bad5a110a..4be10ee24f 100644 --- a/src/components/selection-panel/selection-panel.scss +++ b/src/components/selection-panel/selection-panel.scss @@ -4,6 +4,12 @@ // travel the shadow boundary are defined through this mixin @include sbb.host-component-properties; +// Open/Close animation vars +$open-anim-rows-from: 0fr; +$open-anim-rows-to: 1fr; +$open-anim-opacity-from: 0; +$open-anim-opacity-to: 1; + :host { --sbb-selection-panel-cursor: pointer; --sbb-selection-panel-background: var(--sbb-color-white-default); @@ -15,6 +21,9 @@ var(--sbb-spacing-responsive-xxs); --sbb-selection-panel-content-visibility: hidden; --sbb-selection-panel-content-padding-inline: var(--sbb-spacing-responsive-xxs); + + // As the selection panel has always a white/milk background, we have to fix the focus outline color + // to default color for cases where the selection panel is used in a negative context. --sbb-focus-outline-color: var(--sbb-focus-outline-color-default); display: contents; @@ -85,10 +94,15 @@ .sbb-selection-panel__content--wrapper { display: grid; visibility: var(--sbb-selection-panel-content-visibility); - grid-template-rows: 0fr; - opacity: 0; + grid-template-rows: #{$open-anim-rows-from}; + opacity: #{$open-anim-opacity-from}; + + :host([data-state='opened']) & { + grid-template-rows: #{$open-anim-rows-to}; + opacity: #{$open-anim-opacity-to}; + } - :host(:where([data-state='opening'], [data-state='opened'])) & { + :host([data-state='opening']) & { animation-name: open, open-opacity; animation-fill-mode: forwards; animation-duration: var(--sbb-selection-panel-animation-duration); @@ -96,7 +110,7 @@ animation-delay: 0s, var(--sbb-selection-panel-animation-duration); } - :host(:where([data-state='closing'])) & { + :host([data-state='closing']) & { animation-name: close; animation-duration: var(--sbb-selection-panel-animation-duration); animation-timing-function: var(--sbb-animation-easing); @@ -133,32 +147,32 @@ sbb-divider { @keyframes open { from { - grid-template-rows: 0fr; + grid-template-rows: #{$open-anim-rows-from}; } to { - grid-template-rows: 1fr; + grid-template-rows: #{$open-anim-rows-to}; } } @keyframes open-opacity { from { - opacity: 0; + opacity: #{$open-anim-opacity-from}; } to { - opacity: 1; + opacity: #{$open-anim-opacity-to}; } } @keyframes close { from { - grid-template-rows: 1fr; - opacity: 1; + grid-template-rows: #{$open-anim-rows-to}; + opacity: #{$open-anim-opacity-to}; } to { - grid-template-rows: 0fr; - opacity: 0; + grid-template-rows: #{$open-anim-rows-from}; + opacity: #{$open-anim-opacity-from}; } } diff --git a/src/components/selection-panel/selection-panel.ts b/src/components/selection-panel/selection-panel.ts index e228d96330..5a02313294 100644 --- a/src/components/selection-panel/selection-panel.ts +++ b/src/components/selection-panel/selection-panel.ts @@ -59,37 +59,28 @@ export class SbbSelectionPanelElement extends LitElement { private _willOpen: EventEmitter = new EventEmitter( this, SbbSelectionPanelElement.events.willOpen, - { - bubbles: true, - composed: true, - }, ); /** Emits whenever the content section is opened. */ private _didOpen: EventEmitter = new EventEmitter( this, SbbSelectionPanelElement.events.didOpen, - { - bubbles: true, - composed: true, - }, ); /** Emits whenever the content section begins the closing transition. */ private _willClose: EventEmitter<{ closeTarget: HTMLElement }> = new EventEmitter( this, SbbSelectionPanelElement.events.willClose, - { bubbles: true, composed: true }, ); /** Emits whenever the content section is closed. */ private _didClose: EventEmitter<{ closeTarget: HTMLElement }> = new EventEmitter( this, SbbSelectionPanelElement.events.didClose, - { bubbles: true, composed: true }, ); private _abort = new ConnectedAbortController(this); + private _initialized: boolean = false; /** * Whether it has an expandable content @@ -119,21 +110,30 @@ export class SbbSelectionPanelElement extends LitElement { } } + protected override firstUpdated(): void { + this._initialized = true; + } + private _updateState(): void { if (!this.hasContent) { return; } - this.forceOpen || this._checked ? this._open() : this._close(); + this.forceOpen || this._checked ? this._open(!this._initialized) : this._close(); } - private _open(): void { + private _open(skipAnimation = false): void { if (this._state !== 'closed' && this._state !== 'closing') { return; } this._state = 'opening'; this._willOpen.emit(); + + if (skipAnimation) { + this._state = 'opened'; + this._didOpen.emit(); + } } private _close(): void { From a0be039f8f40f9f7008ff70d457b7d2a67b1f329 Mon Sep 17 00:00:00 2001 From: Tommmaso Menga Date: Thu, 25 Jan 2024 12:04:04 +0100 Subject: [PATCH 20/20] docs(selection-panel): update docs --- src/components/selection-panel/readme.md | 12 ++++++------ src/components/selection-panel/selection-panel.ts | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/selection-panel/readme.md b/src/components/selection-panel/readme.md index 22be1f6340..384e926ad2 100644 --- a/src/components/selection-panel/readme.md +++ b/src/components/selection-panel/readme.md @@ -86,12 +86,12 @@ It's also possible to display the `sbb-selection-panel` without border by settin ## Events -| Name | Type | Description | Inherited From | -| ----------- | ------------------------------------------- | ----------------------------------------------------------------- | -------------- | -| `willOpen` | `CustomEvent` | Emits whenever the content section starts the opening transition. | | -| `didOpen` | `CustomEvent` | Emits whenever the content section is opened. | | -| `willClose` | `CustomEvent<{ closeTarget: HTMLElement }>` | Emits whenever the content section begins the closing transition. | | -| `didClose` | `CustomEvent<{ closeTarget: HTMLElement }>` | Emits whenever the content section is closed. | | +| Name | Type | Description | Inherited From | +| ----------- | ------------------- | ----------------------------------------------------------------- | -------------- | +| `willOpen` | `CustomEvent` | Emits whenever the content section starts the opening transition. | | +| `didOpen` | `CustomEvent` | Emits whenever the content section is opened. | | +| `willClose` | `CustomEvent` | Emits whenever the content section begins the closing transition. | | +| `didClose` | `CustomEvent` | Emits whenever the content section is closed. | | ## Slots diff --git a/src/components/selection-panel/selection-panel.ts b/src/components/selection-panel/selection-panel.ts index 5a02313294..6404985648 100644 --- a/src/components/selection-panel/selection-panel.ts +++ b/src/components/selection-panel/selection-panel.ts @@ -20,8 +20,8 @@ import '../divider'; * @slot content - Use this slot to provide custom content for the panel (optional). * @event {CustomEvent} willOpen - Emits whenever the content section starts the opening transition. * @event {CustomEvent} didOpen - Emits whenever the content section is opened. - * @event {CustomEvent<{ closeTarget: HTMLElement }>} willClose - Emits whenever the content section begins the closing transition. - * @event {CustomEvent<{ closeTarget: HTMLElement }>} didClose - Emits whenever the content section is closed. + * @event {CustomEvent} willClose - Emits whenever the content section begins the closing transition. + * @event {CustomEvent} didClose - Emits whenever the content section is closed. */ @customElement('sbb-selection-panel') export class SbbSelectionPanelElement extends LitElement { @@ -68,13 +68,13 @@ export class SbbSelectionPanelElement extends LitElement { ); /** Emits whenever the content section begins the closing transition. */ - private _willClose: EventEmitter<{ closeTarget: HTMLElement }> = new EventEmitter( + private _willClose: EventEmitter = new EventEmitter( this, SbbSelectionPanelElement.events.willClose, ); /** Emits whenever the content section is closed. */ - private _didClose: EventEmitter<{ closeTarget: HTMLElement }> = new EventEmitter( + private _didClose: EventEmitter = new EventEmitter( this, SbbSelectionPanelElement.events.didClose, );