Add support for form submission and validation in color picker

pull/224/head
Cory LaViska 2020-09-04 10:18:46 -04:00
rodzic 9285dd5926
commit 56b64940ca
3 zmienionych plików z 84 dodań i 26 usunięć

Wyświetl plik

@ -2,12 +2,12 @@
## Next
- Added `name` prop to `sl-color-picker`
- Added `name` and `invalid` prop to `sl-color-picker`
- Added support for form submission and validation to `sl-color-picker`
- Fixed a bug where swapping an animated element wouldn't restart the animation in `sl-animation`
- Fixed a bug where the cursor was incorrect when `sl-select` was disabled
- Fixed a bug where `slBlur` and `slFocus` were emitted twice in `sl-select`
- Fixed a bug where clicking on `sl-menu` wouldn't focus it
- Fixed a bug where `sl-color-picker` wasn't submitted with forms
- Fixed a bug in the popover utility where `onAfterShow` would fire too soon
- Improved keyboard logic in `sl-dropdown`, `sl-menu`, and `sl-select`
- Updated `sl-animation` to stable

16
src/components.d.ts vendored
Wyświetl plik

@ -267,6 +267,10 @@ export namespace Components {
* Set to true to render the color picker inline rather than inside a dropdown.
*/
"inline": boolean;
/**
* This will be true when the control is in an invalid state. Validity is determined by the `setCustomValidity()` method using the browser's constraint validation API.
*/
"invalid": boolean;
/**
* The input's name attribute.
*/
@ -275,6 +279,14 @@ export namespace Components {
* Whether to show the opacity slider.
*/
"opacity": boolean;
/**
* Checks for validity and shows the browser's validation message if the control is invalid.
*/
"reportValidity": () => Promise<unknown>;
/**
* Sets a custom validation message. If `message` is not empty, the field will be considered invalid.
*/
"setCustomValidity": (message: string) => Promise<void>;
/**
* When `inline` is true, this determines the size of the color picker's trigger.
*/
@ -1654,6 +1666,10 @@ declare namespace LocalJSX {
* Set to true to render the color picker inline rather than inside a dropdown.
*/
"inline"?: boolean;
/**
* This will be true when the control is in an invalid state. Validity is determined by the `setCustomValidity()` method using the browser's constraint validation API.
*/
"invalid"?: boolean;
/**
* The input's name attribute.
*/

Wyświetl plik

@ -1,4 +1,4 @@
import { Component, Element, Event, EventEmitter, Prop, State, Watch, h } from '@stencil/core';
import { Component, Element, Event, EventEmitter, Method, Prop, State, Watch, h } from '@stencil/core';
import color from 'color';
import { clamp } from '../../utilities/math';
@ -33,12 +33,12 @@ export class ColorPicker {
dropdown: HTMLSlDropdownElement;
lastValueEmitted: string;
menu: HTMLElement;
textInput: HTMLSlInputElement;
trigger: HTMLElement;
input: HTMLSlInputElement;
trigger: HTMLButtonElement;
@Element() host: HTMLSlColorPickerElement;
@State() textInputValue = '';
@State() inputValue = '';
@State() hue = 0;
@State() saturation = 100;
@State() lightness = 100;
@ -67,6 +67,12 @@ export class ColorPicker {
/** Set to true to disable the color picker. */
@Prop() disabled = false;
/**
* This will be true when the control is in an invalid state. Validity is determined by the `setCustomValidity()`
* method using the browser's constraint validation API.
*/
@Prop({ mutable: true, reflect: true }) invalid = false;
/**
* Enable this option to prevent the panel from being clipped when the component is placed inside a container with
* `overflow: auto|scroll`.
@ -123,13 +129,13 @@ export class ColorPicker {
const newColor = this.parseColor(newValue);
if (newColor) {
this.textInputValue = this.value;
this.inputValue = this.value;
this.hue = newColor.hsla.h;
this.saturation = newColor.hsla.s;
this.lightness = newColor.hsla.l;
this.alpha = newColor.hsla.a * 100;
} else {
this.textInputValue = oldValue;
this.inputValue = oldValue;
}
}
@ -157,8 +163,8 @@ export class ColorPicker {
this.handleHueKeyDown = this.handleHueKeyDown.bind(this);
this.handleLightnessInput = this.handleLightnessInput.bind(this);
this.handleSaturationInput = this.handleSaturationInput.bind(this);
this.handleTextInputChange = this.handleTextInputChange.bind(this);
this.handleTextInputKeyDown = this.handleTextInputKeyDown.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleInputKeyDown = this.handleInputKeyDown.bind(this);
}
componentWillLoad() {
@ -166,13 +172,46 @@ export class ColorPicker {
this.setColor(`#ffff`);
}
this.textInputValue = this.value;
this.inputValue = this.value;
this.lastValueEmitted = this.value;
this.syncValues();
}
/** Checks for validity and shows the browser's validation message if the control is invalid. */
@Method()
async reportValidity() {
// If the input is invalid, show the dropdown so the browser can focus on it
if (!this.inline && this.input.invalid) {
// this.dropdown.show();
// setTimeout(() => {
// this.input.reportValidity();
// }, 300);
return new Promise(resolve => {
this.dropdown.addEventListener(
'slAfterShow',
() => {
this.input.reportValidity();
resolve();
},
{ once: true }
);
this.dropdown.show();
});
} else {
return this.input.reportValidity();
}
}
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
@Method()
async setCustomValidity(message: string) {
await this.input.setCustomValidity(message);
this.invalid = this.input.invalid;
}
handleCopy() {
this.textInput.select().then(() => {
this.input.select().then(() => {
document.execCommand('copy');
this.copyButton.setFocus();
this.showCopyCheckmark = true;
@ -358,7 +397,7 @@ export class ColorPicker {
}
}
handleTextInputChange(event: CustomEvent) {
handleInputChange(event: CustomEvent) {
const target = event.target as HTMLInputElement;
this.setColor(target.value);
@ -366,11 +405,11 @@ export class ColorPicker {
event.stopPropagation();
}
handleTextInputKeyDown(event: KeyboardEvent) {
handleInputKeyDown(event: KeyboardEvent) {
if (event.key === 'Enter') {
this.setColor(this.textInput.value);
this.textInput.value = this.value;
setTimeout(() => this.textInput.select());
this.setColor(this.input.value);
this.input.value = this.value;
setTimeout(() => this.input.select());
}
}
@ -562,17 +601,17 @@ export class ColorPicker {
// Update the value
if (this.format === 'hsl') {
this.textInputValue = this.opacity ? currentColor.hsla.string : currentColor.hsl.string;
this.inputValue = this.opacity ? currentColor.hsla.string : currentColor.hsl.string;
} else if (this.format === 'rgb') {
this.textInputValue = this.opacity ? currentColor.rgba.string : currentColor.rgb.string;
this.inputValue = this.opacity ? currentColor.rgba.string : currentColor.rgb.string;
} else {
this.textInputValue = this.opacity ? currentColor.hexa : currentColor.hex;
this.inputValue = this.opacity ? currentColor.hexa : currentColor.hex;
}
// Setting this.value will trigger the watcher which parses the new color. We want to bypass that behavior because
// a) we've already done it in this function and b) conversion/rounding can lead to values changing slightly.
this.bypassValueParse = true;
this.value = this.textInputValue;
this.value = this.inputValue;
this.bypassValueParse = false;
}
@ -689,16 +728,19 @@ export class ColorPicker {
<div class="color-picker__user-input">
<sl-input
ref={el => (this.textInput = el)}
ref={el => (this.input = el)}
part="input"
size="small"
type="text"
name={this.name}
pattern="[a-fA-F\d]+"
value={this.textInputValue}
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="off"
value={this.inputValue}
disabled={this.disabled}
onKeyDown={this.handleTextInputKeyDown}
onSlChange={this.handleTextInputChange}
onKeyDown={this.handleInputKeyDown}
onSlChange={this.handleInputChange}
/>
<sl-button
ref={el => (this.copyButton = el)}