mirror of
https://github.com/shoelace-style/shoelace.git
synced 2026-01-12 02:59:13 +00:00
updates
This commit is contained in:
@@ -151,7 +151,7 @@ module.exports = {
|
||||
'prefer-rest-params': 'warn',
|
||||
'prefer-spread': 'warn',
|
||||
'prefer-template': 'off',
|
||||
'no-else-return': 'warn',
|
||||
'no-else-return': 'off',
|
||||
'func-names': ['warn', 'never'],
|
||||
'one-var': ['warn', 'never'],
|
||||
'operator-assignment': 'warn',
|
||||
|
||||
@@ -3,459 +3,35 @@
|
||||
[component-header:sl-select]
|
||||
|
||||
```html preview
|
||||
<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-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-select>
|
||||
```
|
||||
|
||||
```jsx 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>
|
||||
<SlOption value="option-4">Option 4</SlOption>
|
||||
<SlOption value="option-5">Option 5</SlOption>
|
||||
<SlOption value="option-6">Option 6</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
### Labels
|
||||
|
||||
Use the `label` attribute to give the select an accessible label. For labels that contain HTML, use the `label` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-select label="Select one">
|
||||
<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-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect label="Select one">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Help Text
|
||||
|
||||
Add descriptive help text to a select with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-select label="Experience" help-text="Please tell us your skill level.">
|
||||
<sl-option value="1">Novice</sl-option>
|
||||
<sl-option value="2">Intermediate</sl-option>
|
||||
<sl-option value="3">Advanced</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect label="Experience" help-text="Please tell us your skill level.">
|
||||
<SlOption value="1">Novice</SlOption>
|
||||
<SlOption value="2">Intermediate</SlOption>
|
||||
<SlOption value="3">Advanced</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Placeholders
|
||||
|
||||
Use the `placeholder` attribute to add a placeholder.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Select one">
|
||||
<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-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect placeholder="Select one">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Clearable
|
||||
|
||||
Use the `clearable` attribute to make the control clearable. The clear button only appears when an option is selected.
|
||||
|
||||
```html preview
|
||||
<sl-select clearable value="option-1">
|
||||
<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-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect placeholder="Clearable" clearable>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Filled Selects
|
||||
|
||||
Add the `filled` attribute to draw a filled select.
|
||||
|
||||
```html preview
|
||||
<sl-select filled>
|
||||
<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-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect filled>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Pill
|
||||
|
||||
Use the `pill` attribute to give selects rounded edges.
|
||||
|
||||
```html preview
|
||||
<sl-select pill>
|
||||
<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-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect pill>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable a select.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Disabled" disabled>
|
||||
<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-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect placeholder="Disabled" disabled>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Setting the Selection
|
||||
|
||||
Use the `value` attribute to set the current selection. When users interact with the control, its `value` will update to reflect the newly selected menu item's value.
|
||||
|
||||
```html preview
|
||||
<sl-select value="option-2">
|
||||
<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-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDivider, SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect value="option-2">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Setting the Selection Imperatively
|
||||
|
||||
To programmatically set the selection, update the `value` property as shown below.
|
||||
|
||||
```html preview
|
||||
<div class="selecting-example">
|
||||
<sl-select>
|
||||
<form id="f" target="_blank" method="GET">
|
||||
<sl-select name="single" label="Select One" clearable>
|
||||
<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-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-select>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-button data-option="option-1">Set 1</sl-button>
|
||||
<sl-button data-option="option-2">Set 2</sl-button>
|
||||
<sl-button data-option="option-3">Set 3</sl-button>
|
||||
</div>
|
||||
<sl-select name="many" label="Select Many" multiple clearable value="option-2">
|
||||
<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-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-select>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-button variant="primary" type="submit">Submit</sl-button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.selecting-example');
|
||||
const select = container.querySelector('sl-select');
|
||||
const form = document.querySelector('#f');
|
||||
|
||||
[...container.querySelectorAll('sl-button')].map(button => {
|
||||
button.addEventListener('click', () => {
|
||||
select.value = button.dataset.option;
|
||||
});
|
||||
});
|
||||
form.addEventListener('submit', event => {});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlButton, SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [value, setValue] = useState('option-1');
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlSelect value={value} onSlChange={event => setValue(event.target.value)}>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
|
||||
<br />
|
||||
|
||||
<SlButton onClick={() => setValue('option-1')}>Set 1</SlButton>
|
||||
<SlButton onClick={() => setValue('option-2')}>Set 2</SlButton>
|
||||
<SlButton onClick={() => setValue('option-3')}>Set 3</SlButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Multiple
|
||||
|
||||
TODO
|
||||
|
||||
### Grouping Options
|
||||
|
||||
Use `<sl-divider>` to group listbox items visually. You can also use `<small>` to provide labels, but they won't be announced by most assistive devices.
|
||||
|
||||
```html preview
|
||||
<sl-select>
|
||||
<small>Section 1</small>
|
||||
<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>
|
||||
<small>Section 2</small>
|
||||
<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-select>
|
||||
```
|
||||
|
||||
```jsx 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>
|
||||
<SlOption value="option-4">Option 4</SlOption>
|
||||
<SlOption value="option-5">Option 5</SlOption>
|
||||
<SlOption value="option-6">Option 6</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Sizes
|
||||
|
||||
Use the `size` attribute to change a select's size. Note that size does not apply to listbox options.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Small" size="small">
|
||||
<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-select>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-select placeholder="Medium" size="medium">
|
||||
<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-select>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-select placeholder="Large" size="large">
|
||||
<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-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlSelect placeholder="Small" size="small">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
|
||||
<br />
|
||||
|
||||
<SlSelect placeholder="Medium" size="medium">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
|
||||
<br />
|
||||
|
||||
<SlSelect placeholder="Large" size="large">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Placement
|
||||
|
||||
The preferred placement of the select's menu can be set with the `placement` attribute. Note that the actual position may vary to ensure the panel remains in the viewport. Valid placements are `top` and `bottom`.
|
||||
|
||||
```html preview
|
||||
<sl-select placement="top">
|
||||
<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-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import {
|
||||
SlOption,
|
||||
SlSelect
|
||||
} from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect placement="top">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlDropdown>
|
||||
);
|
||||
```
|
||||
|
||||
### Prefix Icons
|
||||
|
||||
Use the `prefix` slot to add an icon.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Small" size="small" clearable>
|
||||
<sl-icon name="house" slot="prefix"></sl-icon>
|
||||
<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-select>
|
||||
<br />
|
||||
<sl-select placeholder="Medium" size="medium" clearable>
|
||||
<sl-icon name="house" slot="prefix"></sl-icon>
|
||||
<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-select>
|
||||
<br />
|
||||
<sl-select placeholder="Large" size="large" clearable>
|
||||
<sl-icon name="house" slot="prefix"></sl-icon>
|
||||
<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-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlIcon, SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlSelect placeholder="Small" size="small">
|
||||
<SlIcon name="house" slot="prefix"></SlIcon>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
<br />
|
||||
<SlSelect placeholder="Medium" size="medium">
|
||||
<SlIcon name="house" slot="prefix"></SlIcon>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
<br />
|
||||
<SlSelect placeholder="Large" size="large">
|
||||
<SlIcon name="house" slot="prefix"></SlIcon>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-select]
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -13,7 +13,7 @@
|
||||
"@floating-ui/dom": "^1.0.7",
|
||||
"@lit-labs/react": "^1.1.0",
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
"@shoelace-style/localize": "^3.0.3",
|
||||
"@shoelace-style/localize": "^3.0.4",
|
||||
"lit": "^2.4.1",
|
||||
"qr-creator": "^1.0.0"
|
||||
},
|
||||
@@ -1152,9 +1152,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shoelace-style/localize": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@shoelace-style/localize/-/localize-3.0.3.tgz",
|
||||
"integrity": "sha512-BVYDsMTpSCjvC8akhTkcnl4WIgAv7AnRq8fSuNhdGDLjkpmN1ARdAXic5luAozoMVjft85ocOmEdT8z7MbXdmQ=="
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@shoelace-style/localize/-/localize-3.0.4.tgz",
|
||||
"integrity": "sha512-HFY90KD+b1Td2otSBryCOpQjBEArIwlV6Tv4J4rC/E/D5wof2eLF6JUVrbiRNn8GRmwATe4YDAEK7NUD08xO1w=="
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "0.7.0",
|
||||
@@ -16027,9 +16027,9 @@
|
||||
"integrity": "sha512-Be+cahtZyI2dPKRm8EZSx3YJQ+jLvEcn3xzRP7tM4tqBnvd/eW/64Xh0iOf0t2w5P8iJKfdBbpVNE9naCaOf2g=="
|
||||
},
|
||||
"@shoelace-style/localize": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@shoelace-style/localize/-/localize-3.0.3.tgz",
|
||||
"integrity": "sha512-BVYDsMTpSCjvC8akhTkcnl4WIgAv7AnRq8fSuNhdGDLjkpmN1ARdAXic5luAozoMVjft85ocOmEdT8z7MbXdmQ=="
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@shoelace-style/localize/-/localize-3.0.4.tgz",
|
||||
"integrity": "sha512-HFY90KD+b1Td2otSBryCOpQjBEArIwlV6Tv4J4rC/E/D5wof2eLF6JUVrbiRNn8GRmwATe4YDAEK7NUD08xO1w=="
|
||||
},
|
||||
"@sindresorhus/is": {
|
||||
"version": "0.7.0",
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"@floating-ui/dom": "^1.0.7",
|
||||
"@lit-labs/react": "^1.1.0",
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
"@shoelace-style/localize": "^3.0.3",
|
||||
"@shoelace-style/localize": "^3.0.4",
|
||||
"lit": "^2.4.1",
|
||||
"qr-creator": "^1.0.0"
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@ export default css`
|
||||
color: inherit;
|
||||
padding: var(--sl-spacing-x-small);
|
||||
cursor: pointer;
|
||||
transition: var(--sl-transition-medium) color;
|
||||
transition: var(--sl-transition-x-fast) color;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { getTextContent } from '../../internal/slot';
|
||||
@@ -39,17 +39,14 @@ export default class SlOption extends ShoelaceElement {
|
||||
// @ts-expect-error -- Controller is currently unused
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@state() current = false; // the user has keyed into the option, but hasn't selected it yet (shows a highlight)
|
||||
@state() selected = false; // the option is selected and has aria-selected="true"
|
||||
|
||||
@query('.option__label') defaultSlot: HTMLSlotElement;
|
||||
|
||||
/** The option's value. When selected, the containing form control will receive this value. */
|
||||
@property() value = '';
|
||||
|
||||
/** Draws the option in a current state, meaning the user has keyed into it but hasn't selected it yet. */
|
||||
@property({ type: Boolean, reflect: true }) current = false;
|
||||
|
||||
/** Draws the option in a selected state. */
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
|
||||
/** Draws the option in a disabled state, preventing selection. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
@@ -59,6 +56,11 @@ export default class SlOption extends ShoelaceElement {
|
||||
this.setAttribute('aria-selected', 'false');
|
||||
}
|
||||
|
||||
/** Returns a plain text label based on the option's content. */
|
||||
getTextLabel() {
|
||||
return this.textContent ?? '';
|
||||
}
|
||||
|
||||
@watch('disabled')
|
||||
handleDisabledChange() {
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
|
||||
@@ -38,7 +38,7 @@ export default css`
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
align-items: stretch;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
font-family: var(--sl-input-font-family);
|
||||
font-weight: var(--sl-input-font-weight);
|
||||
@@ -51,6 +51,7 @@ export default css`
|
||||
}
|
||||
|
||||
.select__display-input {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
font: inherit;
|
||||
border: none;
|
||||
@@ -66,6 +67,17 @@ export default css`
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Visually hide the display input when multiple is enabled */
|
||||
.select--multiple .select__display-input {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.select__value-input {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
@@ -76,6 +88,23 @@ export default css`
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.select__tags {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-inline-start: var(--sl-spacing-2x-small);
|
||||
}
|
||||
|
||||
.select__tags::slotted(sl-tag) {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.select--disabled .select__tags,
|
||||
.select--disabled .select__tags::slotted(sl-tag) {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
/* Standard selects */
|
||||
.select--standard .select__combobox {
|
||||
background-color: var(--sl-input-background-color);
|
||||
@@ -137,10 +166,19 @@ export default css`
|
||||
margin-inline-end: var(--sl-input-spacing-small);
|
||||
}
|
||||
|
||||
.select--small.select--multiple .select__combobox {
|
||||
padding-inline-start: 0;
|
||||
padding-block: 2px;
|
||||
}
|
||||
|
||||
.select--small .select__tags {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.select--medium .select__combobox {
|
||||
border-radius: var(--sl-input-border-radius-medium);
|
||||
font-size: var(--sl-input-font-size-medium);
|
||||
height: var(--sl-input-height-medium);
|
||||
min-height: var(--sl-input-height-medium);
|
||||
padding: 0 var(--sl-input-spacing-medium);
|
||||
}
|
||||
|
||||
@@ -152,6 +190,15 @@ export default css`
|
||||
margin-inline-end: var(--sl-input-spacing-medium);
|
||||
}
|
||||
|
||||
.select--medium.select--multiple .select__combobox {
|
||||
padding-inline-start: 0;
|
||||
padding-block: 3px;
|
||||
}
|
||||
|
||||
.select--medium .select__tags {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.select--large .select__combobox {
|
||||
border-radius: var(--sl-input-border-radius-large);
|
||||
font-size: var(--sl-input-font-size-large);
|
||||
@@ -167,6 +214,15 @@ export default css`
|
||||
margin-inline-end: var(--sl-input-spacing-large);
|
||||
}
|
||||
|
||||
.select--large.select--multiple .select__combobox {
|
||||
padding-inline-start: 0;
|
||||
padding-block: 4px;
|
||||
}
|
||||
|
||||
.select--large .select__tags {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* Pills */
|
||||
.select--pill.select--small .select__combobox {
|
||||
border-radius: var(--sl-input-height-small);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getAnimation, setDefaultAnimation } from '../../utilities/animation-reg
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import '../icon/icon';
|
||||
import '../popup/popup';
|
||||
import '../tag/tag';
|
||||
import styles from './select.styles';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
import type SlOption from '../option/option';
|
||||
@@ -27,6 +28,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
*
|
||||
* @dependency sl-icon
|
||||
* @dependency sl-popup
|
||||
* @dependency sl-tag
|
||||
*
|
||||
* @slot - The listbox options. Must be `<sl-option>` elements. You can use `<sl-divider>` to group items visually.
|
||||
* @slot label - The input's label. Alternatively, you can use the `label` attribute.
|
||||
@@ -73,18 +75,23 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
private typeToSelectString = '';
|
||||
private typeToSelectTimeout: number;
|
||||
|
||||
@state() displayLabel = '';
|
||||
@state() private hasFocus = false;
|
||||
@state() displayLabel = '';
|
||||
@state() currentOption: SlOption;
|
||||
@state() selectedOptions: SlOption[] = [];
|
||||
@state() invalid = false;
|
||||
|
||||
/** The name of the select, submitted as a name/value pair with form data. */
|
||||
@property() name = '';
|
||||
|
||||
/** The current value of the select, submitted as a name/value pair with form data. */
|
||||
@property() value = '';
|
||||
/**
|
||||
* The current value of the select, submitted as a name/value pair with form data. If `multiple` is enabled, this
|
||||
* property will be an array. Otherwise, it will be a string.
|
||||
*/
|
||||
@property() value: string | string[] = '';
|
||||
|
||||
/** The default value of the form control. Primarily used for resetting the form control. */
|
||||
@defaultValue() defaultValue = '';
|
||||
@defaultValue() defaultValue: string | string[] = '';
|
||||
|
||||
/** The select's size. */
|
||||
@property() size: 'small' | 'medium' | 'large' = 'medium';
|
||||
@@ -92,6 +99,15 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
/** Placeholder text to show as a hint when the select is empty. */
|
||||
@property() placeholder = '';
|
||||
|
||||
/** Allows more than one option to be selected. */
|
||||
@property({ type: Boolean, reflect: true }) multiple = false;
|
||||
|
||||
/**
|
||||
* The maximum number of selected options to show when `multiple` is true. After the maximum, "+n" will be shown to
|
||||
* indicate the number of additional items that are selected. Set to 0 to remove the limit.
|
||||
*/
|
||||
@property({ attribute: 'max-options-visible', type: Number }) maxOptionsVisible = 3;
|
||||
|
||||
/** Disables the select control. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
@@ -171,18 +187,30 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
this.displayInput.blur();
|
||||
}
|
||||
|
||||
handleFocus() {
|
||||
private addOpenListeners() {
|
||||
document.addEventListener('focusin', this.handleDocumentFocusIn);
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
}
|
||||
|
||||
private removeOpenListeners() {
|
||||
document.removeEventListener('focusin', this.handleDocumentFocusIn);
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
}
|
||||
|
||||
private handleFocus() {
|
||||
this.hasFocus = true;
|
||||
this.displayInput.setSelectionRange(0, 0);
|
||||
this.emit('sl-focus');
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
private handleBlur() {
|
||||
this.hasFocus = false;
|
||||
this.emit('sl-blur');
|
||||
}
|
||||
|
||||
handleDocumentFocusIn(event: KeyboardEvent) {
|
||||
private handleDocumentFocusIn(event: KeyboardEvent) {
|
||||
// Close when focusing out of the select
|
||||
const path = event.composedPath();
|
||||
if (this && !path.includes(this)) {
|
||||
@@ -190,12 +218,13 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
}
|
||||
}
|
||||
|
||||
handleDocumentKeyDown(event: KeyboardEvent) {
|
||||
private handleDocumentKeyDown(event: KeyboardEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
const isClearButton = target.closest('.select__clear') !== null;
|
||||
const isIconButton = target.closest('sl-icon-button') !== null;
|
||||
|
||||
// Ignore presses when the target is the clear button
|
||||
if (isClearButton) {
|
||||
// Ignore presses when the target is an icon button (e.g. the remove button in <sl-tag>)
|
||||
if (isClearButton || isIconButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -204,12 +233,14 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.hide();
|
||||
this.displayInput.focus();
|
||||
}
|
||||
|
||||
// Handle enter and space. When pressing space, we allow for type to select behaviors so if there's anything in the
|
||||
// buffer we _don't_ close it.
|
||||
if (event.key === 'Enter' || (event.key === ' ' && this.typeToSelectString === '')) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
// If it's not open, open it
|
||||
if (!this.open) {
|
||||
@@ -218,14 +249,20 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
}
|
||||
|
||||
// If it is open, update the value based on the current selection and close it
|
||||
const currentOption = this.getCurrentOption();
|
||||
if (currentOption && !currentOption.disabled) {
|
||||
this.setSelectedOption(currentOption);
|
||||
this.setValueFromOption(currentOption);
|
||||
if (this.currentOption && !this.currentOption.disabled) {
|
||||
if (this.multiple) {
|
||||
this.toggleOptionSelection(this.currentOption);
|
||||
} else {
|
||||
this.setSelectedOptions(this.currentOption);
|
||||
}
|
||||
|
||||
this.emit('sl-input');
|
||||
this.emit('sl-change');
|
||||
this.hide();
|
||||
this.displayInput.focus();
|
||||
|
||||
if (!this.multiple) {
|
||||
this.hide();
|
||||
this.displayInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -234,8 +271,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
// Navigate options
|
||||
if (['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
|
||||
const allOptions = this.getAllOptions();
|
||||
const currentOption = this.getCurrentOption();
|
||||
const currentIndex = allOptions.indexOf(currentOption);
|
||||
const currentIndex = allOptions.indexOf(this.currentOption);
|
||||
let newIndex = Math.max(0, currentIndex);
|
||||
|
||||
// Prevent scrolling
|
||||
@@ -247,7 +283,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
|
||||
// If an option is already selected, stop here because we want that one to remain highlighted when the listbox
|
||||
// opens for the first time
|
||||
if (currentOption) {
|
||||
if (this.currentOption) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -298,7 +334,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
}
|
||||
|
||||
for (const option of allOptions) {
|
||||
const label = (option.textContent ?? '').toLowerCase();
|
||||
const label = option.getTextLabel().toLowerCase();
|
||||
|
||||
if (label.startsWith(this.typeToSelectString)) {
|
||||
this.setCurrentOption(option);
|
||||
@@ -308,7 +344,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
}
|
||||
}
|
||||
|
||||
handleDocumentMouseDown(event: MouseEvent) {
|
||||
private handleDocumentMouseDown(event: MouseEvent) {
|
||||
// Close when clicking outside of the select
|
||||
const path = event.composedPath();
|
||||
if (this && !path.includes(this)) {
|
||||
@@ -316,27 +352,35 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
}
|
||||
}
|
||||
|
||||
handleLabelClick() {
|
||||
private handleLabelClick() {
|
||||
this.displayInput.focus();
|
||||
}
|
||||
|
||||
// We use mousedown/mouseup instead of click to allow macOS-style menu behavior
|
||||
handleComboboxMouseDown(event: MouseEvent) {
|
||||
private handleComboboxMouseDown(event: MouseEvent) {
|
||||
const path = event.composedPath();
|
||||
const isIconButton = path.some(el => el instanceof Element && el.tagName.toLowerCase() === 'sl-icon-button');
|
||||
|
||||
// Ignore clicks on tags (remove buttons)
|
||||
if (isIconButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
this.displayInput.focus();
|
||||
this.open = !this.open;
|
||||
}
|
||||
|
||||
handleComboboxKeyDown(event: KeyboardEvent) {
|
||||
private handleComboboxKeyDown(event: KeyboardEvent) {
|
||||
event.stopPropagation();
|
||||
this.handleDocumentKeyDown(event);
|
||||
}
|
||||
|
||||
handleClearClick(event: MouseEvent) {
|
||||
private handleClearClick(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.value !== '') {
|
||||
this.setValueFromOption(null);
|
||||
this.setSelectedOptions([]);
|
||||
this.displayInput.focus();
|
||||
this.emit('sl-clear');
|
||||
this.emit('sl-input');
|
||||
@@ -344,21 +388,26 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
}
|
||||
}
|
||||
|
||||
handleClearMouseDown(event: MouseEvent) {
|
||||
private handleClearMouseDown(event: MouseEvent) {
|
||||
// Don't lose focus or propagate events when clicking the clear button
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// We use mousedown/mouseup instead of click to allow macOS-style menu behavior
|
||||
handleOptionMouseUp(event: MouseEvent) {
|
||||
private handleOptionMouseUp(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
const option = target.closest('sl-option');
|
||||
const oldValue = this.value;
|
||||
|
||||
if (option && !option.disabled) {
|
||||
// Update the value and focus after updating so the value is read by screen readers
|
||||
this.setValueFromOption(option);
|
||||
if (this.multiple) {
|
||||
this.toggleOptionSelection(option);
|
||||
} else {
|
||||
this.setSelectedOptions(option);
|
||||
}
|
||||
|
||||
// Set focus after updating so the value is announced by screen readers
|
||||
this.updateComplete.then(() => this.displayInput.focus());
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
@@ -366,11 +415,14 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
this.emit('sl-change');
|
||||
}
|
||||
|
||||
this.hide();
|
||||
if (!this.multiple) {
|
||||
this.hide();
|
||||
this.displayInput.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleDefaultSlotChange() {
|
||||
private handleDefaultSlotChange() {
|
||||
const allOptions = this.getAllOptions();
|
||||
const values: string[] = [];
|
||||
|
||||
@@ -382,39 +434,23 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
values.push(option.value);
|
||||
});
|
||||
|
||||
// Update the selected option
|
||||
const option = this.getOptionByValue(this.value);
|
||||
if (option) {
|
||||
this.setSelectedOption(option);
|
||||
this.setValueFromOption(option);
|
||||
} else {
|
||||
// Clear selection
|
||||
this.setSelectedOption(null);
|
||||
}
|
||||
// Update the selection since it probably changed
|
||||
this.selectionChanged();
|
||||
}
|
||||
|
||||
// Gets an array of all <sl-option> elements
|
||||
getAllOptions() {
|
||||
private getAllOptions() {
|
||||
return [...this.querySelectorAll<SlOption>('sl-option')];
|
||||
}
|
||||
|
||||
// Gets the first <sl-option> element
|
||||
getFirstOption() {
|
||||
private getFirstOption() {
|
||||
return this.querySelector<SlOption>('sl-option');
|
||||
}
|
||||
|
||||
// Gets an option based on its value
|
||||
getOptionByValue(value: string) {
|
||||
return this.getAllOptions().filter((el: SlOption) => el.value === value)[0];
|
||||
}
|
||||
|
||||
// Gets the current option
|
||||
getCurrentOption() {
|
||||
return this.getAllOptions().filter(el => el.current)[0];
|
||||
}
|
||||
|
||||
// Sets the current option
|
||||
setCurrentOption(option: SlOption | null) {
|
||||
// Sets the current option, which is the option the user is currently interacting with (e.g. via keyboard). Only one
|
||||
// option may be "current" at a time.
|
||||
private setCurrentOption(option: SlOption | null) {
|
||||
const allOptions = this.getAllOptions();
|
||||
|
||||
// Clear selection
|
||||
@@ -425,6 +461,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
|
||||
// Select the target option
|
||||
if (option) {
|
||||
this.currentOption = option;
|
||||
option.current = true;
|
||||
option.tabIndex = 0;
|
||||
option.focus();
|
||||
@@ -432,49 +469,64 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the selected option
|
||||
getSelectedOption() {
|
||||
return this.getAllOptions().filter(el => el.selected)[0];
|
||||
}
|
||||
|
||||
// Sets the selected option
|
||||
setSelectedOption(option: SlOption | null) {
|
||||
// Sets the selected option(s)
|
||||
private setSelectedOptions(option: SlOption | SlOption[]) {
|
||||
const allOptions = this.getAllOptions();
|
||||
const newSelectedOptions = Array.isArray(option) ? option : [option];
|
||||
|
||||
// Clear selection
|
||||
// Clear existing selection
|
||||
allOptions.forEach(el => (el.selected = false));
|
||||
|
||||
// Select the target option
|
||||
if (option) {
|
||||
option.selected = true;
|
||||
scrollIntoView(option, this.listbox);
|
||||
// Set the new selection
|
||||
if (newSelectedOptions.length) {
|
||||
newSelectedOptions.forEach(el => (el.selected = true));
|
||||
|
||||
// Scroll the first selected option into view
|
||||
scrollIntoView(newSelectedOptions[0]!, this.listbox);
|
||||
}
|
||||
|
||||
// Update selection, value, and display label
|
||||
this.selectionChanged();
|
||||
}
|
||||
|
||||
// Sets the value, display label, and syncs validity
|
||||
setValueFromOption(option: SlOption | null) {
|
||||
const displayLabel = option ? (option.textContent ?? '').trim() : '';
|
||||
const value = option ? option.value : '';
|
||||
// Toggles an option's selected state
|
||||
private toggleOptionSelection(option: SlOption, force?: boolean) {
|
||||
if (force === true || force === false) {
|
||||
option.selected = force;
|
||||
} else {
|
||||
option.selected = !option.selected;
|
||||
}
|
||||
|
||||
this.displayLabel = displayLabel;
|
||||
this.value = value;
|
||||
this.valueInput.value = value; // synchronous update for validation
|
||||
this.selectionChanged();
|
||||
}
|
||||
|
||||
// This method must be called whenever the selection changes. It will sync the selected options cache, update the
|
||||
// current value, and update the display value.
|
||||
private selectionChanged() {
|
||||
console.log('selectionChanged');
|
||||
// Update selection options cache
|
||||
this.selectedOptions = this.getAllOptions().filter(el => el.selected);
|
||||
|
||||
// Update the value and display label
|
||||
if (this.multiple) {
|
||||
this.value = this.selectedOptions.map(el => el.value);
|
||||
this.displayLabel = this.localize.term('numOptionsSelected', this.selectedOptions.length);
|
||||
} else {
|
||||
this.value = this.selectedOptions[0]?.value ?? '';
|
||||
this.displayLabel = this.selectedOptions[0]?.getTextLabel() ?? '';
|
||||
}
|
||||
|
||||
// Update validity
|
||||
this.invalid = !this.checkValidity();
|
||||
}
|
||||
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
handleValueChange() {
|
||||
const option = this.getOptionByValue(this.value);
|
||||
const allOptions = this.getAllOptions();
|
||||
const value = Array.isArray(this.value) ? this.value : [this.value];
|
||||
|
||||
// Update the selection
|
||||
this.setSelectedOption(option);
|
||||
|
||||
if (option) {
|
||||
this.setValueFromOption(option);
|
||||
} else {
|
||||
// No option, reset the control
|
||||
this.setValueFromOption(null);
|
||||
}
|
||||
// Select only the options that match the new value
|
||||
this.setSelectedOptions(allOptions.filter(el => value.includes(el.value)));
|
||||
}
|
||||
|
||||
/** Shows the listbox. */
|
||||
@@ -499,18 +551,6 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
addOpenListeners() {
|
||||
document.addEventListener('focusin', this.handleDocumentFocusIn);
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
}
|
||||
|
||||
removeOpenListeners() {
|
||||
document.removeEventListener('focusin', this.handleDocumentFocusIn);
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
async handleOpenChange() {
|
||||
if (this.disabled) {
|
||||
@@ -519,8 +559,8 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
}
|
||||
|
||||
if (this.open) {
|
||||
const selectedOption = this.getOptionByValue(this.value);
|
||||
const currentOption = selectedOption || this.getFirstOption();
|
||||
// Reset the current option
|
||||
this.setCurrentOption(this.selectedOptions[0] || this.getFirstOption());
|
||||
|
||||
// Show
|
||||
this.emit('sl-show');
|
||||
@@ -532,16 +572,15 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
|
||||
// Select the appropriate option based on value after the listbox opens
|
||||
requestAnimationFrame(() => {
|
||||
this.setSelectedOption(selectedOption);
|
||||
this.setCurrentOption(currentOption);
|
||||
this.setCurrentOption(this.currentOption);
|
||||
});
|
||||
|
||||
const { keyframes, options } = getAnimation(this, 'select.show', { dir: this.localize.dir() });
|
||||
await animateTo(this.popup.popup, keyframes, options);
|
||||
|
||||
// Make sure the current option is scrolled into view (required for Safari)
|
||||
if (currentOption) {
|
||||
scrollIntoView(currentOption, this.listbox, 'vertical', 'auto');
|
||||
if (this.currentOption) {
|
||||
scrollIntoView(this.currentOption, this.listbox, 'vertical', 'auto');
|
||||
}
|
||||
|
||||
this.emit('sl-after-show');
|
||||
@@ -589,6 +628,8 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
<slot name="label">${this.label}</slot>
|
||||
</label>
|
||||
|
||||
/ Value: ${Array.isArray(this.value) ? this.value.join(' + ') : this.value}
|
||||
|
||||
<div part="form-control-input" class="form-control-input">
|
||||
<sl-popup
|
||||
class=${classMap({
|
||||
@@ -598,6 +639,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
'select--pill': this.pill,
|
||||
'select--open': this.open,
|
||||
'select--disabled': this.disabled,
|
||||
'select--multiple': this.multiple,
|
||||
'select--focused': this.hasFocus,
|
||||
'select--top': this.placement === 'top',
|
||||
'select--bottom': this.placement === 'bottom',
|
||||
@@ -645,12 +687,41 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
@blur=${this.handleBlur}
|
||||
/>
|
||||
|
||||
${this.multiple
|
||||
? html`
|
||||
<div part="tags" class="select__tags">
|
||||
${this.selectedOptions.map((option, index) => {
|
||||
if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) {
|
||||
return html`
|
||||
<sl-tag
|
||||
size=${this.size}
|
||||
removable
|
||||
@sl-remove=${(event: CustomEvent) => {
|
||||
event.stopPropagation();
|
||||
if (!this.disabled) {
|
||||
this.toggleOptionSelection(option, false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
${option.getTextLabel()}
|
||||
</sl-tag>
|
||||
`;
|
||||
} else if (index === this.maxOptionsVisible) {
|
||||
return html` <sl-tag size=${this.size}> +${this.selectedOptions.length - index} </sl-tag> `;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
|
||||
<input
|
||||
class="select__value-input"
|
||||
type="text"
|
||||
?disabled=${this.disabled}
|
||||
?required=${this.required}
|
||||
.value=${this.value}
|
||||
.value=${Array.isArray(this.value) ? this.value.join(', ') : this.value}
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
@focus=${() => this.focus()}
|
||||
@@ -683,7 +754,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
id="listbox"
|
||||
role="listbox"
|
||||
aria-expanded=${this.open ? 'true' : 'false'}
|
||||
aria-multiselectable="false"
|
||||
aria-multiselectable=${this.multiple ? 'true' : 'false'}
|
||||
aria-labelledby="label"
|
||||
part="listbox"
|
||||
class="select__listbox"
|
||||
|
||||
@@ -15,7 +15,6 @@ export default css`
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tag__remove::part(base) {
|
||||
@@ -33,30 +32,50 @@ export default css`
|
||||
color: var(--sl-color-primary-800);
|
||||
}
|
||||
|
||||
.tag--primary:active > sl-icon-button {
|
||||
color: var(--sl-color-primary-600);
|
||||
}
|
||||
|
||||
.tag--success {
|
||||
background-color: var(--sl-color-success-50);
|
||||
border-color: var(--sl-color-success-200);
|
||||
color: var(--sl-color-success-800);
|
||||
}
|
||||
|
||||
.tag--success:active > sl-icon-button {
|
||||
color: var(--sl-color-success-600);
|
||||
}
|
||||
|
||||
.tag--neutral {
|
||||
background-color: var(--sl-color-neutral-50);
|
||||
border-color: var(--sl-color-neutral-200);
|
||||
color: var(--sl-color-neutral-800);
|
||||
}
|
||||
|
||||
.tag--neutral:active > sl-icon-button {
|
||||
color: var(--sl-color-neutral-600);
|
||||
}
|
||||
|
||||
.tag--warning {
|
||||
background-color: var(--sl-color-warning-50);
|
||||
border-color: var(--sl-color-warning-200);
|
||||
color: var(--sl-color-warning-800);
|
||||
}
|
||||
|
||||
.tag--warning:active > sl-icon-button {
|
||||
color: var(--sl-color-warning-600);
|
||||
}
|
||||
|
||||
.tag--danger {
|
||||
background-color: var(--sl-color-danger-50);
|
||||
border-color: var(--sl-color-danger-200);
|
||||
color: var(--sl-color-danger-800);
|
||||
}
|
||||
|
||||
.tag--danger:active > sl-icon-button {
|
||||
color: var(--sl-color-danger-600);
|
||||
}
|
||||
|
||||
/*
|
||||
* Size modifiers
|
||||
*/
|
||||
|
||||
@@ -82,6 +82,7 @@ export default class SlTag extends ShoelaceElement {
|
||||
label=${this.localize.term('remove')}
|
||||
class="tag__remove"
|
||||
@click=${this.handleRemoveClick}
|
||||
tabindex="-1"
|
||||
></sl-icon-button>
|
||||
`
|
||||
: ''}
|
||||
|
||||
@@ -3,12 +3,17 @@ import type { Translation } from '../utilities/localize';
|
||||
|
||||
const translation: Translation = {
|
||||
$code: 'da',
|
||||
$name: 'Danish',
|
||||
$name: 'Dansk',
|
||||
$dir: 'ltr',
|
||||
|
||||
clearEntry: 'Ryd indtastning',
|
||||
close: 'Luk',
|
||||
copy: 'Kopier',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'Geen opties geselecteerd';
|
||||
if (num === 1) return '1 optie geselecteerd';
|
||||
return `${num} opties geselecteerd`;
|
||||
},
|
||||
currentValue: 'Nuværende regerer',
|
||||
hidePassword: 'Skjul adgangskode',
|
||||
loading: 'Indlæser',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: 'Eingabe löschen',
|
||||
close: 'Schließen',
|
||||
copy: 'Kopieren',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'Keine Optionen ausgewählt';
|
||||
if (num === 1) return '1 Option ausgewählt';
|
||||
return `${num} optionen ausgewählt`;
|
||||
},
|
||||
currentValue: 'Aktueller Wert',
|
||||
hidePassword: 'Passwort verbergen',
|
||||
loading: 'Wird geladen',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: 'Eingabe löschen',
|
||||
close: 'Schliessen',
|
||||
copy: 'Kopieren',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'Keine Optionen ausgewählt';
|
||||
if (num === 1) return '1 Option ausgewählt';
|
||||
return `${num} optionen ausgewählt`;
|
||||
},
|
||||
currentValue: 'Aktueller Wert',
|
||||
hidePassword: 'Passwort verbergen',
|
||||
loading: 'Wird geladen',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: 'Eingabe löschen',
|
||||
close: 'Schließen',
|
||||
copy: 'Kopieren',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'Keine Optionen ausgewählt';
|
||||
if (num === 1) return '1 Option ausgewählt';
|
||||
return `${num} optionen ausgewählt`;
|
||||
},
|
||||
currentValue: 'Aktueller Wert',
|
||||
hidePassword: 'Passwort verbergen',
|
||||
loading: 'Wird geladen',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: 'Clear entry',
|
||||
close: 'Close',
|
||||
copy: 'Copy',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'No options selected';
|
||||
if (num === 1) return '1 option selected';
|
||||
return `${num} options selected`;
|
||||
},
|
||||
currentValue: 'Current value',
|
||||
hidePassword: 'Hide password',
|
||||
loading: 'Loading',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: 'Clear entry',
|
||||
close: 'Close',
|
||||
copy: 'Copy',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'No options selected';
|
||||
if (num === 1) return '1 option selected';
|
||||
return `${num} options selected`;
|
||||
},
|
||||
currentValue: 'Current value',
|
||||
hidePassword: 'Hide password',
|
||||
loading: 'Loading',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: 'Borrar entrada',
|
||||
close: 'Cerrar',
|
||||
copy: 'Copiar',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'No hay opciones seleccionadas';
|
||||
if (num === 1) return '1 opción seleccionada';
|
||||
return `${num} opción seleccionada`;
|
||||
},
|
||||
currentValue: 'Valor actual',
|
||||
hidePassword: 'Ocultar contraseña',
|
||||
loading: 'Cargando',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: 'پاک کردن ورودی',
|
||||
close: 'بستن',
|
||||
copy: 'رونوشت',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'هیچ گزینه ای انتخاب نشده است';
|
||||
if (num === 1) return '1 گزینه انتخاب شده است';
|
||||
return `${num} گزینه انتخاب شده است`;
|
||||
},
|
||||
currentValue: 'مقدار فعلی',
|
||||
hidePassword: 'پنهان کردن رمز',
|
||||
loading: 'بارگذاری',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: `Effacer l'entrée`,
|
||||
close: 'Fermer',
|
||||
copy: 'Copier',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'Aucune option sélectionnée';
|
||||
if (num === 1) return '1 option sélectionnée';
|
||||
return `${num} options sélectionnées`;
|
||||
},
|
||||
currentValue: 'Valeur actuelle',
|
||||
hidePassword: 'Masquer le mot de passe',
|
||||
loading: 'Chargement',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: 'נקה קלט',
|
||||
close: 'סגור',
|
||||
copy: 'העתק',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'לא נבחרו אפשרויות';
|
||||
if (num === 1) return 'נבחרה אפשרות אחת';
|
||||
return `נבחרו ${num} אפשרויות`;
|
||||
},
|
||||
currentValue: 'ערך נוכחי',
|
||||
hidePassword: 'הסתר סיסמא',
|
||||
loading: 'טוען',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: 'Bejegyzés törlése',
|
||||
close: 'Bezárás',
|
||||
copy: 'Másolás',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'Nincsenek kiválasztva opciók';
|
||||
if (num === 1) return '1 lehetőség kiválasztva';
|
||||
return `${num} lehetőség kiválasztva`;
|
||||
},
|
||||
currentValue: 'Aktuális érték',
|
||||
hidePassword: 'Jelszó elrejtése',
|
||||
loading: 'Betöltés',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: 'クリアエントリ',
|
||||
close: '閉じる',
|
||||
copy: 'コピー',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'オプションが選択されていません';
|
||||
if (num === 1) return '1 つのオプションが選択されました';
|
||||
return `${num} つのオプションが選択されました`;
|
||||
},
|
||||
currentValue: '現在の価値',
|
||||
hidePassword: 'パスワードを隠す',
|
||||
loading: '読み込み中',
|
||||
|
||||
@@ -3,12 +3,17 @@ import type { Translation } from '../utilities/localize';
|
||||
|
||||
const translation: Translation = {
|
||||
$code: 'nl',
|
||||
$name: 'Dutch',
|
||||
$name: 'Nederlands',
|
||||
$dir: 'ltr',
|
||||
|
||||
clearEntry: 'Invoer wissen',
|
||||
close: 'Sluiten',
|
||||
copy: 'Kopiëren',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'Geen optie geselecteerd';
|
||||
if (num === 1) return '1 optie geselecteerd';
|
||||
return `${num} opties geselecteerd`;
|
||||
},
|
||||
currentValue: 'Huidige waarde',
|
||||
hidePassword: 'Verberg wachtwoord',
|
||||
loading: 'Bezig met laden',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: 'Wyczyść wpis',
|
||||
close: 'Zamknij',
|
||||
copy: 'Kopiuj',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'Nie wybrano opcji';
|
||||
if (num === 1) return 'Wybrano 1 opcję';
|
||||
return `Wybrano ${num} opcje`;
|
||||
},
|
||||
currentValue: 'Aktualna wartość',
|
||||
hidePassword: 'Ukryj hasło',
|
||||
loading: 'Ładowanie',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: 'Limpar entrada',
|
||||
close: 'Fechar',
|
||||
copy: 'Copiar',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'Nenhuma opção selecionada';
|
||||
if (num === 1) return '1 opção selecionada';
|
||||
return `${num} opções selecionadas`;
|
||||
},
|
||||
currentValue: 'Valor atual',
|
||||
hidePassword: 'Esconder a senha',
|
||||
loading: 'Carregando',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: 'Очистить запись',
|
||||
close: 'Закрыть',
|
||||
copy: 'Скопировать',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'выбрано 0 вариантов';
|
||||
if (num === 1) return 'Выбран 1 вариант';
|
||||
return `выбрано ${num} варианта`;
|
||||
},
|
||||
currentValue: 'Текущее значение',
|
||||
hidePassword: 'Скрыть пароль',
|
||||
loading: 'Загрузка',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: 'Återställ val',
|
||||
close: 'Stäng',
|
||||
copy: 'Kopiera',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'Inga alternativ har valts';
|
||||
if (num === 1) return '1 alternativ valt';
|
||||
return `${num} alternativ valda`;
|
||||
},
|
||||
currentValue: 'Nuvarande värde',
|
||||
hidePassword: 'Dölj lösenord',
|
||||
loading: 'Läser in',
|
||||
|
||||
@@ -9,6 +9,11 @@ const translation: Translation = {
|
||||
clearEntry: 'Girişi sil',
|
||||
close: 'Kapat',
|
||||
copy: 'Kopya',
|
||||
numOptionsSelected: (num: number) => {
|
||||
if (num === 0) return 'Hiçbir seçenek seçilmedi';
|
||||
if (num === 1) return '1 seçenek seçildi';
|
||||
return `${num} seçenek seçildi`;
|
||||
},
|
||||
currentValue: 'Mevcut değer',
|
||||
hidePassword: 'Şifreyi sakla',
|
||||
loading: 'Yükleme',
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface Translation extends DefaultTranslation {
|
||||
clearEntry: string;
|
||||
close: string;
|
||||
copy: string;
|
||||
numOptionsSelected: (num: number) => string;
|
||||
currentValue: string;
|
||||
hidePassword: string;
|
||||
loading: string;
|
||||
|
||||
Reference in New Issue
Block a user