add focus/blur to color picker

pull/1186/head
Cory LaViska 2023-02-07 17:18:03 -05:00
rodzic 1f1024f4ca
commit b260a4dc29
3 zmienionych plików z 169 dodań i 2 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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`

Wyświetl plik

@ -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({