@0001-labs/design-system-1 0.3.0-alpha.19 → 0.3.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DS1/0-face/app-banner.ts +1 -1
- package/DS1/0-face/bento.ts +84 -0
- package/DS1/0-face/calendar.ts +2 -2
- package/DS1/1-root/bento.css +97 -0
- package/DS1/1-root/card.css +6 -1
- package/DS1/1-root/cjk.css +1 -1
- package/DS1/1-root/controls.css +1 -0
- package/DS1/1-root/fonts.css +1 -1
- package/DS1/1-root/one.css +1 -1
- package/DS1/1-root/one.semantic.css +1 -1
- package/DS1/3-unit/calendar-base.ts +31 -22
- package/DS1/3-unit/ds-bento.ts +67 -0
- package/DS1/3-unit/ds-calendar.ts +477 -22
- package/DS1/3-unit/styles/ds-bento.css +66 -0
- package/DS1/3-unit/styles/ds-calendar.css +119 -1
- package/DS1/index.ts +1 -1
- package/dist/0-face/app-banner.js +1 -1
- package/dist/0-face/bento.d.ts +41 -0
- package/dist/0-face/bento.d.ts.map +1 -0
- package/dist/0-face/bento.js +77 -0
- package/dist/0-face/calendar.js +2 -2
- package/dist/3-unit/calendar-base.d.ts +17 -12
- package/dist/3-unit/calendar-base.d.ts.map +1 -1
- package/dist/3-unit/calendar-base.js +21 -16
- package/dist/3-unit/ds-bento.d.ts +31 -0
- package/dist/3-unit/ds-bento.d.ts.map +1 -0
- package/dist/3-unit/ds-bento.js +47 -0
- package/dist/3-unit/ds-calendar-large.d.ts +3 -4
- package/dist/3-unit/ds-calendar-large.d.ts.map +1 -1
- package/dist/3-unit/ds-calendar.d.ts +139 -12
- package/dist/3-unit/ds-calendar.d.ts.map +1 -1
- package/dist/3-unit/ds-calendar.js +464 -23
- package/dist/design-system-1.bundle.js +1125 -1095
- package/dist/design-system-1.bundle.js.map +1 -1
- package/dist/design-system-1.bundle.min.js +2484 -2457
- package/dist/design-system-1.bundle.min.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/DS1/3-unit/ds-date-picker.ts +0 -540
- package/DS1/3-unit/styles/ds-date-picker.css +0 -124
- package/dist/3-unit/ds-date-picker.d.ts +0 -203
- package/dist/3-unit/ds-date-picker.d.ts.map +0 -1
- package/dist/3-unit/ds-date-picker.js +0 -499
package/dist/index.d.ts
CHANGED
|
@@ -48,8 +48,8 @@ export * from "./2-core/ds-avatar";
|
|
|
48
48
|
export * from "./3-unit/ds-avatar-group";
|
|
49
49
|
export * from "./3-unit/ds-calendar";
|
|
50
50
|
export * from "./3-unit/ds-calendar-large";
|
|
51
|
+
export * from "./3-unit/ds-bento";
|
|
51
52
|
export * from "./3-unit/ds-carousel";
|
|
52
|
-
export * from "./3-unit/ds-date-picker";
|
|
53
53
|
export * from "./3-unit/ds-form";
|
|
54
54
|
export * from "./3-unit/ds-list";
|
|
55
55
|
export * from "./3-unit/ds-matrix";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../DS1/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,iBAAiB,CAAC;AAGzB,OAAO,eAAe,CAAC;AAMvB,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AAMjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AAMpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../DS1/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,iBAAiB,CAAC;AAGzB,OAAO,eAAe,CAAC;AAMvB,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AAMjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AAMpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAMlC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AAErC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AAExC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,mBAAmB,CAAC;AAElC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -62,8 +62,8 @@ export * from "./2-core/ds-avatar";
|
|
|
62
62
|
export * from "./3-unit/ds-avatar-group";
|
|
63
63
|
export * from "./3-unit/ds-calendar";
|
|
64
64
|
export * from "./3-unit/ds-calendar-large";
|
|
65
|
+
export * from "./3-unit/ds-bento";
|
|
65
66
|
export * from "./3-unit/ds-carousel";
|
|
66
|
-
export * from "./3-unit/ds-date-picker";
|
|
67
67
|
export * from "./3-unit/ds-form";
|
|
68
68
|
export * from "./3-unit/ds-list";
|
|
69
69
|
export * from "./3-unit/ds-matrix";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0001-labs/design-system-1",
|
|
3
|
-
"version": "0.3.0-alpha.
|
|
3
|
+
"version": "0.3.0-alpha.21",
|
|
4
4
|
"description": "A component-based design system built with TypeScript and LitElement that provides reusable UI components with built-in theming, internationalization, and accessibility features.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"components",
|
|
@@ -1,540 +0,0 @@
|
|
|
1
|
-
// ds-date-picker.ts
|
|
2
|
-
// A field that opens a panel below it: the first and last day written out
|
|
3
|
-
// where they can be typed, a small month one square a day, a switch for a
|
|
4
|
-
// time, and Clear. It always collects a range: the first day pressed starts
|
|
5
|
-
// it and the second ends it; leaving after the first keeps that one day. A
|
|
6
|
-
// form takes the value as ISO 8601: a day, a day and a time, or two of them
|
|
7
|
-
// joined by a slash.
|
|
8
|
-
|
|
9
|
-
import { LitElement, html, nothing, unsafeCSS, type PropertyValues } from "lit";
|
|
10
|
-
import { define } from "../0-face/define";
|
|
11
|
-
import { getText } from "../0-face/i18n";
|
|
12
|
-
import { dateFormatter, type DateLength } from "../0-face/format";
|
|
13
|
-
import { dayOf, isDay, today as todayDay, type Day } from "../0-face/calendar";
|
|
14
|
-
import "../2-core/ds-icon";
|
|
15
|
-
import "./ds-calendar";
|
|
16
|
-
import type { Calendar, CalendarChangeDetail } from "./ds-calendar";
|
|
17
|
-
import metrics from "../1-root/component-metrics.css?inline";
|
|
18
|
-
import controls from "../1-root/controls.css?inline";
|
|
19
|
-
import styles from "./styles/ds-date-picker.css?inline";
|
|
20
|
-
|
|
21
|
-
/** Detail of `change` and `input`: the value now and before; each is empty when there is none. */
|
|
22
|
-
export interface DatePickerChangeDetail {
|
|
23
|
-
value: string;
|
|
24
|
-
previous: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** One end of a value: its day and, when it has one, its time (`14:30`). */
|
|
28
|
-
export interface DatePickerEnd {
|
|
29
|
-
day: Day;
|
|
30
|
-
time: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const TIME = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
34
|
-
|
|
35
|
-
/** Reads a value: `2026-10-21`, `2026-10-21T14:30`, or two of them joined by `/`. */
|
|
36
|
-
export function readDateValue(value: string | null | undefined): { start?: DatePickerEnd; end?: DatePickerEnd } {
|
|
37
|
-
const read = (part: string | undefined): DatePickerEnd | undefined => {
|
|
38
|
-
const [day = "", time = ""] = (part ?? "").trim().split("T");
|
|
39
|
-
if (!isDay(day)) return undefined;
|
|
40
|
-
const clock = time.slice(0, 5);
|
|
41
|
-
return { day, time: TIME.test(clock) ? clock : "" };
|
|
42
|
-
};
|
|
43
|
-
const [first, second] = (value ?? "").split("/");
|
|
44
|
-
let start = read(first), end = read(second);
|
|
45
|
-
if (start && end && `${end.day}T${end.time}` < `${start.day}T${start.time}`) [start, end] = [end, start];
|
|
46
|
-
return { start, end };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Writes a value from its ends, the times only with `time`. An end that is
|
|
51
|
-
* the same moment as the start leaves the value one day.
|
|
52
|
-
*/
|
|
53
|
-
export function writeDateValue(start: DatePickerEnd | undefined, end: DatePickerEnd | undefined, options: { time: boolean }): string {
|
|
54
|
-
if (!start) return "";
|
|
55
|
-
const part = (side: DatePickerEnd) => (options.time && side.time ? `${side.day}T${side.time}` : side.day);
|
|
56
|
-
return end && part(end) !== part(start) ? `${part(start)}/${part(end)}` : part(start);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Reads what a person typed: a day written `YYYY-MM-DD`, or a date the browser can read ("Oct 21, 2026"). */
|
|
60
|
-
export function parseTypedDay(text: string): Day | null {
|
|
61
|
-
const trimmed = text.trim();
|
|
62
|
-
if (isDay(trimmed)) return trimmed;
|
|
63
|
-
const moment = Date.parse(trimmed);
|
|
64
|
-
return Number.isFinite(moment) ? dayOf(new Date(moment)) : null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const openPickers = new Set<DatePicker>();
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* @element ds-date-picker
|
|
71
|
-
* @prop {string} value - `YYYY-MM-DD`, `YYYY-MM-DDTHH:MM`, or two joined by
|
|
72
|
-
* `/` for a range; empty for none.
|
|
73
|
-
* @prop {string} name - The form field's name.
|
|
74
|
-
* @prop {string} label - The field's accessible name: a translation key, or
|
|
75
|
-
* literal words.
|
|
76
|
-
* @prop {boolean} literal - Show `label` and `placeholder` as written.
|
|
77
|
-
* @prop {string} placeholder - The words before a day is chosen; the shared
|
|
78
|
-
* "Choose a date" by default.
|
|
79
|
-
* @prop {string} length - How a day is written: `short`, `medium` (the
|
|
80
|
-
* default) or `long`.
|
|
81
|
-
* @prop {boolean} time - Choose a time with each day (the Include time switch).
|
|
82
|
-
* @prop {boolean} noOptions - Leave the Include time switch out (`no-options`);
|
|
83
|
-
* `time` still applies.
|
|
84
|
-
* @prop {string} min - The first day a person may choose.
|
|
85
|
-
* @prop {string} max - The last day a person may choose.
|
|
86
|
-
* @prop {string} weekStart - The week's first day (`week-start`); the
|
|
87
|
-
* language's own by default.
|
|
88
|
-
* @prop {string} today - The day drawn as today; the reader's own by default.
|
|
89
|
-
* @prop {boolean} required - A form needs a value; Clear is left out.
|
|
90
|
-
* @prop {boolean} disabled - The field cannot be opened.
|
|
91
|
-
* @prop {boolean} open - The panel is open.
|
|
92
|
-
*
|
|
93
|
-
* @fires change - A person changed the value; `{ value, previous }`.
|
|
94
|
-
* @fires input - The same moment, for code listening as to a field.
|
|
95
|
-
* @fires ds-date-picker-toggle - The panel opened or closed; `{ open }`.
|
|
96
|
-
*
|
|
97
|
-
* @example
|
|
98
|
-
* <ds-date-picker label="When" literal name="when" value="2026-10-21/2026-10-24"></ds-date-picker>
|
|
99
|
-
*/
|
|
100
|
-
export class DatePicker extends LitElement {
|
|
101
|
-
static formAssociated = true;
|
|
102
|
-
|
|
103
|
-
static properties = {
|
|
104
|
-
value: { type: String, reflect: true },
|
|
105
|
-
name: { type: String, reflect: true },
|
|
106
|
-
label: { type: String },
|
|
107
|
-
literal: { type: Boolean, reflect: true },
|
|
108
|
-
placeholder: { type: String },
|
|
109
|
-
length: { type: String },
|
|
110
|
-
time: { type: Boolean, reflect: true },
|
|
111
|
-
noOptions: { type: Boolean, attribute: "no-options", reflect: true },
|
|
112
|
-
min: { type: String },
|
|
113
|
-
max: { type: String },
|
|
114
|
-
weekStart: { type: String, attribute: "week-start" },
|
|
115
|
-
today: { type: String },
|
|
116
|
-
required: { type: Boolean, reflect: true },
|
|
117
|
-
disabled: { type: Boolean, reflect: true },
|
|
118
|
-
open: { type: Boolean, reflect: true },
|
|
119
|
-
awaitingEnd: { state: true },
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
static styles = [unsafeCSS(metrics), unsafeCSS(controls), unsafeCSS(styles)];
|
|
123
|
-
|
|
124
|
-
declare value: string;
|
|
125
|
-
declare name: string | undefined;
|
|
126
|
-
declare label: string | undefined;
|
|
127
|
-
declare literal: boolean;
|
|
128
|
-
declare placeholder: string | undefined;
|
|
129
|
-
declare length: DateLength | undefined;
|
|
130
|
-
declare time: boolean;
|
|
131
|
-
declare noOptions: boolean;
|
|
132
|
-
declare min: string | undefined;
|
|
133
|
-
declare max: string | undefined;
|
|
134
|
-
declare weekStart: string | undefined;
|
|
135
|
-
declare today: string | undefined;
|
|
136
|
-
declare required: boolean;
|
|
137
|
-
declare disabled: boolean;
|
|
138
|
-
declare open: boolean;
|
|
139
|
-
/** A first day has been pressed in this opening and the last is still to come. */
|
|
140
|
-
declare awaitingEnd: boolean;
|
|
141
|
-
|
|
142
|
-
private readonly internals = typeof this.attachInternals === "function" ? this.attachInternals() : null;
|
|
143
|
-
/** The value the field started with, for a form's reset. */
|
|
144
|
-
private initial = "";
|
|
145
|
-
private shown = false;
|
|
146
|
-
/** Whether opening should move focus into the month (a key opened it). */
|
|
147
|
-
private focusMonth = false;
|
|
148
|
-
private readonly relabel = () => this.requestUpdate();
|
|
149
|
-
|
|
150
|
-
constructor() {
|
|
151
|
-
super();
|
|
152
|
-
this.value = "";
|
|
153
|
-
this.literal = false;
|
|
154
|
-
this.time = false;
|
|
155
|
-
this.noOptions = false;
|
|
156
|
-
this.required = false;
|
|
157
|
-
this.disabled = false;
|
|
158
|
-
this.open = false;
|
|
159
|
-
this.awaitingEnd = false;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
connectedCallback() {
|
|
163
|
-
super.connectedCallback();
|
|
164
|
-
window.addEventListener("language-changed", this.relabel);
|
|
165
|
-
window.addEventListener("translations-loaded", this.relabel);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
disconnectedCallback() {
|
|
169
|
-
window.removeEventListener("language-changed", this.relabel);
|
|
170
|
-
window.removeEventListener("translations-loaded", this.relabel);
|
|
171
|
-
this.hide(false);
|
|
172
|
-
super.disconnectedCallback();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
protected firstUpdated() {
|
|
176
|
-
this.initial = this.value;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** The form was reset: back to the value it started with. */
|
|
180
|
-
formResetCallback() {
|
|
181
|
-
this.value = this.initial;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
formDisabledCallback(disabled: boolean) {
|
|
185
|
-
this.disabled = disabled;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private words(value: string | undefined, fallback: string): string {
|
|
189
|
-
if (value && this.literal) return value;
|
|
190
|
-
return getText(value || fallback) || value || fallback;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private get trigger() {
|
|
194
|
-
return this.renderRoot.querySelector<HTMLButtonElement>(".trigger");
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
private get panel() {
|
|
198
|
-
return this.renderRoot.querySelector<HTMLElement>(".popover");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private get calendar() {
|
|
202
|
-
return this.renderRoot.querySelector<Calendar>("ds-calendar");
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
private get ends() {
|
|
206
|
-
return readDateValue(this.value);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
protected willUpdate(changed: PropertyValues<this>) {
|
|
210
|
-
super.willUpdate(changed);
|
|
211
|
-
if (changed.has("value") && this.value) {
|
|
212
|
-
const { start, end } = this.ends;
|
|
213
|
-
// A value with a time turns the switch on.
|
|
214
|
-
if (!start) this.value = "";
|
|
215
|
-
else if (start.time || end?.time) this.time = true;
|
|
216
|
-
}
|
|
217
|
-
if (changed.has("disabled") && this.disabled) this.open = false;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
protected updated(changed: PropertyValues<this>) {
|
|
221
|
-
super.updated(changed);
|
|
222
|
-
if (changed.has("value") || changed.has("required")) {
|
|
223
|
-
this.internals?.setFormValue(this.value || null);
|
|
224
|
-
if (this.required && !this.value) this.internals?.setValidity({ valueMissing: true }, this.words(undefined, "Choose a date"), this.trigger ?? undefined);
|
|
225
|
-
else this.internals?.setValidity({});
|
|
226
|
-
}
|
|
227
|
-
if (changed.has("open")) {
|
|
228
|
-
if (this.open) this.show();
|
|
229
|
-
else this.hide(changed.get("open") === true);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** Opens the panel. */
|
|
234
|
-
show() {
|
|
235
|
-
if (this.shown || this.disabled || !this.isConnected) return;
|
|
236
|
-
const panel = this.panel;
|
|
237
|
-
if (!panel) return;
|
|
238
|
-
for (const other of openPickers) if (other !== this) other.open = false;
|
|
239
|
-
openPickers.add(this);
|
|
240
|
-
this.shown = true;
|
|
241
|
-
this.open = true;
|
|
242
|
-
this.awaitingEnd = false;
|
|
243
|
-
const calendar = this.calendar;
|
|
244
|
-
if (calendar) calendar.month = (this.ends.start?.day || this.today || todayDay()).slice(0, 7);
|
|
245
|
-
panel.showPopover?.();
|
|
246
|
-
this.place();
|
|
247
|
-
if (calendar) void calendar.updateComplete.then(this.place);
|
|
248
|
-
this.ownerDocument.addEventListener("pointerdown", this.onOutside, true);
|
|
249
|
-
this.ownerDocument.addEventListener("scroll", this.place, true);
|
|
250
|
-
window.addEventListener("resize", this.place);
|
|
251
|
-
this.dispatchEvent(new CustomEvent("ds-date-picker-toggle", { bubbles: true, composed: true, detail: { open: true } }));
|
|
252
|
-
if (this.focusMonth) void this.focusDay();
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Closes the panel, giving focus back to the field when it was inside. */
|
|
256
|
-
hide(restore = true) {
|
|
257
|
-
if (!this.shown) return;
|
|
258
|
-
this.shown = false;
|
|
259
|
-
openPickers.delete(this);
|
|
260
|
-
this.awaitingEnd = false;
|
|
261
|
-
const panel = this.panel;
|
|
262
|
-
const within = panel?.matches(":focus-within");
|
|
263
|
-
if (panel?.matches(":popover-open")) panel.hidePopover?.();
|
|
264
|
-
this.ownerDocument.removeEventListener("pointerdown", this.onOutside, true);
|
|
265
|
-
this.ownerDocument.removeEventListener("scroll", this.place, true);
|
|
266
|
-
window.removeEventListener("resize", this.place);
|
|
267
|
-
this.open = false;
|
|
268
|
-
if (restore && within) this.trigger?.focus({ preventScroll: true });
|
|
269
|
-
this.dispatchEvent(new CustomEvent("ds-date-picker-toggle", { bubbles: true, composed: true, detail: { open: false } }));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
private async focusDay() {
|
|
273
|
-
const calendar = this.calendar;
|
|
274
|
-
if (!calendar) return;
|
|
275
|
-
await calendar.updateComplete;
|
|
276
|
-
calendar.renderRoot.querySelector<HTMLElement>('[data-day][tabindex="0"]')?.focus({ preventScroll: true });
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** Below the field, half a square from it, or above when there is more room there; held inside the screen. */
|
|
280
|
-
private place = () => {
|
|
281
|
-
const panel = this.panel, trigger = this.trigger;
|
|
282
|
-
if (!this.shown || !panel || !trigger) return;
|
|
283
|
-
const rect = trigger.getBoundingClientRect();
|
|
284
|
-
const view = this.ownerDocument.defaultView!;
|
|
285
|
-
const viewport = view.visualViewport;
|
|
286
|
-
const left = viewport?.offsetLeft ?? 0, top = viewport?.offsetTop ?? 0;
|
|
287
|
-
const width = viewport?.width ?? view.innerWidth, height = viewport?.height ?? view.innerHeight;
|
|
288
|
-
const box = panel.getBoundingClientRect();
|
|
289
|
-
const gap = (parseFloat(getComputedStyle(panel).paddingTop) || 9) + 1;
|
|
290
|
-
// The face is one control high at the foot of its square: measure from that square.
|
|
291
|
-
const square = Math.max(rect.height, gap * 2);
|
|
292
|
-
const foot = rect.bottom, head = rect.bottom - square;
|
|
293
|
-
const below = top + height - foot - 2 * gap, above = head - top - 2 * gap;
|
|
294
|
-
const up = below < box.height && above > below;
|
|
295
|
-
const x = getComputedStyle(this).direction === "rtl" ? rect.right - box.width : rect.left;
|
|
296
|
-
panel.style.left = `${Math.max(left + gap, Math.min(x, left + width - gap - box.width))}px`;
|
|
297
|
-
panel.style.top = `${Math.max(top + gap, up ? head - gap - box.height : foot + gap)}px`;
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
private onOutside = (event: Event) => {
|
|
301
|
-
if (!event.composedPath().includes(this)) this.open = false;
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
private toggle = () => {
|
|
305
|
-
if (this.disabled) return;
|
|
306
|
-
this.focusMonth = false;
|
|
307
|
-
this.open = !this.open;
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
private onTriggerKey = (event: KeyboardEvent) => {
|
|
311
|
-
if (this.disabled) return;
|
|
312
|
-
if (event.key === "ArrowDown" || event.key === "ArrowUp" || ((event.key === "Enter" || event.key === " ") && !this.open)) {
|
|
313
|
-
event.preventDefault();
|
|
314
|
-
this.focusMonth = true;
|
|
315
|
-
if (this.open) void this.focusDay();
|
|
316
|
-
else this.open = true;
|
|
317
|
-
}
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
private onPanelKey = (event: KeyboardEvent) => {
|
|
321
|
-
if (event.key === "Escape" && !event.defaultPrevented) {
|
|
322
|
-
event.preventDefault();
|
|
323
|
-
event.stopPropagation();
|
|
324
|
-
this.open = false;
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
private onFocusOut = (event: FocusEvent) => {
|
|
329
|
-
const next = event.relatedTarget;
|
|
330
|
-
if (this.open && next instanceof Node && !this.contains(next) && !this.renderRoot.contains(next)) this.open = false;
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
/** Takes a new value and says so. */
|
|
334
|
-
private commit(value: string) {
|
|
335
|
-
const previous = this.value || "";
|
|
336
|
-
if (value === previous) return;
|
|
337
|
-
this.value = value;
|
|
338
|
-
const detail: DatePickerChangeDetail = { value, previous };
|
|
339
|
-
this.dispatchEvent(new CustomEvent("input", { bubbles: true, composed: true, detail }));
|
|
340
|
-
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true, detail }));
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
private write(start: DatePickerEnd | undefined, end: DatePickerEnd | undefined) {
|
|
344
|
-
this.commit(writeDateValue(start, end, { time: this.time }));
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* A day pressed in the month. The first press of an opening starts the range
|
|
349
|
-
* (the value is that day until another is pressed); the second ends it and,
|
|
350
|
-
* without times to set, closes the panel.
|
|
351
|
-
*/
|
|
352
|
-
private picked = (event: CustomEvent<CalendarChangeDetail>) => {
|
|
353
|
-
event.stopPropagation();
|
|
354
|
-
const { start, end } = this.ends;
|
|
355
|
-
const first: DatePickerEnd = { day: event.detail.start, time: start?.time || (this.time ? "09:00" : "") };
|
|
356
|
-
if (!event.detail.end) {
|
|
357
|
-
this.awaitingEnd = true;
|
|
358
|
-
this.write(first, undefined);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
this.awaitingEnd = false;
|
|
362
|
-
const last: DatePickerEnd = { day: event.detail.end, time: end?.time || (this.time ? "10:00" : "") };
|
|
363
|
-
this.write(first, last);
|
|
364
|
-
if (!this.time) this.open = false;
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
/** A day typed in a field, read when the person leaves it or presses Enter. */
|
|
368
|
-
private typed(side: "start" | "end", input: HTMLInputElement) {
|
|
369
|
-
const { start, end } = this.ends;
|
|
370
|
-
const text = input.value.trim();
|
|
371
|
-
if (!text) {
|
|
372
|
-
if (side === "start") this.commit("");
|
|
373
|
-
else this.write(start, undefined);
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
const day = parseTypedDay(text);
|
|
377
|
-
if (!day) {
|
|
378
|
-
input.value = this.fieldText(side === "start" ? start : end);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
const current = side === "start" ? start : end;
|
|
382
|
-
const next = { day, time: current?.time || (this.time ? (side === "start" ? "09:00" : "10:00") : "") };
|
|
383
|
-
if (side === "start") this.write(next, end);
|
|
384
|
-
else this.write(start ?? next, next);
|
|
385
|
-
this.awaitingEnd = false;
|
|
386
|
-
const calendar = this.calendar;
|
|
387
|
-
if (calendar) calendar.month = day.slice(0, 7);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
private timed(side: "start" | "end", input: HTMLInputElement) {
|
|
391
|
-
const { start, end } = this.ends;
|
|
392
|
-
const clock = TIME.test(input.value) ? input.value : "";
|
|
393
|
-
if (side === "start" && start) this.write({ ...start, time: clock }, end);
|
|
394
|
-
if (side === "end" && start) this.write(start, { day: (end ?? start).day, time: clock });
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
private setTime = () => {
|
|
398
|
-
this.time = !this.time;
|
|
399
|
-
const { start, end } = this.ends;
|
|
400
|
-
const timed = (side: DatePickerEnd | undefined, clock: string) => (side ? { ...side, time: this.time ? side.time || clock : "" } : undefined);
|
|
401
|
-
this.write(timed(start, "09:00"), timed(end ?? (this.time ? start : undefined), "10:00"));
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
/** Pressing the switch's row presses the switch, as a label would. */
|
|
405
|
-
private passPress = (event: MouseEvent) => {
|
|
406
|
-
const row = event.currentTarget as HTMLElement;
|
|
407
|
-
const control = row.querySelector<HTMLButtonElement>(".ds-switch");
|
|
408
|
-
if (control && !event.composedPath().includes(control)) control.click();
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
private clear = () => {
|
|
412
|
-
this.commit("");
|
|
413
|
-
this.open = false;
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
private moment(side: DatePickerEnd) {
|
|
417
|
-
const [year, month, date] = side.day.split("-").map(Number);
|
|
418
|
-
const [hours, minutes] = (side.time || "12:00").split(":").map(Number);
|
|
419
|
-
return Date.UTC(year, month - 1, date, hours, minutes);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
private formatter(withTime: boolean) {
|
|
423
|
-
return dateFormatter({ dateStyle: this.length ?? "medium", ...(withTime ? { timeStyle: "short" } : {}), timeZone: "UTC" }) as Intl.DateTimeFormat & {
|
|
424
|
-
formatRange?: (from: number, to: number) => string;
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
private fieldText(side: DatePickerEnd | undefined) {
|
|
429
|
-
return side ? this.formatter(false).format(this.moment(side)) : "";
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/** The face's words: one day, or a range written as the language writes one ("Oct 21 – 24, 2026"). */
|
|
433
|
-
private faceText(): string {
|
|
434
|
-
const { start, end } = this.ends;
|
|
435
|
-
if (!start) return this.words(this.placeholder, "Choose a date");
|
|
436
|
-
const formatter = this.formatter(Boolean(start.time || end?.time));
|
|
437
|
-
if (!end) return formatter.format(this.moment(start));
|
|
438
|
-
return formatter.formatRange ? formatter.formatRange(this.moment(start), this.moment(end)) : `${formatter.format(this.moment(start))} – ${formatter.format(this.moment(end))}`;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
private renderEnd(side: "start" | "end", value: DatePickerEnd | undefined, name: string) {
|
|
442
|
-
const label = side === "start" ? name : `${name}, ${this.words(undefined, "End date")}`;
|
|
443
|
-
return html`<div class="field-row" part="field">
|
|
444
|
-
<input
|
|
445
|
-
class="ds-input field"
|
|
446
|
-
type="text"
|
|
447
|
-
autocomplete="off"
|
|
448
|
-
aria-label=${label}
|
|
449
|
-
placeholder=${side === "start" ? this.words(this.placeholder, "Choose a date") : this.words(undefined, "End date")}
|
|
450
|
-
.value=${this.fieldText(value)}
|
|
451
|
-
@change=${(event: Event) => this.typed(side, event.target as HTMLInputElement)}
|
|
452
|
-
@keydown=${(event: KeyboardEvent) => { if (event.key === "Enter") { event.preventDefault(); this.typed(side, event.target as HTMLInputElement); } }}
|
|
453
|
-
/>
|
|
454
|
-
</div>
|
|
455
|
-
${this.time
|
|
456
|
-
? html`<div class="field-row" part="field">
|
|
457
|
-
<input
|
|
458
|
-
class="ds-input clock"
|
|
459
|
-
type="time"
|
|
460
|
-
aria-label=${`${label}, ${this.words(undefined, "Time")}`}
|
|
461
|
-
.value=${value?.time ?? ""}
|
|
462
|
-
?disabled=${!this.ends.start}
|
|
463
|
-
@change=${(event: Event) => this.timed(side, event.target as HTMLInputElement)}
|
|
464
|
-
/>
|
|
465
|
-
</div>`
|
|
466
|
-
: nothing}`;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
render() {
|
|
470
|
-
const { start, end } = this.ends;
|
|
471
|
-
const text = this.faceText();
|
|
472
|
-
const name = this.label ? this.words(this.label, this.label) : this.words(undefined, "Date");
|
|
473
|
-
// While the end is still to come the month holds the start alone; otherwise the range is whole (one day is a range of one).
|
|
474
|
-
const days = start ? (this.awaitingEnd ? start.day : `${start.day}/${(end ?? start).day}`) : "";
|
|
475
|
-
return html`<button
|
|
476
|
-
class="ds-select trigger"
|
|
477
|
-
part="trigger"
|
|
478
|
-
type="button"
|
|
479
|
-
aria-haspopup="dialog"
|
|
480
|
-
aria-expanded=${String(this.open)}
|
|
481
|
-
aria-label=${start ? `${name}, ${text}` : name}
|
|
482
|
-
?disabled=${this.disabled}
|
|
483
|
-
?data-empty=${!start}
|
|
484
|
-
@click=${this.toggle}
|
|
485
|
-
@keydown=${this.onTriggerKey}
|
|
486
|
-
>
|
|
487
|
-
<ds-icon class="icon" type="calendar" aria-hidden="true"></ds-icon>
|
|
488
|
-
<span class="text">${text}</span>
|
|
489
|
-
<ds-icon class="arrow" type="down" aria-hidden="true"></ds-icon>
|
|
490
|
-
</button>
|
|
491
|
-
<div
|
|
492
|
-
class="popover"
|
|
493
|
-
part="popover"
|
|
494
|
-
popover="manual"
|
|
495
|
-
role="dialog"
|
|
496
|
-
aria-label=${name}
|
|
497
|
-
@keydown=${this.onPanelKey}
|
|
498
|
-
@focusout=${this.onFocusOut}
|
|
499
|
-
>
|
|
500
|
-
<div class="fields">
|
|
501
|
-
${this.renderEnd("start", start, name)}
|
|
502
|
-
${this.renderEnd("end", start ? end ?? start : undefined, name)}
|
|
503
|
-
</div>
|
|
504
|
-
<ds-calendar
|
|
505
|
-
compact
|
|
506
|
-
selectable
|
|
507
|
-
range
|
|
508
|
-
fixed
|
|
509
|
-
outside-days
|
|
510
|
-
.value=${days}
|
|
511
|
-
.min=${this.min}
|
|
512
|
-
.max=${this.max}
|
|
513
|
-
.weekStart=${this.weekStart}
|
|
514
|
-
.today=${this.today}
|
|
515
|
-
@change=${this.picked}
|
|
516
|
-
></ds-calendar>
|
|
517
|
-
<div class="options" part="options">
|
|
518
|
-
${this.noOptions
|
|
519
|
-
? nothing
|
|
520
|
-
: html`<div class="separator" aria-hidden="true"></div>
|
|
521
|
-
<div class="option" @click=${this.passPress}>
|
|
522
|
-
<span>${this.words(undefined, "Include time")}</span>
|
|
523
|
-
<button class="ds-switch" type="button" role="switch" aria-label=${this.words(undefined, "Include time")} aria-checked=${String(this.time)} @click=${this.setTime}></button>
|
|
524
|
-
</div>`}
|
|
525
|
-
${this.required
|
|
526
|
-
? nothing
|
|
527
|
-
: html`<div class="separator" aria-hidden="true"></div>
|
|
528
|
-
<button class="option clear" part="clear" type="button" ?disabled=${!start} @click=${this.clear}>${this.words(undefined, "Clear")}</button>`}
|
|
529
|
-
</div>
|
|
530
|
-
</div>`;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
define("ds-date-picker", DatePicker);
|
|
535
|
-
|
|
536
|
-
declare global {
|
|
537
|
-
interface HTMLElementTagNameMap {
|
|
538
|
-
"ds-date-picker": DatePicker;
|
|
539
|
-
}
|
|
540
|
-
}
|