Merge pull request #660 from ing-bank/feat/select-rich-no-default

feat(select-rich): add has no default selection feature
This commit is contained in:
Joren Broekema 2020-03-26 15:24:07 +01:00 committed by GitHub
commit f6168da504
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 176 additions and 3 deletions

View file

@ -591,6 +591,7 @@ export class OverlayController {
break;
case 'min':
this._contentNodeWrapper.style.minWidth = referenceWidth;
this._contentNodeWrapper.style.width = 'auto';
break;
/* no default */
}

View file

@ -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() {

View file

@ -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;
@ -619,13 +635,34 @@ export class LionSelectRich extends ScopedElementsMixin(
};
}
/**
* With no selected element, we should override the inheritsReferenceWidth in most cases.
* By default, we will set it to 'min', and then set it back to what it was initially when
* something is selected.
* As a subclasser you can override this behavior.
*/
_noDefaultSelectedInheritsWidth() {
if (this.checkedIndex === -1) {
this._overlayCtrl.inheritsReferenceWidth = 'min';
} else {
this._overlayCtrl.inheritsReferenceWidth = this._initialInheritsReferenceWidth;
}
}
__setupOverlay() {
this._initialInheritsReferenceWidth = this._overlayCtrl.inheritsReferenceWidth;
this.__overlayBeforeShow = () => {
if (this.hasNoDefaultSelected) {
this._noDefaultSelectedInheritsWidth();
}
};
this.__overlayOnShow = () => {
if (this.checkedIndex != null) {
this.activeIndex = this.checkedIndex;
}
this._listboxNode.focus();
};
this._overlayCtrl.addEventListener('before-show', this.__overlayBeforeShow);
this._overlayCtrl.addEventListener('show', this.__overlayOnShow);
this.__overlayOnHide = () => {
@ -639,6 +676,7 @@ export class LionSelectRich extends ScopedElementsMixin(
__teardownOverlay() {
this._overlayCtrl.removeEventListener('show', this.__overlayOnShow);
this._overlayCtrl.removeEventListener('before-show', this.__overlayBeforeShow);
this._overlayCtrl.removeEventListener('hide', this.__overlayOnHide);
this._scrollTargetNode.removeEventListener('keydown', this.__overlayOnHide);
}

View file

@ -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
<lion-option .choiceValue=${null}>select a color</lion-option>
```
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.
<Story name="No default selected">
{html`
<lion-select-rich name="favoriteColor" label="Favorite color" has-no-default-selected>
<lion-options slot="input">
<lion-option .choiceValue=${'red'}>Red</lion-option>
<lion-option .choiceValue=${'hotpink'}>Hotpink</lion-option>
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-options>
</lion-select-rich>
`}
</Story>
```html
<lion-select-rich name="favoriteColor" label="Favorite color" has-no-default-selected>
<lion-options slot="input">
<lion-option .choiceValue=${'red'}>Red</lion-option>
<lion-option .choiceValue=${'hotpink'}>Hotpink</lion-option>
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-options>
</lion-select-rich>
```
> 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.

View file

@ -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`
<lion-select-rich id="color" name="color" label="Favorite color" has-no-default-selected>
<lion-options slot="input">
<lion-option .choiceValue=${'red'}>Red</lion-option>
<lion-option .choiceValue=${'hotpink'} disabled>Hotpink</lion-option>
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-options>
</lion-select-rich>
`);
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`
@ -363,6 +378,32 @@ describe('lion-select-rich', () => {
await elDisabled.updateComplete;
expect(elDisabled.opened).to.be.false;
});
it('should override the inheritsWidth prop when no default selected feature is used', async () => {
const el = await fixture(html`
<lion-select-rich name="favoriteColor" label="Favorite color" has-no-default-selected>
<lion-options slot="input">
<lion-option .choiceValue=${'red'}>Red</lion-option>
<lion-option .choiceValue=${'hotpink'}>Hotpink</lion-option>
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-options>
</lion-select-rich>
`);
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('full');
el.opened = true;
await el.updateComplete;
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('min');
// Emulate selecting hotpink, it closing, and opening it again
el.modelValue = 'hotpink';
el.opened = false;
await el.updateComplete; // necessary for overlay controller to actually close and re-open
el.opened = true;
await el.updateComplete;
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('full');
});
});
describe('interaction-mode', () => {
@ -711,5 +752,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>
<lion-options slot="input">
<lion-option .choiceValue=${'red'}>Red</lion-option>
<lion-option .choiceValue=${'hotpink'} disabled>Hotpink</lion-option>
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-options>
</${selectTag}>
`);
expect(el._invokerNode.shadowRoot.getElementById('content-wrapper')).dom.to.equal(
`<div id="content-wrapper">Please select an option..</div>`,
);
expect(el.modelValue).to.equal('');
});
});
});