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
|
## 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.
|
You can set the full `modelValue` for each option, which includes the checked property for whether it is checked or not.
|
||||||
|
|
||||||
```html
|
```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
|
## Options with HTML
|
||||||
|
|
|
||||||
|
|
@ -146,8 +146,10 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._invokerNode.selectedElement =
|
this.registrationComplete.then(() => {
|
||||||
this.formElements[/** @type {number} */ (this.checkedIndex)];
|
this._invokerNode.selectedElement =
|
||||||
|
this.formElements[/** @type {number} */ (this.checkedIndex)];
|
||||||
|
});
|
||||||
|
|
||||||
this._invokerNode.hostElement = this;
|
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-select-rich.js';
|
||||||
import '@lion/ui/define/lion-listbox.js';
|
import '@lion/ui/define/lion-listbox.js';
|
||||||
import '@lion/ui/define/lion-option.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 sinon from 'sinon';
|
||||||
import {
|
import {
|
||||||
fixture as _fixture,
|
fixture as _fixture,
|
||||||
|
|
@ -16,7 +17,9 @@ import {
|
||||||
defineCE,
|
defineCE,
|
||||||
expect,
|
expect,
|
||||||
html,
|
html,
|
||||||
|
waitUntil,
|
||||||
} from '@open-wc/testing';
|
} from '@open-wc/testing';
|
||||||
|
import { cache } from 'lit/directives/cache.js';
|
||||||
|
|
||||||
import { isActiveElement } from '../../core/test-helpers/isActiveElement.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', () => {
|
describe('Teardown', () => {
|
||||||
it('correctly removes event listeners when disconnected from dom', async () => {
|
it('correctly removes event listeners when disconnected from dom', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue