mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-14 04:59:15 +00:00
Compare commits
31 Commits
docs-fixme
...
file-input
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c8a077d48 | ||
|
|
a2fbe121c3 | ||
|
|
ab770c566e | ||
|
|
1867603225 | ||
|
|
cf195da424 | ||
|
|
0cb6aa5d12 | ||
|
|
47871c4ac4 | ||
|
|
7e4d4c3c98 | ||
|
|
b5ef3191b7 | ||
|
|
f30481e229 | ||
|
|
ae010c333b | ||
|
|
43d1f9ee7a | ||
|
|
ec17e8736d | ||
|
|
44b27e791e | ||
|
|
02385027db | ||
|
|
b311072d9b | ||
|
|
87ac077b0a | ||
|
|
51c4274d84 | ||
|
|
53d5942879 | ||
|
|
a6e6147e7a | ||
|
|
0a7b05f456 | ||
|
|
b22d4e29d3 | ||
|
|
0f3327e23b | ||
|
|
07fe2c3c4c | ||
|
|
647e05f93b | ||
|
|
e3126e0b2c | ||
|
|
37a41f497b | ||
|
|
5066298948 | ||
|
|
858bfff1f5 | ||
|
|
f4a8dd4663 | ||
|
|
00c5053401 |
@@ -187,7 +187,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#properties') }}">attributes and properties</a>.</em></p>
|
||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#attributes-and-properties') }}">attributes and properties</a>.</em></p>
|
||||
{% endif %}
|
||||
|
||||
{# Events #}
|
||||
@@ -307,7 +307,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#component-parts') }}">customizing CSS parts</a>.</em></p>
|
||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/customizing/#css-parts') }}">customizing CSS parts</a>.</em></p>
|
||||
{% endif %}
|
||||
|
||||
{# Animations #}
|
||||
@@ -331,7 +331,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#animations') }}">customizing animations</a>.</em></p>
|
||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/customizing#animations') }}">customizing animations</a>.</em></p>
|
||||
{% endif %}
|
||||
|
||||
{# Dependencies #}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
let codeBlockId = 0;
|
||||
|
||||
/**
|
||||
* Adds copy code buttons to code fields. The provided doc should be a document object provided by JSDOM. The same
|
||||
* document will be returned with the appropriate DOM manipulations.
|
||||
@@ -5,19 +7,14 @@
|
||||
module.exports = function (doc) {
|
||||
doc.querySelectorAll('pre > code').forEach(code => {
|
||||
const pre = code.closest('pre');
|
||||
const button = doc.createElement('button');
|
||||
button.setAttribute('type', 'button');
|
||||
button.classList.add('copy-code-button');
|
||||
button.setAttribute('aria-label', 'Copy');
|
||||
button.innerHTML = `
|
||||
<svg class="copy-code-button__copy-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-files" viewBox="0 0 16 16" part="svg">
|
||||
<path d="M13 0H6a2 2 0 0 0-2 2 2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2 2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm0 13V4a2 2 0 0 0-2-2H5a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zM3 4a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4z"></path>
|
||||
</svg>
|
||||
const button = doc.createElement('sl-copy-button');
|
||||
|
||||
<svg class="copy-code-button__copied-icon" style="display: none;" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-lg" viewBox="0 0 16 16" part="svg">
|
||||
<path d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z"></path>
|
||||
</svg>
|
||||
`;
|
||||
if (!code.id) {
|
||||
code.id = `code-block-${++codeBlockId}`;
|
||||
}
|
||||
|
||||
button.classList.add('copy-code-button');
|
||||
button.setAttribute('from', code.id);
|
||||
|
||||
pre.append(button);
|
||||
});
|
||||
|
||||
@@ -163,32 +163,6 @@
|
||||
});
|
||||
})();
|
||||
|
||||
//
|
||||
// Copy code buttons
|
||||
//
|
||||
(() => {
|
||||
document.addEventListener('click', event => {
|
||||
const button = event.target.closest('.copy-code-button');
|
||||
const pre = button?.closest('pre');
|
||||
const code = pre?.querySelector('code');
|
||||
const copyIcon = button?.querySelector('.copy-code-button__copy-icon');
|
||||
const copiedIcon = button?.querySelector('.copy-code-button__copied-icon');
|
||||
|
||||
if (button && code) {
|
||||
navigator.clipboard.writeText(code.innerText);
|
||||
copyIcon.style.display = 'none';
|
||||
copiedIcon.style.display = 'inline';
|
||||
button.classList.add('copy-code-button--copied');
|
||||
|
||||
setTimeout(() => {
|
||||
copyIcon.style.display = 'inline';
|
||||
copiedIcon.style.display = 'none';
|
||||
button.classList.remove('copy-code-button--copied');
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
//
|
||||
// Smooth links
|
||||
//
|
||||
|
||||
@@ -373,4 +373,12 @@
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
// We're using Turbo, so when a user searches for something, visits a result, and presses the back button, the search
|
||||
// UI will still be visible but not interactive. This removes the search UI when Turbo renders a page so they don't
|
||||
// get trapped.
|
||||
window.addEventListener('turbo:render', () => {
|
||||
document.body.classList.remove('search-visible');
|
||||
document.querySelectorAll('.search__overlay, .search__dialog').forEach(el => el.remove());
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -506,46 +506,39 @@ pre .token.italic {
|
||||
|
||||
/* Copy code button */
|
||||
.copy-code-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
background: var(--sl-color-neutral-0);
|
||||
border-radius: calc(var(--docs-border-radius) * 0.875);
|
||||
border: solid 1px var(--sl-color-neutral-200);
|
||||
top: 0;
|
||||
right: 0;
|
||||
white-space: normal;
|
||||
color: var(--sl-color-neutral-800);
|
||||
text-transform: uppercase;
|
||||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
transition: 100ms opacity, 100ms scale;
|
||||
transition: 150ms opacity, 150ms scale;
|
||||
}
|
||||
|
||||
.copy-code-button svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
.copy-code-button::part(button) {
|
||||
background-color: var(--sl-color-neutral-50);
|
||||
border-radius: 0 var(--docs-border-radius) 0 var(--docs-border-radius);
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.copy-code-button::part(button):hover {
|
||||
background-color: color-mix(in oklch, var(--sl-color-neutral-50), var(--sl-color-neutral-1000) 3%);
|
||||
}
|
||||
|
||||
.copy-code-button::part(button):active {
|
||||
background-color: color-mix(in oklch, var(--sl-color-neutral-50), var(--sl-color-neutral-1000) 6%);
|
||||
}
|
||||
|
||||
pre .copy-code-button {
|
||||
opacity: 0;
|
||||
scale: 0.9;
|
||||
scale: 0.75;
|
||||
}
|
||||
|
||||
pre:hover .copy-code-button,
|
||||
.copy-code-button:focus-visible {
|
||||
.copy-code-button:focus-within {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
pre:hover .copy-code-button:hover,
|
||||
pre:hover .copy-code-button--copied {
|
||||
background: var(--sl-color-neutral-200);
|
||||
border-color: var(--sl-color-neutral-300);
|
||||
color: var(--sl-color-neutral-900);
|
||||
}
|
||||
|
||||
/* Callouts */
|
||||
.callout {
|
||||
position: relative;
|
||||
|
||||
42
docs/pages/components/file-input.md
Normal file
42
docs/pages/components/file-input.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
meta:
|
||||
title: File Input
|
||||
description: A description of the component goes here.
|
||||
layout: component
|
||||
---
|
||||
|
||||
```html:preview
|
||||
<form id="upload-form">
|
||||
<sl-file-input label="Upload a file" help-text="Select some files" name="myfiles" multiple></sl-file-input>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-button variant="primary" type="submit">Submit</sl-button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById('upload-form');
|
||||
|
||||
form.addEventListener('submit', event => {
|
||||
const formData = new FormData(form);
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
for (const file of formData.values()) {
|
||||
console.log(file);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### First Example
|
||||
|
||||
TODO
|
||||
|
||||
### Second Example
|
||||
|
||||
TODO
|
||||
|
||||
[component-metadata:sl-file-input]
|
||||
@@ -14,14 +14,21 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
||||
|
||||
## Next
|
||||
|
||||
- Fixed a bug in `<sl-switch>` that resulted in improper spacing between the label and the required asterisk [#1540]
|
||||
- Updated `@ctrl/tinycolor` to 4.0.1 [#1542]
|
||||
|
||||
## 2.8.0
|
||||
|
||||
- Added `--isolatedModules` and `--verbatimModuleSyntax` to `tsconfig.json`. For anyone directly importing event types, they no longer provide a default export due to these options being enabled. For people using the `events/event.js` file directly, there is no change.
|
||||
- Added support for submenus in `<sl-menu-item>` [#1410]
|
||||
- Added the `--submenu-offset` custom property to `<sl-menu-item>` [#1410]
|
||||
- Fixed an issue with focus trapping elements like `<sl-dialog>` when wrapped by other elements not checking the assigned elements of `<slot>`s. [#1537]
|
||||
- Fixed type issues with the `ref` attribute in React Wrappers. [#1526]
|
||||
- Fixed a regression that caused `<sl-radio-button>` to render incorrectly with gaps [#1523]
|
||||
- Improved expand/collapse behavior of `<sl-tree>` to work more like users expect [#1521]
|
||||
- Improved `<sl-menu-item>` so labels truncate properly instead of getting chopped and overflowing
|
||||
- Removed the extra `React.Component` around `@lit-labs/react` wrapper. [#1531]
|
||||
- Upgrade `@lit-labs/react` to v2.0.1. [#1531]
|
||||
- Updated `@lit-labs/react` to v2.0.1. [#1531]
|
||||
|
||||
## 2.7.0
|
||||
|
||||
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"version": "2.7.0",
|
||||
"version": "2.8.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"version": "2.7.0",
|
||||
"version": "2.8.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.5.0",
|
||||
"@ctrl/tinycolor": "^4.0.1",
|
||||
"@floating-ui/dom": "^1.2.1",
|
||||
"@lit-labs/react": "^2.0.1",
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
@@ -833,11 +833,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ctrl/tinycolor": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.5.0.tgz",
|
||||
"integrity": "sha512-tlJpwF40DEQcfR/QF+wNMVyGMaO9FQp6Z1Wahj4Gk3CJQYHwA2xVG7iKDFdW6zuxZY9XWOpGcfNCTsX4McOsOg==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.0.1.tgz",
|
||||
"integrity": "sha512-dfimuE1mfaqL8P8jyQzdk9yFeFUWCyhjK5VyydXgDtQO0fezr6aWaGauHnlI07BZBIF45gahb0oxJjkUcylDwQ==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@custom-elements-manifest/analyzer": {
|
||||
@@ -17913,9 +17913,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@ctrl/tinycolor": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.5.0.tgz",
|
||||
"integrity": "sha512-tlJpwF40DEQcfR/QF+wNMVyGMaO9FQp6Z1Wahj4Gk3CJQYHwA2xVG7iKDFdW6zuxZY9XWOpGcfNCTsX4McOsOg=="
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.0.1.tgz",
|
||||
"integrity": "sha512-dfimuE1mfaqL8P8jyQzdk9yFeFUWCyhjK5VyydXgDtQO0fezr6aWaGauHnlI07BZBIF45gahb0oxJjkUcylDwQ=="
|
||||
},
|
||||
"@custom-elements-manifest/analyzer": {
|
||||
"version": "0.8.3",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"description": "A forward-thinking library of web components.",
|
||||
"version": "2.7.0",
|
||||
"version": "2.8.0",
|
||||
"homepage": "https://github.com/shoelace-style/shoelace",
|
||||
"author": "Cory LaViska",
|
||||
"license": "MIT",
|
||||
@@ -60,7 +60,7 @@
|
||||
"node": ">=14.17.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.5.0",
|
||||
"@ctrl/tinycolor": "^4.0.1",
|
||||
"@floating-ui/dom": "^1.2.1",
|
||||
"@lit-labs/react": "^2.0.1",
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
|
||||
@@ -53,6 +53,10 @@ async function buildTheDocs(watch = false) {
|
||||
output.push(data.toString());
|
||||
});
|
||||
|
||||
child.stderr.on('data', data => {
|
||||
output.push(data.toString());
|
||||
});
|
||||
|
||||
if (watch) {
|
||||
// The process doesn't terminate in watch mode so, before resolving, we listen for a known signal in stdout that
|
||||
// tells us when the first build completes.
|
||||
|
||||
@@ -5,7 +5,7 @@ meta:
|
||||
layout: component
|
||||
---
|
||||
|
||||
```html preview
|
||||
```html:preview
|
||||
<{{ tag }}></{{ tag }}>
|
||||
```
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ import SlVisuallyHidden from '../visually-hidden/visually-hidden.component.js';
|
||||
import styles from './color-picker.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
|
||||
import type SlChangeEvent from '../../events/sl-change.js';
|
||||
import type SlInputEvent from '../../events/sl-input.js';
|
||||
import type { SlChangeEvent } from '../../events/sl-change.js';
|
||||
import type { SlInputEvent } from '../../events/sl-input.js';
|
||||
|
||||
const hasEyeDropper = 'EyeDropper' in window;
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import '../../../dist/shoelace.js';
|
||||
// cspell:dictionaries lorem-ipsum
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import type { SlHideEvent } from '../../events/sl-hide';
|
||||
import type { SlShowEvent } from '../../events/sl-show';
|
||||
import type SlDetails from './details';
|
||||
import type SlHideEvent from '../../events/sl-hide';
|
||||
import type SlShowEvent from '../../events/sl-show';
|
||||
|
||||
describe('<sl-details>', () => {
|
||||
describe('accessibility', () => {
|
||||
|
||||
@@ -11,10 +11,10 @@ import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlPopup from '../popup/popup.component.js';
|
||||
import styles from './dropdown.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
import type { SlSelectEvent } from '../../events/sl-select.js';
|
||||
import type SlButton from '../button/button.js';
|
||||
import type SlIconButton from '../icon-button/icon-button.js';
|
||||
import type SlMenu from '../menu/menu.js';
|
||||
import type SlSelectEvent from '../../events/sl-select.js';
|
||||
|
||||
/**
|
||||
* @summary Dropdowns expose additional content that "drops down" in a panel.
|
||||
|
||||
246
src/components/file-input/file-input.component.ts
Normal file
246
src/components/file-input/file-input.component.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import '../format-bytes/format-bytes.js';
|
||||
import '../icon-button/icon-button.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { defaultValue } from '../../internal/default-value.js';
|
||||
import { FormControlController } from '../../internal/form.js';
|
||||
import { HasSlotController } from '../../internal/slot.js';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
// import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import styles from './file-input.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
|
||||
//
|
||||
// TODO
|
||||
//
|
||||
// - button-only version
|
||||
// - drag and drop support
|
||||
// - localization
|
||||
//
|
||||
|
||||
/**
|
||||
* @summary Short summary of the component's intended use.
|
||||
* @documentation https://shoelace.style/components/file-input
|
||||
* @status experimental
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-format-bytes
|
||||
* @dependency sl-icon-button
|
||||
*
|
||||
* @event sl-input - Emitted when the form control receives input.
|
||||
*
|
||||
* @slot label - The input's label. Alternatively, you can use the `label` attribute.
|
||||
* @slot help-text - Text that describes how to use the input. Alternatively, you can use the `help-text` attribute.
|
||||
|
||||
* @csspart base - The component's base wrapper.
|
||||
*/
|
||||
export default class SlFileInput extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
private readonly formControlController = new FormControlController(this, {
|
||||
value: (control: SlFileInput) => control.files,
|
||||
assumeInteractionOn: ['sl-input']
|
||||
});
|
||||
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('input[type="file"]') input: HTMLInputElement;
|
||||
|
||||
@state() private files: File[] = [];
|
||||
@state() private hasFocus = false;
|
||||
|
||||
/** The name of the input, submitted as a name/value pair with form data. */
|
||||
@property() name = '';
|
||||
|
||||
/** The current value of the input, submitted as a name/value pair with form data. */
|
||||
@property() value = '';
|
||||
|
||||
/** The default value of the form control. Primarily used for resetting the form control. */
|
||||
@defaultValue() defaultValue = '';
|
||||
|
||||
/** The input's size. */
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
/** Draws a filled input. */
|
||||
@property({ type: Boolean, reflect: true }) filled = false;
|
||||
|
||||
/** Draws a pill-style input with rounded edges. */
|
||||
@property({ type: Boolean, reflect: true }) pill = false;
|
||||
|
||||
/** The input's label. If you need to display HTML, use the `label` slot instead. */
|
||||
@property() label = '';
|
||||
|
||||
/** The input's help text. If you need to display HTML, use the `help-text` slot instead. */
|
||||
@property({ attribute: 'help-text' }) helpText = '';
|
||||
|
||||
/** Disables the input. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** A list of acceptable file types. Must be a comma-separated list of [unique file type specifiers](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers). */
|
||||
@property() accept = false;
|
||||
|
||||
/** Allows more than one file to be selected. */
|
||||
@property({ type: Boolean }) multiple = false;
|
||||
|
||||
/** Gets the validity state object */
|
||||
get validity() {
|
||||
return this.input.validity;
|
||||
}
|
||||
|
||||
/** Gets the validation message */
|
||||
get validationMessage() {
|
||||
return this.input.validationMessage;
|
||||
}
|
||||
|
||||
private handleInput() {
|
||||
// Append selected files
|
||||
if (this.input.files) {
|
||||
this.files = this.files.concat([...this.input.files]);
|
||||
}
|
||||
|
||||
// Reset the input
|
||||
this.input.value = '';
|
||||
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
private handleRemoveClick(_: MouseEvent, indexToRemove: number) {
|
||||
this.files = this.files.filter((__, index) => index !== indexToRemove);
|
||||
}
|
||||
|
||||
private isImage(file: File) {
|
||||
return ['image/gif', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/webp'].includes(file.type);
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Gets the associated form, if one exists. */
|
||||
getForm(): HTMLFormElement | null {
|
||||
return this.formControlController.getForm();
|
||||
}
|
||||
|
||||
/** 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. Pass an empty string to restore validity. */
|
||||
setCustomValidity(message: string) {
|
||||
this.input.setCustomValidity(message);
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
render() {
|
||||
const hasLabelSlot = this.hasSlotController.test('label');
|
||||
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
||||
const hasLabel = this.label ? true : !!hasLabelSlot;
|
||||
const hasHelpText = this.helpText ? true : !!hasHelpTextSlot;
|
||||
|
||||
return html`
|
||||
<div
|
||||
part="form-control"
|
||||
class=${classMap({
|
||||
'form-control': true,
|
||||
'form-control--small': this.size === 'small',
|
||||
'form-control--medium': this.size === 'medium',
|
||||
'form-control--large': this.size === 'large',
|
||||
'form-control--has-label': hasLabel,
|
||||
'form-control--has-help-text': hasHelpText
|
||||
})}
|
||||
>
|
||||
<label
|
||||
part="form-control-label"
|
||||
class="form-control__label"
|
||||
for="input"
|
||||
aria-hidden=${hasLabel ? 'false' : 'true'}
|
||||
>
|
||||
<slot name="label">${this.label}</slot>
|
||||
</label>
|
||||
|
||||
<div part="form-control-input" class="form-control-input">
|
||||
<div
|
||||
part="base"
|
||||
class=${classMap({
|
||||
input: true,
|
||||
|
||||
// Sizes
|
||||
'input--small': this.size === 'small',
|
||||
'input--medium': this.size === 'medium',
|
||||
'input--large': this.size === 'large',
|
||||
|
||||
// States
|
||||
'input--pill': this.pill,
|
||||
'input--standard': !this.filled,
|
||||
'input--filled': this.filled,
|
||||
'input--disabled': this.disabled,
|
||||
'input--focused': this.hasFocus,
|
||||
'input--empty': !this.value
|
||||
})}
|
||||
>
|
||||
<input
|
||||
id="input"
|
||||
class="input__control"
|
||||
name=${this.name}
|
||||
type="file"
|
||||
aria-describedby="help-text"
|
||||
?multiple=${this.multiple}
|
||||
@input=${this.handleInput}
|
||||
/>
|
||||
|
||||
<sl-button @click=${() => this.input.click()}>Choose Files</sl-button>
|
||||
|
||||
<div class="input__files">
|
||||
${this.files.map((file, index) => {
|
||||
const isImage = this.isImage(file);
|
||||
|
||||
return html`
|
||||
<div class="input__file">
|
||||
<span class="input__file-preview">
|
||||
${isImage
|
||||
? html`<img
|
||||
class="input__file-preview-image input__file-preview--image"
|
||||
src=${URL.createObjectURL(file)}
|
||||
alt="${file.name}"
|
||||
/>`
|
||||
: html``}
|
||||
</span>
|
||||
<span class="input__file-name">${file.name}</span>
|
||||
<span class="input__file-size">
|
||||
<sl-format-bytes
|
||||
value=${file.size}
|
||||
display="short"
|
||||
lang=${this.localize.lang()}
|
||||
></sl-format-bytes>
|
||||
</span>
|
||||
<sl-icon-button
|
||||
class="input__file-remove"
|
||||
name="x-lg"
|
||||
library="system"
|
||||
label=${this.localize.term('remove')}
|
||||
@click=${(event: MouseEvent) => this.handleRemoveClick(event, index)}
|
||||
></sl-icon-button>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
part="form-control-help-text"
|
||||
id="help-text"
|
||||
class="form-control__help-text"
|
||||
aria-hidden=${hasHelpText ? 'false' : 'true'}
|
||||
>
|
||||
<slot name="help-text">${this.helpText}</slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
62
src/components/file-input/file-input.styles.ts
Normal file
62
src/components/file-input/file-input.styles.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--preview-size: 4rem;
|
||||
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input__control {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.input__files:not(:empty) {
|
||||
margin-block-start: var(--sl-spacing-medium);
|
||||
}
|
||||
|
||||
.input__file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input__file-preview {
|
||||
position: relative;
|
||||
width: var(--preview-size);
|
||||
height: var(--preview-size);
|
||||
margin-inline-end: var(--sl-spacing-medium);
|
||||
}
|
||||
|
||||
.input__file-preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.input__file-name {
|
||||
}
|
||||
|
||||
.input__file-size {
|
||||
font-size: var(--sl-font-size-small);
|
||||
color: var(--sl-color-neutral-800);
|
||||
}
|
||||
|
||||
.input__file-size::before {
|
||||
content: '(';
|
||||
}
|
||||
|
||||
.input__file-size::after {
|
||||
content: ')';
|
||||
}
|
||||
`;
|
||||
9
src/components/file-input/file-input.test.ts
Normal file
9
src/components/file-input/file-input.test.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
describe('<sl-file-input>', () => {
|
||||
it('should render a component', async () => {
|
||||
const el = await fixture(html` <sl-file-input></sl-file-input> `);
|
||||
|
||||
expect(el).to.exist;
|
||||
});
|
||||
});
|
||||
12
src/components/file-input/file-input.ts
Normal file
12
src/components/file-input/file-input.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import SlFileInput from './file-input.component.js';
|
||||
|
||||
export * from './file-input.component.js';
|
||||
export default SlFileInput;
|
||||
|
||||
SlFileInput.define('sl-file-input');
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-file-input': SlFileInput;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { aTimeout, elementUpdated, expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { registerIconLibrary } from '../../../dist/shoelace.js';
|
||||
import type SlErrorEvent from '../../events/sl-error';
|
||||
import type { SlErrorEvent } from '../../events/sl-error';
|
||||
import type { SlLoadEvent } from '../../events/sl-load';
|
||||
import type SlIcon from './icon';
|
||||
import type SlLoadEvent from '../../events/sl-load';
|
||||
|
||||
const testLibraryIcons = {
|
||||
'test-icon1': `
|
||||
|
||||
@@ -2,8 +2,8 @@ import '../../../dist/shoelace.js';
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import type { SlSelectEvent } from '../../events/sl-select';
|
||||
import type SlMenuItem from './menu-item';
|
||||
import type SlSelectEvent from '../../events/sl-select';
|
||||
|
||||
describe('<sl-menu-item>', () => {
|
||||
it('should pass accessibility tests', async () => {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { expect, fixture } from '@open-wc/testing';
|
||||
import { html } from 'lit';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import type { SlSelectEvent } from '../../events/sl-select';
|
||||
import type SlMenu from './menu';
|
||||
import type SlSelectEvent from '../../events/sl-select';
|
||||
|
||||
describe('<sl-menu>', () => {
|
||||
it('emits sl-select with the correct event detail when clicking an item', async () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { clickOnElement } from '../../internal/test.js';
|
||||
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import type SlChangeEvent from '../../events/sl-change.js';
|
||||
import type { SlChangeEvent } from '../../events/sl-change.js';
|
||||
import type SlRadio from '../radio/radio.js';
|
||||
import type SlRadioGroup from './radio-group.js';
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ import SlTag from '../tag/tag.component.js';
|
||||
import styles from './select.styles.js';
|
||||
import type { CSSResultGroup, TemplateResult } from 'lit';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
|
||||
import type { SlRemoveEvent } from '../../events/sl-remove.js';
|
||||
import type SlOption from '../option/option.component.js';
|
||||
import type SlRemoveEvent from '../../events/sl-remove.js';
|
||||
|
||||
/**
|
||||
* @summary Selects allow you to choose items from a menu of predefined options.
|
||||
|
||||
@@ -215,7 +215,9 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
|
||||
<span part="thumb" class="switch__thumb"></span>
|
||||
</span>
|
||||
|
||||
<slot part="label" class="switch__label"></slot>
|
||||
<div part="label" class="switch__label">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</label>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import { queryByTestId } from '../../internal/test/data-testid-helpers.js';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import { waitForScrollingToEnd } from '../../internal/test/wait-for-scrolling.js';
|
||||
import type { HTMLTemplateResult } from 'lit';
|
||||
import type { SlTabShowEvent } from '../../events/sl-tab-show.js';
|
||||
import type SlTab from '../tab/tab.js';
|
||||
import type SlTabGroup from './tab-group.js';
|
||||
import type SlTabPanel from '../tab-panel/tab-panel.js';
|
||||
import type SlTabShowEvent from '../../events/sl-tab-show.js';
|
||||
|
||||
interface ClientRectangles {
|
||||
body?: DOMRect;
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
export type { default as SlAfterCollapseEvent } from './sl-after-collapse';
|
||||
export type { default as SlAfterExpandEvent } from './sl-after-expand';
|
||||
export type { default as SlAfterHideEvent } from './sl-after-hide';
|
||||
export type { default as SlAfterShowEvent } from './sl-after-show';
|
||||
export type { default as SlBlurEvent } from './sl-blur';
|
||||
export type { default as SlCancelEvent } from './sl-cancel';
|
||||
export type { default as SlChangeEvent } from './sl-change';
|
||||
export type { default as SlClearEvent } from './sl-clear';
|
||||
export type { default as SlCloseEvent } from './sl-close';
|
||||
export type { default as SlCollapseEvent } from './sl-collapse';
|
||||
export type { default as SlCopyEvent } from './sl-copy';
|
||||
export type { default as SlErrorEvent } from './sl-error';
|
||||
export type { default as SlExpandEvent } from './sl-expand';
|
||||
export type { default as SlFinishEvent } from './sl-finish';
|
||||
export type { default as SlFocusEvent } from './sl-focus';
|
||||
export type { default as SlHideEvent } from './sl-hide';
|
||||
export type { default as SlHoverEvent } from './sl-hover';
|
||||
export type { default as SlInitialFocusEvent } from './sl-initial-focus';
|
||||
export type { default as SlInputEvent } from './sl-input';
|
||||
export type { default as SlInvalidEvent } from './sl-invalid';
|
||||
export type { default as SlLazyChangeEvent } from './sl-lazy-change';
|
||||
export type { default as SlLazyLoadEvent } from './sl-lazy-load';
|
||||
export type { default as SlLoadEvent } from './sl-load';
|
||||
export type { default as SlMutationEvent } from './sl-mutation';
|
||||
export type { default as SlRemoveEvent } from './sl-remove';
|
||||
export type { default as SlRepositionEvent } from './sl-reposition';
|
||||
export type { default as SlRequestCloseEvent } from './sl-request-close';
|
||||
export type { default as SlResizeEvent } from './sl-resize';
|
||||
export type { default as SlSelectEvent } from './sl-select';
|
||||
export type { default as SlSelectionChangeEvent } from './sl-selection-change';
|
||||
export type { default as SlShowEvent } from './sl-show';
|
||||
export type { default as SlSlideChangeEvent } from './sl-slide-change';
|
||||
export type { default as SlStartEvent } from './sl-start';
|
||||
export type { default as SlTabHideEvent } from './sl-tab-hide';
|
||||
export type { default as SlTabShowEvent } from './sl-tab-show';
|
||||
export type { SlAfterCollapseEvent } from './sl-after-collapse';
|
||||
export type { SlAfterExpandEvent } from './sl-after-expand';
|
||||
export type { SlAfterHideEvent } from './sl-after-hide';
|
||||
export type { SlAfterShowEvent } from './sl-after-show';
|
||||
export type { SlBlurEvent } from './sl-blur';
|
||||
export type { SlCancelEvent } from './sl-cancel';
|
||||
export type { SlChangeEvent } from './sl-change';
|
||||
export type { SlClearEvent } from './sl-clear';
|
||||
export type { SlCloseEvent } from './sl-close';
|
||||
export type { SlCollapseEvent } from './sl-collapse';
|
||||
export type { SlCopyEvent } from './sl-copy';
|
||||
export type { SlErrorEvent } from './sl-error';
|
||||
export type { SlExpandEvent } from './sl-expand';
|
||||
export type { SlFinishEvent } from './sl-finish';
|
||||
export type { SlFocusEvent } from './sl-focus';
|
||||
export type { SlHideEvent } from './sl-hide';
|
||||
export type { SlHoverEvent } from './sl-hover';
|
||||
export type { SlInitialFocusEvent } from './sl-initial-focus';
|
||||
export type { SlInputEvent } from './sl-input';
|
||||
export type { SlInvalidEvent } from './sl-invalid';
|
||||
export type { SlLazyChangeEvent } from './sl-lazy-change';
|
||||
export type { SlLazyLoadEvent } from './sl-lazy-load';
|
||||
export type { SlLoadEvent } from './sl-load';
|
||||
export type { SlMutationEvent } from './sl-mutation';
|
||||
export type { SlRemoveEvent } from './sl-remove';
|
||||
export type { SlRepositionEvent } from './sl-reposition';
|
||||
export type { SlRequestCloseEvent } from './sl-request-close';
|
||||
export type { SlResizeEvent } from './sl-resize';
|
||||
export type { SlSelectEvent } from './sl-select';
|
||||
export type { SlSelectionChangeEvent } from './sl-selection-change';
|
||||
export type { SlShowEvent } from './sl-show';
|
||||
export type { SlSlideChangeEvent } from './sl-slide-change';
|
||||
export type { SlStartEvent } from './sl-start';
|
||||
export type { SlTabHideEvent } from './sl-tab-hide';
|
||||
export type { SlTabShowEvent } from './sl-tab-show';
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlAfterCollapseEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlAfterCollapseEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-after-collapse': SlAfterCollapseEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlAfterCollapseEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlAfterExpandEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlAfterExpandEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-after-expand': SlAfterExpandEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlAfterExpandEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlAfterHideEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlAfterHideEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-after-hide': SlAfterHideEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlAfterHideEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlAfterShowEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlAfterShowEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-after-show': SlAfterShowEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlAfterShowEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlBlurEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlBlurEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-blur': SlBlurEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlBlurEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlCancelEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlCancelEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-cancel': SlCancelEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlCancelEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlChangeEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlChangeEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-change': SlChangeEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlChangeEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlClearEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlClearEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-clear': SlClearEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlClearEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlCloseEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlCloseEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-close': SlCloseEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlCloseEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlCollapseEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlCollapseEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-collapse': SlCollapseEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlCollapseEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlCopyEvent = CustomEvent<{ value: string }>;
|
||||
export type SlCopyEvent = CustomEvent<{ value: string }>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-copy': SlCopyEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlCopyEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlErrorEvent = CustomEvent<{ status?: number }>;
|
||||
export type SlErrorEvent = CustomEvent<{ status?: number }>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-error': SlErrorEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlErrorEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlExpandEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlExpandEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-expand': SlExpandEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlExpandEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlFinishEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlFinishEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-finish': SlFinishEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlFinishEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlFocusEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlFocusEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-focus': SlFocusEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlFocusEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlHideEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlHideEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-hide': SlHideEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlHideEvent;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type SlHoverEvent = CustomEvent<{
|
||||
export type SlHoverEvent = CustomEvent<{
|
||||
phase: 'start' | 'move' | 'end';
|
||||
value: number;
|
||||
}>;
|
||||
@@ -8,5 +8,3 @@ declare global {
|
||||
'sl-hover': SlHoverEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlHoverEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlInitialFocusEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlInitialFocusEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-initial-focus': SlInitialFocusEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlInitialFocusEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlInputEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlInputEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-input': SlInputEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlInputEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlInvalidEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlInvalidEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-invalid': SlInvalidEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlInvalidEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlLazyChangeEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlLazyChangeEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-lazy-change': SlLazyChangeEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlLazyChangeEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlLazyLoadEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlLazyLoadEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-lazy-load': SlLazyLoadEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlLazyLoadEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlLoadEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlLoadEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-load': SlLoadEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlLoadEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlMutationEvent = CustomEvent<{ mutationList: MutationRecord[] }>;
|
||||
export type SlMutationEvent = CustomEvent<{ mutationList: MutationRecord[] }>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-mutation': SlMutationEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlMutationEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlRemoveEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlRemoveEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-remove': SlRemoveEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlRemoveEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlRepositionEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlRepositionEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-reposition': SlRepositionEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlRepositionEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlRequestCloseEvent = CustomEvent<{ source: 'close-button' | 'keyboard' | 'overlay' }>;
|
||||
export type SlRequestCloseEvent = CustomEvent<{ source: 'close-button' | 'keyboard' | 'overlay' }>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-request-close': SlRequestCloseEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlRequestCloseEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlResizeEvent = CustomEvent<{ entries: ResizeObserverEntry[] }>;
|
||||
export type SlResizeEvent = CustomEvent<{ entries: ResizeObserverEntry[] }>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-resize': SlResizeEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlResizeEvent;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type SlMenuItem from '../components/menu-item/menu-item';
|
||||
|
||||
type SlSelectEvent = CustomEvent<{ item: SlMenuItem }>;
|
||||
export type SlSelectEvent = CustomEvent<{ item: SlMenuItem }>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-select': SlSelectEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlSelectEvent;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type SlTreeItem from '../components/tree-item/tree-item';
|
||||
|
||||
type SlSelectionChangeEvent = CustomEvent<{ selection: SlTreeItem[] }>;
|
||||
export type SlSelectionChangeEvent = CustomEvent<{ selection: SlTreeItem[] }>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-selection-change': SlSelectionChangeEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlSelectionChangeEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlShowEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlShowEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-show': SlShowEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlShowEvent;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type SlCarouselItem from '../components/carousel-item/carousel-item';
|
||||
|
||||
type SlSlideChangeEvent = CustomEvent<{ index: number; slide: SlCarouselItem }>;
|
||||
export type SlSlideChangeEvent = CustomEvent<{ index: number; slide: SlCarouselItem }>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-slide-change': SlSlideChangeEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlSlideChangeEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlStartEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
export type SlStartEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-start': SlStartEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlStartEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlTabHideEvent = CustomEvent<{ name: string }>;
|
||||
export type SlTabHideEvent = CustomEvent<{ name: string }>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-tab-hide': SlTabHideEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlTabHideEvent;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
type SlTabShowEvent = CustomEvent<{ name: string }>;
|
||||
export type SlTabShowEvent = CustomEvent<{ name: string }>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-tab-show': SlTabShowEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlTabShowEvent;
|
||||
|
||||
22
src/internal/active-elements.ts
Normal file
22
src/internal/active-elements.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Use a generator so we can iterate and possibly break early.
|
||||
* @example
|
||||
* // to operate like a regular array. This kinda nullifies generator benefits, but worth knowing if you need the whole array.
|
||||
* const allActiveElements = [...activeElements()]
|
||||
*
|
||||
* // Early return
|
||||
* for (const activeElement of activeElements()) {
|
||||
* if (<cond>) {
|
||||
* break; // Break the loop, dont need to iterate over the whole array or store an array in memory!
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export function* activeElements(activeElement: Element | null = document.activeElement): Generator<Element> {
|
||||
if (activeElement === null || activeElement === undefined) return;
|
||||
|
||||
yield activeElement;
|
||||
|
||||
if ('shadowRoot' in activeElement && activeElement.shadowRoot && activeElement.shadowRoot.mode !== 'closed') {
|
||||
yield* activeElements(activeElement.shadowRoot.activeElement);
|
||||
}
|
||||
}
|
||||
@@ -182,7 +182,11 @@ export class FormControlController implements ReactiveController {
|
||||
if (!disabled && !isButton && typeof name === 'string' && name.length > 0 && typeof value !== 'undefined') {
|
||||
if (Array.isArray(value)) {
|
||||
(value as unknown[]).forEach(val => {
|
||||
event.formData.append(name, (val as string | number | boolean).toString());
|
||||
if (val instanceof File) {
|
||||
event.formData.append(name, val, val.name);
|
||||
} else {
|
||||
event.formData.append(name, (val as string | number | boolean).toString());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
event.formData.append(name, (value as string | number | boolean).toString());
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { activeElements } from './active-elements.js';
|
||||
import { getTabbableElements } from './tabbable.js';
|
||||
|
||||
let activeModals: HTMLElement[] = [];
|
||||
@@ -55,6 +56,20 @@ export default class Modal {
|
||||
return getTabbableElements(this.element).findIndex(el => el === this.currentFocus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the `startElement` is already focused. This is important if the modal already
|
||||
* has an existing focus prior to the first tab key.
|
||||
*/
|
||||
startElementAlreadyFocused(startElement: HTMLElement) {
|
||||
for (const activeElement of activeElements()) {
|
||||
if (startElement === activeElement) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'Tab') return;
|
||||
|
||||
@@ -68,7 +83,10 @@ export default class Modal {
|
||||
|
||||
const tabbableElements = getTabbableElements(this.element);
|
||||
const start = tabbableElements[0];
|
||||
let focusIndex = this.currentFocusIndex;
|
||||
|
||||
// Sometimes we programmatically focus the first element in a modal.
|
||||
// Lets make sure the start element isn't already focused.
|
||||
let focusIndex = this.startElementAlreadyFocused(start) ? 0 : this.currentFocusIndex;
|
||||
|
||||
if (focusIndex === -1) {
|
||||
this.currentFocus = start;
|
||||
|
||||
147
src/internal/tabbable.test.ts
Normal file
147
src/internal/tabbable.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { elementUpdated, expect, fixture } from '@open-wc/testing';
|
||||
|
||||
import '../../dist/shoelace.js';
|
||||
import { activeElements } from './active-elements.js';
|
||||
import { html } from 'lit';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
|
||||
async function holdShiftKey(callback: () => Promise<void>) {
|
||||
await sendKeys({ down: 'Shift' });
|
||||
await callback();
|
||||
await sendKeys({ up: 'Shift' });
|
||||
}
|
||||
|
||||
const tabKey =
|
||||
navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('HeadlessChrome') ? 'Alt+Tab' : 'Tab';
|
||||
|
||||
// Simple helper to turn the activeElements generator into an array
|
||||
function activeElementsArray() {
|
||||
return [...activeElements()];
|
||||
}
|
||||
|
||||
function getDeepestActiveElement() {
|
||||
return activeElementsArray().pop();
|
||||
}
|
||||
|
||||
window.customElements.define(
|
||||
'tab-test-1',
|
||||
class extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
connectedCallback() {
|
||||
this.shadowRoot!.innerHTML = `
|
||||
<sl-drawer>
|
||||
<slot name="label" slot="label"></slot>
|
||||
|
||||
<slot></slot>
|
||||
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</sl-drawer>
|
||||
`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
it('Should allow tabbing to slotted elements', async () => {
|
||||
const el = await fixture(html`
|
||||
<tab-test-1>
|
||||
<div slot="label">
|
||||
<sl-button id="focus-1">Focus 1</sl-button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- Focus 2 lives as the close-button from <sl-drawer> -->
|
||||
<sl-button id="focus-3">Focus 3</sl-button>
|
||||
<button id="focus-4">Focus 4</sl-button>
|
||||
<input id="focus-5" value="Focus 5">
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<div id="focus-6" tabindex="0">Focus 6</div>
|
||||
<button tabindex="-1">No Focus</button>
|
||||
</div>
|
||||
</tab-test-1>
|
||||
`);
|
||||
|
||||
const drawer = el.shadowRoot?.querySelector('sl-drawer');
|
||||
|
||||
if (drawer === null || drawer === undefined) throw Error('Could not find drawer inside of the test element');
|
||||
|
||||
await drawer.show();
|
||||
|
||||
await elementUpdated(drawer);
|
||||
|
||||
const focusZero = drawer.shadowRoot?.querySelector("[role='dialog']");
|
||||
|
||||
if (focusZero === null || focusZero === undefined) throw Error('Could not find dialog panel inside <sl-drawer>');
|
||||
|
||||
const focusOne = el.querySelector('#focus-1');
|
||||
const focusTwo = drawer.shadowRoot?.querySelector("[part~='close-button']");
|
||||
|
||||
if (focusTwo === null || focusTwo === undefined) throw Error('Could not find close button inside <sl-drawer>');
|
||||
|
||||
const focusThree = el.querySelector('#focus-3');
|
||||
const focusFour = el.querySelector('#focus-4');
|
||||
const focusFive = el.querySelector('#focus-5');
|
||||
const focusSix = el.querySelector('#focus-6');
|
||||
|
||||
// When we open drawer, we should be focused on the panel to start.
|
||||
expect(getDeepestActiveElement()).to.equal(focusZero);
|
||||
|
||||
await sendKeys({ press: tabKey });
|
||||
expect(activeElementsArray()).to.include(focusOne);
|
||||
|
||||
// When we hit the <Tab> key we should go to the "close button" on the drawer
|
||||
await sendKeys({ press: tabKey });
|
||||
expect(activeElementsArray()).to.include(focusTwo);
|
||||
|
||||
await sendKeys({ press: tabKey });
|
||||
expect(activeElementsArray()).to.include(focusThree);
|
||||
|
||||
await sendKeys({ press: tabKey });
|
||||
expect(activeElementsArray()).to.include(focusFour);
|
||||
|
||||
await sendKeys({ press: tabKey });
|
||||
expect(activeElementsArray()).to.include(focusFive);
|
||||
|
||||
await sendKeys({ press: tabKey });
|
||||
expect(activeElementsArray()).to.include(focusSix);
|
||||
|
||||
// Now we should loop back to #panel
|
||||
await sendKeys({ press: tabKey });
|
||||
expect(activeElementsArray()).to.include(focusZero);
|
||||
|
||||
// Now we should loop back to #panel
|
||||
await sendKeys({ press: tabKey });
|
||||
expect(activeElementsArray()).to.include(focusOne);
|
||||
|
||||
// Let's reset and try from starting point 0 and go backwards.
|
||||
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||
expect(activeElementsArray()).to.include(focusZero);
|
||||
|
||||
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||
expect(activeElementsArray()).to.include(focusSix);
|
||||
|
||||
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||
expect(activeElementsArray()).to.include(focusFive);
|
||||
|
||||
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||
expect(activeElementsArray()).to.include(focusFour);
|
||||
|
||||
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||
expect(activeElementsArray()).to.include(focusThree);
|
||||
|
||||
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||
expect(activeElementsArray()).to.include(focusTwo);
|
||||
|
||||
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||
expect(activeElementsArray()).to.include(focusOne);
|
||||
|
||||
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||
expect(activeElementsArray()).to.include(focusZero);
|
||||
|
||||
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||
expect(activeElementsArray()).to.include(focusSix);
|
||||
});
|
||||
@@ -69,11 +69,32 @@ export function getTabbableBoundary(root: HTMLElement | ShadowRoot) {
|
||||
}
|
||||
|
||||
export function getTabbableElements(root: HTMLElement | ShadowRoot) {
|
||||
const allElements: HTMLElement[] = [];
|
||||
const tabbableElements: HTMLElement[] = [];
|
||||
|
||||
function walk(el: HTMLElement | ShadowRoot) {
|
||||
if (el instanceof Element) {
|
||||
allElements.push(el);
|
||||
// if the element has "inert" we can just no-op it.
|
||||
if (el.hasAttribute('inert')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tabbableElements.includes(el) && isTabbable(el)) {
|
||||
tabbableElements.push(el);
|
||||
}
|
||||
|
||||
/**
|
||||
* This looks funky. Basically a slot's children will always be picked up *if* they're within the `root` element.
|
||||
* However, there is an edge case when, if the `root` is wrapped by another shadow DOM, it won't grab the children.
|
||||
* This fixes that fun edge case.
|
||||
*/
|
||||
const slotChildrenOutsideRootElement = (slotElement: HTMLSlotElement) =>
|
||||
(slotElement.getRootNode({ composed: true }) as ShadowRoot | null)?.host !== root;
|
||||
|
||||
if (el instanceof HTMLSlotElement && slotChildrenOutsideRootElement(el)) {
|
||||
el.assignedElements({ flatten: true }).forEach((assignedEl: HTMLElement) => {
|
||||
walk(assignedEl);
|
||||
});
|
||||
}
|
||||
|
||||
if (el.shadowRoot !== null && el.shadowRoot.mode === 'open') {
|
||||
walk(el.shadowRoot);
|
||||
@@ -86,10 +107,14 @@ export function getTabbableElements(root: HTMLElement | ShadowRoot) {
|
||||
// Collect all elements including the root
|
||||
walk(root);
|
||||
|
||||
return allElements.filter(isTabbable).sort((a, b) => {
|
||||
// Make sure we sort by tabindex.
|
||||
const aTabindex = Number(a.getAttribute('tabindex')) || 0;
|
||||
const bTabindex = Number(b.getAttribute('tabindex')) || 0;
|
||||
return bTabindex - aTabindex;
|
||||
});
|
||||
return tabbableElements;
|
||||
|
||||
// Is this worth having? Most sorts will always add increased overhead. And positive tabindexes shouldn't really be used.
|
||||
// So is it worth being right? Or fast?
|
||||
// return tabbableElements.filter(isTabbable).sort((a, b) => {
|
||||
// // Make sure we sort by tabindex.
|
||||
// const aTabindex = Number(a.getAttribute('tabindex')) || 0;
|
||||
// const bTabindex = Number(b.getAttribute('tabindex')) || 0;
|
||||
// return bTabindex - aTabindex;
|
||||
// });
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export { default as SlDialog } from './components/dialog/dialog.js';
|
||||
export { default as SlDivider } from './components/divider/divider.js';
|
||||
export { default as SlDrawer } from './components/drawer/drawer.js';
|
||||
export { default as SlDropdown } from './components/dropdown/dropdown.js';
|
||||
export { default as SlFileInput } from './components/file-input/file-input.js';
|
||||
export { default as SlFormatBytes } from './components/format-bytes/format-bytes.js';
|
||||
export { default as SlFormatDate } from './components/format-date/format-date.js';
|
||||
export { default as SlFormatNumber } from './components/format-number/format-number.js';
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"types": [
|
||||
"mocha",
|
||||
"user-agent-data-types"
|
||||
|
||||
Reference in New Issue
Block a user