From 2747a54ede2487f47d4ff7512ca8c0b4703350d1 Mon Sep 17 00:00:00 2001 From: Jeremias Peier Date: Mon, 16 Dec 2024 09:34:24 +0100 Subject: [PATCH] fix(sbb-select): update displayed value on option label change --- .../autocomplete-grid-option.ts | 10 +++ .../option/option/option-base-element.ts | 10 ++- src/elements/option/option/option.ts | 10 +++ src/elements/select/select.spec.ts | 50 ++++++++++++++ src/elements/select/select.ts | 67 ++++++++++++++----- 5 files changed, 131 insertions(+), 16 deletions(-) diff --git a/src/elements/autocomplete-grid/autocomplete-grid-option/autocomplete-grid-option.ts b/src/elements/autocomplete-grid/autocomplete-grid-option/autocomplete-grid-option.ts index a63995b0f0..a49b455c26 100644 --- a/src/elements/autocomplete-grid/autocomplete-grid-option/autocomplete-grid-option.ts +++ b/src/elements/autocomplete-grid/autocomplete-grid-option/autocomplete-grid-option.ts @@ -29,6 +29,7 @@ class SbbAutocompleteGridOptionElement extends SbbOptionBaseElement { public static readonly events = { selectionChange: 'autocompleteOptionSelectionChange', optionSelected: 'autocompleteOptionSelected', + optionLabelChanged: 'optionLabelChanged', } as const; protected optionId = autocompleteGridOptionId; @@ -45,6 +46,15 @@ class SbbAutocompleteGridOptionElement extends SbbOptionBaseElement { SbbAutocompleteGridOptionElement.events.optionSelected, ); + /** + * @internal + * Emits when the label changed. + */ + protected optionLabelChanged: EventEmitter = new EventEmitter( + this, + SbbAutocompleteGridOptionElement.events.optionLabelChanged, + ); + protected override onOptionAttributesChange(mutationsList: MutationRecord[]): void { super.onOptionAttributesChange(mutationsList); this.closest?.('sbb-autocomplete-grid-row')?.toggleAttribute( diff --git a/src/elements/option/option/option-base-element.ts b/src/elements/option/option/option-base-element.ts index 0674c3d60d..fafc9a5eac 100644 --- a/src/elements/option/option/option-base-element.ts +++ b/src/elements/option/option/option-base-element.ts @@ -62,6 +62,9 @@ abstract class SbbOptionBaseElement extends SbbDisabledMixin( /** Emits when an option was selected by user. */ protected abstract optionSelected: EventEmitter; + /** Emits when the label changes. */ + protected abstract optionLabelChanged: EventEmitter; + /** Whether to apply the negative styling */ @state() protected accessor negative = false; @@ -261,13 +264,18 @@ abstract class SbbOptionBaseElement extends SbbDisabledMixin( return nothing; } + private _handleSlotChange(): void { + this.handleHighlightState(); + this.optionLabelChanged.emit(); + } + protected override render(): TemplateResult { return html`
${this.renderIcon()} - + ${this.renderLabel()} ${this._inertAriaGroups && this.getAttribute('data-group-label') ? html` diff --git a/src/elements/option/option/option.ts b/src/elements/option/option/option.ts index 2e6520655b..dc2ca86a8e 100644 --- a/src/elements/option/option/option.ts +++ b/src/elements/option/option/option.ts @@ -31,6 +31,7 @@ class SbbOptionElement extends SbbOptionBaseElement { public static readonly events = { selectionChange: 'optionSelectionChange', optionSelected: 'optionSelected', + optionLabelChanged: 'optionLabelChanged', } as const; protected optionId = `sbb-option`; @@ -47,6 +48,15 @@ class SbbOptionElement extends SbbOptionBaseElement { SbbOptionElement.events.optionSelected, ); + /** + * @internal + * Emits when the label changed. + */ + protected optionLabelChanged: EventEmitter = new EventEmitter( + this, + SbbOptionElement.events.optionLabelChanged, + ); + private set _variant(state: SbbOptionVariant) { if (state) { this.setAttribute('data-variant', state); diff --git a/src/elements/select/select.spec.ts b/src/elements/select/select.spec.ts index 9df1e1ca32..d6d727de6b 100644 --- a/src/elements/select/select.spec.ts +++ b/src/elements/select/select.spec.ts @@ -470,6 +470,56 @@ describe(`sbb-select`, () => { expect(element).to.have.attribute('data-state', 'opened'); }); + + it('updates displayed value on option value change', async () => { + expect(displayValue.textContent!.trim()).to.be.equal('Placeholder'); + firstOption.click(); + await waitForLitRender(element); + displayValue = element.shadowRoot!.querySelector('.sbb-select__trigger')!; + + expect(displayValue.textContent!.trim()).to.be.equal('First'); + + firstOption.textContent = 'First modified'; + await waitForLitRender(element); + displayValue = element.shadowRoot!.querySelector('.sbb-select__trigger')!; + + expect(displayValue.textContent!.trim()).to.be.equal('First modified'); + + // Deselection + element.value = ''; + await waitForLitRender(element); + displayValue = element.shadowRoot!.querySelector('.sbb-select__trigger')!; + + expect(displayValue.textContent!.trim()).to.be.equal('Placeholder'); + }); + + it('updates displayed value on option value change if multiple', async () => { + element.multiple = true; + await waitForLitRender(element); + + expect(displayValue.textContent!.trim()).to.be.equal('Placeholder'); + + firstOption.click(); + secondOption.click(); + await waitForLitRender(element); + displayValue = element.shadowRoot!.querySelector('.sbb-select__trigger')!; + + expect(displayValue.textContent!.trim()).to.be.equal('First, Second'); + + firstOption.textContent = 'First modified'; + await waitForLitRender(element); + displayValue = element.shadowRoot!.querySelector('.sbb-select__trigger')!; + + expect(displayValue.textContent!.trim()).to.be.equal('First modified, Second'); + + // Deselection + firstOption.click(); + secondOption.click(); + await waitForLitRender(element); + displayValue = element.shadowRoot!.querySelector('.sbb-select__trigger')!; + + expect(displayValue.textContent!.trim()).to.be.equal('Placeholder'); + }); }); describe('form association', () => { diff --git a/src/elements/select/select.ts b/src/elements/select/select.ts index c3738d9ddb..4548d5e24e 100644 --- a/src/elements/select/select.ts +++ b/src/elements/select/select.ts @@ -265,18 +265,44 @@ class SbbSelectElement extends SbbUpdateSchedulerMixin( } } + /** Listens to option changes. */ + private _onOptionLabelChanged(event: Event): void { + const target = event.target as SbbOptionElement; + const selectedOption = this._getSelectedOption(); + + if ( + (!Array.isArray(selectedOption) && target !== selectedOption) || + (Array.isArray(selectedOption) && !selectedOption.includes(target)) + ) { + return; + } + + this._updateDisplayValue(selectedOption); + } + + private _updateDisplayValue(selectedOption: SbbOptionElement | SbbOptionElement[] | null): void { + if (Array.isArray(selectedOption)) { + this._displayValue = selectedOption.map((o) => o.textContent).join(', ') || null; + } else if (selectedOption) { + this._displayValue = selectedOption?.textContent || null; + } else { + this._displayValue = null; + } + } + /** Sets the _displayValue by checking the internal sbb-options and setting the correct `selected` value on them. */ private _onValueChanged(newValue: string | string[]): void { const options = this._filteredOptions; if (!Array.isArray(newValue)) { - const optionElement = options.find((o) => (o.value ?? o.getAttribute('value')) === newValue); + const optionElement = + options.find((o) => (o.value ?? o.getAttribute('value')) === newValue) ?? null; if (optionElement) { optionElement.selected = true; } options .filter((o) => (o.value ?? o.getAttribute('value')) !== newValue) .forEach((o) => (o.selected = false)); - this._displayValue = optionElement?.textContent || null; + this._updateDisplayValue(optionElement); } else { options .filter((o) => !newValue.includes(o.value ?? o.getAttribute('value'))) @@ -285,7 +311,7 @@ class SbbSelectElement extends SbbUpdateSchedulerMixin( newValue.includes(o.value ?? o.getAttribute('value')), ); selectedOptionElements.forEach((o) => (o.selected = true)); - this._displayValue = selectedOptionElements.map((o) => o.textContent).join(', ') || null; + this._updateDisplayValue(selectedOptionElements); } this._stateChange.emit({ type: 'value', value: newValue }); } @@ -352,6 +378,11 @@ class SbbSelectElement extends SbbUpdateSchedulerMixin( (e: CustomEvent) => this._onOptionChanged(e), { signal }, ); + + this.addEventListener('optionLabelChanged', (e: Event) => this._onOptionLabelChanged(e), { + signal, + }); + this.addEventListener( 'click', (e: MouseEvent) => { @@ -762,23 +793,29 @@ class SbbSelectElement extends SbbUpdateSchedulerMixin( }; private _setValueFromSelectedOption(): void { - if (!this.multiple) { - const selectedOption = this._filteredOptions.find((option) => option.selected); - if (selectedOption) { - this._activeItemIndex = this._filteredOptions.findIndex( - (option) => option === selectedOption, - ); - this.value = selectedOption.value; - } - } else { - const options = this._filteredOptions.filter((option) => option.selected); - if (options && options.length > 0) { + const selectedOption = this._getSelectedOption(); + + if (Array.isArray(selectedOption)) { + if (selectedOption && selectedOption.length > 0) { const value: string[] = []; - for (const option of options) { + for (const option of selectedOption) { value.push(option.value!); } this.value = value; } + } else if (selectedOption) { + this._activeItemIndex = this._filteredOptions.findIndex( + (option) => option === selectedOption, + ); + this.value = selectedOption.value; + } + } + + private _getSelectedOption(): SbbOptionElement | SbbOptionElement[] | null { + if (this.multiple) { + return this._filteredOptions.filter((option) => option.selected); + } else { + return this._filteredOptions.find((option) => option.selected) ?? null; } }