From 94a1043fb8a766ff4318f047c7c026c29fd8ebdb Mon Sep 17 00:00:00 2001 From: Chris Krawietz Date: Mon, 2 Dec 2024 19:21:07 +0100 Subject: [PATCH 1/3] refactor turbo.js query selector (#2293) Updated esm import of turbo from hotwire & fixed spelling of querySelector --- docs/assets/scripts/turbo.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/assets/scripts/turbo.js b/docs/assets/scripts/turbo.js index 7075217f..6d506ef6 100644 --- a/docs/assets/scripts/turbo.js +++ b/docs/assets/scripts/turbo.js @@ -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]; }); From a8900fd56c63bd77964bd513bc97f982761ec4ac Mon Sep 17 00:00:00 2001 From: Konnor Rogers Date: Mon, 2 Dec 2024 13:22:31 -0500 Subject: [PATCH 2/3] fix select regressions (#2255) * fix select regressions * prettier * prettier * fix value not clearing with clear / tag removal * prettier * prettier * prettier * prettier * add changelog entry * prettier --------- Co-authored-by: Cory LaViska --- docs/pages/resources/changelog.md | 3 +- src/components/select/select.component.ts | 61 +++++++++++++++++++---- src/components/select/select.test.ts | 46 +++++++++++++++++ 3 files changed, 100 insertions(+), 10 deletions(-) diff --git a/docs/pages/resources/changelog.md b/docs/pages/resources/changelog.md index 4b711c1e..437af4e9 100644 --- a/docs/pages/resources/changelog.md +++ b/docs/pages/resources/changelog.md @@ -17,7 +17,8 @@ New versions of Shoelace are released as-needed and generally occur when a criti - Added Norwegian translations for Bokmål and Nynorsk [#2268] - Added Ukrainian translation [#2270] - Added support for Enter to `` to align with ARIA APG's [window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/) [#2234] -- Fixed a bug in `` where it was using the wrong tag name,. [#2287] +- Fixed a bug in `` when setting the value property before the element connected. [#2255] +- Fixed a bug in `` where it was using the wrong tag name. [#2287] - Fixed a bug in `` that caused the navigation icons to be reversed - Fixed a bug in `` that prevented label changes in `` from updating the controller [#1971] - Fixed a bug in `` that caused a console warning in Firefox when typing [#2107] diff --git a/src/components/select/select.component.ts b/src/components/select/select.component.ts index 3af09dc4..6016ee84 100644 --- a/src/components/select/select.component.ts +++ b/src/components/select/select.component.ts @@ -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]; diff --git a/src/components/select/select.test.ts b/src/components/select/select.test.ts index a9ca7a22..e08c1c6d 100644 --- a/src/components/select/select.test.ts +++ b/src/components/select/select.test.ts @@ -740,6 +740,52 @@ describe('', () => { 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(html` + + Foo + Bar + + `); + + // 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(html` + + Foo + Bar + + `); + + // 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'); From 9f401f8e8b8353e14e7bdf862a46db437afa9b2e Mon Sep 17 00:00:00 2001 From: Emanuel Saramago Date: Mon, 2 Dec 2024 18:23:04 +0000 Subject: [PATCH 3/3] Documentation for Svelte (#2262) * Documentation for Svelte #2261 * fix bracket --------- Co-authored-by: esaramago --- docs/_includes/sidebar.njk | 1 + docs/pages/frameworks/svelte.md | 85 +++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 docs/pages/frameworks/svelte.md diff --git a/docs/_includes/sidebar.njk b/docs/_includes/sidebar.njk index 11745e65..df0958bb 100644 --- a/docs/_includes/sidebar.njk +++ b/docs/_includes/sidebar.njk @@ -17,6 +17,7 @@
  • React
  • Vue
  • Angular
  • +
  • Svelte
  • diff --git a/docs/pages/frameworks/svelte.md b/docs/pages/frameworks/svelte.md new file mode 100644 index 00000000..0e11967b --- /dev/null +++ b/docs/pages/frameworks/svelte.md @@ -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 +

    Live editing

    + + message = event.target.value}> + + + + {message} + + + +``` + +### Two-way Binding + +One caveat is there's currently Svelte only supports `bind:value` directive in ``, `