Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions demo/story-components/story-styles-settings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { fixture } 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<StoryStylesSettings> {
const el = await fixture<StoryStylesSettings>(html`
<story-styles-settings .styleInputData=${data}></story-styles-settings>
`);
await el.updateComplete;
return el;
}

function getInput(el: StoryStylesSettings, id: string): HTMLInputElement {
const input = el.shadowRoot?.querySelector<HTMLInputElement>(`#${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<HTMLOutputElement>(
'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<HTMLOutputElement>(
'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<HTMLOutputElement>(
'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');
});
});
});
110 changes: 88 additions & 22 deletions demo/story-components/story-styles-settings.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
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';

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;
inputType?: 'color' | 'text';
defaultValue: string | number;
inputType?: StyleInputType;
min?: number;
max?: number;
step?: number;
unit?: string;
};

export type StyleInputData = {
Expand All @@ -32,37 +46,75 @@ export class StoryStylesSettings extends LitElement {
return html`
<div class="settings-options">
<table>
${this.styleInputData.settings.map(
(input) => html`
<tr>
<td>
<label for=${labelToId(input.label)}>${input.label}</label>
</td>
<td>
<input
id=${labelToId(input.label)}
class="style-input"
type=${input.inputType ?? 'text'}
value=${input.defaultValue ?? ''}
data-variable=${input.cssVariable}
/>
</td>
</tr>
`,
${this.styleInputData.settings.map((input) =>
this.renderStyleRow(input),
)}
</table>
<button @click=${this.applyStyles}>Apply</button>
</div>
`;
}

/* Applies styles to demo component. */
/**
* 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`
<tr>
<td>
<label for=${inputId}>${input.label}</label>
</td>
<td class="style-input-cell">
<input
id=${inputId}
class="style-input"
type=${input.inputType ?? 'text'}
min=${ifDefined(isNumeric ? input.min : undefined)}
max=${ifDefined(isNumeric ? input.max : undefined)}
step=${ifDefined(isNumeric ? input.step : undefined)}
value=${input.defaultValue}
data-variable=${input.cssVariable}
data-unit=${ifDefined(input.unit)}
@input=${input.inputType === 'range'
? this.updateRangeReadout
: undefined}
/>
${input.inputType === 'range'
? html`<output class="style-readout" for=${inputId}
>${input.defaultValue}${input.unit ?? ''}</output
>`
: nothing}
</td>
</tr>
`;
}

/**
* 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<HTMLOutputElement>(
`output[for="${CSS.escape(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(
Expand All @@ -80,6 +132,20 @@ export class StoryStylesSettings extends LitElement {
background-color: var(--primary-background-color);
padding: 1em;
}

.style-input-cell {
display: flex;
align-items: center;
}

.style-readout {
min-width: 3.5em;
text-align: right;
}

input[type='range'] {
margin: 5px;
}
`,
];
}
Expand Down
10 changes: 10 additions & 0 deletions src/elements/ia-combo-box/ia-combo-box-story.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/elements/ia-combo-box/ia-combo-box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +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
);
}

#container {
Expand Down Expand Up @@ -1317,7 +1321,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading