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