Merge branch 'next' into current

current
Cory LaViska 2024-12-02 14:22:34 -05:00
commit a40d2ca1b5
35 zmienionych plików z 5339 dodań i 10981 usunięć

Wyświetl plik

@ -79,6 +79,7 @@ module.exports = {
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'warn',
'@typescript-eslint/no-unnecessary-condition': 'off',
'@typescript-eslint/no-unnecessary-qualifier': 'warn',
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
'@typescript-eslint/non-nullable-type-assertion-style': 'warn',
'@typescript-eslint/prefer-for-of': 'warn',
'@typescript-eslint/prefer-optional-chain': 'warn',

Wyświetl plik

@ -9,7 +9,7 @@ A forward-thinking library of web components.
- Built with accessibility in mind ♿️
- Open source 😸
Designed in New Hampshire by [Cory LaViska](https://twitter.com/claviska).
Designed in New Hampshire by [Cory LaViska](https://twitter.com/cory_laviska).
---
@ -77,6 +77,6 @@ Shoelace is an open source project and contributions are encouraged! If you're i
## License
Shoelace was created by [Cory LaViska](https://twitter.com/claviska) and is available under the terms of the MIT license.
Shoelace was created by [Cory LaViska](https://twitter.com/cory_laviska) and is available under the terms of the MIT license.
Whether you're building Shoelace or building something _with_ Shoelace — have fun creating! 🥾

Wyświetl plik

@ -14,6 +14,7 @@
"autoloading",
"autoplay",
"bezier",
"Bokmål",
"boxicons",
"CACHEABLE",
"callout",
@ -166,6 +167,7 @@
"valpha",
"valuenow",
"valuetext",
"vuejs",
"WEBP",
"Webpacker",
"wordmark"

Wyświetl plik

@ -283,7 +283,7 @@
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#custom-properties') }}">customizing CSS custom properties</a>.</em></p>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/customizing#custom-properties') }}">customizing CSS custom properties</a>.</em></p>
{% endif %}
{# CSS Parts #}

Wyświetl plik

@ -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>

Wyświetl plik

@ -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];
});

Wyświetl plik

@ -504,17 +504,17 @@ Remember that custom tags are rendered in a shadow root. To style them, you can
### Lazy loading options
Lazy loading options is very hard to get right. `<wa-select>` largely follows how a native `<select>` works.
Lazy loading options is very hard to get right. `<sl-select>` largely follows how a native `<select>` works.
Here are the following conditions:
- If a `<wa-select>` is created without any options, but is given a `value` attribute, its `value` will be `""`, and then when options are added, if any of the options have a value equal to the `<wa-select>` value, the value of the `<wa-select>` will equal that of the option.
- If a `<sl-select>` is created without any options, but is given a `value` attribute, its `value` will be `""`, and then when options are added, if any of the options have a value equal to the `<sl-select>` value, the value of the `<sl-select>` will equal that of the option.
EX: `<wa-select value="foo">` will have a value of `""` until `<wa-option value="foo">Foo</wa-option>` connects, at which point its value will become `"foo"` when submitting.
EX: `<sl-select value="foo">` will have a value of `""` until `<sl-option value="foo">Foo</sl-option>` connects, at which point its value will become `"foo"` when submitting.
- If a `<wa-select multiple>` with an initial value has multiple values, but only some of the options are present, it will only respect the options that are present, and if a selected option is loaded in later, _AND_ the value of the select has not changed via user interaction or direct property assignment, it will add the selected option to the form value and to the `.value` of the select.
- If a `<sl-select multiple>` with an initial value has multiple values, but only some of the options are present, it will only respect the options that are present, and if a selected option is loaded in later, _AND_ the value of the select has not changed via user interaction or direct property assignment, it will add the selected option to the form value and to the `.value` of the select.
This can be hard to conceptualize, so heres a fairly large example showing how lazy loaded options work with `<wa-select>` and `<wa-select multiple>` when given initial value attributes. Feel free to play around with it in a codepen.
This can be hard to conceptualize, so heres a fairly large example showing how lazy loaded options work with `<sl-select>` and `<sl-select multiple>` when given initial value attributes. Feel free to play around with it in a codepen.
```html:preview
<form id="lazy-options-example">

Wyświetl plik

@ -31,7 +31,7 @@ const App = () => (
### Sizes
Use the `size` attribute to change a tab's size.
Use the `size` attribute to change a tag's size.
```html:preview
<sl-tag size="small">Small</sl-tag>
@ -53,7 +53,7 @@ const App = () => (
### Pill
Use the `pill` attribute to give tabs rounded edges.
Use the `pill` attribute to give tags rounded edges.
```html:preview
<sl-tag size="small" pill>Small</sl-tag>

Wyświetl plik

@ -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>
```

Wyświetl plik

@ -106,7 +106,7 @@ If you need to support IE11 or pre-Chromium Edge, this library isn't for you. Al
## License
Shoelace was created in New Hampshire by [Cory LaViska](https://twitter.com/claviska). It's available under the terms of the [MIT license](https://github.com/shoelace-style/shoelace/blob/next/LICENSE.md).
Shoelace was created in New Hampshire by [Cory LaViska](https://twitter.com/cory_laviska). It's available under the terms of the [MIT license](https://github.com/shoelace-style/shoelace/blob/next/LICENSE.md).
## Attribution

Wyświetl plik

@ -12,9 +12,28 @@ Components with the <sl-badge variant="warning" pill>Experimental</sl-badge> bad
New versions of Shoelace are released as-needed and generally occur when a critical mass of changes have accumulated. At any time, you can see what's coming in the next release by visiting [next.shoelace.style](https://next.shoelace.style).
## 2.19.0
- Added Norwegian translations for Bokmål and Nynorsk [#2268]
- Added Ukrainian translation [#2270]
- 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]
- Fixed a bug in `<sl-rating>` that allowed tabbing into the rating when readonly [#2271]
- Fixed a bug in `<sl-select>` where it was using the wrong tag name,. [#2287]
- Fixed a bug in `<sl-select>` that prevented label changes in `<sl-option>` from updating the controller [#1971]
- Fixed a bug in `<sl-select>` that caused the placeholder to display incorrectly when using `placeholder` and `multiple` [#2292]
- Fixed a bug in `<sl-textarea>` that caused a console warning in Firefox when typing [#2107]
- Improved accessibility of `<sl-split-panel>` by adding support for <kbd>Enter</kbd> to align with ARIA APG's [window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/) [#2234]
- Improved performance of `<sl-range>` by skipping positioning logic when tooltip isn't shown [#2064]
- Updated many dependencies to their latest versions
## 2.18.0
- Added Finnish translations [#2211]
- Added Finnish translation [#2211]
- Added the `.focus` function to `<sl-radio-group>` [#2192]
- Fixed a bug in `<sl-tab-group>` when removed from the DOM too quickly. [#2218]
- Fixed a bug with `<sl-select>` not respecting its initial value. [#2204]

Wyświetl plik

@ -49,7 +49,7 @@ You can post questions on Stack Overflow using [the "shoelace" tag](https://stac
## Twitter
Follow [@shoelace_style](https://twitter.com/shoelace_style) on Twitter for general updates and announcements about Shoelace. This is a great place to say "hi" or to share something you're working on. You're also welcome to follow [@claviska](https://twitter.com/claviska), the creator, for tweets about web components, web development, and life.
Follow [@shoelace_style](https://twitter.com/shoelace_style) on Twitter for general updates and announcements about Shoelace. This is a great place to say "hi" or to share something you're working on. You're also welcome to follow [@cory_laviska](https://twitter.com/cory_laviska), the creator, for tweets about web components, web development, and life.
**Please avoid using Twitter for support questions.** The [discussion forum](https://github.com/shoelace-style/shoelace/discussions) is a much better place to share code snippets, screenshots, and other troubleshooting info. You'll have much better luck there, as more users will have a chance to help you.

15703
package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -1,7 +1,7 @@
{
"name": "@shoelace-style/shoelace",
"description": "A forward-thinking library of web components.",
"version": "2.18.0",
"version": "2.19.0",
"homepage": "https://github.com/shoelace-style/shoelace",
"author": "Cory LaViska",
"license": "MIT",
@ -64,84 +64,83 @@
"test:component": "web-test-runner -- --watch --group",
"test:watch": "web-test-runner --watch --group default",
"spellcheck": "cspell \"**/*.{js,ts,json,html,css,md}\" --no-progress",
"list-outdated-dependencies": "npm-check-updates --format repo --peer",
"update-dependencies": "npm-check-updates --peer -u && npm install"
"check-updates": "npx npm-check-updates --interactive --format group"
},
"engines": {
"node": ">=14.17.0"
},
"dependencies": {
"@ctrl/tinycolor": "^4.0.2",
"@floating-ui/dom": "^1.5.3",
"@lit/react": "^1.0.0",
"@shoelace-style/animations": "^1.1.0",
"@shoelace-style/localize": "^3.1.2",
"composed-offset-position": "^0.0.4",
"lit": "^3.0.0",
"@ctrl/tinycolor": "^4.1.0",
"@floating-ui/dom": "^1.6.12",
"@lit/react": "^1.0.6",
"@shoelace-style/animations": "^1.2.0",
"@shoelace-style/localize": "^3.2.1",
"composed-offset-position": "^0.0.6",
"lit": "^3.2.1",
"qr-creator": "^1.0.0"
},
"devDependencies": {
"@11ty/eleventy": "^2.0.1",
"@custom-elements-manifest/analyzer": "^0.8.4",
"@open-wc/testing": "^3.2.0",
"@types/mocha": "^10.0.2",
"@types/react": "^18.2.28",
"@custom-elements-manifest/analyzer": "^0.10.3",
"@open-wc/testing": "^4.0.0",
"@types/mocha": "^10.0.10",
"@types/react": "^18.3.12",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"@web/dev-server-esbuild": "^0.3.6",
"@web/test-runner": "^0.18.0",
"@web/dev-server-esbuild": "^1.0.3",
"@web/test-runner": "^0.19.0",
"@web/test-runner-commands": "^0.9.0",
"@web/test-runner-playwright": "^0.11.0",
"bootstrap-icons": "^1.11.1",
"browser-sync": "^2.29.3",
"bootstrap-icons": "^1.11.3",
"browser-sync": "^3.0.3",
"chalk": "^5.3.0",
"change-case": "^4.1.2",
"command-line-args": "^5.2.1",
"comment-parser": "^1.4.0",
"cspell": "^6.18.1",
"custom-element-jet-brains-integration": "^1.4.0",
"custom-element-vs-code-integration": "^1.2.1",
"custom-element-vuejs-integration": "^1.0.0",
"del": "^7.1.0",
"command-line-args": "^6.0.1",
"comment-parser": "^1.4.1",
"cspell": "^8.16.1",
"custom-element-jet-brains-integration": "^1.6.2",
"custom-element-vs-code-integration": "^1.4.1",
"custom-element-vuejs-integration": "^1.3.3",
"del": "^8.0.0",
"download": "^8.0.0",
"esbuild": "^0.19.4",
"esbuild": "^0.24.0",
"esbuild-plugin-replace": "^1.4.0",
"eslint": "^8.51.0",
"eslint-plugin-chai-expect": "^3.0.0",
"eslint-plugin-chai-expect": "^3.1.0",
"eslint-plugin-chai-friendly": "^0.7.2",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-lit": "^1.9.1",
"eslint-plugin-lit-a11y": "^4.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-lit": "^1.15.0",
"eslint-plugin-lit-a11y": "^4.1.4",
"eslint-plugin-markdown": "^3.0.1",
"eslint-plugin-sort-imports-es6-autofix": "^0.6.0",
"eslint-plugin-wc": "^2.0.4",
"eslint-plugin-wc": "^2.2.0",
"front-matter": "^4.0.2",
"get-port": "^7.0.0",
"globby": "^13.2.2",
"husky": "^8.0.3",
"jsdom": "^22.1.0",
"lint-staged": "^14.0.1",
"get-port": "^7.1.0",
"globby": "^14.0.2",
"husky": "^9.1.7",
"jsdom": "^25.0.1",
"lint-staged": "^15.2.10",
"lunr": "^2.3.9",
"markdown-it-container": "^3.0.0",
"markdown-it-ins": "^3.0.1",
"markdown-it-container": "^4.0.0",
"markdown-it-ins": "^4.0.0",
"markdown-it-kbd": "^2.2.2",
"markdown-it-mark": "^3.0.1",
"markdown-it-mark": "^4.0.0",
"markdown-it-replace-it": "^1.0.0",
"npm-check-updates": "^16.14.6",
"ora": "^7.0.1",
"pascal-case": "^3.1.2",
"plop": "^4.0.0",
"prettier": "^3.0.3",
"npm-check-updates": "^17.1.11",
"ora": "^8.1.1",
"pascal-case": "^4.0.0",
"plop": "^4.0.1",
"prettier": "^3.4.1",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react": "^18.3.1",
"recursive-copy": "^2.0.14",
"sinon": "^16.1.0",
"sinon": "^19.0.2",
"smartquotes": "^2.3.2",
"source-map": "^0.7.4",
"strip-css-comments": "^5.0.0",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"user-agent-data-types": "^0.3.1"
"tslib": "^2.8.1",
"typescript": "^5.7.2",
"user-agent-data-types": "^0.4.2"
},
"lint-staged": {
"*.{ts,js}": [

Wyświetl plik

@ -92,6 +92,7 @@ export default class SlCarousel extends ShoelaceElement {
@state() dragging = false;
private autoplayController = new AutoplayController(this, () => this.next());
private dragStartPosition: [number, number] = [-1, -1];
private readonly localize = new LocalizeController(this);
private mutationObserver: MutationObserver;
private pendingSlideChange = false;
@ -151,6 +152,20 @@ export default class SlCarousel extends ShoelaceElement {
) as SlCarouselItem[];
}
private handleClick(event: MouseEvent) {
if (this.dragging && this.dragStartPosition[0] > 0 && this.dragStartPosition[1] > 0) {
const deltaX = Math.abs(this.dragStartPosition[0] - event.clientX);
const deltaY = Math.abs(this.dragStartPosition[1] - event.clientY);
const delta = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// Prevents clicks on interactive elements while dragging if the click is within a small range. This prevents
// accidental drags from interfering with intentional clicks.
if (delta >= 10) {
event.preventDefault();
}
}
}
private handleKeyDown(event: KeyboardEvent) {
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
const target = event.target as HTMLElement;
@ -208,6 +223,7 @@ export default class SlCarousel extends ShoelaceElement {
// Start dragging if it hasn't yet
this.scrollContainer.style.setProperty('scroll-snap-type', 'none');
this.dragging = true;
this.dragStartPosition = [event.clientX, event.clientY];
}
this.scrollContainer.scrollBy({
@ -255,6 +271,7 @@ export default class SlCarousel extends ShoelaceElement {
scrollContainer.style.removeProperty('scroll-snap-type');
this.dragging = false;
this.dragStartPosition = [-1, -1];
this.handleScrollEnd();
});
};
@ -364,10 +381,10 @@ export default class SlCarousel extends ShoelaceElement {
this.createClones();
}
this.synchronizeSlides();
// Because the DOM may be changed, restore the scroll position to the active slide
this.goToSlide(this.activeSlide, 'auto');
this.synchronizeSlides();
}
private createClones() {
@ -512,7 +529,7 @@ export default class SlCarousel extends ShoelaceElement {
const currentPage = this.getCurrentPage();
const prevEnabled = this.canScrollPrev();
const nextEnabled = this.canScrollNext();
const isLtr = this.localize.dir() === 'rtl';
const isLtr = this.localize.dir() === 'ltr';
return html`
<div part="base" class="carousel">
@ -533,6 +550,7 @@ export default class SlCarousel extends ShoelaceElement {
@mousedown="${this.handleMouseDragStart}"
@scroll="${this.handleScroll}"
@scrollend=${this.handleScrollEnd}
@click=${this.handleClick}
>
<slot></slot>
</div>

Wyświetl plik

@ -1053,7 +1053,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
<sl-dropdown
class="color-dropdown"
aria-disabled=${this.disabled ? 'true' : 'false'}
.containing-element=${this}
.containingElement=${this}
?disabled=${this.disabled}
?hoist=${this.hoist}
@sl-after-hide=${this.handleAfterHide}

Wyświetl plik

@ -114,7 +114,7 @@ export default class SlDialog extends ShoelaceElement {
super.disconnectedCallback();
this.modal.deactivate();
unlockBodyScrolling(this);
this.closeWatcher?.destroy();
this.removeOpenListeners();
}
private requestClose(source: 'close-button' | 'keyboard' | 'overlay') {

Wyświetl plik

@ -131,7 +131,7 @@ export default class SlDrawer extends ShoelaceElement {
disconnectedCallback() {
super.disconnectedCallback();
unlockBodyScrolling(this);
this.closeWatcher?.destroy();
this.removeOpenListeners();
}
private requestClose(source: 'close-button' | 'keyboard' | 'overlay') {

Wyświetl plik

@ -130,8 +130,8 @@ export class SubmenuController implements ReactiveController {
} else {
this.enableSubmenu(false);
this.host.updateComplete.then(() => {
if (menuItems![0] instanceof HTMLElement) {
menuItems![0].focus();
if (menuItems[0] instanceof HTMLElement) {
menuItems[0].focus();
}
});
this.host.requestUpdate();

Wyświetl plik

@ -31,7 +31,6 @@ export default class SlOption extends ShoelaceElement {
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = { 'sl-icon': SlIcon };
private cachedTextLabel: string;
// @ts-expect-error - Controller is currently unused
private readonly localize = new LocalizeController(this);
@ -58,19 +57,13 @@ export default class SlOption extends ShoelaceElement {
}
private handleDefaultSlotChange() {
const textLabel = this.getTextLabel();
// Ignore the first time the label is set
if (typeof this.cachedTextLabel === 'undefined') {
this.cachedTextLabel = textLabel;
return;
}
// When the label changes, emit a slotchange event so parent controls see it
if (textLabel !== this.cachedTextLabel) {
this.cachedTextLabel = textLabel;
this.emit('slotchange', { bubbles: true, composed: false, cancelable: false });
}
// When the label changes, tell the controller to update
customElements.whenDefined('sl-select').then(() => {
const controller = this.closest('sl-select');
if (controller) {
controller.handleDefaultSlotChange();
}
});
}
private handleMouseEnter() {

Wyświetl plik

@ -1,6 +1,5 @@
import '../../../dist/shoelace.js';
import { aTimeout, expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import { aTimeout, expect, fixture, html } from '@open-wc/testing';
import type SlOption from './option.js';
describe('<sl-option>', () => {
@ -32,17 +31,6 @@ describe('<sl-option>', () => {
expect(el.getAttribute('aria-disabled')).to.equal('true');
});
it('emits the slotchange event when the label changes', async () => {
const el = await fixture<SlOption>(html` <sl-option>Text</sl-option> `);
const slotChangeHandler = sinon.spy();
el.addEventListener('slotchange', slotChangeHandler);
el.textContent = 'New Text';
await waitUntil(() => slotChangeHandler.calledOnce);
expect(slotChangeHandler).to.have.been.calledOnce;
});
it('should convert non-string values to string', async () => {
const el = await fixture<SlOption>(html` <sl-option>Text</sl-option> `);

Wyświetl plik

@ -272,8 +272,8 @@ export default class SlPopup extends ShoelaceElement {
}
private start() {
// We can't start the positioner without an anchor
if (!this.anchorEl) {
// We can't start the positioner without an anchor or when the popup is inactive
if (!this.anchorEl || !this.active) {
return;
}

Wyświetl plik

@ -1,10 +1,33 @@
import '../../../dist/shoelace.js';
import { expect, fixture, html } from '@open-wc/testing';
import type SlPopup from './popup.js';
describe('<sl-popup>', () => {
let element: SlPopup;
it('should render a component', async () => {
const el = await fixture(html` <sl-popup></sl-popup> `);
expect(el).to.exist;
});
it('should properly handle positioning when active changes', async () => {
element = await fixture('<sl-popup></sl-popup>');
element.active = true;
await element.updateComplete;
// SImulate a scroll event
const event = new Event('scroll');
window.dispatchEvent(event);
element.active = false;
await element.updateComplete;
// The component should not throw an error when the window is scrolled
expect(() => {
element.active = true;
window.dispatchEvent(event);
}).not.to.throw();
});
});

Wyświetl plik

@ -215,7 +215,7 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
this.syncProgress(percent);
if (this.tooltip !== 'none') {
if (this.tooltip !== 'none' && this.hasTooltip) {
// Ensure updates are drawn before we sync the tooltip
this.updateComplete.then(() => this.syncTooltip(percent));
}

Wyświetl plik

@ -240,7 +240,7 @@ export default class SlRating extends ShoelaceElement {
aria-valuenow=${this.value}
aria-valuemin=${0}
aria-valuemax=${this.max}
tabindex=${this.disabled ? '-1' : '0'}
tabindex=${this.disabled || this.readonly ? '-1' : '0'}
@click=${this.handleClick}
@keydown=${this.handleKeyDown}
@mouseenter=${this.handleMouseEnter}

Wyświetl plik

@ -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 });
@ -501,9 +516,10 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
}
}
private handleDefaultSlotChange() {
if (!customElements.get('wa-option')) {
customElements.whenDefined('wa-option').then(() => this.handleDefaultSlotChange());
/* @internal - used by options to update labels */
public handleDefaultSlotChange() {
if (!customElements.get('sl-option')) {
customElements.whenDefined('sl-option').then(() => this.handleDefaultSlotChange());
}
const allOptions = this.getAllOptions();
@ -521,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);
@ -597,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);
@ -612,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) {
@ -648,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];

Wyświetl plik

@ -176,7 +176,7 @@ export default css`
margin-inline-end: var(--sl-input-spacing-small);
}
.select--small.select--multiple .select__prefix::slotted(*) {
.select--small.select--multiple:not(.select--placeholder-visible) .select__prefix::slotted(*) {
margin-inline-start: var(--sl-input-spacing-small);
}
@ -205,11 +205,11 @@ export default css`
margin-inline-end: var(--sl-input-spacing-medium);
}
.select--medium.select--multiple .select__prefix::slotted(*) {
.select--medium.select--multiple:not(.select--placeholder-visible) .select__prefix::slotted(*) {
margin-inline-start: var(--sl-input-spacing-medium);
}
.select--medium.select--multiple .select__combobox {
.select--medium.select--multiple:not(.select--placeholder-visible) .select__combobox {
padding-inline-start: 0;
padding-block: 3px;
}
@ -234,11 +234,11 @@ export default css`
margin-inline-end: var(--sl-input-spacing-large);
}
.select--large.select--multiple .select__prefix::slotted(*) {
.select--large.select--multiple:not(.select--placeholder-visible) .select__prefix::slotted(*) {
margin-inline-start: var(--sl-input-spacing-large);
}
.select--large.select--multiple .select__combobox {
.select--large.select--multiple:not(.select--placeholder-visible) .select__combobox {
padding-inline-start: 0;
padding-block: 4px;
}

Wyświetl plik

@ -506,7 +506,9 @@ describe('<sl-select>', () => {
expect(displayInput.value).to.equal('Option 1');
option.textContent = 'updated';
await oneEvent(option, 'slotchange');
await aTimeout(250);
await option.updateComplete;
await el.updateComplete;
expect(displayInput.value).to.equal('updated');
@ -738,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');

Wyświetl plik

@ -37,7 +37,9 @@ export default class SlSplitPanel extends ShoelaceElement {
static styles: CSSResultGroup = [componentStyles, styles];
private cachedPositionInPixels: number;
private isCollapsed = false;
private readonly localize = new LocalizeController(this);
private positionBeforeCollapsing = 0;
private resizeObserver: ResizeObserver;
private size: number;
@ -159,7 +161,7 @@ export default class SlSplitPanel extends ShoelaceElement {
return;
}
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter'].includes(event.key)) {
let newPosition = this.position;
const incr = (event.shiftKey ? 10 : 1) * (this.primary === 'end' ? -1 : 1);
@ -181,6 +183,24 @@ export default class SlSplitPanel extends ShoelaceElement {
newPosition = this.primary === 'end' ? 0 : 100;
}
// Collapse/expand the primary panel when enter is pressed
if (event.key === 'Enter') {
if (this.isCollapsed) {
newPosition = this.positionBeforeCollapsing;
this.isCollapsed = false;
} else {
const positionBeforeCollapsing = this.position;
newPosition = 0;
// Wait for position to update before setting the collapsed state
requestAnimationFrame(() => {
this.isCollapsed = true;
this.positionBeforeCollapsing = positionBeforeCollapsing;
});
}
}
this.position = clamp(newPosition, 0, 100);
}
}
@ -206,6 +226,8 @@ export default class SlSplitPanel extends ShoelaceElement {
@watch('position')
handlePositionChange() {
this.cachedPositionInPixels = this.percentageToPixels(this.position);
this.isCollapsed = false;
this.positionBeforeCollapsing = 0;
this.positionInPixels = this.percentageToPixels(this.position);
this.emit('sl-reposition');
}

Wyświetl plik

@ -31,11 +31,8 @@ export default css`
outline: transparent;
}
:host(:focus-visible):not([disabled]) {
color: var(--sl-color-primary-600);
}
:host(:focus-visible) {
color: var(--sl-color-primary-600);
outline: var(--sl-focus-ring);
outline-offset: calc(-1 * var(--sl-focus-ring-width) - var(--sl-focus-ring-offset));
}

Wyświetl plik

@ -202,7 +202,7 @@ export default class SlTextarea extends ShoelaceElement implements ShoelaceFormC
this.input.style.height = 'auto';
this.input.style.height = `${this.input.scrollHeight}px`;
} else {
(this.input.style.height as string | undefined) = undefined;
this.input.style.height = '';
}
}

Wyświetl plik

@ -35,7 +35,7 @@ export const defaultValue =
if (name === attributeName) {
const converter = options.converter || defaultConverter;
const fromAttribute =
typeof converter === 'function' ? converter : converter?.fromAttribute ?? defaultConverter.fromAttribute;
typeof converter === 'function' ? converter : (converter?.fromAttribute ?? defaultConverter.fromAttribute);
const newValue: unknown = fromAttribute!(value, options.type);

Wyświetl plik

@ -0,0 +1,39 @@
import { registerTranslation } from '../utilities/localize.js';
import type { Translation } from '../utilities/localize.js';
const translation: Translation = {
$code: 'nb',
$name: 'Norwegian Bokmål',
$dir: 'ltr',
carousel: 'Karusell',
clearEntry: 'Tøm felt',
close: 'Lukk',
copied: 'Kopiert',
copy: 'Kopier',
currentValue: 'Nåværende verdi',
error: 'Feil',
goToSlide: (slide, count) => `Gå til visning ${slide} av ${count}`,
hidePassword: 'Skjul passord',
loading: 'Laster',
nextSlide: 'Neste visning',
numOptionsSelected: num => {
if (num === 0) return 'Ingen alternativer valgt';
if (num === 1) return 'Ett alternativ valgt';
return `${num} alternativer valgt`;
},
previousSlide: 'Forrige visning',
progress: 'Fremdrift',
remove: 'Fjern',
resize: 'Endre størrelse',
scrollToEnd: 'Rull til slutten',
scrollToStart: 'Rull til starten',
selectAColorFromTheScreen: 'Velg en farge fra skjermen',
showPassword: 'Vis passord',
slideNum: slide => `Visning ${slide}`,
toggleColorFormat: 'Bytt fargeformat'
};
registerTranslation(translation);
export default translation;

Wyświetl plik

@ -0,0 +1,39 @@
import { registerTranslation } from '../utilities/localize.js';
import type { Translation } from '../utilities/localize.js';
const translation: Translation = {
$code: 'nn',
$name: 'Norwegian Nynorsk',
$dir: 'ltr',
carousel: 'Karusell',
clearEntry: 'Tøm felt',
close: 'Lukk',
copied: 'Kopiert',
copy: 'Kopier',
currentValue: 'Nåverande verdi',
error: 'Feil',
goToSlide: (slide, count) => `Gå til visning ${slide} av ${count}`,
hidePassword: 'Gøym passord',
loading: 'Lastar',
nextSlide: 'Neste visning',
numOptionsSelected: num => {
if (num === 0) return 'Ingen alternativ valt';
if (num === 1) return 'Eitt alternativ valt';
return `${num} alternativ valt`;
},
previousSlide: 'Førre visning',
progress: 'Framdrift',
remove: 'Fjern',
resize: 'Endre storleik',
scrollToEnd: 'Rull til slutten',
scrollToStart: 'Rull til starten',
selectAColorFromTheScreen: 'Vel ein farge frå skjermen',
showPassword: 'Vis passord',
slideNum: slide => `Visning ${slide}`,
toggleColorFormat: 'Byt fargeformat'
};
registerTranslation(translation);
export default translation;

Wyświetl plik

@ -0,0 +1,41 @@
import { registerTranslation } from '../utilities/localize.js';
import type { Translation } from '../utilities/localize.js';
const translation: Translation = {
$code: 'uk',
$name: 'Українська',
$dir: 'ltr',
carousel: 'Карусель',
clearEntry: 'Очистити поле',
close: 'Закрити',
copied: 'Скопійовано',
copy: 'Скопіювати',
currentValue: 'Поточне значення',
error: 'Збій',
goToSlide: (slide, count) => `Перейти до слайда №${slide} з ${count}`,
hidePassword: 'Приховати пароль',
loading: 'Завантаження',
nextSlide: 'Наступний слайд',
numOptionsSelected: num => {
const n = num % 10;
if (n === 0) return 'не вибрано варіантів';
if (n === 1) return 'вибрано 1 варіант';
if (n === 2 || n === 3 || n === 4) return `вибрано ${num} варіанти`;
return `вибрано ${num} варіантів`;
},
previousSlide: 'Попередній слайд',
progress: 'Поступ',
remove: 'Видалити',
resize: 'Змінити розмір',
scrollToEnd: 'Прокрутити в кінець',
scrollToStart: 'Прокрутити на початок',
selectAColorFromTheScreen: 'Виберіть колір на екрані',
showPassword: 'Показати пароль',
slideNum: slide => `Слайд ${slide}`,
toggleColorFormat: 'Переключити кольорову модель'
};
registerTranslation(translation);
export default translation;