kopia lustrzana https://github.com/shoelace-style/shoelace
fix validity and events
rodzic
9f79445292
commit
2dc275defd
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue