mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-19 07:29:14 +00:00
Compare commits
49 Commits
native-cod
...
konnorroge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c094c1e95b | ||
|
|
24c9922251 | ||
|
|
b7a0820e1f | ||
|
|
5646f6992d | ||
|
|
b2f47885e2 | ||
|
|
39d4d7480d | ||
|
|
f54cd3f8f0 | ||
|
|
ecdf3dff6d | ||
|
|
a10ee8b70f | ||
|
|
fb899ad676 | ||
|
|
7390ac37bf | ||
|
|
2e2cee4cd6 | ||
|
|
38a865ef9c | ||
|
|
55a2f29800 | ||
|
|
5e3c793974 | ||
|
|
05214a2887 | ||
|
|
8317cb38e4 | ||
|
|
ca539055eb | ||
|
|
5d55d0592e | ||
|
|
92ff4fc950 | ||
|
|
12e9ee836f | ||
|
|
6b51d4a8a0 | ||
|
|
e3e09499a2 | ||
|
|
b4badae867 | ||
|
|
d93f775106 | ||
|
|
37e5a6eae4 | ||
|
|
c0d449de03 | ||
|
|
543ce8997f | ||
|
|
abc89ef983 | ||
|
|
c1bfaed14f | ||
|
|
38754d96cb | ||
|
|
85558b0e42 | ||
|
|
0c5f14abc6 | ||
|
|
6359367706 | ||
|
|
25a7a9428d | ||
|
|
e01341fec5 | ||
|
|
57788ff92a | ||
|
|
a915fdd463 | ||
|
|
8f1de40825 | ||
|
|
7947c00605 | ||
|
|
4a2ef8315c | ||
|
|
405da0d135 | ||
|
|
475d690751 | ||
|
|
b24a148c3c | ||
|
|
c61d38b0c9 | ||
|
|
8ba3f07bd2 | ||
|
|
205a82333d | ||
|
|
3af145c00c | ||
|
|
7c0e9dad8c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ docs/search.json
|
||||
dist
|
||||
node_modules
|
||||
src/react
|
||||
cdn/
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"autoplay",
|
||||
"bezier",
|
||||
"boxicons",
|
||||
"CACHEABLE",
|
||||
"callout",
|
||||
"callouts",
|
||||
"chatbubble",
|
||||
@@ -112,6 +113,7 @@
|
||||
"resizer",
|
||||
"resizers",
|
||||
"retargeted",
|
||||
"RETRYABLE",
|
||||
"rgba",
|
||||
"roadmap",
|
||||
"Roboto",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[component-header:sl-menu]
|
||||
|
||||
You can use [menu items](/components/menu-item), [menu labels](/components/menu-label), and [dividers](/components/divider) to compose a menu. Menus support keyboard interactions, including type-to-select an option.
|
||||
You can use [menu items](/components/menu-item), [menu labels](/components/menu-label), and [dividers](/components/divider) to compose a menu.
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px;">
|
||||
|
||||
@@ -78,29 +78,29 @@ const App = () => (
|
||||
|
||||
### Sizes
|
||||
|
||||
Use the `size` attribute to change a radio button's size.
|
||||
Add the `size` attribute to the [Radio Group](/components/radio-group) to change the size of the radio buttons.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio-button size="small" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button size="small" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button size="small" value="3">Option 3</sl-radio-button>
|
||||
<sl-radio-group label="Select an option" size="small" value="1">
|
||||
<sl-radio-button value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio-button size="medium" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button size="medium" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button size="medium" value="3">Option 3</sl-radio-button>
|
||||
<sl-radio-group label="Select an option" size="medium" value="1">
|
||||
<sl-radio-button value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio-button size="large" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button size="large" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button size="large" value="3">Option 3</sl-radio-button>
|
||||
<sl-radio-group label="Select an option" size="large" value="1">
|
||||
<sl-radio-button value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
@@ -108,27 +108,29 @@ Use the `size` attribute to change a radio button's size.
|
||||
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadioButton size="small" value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton size="small" value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton size="small" value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
<>
|
||||
<SlRadioGroup label="Select an option" size="small" value="1">
|
||||
<SlRadioButton value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadioButton size="medium" value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton size="medium" value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton size="medium" value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
<SlRadioGroup label="Select an option" size="medium" value="1">
|
||||
<SlRadioButton value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadioButton size="large" value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton size="large" value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton size="large" value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
<SlRadioGroup label="Select an option" size="large" value="1">
|
||||
<SlRadioButton value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
@@ -98,6 +98,53 @@ const App = () => (
|
||||
);
|
||||
```
|
||||
|
||||
### Sizing Options
|
||||
|
||||
The size of [Radios](/components/radio) and [Radio Buttons](/components/radio-buttons) will be determined by the Radio Group's `size` attribute.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" size="medium" value="medium" class="radio-group-size">
|
||||
<sl-radio value="small">Small</sl-radio>
|
||||
<sl-radio value="medium">Medium</sl-radio>
|
||||
<sl-radio value="large">Large</sl-radio>
|
||||
</sl-radio-group>
|
||||
|
||||
<script>
|
||||
const radioGroup = document.querySelector('.radio-group-size');
|
||||
|
||||
radioGroup.addEventListener('sl-change', () => {
|
||||
radioGroup.size = radioGroup.value;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [size, setSize] = useState('medium');
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlRadioGroup
|
||||
label="Select an option"
|
||||
size={size}
|
||||
value={size}
|
||||
class="radio-group-size"
|
||||
onSlChange={event => setSize(event.target.value)}
|
||||
>
|
||||
<SlRadio value="small">Small</SlRadio>
|
||||
<SlRadio value="medium">Medium</SlRadio>
|
||||
<SlRadio value="large">Large</SlRadio>
|
||||
</SlRadioGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
?> [Radios](/components/radio) and [Radio Buttons](/components/radio-button) also have a `size` attribute. This can be useful in certain compositions, but it will be ignored when used inside of a Radio Group.
|
||||
|
||||
### Validation
|
||||
|
||||
Setting the `required` attribute to make selecting an option mandatory. If a value has not been selected, it will prevent the form from submitting and display an error message.
|
||||
|
||||
@@ -80,12 +80,30 @@ const App = () => (
|
||||
|
||||
## Sizes
|
||||
|
||||
Use the `size` attribute to change a radio's size.
|
||||
Add the `size` attribute to the [Radio Group](/components/radio-group) to change the size of the radios.
|
||||
|
||||
```html preview
|
||||
<sl-radio size="small">Small</sl-radio>
|
||||
<sl-radio size="medium">Medium</sl-radio>
|
||||
<sl-radio size="large">Large</sl-radio>
|
||||
<sl-radio-group label="Select an option" size="small" value="1">
|
||||
<sl-radio value="1">Small 1</sl-radio>
|
||||
<sl-radio value="2">Small 2</sl-radio>
|
||||
<sl-radio value="3">Small 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-radio-group label="Select an option" size="medium" value="1">
|
||||
<sl-radio value="1">Medium 1</sl-radio>
|
||||
<sl-radio value="2">Medium 2</sl-radio>
|
||||
<sl-radio value="3">Medium 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-radio-group label="Select an option" size="large" value="1">
|
||||
<sl-radio value="1">Large 1</sl-radio>
|
||||
<sl-radio value="2">Large 2</sl-radio>
|
||||
<sl-radio value="3">Large 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
@@ -93,11 +111,27 @@ import { SlRadio } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlRadio size="small">Small</SlRadio>
|
||||
<SlRadioGroup size="small" value="1">
|
||||
<SlRadio value="1">Small 1</SlRadio>
|
||||
<SlRadio value="2">Small 2</SlRadio>
|
||||
<SlRadio value="3">Small 3</SlRadio>
|
||||
</SlRadioGroup>
|
||||
|
||||
<br />
|
||||
<SlRadio size="medium">Medium</SlRadio>
|
||||
|
||||
<SlRadioGroup size="medium" value="1">
|
||||
<SlRadio value="1">Medium 1</SlRadio>
|
||||
<SlRadio value="2">Medium 2</SlRadio>
|
||||
<SlRadio value="3">Medium 3</SlRadio>
|
||||
</SlRadioGroup>
|
||||
|
||||
<br />
|
||||
<SlRadio size="large">Large</SlRadio>
|
||||
|
||||
<SlRadioGroup size="large" value="1">
|
||||
<SlRadio value="1">Large 1</SlRadio>
|
||||
<SlRadio value="2">Large 2</SlRadio>
|
||||
<SlRadio value="3">Large 3</SlRadio>
|
||||
</SlRadioGroup>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
@@ -10,10 +10,30 @@ To add Shoelace to your Angular app, install the package from npm.
|
||||
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.
|
||||
Next, [include a theme](/getting-started/themes) into your project. In this example, we'll import the light theme and use the CDN as a base path.
|
||||
|
||||
Include the theme into your `angular.json` config.
|
||||
|
||||
```json
|
||||
...
|
||||
"styles": [
|
||||
"src/app/theme/styles.scss",
|
||||
"@shoelace-style/shoelace/dist/themes/light.css"
|
||||
],
|
||||
...
|
||||
```
|
||||
|
||||
OR
|
||||
include it into your `styles.scss`.
|
||||
|
||||
```scss
|
||||
@use "@shoelace-style/shoelace/dist/themes/light.css";
|
||||
...
|
||||
```
|
||||
|
||||
After that set the [base path](/getting-started/installation#setting-the-base-path) in your `AppModule` for icons and other assets.
|
||||
|
||||
```jsx
|
||||
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%/dist/');
|
||||
@@ -31,6 +51,10 @@ import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path';
|
||||
|
||||
setBasePath('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/');
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [BrowserModule],
|
||||
@@ -48,7 +72,8 @@ import { SlDrawer } from '@shoelace-style/shoelace';
|
||||
|
||||
@Component({
|
||||
selector: 'app-drawer-example',
|
||||
template: '<div id="page"><button (click)="showDrawer()">Show drawer</button><sl-drawer #drawer label="Drawer" class="drawer-focus" style="--size: 50vw"><p>Drawer content</p></sl-drawer></div>'
|
||||
template: '<div id="page"><button (click)="showDrawer()">Show drawer</button><sl-drawer #drawer label="Drawer" class="drawer-focus" style="--size: 50vw"><p>Drawer content</p></sl-drawer></div>',
|
||||
providers: [SlDrawer]
|
||||
})
|
||||
export class DrawerExampleComponent implements OnInit {
|
||||
|
||||
|
||||
@@ -8,17 +8,38 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
||||
|
||||
## Next
|
||||
|
||||
- Added the `checkbox` part and related exported parts to `<sl-tree-item>` so you can target it with CSS [#1318](https://github.com/shoelace-style/shoelace/discussions/1318)
|
||||
- Added the `submenu-icon` part to `<sl-menu-item>` (submenus have not been implemented yet, but this part is required to allow customizations)
|
||||
- Added tests for `<sl-split-panel>` [#1343](https://github.com/shoelace-style/shoelace/pull/1343)
|
||||
- Fixed a bug where changing the size of `<sl-radio-group>` wouldn't update the size of child elements
|
||||
- Fixed a bug in `<sl-select>` and `<sl-color-picker>` where the `size` attribute wasn't being reflected [#1318](https://github.com/shoelace-style/shoelace/issues/1348)
|
||||
- Improved `<sl-button>` so it can accept children of variable heights [#1317](https://github.com/shoelace-style/shoelace/pull/1317)
|
||||
- Improved the docs to more clearly explain sizing radios and radio buttons
|
||||
- Improved the performance of `<sl-rating>` by partially rendering unseen icons [#1310](https://github.com/shoelace-style/shoelace/pull/1310)
|
||||
- Improved the Portuguese translation [#1336](https://github.com/shoelace-style/shoelace/pull/1336)
|
||||
- Improved the German translation [#1339](https://github.com/shoelace-style/shoelace/pull/1339)
|
||||
- Improved the autoloader so it watches `<html>` instead of `<body>` since the latter gets replaced by some frameworks [#1338](https://github.com/shoelace-style/shoelace/pull/1338)
|
||||
- Improved the Rails documentation [#1258](https://github.com/shoelace-style/shoelace/pull/1258)
|
||||
|
||||
## 2.4.0
|
||||
|
||||
- Added the `discover()` function to the experimental autoloader's exports [#1236](https://github.com/shoelace-style/shoelace/pull/1236)
|
||||
- Added more tests for `<sl-animated-image>` [#1246](https://github.com/shoelace-style/shoelace/pull/1246)
|
||||
- Added the `size` attribute to `<sl-radio-group>` so labels and controls will be sized consistently [#1301](https://github.com/shoelace-style/shoelace/issues/1301)
|
||||
- Added tests for `<sl-animated-image>` [#1246](https://github.com/shoelace-style/shoelace/pull/1246)
|
||||
- Added tests for `<sl-animation>` [#1274](https://github.com/shoelace-style/shoelace/pull/1274)
|
||||
- Fixed a bug in `<sl-tree-item>` that prevented long labels from wrapping [#1243](https://github.com/shoelace-style/shoelace/issues/1243)
|
||||
- Fixed a bug in `<sl-tree-item>` that caused labels to be misaligned when text wraps [#1244](https://github.com/shoelace-style/shoelace/issues/1244)
|
||||
- Fixed an incorrect CSS property value in `<sl-checkbox>` [#1272](https://github.com/shoelace-style/shoelace/pull/1272)
|
||||
- Fixed a bug in `<sl-avatar>` that caused the initials to show up behind images with transparency [#1260](https://github.com/shoelace-style/shoelace/pull/1260)
|
||||
- Fixed a bug in `<sl-split-panel>` that prevented the divider from being focusable in some browsers [#1288](https://github.com/shoelace-style/shoelace/issues/1288)
|
||||
- Fixed a bug that caused `<sl-tab-group>` to affect scrolling when initializing [#1292](https://github.com/shoelace-style/shoelace/issues/1292)
|
||||
- Fixed a bug in `<sl-menu-item>` that allowed the hover state to show when focused [#1282](https://github.com/shoelace-style/shoelace/issues/1282)
|
||||
- Fixed a bug in `<sl-carousel>` that prevented interactive elements from receiving clicks [#1262](https://github.com/shoelace-style/shoelace/issues/1262)
|
||||
- Fixed a bug in `<sl-input>` that caused `valueAsDate` and `valueAsNumber` to not be set synchronously in some cases [#1302](https://github.com/shoelace-style/shoelace/issues/1302)
|
||||
- Improved the behavior of `<sl-carousel>` when used inside a flex container [#1235](https://github.com/shoelace-style/shoelace/pull/1235)
|
||||
- Improved the behavior of `<sl-tree-item>` to support buttons and other interactive elements [#1234](https://github.com/shoelace-style/shoelace/issues/1234)
|
||||
- Improved the performance of `<sl-include>` to prevent an apparent memory leak in some browsers [#1284](https://github.com/shoelace-style/shoelace/pull/1284)
|
||||
- Improved the accessibility of `<sl-select>`, `<sl-split-panel>`, and `<sl-details>` by ensuring slots don't have roles [#1287](https://github.com/shoelace-style/shoelace/issues/1287)
|
||||
|
||||
## 2.3.0
|
||||
|
||||
|
||||
@@ -345,12 +345,10 @@ For non-dependencies, _the user_ should decide what gets registered, even if it
|
||||
|
||||
### Form Controls
|
||||
|
||||
Form controls should support submission and validation through the following conventions:
|
||||
Form controls should support submission and validation through the following conventions by implementing the `ShoelaceFormControl` interface:
|
||||
|
||||
- All form controls must use `name`, `value`, and `disabled` properties in the same manner as `HTMLInputElement`
|
||||
- All form controls must have a `setCustomValidity()` method so the user can set a custom validation message
|
||||
- All form controls must have a `reportValidity()` method that report their validity during form submission
|
||||
- All form controls must have an `invalid` property that reflects their validity
|
||||
- All form controls must use `name`, `value`, `disabled`, etc. in the same manner as `HTMLInputElement`
|
||||
- All form controls must implement `checkValidity()`, `reportValidity()`, `setCustomValidity()`, etc. for validation
|
||||
- All form controls should mirror their native validation attributes such as `required`, `pattern`, `minlength`, `maxlength`, etc. when possible
|
||||
- All form controls must be tested to work with the standard `<form>` element
|
||||
|
||||
@@ -369,7 +367,7 @@ This will render the icons instantly whereas the default library will fetch them
|
||||
What to test for a given component:
|
||||
|
||||
- Start with a simple test that checks that the default version of the component still renders.
|
||||
- Add at least one accessibility test:
|
||||
- Add at least one accessibility test (The accessibility check only covers the parts of the DOM which are currently visible and rendered. Depending on the component, more than one accessibility test is required to cover all scenarios.):
|
||||
|
||||
```ts
|
||||
const myComponent = await fixture<SlAlert>(html`<sl-my-component>SomeContent</sl-my-component>`);
|
||||
|
||||
@@ -10,92 +10,148 @@ This integration has been tested with the following:
|
||||
|
||||
- Rails >= 6
|
||||
- Node >= 12.10
|
||||
- Webpacker >= 5
|
||||
- Yarn >= 1.22
|
||||
|
||||
## Instructions
|
||||
|
||||
To get started using Shoelace with Rails, the following packages must be installed.
|
||||
When using Shoelace, there are mostly three things that need to be served to the client browser:
|
||||
|
||||
- Javascript files for the Web Components
|
||||
- CSS files for light and dark themes (they can co-exist, but one of them is required)
|
||||
- Shoelace Icons
|
||||
|
||||
Depending on the JS bundler you are using, you may need to do some additional configuration. However, the basic steps
|
||||
should be just about the same. Also, it is recommended to read to the [Bundling section in the Installation](/getting-started/installation?id=bundling)
|
||||
to understand how Shoelace can be set up with a JS bundler in general. In this tutorial, we will assume that your Rails app
|
||||
is already set up with a JS bundler that supports importing CSS files directly (e.g. Turbopack, esbuild, Vite).
|
||||
|
||||
To get started using Shoelace with Rails, the following package must be installed.
|
||||
|
||||
```bash
|
||||
yarn add @shoelace-style/shoelace copy-webpack-plugin
|
||||
yarn add @shoelace-style/shoelace
|
||||
```
|
||||
|
||||
### Importing the Default Theme
|
||||
This is required regardless of the JS bundler you are using.
|
||||
|
||||
The next step is to import Shoelace's default theme (stylesheet) in `app/javascript/stylesheets/application.scss`.
|
||||
### Javascript & CSS
|
||||
|
||||
The next step is to import the JavaScript files and default theme for Shoelace. Add the following code to your
|
||||
entrypoint JS file (generally `application.js`).
|
||||
|
||||
```js
|
||||
// application.js
|
||||
import '@shoelace-style/shoelace';
|
||||
|
||||
// You can also add these two if the JS bundler of your choice supports importing CSS files.
|
||||
import '@shoelace-style/shoelace/dist/themes/light.css';
|
||||
import '@shoelace-style/shoelace/dist/themes/dark.css'; // Optional dark mode
|
||||
```
|
||||
|
||||
!> In this example, all Shoelace components are imported for simplicity. However, importing directly from
|
||||
`@shoelace-style/shoelace` may result in a larger bundle size than necessary. Consider importing only the components
|
||||
you actually need in the actual application.
|
||||
|
||||
You can also import the CSS inside of a `.css` file if you prefer to maintain a separate CSS entrypoint.
|
||||
Such as with CssBundling-Rails.
|
||||
|
||||
```css
|
||||
@import '@shoelace-style/shoelace/dist/themes/light';
|
||||
@import '@shoelace-style/shoelace/dist/themes/dark'; // Optional dark theme
|
||||
// application.css
|
||||
@import '@shoelace-style/shoelace/dist/themes/light.css';
|
||||
```
|
||||
|
||||
Fore more details about themes, please refer to [Theme Basics](/getting-started/themes?id=theme-basics).
|
||||
For more details about themes, please refer to [Theme Basics](/getting-started/themes?id=theme-basics).
|
||||
|
||||
### Importing Required Scripts
|
||||
### Serving up Shoelace Icons
|
||||
|
||||
After importing the theme, you'll need to import the JavaScript files for Shoelace. Add the following code to `app/javascript/packs/application.js`.
|
||||
#### Using the `shoelace-rails` gem
|
||||
|
||||
```js
|
||||
import '../stylesheets/application.scss'
|
||||
import { setBasePath, SlAlert, SlAnimation, SlButton, ... } from '@shoelace-style/shoelace'
|
||||
You do not have to do anything else if you are using [the `shoelace-rails` gem](https://github.com/yuki24/shoelace-rails).
|
||||
Here are how it works in different environments:
|
||||
|
||||
// ...
|
||||
- In development and test, the icons are served by the `ActionDispatch::Static` middleware, directly from the
|
||||
`node_modules/@shoelace-style/shoelace/dist/assets/icons` directory.
|
||||
- In production, the icon files are automatically copied into the `public/assets` directory as part of the
|
||||
`assets:precompile` rake task.
|
||||
|
||||
const rootUrl = document.currentScript.src.replace(/\/packs.*$/, '')
|
||||
#### Copying Icon files with a Rake task
|
||||
|
||||
// Path to the assets folder (should be independent from the current script source path
|
||||
// to work correctly in different environments)
|
||||
setBasePath(rootUrl + '/packs/js/')
|
||||
```
|
||||
If you are not using the `shoelace-rails` gem, you can manually copy the icon files to the `public/assets` directory.
|
||||
One way to do this is to use a rake task and add it as a dependency to the `assets:precompile` task. Most rails
|
||||
deployment processes run the `rake assets:precompile` task as of part deply, which means that the icon files will be
|
||||
copied automatically.
|
||||
|
||||
### webpack Config
|
||||
```ruby
|
||||
# Rakefile
|
||||
namespace :shoelace do
|
||||
namespace :icons do
|
||||
desc "Copy Shoelace icons to the assets path"
|
||||
task copy: :environment do
|
||||
cp_r "node_modules/@shoelace-style/shoelace/dist/assets", Rails.public_path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Next we need to add Shoelace's assets to the final build output. To do this, modify `config/webpack/environment.js` to look like this.
|
||||
|
||||
```js
|
||||
const { environment } = require('@rails/webpacker');
|
||||
|
||||
// Shoelace config
|
||||
const path = require('path');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
// Add shoelace assets to webpack's build process
|
||||
environment.plugins.append(
|
||||
'CopyPlugin',
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.resolve(__dirname, '../../node_modules/@shoelace-style/shoelace/dist/assets'),
|
||||
to: path.resolve(__dirname, '../../public/packs/js/assets')
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
module.exports = environment;
|
||||
```
|
||||
|
||||
### Adding Pack Tags
|
||||
|
||||
The final step is to add the corresponding `pack_tags` to the page. You should have the following `tags` in the `<head>` section of `app/views/layouts/application.html.erb`.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- ... -->
|
||||
|
||||
<%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag
|
||||
'application', 'data-turbolinks-track': 'reload' %>
|
||||
</head>
|
||||
<body>
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
Rake::Task["assets:precompile"].enhance(["shoelace:icons:copy"])
|
||||
```
|
||||
|
||||
Now you can start using Shoelace components with Rails!
|
||||
|
||||
## Using Rails View Helpers
|
||||
|
||||
[the `shoelace-rails` gem](https://github.com/yuki24/shoelace-rails) is a community-maintained library that provides useful Rake tasks and Rails view helpers for
|
||||
Shoelace components. In order to use it, add the gem by running the following command:
|
||||
|
||||
```bash
|
||||
bundle add shoelace-rails
|
||||
```
|
||||
|
||||
Once it is installed, you should be able to use the following view helpers to render Shoelace components:
|
||||
|
||||
```erb
|
||||
<%= sl_form_for @user do |form| %>
|
||||
<% # Text input: https://shoelace.style/components/input %>
|
||||
<%= form.text_field :name %>
|
||||
<%= form.password_field :password, placeholder: "Password Toggle", 'toggle-password': true %>
|
||||
|
||||
<% # Radio buttons: https://shoelace.style/components/color-picker %>
|
||||
<%= form.color_field :color %>
|
||||
|
||||
<% # Radio buttons: https://shoelace.style/components/radio %>
|
||||
<%= form.collection_radio_buttons :status, { id_1: "Option 1", id_2: "Option 2", id_3: "Option 3" }, :first, :last %>
|
||||
|
||||
<% # Select: https://shoelace.style/components/select %>
|
||||
<%= form.collection_select :tag, { id_1: "Option 1", id_2: "Option 2", id_3: "Option 3" }, :first, :last, {}, { placeholder: "Select one" } %>
|
||||
|
||||
<%= form.submit %>
|
||||
<% end %>
|
||||
```
|
||||
|
||||
And this code will produce:
|
||||
|
||||
```html
|
||||
<form class="new_user" id="new_user" data-remote="true" action="/" accept-charset="UTF-8" method="post">
|
||||
<sl-input label="Name" type="text" name="user[name]" id="user_name"></sl-input>
|
||||
<sl-input label="Password" type="password" name="user[password]" id="user_password"></sl-input>
|
||||
<sl-color-picker value="#ffffff" name="user[color]" id="user_color"></sl-color-picker>
|
||||
|
||||
<sl-radio-group no-fieldset="true">
|
||||
<sl-radio value="id_1" name="user[status]" id="user_status_id_1">Option 1</sl-radio>
|
||||
<sl-radio value="id_2" name="user[status]" id="user_status_id_2">Option 2</sl-radio>
|
||||
<sl-radio value="id_3" name="user[status]" id="user_status_id_3">Option 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
|
||||
<sl-select placeholder="Select one" name="user[tag]" id="user_tag">
|
||||
<sl-menu-item value="id_1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="id_2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="id_3">Option 3</sl-menu-item>
|
||||
</sl-select>
|
||||
|
||||
<sl-button submit="true" type="primary" data-disable-with="Create User">Create User</sl-button>
|
||||
</form>
|
||||
```
|
||||
|
||||
For more details about the gem, please refer to [the official README](https://github.com/yuki24/shoelace-rails).
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- There is a third-party [example repo](https://github.com/ParamagicDev/rails-shoelace-example), courtesy of [ParamagicDev](https://github.com/ParamagicDev) available to help you get started.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.5.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"description": "A forward-thinking library of web components.",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0",
|
||||
"homepage": "https://github.com/shoelace-style/shoelace",
|
||||
"author": "Cory LaViska",
|
||||
"license": "MIT",
|
||||
@@ -23,7 +23,8 @@
|
||||
"./dist/translations/*": "./dist/translations/*"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"dist",
|
||||
"cdn"
|
||||
],
|
||||
"keywords": [
|
||||
"web components",
|
||||
|
||||
197
scripts/build.js
197
scripts/build.js
@@ -5,6 +5,7 @@ import commandLineArgs from 'command-line-args';
|
||||
import { deleteSync } from 'del';
|
||||
import esbuild from 'esbuild';
|
||||
import fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import getPort, { portNumbers } from 'get-port';
|
||||
import { globby } from 'globby';
|
||||
import copy from 'recursive-copy';
|
||||
@@ -18,83 +19,105 @@ const { bundle, copydir, dir, serve, types } = commandLineArgs([
|
||||
]);
|
||||
|
||||
const outdir = dir;
|
||||
const cdnDir = 'cdn';
|
||||
|
||||
deleteSync(outdir);
|
||||
fs.mkdirSync(outdir, { recursive: true });
|
||||
const outputDirectories = [cdnDir, outdir];
|
||||
|
||||
outputDirectories.forEach(dir => {
|
||||
deleteSync(dir);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
});
|
||||
(async () => {
|
||||
try {
|
||||
execSync(`node scripts/make-metadata.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-search.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-react.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-web-types.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-themes.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-icons.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
if (types) {
|
||||
console.log('Running the TypeScript compiler...');
|
||||
execSync(`tsc --project ./tsconfig.prod.json --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
}
|
||||
outputDirectories.forEach(dir => {
|
||||
execSync(`node scripts/make-metadata.js --outdir "${dir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-search.js --outdir "${dir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-react.js --outdir "${dir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-web-types.js --outdir "${dir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-themes.js --outdir "${dir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-icons.js --outdir "${dir}"`, { stdio: 'inherit' });
|
||||
|
||||
if (types) {
|
||||
console.log('Running the TypeScript compiler...');
|
||||
execSync(`tsc --project ./tsconfig.prod.json --outdir "${dir}"`, { stdio: 'inherit' });
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(chalk.red(err));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const alwaysExternal = ['@lit-labs/react', 'react'];
|
||||
const buildResult = await esbuild
|
||||
.build({
|
||||
format: 'esm',
|
||||
target: 'es2017',
|
||||
entryPoints: [
|
||||
//
|
||||
// NOTE: Entry points must be mapped in package.json > exports, otherwise users won't be able to import them!
|
||||
//
|
||||
// The whole shebang
|
||||
'./src/shoelace.ts',
|
||||
// The auto-loader
|
||||
'./src/shoelace-autoloader.ts',
|
||||
// Components
|
||||
...(await globby('./src/components/**/!(*.(style|test)).ts')),
|
||||
// Translations
|
||||
...(await globby('./src/translations/**/*.ts')),
|
||||
// Public utilities
|
||||
...(await globby('./src/utilities/**/!(*.(style|test)).ts')),
|
||||
// Theme stylesheets
|
||||
...(await globby('./src/themes/**/!(*.test).ts')),
|
||||
// React wrappers
|
||||
...(await globby('./src/react/**/*.ts'))
|
||||
],
|
||||
outdir,
|
||||
chunkNames: 'chunks/[name].[hash]',
|
||||
incremental: serve,
|
||||
define: {
|
||||
// Floating UI requires this to be set
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
},
|
||||
bundle: true,
|
||||
const bundledConfig = {
|
||||
format: 'esm',
|
||||
target: 'es2017',
|
||||
entryPoints: [
|
||||
//
|
||||
// We don't bundle certain dependencies in the unbundled build. This ensures we ship bare module specifiers,
|
||||
// allowing end users to better optimize when using a bundler. (Only packages that ship ESM can be external.)
|
||||
// NOTE: Entry points must be mapped in package.json > exports, otherwise users won't be able to import them!
|
||||
//
|
||||
// We never bundle React or @lit-labs/react though!
|
||||
//
|
||||
external: bundle
|
||||
? alwaysExternal
|
||||
: [...alwaysExternal, '@floating-ui/dom', '@shoelace-style/animations', 'lit', 'qr-creator'],
|
||||
splitting: true,
|
||||
plugins: []
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(chalk.red(err));
|
||||
process.exit(1);
|
||||
});
|
||||
// The whole shebang
|
||||
'./src/shoelace.ts',
|
||||
// The auto-loader
|
||||
'./src/shoelace-autoloader.ts',
|
||||
// Components
|
||||
...(await globby('./src/components/**/!(*.(style|test)).ts')),
|
||||
// Translations
|
||||
...(await globby('./src/translations/**/*.ts')),
|
||||
// Public utilities
|
||||
...(await globby('./src/utilities/**/!(*.(style|test)).ts')),
|
||||
// Theme stylesheets
|
||||
...(await globby('./src/themes/**/!(*.test).ts')),
|
||||
// React wrappers
|
||||
...(await globby('./src/react/**/*.ts'))
|
||||
],
|
||||
chunkNames: 'chunks/[name].[hash]',
|
||||
incremental: serve,
|
||||
define: {
|
||||
// Floating UI requires this to be set
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
},
|
||||
bundle: true,
|
||||
outdir: cdnDir,
|
||||
//
|
||||
// We don't bundle certain dependencies in the unbundled build. This ensures we ship bare module specifiers,
|
||||
// allowing end users to better optimize when using a bundler. (Only packages that ship ESM can be external.)
|
||||
//
|
||||
// We never bundle React or @lit-labs/react though!
|
||||
//
|
||||
external: bundle
|
||||
? alwaysExternal
|
||||
: [...alwaysExternal, '@floating-ui/dom', '@shoelace-style/animations', 'lit', 'qr-creator'],
|
||||
splitting: true,
|
||||
plugins: []
|
||||
};
|
||||
|
||||
const packageJSON = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json')));
|
||||
const unbundledConfig = {
|
||||
...bundledConfig,
|
||||
// Goes to /dist/npm
|
||||
outdir,
|
||||
target: 'esnext',
|
||||
external: [...Object.keys(packageJSON.dependencies || {}), ...Object.keys(packageJSON.peerDependencies || {})]
|
||||
};
|
||||
|
||||
const unbundledResult = await esbuild.build(unbundledConfig).catch(err => {
|
||||
console.error(chalk.red(err));
|
||||
console.error(chalk.red('\nFailed to build NPM (unbundled) build'));
|
||||
process.exit(1);
|
||||
});
|
||||
const bundledResult = await esbuild.build(bundledConfig).catch(err => {
|
||||
console.error(chalk.red(err));
|
||||
console.error(chalk.red('\nFailed to build CDN (bundled) build'));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Copy the build output to an additional directory
|
||||
if (copydir) {
|
||||
deleteSync(copydir);
|
||||
copy(outdir, copydir);
|
||||
copy(cdnDir, copydir);
|
||||
}
|
||||
|
||||
console.log(chalk.green(`The build has been generated at ${outdir} 📦\n`));
|
||||
console.log(chalk.green(`The build has been generated to: ${outputDirectories.join(', ')} 📦\n`));
|
||||
|
||||
// Dev server
|
||||
if (serve) {
|
||||
@@ -118,7 +141,7 @@ fs.mkdirSync(outdir, { recursive: true });
|
||||
server: {
|
||||
baseDir: 'docs',
|
||||
routes: {
|
||||
'/dist': './dist'
|
||||
'/dist': './cdn'
|
||||
}
|
||||
},
|
||||
//
|
||||
@@ -148,37 +171,49 @@ fs.mkdirSync(outdir, { recursive: true });
|
||||
// Rebuild and reload when source files change
|
||||
bs.watch(['src/**/!(*.test).*']).on('change', async filename => {
|
||||
console.log(`Source file changed - ${filename}`);
|
||||
buildResult
|
||||
[bundledResult, unbundledResult].forEach(build => {
|
||||
// Rebuild and reload
|
||||
.rebuild()
|
||||
.then(() => {
|
||||
// Rebuild stylesheets when a theme file changes
|
||||
if (/^src\/themes/.test(filename)) {
|
||||
execSync(`node scripts/make-themes.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
// Skip metadata when styles are changed
|
||||
if (/(\.css|\.styles\.ts)$/.test(filename)) {
|
||||
return;
|
||||
}
|
||||
build
|
||||
.rebuild()
|
||||
.then(() => {
|
||||
// Rebuild stylesheets when a theme file changes
|
||||
if (/^src\/themes/.test(filename)) {
|
||||
outputDirectories.forEach(dir => {
|
||||
execSync(`node scripts/make-themes.js --outdir "${dir}"`, { stdio: 'inherit' });
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
// Skip metadata when styles are changed
|
||||
if (/(\.css|\.styles\.ts)$/.test(filename)) {
|
||||
return;
|
||||
}
|
||||
|
||||
execSync(`node scripts/make-metadata.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
})
|
||||
.then(() => {
|
||||
bs.reload();
|
||||
})
|
||||
.catch(err => console.error(chalk.red(err)));
|
||||
outputDirectories.forEach(dir => {
|
||||
execSync(`node scripts/make-metadata.js --outdir "${dir}"`, { stdio: 'inherit' });
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
bs.reload();
|
||||
})
|
||||
.catch(err => console.error(chalk.red(err)));
|
||||
});
|
||||
});
|
||||
|
||||
// Reload without rebuilding when the docs change
|
||||
bs.watch(['docs/**/*.md']).on('change', filename => {
|
||||
console.log(`Docs file changed - ${filename}`);
|
||||
execSync(`node scripts/make-search.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
|
||||
outputDirectories.forEach(dir => {
|
||||
execSync(`node scripts/make-search.js --outdir "${dir}"`, { stdio: 'inherit' });
|
||||
});
|
||||
bs.reload();
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup on exit
|
||||
process.on('SIGTERM', () => buildResult.rebuild.dispose());
|
||||
process.on('SIGTERM', () => {
|
||||
bundledResult.rebuild.dispose();
|
||||
unbundledResult.rebuild.dispose();
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -343,22 +343,25 @@ export default css`
|
||||
*/
|
||||
|
||||
.button--small {
|
||||
height: auto;
|
||||
min-height: var(--sl-input-height-small);
|
||||
font-size: var(--sl-button-font-size-small);
|
||||
height: var(--sl-input-height-small);
|
||||
line-height: calc(var(--sl-input-height-small) - var(--sl-input-border-width) * 2);
|
||||
border-radius: var(--sl-input-border-radius-small);
|
||||
}
|
||||
|
||||
.button--medium {
|
||||
height: auto;
|
||||
min-height: var(--sl-input-height-medium);
|
||||
font-size: var(--sl-button-font-size-medium);
|
||||
height: var(--sl-input-height-medium);
|
||||
line-height: calc(var(--sl-input-height-medium) - var(--sl-input-border-width) * 2);
|
||||
border-radius: var(--sl-input-border-radius-medium);
|
||||
}
|
||||
|
||||
.button--large {
|
||||
height: auto;
|
||||
min-height: var(--sl-input-height-large);
|
||||
font-size: var(--sl-button-font-size-large);
|
||||
height: var(--sl-input-height-large);
|
||||
line-height: calc(var(--sl-input-height-large) - var(--sl-input-border-width) * 2);
|
||||
border-radius: var(--sl-input-border-radius-large);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ export class ScrollController<T extends ScrollHost> implements ReactiveControlle
|
||||
@debounce(100)
|
||||
handleScrollEnd() {
|
||||
if (!this.pointers.size) {
|
||||
// If no pointer is active in the scroll area then the scroll has ended
|
||||
this.scrolling = false;
|
||||
this.host.scrollContainer.dispatchEvent(
|
||||
new CustomEvent('scrollend', {
|
||||
@@ -78,6 +79,7 @@ export class ScrollController<T extends ScrollHost> implements ReactiveControlle
|
||||
);
|
||||
this.host.requestUpdate();
|
||||
} else {
|
||||
// otherwise let's wait a bit more
|
||||
this.handleScrollEnd();
|
||||
}
|
||||
}
|
||||
@@ -87,35 +89,33 @@ export class ScrollController<T extends ScrollHost> implements ReactiveControlle
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollContainer = this.host.scrollContainer;
|
||||
this.pointers.add(event.pointerId);
|
||||
scrollContainer.setPointerCapture(event.pointerId);
|
||||
|
||||
if (this.mouseDragging && this.pointers.size === 1) {
|
||||
const canDrag = this.mouseDragging && !this.dragging && event.button === 0;
|
||||
if (canDrag) {
|
||||
event.preventDefault();
|
||||
scrollContainer.addEventListener('pointermove', this.handlePointerMove);
|
||||
|
||||
this.host.scrollContainer.addEventListener('pointermove', this.handlePointerMove);
|
||||
}
|
||||
}
|
||||
|
||||
handlePointerMove(event: PointerEvent) {
|
||||
const host = this.host;
|
||||
const scrollContainer = host.scrollContainer;
|
||||
|
||||
if (scrollContainer.hasPointerCapture(event.pointerId)) {
|
||||
if (!this.dragging) {
|
||||
this.handleDragStart();
|
||||
}
|
||||
const scrollContainer = this.host.scrollContainer;
|
||||
|
||||
const hasMoved = !!event.movementX || !!event.movementY;
|
||||
if (!this.dragging && hasMoved) {
|
||||
// Start dragging if it hasn't yet
|
||||
scrollContainer.setPointerCapture(event.pointerId);
|
||||
this.handleDragStart();
|
||||
} else if (scrollContainer.hasPointerCapture(event.pointerId)) {
|
||||
// Ignore pointers that we are not tracking
|
||||
this.handleDrag(event);
|
||||
}
|
||||
}
|
||||
|
||||
handlePointerUp(event: PointerEvent) {
|
||||
const host = this.host;
|
||||
const scrollContainer = host.scrollContainer;
|
||||
|
||||
this.pointers.delete(event.pointerId);
|
||||
scrollContainer.releasePointerCapture(event.pointerId);
|
||||
this.host.scrollContainer.releasePointerCapture(event.pointerId);
|
||||
|
||||
if (this.pointers.size === 0) {
|
||||
this.handleDragEnd();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { aTimeout, expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { clickOnElement } from '../../internal/test';
|
||||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
@@ -274,6 +274,58 @@ describe('<sl-checkbox>', () => {
|
||||
expect(focusSpy.called).to.equal(true);
|
||||
expect(el.shadowRoot!.activeElement).to.equal(checkbox);
|
||||
});
|
||||
|
||||
it('should not jump the page to the bottom when focusing a checkbox at the bottom of an element with overflow: auto;', async () => {
|
||||
// https://github.com/shoelace-style/shoelace/issues/1169
|
||||
const el = await fixture<HTMLDivElement>(html`
|
||||
<div style="display: flex; flex-direction: column; overflow: auto; max-height: 400px; gap: 8px;">
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
</div>
|
||||
;
|
||||
`);
|
||||
|
||||
const checkboxes = el.querySelectorAll<SlCheckbox>('sl-checkbox');
|
||||
const lastSwitch = checkboxes[checkboxes.length - 1];
|
||||
|
||||
expect(window.scrollY).to.equal(0);
|
||||
// Without these 2 timeouts, tests will pass unexpectedly in Safari.
|
||||
await aTimeout(10);
|
||||
lastSwitch.focus();
|
||||
await aTimeout(10);
|
||||
expect(window.scrollY).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('blur', () => {
|
||||
|
||||
@@ -140,7 +140,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
@property({ type: Boolean, reflect: true }) inline = false;
|
||||
|
||||
/** Determines the size of the color picker's trigger. This has no effect on inline color pickers. */
|
||||
@property() size: 'small' | 'medium' | 'large' = 'medium';
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
/** Removes the button that lets users toggle between format. */
|
||||
@property({ attribute: 'no-format-toggle', type: Boolean }) noFormatToggle = false;
|
||||
|
||||
@@ -6,6 +6,20 @@ import type SlHideEvent from '../../events/sl-hide';
|
||||
import type SlShowEvent from '../../events/sl-show';
|
||||
|
||||
describe('<sl-details>', () => {
|
||||
describe('accessibility', () => {
|
||||
it('should be accessible when closed', async () => {
|
||||
const details = await fixture<SlDetails>(html`<sl-details summary="Test"> Test text </sl-details>`);
|
||||
|
||||
await expect(details).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should be accessible when open', async () => {
|
||||
const details = await fixture<SlDetails>(html`<sl-details open summary="Test">Test text</sl-details>`);
|
||||
|
||||
await expect(details).to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be visible with the open attribute', async () => {
|
||||
const el = await fixture<SlDetails>(html`
|
||||
<sl-details open>
|
||||
|
||||
@@ -170,7 +170,7 @@ export default class SlDetails extends ShoelaceElement {
|
||||
'details--rtl': isRtl
|
||||
})}
|
||||
>
|
||||
<header
|
||||
<div
|
||||
part="header"
|
||||
id="header"
|
||||
class="details__header"
|
||||
@@ -192,10 +192,10 @@ export default class SlDetails extends ShoelaceElement {
|
||||
<sl-icon library="system" name=${isRtl ? 'chevron-left' : 'chevron-right'}></sl-icon>
|
||||
</slot>
|
||||
</span>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="details__body">
|
||||
<slot part="content" id="content" class="details__content" role="region" aria-labelledby="header"></slot>
|
||||
<div class="details__body" role="region" aria-labelledby="header">
|
||||
<slot part="content" id="content" class="details__content"></slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { getIconLibrary, unwatchIcon, watchIcon } from './library';
|
||||
import { html } from 'lit';
|
||||
import { requestIcon } from './request';
|
||||
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './icon.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
const CACHEABLE_ERROR = Symbol();
|
||||
const RETRYABLE_ERROR = Symbol();
|
||||
type SVGResult = SVGSVGElement | typeof RETRYABLE_ERROR | typeof CACHEABLE_ERROR;
|
||||
|
||||
let parser: DOMParser;
|
||||
const iconCache = new Map<string, Promise<SVGResult>>();
|
||||
|
||||
/**
|
||||
* @summary Icons are symbols that can be used to represent various options within an application.
|
||||
@@ -25,7 +27,37 @@ let parser: DOMParser;
|
||||
export default class SlIcon extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@state() private svg = '';
|
||||
/** Given a URL, this function returns the resulting SVG element or an appropriate error symbol. */
|
||||
private static async resolveIcon(url: string): Promise<SVGResult> {
|
||||
let fileData: Response;
|
||||
try {
|
||||
fileData = await fetch(url, { mode: 'cors' });
|
||||
if (!fileData.ok) return fileData.status === 410 ? CACHEABLE_ERROR : RETRYABLE_ERROR;
|
||||
} catch {
|
||||
return RETRYABLE_ERROR;
|
||||
}
|
||||
|
||||
try {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = await fileData.text();
|
||||
|
||||
const svg = div.firstElementChild;
|
||||
if (svg?.tagName?.toLowerCase() !== 'svg') return CACHEABLE_ERROR;
|
||||
|
||||
if (!parser) parser = new DOMParser();
|
||||
const doc = parser.parseFromString(svg.outerHTML, 'text/html');
|
||||
|
||||
const svgEl = doc.body.querySelector('svg');
|
||||
if (!svgEl) return CACHEABLE_ERROR;
|
||||
|
||||
svgEl.part.add('svg');
|
||||
return document.adoptNode(svgEl);
|
||||
} catch {
|
||||
return CACHEABLE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@state() private svg: SVGElement | null = null;
|
||||
|
||||
/** The name of the icon to draw. Available names depend on the icon library being used. */
|
||||
@property({ reflect: true }) name?: string;
|
||||
@@ -87,46 +119,42 @@ export default class SlIcon extends ShoelaceElement {
|
||||
const library = getIconLibrary(this.library);
|
||||
const url = this.getUrl();
|
||||
|
||||
// Create an instance of the DOM parser. We do it here instead of top-level to support SSR while maintaining a
|
||||
// single parser instance for optimal performance.
|
||||
if (!parser) {
|
||||
parser = new DOMParser();
|
||||
if (!url) {
|
||||
this.svg = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
try {
|
||||
const file = await requestIcon(url);
|
||||
if (url !== this.getUrl()) {
|
||||
// If the url has changed while fetching the icon, ignore this request
|
||||
return;
|
||||
} else if (file.ok) {
|
||||
const doc = parser.parseFromString(file.svg, 'text/html');
|
||||
const svgEl = doc.body.querySelector('svg');
|
||||
let iconResolver = iconCache.get(url);
|
||||
if (!iconResolver) {
|
||||
iconResolver = SlIcon.resolveIcon(url);
|
||||
iconCache.set(url, iconResolver);
|
||||
}
|
||||
|
||||
if (svgEl !== null) {
|
||||
svgEl.part.add('svg');
|
||||
library?.mutator?.(svgEl);
|
||||
this.svg = svgEl.outerHTML;
|
||||
this.emit('sl-load');
|
||||
} else {
|
||||
this.svg = '';
|
||||
this.emit('sl-error');
|
||||
}
|
||||
} else {
|
||||
this.svg = '';
|
||||
this.emit('sl-error');
|
||||
}
|
||||
} catch {
|
||||
const svg = await iconResolver;
|
||||
if (svg === RETRYABLE_ERROR) {
|
||||
iconCache.delete(url);
|
||||
}
|
||||
|
||||
if (url !== this.getUrl()) {
|
||||
// If the url has changed while fetching the icon, ignore this request
|
||||
return;
|
||||
}
|
||||
|
||||
switch (svg) {
|
||||
case RETRYABLE_ERROR:
|
||||
case CACHEABLE_ERROR:
|
||||
this.svg = null;
|
||||
this.emit('sl-error');
|
||||
}
|
||||
} else if (this.svg.length > 0) {
|
||||
// If we can't resolve a URL and an icon was previously set, remove it
|
||||
this.svg = '';
|
||||
break;
|
||||
default:
|
||||
this.svg = svg.cloneNode(true) as SVGElement;
|
||||
library?.mutator?.(this.svg);
|
||||
this.emit('sl-load');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` ${unsafeSVG(this.svg)} `;
|
||||
return this.svg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { requestInclude } from '../include/request';
|
||||
|
||||
type IconFile =
|
||||
| {
|
||||
ok: true;
|
||||
status: number;
|
||||
svg: string;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
status: number;
|
||||
svg: null;
|
||||
};
|
||||
|
||||
interface IconFileUnknown {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
svg: string | null;
|
||||
}
|
||||
|
||||
const iconFiles = new Map<string, IconFile>();
|
||||
|
||||
export async function requestIcon(url: string): Promise<IconFile> {
|
||||
if (iconFiles.has(url)) {
|
||||
return iconFiles.get(url)!;
|
||||
}
|
||||
const fileData = await requestInclude(url);
|
||||
const iconFileData: IconFileUnknown = {
|
||||
ok: fileData.ok,
|
||||
status: fileData.status,
|
||||
svg: null
|
||||
};
|
||||
if (fileData.ok) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = fileData.html;
|
||||
const svg = div.firstElementChild;
|
||||
iconFileData.svg = svg?.tagName.toLowerCase() === 'svg' ? svg.outerHTML : '';
|
||||
}
|
||||
|
||||
iconFiles.set(url, iconFileData as IconFile);
|
||||
return iconFileData as IconFile;
|
||||
}
|
||||
@@ -190,13 +190,20 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
*/
|
||||
@property() inputmode: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
|
||||
|
||||
//
|
||||
// NOTE: We use an in-memory input for these getters/setters instead of the one in the template because the properties
|
||||
// can be set before the component is rendered.
|
||||
//
|
||||
|
||||
/** Gets or sets the current value as a `Date` object. Returns `null` if the value can't be converted. */
|
||||
get valueAsDate() {
|
||||
return this.input?.valueAsDate ?? null;
|
||||
const input = document.createElement('input');
|
||||
input.type = 'date';
|
||||
input.value = this.value;
|
||||
return input.valueAsDate;
|
||||
}
|
||||
|
||||
set valueAsDate(newValue: Date | null) {
|
||||
// We use an in-memory input instead of the one in the template because the property can be set before render
|
||||
const input = document.createElement('input');
|
||||
input.type = 'date';
|
||||
input.valueAsDate = newValue;
|
||||
@@ -205,11 +212,13 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
|
||||
/** Gets or sets the current value as a number. Returns `NaN` if the value can't be converted. */
|
||||
get valueAsNumber() {
|
||||
return this.input?.valueAsNumber ?? parseFloat(this.value);
|
||||
const input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
input.value = this.value;
|
||||
return input.valueAsNumber;
|
||||
}
|
||||
|
||||
set valueAsNumber(newValue: number) {
|
||||
// We use an in-memory input instead of the one in the template because the property can be set before render
|
||||
const input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
input.valueAsNumber = newValue;
|
||||
|
||||
@@ -64,7 +64,7 @@ export default css`
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:host(:hover:not([aria-disabled='true'])) .menu-item {
|
||||
:host(:hover:not([aria-disabled='true'], :focus-visible)) .menu-item {
|
||||
background-color: var(--sl-color-neutral-100);
|
||||
color: var(--sl-color-neutral-1000);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
* @csspart prefix - The prefix container.
|
||||
* @csspart label - The menu item label.
|
||||
* @csspart suffix - The suffix container.
|
||||
* @csspart submenu-icon - The submenu icon, visible only when the menu item has a submenu (not yet implemented).
|
||||
*/
|
||||
@customElement('sl-menu-item')
|
||||
export default class SlMenuItem extends ShoelaceElement {
|
||||
@@ -141,7 +142,7 @@ export default class SlMenuItem extends ShoelaceElement {
|
||||
|
||||
<slot name="suffix" part="suffix" class="menu-item__suffix"></slot>
|
||||
|
||||
<span class="menu-item__chevron">
|
||||
<span part="submenu-icon" class="menu-item__chevron">
|
||||
<sl-icon name="chevron-right" library="system" aria-hidden="true"></sl-icon>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,10 @@ export default class SlRadioButton extends ShoelaceElement {
|
||||
/** Disables the radio button. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** The radio button's size. */
|
||||
/**
|
||||
* The radio button's size. When used inside a radio group, the size will be determined by the radio group's size so
|
||||
* this attribute can typically be omitted.
|
||||
*/
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
/** Draws a pill-style radio button with rounded edges. */
|
||||
|
||||
@@ -151,30 +151,30 @@ describe('<sl-radio-group>', () => {
|
||||
expect(radioGroup.hasAttribute('data-user-invalid')).to.be.false;
|
||||
expect(radioGroup.hasAttribute('data-user-valid')).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
it('should show a constraint validation error when setCustomValidity() is called', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<sl-radio-group value="1">
|
||||
<sl-radio id="radio-1" name="a" value="1"></sl-radio>
|
||||
<sl-radio id="radio-2" name="a" value="2"></sl-radio>
|
||||
</sl-radio-group>
|
||||
<sl-button type="submit">Submit</sl-button>
|
||||
</form>
|
||||
`);
|
||||
const button = form.querySelector('sl-button')!;
|
||||
const radioGroup = form.querySelector<SlRadioGroup>('sl-radio-group')!;
|
||||
const submitHandler = sinon.spy((event: SubmitEvent) => event.preventDefault());
|
||||
it('should show a constraint validation error when setCustomValidity() is called', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<sl-radio-group value="1">
|
||||
<sl-radio id="radio-1" name="a" value="1"></sl-radio>
|
||||
<sl-radio id="radio-2" name="a" value="2"></sl-radio>
|
||||
</sl-radio-group>
|
||||
<sl-button type="submit">Submit</sl-button>
|
||||
</form>
|
||||
`);
|
||||
const button = form.querySelector('sl-button')!;
|
||||
const radioGroup = form.querySelector<SlRadioGroup>('sl-radio-group')!;
|
||||
const submitHandler = sinon.spy((event: SubmitEvent) => event.preventDefault());
|
||||
|
||||
// Submitting the form after setting custom validity should not trigger the handler
|
||||
radioGroup.setCustomValidity('Invalid selection');
|
||||
form.addEventListener('submit', submitHandler);
|
||||
button.click();
|
||||
// Submitting the form after setting custom validity should not trigger the handler
|
||||
radioGroup.setCustomValidity('Invalid selection');
|
||||
form.addEventListener('submit', submitHandler);
|
||||
button.click();
|
||||
|
||||
await aTimeout(100);
|
||||
await aTimeout(100);
|
||||
|
||||
expect(submitHandler).to.not.have.been.called;
|
||||
expect(submitHandler).to.not.have.been.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -252,6 +252,53 @@ describe('when submitting a form', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a size is applied', () => {
|
||||
it('should apply the same size to all radios', async () => {
|
||||
const radioGroup = await fixture<SlRadioGroup>(html`
|
||||
<sl-radio-group size="large">
|
||||
<sl-radio id="radio-1" value="1"></sl-radio>
|
||||
<sl-radio id="radio-2" value="2"></sl-radio>
|
||||
</sl-radio-group>
|
||||
`);
|
||||
const [radio1, radio2] = radioGroup.querySelectorAll('sl-radio')!;
|
||||
|
||||
expect(radio1.size).to.equal('large');
|
||||
expect(radio2.size).to.equal('large');
|
||||
});
|
||||
|
||||
it('should apply the same size to all radio buttons', async () => {
|
||||
const radioGroup = await fixture<SlRadioGroup>(html`
|
||||
<sl-radio-group size="large">
|
||||
<sl-radio-button id="radio-1" value="1"></sl-radio-button>
|
||||
<sl-radio-button id="radio-2" value="2"></sl-radio-button>
|
||||
</sl-radio-group>
|
||||
`);
|
||||
const [radio1, radio2] = radioGroup.querySelectorAll('sl-radio-button')!;
|
||||
|
||||
expect(radio1.size).to.equal('large');
|
||||
expect(radio2.size).to.equal('large');
|
||||
});
|
||||
|
||||
it('should update the size of all radio buttons when size changes', async () => {
|
||||
const radioGroup = await fixture<SlRadioGroup>(html`
|
||||
<sl-radio-group size="small">
|
||||
<sl-radio-button id="radio-1" value="1"></sl-radio-button>
|
||||
<sl-radio-button id="radio-2" value="2"></sl-radio-button>
|
||||
</sl-radio-group>
|
||||
`);
|
||||
const [radio1, radio2] = radioGroup.querySelectorAll('sl-radio-button')!;
|
||||
|
||||
expect(radio1.size).to.equal('small');
|
||||
expect(radio2.size).to.equal('small');
|
||||
|
||||
radioGroup.size = 'large';
|
||||
await radioGroup.updateComplete;
|
||||
|
||||
expect(radio1.size).to.equal('large');
|
||||
expect(radio2.size).to.equal('large');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the value changes', () => {
|
||||
it('should emit sl-change when toggled with the arrow keys', async () => {
|
||||
const radioGroup = await fixture<SlRadioGroup>(html`
|
||||
|
||||
@@ -71,6 +71,9 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
/** The current value of the radio group, submitted as a name/value pair with form data. */
|
||||
@property({ reflect: true }) value = '';
|
||||
|
||||
/** The radio group's size. This size will be applied to all child radios and radio buttons. */
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
/**
|
||||
* By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
|
||||
* to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
|
||||
@@ -196,10 +199,20 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
}
|
||||
}
|
||||
|
||||
private handleSlotChange() {
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitInvalidEvent(event);
|
||||
}
|
||||
|
||||
private syncRadios() {
|
||||
if (customElements.get('sl-radio') || customElements.get('sl-radio-button')) {
|
||||
const radios = this.getAllRadios();
|
||||
radios.forEach(radio => (radio.checked = radio.value === this.value));
|
||||
|
||||
// Sync the checked state and size
|
||||
radios.forEach(radio => {
|
||||
radio.checked = radio.value === this.value;
|
||||
radio.size = this.size;
|
||||
});
|
||||
|
||||
this.hasButtonGroup = radios.some(radio => radio.tagName.toLowerCase() === 'sl-radio-button');
|
||||
|
||||
@@ -224,22 +237,22 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
}
|
||||
} else {
|
||||
// Rerun this handler when <sl-radio> or <sl-radio-button> is registered
|
||||
customElements.whenDefined('sl-radio').then(() => this.handleSlotChange());
|
||||
customElements.whenDefined('sl-radio-button').then(() => this.handleSlotChange());
|
||||
customElements.whenDefined('sl-radio').then(() => this.syncRadios());
|
||||
customElements.whenDefined('sl-radio-button').then(() => this.syncRadios());
|
||||
}
|
||||
}
|
||||
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitInvalidEvent(event);
|
||||
}
|
||||
|
||||
private updateCheckedRadio() {
|
||||
const radios = this.getAllRadios();
|
||||
radios.forEach(radio => (radio.checked = radio.value === this.value));
|
||||
this.formControlController.setValidity(this.validity.valid);
|
||||
}
|
||||
|
||||
@watch('size', { waitUntilFirstUpdate: true })
|
||||
handleSizeChange() {
|
||||
this.syncRadios();
|
||||
}
|
||||
|
||||
@watch('value')
|
||||
handleValueChange() {
|
||||
if (this.hasUpdated) {
|
||||
@@ -302,7 +315,7 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
<slot
|
||||
@click=${this.handleRadioClick}
|
||||
@keydown=${this.handleKeyDown}
|
||||
@slotchange=${this.handleSlotChange}
|
||||
@slotchange=${this.syncRadios}
|
||||
role="presentation"
|
||||
></slot>
|
||||
`;
|
||||
@@ -312,7 +325,9 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
part="form-control"
|
||||
class=${classMap({
|
||||
'form-control': true,
|
||||
'form-control--medium': true,
|
||||
'form-control--small': this.size === 'small',
|
||||
'form-control--medium': this.size === 'medium',
|
||||
'form-control--large': this.size === 'large',
|
||||
'form-control--radio-group': true,
|
||||
'form-control--has-label': hasLabel,
|
||||
'form-control--has-help-text': hasHelpText
|
||||
|
||||
@@ -36,7 +36,10 @@ export default class SlRadio extends ShoelaceElement {
|
||||
/** The radio's value. When selected, the radio group will receive this value. */
|
||||
@property() value: string;
|
||||
|
||||
/** The radio's size. */
|
||||
/**
|
||||
* The radio's size. When used inside a radio group, the size will be determined by the radio group's size so this
|
||||
* attribute can typically be omitted.
|
||||
*/
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
/** Disables the radio. */
|
||||
|
||||
@@ -43,12 +43,19 @@ export default css`
|
||||
padding: var(--symbol-spacing);
|
||||
}
|
||||
|
||||
.rating__symbols--indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
.rating__symbol--active,
|
||||
.rating__partial--filled {
|
||||
color: var(--symbol-color-active);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rating__partial-symbol-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rating__partial--filled {
|
||||
position: absolute;
|
||||
top: var(--symbol-spacing);
|
||||
left: var(--symbol-spacing);
|
||||
}
|
||||
|
||||
.rating__symbol {
|
||||
@@ -79,7 +86,7 @@ export default css`
|
||||
|
||||
/* Forced colors mode */
|
||||
@media (forced-colors: active) {
|
||||
.rating__symbols--indicator {
|
||||
.rating__symbol--active {
|
||||
color: SelectedItem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,16 +249,51 @@ export default class SlRating extends ShoelaceElement {
|
||||
@mousemove=${this.handleMouseMove}
|
||||
@touchmove=${this.handleTouchMove}
|
||||
>
|
||||
<span class="rating__symbols rating__symbols--inactive">
|
||||
<span class="rating__symbols">
|
||||
${counter.map(index => {
|
||||
// Users can click the current value to clear the rating. When this happens, we set this.isHovering to
|
||||
// false to prevent the hover state from confusing them as they move the mouse out of the control. This
|
||||
// extra mouseenter will reinstate it if they happen to mouse over an adjacent symbol.
|
||||
if (displayValue > index && displayValue < index + 1) {
|
||||
// Users can click the current value to clear the rating. When this happens, we set this.isHovering to
|
||||
// false to prevent the hover state from confusing them as they move the mouse out of the control. This
|
||||
// extra mouseenter will reinstate it if they happen to mouse over an adjacent symbol.
|
||||
return html`
|
||||
<span
|
||||
class=${classMap({
|
||||
rating__symbol: true,
|
||||
'rating__partial-symbol-container': true,
|
||||
'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1
|
||||
})}
|
||||
role="presentation"
|
||||
@mouseenter=${this.handleMouseEnter}
|
||||
>
|
||||
<div
|
||||
style=${styleMap({
|
||||
clipPath: isRtl
|
||||
? `inset(0 ${(displayValue - index) * 100}% 0 0)`
|
||||
: `inset(0 0 0 ${(displayValue - index) * 100}%)`
|
||||
})}
|
||||
>
|
||||
${unsafeHTML(this.getSymbol(index + 1))}
|
||||
</div>
|
||||
<div
|
||||
class="rating__partial--filled"
|
||||
style=${styleMap({
|
||||
clipPath: isRtl
|
||||
? `inset(0 0 0 ${100 - (displayValue - index) * 100}%)`
|
||||
: `inset(0 ${100 - (displayValue - index) * 100}% 0 0)`
|
||||
})}
|
||||
>
|
||||
${unsafeHTML(this.getSymbol(index + 1))}
|
||||
</div>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<span
|
||||
class=${classMap({
|
||||
rating__symbol: true,
|
||||
'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1
|
||||
'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1,
|
||||
'rating__symbol--active': displayValue >= index + 1
|
||||
})}
|
||||
role="presentation"
|
||||
@mouseenter=${this.handleMouseEnter}
|
||||
@@ -268,30 +303,6 @@ export default class SlRating extends ShoelaceElement {
|
||||
`;
|
||||
})}
|
||||
</span>
|
||||
|
||||
<span class="rating__symbols rating__symbols--indicator">
|
||||
${counter.map(index => {
|
||||
return html`
|
||||
<span
|
||||
class=${classMap({
|
||||
rating__symbol: true,
|
||||
'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1
|
||||
})}
|
||||
style=${styleMap({
|
||||
clipPath:
|
||||
displayValue > index + 1
|
||||
? 'none'
|
||||
: isRtl
|
||||
? `inset(0 0 0 ${100 - ((displayValue - index) / 1) * 100}%)`
|
||||
: `inset(0 ${100 - ((displayValue - index) / 1) * 100}% 0 0)`
|
||||
})}
|
||||
role="presentation"
|
||||
>
|
||||
${unsafeHTML(this.getSymbol(index + 1))}
|
||||
</span>
|
||||
`;
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -8,15 +8,31 @@ import type SlOption from '../option/option';
|
||||
import type SlSelect from './select';
|
||||
|
||||
describe('<sl-select>', () => {
|
||||
it('should pass accessibility tests', async () => {
|
||||
const el = await fixture<SlSelect>(html`
|
||||
<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>
|
||||
`);
|
||||
await expect(el).to.be.accessible();
|
||||
describe('accessibility', () => {
|
||||
it('should pass accessibility tests when closed', async () => {
|
||||
const select = await fixture<SlSelect>(html`
|
||||
<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>
|
||||
`);
|
||||
await expect(select).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should pass accessibility tests when open', async () => {
|
||||
const select = await fixture<SlSelect>(html`
|
||||
<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>
|
||||
`);
|
||||
|
||||
await select.show();
|
||||
|
||||
await expect(select).to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be disabled with the disabled attribute', async () => {
|
||||
|
||||
@@ -108,7 +108,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
@defaultValue() defaultValue: string | string[] = '';
|
||||
|
||||
/** The select's size. */
|
||||
@property() size: 'small' | 'medium' | 'large' = 'medium';
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
/** Placeholder text to show as a hint when the select is empty. */
|
||||
@property() placeholder = '';
|
||||
@@ -821,7 +821,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<slot
|
||||
<div
|
||||
id="listbox"
|
||||
role="listbox"
|
||||
aria-expanded=${this.open ? 'true' : 'false'}
|
||||
@@ -832,7 +832,9 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
tabindex="-1"
|
||||
@mouseup=${this.handleOptionClick}
|
||||
@slotchange=${this.handleDefaultSlotChange}
|
||||
></slot>
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</sl-popup>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,9 +1,260 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import { dragElement } from '../../internal/test';
|
||||
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { queryByTestId } from '../../internal/test/data-testid-helpers';
|
||||
import { resetMouse } from '@web/test-runner-commands';
|
||||
import type SlSplitPanel from './split-panel';
|
||||
|
||||
const DIVIDER_WIDTH_IN_PX = 4;
|
||||
|
||||
const getPanel = (splitPanel: SlSplitPanel, testid: string): HTMLElement => {
|
||||
const startPanel = queryByTestId<HTMLElement>(splitPanel, testid);
|
||||
expect(startPanel).not.to.be.null;
|
||||
return startPanel!;
|
||||
};
|
||||
|
||||
const getPanelWidth = (splitPanel: SlSplitPanel, testid: string) => {
|
||||
const panel = getPanel(splitPanel, testid);
|
||||
const { width } = panel.getBoundingClientRect();
|
||||
return width;
|
||||
};
|
||||
|
||||
const getPanelHeight = (splitPanel: SlSplitPanel, testid: string) => {
|
||||
const panel = getPanel(splitPanel, testid);
|
||||
const { height } = panel.getBoundingClientRect();
|
||||
return height;
|
||||
};
|
||||
|
||||
const getDivider = (splitPanel: SlSplitPanel): Element => {
|
||||
const divider = splitPanel.shadowRoot?.querySelector('[part="divider"]');
|
||||
expect(divider).not.to.be.null;
|
||||
return divider!;
|
||||
};
|
||||
|
||||
describe('<sl-split-panel>', () => {
|
||||
it('should render a component', async () => {
|
||||
const el = await fixture(html` <sl-split-panel></sl-split-panel> `);
|
||||
afterEach(async () => {
|
||||
await resetMouse();
|
||||
});
|
||||
|
||||
expect(el).to.exist;
|
||||
it('should render a component', async () => {
|
||||
const splitPanel = await fixture(html` <sl-split-panel></sl-split-panel> `);
|
||||
|
||||
expect(splitPanel).to.exist;
|
||||
});
|
||||
|
||||
it('should be accessible', async () => {
|
||||
const splitPanel = await fixture(html`<sl-split-panel>
|
||||
<div slot="start">Start</div>
|
||||
<div slot="end">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
await expect(splitPanel).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should show both panels', async () => {
|
||||
const splitPanel = await fixture(html`<sl-split-panel>
|
||||
<div slot="start">Start</div>
|
||||
<div slot="end">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
expect(splitPanel).to.contain.text('Start');
|
||||
expect(splitPanel).to.contain.text('End');
|
||||
});
|
||||
|
||||
describe('panel sizing horizontal', () => {
|
||||
it('has two evenly sized panels by default', async () => {
|
||||
const splitPanel = await fixture<SlSplitPanel>(html`<sl-split-panel>
|
||||
<div slot="start" data-testid="start-panel">Start</div>
|
||||
<div slot="end" data-testid="end-panel">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
const startPanelWidth = getPanelWidth(splitPanel, 'start-panel');
|
||||
const endPanelWidth = getPanelWidth(splitPanel, 'end-panel');
|
||||
|
||||
expect(startPanelWidth).to.be.equal(endPanelWidth);
|
||||
});
|
||||
|
||||
it('changes the sizing of the panels based on the position attribute', async () => {
|
||||
const splitPanel = await fixture<SlSplitPanel>(html`<sl-split-panel position="25">
|
||||
<div slot="start" data-testid="start-panel">Start</div>
|
||||
<div slot="end" data-testid="end-panel">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
const startPanelWidth = getPanelWidth(splitPanel, 'start-panel');
|
||||
const endPanelWidth = getPanelWidth(splitPanel, 'end-panel');
|
||||
|
||||
expect(startPanelWidth * 3).to.be.equal(endPanelWidth - DIVIDER_WIDTH_IN_PX);
|
||||
});
|
||||
|
||||
it('updates the position in pixels to the correct result', async () => {
|
||||
const splitPanel = await fixture<SlSplitPanel>(html`<sl-split-panel position="25">
|
||||
<div slot="start" data-testid="start-panel">Start</div>
|
||||
<div slot="end" data-testid="end-panel">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
splitPanel.position = 10;
|
||||
|
||||
const startPanelWidth = getPanelWidth(splitPanel, 'start-panel');
|
||||
|
||||
expect(startPanelWidth).to.be.equal(splitPanel.positionInPixels - DIVIDER_WIDTH_IN_PX / 2);
|
||||
});
|
||||
|
||||
it('emits the sl-reposition event on position change', async () => {
|
||||
const splitPanel = await fixture<SlSplitPanel>(html`<sl-split-panel>
|
||||
<div slot="start">Start</div>
|
||||
<div slot="end">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
const repositionPromise = oneEvent(splitPanel, 'sl-reposition');
|
||||
splitPanel.position = 10;
|
||||
return repositionPromise;
|
||||
});
|
||||
|
||||
it('can be resized using the mouse', async () => {
|
||||
const splitPanel = await fixture<SlSplitPanel>(html`<sl-split-panel>
|
||||
<div slot="start">Start</div>
|
||||
<div slot="end">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
const positionInPixels = splitPanel.positionInPixels;
|
||||
|
||||
const divider = getDivider(splitPanel);
|
||||
|
||||
await dragElement(divider, -30);
|
||||
|
||||
const positionInPixelsAfterDrag = splitPanel.positionInPixels;
|
||||
expect(positionInPixelsAfterDrag).to.be.equal(positionInPixels - 30);
|
||||
});
|
||||
|
||||
it('cannot be resized if disabled', async () => {
|
||||
const splitPanel = await fixture<SlSplitPanel>(html`<sl-split-panel disabled>
|
||||
<div slot="start">Start</div>
|
||||
<div slot="end">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
const positionInPixels = splitPanel.positionInPixels;
|
||||
|
||||
const divider = getDivider(splitPanel);
|
||||
|
||||
await dragElement(divider, -30);
|
||||
|
||||
const positionInPixelsAfterDrag = splitPanel.positionInPixels;
|
||||
expect(positionInPixelsAfterDrag).to.be.equal(positionInPixels);
|
||||
});
|
||||
|
||||
it('snaps to predefined positions', async () => {
|
||||
const splitPanel = await fixture<SlSplitPanel>(html`<sl-split-panel>
|
||||
<div slot="start">Start</div>
|
||||
<div slot="end">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
const positionInPixels = splitPanel.positionInPixels;
|
||||
splitPanel.snap = `${positionInPixels - 40}px`;
|
||||
|
||||
const divider = getDivider(splitPanel);
|
||||
|
||||
await dragElement(divider, -30);
|
||||
|
||||
const positionInPixelsAfterDrag = splitPanel.positionInPixels;
|
||||
expect(positionInPixelsAfterDrag).to.be.equal(positionInPixels - 40);
|
||||
});
|
||||
});
|
||||
|
||||
describe('panel sizing vertical', () => {
|
||||
it('has two evenly sized panels by default', async () => {
|
||||
const splitPanel = await fixture<SlSplitPanel>(html`<sl-split-panel vertical style="height: 400px;">
|
||||
<div slot="start" data-testid="start-panel">Start</div>
|
||||
<div slot="end" data-testid="end-panel">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
const startPanelHeight = getPanelHeight(splitPanel, 'start-panel');
|
||||
const endPanelHeight = getPanelHeight(splitPanel, 'end-panel');
|
||||
|
||||
expect(startPanelHeight).to.be.equal(endPanelHeight);
|
||||
});
|
||||
|
||||
it('changes the sizing of the panels based on the position attribute', async () => {
|
||||
const splitPanel = await fixture<SlSplitPanel>(html`<sl-split-panel position="25" vertical style="height: 400px;">
|
||||
<div slot="start" data-testid="start-panel">Start</div>
|
||||
<div slot="end" data-testid="end-panel">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
const startPanelHeight = getPanelHeight(splitPanel, 'start-panel');
|
||||
const endPanelHeight = getPanelHeight(splitPanel, 'end-panel');
|
||||
|
||||
expect(startPanelHeight * 3).to.be.equal(endPanelHeight - DIVIDER_WIDTH_IN_PX);
|
||||
});
|
||||
|
||||
it('updates the position in pixels to the correct result', async () => {
|
||||
const splitPanel = await fixture<SlSplitPanel>(html`<sl-split-panel position="25" vertical style="height: 400px;">
|
||||
<div slot="start" data-testid="start-panel">Start</div>
|
||||
<div slot="end" data-testid="end-panel">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
splitPanel.position = 10;
|
||||
|
||||
const startPanelHeight = getPanelHeight(splitPanel, 'start-panel');
|
||||
|
||||
expect(startPanelHeight).to.be.equal(splitPanel.positionInPixels - DIVIDER_WIDTH_IN_PX / 2);
|
||||
});
|
||||
|
||||
it('emits the sl-reposition event on position change ', async () => {
|
||||
const splitPanel = await fixture<SlSplitPanel>(html`<sl-split-panel vertical style="height: 400px;">
|
||||
<div slot="start">Start</div>
|
||||
<div slot="end">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
const repositionPromise = oneEvent(splitPanel, 'sl-reposition');
|
||||
splitPanel.position = 10;
|
||||
return repositionPromise;
|
||||
});
|
||||
|
||||
it('can be resized using the mouse ', async () => {
|
||||
const splitPanel = await fixture<SlSplitPanel>(html`<sl-split-panel vertical style="height: 400px;">
|
||||
<div slot="start">Start</div>
|
||||
<div slot="end">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
const positionInPixels = splitPanel.positionInPixels;
|
||||
|
||||
const divider = getDivider(splitPanel);
|
||||
|
||||
await dragElement(divider, 0, -30);
|
||||
|
||||
const positionInPixelsAfterDrag = splitPanel.positionInPixels;
|
||||
expect(positionInPixelsAfterDrag).to.be.equal(positionInPixels - 30);
|
||||
});
|
||||
|
||||
it('cannot be resized if disabled', async () => {
|
||||
const splitPanel = await fixture<SlSplitPanel>(html`<sl-split-panel disabled vertical style="height: 400px;">
|
||||
<div slot="start">Start</div>
|
||||
<div slot="end">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
const positionInPixels = splitPanel.positionInPixels;
|
||||
|
||||
const divider = getDivider(splitPanel);
|
||||
|
||||
await dragElement(divider, 0, -30);
|
||||
|
||||
const positionInPixelsAfterDrag = splitPanel.positionInPixels;
|
||||
expect(positionInPixelsAfterDrag).to.be.equal(positionInPixels);
|
||||
});
|
||||
|
||||
it('snaps to predefined positions', async () => {
|
||||
const splitPanel = await fixture<SlSplitPanel>(html`<sl-split-panel vertical style="height: 400px;">
|
||||
<div slot="start">Start</div>
|
||||
<div slot="end">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
const positionInPixels = splitPanel.positionInPixels;
|
||||
splitPanel.snap = `${positionInPixels - 40}px`;
|
||||
|
||||
const divider = getDivider(splitPanel);
|
||||
|
||||
await dragElement(divider, 0, -30);
|
||||
|
||||
const positionInPixelsAfterDrag = splitPanel.positionInPixels;
|
||||
expect(positionInPixelsAfterDrag).to.be.equal(positionInPixels - 40);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -262,5 +262,65 @@ describe('<sl-switch>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not jump the page to the bottom when focusing a switch at the bottom of an element with overflow: auto;', async () => {
|
||||
// https://github.com/shoelace-style/shoelace/issues/1169
|
||||
const el = await fixture<HTMLDivElement>(html`
|
||||
<div style="display: flex; flex-direction: column; overflow: auto; max-height: 400px;">
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
<sl-switch>Switch</sl-switch>
|
||||
</div>
|
||||
;
|
||||
`);
|
||||
|
||||
const switches = el.querySelectorAll<SlSwitch>('sl-switch');
|
||||
const lastSwitch = switches[switches.length - 1];
|
||||
|
||||
expect(window.scrollY).to.equal(0);
|
||||
// Without these 2 timeouts, tests will pass unexpectedly in Safari.
|
||||
await aTimeout(10);
|
||||
lastSwitch.focus();
|
||||
await aTimeout(10);
|
||||
expect(window.scrollY).to.equal(0);
|
||||
});
|
||||
|
||||
runFormControlBaseTests('sl-switch');
|
||||
});
|
||||
|
||||
@@ -70,6 +70,11 @@ export default class SlTabGroup extends ShoelaceElement {
|
||||
@property({ attribute: 'no-scroll-controls', type: Boolean }) noScrollControls = false;
|
||||
|
||||
connectedCallback() {
|
||||
const whenAllDefined = Promise.allSettled([
|
||||
customElements.whenDefined('sl-tab'),
|
||||
customElements.whenDefined('sl-tab-panel')
|
||||
]);
|
||||
|
||||
super.connectedCallback();
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
@@ -89,20 +94,24 @@ export default class SlTabGroup extends ShoelaceElement {
|
||||
}
|
||||
});
|
||||
|
||||
// After the first update...
|
||||
this.updateComplete.then(() => {
|
||||
this.syncTabsAndPanels();
|
||||
this.mutationObserver.observe(this, { attributes: true, childList: true, subtree: true });
|
||||
this.resizeObserver.observe(this.nav);
|
||||
|
||||
// Set initial tab state when the tabs first become visible
|
||||
const intersectionObserver = new IntersectionObserver((entries, observer) => {
|
||||
if (entries[0].intersectionRatio > 0) {
|
||||
this.setAriaLabels();
|
||||
this.setActiveTab(this.getActiveTab() ?? this.tabs[0], { emitEvents: false });
|
||||
observer.unobserve(entries[0].target);
|
||||
}
|
||||
// Wait for tabs and tab panels to be registered
|
||||
whenAllDefined.then(() => {
|
||||
// Set initial tab state when the tabs become visible
|
||||
const intersectionObserver = new IntersectionObserver((entries, observer) => {
|
||||
if (entries[0].intersectionRatio > 0) {
|
||||
this.setAriaLabels();
|
||||
this.setActiveTab(this.getActiveTab() ?? this.tabs[0], { emitEvents: false });
|
||||
observer.unobserve(entries[0].target);
|
||||
}
|
||||
});
|
||||
intersectionObserver.observe(this.tabGroup);
|
||||
});
|
||||
intersectionObserver.observe(this.tabGroup);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -241,8 +250,8 @@ export default class SlTabGroup extends ShoelaceElement {
|
||||
this.activeTab = tab;
|
||||
|
||||
// Sync active tab and panel
|
||||
this.tabs.map(el => (el.active = el === this.activeTab));
|
||||
this.panels.map(el => (el.active = el.name === this.activeTab?.panel));
|
||||
this.tabs.forEach(el => (el.active = el === this.activeTab));
|
||||
this.panels.forEach(el => (el.active = el.name === this.activeTab?.panel));
|
||||
this.syncIndicator();
|
||||
|
||||
if (['top', 'bottom'].includes(this.placement)) {
|
||||
|
||||
@@ -47,6 +47,14 @@ import type { CSSResultGroup, PropertyValueMap } from 'lit';
|
||||
* @csspart expand-button - The container that wraps the tree item's expand button and spinner.
|
||||
* @csspart label - The tree item's label.
|
||||
* @csspart children - The container that wraps the tree item's nested children.
|
||||
* @csspart checkbox - The checkbox that shows when using multiselect.
|
||||
* @csspart checkbox__base - The checkbox's exported `base` part.
|
||||
* @csspart checkbox__control - The checkbox's exported `control` part.
|
||||
* @csspart checkbox__control--checked - The checkbox's exported `control--checked` part.
|
||||
* @csspart checkbox__control--indeterminate - The checkbox's exported `control--indeterminate` part.
|
||||
* @csspart checkbox__checked-icon - The checkbox's exported `checked-icon` part.
|
||||
* @csspart checkbox__indeterminate-icon - The checkbox's exported `indeterminate-icon` part.
|
||||
* @csspart checkbox__label - The checkbox's exported `label` part.
|
||||
*/
|
||||
@customElement('sl-tree-item')
|
||||
export default class SlTreeItem extends ShoelaceElement {
|
||||
@@ -258,11 +266,21 @@ export default class SlTreeItem extends ShoelaceElement {
|
||||
() =>
|
||||
html`
|
||||
<sl-checkbox
|
||||
tabindex="-1"
|
||||
part="checkbox"
|
||||
exportparts="
|
||||
base:checkbox__base,
|
||||
control:checkbox__control,
|
||||
control--checked:checkbox__control--checked,
|
||||
control--indeterminate:checkbox__control--indeterminate,
|
||||
checked-icon:checkbox__checked-icon,
|
||||
indeterminate-icon:checkbox__indeterminate-icon,
|
||||
label:checkbox__label
|
||||
"
|
||||
class="tree-item__checkbox"
|
||||
?disabled="${this.disabled}"
|
||||
?checked="${live(this.selected)}"
|
||||
?indeterminate="${this.indeterminate}"
|
||||
tabindex="-1"
|
||||
></sl-checkbox>
|
||||
`
|
||||
)}
|
||||
|
||||
@@ -65,3 +65,19 @@ export async function moveMouseOnElement(
|
||||
|
||||
await sendMouse({ type: 'move', position: [clickX, clickY] });
|
||||
}
|
||||
|
||||
/** A testing utility that drags an element with the mouse. */
|
||||
export async function dragElement(
|
||||
/** The element to drag */
|
||||
el: Element,
|
||||
/** The horizontal distance to drag in pixels */
|
||||
deltaX = 0,
|
||||
/** The vertical distance to drag in pixels */
|
||||
deltaY = 0
|
||||
): Promise<void> {
|
||||
await moveMouseOnElement(el);
|
||||
await sendMouse({ type: 'down' });
|
||||
const { clickX, clickY } = determineMousePosition(el, 'center', deltaX, deltaY);
|
||||
await sendMouse({ type: 'move', position: [clickX, clickY] });
|
||||
await sendMouse({ type: 'up' });
|
||||
}
|
||||
|
||||
@@ -55,4 +55,4 @@ function register(tagName: string): Promise<void> {
|
||||
discover(document.body);
|
||||
|
||||
// Listen for new undefined elements
|
||||
observer.observe(document.body, { subtree: true, childList: true });
|
||||
observer.observe(document.documentElement, { subtree: true, childList: true });
|
||||
|
||||
@@ -28,7 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Zum Anfang scrollen',
|
||||
selectAColorFromTheScreen: 'Wähle eine Farbe vom Bildschirm',
|
||||
showPassword: 'Passwort anzeigen',
|
||||
slideNum: slide => `Gleiten ${slide}`,
|
||||
slideNum: slide => `Folie ${slide}`,
|
||||
toggleColorFormat: 'Farbformat umschalten'
|
||||
};
|
||||
|
||||
|
||||
@@ -25,10 +25,10 @@ const translation: Translation = {
|
||||
remove: 'Remover',
|
||||
resize: 'Mudar o tamanho',
|
||||
scrollToEnd: 'Rolar até o final',
|
||||
scrollToStart: 'Rolar até o começo',
|
||||
scrollToStart: 'Rolar até o início',
|
||||
selectAColorFromTheScreen: 'Selecionar uma cor da tela',
|
||||
showPassword: 'Mostrar senhaShow password',
|
||||
slideNum: slide => `Diapositivo ${slide}`,
|
||||
showPassword: 'Mostrar senha',
|
||||
slideNum: slide => `Slide ${slide}`,
|
||||
toggleColorFormat: 'Trocar o formato de cor'
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { playwrightLauncher } from '@web/test-runner-playwright';
|
||||
export default {
|
||||
rootDir: '.',
|
||||
files: 'src/**/*.test.ts', // "default" group
|
||||
concurrentBrowsers: 3,
|
||||
concurrentBrowsers: 1,
|
||||
nodeResolve: true,
|
||||
testFramework: {
|
||||
config: {
|
||||
@@ -26,7 +26,9 @@ export default {
|
||||
],
|
||||
testRunnerHtml: testFramework => `
|
||||
<html lang="en-US">
|
||||
<head></head>
|
||||
<head>
|
||||
<script>window.process = {env: { NODE_ENV: "production" }};</script>
|
||||
</head>
|
||||
<body>
|
||||
<link rel="stylesheet" href="dist/themes/light.css">
|
||||
<script type="module" src="dist/shoelace.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user