kopia lustrzana https://github.com/shoelace-style/shoelace
fixes #917
rodzic
1c359fbea9
commit
c6df057e15
|
@ -8,6 +8,13 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
|||
|
||||
?> During the beta period, these restrictions may be relaxed in the event of a mission-critical bug. 🐛
|
||||
|
||||
## Next
|
||||
|
||||
- Added the `sl-input` event to `<sl-checkbox>`, `<sl-color-picker>`, `<sl-radio>`, `<sl-range>`, and `<sl-switch>`
|
||||
- Fixed a bug in `<sl-input>` that caused the `sl-change` event to be incorrectly emitted when the value was set programmatically [#917](https://github.com/shoelace-style/shoelace/issues/917)
|
||||
- Fixed a bug in `<sl-color-picker>` that sometimes prevented the color from updating when clicking or tapping on the controls
|
||||
- Fixed a bug in `<sl-color-picker>` that prevented text from being entered in the color input
|
||||
|
||||
## 2.0.0-beta.86
|
||||
|
||||
- 🚨 BREAKING: changed the default value of `date` in `<sl-relative-time>` to the current date instead of the Unix epoch
|
||||
|
|
|
@ -248,6 +248,10 @@ Components must only emit custom events, and all custom events must start with `
|
|||
|
||||
This convention avoids the problem of browsers lowercasing attributes, causing some frameworks to be unable to listen to them. This problem isn't specific to one framework, but [Vue's documentation](https://vuejs.org/v2/guide/components-custom-events.html#Event-Names) provides a good explanation of the problem.
|
||||
|
||||
### Change Events
|
||||
|
||||
When change events are emitted by Shoelace components, they should be named `sl-change` and they should only be emitted as a result of user input. Programmatic changes, such as setting `el.value = '…'` _should not_ result in a change event being emitted. This is consistent with how native form controls work.
|
||||
|
||||
### CSS Custom Properties
|
||||
|
||||
To expose custom properties as part of a component's API, scope them to the `:host` block.
|
||||
|
|
|
@ -52,27 +52,42 @@ describe('<sl-checkbox>', () => {
|
|||
expect(el.invalid).to.be.false;
|
||||
});
|
||||
|
||||
it('should fire sl-change when clicked', async () => {
|
||||
it('should emit sl-change and sl-input when clicked', async () => {
|
||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
|
||||
setTimeout(() => el.shadowRoot!.querySelector('input')!.click());
|
||||
const event = (await oneEvent(el, 'sl-change')) as CustomEvent;
|
||||
expect(event.target).to.equal(el);
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.click();
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(el.checked).to.be.true;
|
||||
});
|
||||
|
||||
it('should fire sl-change when toggled via keyboard', async () => {
|
||||
it('should emit sl-change and sl-input when toggled with spacebar', async () => {
|
||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
|
||||
const input = el.shadowRoot!.querySelector('input')!;
|
||||
input.focus();
|
||||
setTimeout(() => sendKeys({ press: ' ' }));
|
||||
const event = (await oneEvent(el, 'sl-change')) as CustomEvent;
|
||||
expect(event.target).to.equal(el);
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.focus();
|
||||
await el.updateComplete;
|
||||
await sendKeys({ press: ' ' });
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(el.checked).to.be.true;
|
||||
});
|
||||
|
||||
it('should not fire sl-change when checked is set by javascript', async () => {
|
||||
it('should not emit sl-change or sl-input when checked programmatically', async () => {
|
||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
|
||||
el.addEventListener('sl-change', () => expect.fail('event fired'));
|
||||
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.checked = true;
|
||||
await el.updateComplete;
|
||||
el.checked = false;
|
||||
|
|
|
@ -25,6 +25,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @event sl-blur - Emitted when the checkbox loses focus.
|
||||
* @event sl-change - Emitted when the checked state changes.
|
||||
* @event sl-focus - Emitted when the checkbox gains focus.
|
||||
* @event sl-input - Emitted when the checkbox receives input.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart control - The square container that wraps the checkbox's checked state.
|
||||
|
@ -124,6 +125,10 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
|||
this.emit('sl-blur');
|
||||
}
|
||||
|
||||
handleInput() {
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
|
@ -168,6 +173,7 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
|||
.required=${this.required}
|
||||
aria-checked=${this.checked ? 'true' : 'false'}
|
||||
@click=${this.handleClick}
|
||||
@input=${this.handleInput}
|
||||
@blur=${this.handleBlur}
|
||||
@focus=${this.handleFocus}
|
||||
/>
|
||||
|
|
|
@ -1,21 +1,263 @@
|
|||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { aTimeout, expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import { clickOnElement } from '../../internal/test';
|
||||
import type SlColorPicker from './color-picker';
|
||||
|
||||
describe('<sl-color-picker>', () => {
|
||||
it('should emit change and show correct color when the value changes', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLElement>('[part~="trigger"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const color = 'rgb(255, 204, 0)';
|
||||
describe('when the value changes', () => {
|
||||
it('should not emit sl-change or sl-input when the value is changed programmatically', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const color = 'rgb(255, 204, 0)';
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.value = color;
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-change should not be emitted'));
|
||||
el.value = color;
|
||||
await el.updateComplete;
|
||||
});
|
||||
|
||||
await waitUntil(() => changeHandler.calledOnce);
|
||||
it('should emit sl-change and sl-input when the color grid selector is moved', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const grid = el.shadowRoot!.querySelector<HTMLElement>('[part~="grid"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(trigger.style.color).to.equal(color);
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await clickOnElement(grid); // click on the grid
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when the hue slider is moved', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const slider = el.shadowRoot!.querySelector<HTMLElement>('[part~="hue-slider"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await clickOnElement(slider); // click on the hue slider
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when the opacity slider is moved', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker opacity></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const slider = el.shadowRoot!.querySelector<HTMLElement>('[part~="opacity-slider"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await clickOnElement(slider); // click on the opacity slider
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when toggling the format', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker value="#fff"></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const formatButton = el.shadowRoot!.querySelector<HTMLElement>('[part~="format-button"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await clickOnElement(formatButton); // click on the format button
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.value).to.equal('rgb(255, 255, 255)');
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when clicking on a swatch', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const swatch = el.shadowRoot!.querySelector<HTMLElement>('[part~="swatch"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await clickOnElement(swatch); // click on the swatch
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when selecting a color with the keyboard', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const gridHandle = el.shadowRoot!.querySelector<HTMLElement>('[part~="grid-handle"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
gridHandle.focus();
|
||||
await sendKeys({ press: 'ArrowRight' }); // move the grid handle
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when selecting a color with the keyboard', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="grid-handle"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
handle.focus();
|
||||
await sendKeys({ press: 'ArrowRight' }); // move the handle
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when selecting hue with the keyboard', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="hue-slider"] > span')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
handle.focus();
|
||||
await sendKeys({ press: 'ArrowRight' }); // move the handle
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when selecting opacity with the keyboard', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker opacity></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="opacity-slider"] > span')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
handle.focus();
|
||||
await sendKeys({ press: 'ArrowRight' }); // move the handle
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when entering a value in the color input and pressing enter', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker opacity></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const input = el.shadowRoot!.querySelector<HTMLElement>('[part~="input"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
input.focus(); // focus the input
|
||||
await el.updateComplete;
|
||||
await sendKeys({ type: 'fc0' }); // type in a color
|
||||
await sendKeys({ press: 'Enter' }); // press enter
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when entering a value in the color input and blurring the field', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker opacity></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const input = el.shadowRoot!.querySelector<HTMLElement>('[part~="input"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
input.focus(); // focus the input
|
||||
await el.updateComplete;
|
||||
await sendKeys({ type: 'fc0' }); // type in a color
|
||||
input.blur(); // commit changes by blurring the field
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should render the correct format when selecting a swatch of a different format', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker format="rgb"></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
el.swatches = ['#fff'];
|
||||
await el.updateComplete;
|
||||
const swatch = el.shadowRoot!.querySelector<HTMLElement>('[part~="swatch"]')!;
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await clickOnElement(swatch); // click on the swatch
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.value).to.equal('rgb(255, 255, 255)');
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
it('should render in a dropdown', async () => {
|
||||
|
|
|
@ -3,7 +3,6 @@ import { html } from 'lit';
|
|||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { drag } from '../../internal/drag';
|
||||
|
@ -51,6 +50,7 @@ declare const EyeDropper: EyeDropperConstructor;
|
|||
* @slot label - The color picker's form label. Alternatively, you can use the `label` attribute.
|
||||
*
|
||||
* @event sl-change Emitted when the color picker's value changes.
|
||||
* @event sl-input Emitted when the color picker receives input.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart trigger - The color picker's dropdown trigger.
|
||||
|
@ -270,12 +270,16 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
const formats = ['hex', 'rgb', 'hsl'];
|
||||
const nextIndex = (formats.indexOf(this.format) + 1) % formats.length;
|
||||
this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl';
|
||||
this.setColor(this.value);
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
handleAlphaDrag(event: PointerEvent) {
|
||||
const container = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__slider.color-picker__alpha')!;
|
||||
const handle = container.querySelector<HTMLElement>('.color-picker__slider-handle')!;
|
||||
const { width } = container.getBoundingClientRect();
|
||||
let oldValue = this.value;
|
||||
|
||||
handle.focus();
|
||||
event.preventDefault();
|
||||
|
@ -284,6 +288,12 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
onMove: x => {
|
||||
this.alpha = clamp((x / width) * 100, 0, 100);
|
||||
this.syncValues();
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
oldValue = this.value;
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
},
|
||||
initialEvent: event
|
||||
});
|
||||
|
@ -293,6 +303,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
const container = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__slider.color-picker__hue')!;
|
||||
const handle = container.querySelector<HTMLElement>('.color-picker__slider-handle')!;
|
||||
const { width } = container.getBoundingClientRect();
|
||||
let oldValue = this.value;
|
||||
|
||||
handle.focus();
|
||||
event.preventDefault();
|
||||
|
@ -301,6 +312,12 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
onMove: x => {
|
||||
this.hue = clamp((x / width) * 360, 0, 360);
|
||||
this.syncValues();
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
oldValue = this.value;
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
},
|
||||
initialEvent: event
|
||||
});
|
||||
|
@ -310,6 +327,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
const grid = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__grid')!;
|
||||
const handle = grid.querySelector<HTMLElement>('.color-picker__grid-handle')!;
|
||||
const { width, height } = grid.getBoundingClientRect();
|
||||
let oldValue = this.value;
|
||||
|
||||
handle.focus();
|
||||
event.preventDefault();
|
||||
|
@ -322,6 +340,12 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
this.brightness = clamp(100 - (y / height) * 100, 0, 100);
|
||||
this.lightness = this.getLightness(this.brightness);
|
||||
this.syncValues();
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
oldValue = this.value;
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
},
|
||||
onStop: () => (this.isDraggingGridHandle = false),
|
||||
initialEvent: event
|
||||
|
@ -330,6 +354,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
|
||||
handleAlphaKeyDown(event: KeyboardEvent) {
|
||||
const increment = event.shiftKey ? 10 : 1;
|
||||
const oldValue = this.value;
|
||||
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
|
@ -354,10 +379,16 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
this.alpha = 100;
|
||||
this.syncValues();
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
}
|
||||
|
||||
handleHueKeyDown(event: KeyboardEvent) {
|
||||
const increment = event.shiftKey ? 10 : 1;
|
||||
const oldValue = this.value;
|
||||
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
|
@ -382,10 +413,16 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
this.hue = 360;
|
||||
this.syncValues();
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
}
|
||||
|
||||
handleGridKeyDown(event: KeyboardEvent) {
|
||||
const increment = event.shiftKey ? 10 : 1;
|
||||
const oldValue = this.value;
|
||||
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
|
@ -414,10 +451,19 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
this.lightness = this.getLightness(this.brightness);
|
||||
this.syncValues();
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
}
|
||||
|
||||
handleInputChange(event: CustomEvent) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const oldValue = this.value;
|
||||
|
||||
// Prevent the <sl-input>'s sl-change event from bubbling up
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.input.value) {
|
||||
this.setColor(target.value);
|
||||
|
@ -426,14 +472,30 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
this.value = '';
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
}
|
||||
|
||||
handleInputInput(event: CustomEvent) {
|
||||
// Prevent the <sl-input>'s sl-input event from bubbling up
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
handleInputKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
const oldValue = this.value;
|
||||
|
||||
if (this.input.value) {
|
||||
this.setColor(this.input.value);
|
||||
this.input.value = this.value;
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
setTimeout(() => this.input.select());
|
||||
} else {
|
||||
this.hue = 0;
|
||||
|
@ -441,6 +503,10 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
}
|
||||
}
|
||||
|
||||
handleTouchMove(event: TouchEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
normalizeColorString(colorString: string) {
|
||||
//
|
||||
// The color module we're using doesn't parse % values for the alpha channel in RGBA and HSLA. It also doesn't parse
|
||||
|
@ -634,6 +700,19 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
});
|
||||
}
|
||||
|
||||
selectSwatch(color: string) {
|
||||
const oldValue = this.value;
|
||||
|
||||
if (!this.disabled) {
|
||||
this.setColor(color);
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@watch('format', { waitUntilFirstUpdate: true })
|
||||
handleFormatChange() {
|
||||
this.syncValues();
|
||||
|
@ -671,7 +750,6 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
}
|
||||
|
||||
if (this.value !== this.lastValueEmitted) {
|
||||
this.emit('sl-change');
|
||||
this.lastValueEmitted = this.value;
|
||||
}
|
||||
}
|
||||
|
@ -704,8 +782,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
part="grid"
|
||||
class="color-picker__grid"
|
||||
style=${styleMap({ backgroundColor: `hsl(${this.hue}deg, 100%, 50%)` })}
|
||||
@mousedown=${this.handleGridDrag}
|
||||
@touchstart=${this.handleGridDrag}
|
||||
@pointerdown=${this.handleGridDrag}
|
||||
@touchmove=${this.handleTouchMove}
|
||||
>
|
||||
<span
|
||||
part="grid-handle"
|
||||
|
@ -730,8 +808,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
<div
|
||||
part="slider hue-slider"
|
||||
class="color-picker__hue color-picker__slider"
|
||||
@mousedown=${this.handleHueDrag}
|
||||
@touchstart=${this.handleHueDrag}
|
||||
@pointerdown=${this.handleHueDrag}
|
||||
@touchmove=${this.handleTouchMove}
|
||||
>
|
||||
<span
|
||||
part="slider-handle"
|
||||
|
@ -755,8 +833,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
<div
|
||||
part="slider opacity-slider"
|
||||
class="color-picker__alpha color-picker__slider color-picker__transparent-bg"
|
||||
@mousedown="${this.handleAlphaDrag}"
|
||||
@touchstart="${this.handleAlphaDrag}"
|
||||
@pointerdown="${this.handleAlphaDrag}"
|
||||
@touchmove=${this.handleTouchMove}
|
||||
>
|
||||
<div
|
||||
class="color-picker__alpha-gradient"
|
||||
|
@ -809,11 +887,12 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
.value=${live(this.isEmpty ? '' : this.inputValue)}
|
||||
value=${this.isEmpty ? '' : this.inputValue}
|
||||
?disabled=${this.disabled}
|
||||
aria-label=${this.localize.term('currentValue')}
|
||||
@keydown=${this.handleInputKeyDown}
|
||||
@sl-change=${this.handleInputChange}
|
||||
@sl-input=${this.handleInputInput}
|
||||
></sl-input>
|
||||
|
||||
<sl-button-group>
|
||||
|
@ -870,7 +949,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
tabindex=${ifDefined(this.disabled ? undefined : '0')}
|
||||
role="button"
|
||||
aria-label=${swatch}
|
||||
@click=${() => !this.disabled && this.setColor(swatch)}
|
||||
@click=${() => this.selectSwatch(swatch)}
|
||||
@keydown=${(event: KeyboardEvent) =>
|
||||
!this.disabled && event.key === 'Enter' && this.setColor(swatch)}
|
||||
>
|
||||
|
|
|
@ -234,6 +234,46 @@ describe('<sl-input>', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when the value changes', () => {
|
||||
it('should emit sl-change and sl-input when the user types in the input', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input></sl-input> `);
|
||||
const inputHandler = sinon.spy();
|
||||
const changeHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.focus();
|
||||
await sendKeys({ type: 'abc' });
|
||||
el.blur();
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledThrice;
|
||||
});
|
||||
|
||||
it('should not emit sl-change or sl-input when the value is set programmatically', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input></sl-input> `);
|
||||
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.value = 'abc';
|
||||
|
||||
await el.updateComplete;
|
||||
});
|
||||
|
||||
it('should not emit sl-change or sl-input when calling setRangeText()', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input value="hi there"></sl-input> `);
|
||||
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.focus();
|
||||
el.setSelectionRange(0, 2);
|
||||
el.setRangeText('hello');
|
||||
|
||||
await el.updateComplete;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when type="number"', () => {
|
||||
it('should be valid when the value is within the boundary of a step', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step=".5" value="1.5"></sl-input> `);
|
||||
|
@ -254,7 +294,7 @@ describe('<sl-input>', () => {
|
|||
expect(el.invalid).to.be.true;
|
||||
});
|
||||
|
||||
it('should increment by step if stepUp() is called', async () => {
|
||||
it('should increment by step when stepUp() is called', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step="2" value="2"></sl-input> `);
|
||||
|
||||
el.stepUp();
|
||||
|
@ -262,7 +302,7 @@ describe('<sl-input>', () => {
|
|||
expect(el.value).to.equal('4');
|
||||
});
|
||||
|
||||
it('should decrement by step if stepDown() is called', async () => {
|
||||
it('should decrement by step when stepDown() is called', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step="2" value="2"></sl-input> `);
|
||||
|
||||
el.stepDown();
|
||||
|
@ -270,35 +310,24 @@ describe('<sl-input>', () => {
|
|||
expect(el.value).to.equal('0');
|
||||
});
|
||||
|
||||
it('should fire sl-input and sl-change if stepUp() is called', async () => {
|
||||
it('should not emit sl-input or sl-change when stepUp() is called programmatically', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step="2" value="2"></sl-input> `);
|
||||
|
||||
const inputHandler = sinon.spy();
|
||||
const changeHandler = sinon.spy();
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.stepUp();
|
||||
|
||||
await waitUntil(() => inputHandler.calledOnce);
|
||||
await waitUntil(() => changeHandler.calledOnce);
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
await el.updateComplete;
|
||||
});
|
||||
|
||||
it('should fire sl-input and sl-change if stepDown() is called', async () => {
|
||||
it('should not emit sl-input and sl-change when stepDown() is called programmatically', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step="2" value="2"></sl-input> `);
|
||||
|
||||
const inputHandler = sinon.spy();
|
||||
const changeHandler = sinon.spy();
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.stepDown();
|
||||
|
||||
el.stepUp();
|
||||
|
||||
await waitUntil(() => inputHandler.calledOnce);
|
||||
await waitUntil(() => changeHandler.calledOnce);
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
await el.updateComplete;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -41,11 +41,11 @@ const isFirefox = isChromium ? false : navigator.userAgent.includes('Firefox');
|
|||
* @slot hide-password-icon - An icon to use in lieu of the default hide password icon.
|
||||
* @slot help-text - Text that describes how to use the input. Alternatively, you can use the `help-text` attribute.
|
||||
*
|
||||
* @event sl-blur - Emitted when the control loses focus.
|
||||
* @event sl-change - Emitted when an alteration to the control's value is committed by the user.
|
||||
* @event sl-clear - Emitted when the clear button is activated.
|
||||
* @event sl-input - Emitted when the control receives input and its value changes.
|
||||
* @event sl-focus - Emitted when the control gains focus.
|
||||
* @event sl-blur - Emitted when the control loses focus.
|
||||
* @event sl-input - Emitted when the control receives input.
|
||||
*
|
||||
* @csspart form-control - The form control that wraps the label, input, and help text.
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
|
@ -241,16 +241,15 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
|||
/** Replaces a range of text with a new string. */
|
||||
setRangeText(
|
||||
replacement: string,
|
||||
start: number,
|
||||
end: number,
|
||||
selectMode: 'select' | 'start' | 'end' | 'preserve' = 'preserve'
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: 'select' | 'start' | 'end' | 'preserve'
|
||||
) {
|
||||
// @ts-expect-error - start, end, and selectMode are optional
|
||||
this.input.setRangeText(replacement, start, end, selectMode);
|
||||
|
||||
if (this.value !== this.input.value) {
|
||||
this.value = this.input.value;
|
||||
this.emit('sl-input');
|
||||
this.emit('sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -266,8 +265,6 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
|||
this.input.stepUp();
|
||||
if (this.value !== this.input.value) {
|
||||
this.value = this.input.value;
|
||||
this.emit('sl-input');
|
||||
this.emit('sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -276,8 +273,6 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
|||
this.input.stepDown();
|
||||
if (this.value !== this.input.value) {
|
||||
this.value = this.input.value;
|
||||
this.emit('sl-input');
|
||||
this.emit('sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -162,24 +162,30 @@ describe('when submitting a form', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when emitting "sl-change" event', () => {
|
||||
it('should fire sl-change when toggled via keyboard - arrow key', async () => {
|
||||
describe('when the value changes', () => {
|
||||
it('should emit sl-change when toggled with the arrow keys', async () => {
|
||||
const radioGroup = await fixture<SlRadioGroup>(html`
|
||||
<sl-radio-group>
|
||||
<sl-radio id="radio-1" value="1"></sl-radio>
|
||||
<sl-radio id="radio-2" value="2"></sl-radio>
|
||||
</sl-radio-group>
|
||||
`);
|
||||
const radio1 = radioGroup.querySelector<SlRadio>('#radio-1')!;
|
||||
const firstRadio = radioGroup.querySelector<SlRadio>('#radio-1')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
radio1.focus();
|
||||
setTimeout(() => sendKeys({ press: 'ArrowRight' }));
|
||||
await oneEvent(radioGroup, 'sl-change');
|
||||
radioGroup.addEventListener('sl-change', changeHandler);
|
||||
radioGroup.addEventListener('sl-input', inputHandler);
|
||||
firstRadio.focus();
|
||||
await sendKeys({ press: 'ArrowRight' });
|
||||
await radioGroup.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(radioGroup.value).to.equal('2');
|
||||
});
|
||||
|
||||
it('should fire sl-change when clicked', async () => {
|
||||
it('should emit sl-change and sl-input when clicked', async () => {
|
||||
const radioGroup = await fixture<SlRadioGroup>(html`
|
||||
<sl-radio-group>
|
||||
<sl-radio id="radio-1" value="1"></sl-radio>
|
||||
|
@ -193,7 +199,7 @@ describe('when emitting "sl-change" event', () => {
|
|||
expect(radioGroup.value).to.equal('1');
|
||||
});
|
||||
|
||||
it('should fire sl-change when toggled via keyboard - space', async () => {
|
||||
it('should emit sl-change and sl-input when toggled with spacebar', async () => {
|
||||
const radioGroup = await fixture<SlRadioGroup>(html`
|
||||
<sl-radio-group>
|
||||
<sl-radio id="radio-1" value="1"></sl-radio>
|
||||
|
@ -207,4 +213,18 @@ describe('when emitting "sl-change" event', () => {
|
|||
expect(event.target).to.equal(radioGroup);
|
||||
expect(radioGroup.value).to.equal('1');
|
||||
});
|
||||
|
||||
it('should not emit sl-change or sl-input when the value is changed programmatically', async () => {
|
||||
const radioGroup = await fixture<SlRadioGroup>(html`
|
||||
<sl-radio-group value="1">
|
||||
<sl-radio id="radio-1" value="1"></sl-radio>
|
||||
<sl-radio id="radio-2" value="2"></sl-radio>
|
||||
</sl-radio-group>
|
||||
`);
|
||||
|
||||
radioGroup.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
radioGroup.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
radioGroup.value = '2';
|
||||
await radioGroup.updateComplete;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,6 +25,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* attribute.
|
||||
*
|
||||
* @event sl-change - Emitted when the radio group's selected value changes.
|
||||
* @event sl-input - Emitted when the radio group receives user input.
|
||||
*
|
||||
* @csspart form-control - The form control that wraps the label, input, and help text.
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
|
@ -72,7 +73,6 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
|||
@watch('value')
|
||||
handleValueChange() {
|
||||
if (this.hasUpdated) {
|
||||
this.emit('sl-change');
|
||||
this.updateCheckedRadio();
|
||||
}
|
||||
}
|
||||
|
@ -143,14 +143,20 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
|||
|
||||
handleRadioClick(event: MouseEvent) {
|
||||
const target = event.target as SlRadio | SlRadioButton;
|
||||
const radios = this.getAllRadios();
|
||||
const oldValue = this.value;
|
||||
|
||||
if (target.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.value = target.value;
|
||||
const radios = this.getAllRadios();
|
||||
radios.forEach(radio => (radio.checked = radio === target));
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
|
@ -161,10 +167,13 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
|||
const radios = this.getAllRadios().filter(radio => !radio.disabled);
|
||||
const checkedRadio = radios.find(radio => radio.checked) ?? radios[0];
|
||||
const incr = event.key === ' ' ? 0 : ['ArrowUp', 'ArrowLeft'].includes(event.key) ? -1 : 1;
|
||||
const oldValue = this.value;
|
||||
let index = radios.indexOf(checkedRadio) + incr;
|
||||
|
||||
if (index < 0) {
|
||||
index = radios.length - 1;
|
||||
}
|
||||
|
||||
if (index > radios.length - 1) {
|
||||
index = 0;
|
||||
}
|
||||
|
@ -187,6 +196,11 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
|||
radios[index].shadowRoot!.querySelector('button')!.focus();
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import { clickOnElement } from '../../internal/test';
|
||||
import { serialize } from '../../utilities/form';
|
||||
import type SlRange from './range';
|
||||
|
||||
|
@ -40,8 +42,85 @@ describe('<sl-range>', () => {
|
|||
expect(input.disabled).to.be.true;
|
||||
});
|
||||
|
||||
describe('when the value changes', () => {
|
||||
it('should emit sl-change and sl-input when the value changes from clicking the slider', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range value="0"></sl-range> `);
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
await clickOnElement(el, 'right');
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.value).to.equal(100);
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input and decrease the value when pressing left arrow', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range value="50"></sl-range> `);
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.focus();
|
||||
await sendKeys({ press: 'ArrowLeft' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.value).to.equal(49);
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input and decrease the value when pressing right arrow', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range value="50"></sl-range> `);
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.focus();
|
||||
await sendKeys({ press: 'ArrowRight' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.value).to.equal(51);
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should not emit sl-change or sl-input when changing the value programmatically', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range value="0"></sl-range> `);
|
||||
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.value = 50;
|
||||
|
||||
await el.updateComplete;
|
||||
});
|
||||
|
||||
it('should not emit sl-change or sl-input when stepUp() is called programmatically', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range step="2" value="2"></sl-range> `);
|
||||
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.stepUp();
|
||||
await el.updateComplete;
|
||||
});
|
||||
|
||||
it('should not emit sl-change or sl-input when stepDown() is called programmatically', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range step="2" value="2"></sl-range> `);
|
||||
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.stepDown();
|
||||
await el.updateComplete;
|
||||
});
|
||||
});
|
||||
|
||||
describe('step', () => {
|
||||
it('should increment by step if stepUp() is called', async () => {
|
||||
it('should increment by step when stepUp() is called', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range step="2" value="2"></sl-range> `);
|
||||
|
||||
el.stepUp();
|
||||
|
@ -49,33 +128,13 @@ describe('<sl-range>', () => {
|
|||
expect(el.value).to.equal(4);
|
||||
});
|
||||
|
||||
it('should decrement by step if stepDown() is called', async () => {
|
||||
it('should decrement by step when stepDown() is called', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range step="2" value="2"></sl-range> `);
|
||||
|
||||
el.stepDown();
|
||||
await el.updateComplete;
|
||||
expect(el.value).to.equal(0);
|
||||
});
|
||||
|
||||
it('should fire sl-change if stepUp() is called', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range step="2" value="2"></sl-range> `);
|
||||
|
||||
const changeHandler = sinon.spy();
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.stepUp();
|
||||
await waitUntil(() => changeHandler.calledOnce);
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should fire sl-change if stepDown() is called', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range step="2" value="2"></sl-range> `);
|
||||
|
||||
const changeHandler = sinon.spy();
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.stepUp();
|
||||
await waitUntil(() => changeHandler.calledOnce);
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when serializing', () => {
|
||||
|
|
|
@ -22,9 +22,10 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @slot label - The range's label. Alternatively, you can use the `label` attribute.
|
||||
* @slot help-text - Text that describes how to use the input. Alternatively, you can use the `help-text` attribute.
|
||||
*
|
||||
* @event sl-change - Emitted when the control's value changes.
|
||||
* @event sl-blur - Emitted when the control loses focus.
|
||||
* @event sl-change - Emitted when an alteration to the control's value is committed by the user.
|
||||
* @event sl-focus - Emitted when the control gains focus.
|
||||
* @event sl-input - Emitted when the control receives input.
|
||||
*
|
||||
* @csspart form-control - The form control that wraps the label, input, and help text.
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
|
@ -132,7 +133,6 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
|
|||
this.input.stepUp();
|
||||
if (this.value !== Number(this.input.value)) {
|
||||
this.value = Number(this.input.value);
|
||||
this.emit('sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,7 +141,6 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
|
|||
this.input.stepDown();
|
||||
if (this.value !== Number(this.input.value)) {
|
||||
this.value = Number(this.input.value);
|
||||
this.emit('sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,10 +160,13 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
|
|||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
this.emit('sl-change');
|
||||
}
|
||||
|
||||
handleInput() {
|
||||
this.value = parseFloat(this.input.value);
|
||||
this.emit('sl-change');
|
||||
|
||||
this.emit('sl-input');
|
||||
this.syncRange();
|
||||
}
|
||||
|
||||
|
@ -299,6 +301,7 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
|
|||
step=${ifDefined(this.step)}
|
||||
.value=${live(this.value.toString())}
|
||||
aria-describedby="help-text"
|
||||
@change=${this.handleChange}
|
||||
@input=${this.handleInput}
|
||||
@focus=${this.handleFocus}
|
||||
@blur=${this.handleBlur}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import { clickOnElement, tapOnElement } from '../../internal/test';
|
||||
import type SlRating from './rating';
|
||||
|
||||
describe('<sl-rating>', () => {
|
||||
|
@ -48,6 +51,41 @@ describe('<sl-rating>', () => {
|
|||
expect(base.getAttribute('aria-valuenow')).to.equal('3');
|
||||
});
|
||||
|
||||
it('should emit sl-change when clicked', async () => {
|
||||
const el = await fixture<SlRating>(html` <sl-rating></sl-rating> `);
|
||||
const lastSymbol = el.shadowRoot!.querySelector<HTMLSpanElement>('.rating__symbol:last-child')!;
|
||||
const changeHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
|
||||
await clickOnElement(lastSymbol);
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(el.value).to.equal(5);
|
||||
});
|
||||
|
||||
it('should emit sl-change when the value is changed with the keyboard', async () => {
|
||||
const el = await fixture<SlRating>(html` <sl-rating></sl-rating> `);
|
||||
const changeHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.focus();
|
||||
await el.updateComplete;
|
||||
await sendKeys({ press: 'ArrowRight' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(el.value).to.equal(1);
|
||||
});
|
||||
|
||||
it('should not emit sl-change when the value is changed programmatically', async () => {
|
||||
const el = await fixture<SlRating>(html` <sl-rating label="Test" value="1"></sl-rating> `);
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change incorrectly emitted'));
|
||||
el.value = 5;
|
||||
await el.updateComplete;
|
||||
});
|
||||
|
||||
describe('focus', () => {
|
||||
it('should focus inner div', async () => {
|
||||
const el = await fixture<SlRating>(html` <sl-rating label="Test"></sl-rating> `);
|
||||
|
|
|
@ -5,7 +5,6 @@ import { styleMap } from 'lit/directives/style-map.js';
|
|||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||
import { clamp } from '../../internal/math';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import '../icon/icon';
|
||||
import styles from './rating.styles';
|
||||
|
@ -97,6 +96,7 @@ export default class SlRating extends ShoelaceElement {
|
|||
|
||||
handleClick(event: MouseEvent) {
|
||||
this.setValue(this.getValueFromMousePosition(event));
|
||||
this.emit('sl-change');
|
||||
}
|
||||
|
||||
setValue(newValue: number) {
|
||||
|
@ -111,6 +111,7 @@ export default class SlRating extends ShoelaceElement {
|
|||
handleKeyDown(event: KeyboardEvent) {
|
||||
const isLtr = this.localize.dir() === 'ltr';
|
||||
const isRtl = this.localize.dir() === 'rtl';
|
||||
const oldValue = this.value;
|
||||
|
||||
if (this.disabled || this.readonly) {
|
||||
return;
|
||||
|
@ -137,6 +138,10 @@ export default class SlRating extends ShoelaceElement {
|
|||
this.value = this.max;
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseEnter() {
|
||||
|
@ -166,16 +171,12 @@ export default class SlRating extends ShoelaceElement {
|
|||
handleTouchEnd(event: TouchEvent) {
|
||||
this.isHovering = false;
|
||||
this.setValue(this.hoverValue);
|
||||
this.emit('sl-change');
|
||||
|
||||
// Prevent click on mobile devices
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
handleValueChange() {
|
||||
this.emit('sl-change');
|
||||
}
|
||||
|
||||
roundToPrecision(numberToRound: number, precision = 0.5) {
|
||||
const multiplier = 1 / precision;
|
||||
return Math.ceil(numberToRound * multiplier) / multiplier;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { aTimeout, expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import { clickOnElement } from '../../internal/test';
|
||||
import type SlMenuItem from '../menu-item/menu-item';
|
||||
import type SlSelect from './select';
|
||||
|
||||
describe('<sl-select>', () => {
|
||||
|
@ -45,21 +47,77 @@ describe('<sl-select>', () => {
|
|||
expect(submitHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change when the value changes', async () => {
|
||||
const el = await fixture<SlSelect>(html`
|
||||
<sl-select>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
</sl-select>
|
||||
`);
|
||||
const changeHandler = sinon.spy();
|
||||
describe('when the value changes', () => {
|
||||
it('should emit sl-change when the value is changed with the mouse', async () => {
|
||||
const el = await fixture<SlSelect>(html`
|
||||
<sl-select value="option-1">
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
</sl-select>
|
||||
`);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLElement>('[part~="control"]')!;
|
||||
const secondOption = el.querySelectorAll<SlMenuItem>('sl-menu-item')[1];
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.value = 'option-2';
|
||||
await waitUntil(() => changeHandler.calledOnce);
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
await clickOnElement(trigger);
|
||||
await el.updateComplete;
|
||||
await clickOnElement(secondOption);
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(el.value).to.equal('option-2');
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when the value is changed with the keyboard', async () => {
|
||||
const el = await fixture<SlSelect>(html`
|
||||
<sl-select value="option-1">
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
</sl-select>
|
||||
`);
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
el.focus();
|
||||
await sendKeys({ press: ' ' }); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await sendKeys({ press: 'ArrowDown' }); // select the first option
|
||||
await el.updateComplete;
|
||||
await sendKeys({ press: 'ArrowDown' }); // select the second option
|
||||
await el.updateComplete;
|
||||
await sendKeys({ press: 'Enter' }); // commit the selection
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(el.value).to.equal('option-2');
|
||||
});
|
||||
|
||||
it('should not emit sl-change or sl-input when the value is changed programmatically', async () => {
|
||||
const el = await fixture<SlSelect>(html`
|
||||
<sl-select value="option-1">
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
</sl-select>
|
||||
`);
|
||||
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.value = 'option-2';
|
||||
|
||||
await el.updateComplete;
|
||||
});
|
||||
});
|
||||
|
||||
it('should open the menu when any letter key is pressed with sl-select is on focus', async () => {
|
||||
|
|
|
@ -206,9 +206,17 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
|||
}
|
||||
|
||||
handleClearClick(event: MouseEvent) {
|
||||
const oldValue = this.value;
|
||||
|
||||
event.stopPropagation();
|
||||
this.value = this.multiple ? [] : '';
|
||||
this.emit('sl-clear');
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-clear');
|
||||
this.emit('sl-input');
|
||||
this.emit('sl-change');
|
||||
}
|
||||
|
||||
this.syncItemsFromValue();
|
||||
}
|
||||
|
||||
|
@ -291,6 +299,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
|||
|
||||
handleMenuSelect(event: CustomEvent<MenuSelectEventDetail>) {
|
||||
const item = event.detail.item;
|
||||
const oldValue = this.value;
|
||||
|
||||
if (this.multiple) {
|
||||
this.value = this.value.includes(item.value)
|
||||
|
@ -300,6 +309,11 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
|||
this.value = item.value;
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
this.syncItemsFromValue();
|
||||
}
|
||||
|
||||
|
@ -371,9 +385,6 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
|||
await this.updateComplete;
|
||||
|
||||
this.invalid = !this.input.checkValidity();
|
||||
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
resizeMenu() {
|
||||
|
@ -450,12 +461,18 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
|||
syncValueFromItems() {
|
||||
const checkedItems = this.menuItems.filter(item => item.checked);
|
||||
const checkedValues = checkedItems.map(item => item.value);
|
||||
const oldValue = this.value;
|
||||
|
||||
if (this.multiple) {
|
||||
this.value = (this.value as []).filter(val => checkedValues.includes(val));
|
||||
} else {
|
||||
this.value = checkedValues.length > 0 ? checkedValues[0] : '';
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import type SlSwitch from './switch';
|
||||
|
||||
describe('<sl-switch>', () => {
|
||||
|
@ -40,44 +41,72 @@ describe('<sl-switch>', () => {
|
|||
expect(el.invalid).to.be.false;
|
||||
});
|
||||
|
||||
it('should fire sl-change when clicked', async () => {
|
||||
it('should emit sl-change and sl-input when clicked', async () => {
|
||||
const el = await fixture<SlSwitch>(html` <sl-switch></sl-switch> `);
|
||||
setTimeout(() => el.shadowRoot!.querySelector('input')!.click());
|
||||
const event = (await oneEvent(el, 'sl-change')) as CustomEvent;
|
||||
expect(event.target).to.equal(el);
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.click();
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(el.checked).to.be.true;
|
||||
});
|
||||
|
||||
it('should fire sl-change when toggled with spacebar', async () => {
|
||||
it('should emit sl-change when toggled with spacebar', async () => {
|
||||
const el = await fixture<SlSwitch>(html` <sl-switch></sl-switch> `);
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.focus();
|
||||
setTimeout(() => sendKeys({ press: ' ' }));
|
||||
const event = (await oneEvent(el, 'sl-change')) as CustomEvent;
|
||||
expect(event.target).to.equal(el);
|
||||
await sendKeys({ press: ' ' });
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(el.checked).to.be.true;
|
||||
});
|
||||
|
||||
it('should fire sl-change when toggled with the right arrow', async () => {
|
||||
it('should emit sl-change and sl-input when toggled with the right arrow', async () => {
|
||||
const el = await fixture<SlSwitch>(html` <sl-switch></sl-switch> `);
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.focus();
|
||||
setTimeout(() => sendKeys({ press: 'ArrowRight' }));
|
||||
const event = (await oneEvent(el, 'sl-change')) as CustomEvent;
|
||||
expect(event.target).to.equal(el);
|
||||
await sendKeys({ press: 'ArrowRight' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(el.checked).to.be.true;
|
||||
});
|
||||
|
||||
it('should fire sl-change when toggled with the left arrow', async () => {
|
||||
it('should emit sl-change and sl-input when toggled with the left arrow', async () => {
|
||||
const el = await fixture<SlSwitch>(html` <sl-switch checked></sl-switch> `);
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.focus();
|
||||
setTimeout(() => sendKeys({ press: 'ArrowLeft' }));
|
||||
const event = (await oneEvent(el, 'sl-change')) as CustomEvent;
|
||||
expect(event.target).to.equal(el);
|
||||
await sendKeys({ press: 'ArrowLeft' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(el.checked).to.be.false;
|
||||
});
|
||||
|
||||
it('should not fire sl-change when checked is set by javascript', async () => {
|
||||
it('should not emit sl-change or sl-input when checked is set by javascript', async () => {
|
||||
const el = await fixture<SlSwitch>(html` <sl-switch></sl-switch> `);
|
||||
el.addEventListener('sl-change', () => expect.fail('event fired'));
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change incorrectly emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-change incorrectly emitted'));
|
||||
el.checked = true;
|
||||
await el.updateComplete;
|
||||
el.checked = false;
|
||||
|
|
|
@ -21,6 +21,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
*
|
||||
* @event sl-blur - Emitted when the control loses focus.
|
||||
* @event sl-change - Emitted when the control's checked state changes.
|
||||
* @event sl-input - Emitted when the control receives input.
|
||||
* @event sl-focus - Emitted when the control gains focus.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
|
@ -107,6 +108,10 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
|
|||
this.emit('sl-blur');
|
||||
}
|
||||
|
||||
handleInput() {
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
@watch('checked', { waitUntilFirstUpdate: true })
|
||||
handleCheckedChange() {
|
||||
this.input.checked = this.checked; // force a sync update
|
||||
|
@ -135,12 +140,14 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
|
|||
event.preventDefault();
|
||||
this.checked = false;
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
this.checked = true;
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -167,6 +174,7 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
|
|||
role="switch"
|
||||
aria-checked=${this.checked ? 'true' : 'false'}
|
||||
@click=${this.handleClick}
|
||||
@input=${this.handleInput}
|
||||
@blur=${this.handleBlur}
|
||||
@focus=${this.handleFocus}
|
||||
@keydown=${this.handleKeyDown}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import { serialize } from '../../utilities/form';
|
||||
import type SlTextarea from './textarea';
|
||||
|
@ -63,6 +64,46 @@ describe('<sl-textarea>', () => {
|
|||
expect(submitHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
describe('when the value changes', () => {
|
||||
it('should emit sl-change and sl-input when the user types in the textarea', async () => {
|
||||
const el = await fixture<SlTextarea>(html` <sl-textarea></sl-textarea> `);
|
||||
const inputHandler = sinon.spy();
|
||||
const changeHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.focus();
|
||||
await sendKeys({ type: 'abc' });
|
||||
el.blur();
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledThrice;
|
||||
});
|
||||
|
||||
it('should not emit sl-change or sl-input when the value is set programmatically', async () => {
|
||||
const el = await fixture<SlTextarea>(html` <sl-textarea></sl-textarea> `);
|
||||
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.value = 'abc';
|
||||
|
||||
await el.updateComplete;
|
||||
});
|
||||
|
||||
it('should not emit sl-change or sl-input when calling setRangeText()', async () => {
|
||||
const el = await fixture<SlTextarea>(html` <sl-textarea value="hi there"></sl-textarea> `);
|
||||
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.focus();
|
||||
el.setSelectionRange(0, 2);
|
||||
el.setRangeText('hello');
|
||||
|
||||
await el.updateComplete;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using constraint validation', () => {
|
||||
it('should be valid by default', async () => {
|
||||
const el = await fixture<SlTextarea>(html` <sl-textarea></sl-textarea> `);
|
||||
|
|
|
@ -21,10 +21,10 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @slot label - The textarea's label. Alternatively, you can use the `label` attribute.
|
||||
* @slot help-text - Text that describes how to use the input. Alternatively, you can use the `help-text` attribute.
|
||||
*
|
||||
* @event sl-change - Emitted when an alteration to the control's value is committed by the user.
|
||||
* @event sl-input - Emitted when the control receives input and its value changes.
|
||||
* @event sl-focus - Emitted when the control gains focus.
|
||||
* @event sl-blur - Emitted when the control loses focus.
|
||||
* @event sl-change - Emitted when an alteration to the control's value is committed by the user.
|
||||
* @event sl-focus - Emitted when the control gains focus.
|
||||
* @event sl-input - Emitted when the control receives input.
|
||||
*
|
||||
* @csspart form-control - The form control that wraps the label, input, and help text.
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
|
@ -180,22 +180,20 @@ export default class SlTextarea extends ShoelaceElement implements ShoelaceFormC
|
|||
/** Replaces a range of text with a new string. */
|
||||
setRangeText(
|
||||
replacement: string,
|
||||
start: number,
|
||||
end: number,
|
||||
selectMode: 'select' | 'start' | 'end' | 'preserve' = 'preserve'
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: 'select' | 'start' | 'end' | 'preserve'
|
||||
) {
|
||||
// @ts-expect-error - start, end, and selectMode are optional
|
||||
this.input.setRangeText(replacement, start, end, selectMode);
|
||||
|
||||
if (this.value !== this.input.value) {
|
||||
this.value = this.input.value;
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
if (this.value !== this.input.value) {
|
||||
this.value = this.input.value;
|
||||
this.setTextareaHeight();
|
||||
this.emit('sl-input');
|
||||
this.emit('sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ export function drag(container: HTMLElement, options?: Partial<DragOptions>) {
|
|||
document.addEventListener('pointerup', stop);
|
||||
|
||||
// If an initial event is set, trigger the first drag immediately
|
||||
if (options?.initialEvent?.type === 'pointermove') {
|
||||
if (options?.initialEvent instanceof PointerEvent) {
|
||||
move(options.initialEvent);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { sendMouse } from '@web/test-runner-commands';
|
||||
|
||||
/** A testing utility that measures an element's position and clicks on it. */
|
||||
export async function clickOnElement(
|
||||
/** The element to click */
|
||||
el: HTMLElement,
|
||||
/** The location of the element to click */
|
||||
position: 'top' | 'right' | 'bottom' | 'left' | 'center' = 'center',
|
||||
/** The horizontal offset to apply to the position when clicking */
|
||||
offsetX = 0,
|
||||
/** The vertical offset to apply to the position when clicking */
|
||||
offsetY = 0
|
||||
) {
|
||||
const { x, y, width, height } = el.getBoundingClientRect();
|
||||
const centerX = Math.floor(x + window.pageXOffset + width / 2);
|
||||
const centerY = Math.floor(y + window.pageYOffset + height / 2);
|
||||
let clickX: number;
|
||||
let clickY: number;
|
||||
|
||||
switch (position) {
|
||||
case 'top':
|
||||
clickX = centerX;
|
||||
clickY = y;
|
||||
break;
|
||||
case 'right':
|
||||
clickX = x + width - 1;
|
||||
clickY = centerY;
|
||||
break;
|
||||
case 'bottom':
|
||||
clickX = centerX;
|
||||
clickY = y + height - 1;
|
||||
break;
|
||||
case 'left':
|
||||
clickX = x;
|
||||
clickY = centerY;
|
||||
break;
|
||||
default:
|
||||
clickX = centerX;
|
||||
clickY = centerY;
|
||||
}
|
||||
|
||||
clickX += offsetX;
|
||||
clickY += offsetY;
|
||||
|
||||
await sendMouse({ type: 'click', position: [clickX, clickY] });
|
||||
}
|
Ładowanie…
Reference in New Issue