Compare commits

...

21 Commits

Author SHA1 Message Date
Cory LaViska
9c8a077d48 Merge branch 'next' into file-input 2023-08-28 09:41:10 -04:00
Cory LaViska
a2fbe121c3 update ctrl/tinycolor; fixes #1542 (#1545) 2023-08-28 09:39:16 -04:00
Cory LaViska
ab770c566e fix spacing; #1540 (#1544) 2023-08-28 09:27:57 -04:00
Konnor Rogers
1867603225 log stderr in builds (#1543) 2023-08-25 16:20:19 -04:00
Cory LaViska
cf195da424 fix stuck search 2023-08-25 09:35:05 -04:00
Cory LaViska
0cb6aa5d12 reformat by CEM plugin 2023-08-23 15:36:19 -04:00
Cory LaViska
47871c4ac4 Merge branch 'next' into file-input 2023-08-23 14:19:06 -04:00
Cory LaViska
51c4274d84 Merge branch 'next' into file-input 2023-08-18 12:11:21 -04:00
Cory LaViska
53d5942879 Merge branch 'next' into file-input 2023-08-15 11:27:22 -04:00
Cory LaViska
a6e6147e7a update structure to match next 2023-08-11 13:11:41 -04:00
Cory LaViska
0a7b05f456 Merge branch 'next' into file-input 2023-08-11 13:11:18 -04:00
Cory LaViska
b22d4e29d3 Merge branch 'next' into file-input 2023-08-11 10:52:06 -04:00
Cory LaViska
0f3327e23b update to use new structure 2023-07-31 14:57:09 -04:00
Cory LaViska
07fe2c3c4c Merge branch 'next' into file-input 2023-07-31 14:51:26 -04:00
Cory LaViska
647e05f93b Merge branch 'next' into file-input 2023-07-12 11:28:26 -04:00
Cory LaViska
e3126e0b2c Merge branch 'next' into file-input 2023-06-23 12:25:25 -04:00
Cory LaViska
37a41f497b fix example 2023-06-23 12:25:02 -04:00
Cory LaViska
5066298948 Merge branch 'next' into file-input 2023-06-23 12:17:30 -04:00
Cory LaViska
858bfff1f5 Merge branch 'next' into file-input 2023-04-14 13:01:40 -04:00
Cory LaViska
f4a8dd4663 file input proof of concept 2023-03-03 12:23:11 -05:00
Cory LaViska
00c5053401 sort template imports 2023-03-03 10:58:44 -05:00
13 changed files with 410 additions and 25 deletions

View File

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

View 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]

View File

@@ -12,6 +12,11 @@ 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).
## 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.
@@ -23,7 +28,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti
- 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

16
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"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",

View File

@@ -25,15 +25,8 @@
"./dist/react/*": "./dist/react/*",
"./dist/translations/*": "./dist/translations/*"
},
"files": [
"dist",
"cdn"
],
"keywords": [
"web components",
"custom elements",
"components"
],
"files": ["dist", "cdn"],
"keywords": ["web components", "custom elements", "components"],
"repository": {
"type": "git",
"url": "git+https://github.com/shoelace-style/shoelace.git"
@@ -67,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",
@@ -140,9 +133,6 @@
"user-agent-data-types": "^0.3.0"
},
"lint-staged": {
"*.{ts,js}": [
"eslint --max-warnings 0 --cache --fix",
"prettier --write"
]
"*.{ts,js}": ["eslint --max-warnings 0 --cache --fix", "prettier --write"]
}
}

View File

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

View 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>
`;
}
}

View 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: ')';
}
`;

View 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;
});
});

View 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;
}
}

View File

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

View File

@@ -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());

View File

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