mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
Merge branch 'next' into cem-better-data
This commit is contained in:
@@ -388,7 +388,7 @@
|
||||
</div>
|
||||
|
||||
<div class="component-header__summary">
|
||||
<p>${marked(component.summary)}</p>
|
||||
${component.summary ? `<p>${marked(component.summary)}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -159,9 +159,9 @@ Some input types will automatically trigger constraints, such as `email` and `ur
|
||||
|
||||
```html preview
|
||||
<form class="input-validation-type">
|
||||
<sl-input variant="email" label="Email" placeholder="you@example.com" required></sl-input>
|
||||
<sl-input type="email" label="Email" placeholder="you@example.com" required></sl-input>
|
||||
<br />
|
||||
<sl-input variant="url" label="URL" placeholder="https://example.com/" required></sl-input>
|
||||
<sl-input type="url" label="URL" placeholder="https://example.com/" required></sl-input>
|
||||
<br />
|
||||
<sl-button type="reset" variant="default">Reset</sl-button>
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
@@ -187,9 +187,9 @@ const App = () => {
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<SlInput variant="email" label="Email" placeholder="you@example.com" required />
|
||||
<SlInput type="email" label="Email" placeholder="you@example.com" required />
|
||||
<br />
|
||||
<SlInput variant="url" label="URL" placeholder="https://example.com/" required />
|
||||
<SlInput type="url" label="URL" placeholder="https://example.com/" required />
|
||||
<br />
|
||||
<SlButton type="submit" variant="primary">
|
||||
Submit
|
||||
|
||||
@@ -6,13 +6,23 @@ Components with the <sl-badge variant="warning" pill>Experimental</sl-badge> bad
|
||||
|
||||
New versions of Shoelace are released as-needed and generally occur when a critical mass of changes have accumulated. At any time, you can see what's coming in the next release by visiting [next.shoelace.style](https://next.shoelace.style).
|
||||
|
||||
_During the beta period, these restrictions may be relaxed in the event of a mission-critical bug._ 🐛
|
||||
?> During the beta period, these restrictions may be relaxed in the event of a mission-critical bug. 🐛
|
||||
|
||||
## Next
|
||||
|
||||
- 🚨 BREAKING: Removed the `fieldset` property from `<sl-radio-group>` (use CSS parts if you want to keep the border) [#965](https://github.com/shoelace-style/shoelace/issues/965)
|
||||
- 🚨 BREAKING: Removed `base` and `label` parts from `<sl-radio-group>` (use `form-control` and `form-control__label` instead) [#965](https://github.com/shoelace-style/shoelace/issues/965)
|
||||
- 🚨 BREAKING: Removed the `base` part from `<sl-icon>` (style the host element directly instead)
|
||||
- 🚨 BREAKING: Removed the `invalid` attribute from form controls (use `[data-invalid]` to target it with CSS)
|
||||
- Added validation states to all form controls to allow styling based on various validation states [#1011](https://github.com/shoelace-style/shoelace/issues/1011)
|
||||
- `data-required` - indicates that a value is required
|
||||
- `data-optional` - indicates that a value is NOT required
|
||||
- `data-invalid` - indicates that the form control is invalid
|
||||
- `data-valid` - indicates that the form control is valid
|
||||
- `data-user-invalid` - indicates the form control is invalid and the user has interacted with it
|
||||
- `data-user-valid` - indicates the form control is valid and the user has interacted with it
|
||||
- Added `checkValidity()` method to all form controls
|
||||
- Added `reportValidity()` method to `<sl-range>`
|
||||
- Added `button--checked` to `<sl-radio-button>` and `control--checked` to `<sl-radio>` to style just the checked state [#933](https://github.com/shoelace-style/shoelace/pull/933)
|
||||
- Added tests for `<sl-menu>`, `<sl-menu-item>`, `<sl-menu-label>`, `<sl-rating>`, `<sl-relative-time>`, `<sl-skeleton>`, `<sl-tab-panel>` and `<sl-tag>` [#935](https://github.com/shoelace-style/shoelace/pull/935)
|
||||
[#949](https://github.com/shoelace-style/shoelace/pull/949) [#951](https://github.com/shoelace-style/shoelace/pull/951) [#953](https://github.com/shoelace-style/shoelace/pull/953)
|
||||
@@ -22,9 +32,12 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
|
||||
- Added `--sl-input-required-content-color` custom property to all form controls [#948](https://github.com/shoelace-style/shoelace/pull/948)
|
||||
- Added the ability to cancel `sl-show` and `sl-hide` events in `<sl-details>` [#993](https://github.com/shoelace-style/shoelace/issues/993)
|
||||
- Added `focus()` and `blur()` methods to `<sl-radio-button>`
|
||||
- Added `stepUp()` and `stepDown()` methods to `<sl-input>` and `<sl-range>` [#1013](https://github.com/shoelace-style/shoelace/pull/1013)
|
||||
- Added `showPicker()` method to `<sl-input>` [#1013](https://github.com/shoelace-style/shoelace/pull/1013)
|
||||
- Added the `handle-icon` part to `<sl-image-comparer>`
|
||||
- Added `caret`, `check`, `grip-vertical`, `indeterminate`, and `radio` icons to the system library and removed `check-lg` [#985](https://github.com/shoelace-style/shoelace/issues/985)
|
||||
- Added the `loading` attribute to `<sl-avatar>` to allow lazy loading of image avatars [#1006](https://github.com/shoelace-style/shoelace/pull/1006)
|
||||
- Added `formenctype` attribute to `<sl-button>` [#1009](https://github.com/shoelace-style/shoelace/pull/1009)
|
||||
- Added `exports` to `package.json` and removed the `main` and `module` properties [#1007](https://github.com/shoelace-style/shoelace/pull/1007)
|
||||
- Fixed a bug in `<sl-card>` that prevented the border radius to apply correctly to the header [#934](https://github.com/shoelace-style/shoelace/pull/934)
|
||||
- Fixed a bug in `<sl-button-group>` where the inner border disappeared on focus [#980](https://github.com/shoelace-style/shoelace/pull/980)
|
||||
@@ -37,6 +50,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
|
||||
- Fixed a bug in `<sl-radio-group>` that prevented the `invalid` property from correctly reflecting validity sometimes [#992](https://github.com/shoelace-style/shoelace/issues/992)
|
||||
- Fixed a bug in `<sl-tree>` that prevented selections from working correctly on dynamically added tree items [#963](https://github.com/shoelace-style/shoelace/issues/963)
|
||||
- Fixed module paths in `custom-elements.json` so they point to the dist file instead of the source file [#725](https://github.com/shoelace-style/shoelace/issues/725)
|
||||
- Fixed an incorrect return value for `reportValidity()` in `<sl-color-picker>`
|
||||
- Improved `<sl-badge>` to improve padding and render relative to the current font size
|
||||
- Improved how many components display in forced-colors mode / Windows High Contrast mode
|
||||
- Improved `<sl-color-picker>` so it's usable in forced-colors mode
|
||||
@@ -49,11 +63,14 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
|
||||
- Improved `<sl-split-panel>` so the divider is visible in forced-colors mode
|
||||
- Improved `<sl-tree-item>` so selected items are visible in forced-colors mode
|
||||
- Improved `<sl-tab-group>` so tabs are cleaner and easier to understand in forced-colors mode
|
||||
- Improved positioning of the menu in `<sl-select>` so you can customize the menu width [#1018](https://github.com/shoelace-style/shoelace/issues/1018)
|
||||
- Moved all component descriptions to `@summary` to get them within documentation tools [#962](https://github.com/shoelace-style/shoelace/pull/962)
|
||||
- Refactored form controls to use the `ShoelaceFormControl` interface to improve type safety and consistency
|
||||
- Updated Lit to 2.4.1
|
||||
- Updated `@shoelace-style/localize` t0 3.0.3 to support for extended language codes
|
||||
- Updated Bootstrap Icons to 1.10.2
|
||||
- Updated TypeScript to 4.8.4
|
||||
- Updated esbuild to 0.15.13
|
||||
- Updated esbuild to 0.15.14
|
||||
- Updated all other dependencies to latest versions
|
||||
|
||||
## 2.0.0-beta.83
|
||||
@@ -466,7 +483,7 @@ Thank you for your help and patience with testing Shoelace. I promise, we're not
|
||||
- Improved a11y of the scroll buttons in `<sl-tab-group>`
|
||||
- Improved a11y of the close button in `<sl-tab>`
|
||||
- Improved a11y of `<sl-tab-panel>` by removing an invalid `aria-selected` attribute [#579](https://github.com/shoelace-style/shoelace/issues/579)
|
||||
- Improved a11y of `<sl-icon>` by not using a variation of the `name` attribute for labels (use the `label` prop instead)
|
||||
- Improved a11y of `<sl-icon>` by not using a variation of the `name` attribute for labels (use the `label` attribute instead)
|
||||
- Moved `role` from the shadow root to the host element in `<sl-menu>`
|
||||
- Removed redundant `role="menu"` in `<sl-dropdown>`
|
||||
- Slightly faster animations for showing and hiding `<sl-dropdown>`
|
||||
@@ -528,7 +545,7 @@ This release is the second attempt at unbundling dependencies. This will be a br
|
||||
Shoelace doesn't have a lot of dependencies, but this release unbundles most of them so you can potentially save some extra kilobytes. This will be a breaking change only if your configuration _does not_ support bare module specifiers. CDN users and bundler users will be unaffected.
|
||||
|
||||
- 🚨 BREAKING: renamed the `sl-clear` event to `sl-remove`, the `clear-button` part to `remove-button`, and the `clearable` property to `removable` in `<sl-tag>`
|
||||
- Added the `disabled` prop to `<sl-resize-observer>`
|
||||
- Added the `disabled` attribute to `<sl-resize-observer>`
|
||||
- Fixed a bug in `<sl-mutation-observer>` where setting `disabled` initially didn't work
|
||||
- Unbundled dependencies and configured external imports to be packaged with bare module specifiers
|
||||
|
||||
|
||||
1988
package-lock.json
generated
1988
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -56,22 +56,22 @@
|
||||
"node": ">=14.17.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.4",
|
||||
"@floating-ui/dom": "^1.0.6",
|
||||
"@lit-labs/react": "^1.1.0",
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
"@shoelace-style/localize": "^3.0.1",
|
||||
"@shoelace-style/localize": "^3.0.3",
|
||||
"color": "4.2",
|
||||
"lit": "^2.4.1",
|
||||
"qr-creator": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@custom-elements-manifest/analyzer": "^0.6.6",
|
||||
"@open-wc/testing": "^3.1.6",
|
||||
"@open-wc/testing": "^3.1.7",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/react": "^18.0.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.42.0",
|
||||
"@typescript-eslint/parser": "^5.42.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||
"@typescript-eslint/parser": "^5.43.0",
|
||||
"@web/dev-server-esbuild": "^0.3.3",
|
||||
"@web/test-runner": "^0.15.0",
|
||||
"@web/test-runner-commands": "^0.6.5",
|
||||
@@ -82,10 +82,10 @@
|
||||
"chalk": "^5.1.2",
|
||||
"command-line-args": "^5.2.1",
|
||||
"comment-parser": "^1.3.1",
|
||||
"cspell": "^6.14.1",
|
||||
"cspell": "^6.14.2",
|
||||
"del": "^7.0.0",
|
||||
"download": "^8.0.0",
|
||||
"esbuild": "^0.15.13",
|
||||
"esbuild": "^0.15.14",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-plugin-chai-expect": "^3.0.0",
|
||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||
@@ -97,18 +97,18 @@
|
||||
"front-matter": "^4.0.2",
|
||||
"get-port": "^6.1.2",
|
||||
"globby": "^13.1.2",
|
||||
"husky": "^8.0.1",
|
||||
"husky": "^8.0.2",
|
||||
"jsonata": "^1.8.6",
|
||||
"lint-staged": "^13.0.3",
|
||||
"lunr": "^2.3.9",
|
||||
"npm-check-updates": "^16.3.16",
|
||||
"npm-check-updates": "^16.4.1",
|
||||
"open": "^8.4.0",
|
||||
"pascal-case": "^3.1.2",
|
||||
"plop": "^3.1.1",
|
||||
"prettier": "^2.7.1",
|
||||
"react": "^18.2.0",
|
||||
"recursive-copy": "^2.0.14",
|
||||
"sinon": "^14.0.1",
|
||||
"sinon": "^14.0.2",
|
||||
"strip-css-comments": "^5.0.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "4.8.4",
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Short summary of the component's intended use.
|
||||
*
|
||||
*
|
||||
* @since 2.0
|
||||
* @status experimental
|
||||
*
|
||||
@@ -29,10 +29,10 @@ export default class {{ properCase tag }} extends ShoelaceElement {
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
/** An example property. */
|
||||
@property() prop = 'example';
|
||||
/** An example attribute. */
|
||||
@property() attr = 'example';
|
||||
|
||||
@watch('someProp')
|
||||
@watch('someProperty')
|
||||
doSomething() {
|
||||
// Example event
|
||||
this.emit('sl-event-name');
|
||||
|
||||
@@ -5,21 +5,21 @@ import type SlAlert from './alert';
|
||||
describe('<sl-alert>', () => {
|
||||
it('should be visible with the open attribute', async () => {
|
||||
const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should not be visible without the open attribute', async () => {
|
||||
const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should emit sl-show and sl-after-show when calling show()', async () => {
|
||||
const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('<sl-alert>', () => {
|
||||
|
||||
it('should emit sl-hide and sl-after-hide when calling hide()', async () => {
|
||||
const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('<sl-alert>', () => {
|
||||
|
||||
it('should emit sl-show and sl-after-show when setting open = true', async () => {
|
||||
const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('<sl-alert>', () => {
|
||||
|
||||
it('should emit sl-hide and sl-after-hide when setting open = false', async () => {
|
||||
const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ export default class SlAlert extends ShoelaceElement {
|
||||
private readonly hasSlotController = new HasSlotController(this, 'icon', 'suffix');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('[part="base"]') base: HTMLElement;
|
||||
@query('[part~="base"]') base: HTMLElement;
|
||||
|
||||
/** Indicates whether or not the alert is open. You can use this in lieu of the show/hide methods. */
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
@@ -32,8 +32,8 @@ export default class SlAnimation extends ShoelaceElement {
|
||||
@property() name = 'none';
|
||||
|
||||
/**
|
||||
* Plays the animation. When omitted, the animation will be paused. This prop will be automatically removed when the
|
||||
* animation finishes or gets canceled.
|
||||
* Plays the animation. When omitted, the animation will be paused. This attribute will be automatically removed when
|
||||
* the animation finishes or gets canceled.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) play = false;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('<sl-avatar>', () => {
|
||||
});
|
||||
|
||||
it('should default to circle styling', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
const part = el.shadowRoot!.querySelector('[part~="base"]')!;
|
||||
expect(el.getAttribute('shape')).to.eq('circle');
|
||||
expect(part.classList.value.trim()).to.eq('avatar avatar--circle');
|
||||
});
|
||||
@@ -40,13 +40,13 @@ describe('<sl-avatar>', () => {
|
||||
});
|
||||
|
||||
it('renders "image" part, with src and a role of presentation', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="image"]')!;
|
||||
const part = el.shadowRoot!.querySelector('[part~="image"]')!;
|
||||
|
||||
expect(part.getAttribute('src')).to.eq(image);
|
||||
});
|
||||
|
||||
it('renders the label attribute in the "base" part', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
const part = el.shadowRoot!.querySelector('[part~="base"]')!;
|
||||
|
||||
expect(part.getAttribute('aria-label')).to.eq(label);
|
||||
});
|
||||
@@ -63,7 +63,7 @@ describe('<sl-avatar>', () => {
|
||||
});
|
||||
|
||||
it('renders "initials" part, with initials as the text node', () => {
|
||||
const part = el.shadowRoot!.querySelector<HTMLElement>('[part="initials"]')!;
|
||||
const part = el.shadowRoot!.querySelector<HTMLElement>('[part~="initials"]')!;
|
||||
|
||||
expect(part.innerText).to.eq(initials);
|
||||
});
|
||||
@@ -80,7 +80,7 @@ describe('<sl-avatar>', () => {
|
||||
});
|
||||
|
||||
it('appends the appropriate class on the "base" part', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
const part = el.shadowRoot!.querySelector('[part~="base"]')!;
|
||||
|
||||
expect(el.getAttribute('shape')).to.eq(shape);
|
||||
expect(part.classList.value.trim()).to.eq(`avatar avatar--${shape}`);
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('<sl-badge>', () => {
|
||||
it('should pass accessibility tests with a role of status on the base part.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
const part = el.shadowRoot!.querySelector('[part~="base"]')!;
|
||||
expect(part.getAttribute('role')).to.eq('status');
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('<sl-badge>', () => {
|
||||
});
|
||||
|
||||
it('should default to square styling, with the primary color', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
const part = el.shadowRoot!.querySelector('[part~="base"]')!;
|
||||
expect(part.classList.value.trim()).to.eq('badge badge--primary');
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,7 @@ describe('<sl-badge>', () => {
|
||||
});
|
||||
|
||||
it('should append the pill class to the classlist to render a pill', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
const part = el.shadowRoot!.querySelector('[part~="base"]')!;
|
||||
expect(part.classList.value.trim()).to.eq('badge badge--primary badge--pill');
|
||||
});
|
||||
});
|
||||
@@ -51,7 +51,7 @@ describe('<sl-badge>', () => {
|
||||
});
|
||||
|
||||
it('should append the pulse class to the classlist to render a pulse', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
const part = el.shadowRoot!.querySelector('[part~="base"]')!;
|
||||
expect(part.classList.value.trim()).to.eq('badge badge--primary badge--pulse');
|
||||
});
|
||||
});
|
||||
@@ -67,7 +67,7 @@ describe('<sl-badge>', () => {
|
||||
});
|
||||
|
||||
it('should default to square styling, with the primary color', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
const part = el.shadowRoot!.querySelector('[part~="base"]')!;
|
||||
expect(part.classList.value.trim()).to.eq(`badge badge--${variant}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,12 +14,12 @@ describe('<sl-breadcrumb-item>', () => {
|
||||
});
|
||||
|
||||
it('should hide the separator from screen readers', () => {
|
||||
const separator = el.shadowRoot!.querySelector<HTMLSpanElement>('[part="separator"]');
|
||||
const separator = el.shadowRoot!.querySelector<HTMLSpanElement>('[part~="separator"]');
|
||||
expect(separator).attribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
it('should render a HTMLButtonElement as the part "label", with a set type "button"', () => {
|
||||
const button = el.shadowRoot!.querySelector<HTMLButtonElement>('[part="label"]');
|
||||
const button = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="label"]');
|
||||
expect(button).to.exist;
|
||||
expect(button).attribute('type', 'button');
|
||||
});
|
||||
@@ -38,7 +38,7 @@ describe('<sl-breadcrumb-item>', () => {
|
||||
});
|
||||
|
||||
it('should render a HTMLAnchorElement as the part "label", with the supplied href value', () => {
|
||||
const hyperlink = el.shadowRoot!.querySelector<HTMLAnchorElement>('[part="label"]');
|
||||
const hyperlink = el.shadowRoot!.querySelector<HTMLAnchorElement>('[part~="label"]');
|
||||
expect(hyperlink).attribute('href', 'https://jsonplaceholder.typicode.com/');
|
||||
});
|
||||
});
|
||||
@@ -58,7 +58,7 @@ describe('<sl-breadcrumb-item>', () => {
|
||||
let hyperlink: HTMLAnchorElement | null;
|
||||
|
||||
before(() => {
|
||||
hyperlink = el.shadowRoot!.querySelector<HTMLAnchorElement>('[part="label"]');
|
||||
hyperlink = el.shadowRoot!.querySelector<HTMLAnchorElement>('[part~="label"]');
|
||||
});
|
||||
|
||||
it('should use the supplied href value, as the href attribute value', () => {
|
||||
@@ -124,7 +124,7 @@ describe('<sl-breadcrumb-item>', () => {
|
||||
});
|
||||
|
||||
it('should append class "breadcrumb-item--has-prefix" to "base" part', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
const part = el.shadowRoot!.querySelector('[part~="base"]')!;
|
||||
expect(part.classList.value.trim()).to.equal('breadcrumb-item breadcrumb-item--has-prefix');
|
||||
});
|
||||
});
|
||||
@@ -151,7 +151,7 @@ describe('<sl-breadcrumb-item>', () => {
|
||||
});
|
||||
|
||||
it('should append class "breadcrumb-item--has-suffix" to "base" part', () => {
|
||||
const part = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const part = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
expect(part.classList.value.trim()).to.equal('breadcrumb-item breadcrumb-item--has-suffix');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('<sl-button>', () => {
|
||||
|
||||
it('should not have a caret present', async () => {
|
||||
const el = await fixture<SlButton>(html` <sl-button>Button Label</sl-button> `);
|
||||
expect(el.shadowRoot?.querySelector('[part="caret"]')).not.to.exist;
|
||||
expect(el.shadowRoot?.querySelector('[part~="caret"]')).not.to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,7 +98,7 @@ describe('<sl-button>', () => {
|
||||
describe('when caret', () => {
|
||||
it('should have a caret present', async () => {
|
||||
const el = await fixture<SlButton>(html` <sl-button caret>Button Label</sl-button> `);
|
||||
expect(el.shadowRoot!.querySelector('[part="caret"]')).to.exist;
|
||||
expect(el.shadowRoot!.querySelector('[part~="caret"]')).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@ import { html, literal } from 'lit/static-html.js';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import '../icon/icon';
|
||||
import '../spinner/spinner';
|
||||
import styles from './button.styles';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
@@ -34,13 +36,13 @@ import type { CSSResultGroup } from 'lit';
|
||||
* @csspart caret - The button's caret icon.
|
||||
*/
|
||||
@customElement('sl-button')
|
||||
export default class SlButton extends ShoelaceElement {
|
||||
export default class SlButton extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('.button') button: HTMLButtonElement | HTMLLinkElement;
|
||||
|
||||
private readonly formSubmitController = new FormSubmitController(this, {
|
||||
form: (input: HTMLInputElement) => {
|
||||
form: input => {
|
||||
// Buttons support a form attribute that points to an arbitrary form, so if this attribute it set we need to query
|
||||
// the form from the same root using its id
|
||||
if (input.hasAttribute('form')) {
|
||||
@@ -57,6 +59,7 @@ export default class SlButton extends ShoelaceElement {
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() invalid = false;
|
||||
|
||||
/** The button's variant. */
|
||||
@property({ reflect: true }) variant: 'default' | 'primary' | 'success' | 'neutral' | 'warning' | 'danger' | 'text' =
|
||||
@@ -90,16 +93,16 @@ export default class SlButton extends ShoelaceElement {
|
||||
@property() type: 'button' | 'submit' | 'reset' = 'button';
|
||||
|
||||
/** An optional name for the button. Ignored when `href` is set. */
|
||||
@property() name?: string;
|
||||
@property() name = '';
|
||||
|
||||
/** An optional value for the button. Ignored when `href` is set. */
|
||||
@property() value?: string;
|
||||
@property() value = '';
|
||||
|
||||
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
|
||||
@property() href?: string;
|
||||
@property() href = '';
|
||||
|
||||
/** Tells the browser where to open the link. Only used when `href` is set. */
|
||||
@property() target?: '_blank' | '_parent' | '_self' | '_top';
|
||||
@property() target: '_blank' | '_parent' | '_self' | '_top';
|
||||
|
||||
/** Tells the browser to download the linked file as this filename. Only used when `href` is set. */
|
||||
@property() download?: string;
|
||||
@@ -113,6 +116,10 @@ export default class SlButton extends ShoelaceElement {
|
||||
/** Used to override the form owner's `action` attribute. */
|
||||
@property({ attribute: 'formaction' }) formAction: string;
|
||||
|
||||
/** Used to override the form owner's `enctype` attribute. */
|
||||
@property({ attribute: 'formenctype' })
|
||||
formEnctype: 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain';
|
||||
|
||||
/** Used to override the form owner's `method` attribute. */
|
||||
@property({ attribute: 'formmethod' }) formMethod: 'post' | 'get';
|
||||
|
||||
@@ -122,6 +129,12 @@ export default class SlButton extends ShoelaceElement {
|
||||
/** Used to override the form owner's `target` attribute. */
|
||||
@property({ attribute: 'formtarget' }) formTarget: '_self' | '_blank' | '_parent' | '_top' | string;
|
||||
|
||||
firstUpdated() {
|
||||
if (this.isButton()) {
|
||||
this.invalid = !(this.button as HTMLButtonElement).checkValidity();
|
||||
}
|
||||
}
|
||||
|
||||
/** Simulates a click on the button. */
|
||||
click() {
|
||||
this.button.click();
|
||||
@@ -137,6 +150,32 @@ export default class SlButton extends ShoelaceElement {
|
||||
this.button.blur();
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
checkValidity() {
|
||||
if (this.isButton()) {
|
||||
return (this.button as HTMLButtonElement).checkValidity();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
if (this.isButton()) {
|
||||
return (this.button as HTMLButtonElement).reportValidity();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
setCustomValidity(message: string) {
|
||||
if (this.isButton()) {
|
||||
(this.button as HTMLButtonElement).setCustomValidity(message);
|
||||
this.invalid = !(this.button as HTMLButtonElement).checkValidity();
|
||||
}
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
this.hasFocus = false;
|
||||
this.emit('sl-blur');
|
||||
@@ -163,11 +202,29 @@ export default class SlButton extends ShoelaceElement {
|
||||
}
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
if (this.isButton()) {
|
||||
this.button.disabled = this.disabled;
|
||||
this.invalid = !(this.button as HTMLButtonElement).checkValidity();
|
||||
}
|
||||
}
|
||||
|
||||
private isButton() {
|
||||
return this.href ? false : true;
|
||||
}
|
||||
|
||||
private isLink() {
|
||||
return this.href ? true : false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const isLink = this.href ? true : false;
|
||||
const isLink = this.isLink();
|
||||
const tag = isLink ? literal`a` : literal`button`;
|
||||
|
||||
/* eslint-disable lit/binding-positions, lit/no-invalid-html */
|
||||
/* eslint-disable lit/no-invalid-html */
|
||||
/* eslint-disable lit/binding-positions */
|
||||
return html`
|
||||
<${tag}
|
||||
part="base"
|
||||
@@ -226,7 +283,8 @@ export default class SlButton extends ShoelaceElement {
|
||||
${this.loading ? html`<sl-spinner></sl-spinner>` : ''}
|
||||
</${tag}>
|
||||
`;
|
||||
/* eslint-enable lit/binding-positions, lit/no-invalid-html */
|
||||
/* eslint-enable lit/no-invalid-html */
|
||||
/* eslint-enable lit/binding-positions */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -205,14 +205,14 @@ describe('<sl-checkbox>', () => {
|
||||
describe('indeterminate', () => {
|
||||
it('should render indeterminate icon until checked', async () => {
|
||||
const el = await fixture<SlCheckbox>(html`<sl-checkbox indeterminate></sl-checkbox>`);
|
||||
let indeterminateIcon = el.shadowRoot!.querySelector('[part="indeterminate-icon"]')!;
|
||||
let indeterminateIcon = el.shadowRoot!.querySelector('[part~="indeterminate-icon"]')!;
|
||||
|
||||
expect(indeterminateIcon).not.to.be.null;
|
||||
|
||||
el.click();
|
||||
await el.updateComplete;
|
||||
|
||||
indeterminateIcon = el.shadowRoot!.querySelector('[part="indeterminate-icon"]')!;
|
||||
indeterminateIcon = el.shadowRoot!.querySelector('[part~="indeterminate-icon"]')!;
|
||||
|
||||
expect(indeterminateIcon).to.be.null;
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
import '../icon/icon';
|
||||
import styles from './checkbox.styles';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
@@ -32,7 +33,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
* @csspart label - The checkbox label.
|
||||
*/
|
||||
@customElement('sl-checkbox')
|
||||
export default class SlCheckbox extends ShoelaceElement {
|
||||
export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('input[type="checkbox"]') input: HTMLInputElement;
|
||||
@@ -45,6 +46,7 @@ export default class SlCheckbox extends ShoelaceElement {
|
||||
});
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() invalid = false;
|
||||
|
||||
/** Name of the HTML form control. Submitted with the form as part of a name/value pair. */
|
||||
@property() name: string;
|
||||
@@ -64,12 +66,8 @@ export default class SlCheckbox extends ShoelaceElement {
|
||||
/** Draws the checkbox in an indeterminate state. Usually applies to a checkbox that represents "select all" or "select none" when the items to which it applies are a mix of selected and unselected. */
|
||||
@property({ type: Boolean, reflect: true }) indeterminate = false;
|
||||
|
||||
/** This will be true when the control is in an invalid state. Validity is determined by the `required` prop. */
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue('checked')
|
||||
defaultChecked = false;
|
||||
@defaultValue('checked') defaultChecked = false;
|
||||
|
||||
firstUpdated() {
|
||||
this.invalid = !this.input.checkValidity();
|
||||
@@ -90,6 +88,11 @@ export default class SlCheckbox extends ShoelaceElement {
|
||||
this.input.blur();
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
return this.input.reportValidity();
|
||||
@@ -127,6 +130,8 @@ export default class SlCheckbox extends ShoelaceElement {
|
||||
@watch('checked', { waitUntilFirstUpdate: true })
|
||||
@watch('indeterminate', { waitUntilFirstUpdate: true })
|
||||
handleStateChange() {
|
||||
this.input.checked = this.checked; // force a sync update
|
||||
this.input.indeterminate = this.indeterminate; // force a sync update
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type SlColorPicker from './color-picker';
|
||||
describe('<sl-color-picker>', () => {
|
||||
it('should emit change and show correct color when the value changes', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLElement>('[part="trigger"]')!;
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLElement>('[part~="trigger"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const color = 'rgb(255, 204, 0)';
|
||||
|
||||
@@ -41,15 +41,15 @@ describe('<sl-color-picker>', () => {
|
||||
|
||||
it('should display a color when an initial value is provided', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker value="#000"></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part="trigger"]');
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]');
|
||||
|
||||
expect(trigger?.style.color).to.equal('rgb(0, 0, 0)');
|
||||
});
|
||||
|
||||
it('should display a color with opacity when an initial value with opacity is provided', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker opacity value="#ff000050"></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part="trigger"]');
|
||||
const previewButton = el.shadowRoot!.querySelector<HTMLButtonElement>('[part="preview"]');
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]');
|
||||
const previewButton = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="preview"]');
|
||||
const previewColor = getComputedStyle(previewButton!).getPropertyValue('--preview-color');
|
||||
|
||||
expect(trigger!.style.color).to.equal('rgba(255, 0, 0, 0.314)');
|
||||
|
||||
@@ -19,6 +19,7 @@ import '../icon/icon';
|
||||
import '../input/input';
|
||||
import '../visually-hidden/visually-hidden';
|
||||
import styles from './color-picker.styles';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
import type SlDropdown from '../dropdown/dropdown';
|
||||
import type SlInput from '../input/input';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
@@ -84,11 +85,11 @@ declare const EyeDropper: EyeDropperConstructor;
|
||||
* @cssproperty --swatch-size - The size of each predefined color swatch.
|
||||
*/
|
||||
@customElement('sl-color-picker')
|
||||
export default class SlColorPicker extends ShoelaceElement {
|
||||
export default class SlColorPicker extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('[part="input"]') input: SlInput;
|
||||
@query('[part="preview"]') previewButton: HTMLButtonElement;
|
||||
@query('[part~="input"]') input: SlInput;
|
||||
@query('[part~="preview"]') previewButton: HTMLButtonElement;
|
||||
@query('.color-dropdown') dropdown: SlDropdown;
|
||||
|
||||
// @ts-expect-error -- Controller is currently unused
|
||||
@@ -105,13 +106,13 @@ export default class SlColorPicker extends ShoelaceElement {
|
||||
@state() private lightness = 100;
|
||||
@state() private brightness = 100;
|
||||
@state() private alpha = 100;
|
||||
@state() invalid = false;
|
||||
|
||||
/** The current color. */
|
||||
@property() value = '';
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue()
|
||||
defaultValue = '';
|
||||
@defaultValue() defaultValue = '';
|
||||
|
||||
/**
|
||||
* The color picker's label. This will not be displayed, but it will be announced by assistive devices. If you need to
|
||||
@@ -141,12 +142,6 @@ export default class SlColorPicker extends ShoelaceElement {
|
||||
/** Disables the color picker. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/**
|
||||
* This will be true when the control is in an invalid state. Validity is determined by the `setCustomValidity()`
|
||||
* method using the browser's constraint validation API.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/**
|
||||
* Enable this option to prevent the panel from being clipped when the component is placed inside a container with
|
||||
* `overflow: auto|scroll`.
|
||||
@@ -233,22 +228,20 @@ export default class SlColorPicker extends ShoelaceElement {
|
||||
return clamp(((((200 - this.saturation) * brightness) / 100) * 5) / 10, 0, 100);
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
// If the input is invalid, show the dropdown so the browser can focus on it
|
||||
if (!this.inline && this.input.invalid) {
|
||||
return new Promise<void>(resolve => {
|
||||
this.dropdown.addEventListener(
|
||||
'sl-after-show',
|
||||
() => {
|
||||
this.input.reportValidity();
|
||||
resolve();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
this.dropdown.show();
|
||||
});
|
||||
// If the input is inline and invalid, show the dropdown so the browser can focus on it
|
||||
this.dropdown.show();
|
||||
this.addEventListener('sl-after-show', () => this.input.reportValidity(), { once: true });
|
||||
return this.checkValidity();
|
||||
}
|
||||
|
||||
return this.input.reportValidity();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('<sl-dialog>', () => {
|
||||
const el = await fixture<SlDialog>(html`
|
||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.hidden).to.be.false;
|
||||
});
|
||||
@@ -18,7 +18,7 @@ describe('<sl-dialog>', () => {
|
||||
const el = await fixture<SlDialog>(
|
||||
html` <sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog> `
|
||||
);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.hidden).to.be.true;
|
||||
});
|
||||
@@ -27,7 +27,7 @@ describe('<sl-dialog>', () => {
|
||||
const el = await fixture<SlDialog>(html`
|
||||
<sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('<sl-dialog>', () => {
|
||||
const el = await fixture<SlDialog>(html`
|
||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('<sl-dialog>', () => {
|
||||
const el = await fixture<SlDialog>(html`
|
||||
<sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('<sl-dialog>', () => {
|
||||
const el = await fixture<SlDialog>(html`
|
||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
@@ -107,7 +107,7 @@ describe('<sl-dialog>', () => {
|
||||
const el = await fixture<SlDialog>(html`
|
||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`);
|
||||
const overlay = el.shadowRoot!.querySelector<HTMLElement>('[part="overlay"]')!;
|
||||
const overlay = el.shadowRoot!.querySelector<HTMLElement>('[part~="overlay"]')!;
|
||||
|
||||
el.addEventListener('sl-request-close', event => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('<sl-drawer>', () => {
|
||||
const el = await fixture<SlDrawer>(html`
|
||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.hidden).to.be.false;
|
||||
});
|
||||
@@ -18,7 +18,7 @@ describe('<sl-drawer>', () => {
|
||||
const el = await fixture<SlDrawer>(
|
||||
html` <sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer> `
|
||||
);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.hidden).to.be.true;
|
||||
});
|
||||
@@ -27,7 +27,7 @@ describe('<sl-drawer>', () => {
|
||||
const el = await fixture<SlDrawer>(html`
|
||||
<sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('<sl-drawer>', () => {
|
||||
const el = await fixture<SlDrawer>(html`
|
||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('<sl-drawer>', () => {
|
||||
const el = await fixture<SlDrawer>(html`
|
||||
<sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('<sl-drawer>', () => {
|
||||
const el = await fixture<SlDrawer>(html`
|
||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
@@ -107,7 +107,7 @@ describe('<sl-drawer>', () => {
|
||||
const el = await fixture<SlDrawer>(html`
|
||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`);
|
||||
const overlay = el.shadowRoot!.querySelector<HTMLElement>('[part="overlay"]')!;
|
||||
const overlay = el.shadowRoot!.querySelector<HTMLElement>('[part~="overlay"]')!;
|
||||
|
||||
el.addEventListener('sl-request-close', event => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -95,7 +95,7 @@ export default class SlDrawer extends ShoelaceElement {
|
||||
|
||||
/**
|
||||
* By default, the drawer slides out of its containing block (usually the viewport). To make the drawer slide out of
|
||||
* its parent element, set this prop and add `position: relative` to the parent.
|
||||
* its parent element, set this attribute and add `position: relative` to the parent.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) contained = false;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('<sl-dropdown>', () => {
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
||||
|
||||
expect(panel.hidden).to.be.false;
|
||||
});
|
||||
@@ -31,7 +31,7 @@ describe('<sl-dropdown>', () => {
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
||||
|
||||
expect(panel.hidden).to.be.true;
|
||||
});
|
||||
@@ -47,7 +47,7 @@ describe('<sl-dropdown>', () => {
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('<sl-dropdown>', () => {
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('<sl-dropdown>', () => {
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
@@ -128,7 +128,7 @@ describe('<sl-dropdown>', () => {
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ describe('<sl-image-comparer>', () => {
|
||||
</sl-image-comparer>
|
||||
`);
|
||||
|
||||
const afterPart = el.shadowRoot!.querySelector<HTMLElement>('[part="after"]')!;
|
||||
const afterPart = el.shadowRoot!.querySelector<HTMLElement>('[part~="after"]')!;
|
||||
const iconContainer = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name="handle-icon"]')!;
|
||||
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part="handle"]')!;
|
||||
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
|
||||
|
||||
expect(el.position).to.equal(50);
|
||||
expect(afterPart.getAttribute('style')).to.equal('clip-path:inset(0 50% 0 0);');
|
||||
@@ -49,7 +49,7 @@ describe('<sl-image-comparer>', () => {
|
||||
</sl-image-comparer>
|
||||
`);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
base.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
@@ -69,7 +69,7 @@ describe('<sl-image-comparer>', () => {
|
||||
</sl-image-comparer>
|
||||
`);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
base.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
@@ -89,7 +89,7 @@ describe('<sl-image-comparer>', () => {
|
||||
</sl-image-comparer>
|
||||
`);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
base.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
@@ -109,7 +109,7 @@ describe('<sl-image-comparer>', () => {
|
||||
</sl-image-comparer>
|
||||
`);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
base.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
@@ -132,7 +132,7 @@ describe('<sl-image-comparer>', () => {
|
||||
el.position = 0;
|
||||
await el.updateComplete;
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
base.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
@@ -155,7 +155,7 @@ describe('<sl-image-comparer>', () => {
|
||||
el.position = 100;
|
||||
await el.updateComplete;
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
base.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
@@ -175,7 +175,7 @@ describe('<sl-image-comparer>', () => {
|
||||
</sl-image-comparer>
|
||||
`);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
base.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
@@ -196,7 +196,7 @@ describe('<sl-image-comparer>', () => {
|
||||
</sl-image-comparer>
|
||||
`);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
base.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
@@ -227,8 +227,8 @@ describe('<sl-image-comparer>', () => {
|
||||
<div slot="after" style="width: 50px"></div>
|
||||
</sl-image-comparer>
|
||||
`);
|
||||
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part="handle"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const rect = base.getBoundingClientRect();
|
||||
const offsetX = rect.left + window.pageXOffset;
|
||||
const offsetY = rect.top + window.pageYOffset;
|
||||
|
||||
@@ -10,9 +10,46 @@ describe('<sl-input>', () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('default properties', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input></sl-input> `);
|
||||
|
||||
expect(el.type).to.equal('text');
|
||||
expect(el.size).to.equal('medium');
|
||||
expect(el.name).to.equal('');
|
||||
expect(el.value).to.equal('');
|
||||
expect(el.defaultValue).to.equal('');
|
||||
expect(el.filled).to.be.false;
|
||||
expect(el.pill).to.be.false;
|
||||
expect(el.label).to.equal('');
|
||||
expect(el.helpText).to.equal('');
|
||||
expect(el.clearable).to.be.false;
|
||||
expect(el.passwordToggle).to.be.false;
|
||||
expect(el.passwordVisible).to.be.false;
|
||||
expect(el.noSpinButtons).to.be.false;
|
||||
expect(el.placeholder).to.equal('');
|
||||
expect(el.disabled).to.be.false;
|
||||
expect(el.readonly).to.be.false;
|
||||
expect(el.minlength).to.be.undefined;
|
||||
expect(el.maxlength).to.be.undefined;
|
||||
expect(el.min).to.be.undefined;
|
||||
expect(el.max).to.be.undefined;
|
||||
expect(el.step).to.be.undefined;
|
||||
expect(el.pattern).to.be.undefined;
|
||||
expect(el.required).to.be.false;
|
||||
expect(el.autocapitalize).to.be.undefined;
|
||||
expect(el.autocorrect).to.be.undefined;
|
||||
expect(el.autocomplete).to.be.undefined;
|
||||
expect(el.autofocus).to.be.undefined;
|
||||
expect(el.enterkeyhint).to.be.undefined;
|
||||
expect(el.spellcheck).to.be.undefined;
|
||||
expect(el.inputmode).to.be.undefined;
|
||||
expect(el.valueAsDate).to.be.null;
|
||||
expect(isNaN(el.valueAsNumber)).to.be.true;
|
||||
});
|
||||
|
||||
it('should be disabled with the disabled attribute', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input disabled></sl-input> `);
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>('[part="input"]')!;
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>('[part~="input"]')!;
|
||||
|
||||
expect(input.disabled).to.be.true;
|
||||
});
|
||||
@@ -39,7 +76,7 @@ describe('<sl-input>', () => {
|
||||
|
||||
it('should focus the input when clicking on the label', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input label="Name"></sl-input> `);
|
||||
const label = el.shadowRoot!.querySelector('[part="form-control-label"]')!;
|
||||
const label = el.shadowRoot!.querySelector('[part~="form-control-label"]')!;
|
||||
const submitHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-focus', submitHandler);
|
||||
@@ -208,5 +245,52 @@ describe('<sl-input>', () => {
|
||||
await el.updateComplete;
|
||||
expect(el.invalid).to.be.true;
|
||||
});
|
||||
|
||||
it('should increment by step if stepUp() is called', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step="2" value="2"></sl-input> `);
|
||||
|
||||
el.stepUp();
|
||||
await el.updateComplete;
|
||||
expect(el.value).to.equal('4');
|
||||
});
|
||||
|
||||
it('should decrement by step if stepDown() is called', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step="2" value="2"></sl-input> `);
|
||||
|
||||
el.stepDown();
|
||||
await el.updateComplete;
|
||||
expect(el.value).to.equal('0');
|
||||
});
|
||||
|
||||
it('should fire sl-input and sl-change if stepUp() is called', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step="2" value="2"></sl-input> `);
|
||||
|
||||
const inputHandler = sinon.spy();
|
||||
const changeHandler = sinon.spy();
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
|
||||
el.stepUp();
|
||||
|
||||
await waitUntil(() => inputHandler.calledOnce);
|
||||
await waitUntil(() => changeHandler.calledOnce);
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should fire sl-input and sl-change if stepDown() is called', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step="2" value="2"></sl-input> `);
|
||||
|
||||
const inputHandler = sinon.spy();
|
||||
const changeHandler = sinon.spy();
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
|
||||
el.stepUp();
|
||||
|
||||
await waitUntil(() => inputHandler.calledOnce);
|
||||
await waitUntil(() => changeHandler.calledOnce);
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { watch } from '../../internal/watch';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import '../icon/icon';
|
||||
import styles from './input.styles';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
// It's currently impossible to hide Firefox's built-in clear icon when using <input type="date|time">, so we need this
|
||||
@@ -58,7 +59,7 @@ const isFirefox = isChromium ? false : navigator.userAgent.includes('Firefox');
|
||||
* @csspart suffix - The input suffix container.
|
||||
*/
|
||||
@customElement('sl-input')
|
||||
export default class SlInput extends ShoelaceElement {
|
||||
export default class SlInput extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('.input__control') input: HTMLInputElement;
|
||||
@@ -68,6 +69,7 @@ export default class SlInput extends ShoelaceElement {
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() invalid = false;
|
||||
|
||||
/** The input's type. */
|
||||
@property({ reflect: true }) type:
|
||||
@@ -86,14 +88,13 @@ export default class SlInput extends ShoelaceElement {
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
/** The input's name attribute. */
|
||||
@property() name: string;
|
||||
@property() name = '';
|
||||
|
||||
/** The input's value attribute. */
|
||||
@property() value = '';
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue()
|
||||
defaultValue = '';
|
||||
@defaultValue() defaultValue = '';
|
||||
|
||||
/** Draws a filled input. */
|
||||
@property({ type: Boolean, reflect: true }) filled = false;
|
||||
@@ -120,7 +121,7 @@ export default class SlInput extends ShoelaceElement {
|
||||
@property({ attribute: 'no-spin-buttons', type: Boolean }) noSpinButtons = false;
|
||||
|
||||
/** The input's placeholder text. */
|
||||
@property() placeholder: string;
|
||||
@property() placeholder = '';
|
||||
|
||||
/** Disables the input. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
@@ -135,10 +136,10 @@ export default class SlInput extends ShoelaceElement {
|
||||
@property({ type: Number }) maxlength: number;
|
||||
|
||||
/** The input's minimum value. */
|
||||
@property() min: number | string;
|
||||
@property() min: number;
|
||||
|
||||
/** The input's maximum value. */
|
||||
@property() max: number | string;
|
||||
@property() max: number;
|
||||
|
||||
/**
|
||||
* Specifies the granularity that the value must adhere to, or the special value `any` which means no stepping is
|
||||
@@ -152,12 +153,6 @@ export default class SlInput extends ShoelaceElement {
|
||||
/** Makes the input a required field. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
/**
|
||||
* This will be true when the control is in an invalid state. Validity is determined by props such as `type`,
|
||||
* `required`, `minlength`, `maxlength`, and `pattern` using the browser's constraint validation API.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/** The input's autocapitalize attribute. */
|
||||
@property() autocapitalize: 'off' | 'none' | 'on' | 'sentences' | 'words' | 'characters';
|
||||
|
||||
@@ -252,6 +247,38 @@ export default class SlInput extends ShoelaceElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Displays the browser picker for an input element (only works if the browser supports it for the input type). */
|
||||
showPicker() {
|
||||
if ('showPicker' in HTMLInputElement.prototype) {
|
||||
this.input.showPicker();
|
||||
}
|
||||
}
|
||||
|
||||
/** Increments the value of a numeric input type by the value of the step attribute. */
|
||||
stepUp() {
|
||||
this.input.stepUp();
|
||||
if (this.value !== this.input.value) {
|
||||
this.value = this.input.value;
|
||||
this.emit('sl-input');
|
||||
this.emit('sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
/** Decrements the value of a numeric input type by the value of the step attribute. */
|
||||
stepDown() {
|
||||
this.input.stepDown();
|
||||
if (this.value !== this.input.value) {
|
||||
this.value = this.input.value;
|
||||
this.emit('sl-input');
|
||||
this.emit('sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
return this.input.reportValidity();
|
||||
@@ -338,6 +365,7 @@ export default class SlInput extends ShoelaceElement {
|
||||
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
handleValueChange() {
|
||||
this.input.value = this.value; // force a sync update
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ describe('<sl-progress-bar>', () => {
|
||||
el = await fixture<SlProgressBar>(
|
||||
html`<sl-progress-bar title="Titled Progress Ring" value="25"></sl-progress-bar>`
|
||||
);
|
||||
base = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
indicator = el.shadowRoot!.querySelector('[part="indicator"]')!;
|
||||
base = el.shadowRoot!.querySelector('[part~="base"]')!;
|
||||
indicator = el.shadowRoot!.querySelector('[part~="indicator"]')!;
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
@@ -46,7 +46,7 @@ describe('<sl-progress-bar>', () => {
|
||||
el = await fixture<SlProgressBar>(
|
||||
html`<sl-progress-bar title="Titled Progress Ring" indeterminate></sl-progress-bar>`
|
||||
);
|
||||
base = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
base = el.shadowRoot!.querySelector('[part~="base"]')!;
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('<sl-progress-ring>', () => {
|
||||
el = await fixture<SlProgressRing>(
|
||||
html`<sl-progress-ring title="Titled Progress Ring" value="25"></sl-progress-ring>`
|
||||
);
|
||||
base = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
base = el.shadowRoot!.querySelector('[part~="base"]')!;
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
import '../button-group/button-group';
|
||||
import styles from './radio-group.styles';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
import type SlRadioButton from '../radio-button/radio-button';
|
||||
import type SlRadio from '../radio/radio';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
@@ -32,11 +33,11 @@ import type { CSSResultGroup } from 'lit';
|
||||
* @csspart button-group__base - The button group's `base` part.
|
||||
*/
|
||||
@customElement('sl-radio-group')
|
||||
export default class SlRadioGroup extends ShoelaceElement {
|
||||
export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
protected readonly formSubmitController = new FormSubmitController(this, {
|
||||
defaultValue: (control: SlRadioGroup) => control.defaultValue
|
||||
defaultValue: control => control.defaultValue
|
||||
});
|
||||
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
|
||||
@@ -46,7 +47,8 @@ export default class SlRadioGroup extends ShoelaceElement {
|
||||
@state() private hasButtonGroup = false;
|
||||
@state() private errorMessage = '';
|
||||
@state() private customErrorMessage = '';
|
||||
@state() private defaultValue = '';
|
||||
@state() defaultValue = '';
|
||||
@state() invalid = false;
|
||||
|
||||
/**
|
||||
* The radio group label. Required for proper accessibility. If you need to display HTML, you can use the `label` slot
|
||||
@@ -63,12 +65,6 @@ export default class SlRadioGroup extends ShoelaceElement {
|
||||
/** The name assigned to the radio controls. */
|
||||
@property() name = 'option';
|
||||
|
||||
/**
|
||||
* This will be true when the control is in an invalid state. Validity is determined by props such as `type`,
|
||||
* `required`, `minlength`, `maxlength`, and `pattern` using the browser's constraint validation API.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/** Ensures a child radio is checked before allowing the containing form to submit. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
@@ -89,6 +85,11 @@ export default class SlRadioGroup extends ShoelaceElement {
|
||||
this.invalid = !this.validity.valid;
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
checkValidity() {
|
||||
return this.validity.valid;
|
||||
}
|
||||
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
setCustomValidity(message = '') {
|
||||
this.customErrorMessage = message;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import { serialize } from '../../utilities/form';
|
||||
import type SlRange from './range';
|
||||
|
||||
@@ -8,13 +9,67 @@ describe('<sl-range>', () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('default properties', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range></sl-range> `);
|
||||
|
||||
expect(el.name).to.equal('');
|
||||
expect(el.value).to.equal(0);
|
||||
expect(el.label).to.equal('');
|
||||
expect(el.helpText).to.equal('');
|
||||
expect(el.disabled).to.be.false;
|
||||
expect(el.invalid).to.be.false;
|
||||
expect(el.min).to.equal(0);
|
||||
expect(el.max).to.equal(100);
|
||||
expect(el.step).to.equal(1);
|
||||
expect(el.tooltip).to.equal('top');
|
||||
expect(el.defaultValue).to.equal(0);
|
||||
});
|
||||
|
||||
it('should be disabled with the disabled attribute', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range disabled></sl-range> `);
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>('[part="input"]')!;
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>('[part~="input"]')!;
|
||||
|
||||
expect(input.disabled).to.be.true;
|
||||
});
|
||||
|
||||
describe('step', () => {
|
||||
it('should increment by step if stepUp() is called', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range step="2" value="2"></sl-range> `);
|
||||
|
||||
el.stepUp();
|
||||
await el.updateComplete;
|
||||
expect(el.value).to.equal(4);
|
||||
});
|
||||
|
||||
it('should decrement by step if stepDown() is called', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range step="2" value="2"></sl-range> `);
|
||||
|
||||
el.stepDown();
|
||||
await el.updateComplete;
|
||||
expect(el.value).to.equal(0);
|
||||
});
|
||||
|
||||
it('should fire sl-change if stepUp() is called', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range step="2" value="2"></sl-range> `);
|
||||
|
||||
const changeHandler = sinon.spy();
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.stepUp();
|
||||
await waitUntil(() => changeHandler.calledOnce);
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should fire sl-change if stepDown() is called', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range step="2" value="2"></sl-range> `);
|
||||
|
||||
const changeHandler = sinon.spy();
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.stepUp();
|
||||
await waitUntil(() => changeHandler.calledOnce);
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when serializing', () => {
|
||||
it('should serialize its name and value with FormData', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html` <form><sl-range name="a" value="1"></sl-range></form> `);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import styles from './range.styles';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
@@ -41,7 +42,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
* @cssproperty --track-active-offset - The point of origin of the active track.
|
||||
*/
|
||||
@customElement('sl-range')
|
||||
export default class SlRange extends ShoelaceElement {
|
||||
export default class SlRange extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('.range__control') input: HTMLInputElement;
|
||||
@@ -55,6 +56,7 @@ export default class SlRange extends ShoelaceElement {
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() private hasTooltip = false;
|
||||
@state() invalid = false;
|
||||
|
||||
/** The input's name attribute. */
|
||||
@property() name = '';
|
||||
@@ -71,12 +73,6 @@ export default class SlRange extends ShoelaceElement {
|
||||
/** Disables the range. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/**
|
||||
* This will be true when the control is in an invalid state. Validity in range inputs is determined by the message
|
||||
* provided by the `setCustomValidity` method.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/** The input's min attribute. */
|
||||
@property({ type: Number }) min = 0;
|
||||
|
||||
@@ -93,8 +89,7 @@ export default class SlRange extends ShoelaceElement {
|
||||
@property({ attribute: false }) tooltipFormatter: (value: number) => string = (value: number) => value.toString();
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue()
|
||||
defaultValue = 0;
|
||||
@defaultValue() defaultValue = 0;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
@@ -128,6 +123,34 @@ export default class SlRange extends ShoelaceElement {
|
||||
this.input.blur();
|
||||
}
|
||||
|
||||
/** Increments the value of the input by the value of the step attribute. */
|
||||
stepUp() {
|
||||
this.input.stepUp();
|
||||
if (this.value !== Number(this.input.value)) {
|
||||
this.value = Number(this.input.value);
|
||||
this.emit('sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
/** Decrements the value of the input by the value of the step attribute. */
|
||||
stepDown() {
|
||||
this.input.stepDown();
|
||||
if (this.value !== Number(this.input.value)) {
|
||||
this.value = Number(this.input.value);
|
||||
this.emit('sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
return this.input.reportValidity();
|
||||
}
|
||||
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
setCustomValidity(message: string) {
|
||||
this.input.setCustomValidity(message);
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('<sl-rating>', () => {
|
||||
const el = await fixture<SlRating>(html` <sl-rating label="Test"></sl-rating> `);
|
||||
await expect(el).to.be.accessible();
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.getAttribute('role')).to.equal('slider');
|
||||
expect(base.getAttribute('aria-disabled')).to.equal('false');
|
||||
@@ -20,7 +20,7 @@ describe('<sl-rating>', () => {
|
||||
|
||||
it('should be readonly with the readonly attribute', async () => {
|
||||
const el = await fixture<SlRating>(html` <sl-rating label="Test" readonly></sl-rating> `);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.getAttribute('aria-readonly')).to.equal('true');
|
||||
expect(base.getAttribute('class')).to.equal(' rating rating--readonly ');
|
||||
@@ -28,7 +28,7 @@ describe('<sl-rating>', () => {
|
||||
|
||||
it('should be disabled with the disabled attribute', async () => {
|
||||
const el = await fixture<SlRating>(html` <sl-rating label="Test" disabled></sl-rating> `);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.getAttribute('aria-disabled')).to.equal('true');
|
||||
expect(base.getAttribute('class')).to.equal(' rating rating--disabled ');
|
||||
@@ -36,14 +36,14 @@ describe('<sl-rating>', () => {
|
||||
|
||||
it('should set max value by attribute', async () => {
|
||||
const el = await fixture<SlRating>(html` <sl-rating label="Test" max="12"></sl-rating> `);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.getAttribute('aria-valuemax')).to.equal('12');
|
||||
});
|
||||
|
||||
it('should set selected value by attribute', async () => {
|
||||
const el = await fixture<SlRating>(html` <sl-rating label="Test" value="3"></sl-rating> `);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.getAttribute('aria-valuenow')).to.equal('3');
|
||||
});
|
||||
@@ -52,7 +52,7 @@ describe('<sl-rating>', () => {
|
||||
it('should focus inner div', async () => {
|
||||
const el = await fixture<SlRating>(html` <sl-rating label="Test"></sl-rating> `);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
el.focus();
|
||||
await el.updateComplete;
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('<sl-select>', () => {
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
</sl-select>
|
||||
`);
|
||||
const label = el.shadowRoot!.querySelector('[part="form-control-label"]')!;
|
||||
const label = el.shadowRoot!.querySelector('[part~="form-control-label"]')!;
|
||||
const submitHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-focus', submitHandler);
|
||||
@@ -119,7 +119,7 @@ describe('<sl-select>', () => {
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
</sl-select>
|
||||
`);
|
||||
const displayLabel = el.shadowRoot!.querySelector('[part="display-label"]')!;
|
||||
const displayLabel = el.shadowRoot!.querySelector('[part~="display-label"]')!;
|
||||
const menuItem = el.querySelector('sl-menu-item')!;
|
||||
|
||||
expect(displayLabel.textContent?.trim()).to.equal('Option 1');
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../icon/icon';
|
||||
import '../menu/menu';
|
||||
import '../tag/tag';
|
||||
import styles from './select.styles';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
import type SlDropdown from '../dropdown/dropdown';
|
||||
import type SlIconButton from '../icon-button/icon-button';
|
||||
import type SlMenuItem from '../menu-item/menu-item';
|
||||
@@ -41,6 +42,7 @@ import type { TemplateResult, CSSResultGroup } from 'lit';
|
||||
*
|
||||
* @event sl-clear - Emitted when the clear button is activated.
|
||||
* @event sl-change - Emitted when the control's value changes.
|
||||
* @event sl-input - Emitted when the control receives input.
|
||||
* @event sl-focus - Emitted when the control gains focus.
|
||||
* @event sl-blur - Emitted when the control loses focus.
|
||||
*
|
||||
@@ -63,7 +65,7 @@ import type { TemplateResult, CSSResultGroup } from 'lit';
|
||||
* @csspart tags - The container in which multi select options are rendered.
|
||||
*/
|
||||
@customElement('sl-select')
|
||||
export default class SlSelect extends ShoelaceElement {
|
||||
export default class SlSelect extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('.select') dropdown: SlDropdown;
|
||||
@@ -82,6 +84,7 @@ export default class SlSelect extends ShoelaceElement {
|
||||
@state() private isOpen = false;
|
||||
@state() private displayLabel = '';
|
||||
@state() private displayTags: TemplateResult[] = [];
|
||||
@state() invalid = false;
|
||||
|
||||
/** Enables multi select. With this enabled, value will be an array. */
|
||||
@property({ type: Boolean, reflect: true }) multiple = false;
|
||||
@@ -137,12 +140,8 @@ export default class SlSelect extends ShoelaceElement {
|
||||
/** Adds a clear button when the select is populated. */
|
||||
@property({ type: Boolean }) clearable = false;
|
||||
|
||||
/** This will be true when the control is in an invalid state. Validity is determined by the `required` prop. */
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue()
|
||||
defaultValue = '';
|
||||
@defaultValue() defaultValue = '';
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
@@ -163,6 +162,11 @@ export default class SlSelect extends ShoelaceElement {
|
||||
this.resizeObserver.unobserve(this);
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
return this.input.reportValidity();
|
||||
@@ -365,8 +369,11 @@ export default class SlSelect extends ShoelaceElement {
|
||||
async handleValueChange() {
|
||||
this.syncItemsFromValue();
|
||||
await this.updateComplete;
|
||||
|
||||
this.invalid = !this.input.checkValidity();
|
||||
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
resizeMenu() {
|
||||
@@ -485,7 +492,7 @@ export default class SlSelect extends ShoelaceElement {
|
||||
<sl-dropdown
|
||||
part="base"
|
||||
.hoist=${this.hoist}
|
||||
.placement=${this.placement}
|
||||
.placement=${this.placement === 'bottom' ? 'bottom-start' : 'top-start'}
|
||||
.stayOpenOnSelect=${this.multiple}
|
||||
.containingElement=${this as HTMLElement}
|
||||
?disabled=${this.disabled}
|
||||
|
||||
@@ -7,8 +7,8 @@ describe('<sl-skeleton>', () => {
|
||||
|
||||
await expect(el).to.be.accessible();
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const indicator = el.shadowRoot!.querySelector<HTMLElement>('[part="indicator"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const indicator = el.shadowRoot!.querySelector<HTMLElement>('[part~="indicator"]')!;
|
||||
|
||||
expect(base.getAttribute('class')).to.equal(' skeleton ');
|
||||
expect(base.getAttribute('aria-busy')).to.equal('true');
|
||||
@@ -19,7 +19,7 @@ describe('<sl-skeleton>', () => {
|
||||
it('should set pulse effect by attribute', async () => {
|
||||
const el = await fixture<SlSkeleton>(html` <sl-skeleton effect="pulse"></sl-skeleton> `);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.getAttribute('class')).to.equal(' skeleton skeleton--pulse ');
|
||||
});
|
||||
@@ -27,7 +27,7 @@ describe('<sl-skeleton>', () => {
|
||||
it('should set sheen effect by attribute', async () => {
|
||||
const el = await fixture<SlSkeleton>(html` <sl-skeleton effect="sheen"></sl-skeleton> `);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.getAttribute('class')).to.equal(' skeleton skeleton--sheen ');
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('<sl-spinner>', () => {
|
||||
|
||||
it('should have a role of "status".', () => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions
|
||||
const base = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector('[part~="base"]')!;
|
||||
expect(base).have.attribute('role', 'progressbar');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import { FormSubmitController } from '../../internal/form';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './switch.styles';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
@@ -32,7 +33,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
* @cssproperty --thumb-size - The size of the thumb.
|
||||
*/
|
||||
@customElement('sl-switch')
|
||||
export default class SlSwitch extends ShoelaceElement {
|
||||
export default class SlSwitch extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('input[type="checkbox"]') input: HTMLInputElement;
|
||||
@@ -45,6 +46,7 @@ export default class SlSwitch extends ShoelaceElement {
|
||||
});
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() invalid = false;
|
||||
|
||||
/** The switch's name attribute. */
|
||||
@property() name: string;
|
||||
@@ -61,12 +63,8 @@ export default class SlSwitch extends ShoelaceElement {
|
||||
/** Draws the switch in a checked state. */
|
||||
@property({ type: Boolean, reflect: true }) checked = false;
|
||||
|
||||
/** This will be true when the control is in an invalid state. Validity is determined by the `required` prop. */
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue('checked')
|
||||
defaultChecked = false;
|
||||
@defaultValue('checked') defaultChecked = false;
|
||||
|
||||
firstUpdated() {
|
||||
this.invalid = !this.input.checkValidity();
|
||||
@@ -87,6 +85,11 @@ export default class SlSwitch extends ShoelaceElement {
|
||||
this.input.blur();
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
return this.input.reportValidity();
|
||||
@@ -105,7 +108,7 @@ export default class SlSwitch extends ShoelaceElement {
|
||||
|
||||
@watch('checked', { waitUntilFirstUpdate: true })
|
||||
handleCheckedChange() {
|
||||
this.input.checked = this.checked;
|
||||
this.input.checked = this.checked; // force a sync update
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('<sl-tab>', () => {
|
||||
it('should render default tab', async () => {
|
||||
const el = await fixture<SlTab>(html` <sl-tab>Test</sl-tab> `);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(el.getAttribute('role')).to.equal('tab');
|
||||
expect(el.getAttribute('aria-disabled')).to.equal('false');
|
||||
@@ -30,7 +30,7 @@ describe('<sl-tab>', () => {
|
||||
it('should disable tab by attribute', async () => {
|
||||
const el = await fixture<SlTab>(html` <sl-tab disabled>Test</sl-tab> `);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(el.disabled).to.equal(true);
|
||||
expect(el.getAttribute('aria-disabled')).to.equal('true');
|
||||
@@ -41,7 +41,7 @@ describe('<sl-tab>', () => {
|
||||
it('should set active tab by attribute', async () => {
|
||||
const el = await fixture<SlTab>(html` <sl-tab active>Test</sl-tab> `);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(el.active).to.equal(true);
|
||||
expect(el.getAttribute('aria-selected')).to.equal('true');
|
||||
@@ -52,8 +52,8 @@ describe('<sl-tab>', () => {
|
||||
it('should set closable by attribute', async () => {
|
||||
const el = await fixture<SlTab>(html` <sl-tab closable>Test</sl-tab> `);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const closeButton = el.shadowRoot!.querySelector('[part="close-button"]');
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const closeButton = el.shadowRoot!.querySelector('[part~="close-button"]');
|
||||
|
||||
expect(el.closable).to.equal(true);
|
||||
expect(base.getAttribute('class')).to.equal(' tab tab--closable ');
|
||||
@@ -64,7 +64,7 @@ describe('<sl-tab>', () => {
|
||||
it('should focus inner div', async () => {
|
||||
const el = await fixture<SlTab>(html` <sl-tab>Test</sl-tab> `);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
el.focus();
|
||||
await el.updateComplete;
|
||||
@@ -91,7 +91,7 @@ describe('<sl-tab>', () => {
|
||||
it('should emit close event when close button clicked', async () => {
|
||||
const el = await fixture<SlTab>(html` <sl-tab closable>Test</sl-tab> `);
|
||||
|
||||
const closeButton = el.shadowRoot!.querySelector<HTMLButtonElement>('[part="close-button"]')!;
|
||||
const closeButton = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="close-button"]')!;
|
||||
const spy = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-close', spy, { once: true });
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('<sl-tag>', () => {
|
||||
it('should render default tag', async () => {
|
||||
const el = await fixture<SlTag>(html` <sl-tag>Test</sl-tag> `);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(el.getAttribute('size')).to.equal('medium');
|
||||
expect(base.getAttribute('class')).to.equal(' tag tag--neutral tag--medium ');
|
||||
@@ -15,7 +15,7 @@ describe('<sl-tag>', () => {
|
||||
it('should set variant by attribute', async () => {
|
||||
const el = await fixture<SlTag>(html` <sl-tag variant="danger">Test</sl-tag> `);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.getAttribute('class')).to.equal(' tag tag--danger tag--medium ');
|
||||
});
|
||||
@@ -23,7 +23,7 @@ describe('<sl-tag>', () => {
|
||||
it('should set size by attribute', async () => {
|
||||
const el = await fixture<SlTag>(html` <sl-tag size="large">Test</sl-tag> `);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.getAttribute('class')).to.equal(' tag tag--neutral tag--large ');
|
||||
});
|
||||
@@ -31,7 +31,7 @@ describe('<sl-tag>', () => {
|
||||
it('should set pill-attribute by attribute', async () => {
|
||||
const el = await fixture<SlTag>(html` <sl-tag pill>Test</sl-tag> `);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.getAttribute('class')).to.equal(' tag tag--neutral tag--medium tag--pill ');
|
||||
});
|
||||
@@ -39,8 +39,8 @@ describe('<sl-tag>', () => {
|
||||
it('should set removable by attribute', async () => {
|
||||
const el = await fixture<SlTag>(html` <sl-tag removable>Test</sl-tag> `);
|
||||
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const removeButton = el.shadowRoot!.querySelector('[part="remove-button"]');
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
const removeButton = el.shadowRoot!.querySelector('[part~="remove-button"]');
|
||||
|
||||
expect(el.removable).to.equal(true);
|
||||
expect(base.getAttribute('class')).to.equal(' tag tag--neutral tag--medium tag--removable ');
|
||||
@@ -51,7 +51,7 @@ describe('<sl-tag>', () => {
|
||||
it('should emit remove event when remove button clicked', async () => {
|
||||
const el = await fixture<SlTag>(html` <sl-tag removable>Test</sl-tag> `);
|
||||
|
||||
const removeButton = el.shadowRoot!.querySelector<HTMLButtonElement>('[part="remove-button"]')!;
|
||||
const removeButton = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="remove-button"]')!;
|
||||
const spy = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-remove', spy, { once: true });
|
||||
|
||||
@@ -11,14 +11,14 @@ describe('<sl-textarea>', () => {
|
||||
|
||||
it('should be disabled with the disabled attribute', async () => {
|
||||
const el = await fixture<SlTextarea>(html` <sl-textarea disabled></sl-textarea> `);
|
||||
const textarea = el.shadowRoot!.querySelector<HTMLTextAreaElement>('[part="textarea"]')!;
|
||||
const textarea = el.shadowRoot!.querySelector<HTMLTextAreaElement>('[part~="textarea"]')!;
|
||||
|
||||
expect(textarea.disabled).to.be.true;
|
||||
});
|
||||
|
||||
it('should focus the textarea when clicking on the label', async () => {
|
||||
const el = await fixture<SlTextarea>(html` <sl-textarea label="Name"></sl-textarea> `);
|
||||
const label = el.shadowRoot!.querySelector('[part="form-control-label"]')!;
|
||||
const label = el.shadowRoot!.querySelector('[part~="form-control-label"]')!;
|
||||
const submitHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-focus', submitHandler);
|
||||
|
||||
@@ -9,6 +9,7 @@ import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './textarea.styles';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
@@ -33,7 +34,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
* @csspart textarea - The textarea control.
|
||||
*/
|
||||
@customElement('sl-textarea')
|
||||
export default class SlTextarea extends ShoelaceElement {
|
||||
export default class SlTextarea extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('.textarea__control') input: HTMLTextAreaElement;
|
||||
@@ -44,12 +45,13 @@ export default class SlTextarea extends ShoelaceElement {
|
||||
private resizeObserver: ResizeObserver;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() invalid = false;
|
||||
|
||||
/** The textarea's size. */
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
/** The textarea's name attribute. */
|
||||
@property() name: string;
|
||||
@property() name = '';
|
||||
|
||||
/** The textarea's value attribute. */
|
||||
@property() value = '';
|
||||
@@ -64,7 +66,7 @@ export default class SlTextarea extends ShoelaceElement {
|
||||
@property({ attribute: 'help-text' }) helpText = '';
|
||||
|
||||
/** The textarea's placeholder text. */
|
||||
@property() placeholder: string;
|
||||
@property() placeholder = '';
|
||||
|
||||
/** The number of rows to display by default. */
|
||||
@property({ type: Number }) rows = 4;
|
||||
@@ -87,12 +89,6 @@ export default class SlTextarea extends ShoelaceElement {
|
||||
/** Makes the textarea a required field. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
/**
|
||||
* This will be true when the control is in an invalid state. Validity is determined by props such as `type`,
|
||||
* `required`, `minlength`, and `maxlength` using the browser's constraint validation API.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/** The textarea's autocapitalize attribute. */
|
||||
@property() autocapitalize: 'off' | 'none' | 'on' | 'sentences' | 'words' | 'characters';
|
||||
|
||||
@@ -118,8 +114,7 @@ export default class SlTextarea extends ShoelaceElement {
|
||||
@property() inputmode: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue()
|
||||
defaultValue = '';
|
||||
@defaultValue() defaultValue = '';
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
@@ -200,6 +195,11 @@ export default class SlTextarea extends ShoelaceElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
return this.input.reportValidity();
|
||||
@@ -246,6 +246,7 @@ export default class SlTextarea extends ShoelaceElement {
|
||||
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
handleValueChange() {
|
||||
this.input.value = this.value; // force a sync update
|
||||
this.invalid = !this.input.checkValidity();
|
||||
this.updateComplete.then(() => this.setTextareaHeight());
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ describe('<sl-tooltip>', () => {
|
||||
<sl-button>Hover Me</sl-button>
|
||||
</sl-tooltip>
|
||||
`);
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part~="body"]')!;
|
||||
|
||||
expect(body.hidden).to.be.false;
|
||||
});
|
||||
@@ -21,7 +21,7 @@ describe('<sl-tooltip>', () => {
|
||||
<sl-button>Hover Me</sl-button>
|
||||
</sl-tooltip>
|
||||
`);
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part~="body"]')!;
|
||||
|
||||
expect(body.hidden).to.be.true;
|
||||
});
|
||||
@@ -32,7 +32,7 @@ describe('<sl-tooltip>', () => {
|
||||
<sl-button>Hover Me</sl-button>
|
||||
</sl-tooltip>
|
||||
`);
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part~="body"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('<sl-tooltip>', () => {
|
||||
<sl-button>Hover Me</sl-button>
|
||||
</sl-tooltip>
|
||||
`);
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part~="body"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('<sl-tooltip>', () => {
|
||||
<sl-button>Hover Me</sl-button>
|
||||
</sl-tooltip>
|
||||
`);
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part~="body"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
@@ -98,7 +98,7 @@ describe('<sl-tooltip>', () => {
|
||||
<sl-button>Hover Me</sl-button>
|
||||
</sl-tooltip>
|
||||
`);
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part~="body"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
@@ -120,7 +120,7 @@ describe('<sl-tooltip>', () => {
|
||||
<sl-button>Hover Me</sl-button>
|
||||
</sl-tooltip>
|
||||
`);
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part~="body"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('<sl-tooltip>', () => {
|
||||
<sl-button>Hover Me</sl-button>
|
||||
</sl-tooltip>
|
||||
`);
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part="body"]')!;
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('[part~="body"]')!;
|
||||
await el.updateComplete;
|
||||
|
||||
expect(body.hidden).to.be.false;
|
||||
|
||||
4
src/declaration.d.ts
vendored
4
src/declaration.d.ts
vendored
@@ -10,3 +10,7 @@ declare namespace Chai {
|
||||
accessible: (options?: Object) => PromiseLike<Assertion>;
|
||||
}
|
||||
}
|
||||
|
||||
interface HTMLInputElement {
|
||||
showPicker: () => void;
|
||||
}
|
||||
|
||||
@@ -9,11 +9,9 @@
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// @property({ type: Boolean, reflect: true })
|
||||
// checked = false;
|
||||
// @property({ type: Boolean, reflect: true }) checked = false;
|
||||
//
|
||||
// @defaultValue('checked')
|
||||
// defaultChecked = false;
|
||||
// @defaultValue('checked') defaultChecked = false;
|
||||
//
|
||||
|
||||
import { defaultConverter } from 'lit';
|
||||
|
||||
@@ -1,61 +1,82 @@
|
||||
import './formdata-event-polyfill';
|
||||
import type SlButton from '../components/button/button';
|
||||
import type { ShoelaceFormControl } from '../internal/shoelace-element';
|
||||
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
||||
|
||||
//
|
||||
// We store a WeakMap of forms + controls so we can keep references to all Shoelace controls within a given form. As
|
||||
// elements connect and disconnect to/from the DOM, their containing form is used as the key and the form control is
|
||||
// added and removed from the form's set, respectively.
|
||||
//
|
||||
const formCollections: WeakMap<HTMLFormElement, Set<ShoelaceFormControl>> = new WeakMap();
|
||||
|
||||
//
|
||||
// We store a WeakMap of controls that users have interacted with. This allows us to determine the interaction state
|
||||
// without littering the DOM with additional data attributes.
|
||||
//
|
||||
const userInteractedControls: WeakMap<ShoelaceFormControl, boolean> = new WeakMap();
|
||||
|
||||
//
|
||||
// We store a WeakMap of reportValidity() overloads so we can override it when form controls connect to the DOM and
|
||||
// restore the original behavior when they disconnect.
|
||||
//
|
||||
const reportValidityOverloads: WeakMap<HTMLFormElement, () => boolean> = new WeakMap();
|
||||
|
||||
export interface FormSubmitControllerOptions {
|
||||
/** A function that returns the form containing the form control. */
|
||||
form: (input: unknown) => HTMLFormElement | null;
|
||||
form: (input: ShoelaceFormControl) => HTMLFormElement | null;
|
||||
/** A function that returns the form control's name, which will be submitted with the form data. */
|
||||
name: (input: unknown) => string;
|
||||
name: (input: ShoelaceFormControl) => string;
|
||||
/** A function that returns the form control's current value. */
|
||||
value: (input: unknown) => unknown | unknown[];
|
||||
value: (input: ShoelaceFormControl) => unknown | unknown[];
|
||||
/** A function that returns the form control's default value. */
|
||||
defaultValue: (input: unknown) => unknown | unknown[];
|
||||
defaultValue: (input: ShoelaceFormControl) => unknown | unknown[];
|
||||
/** A function that returns the form control's current disabled state. If disabled, the value won't be submitted. */
|
||||
disabled: (input: unknown) => boolean;
|
||||
disabled: (input: ShoelaceFormControl) => boolean;
|
||||
/**
|
||||
* A function that maps to the form control's reportValidity() function. When the control is invalid, this will
|
||||
* prevent submission and trigger the browser's constraint violation warning.
|
||||
*/
|
||||
reportValidity: (input: unknown) => boolean;
|
||||
|
||||
reportValidity: (input: ShoelaceFormControl) => boolean;
|
||||
/** A function that sets the form control's value */
|
||||
setValue: (input: unknown, value: unknown) => void;
|
||||
setValue: (input: ShoelaceFormControl, value: unknown) => void;
|
||||
}
|
||||
|
||||
export class FormSubmitController implements ReactiveController {
|
||||
host?: ReactiveControllerHost & Element;
|
||||
host: ShoelaceFormControl & ReactiveControllerHost;
|
||||
form?: HTMLFormElement | null;
|
||||
options: FormSubmitControllerOptions;
|
||||
|
||||
constructor(host: ReactiveControllerHost & Element, options?: Partial<FormSubmitControllerOptions>) {
|
||||
constructor(host: ReactiveControllerHost & ShoelaceFormControl, options?: Partial<FormSubmitControllerOptions>) {
|
||||
(this.host = host).addController(this);
|
||||
this.options = {
|
||||
form: (input: HTMLInputElement) => input.closest('form'),
|
||||
name: (input: HTMLInputElement) => input.name,
|
||||
value: (input: HTMLInputElement) => input.value,
|
||||
defaultValue: (input: HTMLInputElement) => input.defaultValue,
|
||||
disabled: (input: HTMLInputElement) => input.disabled,
|
||||
reportValidity: (input: HTMLInputElement) => {
|
||||
return typeof input.reportValidity === 'function' ? input.reportValidity() : true;
|
||||
},
|
||||
setValue: (input: HTMLInputElement, value: string) => {
|
||||
input.value = value;
|
||||
},
|
||||
form: input => input.closest('form'),
|
||||
name: input => input.name,
|
||||
value: input => input.value,
|
||||
defaultValue: input => input.defaultValue,
|
||||
disabled: input => input.disabled ?? false,
|
||||
reportValidity: input => (typeof input.reportValidity === 'function' ? input.reportValidity() : true),
|
||||
setValue: (input, value: string) => (input.value = value),
|
||||
...options
|
||||
};
|
||||
this.handleFormData = this.handleFormData.bind(this);
|
||||
this.handleFormSubmit = this.handleFormSubmit.bind(this);
|
||||
this.handleFormReset = this.handleFormReset.bind(this);
|
||||
this.reportFormValidity = this.reportFormValidity.bind(this);
|
||||
this.handleUserInput = this.handleUserInput.bind(this);
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.form = this.options.form(this.host);
|
||||
|
||||
if (this.form) {
|
||||
// Add this element to the form's collection
|
||||
if (formCollections.has(this.form)) {
|
||||
formCollections.get(this.form)!.add(this.host);
|
||||
} else {
|
||||
formCollections.set(this.form, new Set<ShoelaceFormControl>([this.host]));
|
||||
}
|
||||
|
||||
this.form.addEventListener('formdata', this.handleFormData);
|
||||
this.form.addEventListener('submit', this.handleFormSubmit);
|
||||
this.form.addEventListener('reset', this.handleFormReset);
|
||||
@@ -66,10 +87,15 @@ export class FormSubmitController implements ReactiveController {
|
||||
this.form.reportValidity = () => this.reportFormValidity();
|
||||
}
|
||||
}
|
||||
|
||||
this.host.addEventListener('sl-input', this.handleUserInput);
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
if (this.form) {
|
||||
// Remove this element from the form's collection
|
||||
formCollections.get(this.form)?.delete(this.host);
|
||||
|
||||
this.form.removeEventListener('formdata', this.handleFormData);
|
||||
this.form.removeEventListener('submit', this.handleFormSubmit);
|
||||
this.form.removeEventListener('reset', this.handleFormReset);
|
||||
@@ -82,6 +108,39 @@ export class FormSubmitController implements ReactiveController {
|
||||
|
||||
this.form = undefined;
|
||||
}
|
||||
|
||||
this.host.removeEventListener('sl-input', this.handleUserInput);
|
||||
}
|
||||
|
||||
hostUpdated() {
|
||||
//
|
||||
// We're mapping the following "states" to data attributes. In the future, we can use ElementInternals.states to
|
||||
// create a similar mapping, but instead of [data-invalid] it will look like :--invalid.
|
||||
//
|
||||
// See this RFC for more details: https://github.com/shoelace-style/shoelace/issues/1011
|
||||
//
|
||||
const host = this.host;
|
||||
const hasInteracted = Boolean(userInteractedControls.get(host));
|
||||
const invalid = Boolean(host.invalid);
|
||||
const required = Boolean(host.required);
|
||||
|
||||
if (this.form?.noValidate) {
|
||||
// Form validation is disabled, remove the attributes
|
||||
host.removeAttribute('data-required');
|
||||
host.removeAttribute('data-optional');
|
||||
host.removeAttribute('data-invalid');
|
||||
host.removeAttribute('data-valid');
|
||||
host.removeAttribute('data-user-invalid');
|
||||
host.removeAttribute('data-user-valid');
|
||||
} else {
|
||||
// Form validation is enabled, set the attributes
|
||||
host.toggleAttribute('data-required', required);
|
||||
host.toggleAttribute('data-optional', !required);
|
||||
host.toggleAttribute('data-invalid', invalid);
|
||||
host.toggleAttribute('data-valid', !invalid);
|
||||
host.toggleAttribute('data-user-invalid', invalid && hasInteracted);
|
||||
host.toggleAttribute('data-user-valid', !invalid && hasInteracted);
|
||||
}
|
||||
}
|
||||
|
||||
handleFormData(event: FormDataEvent) {
|
||||
@@ -104,6 +163,13 @@ export class FormSubmitController implements ReactiveController {
|
||||
const disabled = this.options.disabled(this.host);
|
||||
const reportValidity = this.options.reportValidity;
|
||||
|
||||
// Update the interacted state for all controls when the form is submitted
|
||||
if (this.form && !this.form.noValidate) {
|
||||
formCollections.get(this.form)?.forEach(control => {
|
||||
this.setUserInteracted(control, true);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.form && !this.form.noValidate && !disabled && !reportValidity(this.host)) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
@@ -112,6 +178,12 @@ export class FormSubmitController implements ReactiveController {
|
||||
|
||||
handleFormReset() {
|
||||
this.options.setValue(this.host, this.options.defaultValue(this.host));
|
||||
this.setUserInteracted(this.host, false);
|
||||
}
|
||||
|
||||
async handleUserInput() {
|
||||
await this.host.updateComplete;
|
||||
this.setUserInteracted(this.host, true);
|
||||
}
|
||||
|
||||
reportFormValidity() {
|
||||
@@ -144,6 +216,11 @@ export class FormSubmitController implements ReactiveController {
|
||||
return true;
|
||||
}
|
||||
|
||||
setUserInteracted(el: ShoelaceFormControl, hasInteracted: boolean) {
|
||||
userInteractedControls.set(el, hasInteracted);
|
||||
el.requestUpdate();
|
||||
}
|
||||
|
||||
doAction(type: 'submit' | 'reset', invoker?: HTMLInputElement | SlButton) {
|
||||
if (this.form) {
|
||||
const button = document.createElement('button');
|
||||
@@ -157,7 +234,7 @@ export class FormSubmitController implements ReactiveController {
|
||||
|
||||
// Pass form attributes through to the temporary button
|
||||
if (invoker) {
|
||||
['formaction', 'formmethod', 'formnovalidate', 'formtarget'].forEach(attr => {
|
||||
['formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget'].forEach(attr => {
|
||||
if (invoker.hasAttribute(attr)) {
|
||||
button.setAttribute(attr, invoker.getAttribute(attr)!);
|
||||
}
|
||||
|
||||
@@ -21,3 +21,29 @@ export default class ShoelaceElement extends LitElement {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ShoelaceFormControl extends ShoelaceElement {
|
||||
// Standard form attributes
|
||||
name: string;
|
||||
value: unknown;
|
||||
disabled?: boolean;
|
||||
defaultValue?: unknown;
|
||||
defaultChecked?: boolean;
|
||||
|
||||
// Standard validation attributes
|
||||
pattern?: string;
|
||||
min?: number | Date;
|
||||
max?: number | Date;
|
||||
step?: number | 'any';
|
||||
required?: boolean;
|
||||
minlength?: number;
|
||||
maxlength?: number;
|
||||
|
||||
// Proprietary validation properties (non-attributes)
|
||||
invalid: boolean;
|
||||
|
||||
// Validation methods
|
||||
checkValidity: () => boolean;
|
||||
reportValidity: () => boolean;
|
||||
setCustomValidity: (message: string) => void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user