fix validity and events

pull/1090/head
Cory LaViska 2022-12-20 11:40:49 -05:00
rodzic 9f79445292
commit 2dc275defd
3 zmienionych plików z 96 dodań i 69 usunięć

Wyświetl plik

@ -3,40 +3,24 @@
[component-header:sl-select] [component-header:sl-select]
```html preview ```html preview
<sl-select value="option-1"> <sl-select>
<sl-option value="option-1">Option 1</sl-option> <sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option> <sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option> <sl-option value="option-3">Option 3</sl-option>
<sl-divider></sl-divider>
<sl-option value="option-4">Option 4</sl-option> <sl-option value="option-4">Option 4</sl-option>
<sl-option value="option-5">Option 5</sl-option> <sl-option value="option-5">Option 5</sl-option>
<sl-option value="option-6">Option 6</sl-option> <sl-option value="option-6">Option 6</sl-option>
<sl-option value="option-7">Option 7</sl-option>
<sl-option value="option-8">Option 8</sl-option>
<sl-option value="option-9">Option 9</sl-option>
<sl-option value="option-10">Option 10</sl-option>
<sl-option value="option-11">Option 11</sl-option>
<sl-option value="option-12">Option 12</sl-option>
<sl-option value="option-13">Option 13</sl-option>
<sl-option value="option-14">Option 14</sl-option>
<sl-option value="option-15">Option 15</sl-option>
<sl-option value="option-16">Option 16</sl-option>
<sl-option value="option-17">Option 17</sl-option>
<sl-option value="option-18">Option 18</sl-option>
<sl-option value="option-19">Option 19</sl-option>
<sl-option value="option-20">Option 20</sl-option>
</sl-select> </sl-select>
``` ```
```jsx react ```jsx react
import { SlDivider, SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react'; import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
const App = () => ( const App = () => (
<SlSelect> <SlSelect>
<SlOption value="option-1">Option 1</SlOption> <SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption> <SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption> <SlOption value="option-3">Option 3</SlOption>
<SlDivider />
<SlOption value="option-4">Option 4</SlOption> <SlOption value="option-4">Option 4</SlOption>
<SlOption value="option-5">Option 5</SlOption> <SlOption value="option-5">Option 5</SlOption>
<SlOption value="option-6">Option 6</SlOption> <SlOption value="option-6">Option 6</SlOption>

Wyświetl plik

@ -21,11 +21,11 @@ export default css`
z-index: var(--sl-z-index-dropdown); z-index: var(--sl-z-index-dropdown);
} }
.select--top::part(popup) { .select[data-current-placement^='top']::part(popup) {
transform-origin: bottom; transform-origin: bottom;
} }
.select--bottom::part(popup) { .select[data-current-placement^='bottom']::part(popup) {
transform-origin: top; transform-origin: top;
} }
@ -48,15 +48,35 @@ export default css`
.select__combobox { .select__combobox {
flex: 1; flex: 1;
display: flex; display: flex;
position: relative;
align-items: stretch; align-items: stretch;
justify-content: start; justify-content: start;
overflow: hidden; overflow: hidden;
} }
.select__combobox:focus { .select__display-input {
width: 100%;
font: inherit;
border: none;
background: none;
cursor: inherit;
-webkit-appearance: none;
}
.select__display-input:focus {
outline: none; outline: none;
} }
.select__value-input {
position: absolute;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
opacity: 0;
z-index: -1;
}
/* Standard selects */ /* Standard selects */
.select--standard .select__combobox-wrapper { .select--standard .select__combobox-wrapper {
background-color: var(--sl-input-background-color); background-color: var(--sl-input-background-color);
@ -161,24 +181,6 @@ export default css`
border-radius: var(--sl-input-height-large); border-radius: var(--sl-input-height-large);
} }
/* Display label (uses a wrapper to allow vertical centering with flex + text truncation on the label) */
.select__display-label-wrapper {
flex: 1 0 auto;
display: flex;
align-items: center;
width: 100%;
}
.select__display-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.select--placeholder-visible .select__display-label {
color: var(--sl-input-placeholder-color);
}
/* Prefix */ /* Prefix */
.select__prefix { .select__prefix {
flex: 0; flex: 0;

Wyświetl plik

@ -48,6 +48,8 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
@query('.select') popup: SlPopup; @query('.select') popup: SlPopup;
@query('.select__combobox') combobox: HTMLSlotElement; @query('.select__combobox') combobox: HTMLSlotElement;
@query('.select__display-input') displayInput: HTMLInputElement;
@query('.select__value-input') valueInput: HTMLInputElement;
@query('.select__listbox') listbox: HTMLSlotElement; @query('.select__listbox') listbox: HTMLSlotElement;
// @ts-expect-error -- Controller is currently unused // @ts-expect-error -- Controller is currently unused
@ -125,38 +127,45 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
this.open = false; this.open = false;
} }
firstUpdated() {
this.invalid = !this.checkValidity();
}
/** Checks for validity but does not show the browser's validation message. */ /** Checks for validity but does not show the browser's validation message. */
checkValidity() { checkValidity() {
// return this.input.checkValidity(); return this.valueInput.checkValidity();
} }
/** Checks for validity and shows the browser's validation message if the control is invalid. */ /** Checks for validity and shows the browser's validation message if the control is invalid. */
reportValidity() { reportValidity() {
// return this.input.reportValidity(); return this.valueInput.reportValidity();
} }
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */ /** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
setCustomValidity(message: string) { setCustomValidity(message: string) {
// this.input.setCustomValidity(message); this.valueInput.setCustomValidity(message);
this.invalid = !this.input.checkValidity(); this.invalid = !this.valueInput.checkValidity();
} }
/** Sets focus on the control. */ /** Sets focus on the control. */
focus(options?: FocusOptions) { focus(options?: FocusOptions) {
// this.control.focus(options); this.displayInput.focus(options);
} }
/** Removes focus from the control. */ /** Removes focus from the control. */
blur() { blur() {
// this.control.blur(); this.displayInput.blur();
} }
handleFocus() { handleFocus() {
this.hasFocus = true; this.hasFocus = true;
this.displayInput.setSelectionRange(0, 0);
this.emit('sl-focus');
} }
handleBlur() { handleBlur() {
this.hasFocus = false; this.hasFocus = false;
this.emit('sl-blur');
} }
handleDocumentFocusIn(event: KeyboardEvent) { handleDocumentFocusIn(event: KeyboardEvent) {
@ -190,12 +199,16 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
const currentOption = this.getCurrentOption(); const currentOption = this.getCurrentOption();
if (currentOption) { if (currentOption) {
this.setSelectedOption(currentOption); this.setSelectedOption(currentOption);
this.value = currentOption.value;
this.displayLabel = currentOption.textContent ?? ''; this.displayLabel = currentOption.textContent ?? '';
this.value = currentOption.value;
this.valueInput.value = currentOption.value; // synchronous update for validation
this.invalid = !this.checkValidity();
this.emit('sl-input');
this.emit('sl-change');
} }
this.hide(); this.hide();
this.combobox.focus(); this.displayInput.focus();
return; return;
} }
@ -287,13 +300,13 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
} }
handleLabelClick() { handleLabelClick() {
this.combobox.focus(); this.displayInput.focus();
} }
// We use mousedown/mouseup instead of click to allow macOS-style menu behavior // We use mousedown/mouseup instead of click to allow macOS-style menu behavior
handleComboboxMouseDown(event: MouseEvent) { handleComboboxMouseDown(event: MouseEvent) {
event.preventDefault(); event.preventDefault();
this.combobox.focus(); this.displayInput.focus();
this.open = !this.open; this.open = !this.open;
} }
@ -306,9 +319,13 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
event.stopPropagation(); event.stopPropagation();
if (this.value !== '') { if (this.value !== '') {
this.value = '';
this.displayLabel = ''; this.displayLabel = '';
this.value = '';
this.valueInput.value = ''; // synchronous update for validation
this.invalid = !this.checkValidity();
this.emit('sl-clear'); this.emit('sl-clear');
this.emit('sl-input');
this.emit('sl-change');
} }
} }
@ -320,13 +337,22 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
handleOptionMouseUp(event: MouseEvent) { handleOptionMouseUp(event: MouseEvent) {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
const option = target.closest('sl-option'); const option = target.closest('sl-option');
const oldValue = this.value;
if (!option) { if (!option) {
return; return;
} }
// Update the value and focus after updating so the value is read by screen readers // Update the value and focus after updating so the value is read by screen readers
this.value = option.value; this.value = option.value;
this.updateComplete.then(() => this.combobox.focus()); this.valueInput.value = option.value; // synchronous update for validation
this.invalid = !this.checkValidity();
this.updateComplete.then(() => this.displayInput.focus());
if (this.value !== oldValue) {
this.emit('sl-input');
this.emit('sl-change');
}
this.hide(); this.hide();
} }
@ -576,25 +602,40 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
@keydown=${this.handleComboboxKeyDown} @keydown=${this.handleComboboxKeyDown}
@mousedown=${this.handleComboboxMouseDown} @mousedown=${this.handleComboboxMouseDown}
> >
<div <div class="select__combobox">
class="select__combobox"
aria-controls="listbox"
aria-expanded=${this.open ? 'true' : 'false'}
aria-haspopup="listbox"
aria-labelledby="label"
aria-disabled=${this.disabled ? 'true' : 'false'}
aria-describedby="help-text"
role="combobox"
tabindex="0"
@focus=${this.handleFocus}
@blur=${this.handleBlur}
>
<slot name="prefix" class="select__prefix"></slot> <slot name="prefix" class="select__prefix"></slot>
<span class="select__display-label-wrapper">
<span class="select__display-label"> <input
${isPlaceholderVisible ? this.placeholder : this.displayLabel} class="select__display-input"
</span> type="text"
</span> placeholder=${this.placeholder}
.value=${this.displayLabel}
autocomplete="off"
spellcheck="false"
autocapitalize="off"
readonly
aria-controls="listbox"
aria-expanded=${this.open ? 'true' : 'false'}
aria-haspopup="listbox"
aria-labelledby="label"
aria-disabled=${this.disabled ? 'true' : 'false'}
aria-describedby="help-text"
role="combobox"
tabindex="0"
@focus=${this.handleFocus}
@blur=${this.handleBlur}
/>
<input
class="select__value-input"
type="text"
?disabled=${this.disabled}
?required=${this.required}
.value=${this.value}
tabindex="-1"
aria-hidden="true"
@focus=${() => this.focus()}
/>
</div> </div>
${hasClearIcon ${hasClearIcon