From a04b5b498e6479992d791cc5292bede7098d1738 Mon Sep 17 00:00:00 2001 From: Davide Mininni <101575400+DavideMininni-Fincons@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:38:35 +0200 Subject: [PATCH] feat(sbb-selection-expansion-panel): introduce size s (#3030) --- src/elements/core/mixins/panel-common.scss | 5 + ...tion-expansion-panel.snapshot.spec.snap.js | 1 + .../selection-expansion-panel/readme.md | 20 +++ .../selection-expansion-panel.scss | 4 + .../selection-expansion-panel.spec.ts | 92 +++++++++++++ .../selection-expansion-panel.stories.ts | 121 ++++++++++++++---- .../selection-expansion-panel.ts | 45 ++++++- .../selection-expansion-panel.visual.spec.ts | 99 ++++++++++---- 8 files changed, 328 insertions(+), 59 deletions(-) diff --git a/src/elements/core/mixins/panel-common.scss b/src/elements/core/mixins/panel-common.scss index c66ec18b9d..720ecb275d 100644 --- a/src/elements/core/mixins/panel-common.scss +++ b/src/elements/core/mixins/panel-common.scss @@ -34,6 +34,11 @@ outline: none !important; } +:host([size='s']) { + --sbb-selection-panel-input-padding: var(--sbb-spacing-responsive-xxs) + var(--sbb-spacing-responsive-xxxs); +} + :host([color='milk']) { --sbb-selection-panel-background: var( --sbb-selection-expansion-panel-inner-background, diff --git a/src/elements/selection-expansion-panel/__snapshots__/selection-expansion-panel.snapshot.spec.snap.js b/src/elements/selection-expansion-panel/__snapshots__/selection-expansion-panel.snapshot.spec.snap.js index dedcbd0ac0..dbb162d77d 100644 --- a/src/elements/selection-expansion-panel/__snapshots__/selection-expansion-panel.snapshot.spec.snap.js +++ b/src/elements/selection-expansion-panel/__snapshots__/selection-expansion-panel.snapshot.spec.snap.js @@ -3,6 +3,7 @@ export const snapshots = {}; snapshots["sbb-selection-expansion-panel renders DOM"] = ` diff --git a/src/elements/selection-expansion-panel/readme.md b/src/elements/selection-expansion-panel/readme.md index 0480d6c071..864d328872 100644 --- a/src/elements/selection-expansion-panel/readme.md +++ b/src/elements/selection-expansion-panel/readme.md @@ -62,6 +62,26 @@ It's also possible to display the `sbb-selection-expansion-panel` without border ... ``` +The component has no `size` property but, when slotted in a `sbb-radio-button-group` or in a `sbb-checkbox-group`, +it adapts to the parent `size` (`m` or `s`); if there's no wrapping group component, +it adapts its `size` to the slotted `sbb-radio-button-panel` or in a `sbb-checkbox-panel`. + +```html + + + + ... +
Inner Content
+
+
+ + + + ... +
Inner Content
+
+``` + ## Properties diff --git a/src/elements/selection-expansion-panel/selection-expansion-panel.scss b/src/elements/selection-expansion-panel/selection-expansion-panel.scss index 362809acc3..973761a701 100644 --- a/src/elements/selection-expansion-panel/selection-expansion-panel.scss +++ b/src/elements/selection-expansion-panel/selection-expansion-panel.scss @@ -34,6 +34,10 @@ $open-anim-opacity-to: 1; display: contents; } +:host([data-size='s']) { + --sbb-selection-expansion-panel-content-padding-inline: var(--sbb-spacing-responsive-xxxs); +} + :host([color='milk']) { --sbb-selection-expansion-panel-background: var(--sbb-color-milk); } 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 90a39fb47a..62406ea22a 100644 --- a/src/elements/selection-expansion-panel/selection-expansion-panel.spec.ts +++ b/src/elements/selection-expansion-panel/selection-expansion-panel.spec.ts @@ -693,4 +693,96 @@ describe(`sbb-selection-expansion-panel`, () => { expect(checkboxes[5]).not.to.have.attribute('disabled'); }); }); + + describe('size s', () => { + it('checkbox group', async () => { + const root = await fixture(html` + + + Value 1 +
Inner content
+
+ + Value 2 +
Inner content
+
+
+ `); + await waitForLitRender(root); + expect( + root.querySelector('sbb-selection-expansion-panel#one')!.getAttribute('data-size'), + ).to.be.equal('s'); + expect( + root.querySelector('sbb-selection-expansion-panel#two')!.getAttribute('data-size'), + ).to.be.equal('s'); + root.setAttribute('size', 'm'); + await waitForLitRender(root); + expect( + root.querySelector('sbb-selection-expansion-panel#one')!.getAttribute('data-size'), + ).to.be.equal('m'); + expect( + root.querySelector('sbb-selection-expansion-panel#two')!.getAttribute('data-size'), + ).to.be.equal('m'); + }); + + it('checkbox panel', async () => { + const root = await fixture(html` + + Value +
Inner content
+
+ `); + await waitForLitRender(root); + expect(root.getAttribute('data-size')).to.be.equal('s'); + const panel = root.querySelector('sbb-checkbox-panel')!; + panel.setAttribute('size', 'm'); + await waitForLitRender(root); + expect(root.getAttribute('data-size')).to.be.equal('m'); + }); + + it('radio group', async () => { + const root = await fixture(html` + + + Value 1 +
Inner content
+
+ + Value 2 +
Inner content
+
+
+ `); + await waitForLitRender(root); + expect( + root.querySelector('sbb-selection-expansion-panel#one')!.getAttribute('data-size'), + ).to.be.equal('s'); + expect( + root.querySelector('sbb-selection-expansion-panel#two')!.getAttribute('data-size'), + ).to.be.equal('s'); + root.setAttribute('size', 'm'); + await waitForLitRender(root); + expect( + root.querySelector('sbb-selection-expansion-panel#one')!.getAttribute('data-size'), + ).to.be.equal('m'); + expect( + root.querySelector('sbb-selection-expansion-panel#two')!.getAttribute('data-size'), + ).to.be.equal('m'); + }); + + it('radio panel', async () => { + const root = await fixture(html` + + Value +
Inner content
+
+ `); + await waitForLitRender(root); + expect(root.getAttribute('data-size')).to.be.equal('s'); + const panel = root.querySelector('sbb-radio-button-panel')!; + panel.setAttribute('size', 'm'); + await waitForLitRender(root); + expect(root.getAttribute('data-size')).to.be.equal('m'); + }); + }); }); diff --git a/src/elements/selection-expansion-panel/selection-expansion-panel.stories.ts b/src/elements/selection-expansion-panel/selection-expansion-panel.stories.ts index 0e12b47e2d..4043613723 100644 --- a/src/elements/selection-expansion-panel/selection-expansion-panel.stories.ts +++ b/src/elements/selection-expansion-panel/selection-expansion-panel.stories.ts @@ -45,6 +45,16 @@ const borderless: InputType = { }, }; +const size: InputType = { + control: { + type: 'inline-radio', + }, + options: ['m', 's'], + table: { + category: 'Group / Input', + }, +}; + const checkedInput: InputType = { control: { type: 'boolean', @@ -64,9 +74,10 @@ const disabledInput: InputType = { }; const basicArgTypes: ArgTypes = { - color: color, + color, 'force-open': forceOpen, - borderless: borderless, + borderless, + size, checkedInput, disabledInput, }; @@ -75,6 +86,7 @@ const basicArgs: Args = { color: color.options![0], 'force-open': false, borderless: false, + size: size.options![0], checkedInput: false, disabledInput: false, }; @@ -87,11 +99,11 @@ const suffixStyle: Readonly = { const cardBadge = (): TemplateResult => html`%`; -const suffixAndSubtext = (): TemplateResult => html` +const suffixAndSubtext = (size: string): TemplateResult => html` Subtext - CHF 40.00 + CHF 40.00 `; @@ -107,11 +119,12 @@ const innerContent = (): TemplateResult => html` const WithCheckboxTemplate = ({ checkedInput, disabledInput, + size, ...args }: Args): TemplateResult => html` - - Value one ${suffixAndSubtext()} ${cardBadge()} + + Value one ${suffixAndSubtext(size)} ${cardBadge()} ${innerContent()} @@ -120,11 +133,17 @@ const WithCheckboxTemplate = ({ const WithRadioButtonTemplate = ({ checkedInput, disabledInput, + size, ...args }: Args): TemplateResult => html` - - Value one ${suffixAndSubtext()} ${cardBadge()} + + Value one ${suffixAndSubtext(size)} ${cardBadge()} ${innerContent()} @@ -133,25 +152,28 @@ const WithRadioButtonTemplate = ({ const WithCheckboxGroupTemplate = ({ checkedInput, disabledInput, + size, ...args }: Args): TemplateResult => html` - + - Value one ${suffixAndSubtext()} ${cardBadge()} + Value one ${suffixAndSubtext(size)} ${cardBadge()} ${innerContent()} - Value two ${suffixAndSubtext()} ${cardBadge()} + Value two ${suffixAndSubtext(size)} ${cardBadge()} ${innerContent()} - Value three ${suffixAndSubtext()} ${cardBadge()} + + Value three ${suffixAndSubtext(size)} ${cardBadge()} + ${innerContent()} @@ -161,30 +183,32 @@ const WithRadioButtonGroupTemplate = ({ checkedInput, disabledInput, allowEmptySelection, + size, ...args }: Args): TemplateResult => html` - Value one ${suffixAndSubtext()} ${cardBadge()} + Value one ${suffixAndSubtext(size)} ${cardBadge()} ${innerContent()} - Value two ${suffixAndSubtext()} ${cardBadge()} + Value two ${suffixAndSubtext(size)} ${cardBadge()} ${innerContent()} - Value three ${suffixAndSubtext()} ${cardBadge()} + Value three ${suffixAndSubtext(size)} ${cardBadge()} ${innerContent()} @@ -194,12 +218,13 @@ const WithRadioButtonGroupTemplate = ({ const TicketsOptionsExampleTemplate = ({ checkedInput, disabledInput, + size, ...args }: Args): TemplateResult => html` - + - Saving ${suffixAndSubtext()} ${cardBadge()} + Saving ${suffixAndSubtext(size)} ${cardBadge()}
@@ -240,7 +265,7 @@ const TicketsOptionsExampleTemplate = ({ - City offer ${suffixAndSubtext()} ${cardBadge()} + City offer ${suffixAndSubtext(size)} ${cardBadge()}
@@ -284,9 +309,10 @@ const TicketsOptionsExampleTemplate = ({ const NestedRadioTemplate = ({ checkedInput, disabledInput, + size, ...args }: Args): TemplateResult => html` - + Main Option 1 @@ -312,9 +338,10 @@ const NestedRadioTemplate = ({ const NestedCheckboxTemplate = ({ checkedInput, disabledInput, + size, ...args }: Args): TemplateResult => html` - + Main Option 1 @@ -340,6 +367,7 @@ const NestedCheckboxTemplate = ({ const WithCheckboxesErrorMessageTemplate = ({ checkedInput, disabledInput, + size, ...args }: Args): TemplateResult => { const sbbFormError: SbbFormErrorElement = document.createElement('sbb-form-error'); @@ -350,6 +378,7 @@ const WithCheckboxesErrorMessageTemplate = ({ { const checkboxGroup = event.currentTarget as HTMLElement; const hasChecked = Array.from(checkboxGroup.querySelectorAll('sbb-checkbox')).some( @@ -364,20 +393,22 @@ const WithCheckboxesErrorMessageTemplate = ({ > - Value one ${suffixAndSubtext()} ${cardBadge()} + Value one ${suffixAndSubtext(size)} ${cardBadge()} ${innerContent()} - Value two ${suffixAndSubtext()} ${cardBadge()} + Value two ${suffixAndSubtext(size)} ${cardBadge()} ${innerContent()} - Value three ${suffixAndSubtext()} ${cardBadge()} + + Value three ${suffixAndSubtext(size)} ${cardBadge()} + ${innerContent()} ${sbbFormError} @@ -388,6 +419,7 @@ const WithCheckboxesErrorMessageTemplate = ({ const WithRadiosErrorMessageTemplate = ({ checkedInput, disabledInput, + size, ...args }: Args): TemplateResult => { const sbbFormError: SbbFormErrorElement = document.createElement('sbb-form-error'); @@ -398,6 +430,7 @@ const WithRadiosErrorMessageTemplate = ({ ) => { @@ -410,21 +443,21 @@ const WithRadiosErrorMessageTemplate = ({ > - Value one ${suffixAndSubtext()} ${cardBadge()} + Value one ${suffixAndSubtext(size)} ${cardBadge()} ${innerContent()} - Value two ${suffixAndSubtext()} ${cardBadge()} + Value two ${suffixAndSubtext(size)} ${cardBadge()} ${innerContent()} - Value three ${suffixAndSubtext()} ${cardBadge()} + Value three ${suffixAndSubtext(size)} ${cardBadge()} ${innerContent()} @@ -445,6 +478,18 @@ export const WithRadioButton: StoryObj = { args: { ...basicArgs }, }; +export const WithCheckboxSizeS: StoryObj = { + render: WithCheckboxTemplate, + argTypes: basicArgTypes, + args: { ...basicArgs, size: size.options![1] }, +}; + +export const WithRadioButtonSizeS: StoryObj = { + render: WithRadioButtonTemplate, + argTypes: basicArgTypes, + args: { ...basicArgs, size: size.options![1] }, +}; + export const WithCheckboxChecked: StoryObj = { render: WithCheckboxTemplate, argTypes: basicArgTypes, @@ -493,6 +538,18 @@ export const WithRadioButtonGroup: StoryObj = { args: { ...basicArgs, checkedInput: true, disabledInput: true }, }; +export const WithCheckboxGroupSizeS: StoryObj = { + render: WithCheckboxGroupTemplate, + argTypes: basicArgTypes, + args: { ...basicArgs, checkedInput: true, disabledInput: true, size: size.options![1] }, +}; + +export const WithRadioButtonGroupSizeS: StoryObj = { + render: WithRadioButtonGroupTemplate, + argTypes: basicArgTypes, + args: { ...basicArgs, checkedInput: true, disabledInput: true, size: size.options![1] }, +}; + export const WithCheckboxGroupForceOpen: StoryObj = { render: WithCheckboxGroupTemplate, argTypes: basicArgTypes, @@ -632,6 +689,18 @@ export const NestedCheckboxes: StoryObj = { args: { ...basicArgs, checkedInput: true }, }; +export const NestedRadiosSizeS: StoryObj = { + render: NestedRadioTemplate, + argTypes: basicArgTypes, + args: { ...basicArgs, checkedInput: true, size: size.options![1] }, +}; + +export const NestedCheckboxesSizeS: StoryObj = { + render: NestedCheckboxTemplate, + argTypes: basicArgTypes, + args: { ...basicArgs, checkedInput: true, size: size.options![1] }, +}; + const meta: Meta = { decorators: [withActions as Decorator], parameters: { diff --git a/src/elements/selection-expansion-panel/selection-expansion-panel.ts b/src/elements/selection-expansion-panel/selection-expansion-panel.ts index 18fefd8dca..4e8922e8c1 100644 --- a/src/elements/selection-expansion-panel/selection-expansion-panel.ts +++ b/src/elements/selection-expansion-panel/selection-expansion-panel.ts @@ -2,14 +2,15 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; -import type { SbbCheckboxPanelElement } from '../checkbox.js'; +import type { SbbCheckboxGroupElement, SbbCheckboxPanelElement } from '../checkbox.js'; import { SbbConnectedAbortController, SbbLanguageController } from '../core/controllers.js'; import { slotState } from '../core/decorators.js'; import { EventEmitter } from '../core/eventing.js'; import { i18nCollapsed, i18nExpanded } from '../core/i18n.js'; import type { SbbOpenedClosedState, SbbStateChange } from '../core/interfaces.js'; import { SbbHydrationMixin } from '../core/mixins.js'; -import type { SbbRadioButtonPanelElement } from '../radio-button.js'; +import { AgnosticMutationObserver } from '../core/observers.js'; +import type { SbbRadioButtonGroupElement, SbbRadioButtonPanelElement } from '../radio-button.js'; import style from './selection-expansion-panel.scss?lit&inline'; @@ -95,15 +96,22 @@ export class SbbSelectionExpansionPanelElement extends SbbHydrationMixin(LitElem private _language = new SbbLanguageController(this); private _abort = new SbbConnectedAbortController(this); private _initialized: boolean = false; + private _sizeAttributeObserver = new AgnosticMutationObserver((mutationsList: MutationRecord[]) => + this._onSizeAttributesChange(mutationsList), + ); - /** - * Whether it has an expandable content - */ + /** Whether it has an expandable content */ private get _hasContent(): boolean { // We cannot use the NamedSlots because it's too slow to initialize return this.querySelectorAll?.('[slot="content"]').length > 0; } + private get _group(): SbbRadioButtonGroupElement | SbbCheckboxGroupElement | null { + return this.closest('sbb-radio-button-group, sbb-checkbox-group') as + | SbbRadioButtonGroupElement + | SbbCheckboxGroupElement; + } + public override connectedCallback(): void { super.connectedCallback(); @@ -114,6 +122,11 @@ export class SbbSelectionExpansionPanelElement extends SbbHydrationMixin(LitElem this._state ||= 'closed'; } + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this._sizeAttributeObserver.disconnect(); + } + protected override willUpdate(changedProperties: PropertyValues): void { super.willUpdate(changedProperties); @@ -169,9 +182,31 @@ export class SbbSelectionExpansionPanelElement extends SbbHydrationMixin(LitElem this._checked = input.checked; this._disabled = input.disabled; + this._sizeAttributeObserver.disconnect(); + // The size of the inner panel can change due direct change on the panel or due to change of the input-group size. + this._sizeAttributeObserver.observe(input, { attributeFilter: ['size'] }); this._updateState(); } + /** + * Set the data-size in two cases: + * - if there's no group, so the size change comes directly from a change on the inner panel; + * - if there's a wrapper group and its size changes, syncing it with the panel size. + * + * On the other hand, if there's a wrapper group and the size changes on the inner panel, the data-size doesn't change. + */ + private _onSizeAttributesChange(mutationsList: MutationRecord[]): void { + for (const mutation of mutationsList) { + if (mutation.attributeName === 'size') { + const group = this._group; + const size = (mutation.target as HTMLElement).getAttribute('size')!; + if (!group || group.size === size) { + this.setAttribute('data-size', size); + } + } + } + } + private _onInputStateChange(event: CustomEvent): void { if (event.detail.type === 'disabled') { this._disabled = event.detail.disabled; diff --git a/src/elements/selection-expansion-panel/selection-expansion-panel.visual.spec.ts b/src/elements/selection-expansion-panel/selection-expansion-panel.visual.spec.ts index d0e7e1f9e5..0155866945 100644 --- a/src/elements/selection-expansion-panel/selection-expansion-panel.visual.spec.ts +++ b/src/elements/selection-expansion-panel/selection-expansion-panel.visual.spec.ts @@ -28,12 +28,12 @@ describe(`sbb-selection-expansion-panel`, () => { disabled: [false, true], }; - const inputPanelContent = (): TemplateResult => html` + const inputPanelContent = (size: 'm' | 's'): TemplateResult => html` Value one Subtext - CHF 40.00 + CHF 40.00 % `; @@ -50,6 +50,7 @@ describe(`sbb-selection-expansion-panel`, () => { type ParamsType = { [K in keyof typeof cases]: (typeof cases)[K][number] } & { forceOpen?: boolean; value?: string; + size: 'm' | 's'; }; const withCheckboxPanel = (params: Partial): TemplateResult => html` { ?checked=${params.checked} ?disabled=${params.disabled} value=${params.value || nothing} + size=${params.size || 'm'} > - ${inputPanelContent()} + ${inputPanelContent(params.size || 'm')} ${innerContent()} @@ -78,8 +80,9 @@ describe(`sbb-selection-expansion-panel`, () => { ?checked=${params.checked} ?disabled=${params.disabled} value=${params.value || nothing} + size=${params.size || 'm'} > - ${inputPanelContent()} + ${inputPanelContent(params.size || 'm')} ${innerContent()} @@ -115,33 +118,73 @@ describe(`sbb-selection-expansion-panel`, () => { }), ); } + + it( + `size=s`, + visualDiffDefault.with(async (setup) => { + await setup.withFixture(html` + ${input === 'checkbox' + ? withCheckboxPanel({ size: 's' }) + : withRadioPanel({ size: 's' })} + `); + }), + ); }); - it( - `checkbox group with error`, - visualDiffDefault.with(async (setup) => { - await setup.withFixture(html` - - ${withCheckboxPanel({ checked: true })} ${withCheckboxPanel({})} - ${withCheckboxPanel({})} - - Error message - `); - }), - ); + describe('checkbox-group', () => { + for (const size of ['m', 's'] as ('m' | 's')[]) { + describe(`size=${size}`, () => { + for (const error of [true, false]) { + it( + error ? `with error` : '', + visualDiffDefault.with(async (setup) => { + await setup.withFixture(html` + + ${withCheckboxPanel({ checked: true, size })} ${withCheckboxPanel({ size })} + ${withCheckboxPanel({ size })} + + ${error + ? html`Error message` + : nothing} + `); + }), + ); + } + }); + } + }); - it( - `radio button group with error`, - visualDiffDefault.with(async (setup) => { - await setup.withFixture(html` - - ${withRadioPanel({ checked: true, value: '1' })} ${withRadioPanel({ value: '2' })} - ${withRadioPanel({ value: '3' })} - - Error message - `); - }), - ); + describe('radio-button-group', () => { + for (const size of ['m', 's'] as ('m' | 's')[]) { + describe(`size=${size}`, () => { + for (const error of [true, false]) { + it( + error ? `with error` : '', + visualDiffDefault.with(async (setup) => { + await setup.withFixture(html` + + ${withRadioPanel({ checked: true, value: '1', size })} + ${withRadioPanel({ value: '2', size })} + ${withRadioPanel({ value: '3', size })} + + ${error + ? html`Error message` + : nothing} + `); + }), + ); + } + }); + } + }); } }); });