diff --git a/packages/select-rich/src/LionSelectInvoker.js b/packages/select-rich/src/LionSelectInvoker.js index 190ed14be..46fa71b4b 100644 --- a/packages/select-rich/src/LionSelectInvoker.js +++ b/packages/select-rich/src/LionSelectInvoker.js @@ -68,7 +68,15 @@ export class LionSelectInvoker extends LionButton { } return this.selectedElement.textContent; } - return ``; + return this._noSelectionTemplate(); + } + + /** + * To be overriden for a placeholder, used when `hasNoDefaultSelected` is true on the select rich + */ + // eslint-disable-next-line class-methods-use-this + _noSelectionTemplate() { + return html``; } _beforeTemplate() { diff --git a/packages/select-rich/src/LionSelectRich.js b/packages/select-rich/src/LionSelectRich.js index 02c2bfc96..1ec984d95 100644 --- a/packages/select-rich/src/LionSelectRich.js +++ b/packages/select-rich/src/LionSelectRich.js @@ -79,6 +79,17 @@ export class LionSelectRich extends ScopedElementsMixin( type: String, attribute: 'interaction-mode', }, + + /** + * When setting this to true, on initial render, no option will be selected. + * It it advisable to override `_noSelectionTemplate` method in the select-invoker + * to render some kind of placeholder initially + */ + hasNoDefaultSelected: { + type: Boolean, + reflect: true, + attribute: 'has-no-default-selected', + }, }; } @@ -184,6 +195,7 @@ export class LionSelectRich extends ScopedElementsMixin( // for interaction states this._listboxActiveDescendant = null; this.__hasInitialSelectedFormElement = false; + this.hasNoDefaultSelected = false; this._repropagationRole = 'choice-group'; // configures FormControlMixin this.__setupEventListeners(); this.__initInteractionStates(); @@ -335,7 +347,11 @@ export class LionSelectRich extends ScopedElementsMixin( } // the first elements checked by default - if (!this.__hasInitialSelectedFormElement && (!child.disabled || this.disabled)) { + if ( + !this.hasNoDefaultSelected && + !this.__hasInitialSelectedFormElement && + (!child.disabled || this.disabled) + ) { child.active = true; child.checked = true; this.__hasInitialSelectedFormElement = true; diff --git a/packages/select-rich/stories/index.stories.mdx b/packages/select-rich/stories/index.stories.mdx index 9b0d8c7bc..46b94bc4b 100644 --- a/packages/select-rich/stories/index.stories.mdx +++ b/packages/select-rich/stories/index.stories.mdx @@ -462,6 +462,51 @@ console.log(`checkedIndex: ${selectEl.checkedIndex}`); // 0 console.log(`checkedValue: ${selectEl.checkedValue}`); // 'red' ``` +### No default selection + +If you want to set a placeholder option with something like 'Please select', you can of course do this, the same way you would do it in a native select. + +Simply put an option with a modelValue that is `null`. + +```html +select a color +``` + +However, this allows the user to explicitly select this option. + +Often, you may want a placeholder that appears initially, but cannot be selected explicitly by the user. +For this you can use `has-no-default-selected` attribute. + +Both methods work with the `Required` validator. + + + {html` + + + Red + Hotpink + Teal + + + `} + + + +```html + + + Red + Hotpink + Teal + + +``` + +> By default, the placeholder is completely empty in the `LionSelectInvoker`, +> but subclassers can easily override this in their extension, by the overriding `_noSelectionTemplate()` method. + + + ### Custom Invoker You can provide a custom invoker using the invoker slot. diff --git a/packages/select-rich/test/lion-select-rich.test.js b/packages/select-rich/test/lion-select-rich.test.js index 8483d8e55..7cb648022 100644 --- a/packages/select-rich/test/lion-select-rich.test.js +++ b/packages/select-rich/test/lion-select-rich.test.js @@ -3,7 +3,7 @@ import { formFixture as fixture } from '@lion/field/test-helpers.js'; import { OverlayController } from '@lion/overlays'; import { Required } from '@lion/validate'; import { aTimeout, defineCE, expect, html, nextFrame, unsafeStatic } from '@open-wc/testing'; -import { LionSelectRich } from '../index.js'; +import { LionSelectInvoker, LionSelectRich } from '../index.js'; import '../lion-option.js'; import '../lion-options.js'; import '../lion-select-rich.js'; @@ -204,6 +204,21 @@ describe('lion-select-rich', () => { expect(el.showsFeedbackFor.includes('error')).to.be.true; }); + it('supports having no default selection initially', async () => { + const el = await fixture(html` + + + Red + Hotpink + Teal + + + `); + + expect(el.selectedElement).to.be.undefined; + expect(el.modelValue).to.equal(''); + }); + describe('Invoker', () => { it('generates an lion-select-invoker if no invoker is provided', async () => { const el = await fixture(html` @@ -711,5 +726,45 @@ describe('lion-select-rich', () => { el.dispatchEvent(new Event('switch')); expect(el._overlayCtrl.placementMode).to.equal('local'); }); + + it('supports putting a placeholder template when there is no default selection initially', async () => { + const invokerTagName = defineCE( + class extends LionSelectInvoker { + _noSelectionTemplate() { + return html` + Please select an option.. + `; + } + }, + ); + const invokerTag = unsafeStatic(invokerTagName); + + const selectTagName = defineCE( + class extends LionSelectRich { + get slots() { + return { + ...super.slots, + invoker: () => document.createElement(invokerTag.d), + }; + } + }, + ); + const selectTag = unsafeStatic(selectTagName); + + const el = await fixture(html` + <${selectTag} id="color" name="color" label="Favorite color" has-no-default-selected> + + Red + Hotpink + Teal + + + `); + + expect(el._invokerNode.shadowRoot.getElementById('content-wrapper')).dom.to.equal( + `
Please select an option..
`, + ); + expect(el.modelValue).to.equal(''); + }); }); });