From 5d40b600dbcd6075ca6091bb8a258fda611311ab Mon Sep 17 00:00:00 2001 From: Laton Vermette <1619661+latonv@users.noreply.github.com> Date: Wed, 20 May 2026 10:49:29 -0700 Subject: [PATCH 1/7] Add 'range' and 'number' options to style prop settings --- .../story-components/story-styles-settings.ts | 65 ++++++++++++++++--- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/demo/story-components/story-styles-settings.ts b/demo/story-components/story-styles-settings.ts index 253e764..3fd527f 100644 --- a/demo/story-components/story-styles-settings.ts +++ b/demo/story-components/story-styles-settings.ts @@ -1,6 +1,7 @@ import { css, html, LitElement, nothing, type CSSResultGroup } from 'lit'; import { property, queryAll } from 'lit/decorators.js'; import { customElement } from 'lit/decorators/custom-element.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; import themeStyles from '@src/themes/theme-styles'; import { labelToId } from '../story-utils'; @@ -8,8 +9,12 @@ import { labelToId } from '../story-utils'; export type StyleInputSettings = { label: string; cssVariable: string; - defaultValue?: string; - inputType?: 'color' | 'text'; + defaultValue?: string | number; + inputType?: 'color' | 'text' | 'number' | 'range'; + min?: number; + max?: number; + step?: number; + unit?: string; }; export type StyleInputData = { @@ -32,37 +37,64 @@ export class StoryStylesSettings extends LitElement { return html`
- ${this.styleInputData.settings.map( - (input) => html` + ${this.styleInputData.settings.map((input) => { + const inputId = labelToId(input.label); + const isNumeric = + input.inputType === 'number' || input.inputType === 'range'; + return html` - - `, - )} + `; + })}
- + + + ${input.inputType === 'range' + ? html`${input.defaultValue ?? ''}${input.unit ?? ''}` + : nothing}
`; } + /* Updates the live readout next to a range slider as it moves. */ + private updateRangeReadout(e: Event): void { + const input = e.currentTarget as HTMLInputElement; + const output = this.renderRoot.querySelector( + `output[for="${input.id}"]`, + ); + if (!output) return; + const unit = input.dataset.unit ?? ''; + output.textContent = `${input.value}${unit}`; + } + /* Applies styles to demo component. */ private applyStyles(): void { const appliedStyles: string[] = []; this.styleInputs?.forEach((input) => { if (!input.dataset.variable || !input.value) return; - appliedStyles.push(`${input.dataset.variable}: ${input.value};`); + const unit = input.dataset.unit ?? ''; + appliedStyles.push(`${input.dataset.variable}: ${input.value}${unit};`); }); this.dispatchEvent( @@ -80,6 +112,19 @@ export class StoryStylesSettings extends LitElement { background-color: var(--primary-background-color); padding: 1em; } + + .style-input-cell { + display: flex; + align-items: center; + } + + .style-readout { + display: inline-block; + } + + input[type="range"] { + margin: 5px; + } `, ]; } From 4b9f9b11fa35ac93cdb10ddde1267bd8145f531a Mon Sep 17 00:00:00 2001 From: Laton Vermette <1619661+latonv@users.noreply.github.com> Date: Wed, 20 May 2026 10:49:46 -0700 Subject: [PATCH 2/7] Add a range style setting to the combo box story --- src/elements/ia-combo-box/ia-combo-box-story.ts | 10 ++++++++++ src/elements/ia-combo-box/ia-combo-box.ts | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/elements/ia-combo-box/ia-combo-box-story.ts b/src/elements/ia-combo-box/ia-combo-box-story.ts index 5dd283c..5aa8060 100644 --- a/src/elements/ia-combo-box/ia-combo-box-story.ts +++ b/src/elements/ia-combo-box/ia-combo-box-story.ts @@ -40,6 +40,16 @@ const styleInputSettings: StyleInputSettings[] = [ defaultValue: '250px', inputType: 'text', }, + { + label: 'Dropdown fade duration', + cssVariable: '--combo-box-list-fade-duration', + defaultValue: 125, + inputType: 'range', + min: 0, + max: 1000, + step: 25, + unit: 'ms', + }, ]; // Option sets diff --git a/src/elements/ia-combo-box/ia-combo-box.ts b/src/elements/ia-combo-box/ia-combo-box.ts index 6e925c7..39c2615 100644 --- a/src/elements/ia-combo-box/ia-combo-box.ts +++ b/src/elements/ia-combo-box/ia-combo-box.ts @@ -1217,6 +1217,7 @@ export class IAComboBox extends LitElement { --combo-box-padding--: var(--padding-sm); --combo-box-list-width--: var(--combo-box-list-width, unset); --combo-box-list-max-height--: var(--combo-box-list-max-height, 250px); + --combo-box-list-fade-duration--: var(--combo-box-list-fade-duration, 125ms); } #container { @@ -1317,7 +1318,7 @@ export class IAComboBox extends LitElement { max-height: 400px; box-shadow: 0 0 1px 1px #ddd; opacity: 0; - transition: opacity 0.125s ease; + transition: opacity var(--combo-box-list-fade-duration--) ease; } #options-list.visible { From 5bbe900eb00fe629ebc073a5f7e9530aa3add22e Mon Sep 17 00:00:00 2001 From: Laton Vermette <1619661+latonv@users.noreply.github.com> Date: Wed, 20 May 2026 10:50:01 -0700 Subject: [PATCH 3/7] Add a numeric style setting to the search bar story --- .../ia-dropdown-search-bar-story.ts | 8 ++++++++ .../ia-dropdown-search-bar/ia-dropdown-search-bar.ts | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/elements/ia-dropdown-search-bar/ia-dropdown-search-bar-story.ts b/src/elements/ia-dropdown-search-bar/ia-dropdown-search-bar-story.ts index db1618c..4748b8b 100644 --- a/src/elements/ia-dropdown-search-bar/ia-dropdown-search-bar-story.ts +++ b/src/elements/ia-dropdown-search-bar/ia-dropdown-search-bar-story.ts @@ -28,6 +28,14 @@ const styleInputSettings: StyleInputSettings[] = [ defaultValue: '5px', inputType: 'text', }, + { + label: 'Dropdown z-index', + cssVariable: '--dropdown-z-index', + defaultValue: 2, + inputType: 'number', + min: 0, + step: 1, + }, ]; // Component defaults diff --git a/src/elements/ia-dropdown-search-bar/ia-dropdown-search-bar.ts b/src/elements/ia-dropdown-search-bar/ia-dropdown-search-bar.ts index 0ee51c3..426d800 100644 --- a/src/elements/ia-dropdown-search-bar/ia-dropdown-search-bar.ts +++ b/src/elements/ia-dropdown-search-bar/ia-dropdown-search-bar.ts @@ -75,7 +75,7 @@ export class IADropdownSearchBar extends LitElement { `; } - + willUpdate(changed: PropertyValues) { // Push new categories down to the inner dropdown immediately, since ia-dropdown // mutates its own selected option on interaction which can cause Lit's @@ -235,6 +235,7 @@ export class IADropdownSearchBar extends LitElement { --search-bar-width--: var(--search-bar-width, 300px); --search-bar-internal-padding--: var(--padding-sm, 5px); --clear-button-offset--: var(--clear-button-offset, 0); + --dropdown-z-index--: var(--dropdown-z-index, initial); } #container { @@ -276,6 +277,7 @@ export class IADropdownSearchBar extends LitElement { --dropdownBorderRadius: 4px; --buttonSlotPaddingRight: 0; --dropdownTextAlign: left; + --dropdownListZIndex: var(--dropdown-z-index--); } #category-dropdown [slot='dropdown-label'] { From 3eaaf62d2d33b17da8f86030122213157efea7d9 Mon Sep 17 00:00:00 2001 From: Laton Vermette <1619661+latonv@users.noreply.github.com> Date: Wed, 20 May 2026 12:17:37 -0700 Subject: [PATCH 4/7] Extract helpers and other minor cleanup --- .../story-components/story-styles-settings.ts | 93 +++++++++++-------- src/elements/ia-combo-box/ia-combo-box.ts | 5 +- 2 files changed, 58 insertions(+), 40 deletions(-) diff --git a/demo/story-components/story-styles-settings.ts b/demo/story-components/story-styles-settings.ts index 3fd527f..20c1f6d 100644 --- a/demo/story-components/story-styles-settings.ts +++ b/demo/story-components/story-styles-settings.ts @@ -1,4 +1,11 @@ -import { css, html, LitElement, nothing, type CSSResultGroup } from 'lit'; +import { + css, + html, + LitElement, + nothing, + type CSSResultGroup, + type TemplateResult, +} from 'lit'; import { property, queryAll } from 'lit/decorators.js'; import { customElement } from 'lit/decorators/custom-element.js'; import { ifDefined } from 'lit/directives/if-defined.js'; @@ -6,11 +13,13 @@ import { ifDefined } from 'lit/directives/if-defined.js'; import themeStyles from '@src/themes/theme-styles'; import { labelToId } from '../story-utils'; +export type StyleInputType = 'color' | 'text' | 'number' | 'range'; + export type StyleInputSettings = { label: string; cssVariable: string; - defaultValue?: string | number; - inputType?: 'color' | 'text' | 'number' | 'range'; + defaultValue: string | number; + inputType?: StyleInputType; min?: number; max?: number; step?: number; @@ -37,50 +46,55 @@ export class StoryStylesSettings extends LitElement { return html`
- ${this.styleInputData.settings.map((input) => { - const inputId = labelToId(input.label); - const isNumeric = - input.inputType === 'number' || input.inputType === 'range'; - return html` - - - - - `; - })} + ${this.styleInputData.settings.map((input) => + this.renderStyleRow(input), + )}
- - - - ${input.inputType === 'range' - ? html`${input.defaultValue ?? ''}${input.unit ?? ''}` - : nothing} -
`; } + /* Renders one row of the settings table for the given style input. */ + private renderStyleRow(input: StyleInputSettings): TemplateResult { + const inputId = labelToId(input.label); + const isNumeric = + input.inputType === 'number' || input.inputType === 'range'; + return html` + + + + + + + ${input.inputType === 'range' + ? html`${input.defaultValue}${input.unit ?? ''}` + : nothing} + + + `; + } + /* Updates the live readout next to a range slider as it moves. */ private updateRangeReadout(e: Event): void { const input = e.currentTarget as HTMLInputElement; const output = this.renderRoot.querySelector( - `output[for="${input.id}"]`, + `output[for="${CSS.escape(input.id)}"]`, ); if (!output) return; const unit = input.dataset.unit ?? ''; @@ -119,10 +133,11 @@ export class StoryStylesSettings extends LitElement { } .style-readout { - display: inline-block; + min-width: 3.5em; + text-align: right; } - input[type="range"] { + input[type='range'] { margin: 5px; } `, diff --git a/src/elements/ia-combo-box/ia-combo-box.ts b/src/elements/ia-combo-box/ia-combo-box.ts index 39c2615..44f2187 100644 --- a/src/elements/ia-combo-box/ia-combo-box.ts +++ b/src/elements/ia-combo-box/ia-combo-box.ts @@ -1217,7 +1217,10 @@ export class IAComboBox extends LitElement { --combo-box-padding--: var(--padding-sm); --combo-box-list-width--: var(--combo-box-list-width, unset); --combo-box-list-max-height--: var(--combo-box-list-max-height, 250px); - --combo-box-list-fade-duration--: var(--combo-box-list-fade-duration, 125ms); + --combo-box-list-fade-duration--: var( + --combo-box-list-fade-duration, + 125ms + ); } #container { From a187c83019be546e43510c91c13002562b6284a1 Mon Sep 17 00:00:00 2001 From: Laton Vermette <1619661+latonv@users.noreply.github.com> Date: Wed, 20 May 2026 15:22:48 -0700 Subject: [PATCH 5/7] Add unit tests for story style settings --- .../story-styles-settings.test.ts | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 demo/story-components/story-styles-settings.test.ts diff --git a/demo/story-components/story-styles-settings.test.ts b/demo/story-components/story-styles-settings.test.ts new file mode 100644 index 0000000..fa2a9eb --- /dev/null +++ b/demo/story-components/story-styles-settings.test.ts @@ -0,0 +1,174 @@ +import { fixture, oneEvent } from '@open-wc/testing-helpers'; +import { describe, expect, test } from 'vitest'; +import { html } from 'lit'; + +import type { + StoryStylesSettings, + StyleInputData, +} from './story-styles-settings'; +import './story-styles-settings'; + +async function makeSettings( + data: StyleInputData, +): Promise { + const el = await fixture(html` + + `); + await el.updateComplete; + return el; +} + +function getInput(el: StoryStylesSettings, id: string): HTMLInputElement { + const input = el.shadowRoot?.querySelector(`#${id}`); + expect(input, `input #${id} should exist`).to.exist; + return input as HTMLInputElement; +} + +describe('StoryStylesSettings', () => { + describe('range inputs', () => { + const rangeData: StyleInputData = { + settings: [ + { + label: 'Foos', + cssVariable: '--foo', + defaultValue: 50, + inputType: 'range', + min: 0, + max: 250, + step: 10, + unit: 'px', + }, + ], + }; + + test('renders an input of type range with min/max/step set', async () => { + const el = await makeSettings(rangeData); + const input = getInput(el, 'foos'); + + expect(input.type).to.equal('range'); + expect(input.min).to.equal('0'); + expect(input.max).to.equal('250'); + expect(input.step).to.equal('10'); + expect(input.value).to.equal('50'); + expect(input.dataset.unit).to.equal('px'); + }); + + test('renders a readout linked to the input', async () => { + const el = await makeSettings(rangeData); + + const readout = el.shadowRoot?.querySelector( + 'output.style-readout', + ); + expect(readout).to.exist; + expect(readout?.getAttribute('for')).to.equal('foos'); + expect(readout?.textContent).to.equal('50px'); + }); + + test('readout updates on input event with value + unit', async () => { + const el = await makeSettings(rangeData); + const input = getInput(el, 'foos'); + + input.value = '200'; + input.dispatchEvent(new Event('input')); + await el.updateComplete; + + const readout = el.shadowRoot?.querySelector( + 'output.style-readout', + ); + expect(readout?.textContent).to.equal('200px'); + }); + + test('readout omits unit when unit is not provided', async () => { + const el = await makeSettings({ + settings: [ + { + label: 'Foos', + cssVariable: '--foo', + defaultValue: 0.5, + inputType: 'range', + min: 0, + max: 1, + step: 0.1, + }, + ], + }); + const input = getInput(el, 'foos'); + + const readout = el.shadowRoot?.querySelector( + 'output.style-readout', + ); + expect(readout?.textContent).to.equal('0.5'); + + input.value = '0.8'; + input.dispatchEvent(new Event('input')); + await el.updateComplete; + + expect(readout?.textContent).to.equal('0.8'); // No unit included + }); + }); + + describe('number inputs', () => { + const numberData: StyleInputData = { + settings: [ + { + label: 'Foos', + cssVariable: '--foo', + defaultValue: 1, + inputType: 'number', + min: 0, + step: 1, + }, + ], + }; + + test('renders an input of type number with min/step set', async () => { + const el = await makeSettings(numberData); + const input = getInput(el, 'foos'); + + expect(input.type).to.equal('number'); + expect(input.min).to.equal('0'); + expect(input.step).to.equal('1'); + expect(input.value).to.equal('1'); + }); + }); + + describe('non-numeric inputs', () => { + test('text input ignores min/max/step even when set on the settings object', async () => { + const el = await makeSettings({ + settings: [ + { + label: 'Foos', + cssVariable: '--foos', + defaultValue: '200px', + inputType: 'text', + // These should be ignored for non-numeric input types + min: 0, + max: 100, + step: 1, + }, + ], + }); + const input = getInput(el, 'foos'); + + expect(input.type).to.equal('text'); + expect(input.hasAttribute('min')).to.be.false; + expect(input.hasAttribute('max')).to.be.false; + expect(input.hasAttribute('step')).to.be.false; + }); + + test('input without inputType defaults to type=text', async () => { + const el = await makeSettings({ + settings: [ + { + label: 'Untyped', + cssVariable: '--untyped', + defaultValue: 'foo', + }, + ], + }); + const input = getInput(el, 'untyped'); + + expect(input.type).to.equal('text'); + }); + }); +}); From 8e6fd13e41c4b07ba530beb11ce2ec2357c00c72 Mon Sep 17 00:00:00 2001 From: Laton Vermette <1619661+latonv@users.noreply.github.com> Date: Wed, 20 May 2026 15:25:15 -0700 Subject: [PATCH 6/7] Fix doc comments --- demo/story-components/story-styles-settings.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/demo/story-components/story-styles-settings.ts b/demo/story-components/story-styles-settings.ts index 20c1f6d..15bd285 100644 --- a/demo/story-components/story-styles-settings.ts +++ b/demo/story-components/story-styles-settings.ts @@ -55,7 +55,9 @@ export class StoryStylesSettings extends LitElement { `; } - /* Renders one row of the settings table for the given style input. */ + /** + * Renders one row of the settings table for the given style input. + */ private renderStyleRow(input: StyleInputSettings): TemplateResult { const inputId = labelToId(input.label); const isNumeric = @@ -90,7 +92,9 @@ export class StoryStylesSettings extends LitElement { `; } - /* Updates the live readout next to a range slider as it moves. */ + /** + * Updates the live readout next to a range slider as it moves. + */ private updateRangeReadout(e: Event): void { const input = e.currentTarget as HTMLInputElement; const output = this.renderRoot.querySelector( @@ -101,7 +105,9 @@ export class StoryStylesSettings extends LitElement { output.textContent = `${input.value}${unit}`; } - /* Applies styles to demo component. */ + /** + * Applies styles to demo component. + */ private applyStyles(): void { const appliedStyles: string[] = []; From 5a79cbbf3027d7faa0ce57cd4894b524fd2b3de0 Mon Sep 17 00:00:00 2001 From: Laton Vermette <1619661+latonv@users.noreply.github.com> Date: Wed, 20 May 2026 15:27:06 -0700 Subject: [PATCH 7/7] Remove unused import --- demo/story-components/story-styles-settings.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/story-components/story-styles-settings.test.ts b/demo/story-components/story-styles-settings.test.ts index fa2a9eb..cb7b943 100644 --- a/demo/story-components/story-styles-settings.test.ts +++ b/demo/story-components/story-styles-settings.test.ts @@ -1,4 +1,4 @@ -import { fixture, oneEvent } from '@open-wc/testing-helpers'; +import { fixture } from '@open-wc/testing-helpers'; import { describe, expect, test } from 'vitest'; import { html } from 'lit';