fix: improve rendering for lion-select-rich by lit cache (#2566)
This commit is contained in:
parent
07f7ea6bab
commit
5360c5a969
4 changed files with 400 additions and 4 deletions
5
.changeset/plenty-deers-clean.md
Normal file
5
.changeset/plenty-deers-clean.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@lion/ui': patch
|
||||
---
|
||||
|
||||
[select-rich] improve rendering by the `lit` `cache` function
|
||||
|
|
@ -20,10 +20,24 @@ import '@lion/ui/define/lion-option.js';
|
|||
|
||||
## Model value
|
||||
|
||||
### Setting model by the `modelValue` property
|
||||
|
||||
You can set the full `modelValue` for each option, which includes the checked property for whether it is checked or not.
|
||||
|
||||
```html
|
||||
<lion-option .modelValue="${{ value: 'red', checked: false }}">Red</lion-option>
|
||||
<lion-option .modelValue="${ value: 'red', checked: false }">Red</lion-option>
|
||||
```
|
||||
|
||||
Note, when rendering with the help of the `cache` function imported from `lit/directives/cache.js`, setting model by
|
||||
the `modelValue` property is not fully supported. Consider setting the model by the `choiceValue` property instead. See [Setting model by `choiceValue` property](#setting-model-by-the-choicevalue-property) for more details.
|
||||
|
||||
### Setting model by the `choiceValue` property
|
||||
|
||||
You can set the model for each option, providing the value and the checked status as follows:
|
||||
|
||||
```html
|
||||
<lion-option .choiceValue="${'red'}" checked>Red</lion-option>
|
||||
<lion-option .choiceValue="${'blue'}">Blue</lion-option>
|
||||
```
|
||||
|
||||
## Options with HTML
|
||||
|
|
|
|||
|
|
@ -146,8 +146,10 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.registrationComplete.then(() => {
|
||||
this._invokerNode.selectedElement =
|
||||
this.formElements[/** @type {number} */ (this.checkedIndex)];
|
||||
});
|
||||
|
||||
this._invokerNode.hostElement = this;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { LionOption } from '@lion/ui/listbox.js';
|
|||
import '@lion/ui/define/lion-select-rich.js';
|
||||
import '@lion/ui/define/lion-listbox.js';
|
||||
import '@lion/ui/define/lion-option.js';
|
||||
import { LitElement } from 'lit';
|
||||
import '@lion/ui/define/lion-tabs.js';
|
||||
import { LitElement, nothing } from 'lit';
|
||||
import sinon from 'sinon';
|
||||
import {
|
||||
fixture as _fixture,
|
||||
|
|
@ -16,7 +17,9 @@ import {
|
|||
defineCE,
|
||||
expect,
|
||||
html,
|
||||
waitUntil,
|
||||
} from '@open-wc/testing';
|
||||
import { cache } from 'lit/directives/cache.js';
|
||||
|
||||
import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
|
||||
|
||||
|
|
@ -724,6 +727,378 @@ describe('lion-select-rich', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Render from `lit` `cache`', () => {
|
||||
describe('when `lion-option` is set by `.choiceValue` property with and no `checked` attribute', () => {
|
||||
it('should display the second option', async () => {
|
||||
const colours = [
|
||||
{
|
||||
label: 'Red',
|
||||
value: 'red',
|
||||
},
|
||||
{
|
||||
label: 'Blue',
|
||||
value: 'blue',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Note, inactive tab content is **destroyed** on every tab switch.
|
||||
*/
|
||||
class Wrapper extends LitElement {
|
||||
static properties = {
|
||||
...super.properties,
|
||||
activeTabIndex: { type: Number },
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.activeTabIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
*/
|
||||
changeActiveTabIndex(index) {
|
||||
this.activeTabIndex = index;
|
||||
}
|
||||
|
||||
render() {
|
||||
const changeActiveTabIndexRef = this.changeActiveTabIndex.bind(this);
|
||||
return html`
|
||||
<lion-tabs>
|
||||
<button slot="tab" class="first-button" @click=${() => changeActiveTabIndexRef(0)}>
|
||||
Info
|
||||
</button>
|
||||
<p slot="panel">
|
||||
${cache(
|
||||
this.activeTabIndex === 0
|
||||
? html`<lion-select-rich name="favoriteColor" label="Favorite color">
|
||||
${colours.map(
|
||||
colour =>
|
||||
html`<lion-option .choiceValue="${colour.value}"
|
||||
>${colour.label}</lion-option
|
||||
>`,
|
||||
)}
|
||||
</lion-select-rich>`
|
||||
: nothing,
|
||||
)}
|
||||
</p>
|
||||
<button slot="tab" class="second-button" @click=${() => changeActiveTabIndexRef(1)}>
|
||||
Work
|
||||
</button>
|
||||
<p slot="panel">Info page with lots of information about us.</p>
|
||||
</lion-tabs>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const wrapperFixture = /** @type {(arg: TemplateResult) => Promise<Wrapper>} */ (_fixture);
|
||||
const tagString = defineCE(Wrapper);
|
||||
const wrapperTag = unsafeStatic(tagString);
|
||||
const wrapperElement = /** @type {Wrapper} */ (
|
||||
await wrapperFixture(html`<${wrapperTag}></${wrapperTag}>`)
|
||||
);
|
||||
await wrapperElement.updateComplete;
|
||||
const wrapperElementShadowRoot = wrapperElement.shadowRoot;
|
||||
/**
|
||||
* @returns { HTMLElement | null | undefined }
|
||||
*/
|
||||
const getFirstButton = () => wrapperElementShadowRoot?.querySelector('.first-button');
|
||||
/**
|
||||
* @returns { HTMLElement | null | undefined }
|
||||
*/
|
||||
const getSecondButton = () => wrapperElementShadowRoot?.querySelector('.second-button');
|
||||
/**
|
||||
* @returns { HTMLElement | null | undefined }
|
||||
*/
|
||||
const getInvoker = () => wrapperElementShadowRoot?.querySelector('lion-select-invoker');
|
||||
/**
|
||||
* @returns { boolean }
|
||||
*/
|
||||
const isSelectRichRendered = () => !!getInvoker()?.shadowRoot?.childNodes.length;
|
||||
/**
|
||||
* @returns { string | undefined }
|
||||
*/
|
||||
const getSelectedColourLabel = () =>
|
||||
getInvoker()?.shadowRoot?.querySelector('#content-wrapper')?.textContent?.trim();
|
||||
const getDialog = () =>
|
||||
wrapperElementShadowRoot
|
||||
?.querySelector('lion-select-rich')
|
||||
?.shadowRoot?.querySelector('dialog');
|
||||
const isDialogVisible = () =>
|
||||
// @ts-ignore
|
||||
getDialog()?.checkVisibility() === true &&
|
||||
getDialog()?.querySelector('#overlay-content-node-wrapper[data-popper-placement]');
|
||||
await waitUntil(isSelectRichRendered);
|
||||
getInvoker()?.click();
|
||||
await waitUntil(isDialogVisible);
|
||||
const optionBlue = /** @type { HTMLElement | undefined } */ (
|
||||
wrapperElementShadowRoot?.querySelectorAll('lion-option')
|
||||
)?.[1];
|
||||
optionBlue?.click();
|
||||
await waitUntil(() => !isDialogVisible());
|
||||
const selectedColourLabelBeforeTabSwitch = getSelectedColourLabel();
|
||||
expect(selectedColourLabelBeforeTabSwitch).to.equal('Blue');
|
||||
getSecondButton()?.click();
|
||||
await waitUntil(() => !isSelectRichRendered());
|
||||
getFirstButton()?.click();
|
||||
await waitUntil(isSelectRichRendered);
|
||||
const selectedColourAfterTabSwitch = getSelectedColourLabel();
|
||||
expect(selectedColourAfterTabSwitch).to.equal('Blue');
|
||||
});
|
||||
});
|
||||
describe('when `lion-option` is set by `.choiceValue` property and `checked` attribute', () => {
|
||||
it('should display the second option', async () => {
|
||||
const colours = [
|
||||
{
|
||||
label: 'Red',
|
||||
value: 'red',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
label: 'Blue',
|
||||
value: 'blue',
|
||||
checked: false,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Note, inactive tab content is **destroyed** on every tab switch.
|
||||
*/
|
||||
class Wrapper extends LitElement {
|
||||
static properties = {
|
||||
...super.properties,
|
||||
activeTabIndex: { type: Number },
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.activeTabIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
*/
|
||||
changeActiveTabIndex(index) {
|
||||
this.activeTabIndex = index;
|
||||
}
|
||||
|
||||
render() {
|
||||
const changeActiveTabIndexRef = this.changeActiveTabIndex.bind(this);
|
||||
return html`
|
||||
<lion-tabs>
|
||||
<button slot="tab" class="first-button" @click=${() => changeActiveTabIndexRef(0)}>
|
||||
Info
|
||||
</button>
|
||||
<p slot="panel">
|
||||
${cache(
|
||||
this.activeTabIndex === 0
|
||||
? html`<lion-select-rich name="favoriteColor" label="Favorite color">
|
||||
${colours.map(
|
||||
colour =>
|
||||
html`<lion-option
|
||||
.choiceValue="${colour.value}"
|
||||
checked="${colour.checked || nothing}"
|
||||
>${colour.label}</lion-option
|
||||
>`,
|
||||
)}
|
||||
</lion-select-rich>`
|
||||
: nothing,
|
||||
)}
|
||||
</p>
|
||||
<button slot="tab" class="second-button" @click=${() => changeActiveTabIndexRef(1)}>
|
||||
Work
|
||||
</button>
|
||||
<p slot="panel">Info page with lots of information about us.</p>
|
||||
</lion-tabs>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const wrapperFixture = /** @type {(arg: TemplateResult) => Promise<Wrapper>} */ (_fixture);
|
||||
const tagString = defineCE(Wrapper);
|
||||
const wrapperTag = unsafeStatic(tagString);
|
||||
const wrapperElement = /** @type {Wrapper} */ (
|
||||
await wrapperFixture(html`<${wrapperTag}></${wrapperTag}>`)
|
||||
);
|
||||
await wrapperElement.updateComplete;
|
||||
const wrapperElementShadowRoot = wrapperElement.shadowRoot;
|
||||
/**
|
||||
* @returns { HTMLElement | null | undefined }
|
||||
*/
|
||||
const getFirstButton = () => wrapperElementShadowRoot?.querySelector('.first-button');
|
||||
/**
|
||||
* @returns { HTMLElement | null | undefined }
|
||||
*/
|
||||
const getSecondButton = () => wrapperElementShadowRoot?.querySelector('.second-button');
|
||||
/**
|
||||
* @returns { HTMLElement | null | undefined }
|
||||
*/
|
||||
const getInvoker = () => wrapperElementShadowRoot?.querySelector('lion-select-invoker');
|
||||
/**
|
||||
* @returns { boolean }
|
||||
*/
|
||||
const isSelectRichRendered = () => !!getInvoker()?.shadowRoot?.childNodes.length;
|
||||
/**
|
||||
* @returns { string | undefined }
|
||||
*/
|
||||
const getSelectedColourLabel = () =>
|
||||
getInvoker()?.shadowRoot?.querySelector('#content-wrapper')?.textContent?.trim();
|
||||
const getDialog = () =>
|
||||
wrapperElementShadowRoot
|
||||
?.querySelector('lion-select-rich')
|
||||
?.shadowRoot?.querySelector('dialog');
|
||||
const isDialogVisible = () =>
|
||||
// @ts-ignore
|
||||
getDialog()?.checkVisibility() === true &&
|
||||
getDialog()?.querySelector('#overlay-content-node-wrapper[data-popper-placement]');
|
||||
await waitUntil(isSelectRichRendered);
|
||||
getInvoker()?.click();
|
||||
await waitUntil(isDialogVisible);
|
||||
const optionBlue = /** @type { HTMLElement | undefined } */ (
|
||||
wrapperElementShadowRoot?.querySelectorAll('lion-option')
|
||||
)?.[1];
|
||||
optionBlue?.click();
|
||||
await waitUntil(() => !isDialogVisible());
|
||||
const selectedColourLabelBeforeTabSwitch = getSelectedColourLabel();
|
||||
expect(selectedColourLabelBeforeTabSwitch).to.equal('Blue');
|
||||
getSecondButton()?.click();
|
||||
await waitUntil(() => !isSelectRichRendered());
|
||||
getFirstButton()?.click();
|
||||
await waitUntil(isSelectRichRendered);
|
||||
const selectedColourAfterTabSwitch = getSelectedColourLabel();
|
||||
expect(selectedColourAfterTabSwitch).to.equal('Blue');
|
||||
});
|
||||
});
|
||||
describe('when `lion-option` is set by `.modelValue` property', () => {
|
||||
it('should display the second option', async () => {
|
||||
const colours = [
|
||||
{
|
||||
label: 'Red',
|
||||
value: 'red',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
label: 'Blue',
|
||||
value: 'blue',
|
||||
checked: false,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Note, inactive tab content is **destroyed** on every tab switch.
|
||||
*/
|
||||
class Wrapper extends LitElement {
|
||||
static properties = {
|
||||
...super.properties,
|
||||
activeTabIndex: { type: Number },
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.activeTabIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
*/
|
||||
changeActiveTabIndex(index) {
|
||||
this.activeTabIndex = index;
|
||||
}
|
||||
|
||||
render() {
|
||||
const changeActiveTabIndexRef = this.changeActiveTabIndex.bind(this);
|
||||
return html`
|
||||
<lion-tabs>
|
||||
<button slot="tab" class="first-button" @click=${() => changeActiveTabIndexRef(0)}>
|
||||
Info
|
||||
</button>
|
||||
<p slot="panel">
|
||||
${cache(
|
||||
this.activeTabIndex === 0
|
||||
? html`<lion-select-rich name="favoriteColor" label="Favorite color">
|
||||
${colours.map(
|
||||
colour =>
|
||||
html`<lion-option
|
||||
@model-value-changed=${(/** @type {Event} */ ev) => {
|
||||
const lionOption = /** @type {LionOption} */ (ev.target);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
colour.checked = lionOption.modelValue.checked;
|
||||
}}
|
||||
.modelValue=${{ value: colour.value, checked: colour.checked }}
|
||||
>${colour.label}</lion-option
|
||||
>`,
|
||||
)}
|
||||
</lion-select-rich>`
|
||||
: nothing,
|
||||
)}
|
||||
</p>
|
||||
<button slot="tab" class="second-button" @click=${() => changeActiveTabIndexRef(1)}>
|
||||
Work
|
||||
</button>
|
||||
<p slot="panel">Info page with lots of information about us.</p>
|
||||
</lion-tabs>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const wrapperFixture = /** @type {(arg: TemplateResult) => Promise<Wrapper>} */ (_fixture);
|
||||
const tagString = defineCE(Wrapper);
|
||||
const wrapperTag = unsafeStatic(tagString);
|
||||
const wrapperElement = /** @type {Wrapper} */ (
|
||||
await wrapperFixture(html`<${wrapperTag}></${wrapperTag}>`)
|
||||
);
|
||||
await wrapperElement.updateComplete;
|
||||
const wrapperElementShadowRoot = wrapperElement.shadowRoot;
|
||||
/**
|
||||
* @returns { HTMLElement | null | undefined }
|
||||
*/
|
||||
const getFirstButton = () => wrapperElementShadowRoot?.querySelector('.first-button');
|
||||
/**
|
||||
* @returns { HTMLElement | null | undefined }
|
||||
*/
|
||||
const getSecondButton = () => wrapperElementShadowRoot?.querySelector('.second-button');
|
||||
/**
|
||||
* @returns { HTMLElement | null | undefined }
|
||||
*/
|
||||
const getInvoker = () => wrapperElementShadowRoot?.querySelector('lion-select-invoker');
|
||||
/**
|
||||
* @returns { boolean }
|
||||
*/
|
||||
const isSelectRichRendered = () => !!getInvoker()?.shadowRoot?.childNodes.length;
|
||||
/**
|
||||
* @returns { string | undefined }
|
||||
*/
|
||||
const getSelectedColourLabel = () =>
|
||||
getInvoker()?.shadowRoot?.querySelector('#content-wrapper')?.textContent?.trim();
|
||||
const getDialog = () =>
|
||||
wrapperElementShadowRoot
|
||||
?.querySelector('lion-select-rich')
|
||||
?.shadowRoot?.querySelector('dialog');
|
||||
const isDialogVisible = () =>
|
||||
// @ts-ignore
|
||||
getDialog()?.checkVisibility() === true &&
|
||||
getDialog()?.querySelector('#overlay-content-node-wrapper[data-popper-placement]');
|
||||
await waitUntil(isSelectRichRendered);
|
||||
getInvoker()?.click();
|
||||
await waitUntil(isDialogVisible);
|
||||
const optionBlue = /** @type { HTMLElement | undefined } */ (
|
||||
wrapperElementShadowRoot?.querySelectorAll('lion-option')
|
||||
)?.[1];
|
||||
optionBlue?.click();
|
||||
await waitUntil(() => !isDialogVisible());
|
||||
const selectedColourLabelBeforeTabSwitch = getSelectedColourLabel();
|
||||
expect(selectedColourLabelBeforeTabSwitch).to.equal('Blue');
|
||||
getSecondButton()?.click();
|
||||
await waitUntil(() => !isSelectRichRendered());
|
||||
getFirstButton()?.click();
|
||||
await waitUntil(isSelectRichRendered);
|
||||
const selectedColourAfterTabSwitch = getSelectedColourLabel();
|
||||
expect(selectedColourAfterTabSwitch).to.equal('Blue');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Teardown', () => {
|
||||
it('correctly removes event listeners when disconnected from dom', async () => {
|
||||
const el = await fixture(html`
|
||||
|
|
|
|||
Loading…
Reference in a new issue