mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-16 05:59:15 +00:00
Compare commits
16 Commits
calendar
...
file-input
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c8a077d48 | ||
|
|
47871c4ac4 | ||
|
|
51c4274d84 | ||
|
|
53d5942879 | ||
|
|
a6e6147e7a | ||
|
|
0a7b05f456 | ||
|
|
b22d4e29d3 | ||
|
|
0f3327e23b | ||
|
|
07fe2c3c4c | ||
|
|
647e05f93b | ||
|
|
e3126e0b2c | ||
|
|
37a41f497b | ||
|
|
5066298948 | ||
|
|
858bfff1f5 | ||
|
|
f4a8dd4663 | ||
|
|
00c5053401 |
@@ -1,67 +0,0 @@
|
||||
---
|
||||
meta:
|
||||
title: Calendar
|
||||
description: Calendar shows a monthly view of the Gregorian calendar, optionally allowing users to interact with dates.
|
||||
layout: component
|
||||
---
|
||||
|
||||
```html:preview
|
||||
<sl-calendar></sl-calendar>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Month & Day Labels
|
||||
|
||||
Month and day labels can be customized using the `month-labels` and `day-labels` attributes. Note that month names are localized automatically based on the component's `lang` attribute, falling back to the document language.
|
||||
|
||||
```html:preview
|
||||
<sl-calendar month-labels="short" day-labels="narrow"></sl-calendar>
|
||||
```
|
||||
|
||||
### Showing Adjacent Dates
|
||||
|
||||
By default, only dates in the target month are shown. You can fill the grid with adjacent dates using the `show-adjacent-dates` attribute.
|
||||
|
||||
```html:preview
|
||||
<sl-calendar show-adjacent-dates></sl-calendar>
|
||||
```
|
||||
|
||||
### Date Selection
|
||||
|
||||
One or more dates can be selected by setting the `selectedDates` property. An array of dates is accepted and the selection does not have to be continuous.
|
||||
|
||||
```html:preview
|
||||
<sl-calendar class="calendar-selection"></sl-calendar>
|
||||
|
||||
<script>
|
||||
const calendar = document.querySelector('.calendar-selection');
|
||||
const today = new Date();
|
||||
|
||||
// Set the selected date range from the 12-15 of the current month
|
||||
calendar.selectedDates = [
|
||||
new Date(today.getFullYear(), today.getMonth(), 12),
|
||||
new Date(today.getFullYear(), today.getMonth(), 13),
|
||||
new Date(today.getFullYear(), today.getMonth(), 14),
|
||||
new Date(today.getFullYear(), today.getMonth(), 15)
|
||||
];
|
||||
</script>
|
||||
```
|
||||
|
||||
### With Borders
|
||||
|
||||
To add a border, set the `--border-width` custom property. You can further customize the border with `--border-color` and `--border-radius`.
|
||||
|
||||
```html:preview
|
||||
<sl-calendar style="--border-width: 1px;"></sl-calendar>
|
||||
```
|
||||
|
||||
### Localizing the Calendar
|
||||
|
||||
By default, the calendar will use the document's locale. You can use the `lang` attribute to change this.
|
||||
|
||||
```html:preview
|
||||
<sl-calendar lang="es"></sl-calendar>
|
||||
```
|
||||
|
||||
[component-metadata:sl-calendar]
|
||||
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]
|
||||
@@ -1,204 +0,0 @@
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { generateCalendarGrid, getAllDayNames, getMonthName, isSameDay } from '../../internal/calendar.js';
|
||||
import { HasSlotController } from '../../internal/slot.js';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import { partMap } from '../../internal/part-map.js';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import styles from './calendar.styles';
|
||||
import type { CSSResultGroup, TemplateResult } from 'lit';
|
||||
|
||||
export interface RenderDayOptions {
|
||||
disabled?: boolean;
|
||||
content: string | TemplateResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary A calendar prototype for Shoelace.
|
||||
* @documentation https://shoelace.style/components/calendar
|
||||
*
|
||||
* @since 2.0
|
||||
* @status experimental
|
||||
*
|
||||
* @dependency sl-example
|
||||
*
|
||||
* @event sl-change - Emitted when the date changes.
|
||||
*
|
||||
* @slot footer - Optional content to place in the calendar's footer.
|
||||
*
|
||||
* @csspart day - Targets day cells.
|
||||
* @csspart day-label - Targets the day labels (the name of the days in the grid).
|
||||
* @csspart day-weekend - Targets days that fall on weekends.
|
||||
* @csspart day-weekday - Targets weekdays.
|
||||
* @csspart day-current-month - Targets days in the current month.
|
||||
* @csspart day-previous-month - Targets days in the previous month.
|
||||
* @csspart day-next-month - Targets days in the next month.
|
||||
* @csspart day-today - Targets today.
|
||||
* @csspart day-selected - Targets selected days.
|
||||
* @csspart day-selection-start - Targets days that begin a selection.
|
||||
* @csspart day-selection-end - Targets days that end a selection.
|
||||
*
|
||||
* @cssproperty --border-color - The calendar's border color.
|
||||
* @cssproperty --border-width - The calendar's border width.
|
||||
* @cssproperty --border-radius - The border radius of the calendar.
|
||||
*/
|
||||
export default class SlCalendar extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private readonly hasSlotController = new HasSlotController(this, 'prefix', 'suffix');
|
||||
|
||||
/** The month to render, 1-12/ */
|
||||
@property({ type: Number, reflect: true }) month: number = new Date().getMonth() + 1;
|
||||
|
||||
/** The year to render. */
|
||||
@property({ type: Number, reflect: true }) year: number = new Date().getFullYear();
|
||||
|
||||
/** Determines how day labels are shown, e.g. "M", "Mon", or "Monday". */
|
||||
@property({ attribute: 'day-labels' }) dayLabels: 'narrow' | 'short' | 'long' = 'short';
|
||||
|
||||
/** Determines how month labels are shown, e.g. "J", "Jan", or "January". */
|
||||
@property({ attribute: 'month-labels' }) monthLabels: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow' = 'long';
|
||||
|
||||
/** When true, dates from the previous and next month will also be shown to fill out the grid. */
|
||||
@property({ attribute: 'show-adjacent-dates', type: Boolean }) showAdjacentDates = false;
|
||||
|
||||
/** Draws the target dates as a selection in the calendar. */
|
||||
@property({ type: Array }) selectedDates: Date[] = [];
|
||||
|
||||
/** Moves the calendar to the current month and year. */
|
||||
goToToday() {
|
||||
this.month = new Date().getMonth() + 1;
|
||||
this.year = new Date().getFullYear();
|
||||
}
|
||||
|
||||
/** Moves the calendar to the previous month. */
|
||||
goToPreviousMonth() {
|
||||
if (this.month === 1) {
|
||||
this.month = 12;
|
||||
this.year--;
|
||||
} else {
|
||||
this.month--;
|
||||
}
|
||||
}
|
||||
|
||||
/** Moves the calendar to the next month. */
|
||||
goToNextMonth() {
|
||||
if (this.month === 12) {
|
||||
this.month = 1;
|
||||
this.year++;
|
||||
} else {
|
||||
this.month++;
|
||||
}
|
||||
}
|
||||
|
||||
@watch('month')
|
||||
@watch('year')
|
||||
handleMonthChange() {
|
||||
this.emit('sl-change');
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.month < 1 || this.month > 12) {
|
||||
throw new Error(`The value "${this.month}" is not a valid month.`);
|
||||
}
|
||||
|
||||
const lang = this.lang || document.documentElement.lang;
|
||||
const month = new Date(this.year, this.month - 1, 1);
|
||||
const dayGrid = generateCalendarGrid(this.year, this.month);
|
||||
const dayNames = getAllDayNames(lang, this.dayLabels);
|
||||
|
||||
//
|
||||
// TODO - December is not showing a label because the month is calculated as Sat Jan 01 2022 00:00:00 GMT-0500
|
||||
//
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
calendar: true,
|
||||
'calendar--has-footer': this.hasSlotController.test('footer'),
|
||||
'calendar--show-adjacent-dates': this.showAdjacentDates
|
||||
})}
|
||||
>
|
||||
<header class="calendar__header">
|
||||
<sl-icon-button
|
||||
name="chevron-left"
|
||||
label=${this.localize.term('previousMonth')}
|
||||
@click=${this.goToPreviousMonth}
|
||||
></sl-icon-button>
|
||||
|
||||
<span class="calendar__label">
|
||||
<span class="calendar__month-label">${getMonthName(month, lang, this.monthLabels)}</span>
|
||||
<span class="calendar__year-label">${month.getFullYear()}</span>
|
||||
</span>
|
||||
|
||||
<sl-icon-button
|
||||
name="chevron-right"
|
||||
label=${this.localize.term('nextMonth')}
|
||||
@click=${this.goToNextMonth}
|
||||
></sl-icon-button>
|
||||
</header>
|
||||
|
||||
<div class="calendar__days">
|
||||
${[0, 1, 2, 3, 4, 5, 6].map(day => {
|
||||
return html`
|
||||
<span
|
||||
part=${partMap({
|
||||
day: true,
|
||||
'day-label': true,
|
||||
'day-weekday': day > 0 && day < 6,
|
||||
'day-weekend': day === 0 || day === 6
|
||||
})}
|
||||
class="calendar__day"
|
||||
>
|
||||
${dayNames[day]}
|
||||
</span>
|
||||
`;
|
||||
})}
|
||||
${dayGrid.map((day, index) => {
|
||||
if (day.isCurrentMonth || this.showAdjacentDates) {
|
||||
const isSelected = Array.isArray(this.selectedDates)
|
||||
? this.selectedDates.some(d => isSameDay(d, day.date))
|
||||
: false;
|
||||
const previousDay = index > 0 ? dayGrid[index - 1] : null;
|
||||
const nextDay = index < dayGrid.length - 1 ? dayGrid[index + 1] : null;
|
||||
const isSelectionStart =
|
||||
isSelected && previousDay ? !this.selectedDates.some(d => isSameDay(d, previousDay.date)) : false;
|
||||
const isSelectionEnd =
|
||||
isSelected && nextDay ? !this.selectedDates.some(d => isSameDay(d, nextDay.date)) : false;
|
||||
|
||||
return html`
|
||||
<button
|
||||
type="button"
|
||||
part=${partMap({
|
||||
day: true,
|
||||
'day-current-month': day.isCurrentMonth,
|
||||
'day-previous-month': day.isPreviousMonth,
|
||||
'day-next-month': day.isNextMonth,
|
||||
'day-today': day.isToday,
|
||||
'day-weekday': day.isWeekday,
|
||||
'day-weekend': day.isWeekend,
|
||||
'day-selected': isSelected,
|
||||
'day-selection-start': isSelectionStart,
|
||||
'day-selection-end': isSelectionEnd
|
||||
})}
|
||||
class="calendar__day"
|
||||
>
|
||||
${day.date.getDate()}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
return html` <div class="calendar__day calendar__day--empty"></div> `;
|
||||
})}
|
||||
</div>
|
||||
|
||||
<footer class="calendar__footer">
|
||||
<slot name="footer"></slot>
|
||||
</footer>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--border-color: var(--sl-color-neutral-200);
|
||||
--border-radius: var(--sl-border-radius-medium);
|
||||
--border-width: 0;
|
||||
|
||||
display: block;
|
||||
}
|
||||
|
||||
.calendar__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--sl-spacing-x-small);
|
||||
}
|
||||
|
||||
.calendar__header sl-icon-button {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.calendar__label {
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.calendar__days {
|
||||
isolation: isolate;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
}
|
||||
|
||||
.calendar__day {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
border: solid var(--border-width) var(--border-color);
|
||||
border-bottom: none;
|
||||
background: none;
|
||||
background-color: var(--sl-color-neutral-0);
|
||||
font: inherit;
|
||||
color: var(--sl-color-neutral-900);
|
||||
min-height: 3rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.calendar__day:nth-child(1) {
|
||||
border-top-left-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.calendar__day:nth-child(7) {
|
||||
border-top-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.calendar__day:nth-last-child(1) {
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.calendar__day:nth-last-child(7) {
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.calendar__day:not(:nth-child(7n)) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.calendar__day:nth-last-child(1),
|
||||
.calendar__day:nth-last-child(2),
|
||||
.calendar__day:nth-last-child(3),
|
||||
.calendar__day:nth-last-child(4),
|
||||
.calendar__day:nth-last-child(5),
|
||||
.calendar__day:nth-last-child(6),
|
||||
.calendar__day:nth-last-child(7) {
|
||||
border-bottom: solid var(--border-width) var(--border-color);
|
||||
}
|
||||
|
||||
.calendar__day:focus-visible {
|
||||
outline: solid 2px var(--sl-color-primary-600);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.calendar__day[part~='day-weekend'] {
|
||||
color: var(--sl-color-rose-600);
|
||||
}
|
||||
|
||||
.calendar__day[part~='day-today'] {
|
||||
font-weight: var(--sl-font-weight-bold);
|
||||
}
|
||||
|
||||
.calendar__day[part~='day-selected'] {
|
||||
background-color: var(--sl-color-primary-100);
|
||||
}
|
||||
|
||||
.calendar__day[part~='day-selection-start'] {
|
||||
border-top-left-radius: var(--sl-border-radius-pill);
|
||||
border-bottom-left-radius: var(--sl-border-radius-pill);
|
||||
}
|
||||
|
||||
.calendar__day[part~='day-selection-end'] {
|
||||
border-top-right-radius: var(--sl-border-radius-pill);
|
||||
border-bottom-right-radius: var(--sl-border-radius-pill);
|
||||
}
|
||||
|
||||
.calendar__day .calendar__day[part~='day-previous-month'],
|
||||
.calendar__day[part~='day-next-month'] {
|
||||
color: var(--sl-color-neutral-400);
|
||||
}
|
||||
|
||||
.calendar__day[part~='day-previous-month'][part~='day-weekend'],
|
||||
.calendar__day[part~='day-next-month'][part~='day-weekend'] {
|
||||
color: var(--sl-color-rose-400);
|
||||
}
|
||||
`;
|
||||
@@ -1,9 +0,0 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
|
||||
describe('<sl-calendar>', () => {
|
||||
it('should render a component', async () => {
|
||||
// const el = await fixture(html` <sl-calendar></sl-calendar> `);
|
||||
const a = true;
|
||||
expect(a).to.be.true;
|
||||
});
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import SlCalendar from './calendar.component.js';
|
||||
|
||||
export * from './calendar.component.js';
|
||||
export default SlCalendar;
|
||||
|
||||
SlCalendar.define('sl-calendar');
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-calendar': SlCalendar;
|
||||
}
|
||||
}
|
||||
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,141 +0,0 @@
|
||||
export interface CalendarDay {
|
||||
date: Date;
|
||||
isToday: boolean;
|
||||
isWeekday: boolean;
|
||||
isWeekend: boolean;
|
||||
isCurrentMonth: boolean;
|
||||
isPreviousMonth: boolean;
|
||||
isNextMonth: boolean;
|
||||
}
|
||||
|
||||
export interface CalendarGridOptions {
|
||||
weekStartsWith: 'sunday' | 'monday';
|
||||
}
|
||||
|
||||
/** Generates a calendar grid. Month should be 1-12, not 0-11. */
|
||||
export function generateCalendarGrid(year: number, month: number, options?: Partial<CalendarGridOptions>) {
|
||||
const weekStartsWith = options?.weekStartsWith || 'sunday';
|
||||
const today = new Date();
|
||||
const dayThisMonthStartsWith = new Date(year, month - 1, 1).getDay();
|
||||
const lastDayOfMonth = new Date(year, month, 0).getDate();
|
||||
const lastDayOfPreviousMonth =
|
||||
month === 1 ? new Date(year - 1, 1, 0).getDate() : new Date(year, month - 1, 0).getDate();
|
||||
const dayGrid: CalendarDay[] = [];
|
||||
let day = 1;
|
||||
|
||||
do {
|
||||
const date = new Date(year, month - 1, day);
|
||||
let dayOfWeek = new Date(year, month - 1, day).getDay();
|
||||
|
||||
if (weekStartsWith === 'sunday') {
|
||||
//
|
||||
// TODO
|
||||
//
|
||||
}
|
||||
|
||||
// Days in the previous month
|
||||
if (day === 1) {
|
||||
let lastMonthDay = lastDayOfPreviousMonth - dayThisMonthStartsWith + 1;
|
||||
for (let i = 0; i < dayThisMonthStartsWith; i++) {
|
||||
const dayOfLastMonth = new Date(year, month - 2, lastMonthDay);
|
||||
|
||||
dayGrid.push({
|
||||
date: dayOfLastMonth,
|
||||
isToday: isSameDay(dayOfLastMonth, today),
|
||||
isWeekday: isWeekday(dayOfLastMonth),
|
||||
isWeekend: isWeekend(dayOfLastMonth),
|
||||
isCurrentMonth: false,
|
||||
isPreviousMonth: true,
|
||||
isNextMonth: false
|
||||
});
|
||||
|
||||
lastMonthDay++;
|
||||
}
|
||||
}
|
||||
|
||||
dayGrid.push({
|
||||
date,
|
||||
isToday: isSameDay(date, today),
|
||||
isWeekday: isWeekday(date),
|
||||
isWeekend: isWeekend(date),
|
||||
isCurrentMonth: true,
|
||||
isPreviousMonth: false,
|
||||
isNextMonth: false
|
||||
});
|
||||
|
||||
// Days in the next month
|
||||
if (day === lastDayOfMonth) {
|
||||
let nextMonthDay = 1;
|
||||
for (dayOfWeek; dayOfWeek < 6; dayOfWeek++) {
|
||||
const dayOfNextMonth = new Date(year, month, nextMonthDay);
|
||||
|
||||
dayGrid.push({
|
||||
date: dayOfNextMonth,
|
||||
isToday: isSameDay(dayOfNextMonth, today),
|
||||
isWeekday: isWeekday(dayOfNextMonth),
|
||||
isWeekend: isWeekend(dayOfNextMonth),
|
||||
isCurrentMonth: false,
|
||||
isPreviousMonth: false,
|
||||
isNextMonth: true
|
||||
});
|
||||
|
||||
nextMonthDay++;
|
||||
}
|
||||
}
|
||||
|
||||
day++;
|
||||
} while (day <= lastDayOfMonth);
|
||||
|
||||
return dayGrid;
|
||||
}
|
||||
|
||||
/** Generates a localized array of day names. */
|
||||
export function getAllDayNames(locale = 'en', format: Intl.DateTimeFormatOptions['weekday'] = 'long') {
|
||||
const formatter = new Intl.DateTimeFormat(locale, { weekday: format, timeZone: 'UTC' });
|
||||
const days = [1, 2, 3, 4, 5, 6, 7].map(day => {
|
||||
const dd = day < 10 ? `0${day}` : day;
|
||||
return new Date(`2017-01-${dd}T00:00:00+00:00`);
|
||||
});
|
||||
return days.map(date => formatter.format(date));
|
||||
}
|
||||
|
||||
/** Generates a localized array of month names. */
|
||||
export function getAllMonthNames(locale = 'en', format: Intl.DateTimeFormatOptions['month'] = 'long') {
|
||||
const formatter = new Intl.DateTimeFormat(locale, { month: format, timeZone: 'UTC' });
|
||||
const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map(month => {
|
||||
const mm = month < 10 ? `0${month}` : month;
|
||||
return new Date(`2017-${mm}-01T00:00:00+00:00`);
|
||||
});
|
||||
return months.map(date => formatter.format(date));
|
||||
}
|
||||
|
||||
/** Determines if two dates are the same day. */
|
||||
export function isSameDay(date1: Date, date2: Date) {
|
||||
return (
|
||||
date1.getFullYear() === date2.getFullYear() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getDate() === date2.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
/** Determines if the date is a weekend. */
|
||||
export function isWeekend(date: Date) {
|
||||
const day = date.getDay();
|
||||
return day === 0 || day === 6;
|
||||
}
|
||||
|
||||
/** Determines if the date is a weekday. */
|
||||
export function isWeekday(date: Date) {
|
||||
const day = date.getDay();
|
||||
return day > 0 && day < 6;
|
||||
}
|
||||
|
||||
/** Returns a localized, human-readable day name. */
|
||||
export function getDayName(date: Date, locale = 'en', format: Intl.DateTimeFormatOptions['weekday'] = 'long') {
|
||||
return getAllDayNames(locale, format)[date.getDate() - 1];
|
||||
}
|
||||
|
||||
/** Returns a localized, human-readable month name. */
|
||||
export function getMonthName(date: Date, locale = 'en', format: Intl.DateTimeFormatOptions['month'] = 'long') {
|
||||
return getAllMonthNames(locale, format)[date.getMonth()];
|
||||
}
|
||||
@@ -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,11 +0,0 @@
|
||||
export function partMap(map: { [partName: string]: boolean }) {
|
||||
const parts = [];
|
||||
|
||||
for (const [key, value] of Object.entries(map)) {
|
||||
if (value) {
|
||||
parts.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
@@ -8,7 +8,6 @@ export { default as SlBreadcrumb } from './components/breadcrumb/breadcrumb.js';
|
||||
export { default as SlBreadcrumbItem } from './components/breadcrumb-item/breadcrumb-item.js';
|
||||
export { default as SlButton } from './components/button/button.js';
|
||||
export { default as SlButtonGroup } from './components/button-group/button-group.js';
|
||||
export { default as SlCalendar } from './components/calendar/calendar.js';
|
||||
export { default as SlCard } from './components/card/card.js';
|
||||
export { default as SlCarousel } from './components/carousel/carousel.js';
|
||||
export { default as SlCarouselItem } from './components/carousel-item/carousel-item.js';
|
||||
@@ -20,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';
|
||||
|
||||
@@ -16,14 +16,12 @@ const translation: Translation = {
|
||||
goToSlide: (slide, count) => `Go to slide ${slide} of ${count}`,
|
||||
hidePassword: 'Hide password',
|
||||
loading: 'Loading',
|
||||
nextMonth: 'Next month',
|
||||
nextSlide: 'Next slide',
|
||||
numOptionsSelected: num => {
|
||||
if (num === 0) return 'No options selected';
|
||||
if (num === 1) return '1 option selected';
|
||||
return `${num} options selected`;
|
||||
},
|
||||
previousMonth: 'Previous month',
|
||||
previousSlide: 'Previous slide',
|
||||
progress: 'Progress',
|
||||
remove: 'Remove',
|
||||
|
||||
@@ -23,10 +23,8 @@ export interface Translation extends DefaultTranslation {
|
||||
goToSlide: (slide: number, count: number) => string;
|
||||
hidePassword: string;
|
||||
loading: string;
|
||||
nextMonth?: string; // TODO - add to other language packs and remove optional ? flag
|
||||
nextSlide: string;
|
||||
numOptionsSelected: (num: number) => string;
|
||||
previousMonth?: string; // TODO - add to other language packs and remove optional ? flag
|
||||
previousSlide: string;
|
||||
progress: string;
|
||||
remove: string;
|
||||
|
||||
Reference in New Issue
Block a user