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]
```html preview
<sl-select value="option-1">
<sl-select>
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</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-5">Option 5</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>
```
```jsx react
import { SlDivider, SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
const App = () => (
<SlSelect>
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
<SlDivider />
<SlOption value="option-4">Option 4</SlOption>
<SlOption value="option-5">Option 5</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);
}
.select--top::part(popup) {
.select[data-current-placement^='top']::part(popup) {
transform-origin: bottom;
}
.select--bottom::part(popup) {
.select[data-current-placement^='bottom']::part(popup) {
transform-origin: top;
}
@ -48,15 +48,35 @@ export default css`
.select__combobox {
flex: 1;
display: flex;
position: relative;
align-items: stretch;
justify-content: start;
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;
}
.select__value-input {
position: absolute;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
opacity: 0;
z-index: -1;
}
/* Standard selects */
.select--standard .select__combobox-wrapper {
background-color: var(--sl-input-background-color);
@ -161,24 +181,6 @@ export default css`
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 */
.select__prefix {
flex: 0;

Wyświetl plik

@ -48,6 +48,8 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
@query('.select') popup: SlPopup;
@query('.select__combobox') combobox: HTMLSlotElement;
@query('.select__display-input') displayInput: HTMLInputElement;
@query('.select__value-input') valueInput: HTMLInputElement;
@query('.select__listbox') listbox: HTMLSlotElement;
// @ts-expect-error -- Controller is currently unused
@ -125,38 +127,45 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
this.open = false;
}
firstUpdated() {
this.invalid = !this.checkValidity();
}
/** Checks for validity but does not show the browser's validation message. */
checkValidity() {
// return this.input.checkValidity();
return this.valueInput.checkValidity();
}
/** Checks for validity and shows the browser's validation message if the control is invalid. */
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. */
setCustomValidity(message: string) {
// this.input.setCustomValidity(message);
this.invalid = !this.input.checkValidity();
this.valueInput.setCustomValidity(message);
this.invalid = !this.valueInput.checkValidity();
}
/** Sets focus on the control. */
focus(options?: FocusOptions) {
// this.control.focus(options);
this.displayInput.focus(options);
}
/** Removes focus from the control. */
blur() {
// this.control.blur();
this.displayInput.blur();
}
handleFocus() {
this.hasFocus = true;
this.displayInput.setSelectionRange(0, 0);
this.emit('sl-focus');
}
handleBlur() {
this.hasFocus = false;
this.emit('sl-blur');
}
handleDocumentFocusIn(event: KeyboardEvent) {
@ -190,12 +199,16 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
const currentOption = this.getCurrentOption();
if (currentOption) {
this.setSelectedOption(currentOption);
this.value = currentOption.value;
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.combobox.focus();
this.displayInput.focus();
return;
}
@ -287,13 +300,13 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
}
handleLabelClick() {
this.combobox.focus();
this.displayInput.focus();
}
// We use mousedown/mouseup instead of click to allow macOS-style menu behavior
handleComboboxMouseDown(event: MouseEvent) {
event.preventDefault();
this.combobox.focus();
this.displayInput.focus();
this.open = !this.open;
}
@ -306,9 +319,13 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
event.stopPropagation();
if (this.value !== '') {
this.value = '';
this.displayLabel = '';
this.value = '';
this.valueInput.value = ''; // synchronous update for validation
this.invalid = !this.checkValidity();
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) {
const target = event.target as HTMLElement;
const option = target.closest('sl-option');
const oldValue = this.value;
if (!option) {
return;
}
// Update the value and focus after updating so the value is read by screen readers
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();
}
@ -576,25 +602,40 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
@keydown=${this.handleComboboxKeyDown}
@mousedown=${this.handleComboboxMouseDown}
>
<div
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}
>
<div class="select__combobox">
<slot name="prefix" class="select__prefix"></slot>
<span class="select__display-label-wrapper">
<span class="select__display-label">
${isPlaceholderVisible ? this.placeholder : this.displayLabel}
</span>
</span>
<input
class="select__display-input"
type="text"
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>
${hasClearIcon