mirror of
https://github.com/shoelace-style/shoelace.git
synced 2026-01-12 02:59:13 +00:00
Merge branch 'next' of https://github.com/shoelace-style/shoelace into next
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
<li><a href="/frameworks/react">React</a></li>
|
||||
<li><a href="/frameworks/vue">Vue</a></li>
|
||||
<li><a href="/frameworks/angular">Angular</a></li>
|
||||
<li><a href="/frameworks/svelte">Svelte</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Turbo from 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@7.3.0/+esm';
|
||||
import * as Turbo from 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+esm';
|
||||
|
||||
(() => {
|
||||
if (!window.scrollPositions) {
|
||||
@@ -6,13 +6,13 @@ import * as Turbo from 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@7.3.0/+esm'
|
||||
}
|
||||
|
||||
function preserveScroll() {
|
||||
document.querySelectorAll('[data-preserve-scroll').forEach(element => {
|
||||
document.querySelectorAll('[data-preserve-scroll]').forEach(element => {
|
||||
scrollPositions[element.id] = element.scrollTop;
|
||||
});
|
||||
}
|
||||
|
||||
function restoreScroll(event) {
|
||||
document.querySelectorAll('[data-preserve-scroll').forEach(element => {
|
||||
document.querySelectorAll('[data-preserve-scroll]').forEach(element => {
|
||||
element.scrollTop = scrollPositions[element.id];
|
||||
});
|
||||
|
||||
|
||||
85
docs/pages/frameworks/svelte.md
Normal file
85
docs/pages/frameworks/svelte.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
meta:
|
||||
title: Svelte
|
||||
description: Tips for using Shoelace in your Svelte app.
|
||||
---
|
||||
|
||||
# Svelte
|
||||
|
||||
Svelte [plays nice](https://custom-elements-everywhere.com/#svelte) with custom elements, so you can use Shoelace in your Svelte apps with ease.
|
||||
|
||||
## Installation
|
||||
|
||||
To add Shoelace to your Svelte app, install the package from npm.
|
||||
|
||||
```bash
|
||||
npm install @shoelace-style/shoelace
|
||||
```
|
||||
|
||||
Next, [include a theme](/getting-started/themes) and set the [base path](/getting-started/installation#setting-the-base-path) for icons and other assets. In this example, we'll import the light theme and use the CDN as a base path.
|
||||
|
||||
```jsx
|
||||
// main.js or main.ts
|
||||
import '@shoelace-style/shoelace/dist/themes/light.css';
|
||||
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path';
|
||||
|
||||
setBasePath('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/%CDNDIR%/');
|
||||
```
|
||||
|
||||
:::tip
|
||||
If you'd rather not use the CDN for assets, you can create a build task that copies `node_modules/@shoelace-style/shoelace/dist/assets` into a public folder in your app. Then you can point the base path to that folder instead.
|
||||
:::
|
||||
|
||||
## Usage
|
||||
|
||||
### QR code generator example
|
||||
|
||||
```jsx
|
||||
<h1>Live editing</h1>
|
||||
|
||||
<sl-input label="Message" value={message} oninput={event => message = event.target.value}></sl-input>
|
||||
|
||||
<sl-alert open>
|
||||
<sl-icon slot="icon" name="info-circle"></sl-icon>
|
||||
{message}
|
||||
</sl-alert>
|
||||
|
||||
<script>
|
||||
import '@shoelace-style/shoelace/dist/components/alert/alert.js'
|
||||
import '@shoelace-style/shoelace/dist/components/input/input.js';
|
||||
|
||||
let message = $state('')
|
||||
</script>
|
||||
```
|
||||
|
||||
### Two-way Binding
|
||||
|
||||
One caveat is there's currently Svelte only supports `bind:value` directive in `<input>`, `<textarea>` and `<select>`, but you can still achieve two-way binding manually.
|
||||
|
||||
```jsx
|
||||
// This doesn't work
|
||||
<sl-input bind:value="name"></sl-input>
|
||||
// This works, but it's a bit longer
|
||||
<sl-input value={name} oninput={event => message = event.target.value}></sl-input>
|
||||
```
|
||||
|
||||
:::tip
|
||||
Are you using Shoelace with Svelte? [Help us improve this page!](https://github.com/shoelace-style/shoelace/blob/next/docs/frameworks/svelte.md)
|
||||
:::
|
||||
|
||||
### Slots
|
||||
|
||||
Slots in Shoelace/web components are functionally the same as basic slots in Svelte. Slots can be assigned to elements using the `slot` attribute followed by the name of the slot it is being assigned to.
|
||||
|
||||
Here is an example:
|
||||
|
||||
```jsx
|
||||
<sl-drawer label="Drawer" placement="start" class="drawer-placement-start" bind:open={drawerIsOpen}>
|
||||
This drawer slides in from the start.
|
||||
<div slot="footer">
|
||||
<sl-button variant="primary" onclick={() => (drawerIsOpen = false)}>
|
||||
Close
|
||||
</sl-button>
|
||||
</div>
|
||||
</sl-drawer>
|
||||
```
|
||||
@@ -18,6 +18,8 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
||||
- Added Ukrainian translation [#2270]
|
||||
- Added support for <kbd>Enter</kbd> to `<sl-split-panel>` to align with ARIA APG's [window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/) [#2234]
|
||||
- Added community maintained docs for Svelte [#2262]
|
||||
- Fixed a bug in `<sl-select>` when setting the value property before the element connected. [#2255]
|
||||
- Fixed a bug in `<sl-select>` where it was using the wrong tag name. [#2287]
|
||||
- Fixed a bug in `<sl-carousel>` that caused the navigation icons to be reversed
|
||||
- Fixed a bug in `<sl-carousel>` that caused interactive elements to be activated when dragging [#2196]
|
||||
- Fixed a bug in `<sl-carousel>` that caused out of order slides when used inside a resize observer [#2260]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { animateTo, stopAnimations } from '../../internal/animate.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { defaultValue } from '../../internal/default-value.js';
|
||||
import { FormControlController } from '../../internal/form.js';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry.js';
|
||||
import { HasSlotController } from '../../internal/slot.js';
|
||||
@@ -102,21 +101,35 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
/** The name of the select, submitted as a name/value pair with form data. */
|
||||
@property() name = '';
|
||||
|
||||
private _value: string | string[] = '';
|
||||
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current value of the select, submitted as a name/value pair with form data. When `multiple` is enabled, the
|
||||
* value attribute will be a space-delimited list of values based on the options selected, and the value property will
|
||||
* be an array. **For this reason, values must not contain spaces.**
|
||||
*/
|
||||
@property({
|
||||
converter: {
|
||||
fromAttribute: (value: string) => value.split(' '),
|
||||
toAttribute: (value: string[]) => value.join(' ')
|
||||
@state()
|
||||
set value(val: string | string[]) {
|
||||
if (this.multiple) {
|
||||
val = Array.isArray(val) ? val : val.split(' ');
|
||||
} else {
|
||||
val = Array.isArray(val) ? val.join(' ') : val;
|
||||
}
|
||||
})
|
||||
value: string | string[] = '';
|
||||
|
||||
if (this._value === val) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.valueHasChanged = true;
|
||||
this._value = val;
|
||||
}
|
||||
|
||||
/** The default value of the form control. Primarily used for resetting the form control. */
|
||||
@defaultValue() defaultValue: string | string[] = '';
|
||||
@property({ attribute: 'value' }) defaultValue: string | string[] = '';
|
||||
|
||||
/** The select's size. */
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
@@ -451,6 +464,8 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
private handleClearClick(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
|
||||
this.valueHasChanged = true;
|
||||
|
||||
if (this.value !== '') {
|
||||
this.setSelectedOptions([]);
|
||||
this.displayInput.focus({ preventScroll: true });
|
||||
@@ -522,6 +537,8 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
private handleTagRemove(event: SlRemoveEvent, option: SlOption) {
|
||||
event.stopPropagation();
|
||||
|
||||
this.valueHasChanged = true;
|
||||
|
||||
if (!this.disabled) {
|
||||
this.toggleOptionSelection(option, false);
|
||||
|
||||
@@ -598,6 +615,9 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
// Update selected options cache
|
||||
this.selectedOptions = options.filter(el => el.selected);
|
||||
|
||||
// Keep a reference to the previous `valueHasChanged`. Changes made here don't count has changing the value.
|
||||
const cachedValueHasChanged = this.valueHasChanged;
|
||||
|
||||
// Update the value and display label
|
||||
if (this.multiple) {
|
||||
this.value = this.selectedOptions.map(el => el.value);
|
||||
@@ -613,12 +633,14 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
this.value = selectedOption?.value ?? '';
|
||||
this.displayLabel = selectedOption?.getTextLabel?.() ?? '';
|
||||
}
|
||||
this.valueHasChanged = cachedValueHasChanged;
|
||||
|
||||
// Update validity
|
||||
this.updateComplete.then(() => {
|
||||
this.formControlController.updateValidity();
|
||||
});
|
||||
}
|
||||
|
||||
protected get tags() {
|
||||
return this.selectedOptions.map((option, index) => {
|
||||
if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) {
|
||||
@@ -649,8 +671,29 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
}
|
||||
}
|
||||
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
attributeChangedCallback(name: string, oldVal: string | null, newVal: string | null) {
|
||||
super.attributeChangedCallback(name, oldVal, newVal);
|
||||
|
||||
/** This is a backwards compatibility call. In a new major version we should make a clean separation between "value" the attribute mapping to "defaultValue" property and "value" the property not reflecting. */
|
||||
if (name === 'value') {
|
||||
const cachedValueHasChanged = this.valueHasChanged;
|
||||
this.value = this.defaultValue;
|
||||
|
||||
// Set it back to false since this isn't an interaction.
|
||||
this.valueHasChanged = cachedValueHasChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@watch(['defaultValue', 'value'], { waitUntilFirstUpdate: true })
|
||||
handleValueChange() {
|
||||
if (!this.valueHasChanged) {
|
||||
const cachedValueHasChanged = this.valueHasChanged;
|
||||
this.value = this.defaultValue;
|
||||
|
||||
// Set it back to false since this isn't an interaction.
|
||||
this.valueHasChanged = cachedValueHasChanged;
|
||||
}
|
||||
|
||||
const allOptions = this.getAllOptions();
|
||||
const value = Array.isArray(this.value) ? this.value : [this.value];
|
||||
|
||||
|
||||
@@ -740,6 +740,52 @@ describe('<sl-select>', () => {
|
||||
expect(new FormData(form).getAll('select')).to.have.members(['foo', 'bar', 'baz']);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @see {https://github.com/shoelace-style/shoelace/issues/2254}
|
||||
*/
|
||||
it('Should account for if `value` changed before connecting', async () => {
|
||||
const select = await fixture<SlSelect>(html`
|
||||
<sl-select label="Search By" multiple clearable .value=${['foo', 'bar']}>
|
||||
<sl-option value="foo">Foo</sl-option>
|
||||
<sl-option value="bar">Bar</sl-option>
|
||||
</sl-select>
|
||||
`);
|
||||
|
||||
// just for safe measure.
|
||||
await aTimeout(10);
|
||||
|
||||
expect(select.value).to.deep.equal(['foo', 'bar']);
|
||||
});
|
||||
|
||||
/**
|
||||
* @see {https://github.com/shoelace-style/shoelace/issues/2254}
|
||||
*/
|
||||
it('Should still work if using the value attribute', async () => {
|
||||
const select = await fixture<SlSelect>(html`
|
||||
<sl-select label="Search By" multiple clearable value="foo bar">
|
||||
<sl-option value="foo">Foo</sl-option>
|
||||
<sl-option value="bar">Bar</sl-option>
|
||||
</sl-select>
|
||||
`);
|
||||
|
||||
// just for safe measure.
|
||||
await aTimeout(10);
|
||||
|
||||
expect(select.value).to.deep.equal(['foo', 'bar']);
|
||||
|
||||
await clickOnElement(select);
|
||||
await select.updateComplete;
|
||||
await clickOnElement(select.querySelector("[value='foo']")!);
|
||||
|
||||
await select.updateComplete;
|
||||
await aTimeout(10);
|
||||
expect(select.value).to.deep.equal(['bar']);
|
||||
|
||||
select.setAttribute('value', 'foo bar');
|
||||
await aTimeout(10);
|
||||
expect(select.value).to.deep.equal(['foo', 'bar']);
|
||||
});
|
||||
});
|
||||
|
||||
runFormControlBaseTests('sl-select');
|
||||
|
||||
Reference in New Issue
Block a user