kopia lustrzana https://github.com/shoelace-style/shoelace
add focus/blur to color picker
rodzic
1f1024f4ca
commit
b260a4dc29
|
@ -8,6 +8,8 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
|||
|
||||
## Next
|
||||
|
||||
- Added the `sl-focus` and `sl-blur` events to `<sl-color-picker>`
|
||||
- Added the `focus()` and `blur()` methods to `<sl-color-picker>`
|
||||
- Fixed a bug in `<sl-animated-image>` where the play and pause buttons were transposed [#1147](https://github.com/shoelace-style/shoelace/issues/1147)
|
||||
- Fixed a bug that prevented `web-types.json` from being generated [#1154](https://github.com/shoelace-style/shoelace/discussions/1154)
|
||||
- Fixed a bug in `<sl-color-picker>` that prevented `sl-change` and `sl-input` from emitting when using the eye dropper [#1157](https://github.com/shoelace-style/shoelace/issues/1157)
|
||||
|
|
|
@ -324,6 +324,101 @@ describe('<sl-color-picker>', () => {
|
|||
expect(previewColor).to.equal('#ff000050');
|
||||
});
|
||||
|
||||
it('should emit sl-focus when rendered as a dropdown and focused', async () => {
|
||||
const el = await fixture<SlColorPicker>(html`
|
||||
<div>
|
||||
<sl-color-picker></sl-color-picker>
|
||||
<button type="button">Click me</button>
|
||||
</div>
|
||||
`);
|
||||
const colorPicker = el.querySelector('sl-color-picker')!;
|
||||
const trigger = colorPicker.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const button = el.querySelector('button')!;
|
||||
const focusHandler = sinon.spy();
|
||||
const blurHandler = sinon.spy();
|
||||
|
||||
colorPicker.addEventListener('sl-focus', focusHandler);
|
||||
colorPicker.addEventListener('sl-blur', blurHandler);
|
||||
|
||||
await clickOnElement(trigger);
|
||||
await colorPicker.updateComplete;
|
||||
expect(focusHandler).to.have.been.calledOnce;
|
||||
|
||||
await clickOnElement(button);
|
||||
await colorPicker.updateComplete;
|
||||
expect(blurHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-focus when rendered inline and focused', async () => {
|
||||
const el = await fixture<SlColorPicker>(html`
|
||||
<div>
|
||||
<sl-color-picker inline></sl-color-picker>
|
||||
<button type="button">Click me</button>
|
||||
</div>
|
||||
`);
|
||||
const colorPicker = el.querySelector('sl-color-picker')!;
|
||||
const button = el.querySelector('button')!;
|
||||
const focusHandler = sinon.spy();
|
||||
const blurHandler = sinon.spy();
|
||||
|
||||
colorPicker.addEventListener('sl-focus', focusHandler);
|
||||
colorPicker.addEventListener('sl-blur', blurHandler);
|
||||
|
||||
await clickOnElement(colorPicker);
|
||||
await colorPicker.updateComplete;
|
||||
expect(focusHandler).to.have.been.calledOnce;
|
||||
|
||||
await clickOnElement(button);
|
||||
await colorPicker.updateComplete;
|
||||
expect(blurHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should focus and blur when calling focus() and blur() and rendered as a dropdown', async () => {
|
||||
const colorPicker = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const focusHandler = sinon.spy();
|
||||
const blurHandler = sinon.spy();
|
||||
|
||||
colorPicker.addEventListener('sl-focus', focusHandler);
|
||||
colorPicker.addEventListener('sl-blur', blurHandler);
|
||||
|
||||
// Focus
|
||||
colorPicker.focus();
|
||||
await colorPicker.updateComplete;
|
||||
|
||||
expect(document.activeElement).to.equal(colorPicker);
|
||||
expect(focusHandler).to.have.been.calledOnce;
|
||||
|
||||
// Blur
|
||||
colorPicker.blur();
|
||||
await colorPicker.updateComplete;
|
||||
|
||||
expect(document.activeElement).to.equal(document.body);
|
||||
expect(blurHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should focus and blur when calling focus() and blur() and rendered inline', async () => {
|
||||
const colorPicker = await fixture<SlColorPicker>(html` <sl-color-picker inline></sl-color-picker> `);
|
||||
const focusHandler = sinon.spy();
|
||||
const blurHandler = sinon.spy();
|
||||
|
||||
colorPicker.addEventListener('sl-focus', focusHandler);
|
||||
colorPicker.addEventListener('sl-blur', blurHandler);
|
||||
|
||||
// Focus
|
||||
colorPicker.focus();
|
||||
await colorPicker.updateComplete;
|
||||
|
||||
expect(document.activeElement).to.equal(colorPicker);
|
||||
expect(focusHandler).to.have.been.calledOnce;
|
||||
|
||||
// Blur
|
||||
colorPicker.blur();
|
||||
await colorPicker.updateComplete;
|
||||
|
||||
expect(document.activeElement).to.equal(document.body);
|
||||
expect(blurHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
describe('when submitting a form', () => {
|
||||
it('should serialize its name and value with FormData', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
|
|
|
@ -49,7 +49,9 @@ declare const EyeDropper: EyeDropperConstructor;
|
|||
*
|
||||
* @slot label - The color picker's form label. Alternatively, you can use the `label` attribute.
|
||||
*
|
||||
* @event sl-blur Emitted when the color picker loses focus.
|
||||
* @event sl-change Emitted when the color picker's value changes.
|
||||
* @event sl-focus Emitted when the color picker receives focus.
|
||||
* @event sl-input Emitted when the color picker receives input.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
|
@ -94,10 +96,13 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
private isSafeValue = false;
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('[part~="base"]') base: HTMLElement;
|
||||
@query('[part~="input"]') input: SlInput;
|
||||
@query('[part~="preview"]') previewButton: HTMLButtonElement;
|
||||
@query('.color-dropdown') dropdown: SlDropdown;
|
||||
@query('[part~="preview"]') previewButton: HTMLButtonElement;
|
||||
@query('[part~="trigger"]') trigger: HTMLButtonElement;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() private isDraggingGridHandle = false;
|
||||
@state() private isEmpty = false;
|
||||
@state() private inputValue = '';
|
||||
|
@ -169,6 +174,20 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
*/
|
||||
@property({ reflect: true }) form = '';
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleFocusIn = this.handleFocusIn.bind(this);
|
||||
this.handleFocusOut = this.handleFocusOut.bind(this);
|
||||
this.addEventListener('focusin', this.handleFocusIn);
|
||||
this.addEventListener('focusout', this.handleFocusOut);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener('focusin', this.handleFocusIn);
|
||||
this.removeEventListener('focusout', this.handleFocusOut);
|
||||
}
|
||||
|
||||
private handleCopy() {
|
||||
this.input.select();
|
||||
document.execCommand('copy');
|
||||
|
@ -181,6 +200,16 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
});
|
||||
}
|
||||
|
||||
private handleFocusIn() {
|
||||
this.hasFocus = true;
|
||||
this.emit('sl-focus');
|
||||
}
|
||||
|
||||
private handleFocusOut() {
|
||||
this.hasFocus = false;
|
||||
this.emit('sl-blur');
|
||||
}
|
||||
|
||||
private handleFormatToggle() {
|
||||
const formats = ['hex', 'rgb', 'hsl', 'hsv'];
|
||||
const nextIndex = (formats.indexOf(this.format) + 1) % formats.length;
|
||||
|
@ -389,6 +418,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
}
|
||||
|
||||
private handleInputInput(event: CustomEvent) {
|
||||
this.formControlController.updateValidity();
|
||||
|
||||
// Prevent the <sl-input>'s sl-input event from bubbling up
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
@ -601,6 +632,11 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
return color.toHex8String();
|
||||
}
|
||||
|
||||
// Prevents nested components from leaking events
|
||||
private stopNestedEventPropagation(event: CustomEvent) {
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
@watch('format', { waitUntilFirstUpdate: true })
|
||||
handleFormatChange() {
|
||||
this.syncValues();
|
||||
|
@ -638,6 +674,32 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
}
|
||||
}
|
||||
|
||||
/** Sets focus on the color picker. */
|
||||
focus(options?: FocusOptions) {
|
||||
if (this.inline) {
|
||||
this.base.focus(options);
|
||||
} else {
|
||||
this.trigger.focus(options);
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes focus from the color picker. */
|
||||
blur() {
|
||||
const elementToBlur = this.inline ? this.base : this.trigger;
|
||||
|
||||
if (this.hasFocus) {
|
||||
// We don't know which element in the color picker has focus, so we'll move it to the trigger or base (inline) and
|
||||
// blur that instead. This results in document.activeElement becoming the <body>. This doesn't cause another focus
|
||||
// event because we're using focusin and something inside the color picker already has focus.
|
||||
elementToBlur.focus({ preventScroll: true });
|
||||
elementToBlur.blur();
|
||||
}
|
||||
|
||||
if (this.dropdown?.open) {
|
||||
this.dropdown.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the current value as a string in the specified format. */
|
||||
getFormattedValue(format: 'hex' | 'hexa' | 'rgb' | 'rgba' | 'hsl' | 'hsla' | 'hsv' | 'hsva' = 'hex') {
|
||||
const currentColor = this.parseColor(
|
||||
|
@ -706,7 +768,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
class=${classMap({
|
||||
'color-picker': true,
|
||||
'color-picker--inline': this.inline,
|
||||
'color-picker--disabled': this.disabled
|
||||
'color-picker--disabled': this.disabled,
|
||||
'color-picker--focused': this.hasFocus
|
||||
})}
|
||||
aria-disabled=${this.disabled ? 'true' : 'false'}
|
||||
aria-labelledby="label"
|
||||
|
@ -835,6 +898,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
@keydown=${this.handleInputKeyDown}
|
||||
@sl-change=${this.handleInputChange}
|
||||
@sl-input=${this.handleInputInput}
|
||||
@sl-blur=${this.stopNestedEventPropagation}
|
||||
@sl-focus=${this.stopNestedEventPropagation}
|
||||
></sl-input>
|
||||
|
||||
<sl-button-group>
|
||||
|
@ -851,6 +916,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
caret:format-button__caret
|
||||
"
|
||||
@click=${this.handleFormatToggle}
|
||||
@sl-blur=${this.stopNestedEventPropagation}
|
||||
@sl-focus=${this.stopNestedEventPropagation}
|
||||
>
|
||||
${this.setLetterCase(this.format)}
|
||||
</sl-button>
|
||||
|
@ -868,6 +935,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
caret:eye-dropper-button__caret
|
||||
"
|
||||
@click=${this.handleEyeDropper}
|
||||
@sl-blur=${this.stopNestedEventPropagation}
|
||||
@sl-focus=${this.stopNestedEventPropagation}
|
||||
>
|
||||
<sl-icon
|
||||
library="system"
|
||||
|
@ -941,6 +1010,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
'color-dropdown__trigger--medium': this.size === 'medium',
|
||||
'color-dropdown__trigger--large': this.size === 'large',
|
||||
'color-dropdown__trigger--empty': this.isEmpty,
|
||||
'color-dropdown__trigger--focused': this.hasFocus,
|
||||
'color-picker__transparent-bg': true
|
||||
})}
|
||||
style=${styleMap({
|
||||
|
|
Ładowanie…
Reference in New Issue