@reliability-design/web 1.4.4 → 1.4.6
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/dist/components/rel-accordion/rel-accordion.ts +13 -1
- package/dist/components/rel-alert/rel-alert.ts +1 -1
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +33 -2
- package/dist/components/rel-badge/rel-badge.ts +6 -1
- package/dist/components/rel-button/rel-button.ts +18 -2
- package/dist/components/rel-calendar/rel-calendar.ts +86 -18
- package/dist/components/rel-carousel/rel-carousel.ts +45 -2
- package/dist/components/rel-checkbox/rel-checkbox.ts +4 -1
- package/dist/components/rel-chip/rel-chip.ts +59 -19
- package/dist/components/rel-date-picker/date-parse.test.ts +116 -0
- package/dist/components/rel-date-picker/date-parse.ts +178 -0
- package/dist/components/rel-date-picker/rel-date-picker.ts +675 -93
- package/dist/components/rel-drawer/rel-drawer.ts +28 -61
- package/dist/components/rel-menu/rel-menu.ts +130 -15
- package/dist/components/rel-menubar/rel-menubar.ts +221 -63
- package/dist/components/rel-nav-menu/rel-nav-menu.ts +39 -3
- package/dist/components/rel-otp/rel-otp.ts +14 -4
- package/dist/components/rel-pagination/rel-pagination.ts +13 -0
- package/dist/components/rel-popover/rel-popover.ts +64 -5
- package/dist/components/rel-progress/rel-progress.ts +13 -4
- package/dist/components/rel-radio/README.md +6 -3
- package/dist/components/rel-radio/rel-radio.ts +236 -60
- package/dist/components/rel-range-picker/rel-range-picker.ts +581 -91
- package/dist/components/rel-resizable/rel-resizable-group.ts +20 -0
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +55 -13
- package/dist/components/rel-select/rel-select.ts +203 -24
- package/dist/components/rel-sheet/rel-sheet.ts +184 -18
- package/dist/components/rel-sidebar/rel-sidebar.ts +50 -40
- package/dist/components/rel-skeleton/rel-skeleton.ts +9 -0
- package/dist/components/rel-slider/rel-slider.ts +6 -3
- package/dist/components/rel-snackbar/rel-snackbar.ts +20 -16
- package/dist/components/rel-stepper/rel-stepper.ts +47 -2
- package/dist/components/rel-switch/rel-switch.ts +3 -1
- package/dist/components/rel-tab/rel-tab.ts +32 -7
- package/dist/components/rel-textfield/rel-textfield.ts +263 -75
- package/dist/components/rel-time-picker/rel-time-picker.ts +749 -261
- package/dist/components/rel-time-picker/time-parse.test.ts +67 -0
- package/dist/components/rel-time-picker/time-parse.ts +85 -0
- package/dist/components/rel-toast/rel-toast.ts +4 -2
- package/dist/components/rel-tooltip/README.md +13 -5
- package/dist/components/rel-tooltip/rel-tooltip.ts +33 -0
- package/dist/components/rel-topbar/rel-topbar-actions.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-brand.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-dropdown.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-item.ts +318 -319
- package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-profile.ts +224 -224
- package/dist/components/rel-topbar/rel-topbar-utilities.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar.ts +440 -440
- package/dist/components/rel-upload/rel-upload.ts +25 -0
- package/dist/index.d.mts +461 -23
- package/dist/index.d.ts +461 -23
- package/dist/index.js +3906 -807
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4143 -1055
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { LitElement, html, css, nothing } from "lit";
|
|
2
2
|
import { customElement, property, state, query } from "lit/decorators.js";
|
|
3
|
+
import { live } from "lit/directives/live.js";
|
|
3
4
|
import "@reliability-design/icons/rel-icon";
|
|
4
5
|
import "../rel-button/rel-button";
|
|
6
|
+
import { parseTimeInput } from "./time-parse";
|
|
7
|
+
import { announce } from "../../utils/announce";
|
|
5
8
|
|
|
6
|
-
type TimeColumn = "hour" | "minute" | "period";
|
|
9
|
+
type TimeColumn = "hour" | "minute" | "second" | "period";
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* `<rel-time-picker>` — time selection field.
|
|
@@ -13,12 +16,18 @@ type TimeColumn = "hour" | "minute" | "period";
|
|
|
13
16
|
* column selections are held in draft state until Apply is clicked; Cancel,
|
|
14
17
|
* Escape, or an outside click discards the draft.
|
|
15
18
|
*
|
|
16
|
-
* The committed `value` is ALWAYS 24-hour "HH:mm"
|
|
17
|
-
* `format`, so consumers get
|
|
19
|
+
* The committed `value` is ALWAYS 24-hour "HH:mm" (or "HH:mm:ss" when
|
|
20
|
+
* `seconds` is enabled) regardless of the display `format`, so consumers get
|
|
21
|
+
* a consistent machine-readable string.
|
|
22
|
+
*
|
|
23
|
+
* Panel visuals follow the Figma "Time Picker" component (Reliability DS
|
|
24
|
+
* V1.0, node 8301:3771): fixed panel widths 220/250/280 by size, full-bleed
|
|
25
|
+
* columns with 1px dividers, neutral selected state, and a Now / Cancel / OK
|
|
26
|
+
* footer whose buttons are always size S.
|
|
18
27
|
*/
|
|
19
28
|
@customElement("rel-time-picker")
|
|
20
29
|
export class RelTimePicker extends LitElement {
|
|
21
|
-
/** Committed time, 24-hour "HH:mm" (e.g. "09:30", "17:05"). */
|
|
30
|
+
/** Committed time, 24-hour "HH:mm" or "HH:mm:ss" (e.g. "09:30", "17:05:30"). */
|
|
22
31
|
@property({ type: String, reflect: true }) value: string | null = null;
|
|
23
32
|
@property({ type: String }) placeholder = "Select time";
|
|
24
33
|
@property({ type: String }) label: string | null = null;
|
|
@@ -26,10 +35,12 @@ export class RelTimePicker extends LitElement {
|
|
|
26
35
|
@property({ type: Boolean }) required = false;
|
|
27
36
|
@property({ type: Boolean, reflect: true }) readonly = false;
|
|
28
37
|
|
|
29
|
-
/** Display format of the trigger text and hour column. Value stays
|
|
38
|
+
/** Display format of the trigger text and hour column. Value stays 24-hour. */
|
|
30
39
|
@property({ type: String }) format: "12h" | "24h" = "12h";
|
|
40
|
+
/** Shows the seconds column; committed value becomes "HH:mm:ss". */
|
|
41
|
+
@property({ type: Boolean, reflect: true }) seconds = false;
|
|
31
42
|
/** Minute granularity of the minutes column (1, 5, 10, 15, 30, …). */
|
|
32
|
-
@property({ type: Number, attribute: "minute-step" }) minuteStep =
|
|
43
|
+
@property({ type: Number, attribute: "minute-step" }) minuteStep = 5;
|
|
33
44
|
|
|
34
45
|
/** Earliest selectable time, "HH:mm" (inclusive). */
|
|
35
46
|
@property({ type: String, attribute: "min-time" }) minTime: string | null = null;
|
|
@@ -50,11 +61,25 @@ export class RelTimePicker extends LitElement {
|
|
|
50
61
|
@property({ type: String, reflect: true }) variant: "outlined" = "outlined";
|
|
51
62
|
@property({ type: Boolean, reflect: true }) fullWidth = false;
|
|
52
63
|
|
|
53
|
-
// Draft selection (hour is 0–23 internally; committed on
|
|
64
|
+
// Draft selection (hour is 0–23 internally; committed on OK only)
|
|
54
65
|
@state() private draftHour: number | null = null;
|
|
55
66
|
@state() private draftMinute: number | null = null;
|
|
67
|
+
@state() private draftSecond: number | null = null;
|
|
68
|
+
/** Uncommitted typed text; null → the input shows the committed value. */
|
|
69
|
+
@state() private inputText: string | null = null;
|
|
70
|
+
/** Parse/range error message for the typed text (cleared on next input). */
|
|
71
|
+
@state() private inputError: string | null = null;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether the next popover open should move focus into the columns.
|
|
75
|
+
* Icon-click / ArrowDown / external `open` do (previous behavior); opening
|
|
76
|
+
* by clicking into the text input must NOT, or typing would be impossible.
|
|
77
|
+
*/
|
|
78
|
+
private focusPanelOnOpen = true;
|
|
56
79
|
|
|
57
|
-
@query(".trigger-
|
|
80
|
+
@query(".trigger-input") private triggerInput?: HTMLInputElement;
|
|
81
|
+
@query(".trigger-button") private triggerButton?: HTMLElement;
|
|
82
|
+
@query(".popover") private popoverElement?: HTMLElement;
|
|
58
83
|
@query(".columns") private columnsElement?: HTMLElement;
|
|
59
84
|
|
|
60
85
|
static styles = css`
|
|
@@ -118,6 +143,11 @@ export class RelTimePicker extends LitElement {
|
|
|
118
143
|
color: var(--rel-text-danger);
|
|
119
144
|
}
|
|
120
145
|
|
|
146
|
+
/* Persistent live region — out of flow while empty so it adds no gap. */
|
|
147
|
+
.time-picker-support.empty {
|
|
148
|
+
position: absolute;
|
|
149
|
+
}
|
|
150
|
+
|
|
121
151
|
.trigger-button {
|
|
122
152
|
width: 100%;
|
|
123
153
|
min-width: 160px;
|
|
@@ -140,28 +170,48 @@ export class RelTimePicker extends LitElement {
|
|
|
140
170
|
letter-spacing: var(--rel-body-md-regular-letter-spacing);
|
|
141
171
|
}
|
|
142
172
|
|
|
143
|
-
|
|
173
|
+
:host(:not([disabled])) .trigger-button:hover {
|
|
144
174
|
border-color: var(--rel-border-strong);
|
|
145
175
|
}
|
|
146
176
|
|
|
147
|
-
.trigger-button:focus-
|
|
177
|
+
.trigger-button:focus-within {
|
|
148
178
|
outline: 2px solid var(--rel-border-focus);
|
|
149
179
|
outline-offset: 2px;
|
|
150
180
|
border-color: var(--rel-border-focus);
|
|
151
181
|
}
|
|
152
182
|
|
|
153
|
-
.trigger-button
|
|
183
|
+
:host([disabled]) .trigger-button {
|
|
154
184
|
cursor: not-allowed;
|
|
155
185
|
background: var(--rel-bg-surface-elevated);
|
|
156
186
|
}
|
|
157
187
|
|
|
188
|
+
/* The editable field itself — the wrapper carries the box styles. */
|
|
189
|
+
.trigger-input {
|
|
190
|
+
flex: 1;
|
|
191
|
+
min-width: 0;
|
|
192
|
+
border: none;
|
|
193
|
+
background: transparent;
|
|
194
|
+
padding: 0;
|
|
195
|
+
margin: 0;
|
|
196
|
+
outline: none;
|
|
197
|
+
color: var(--rel-text-default);
|
|
198
|
+
font: inherit;
|
|
199
|
+
letter-spacing: inherit;
|
|
200
|
+
cursor: inherit;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.trigger-input::placeholder {
|
|
204
|
+
color: var(--rel-text-placeholder);
|
|
205
|
+
opacity: 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
158
208
|
/* Error state */
|
|
159
209
|
.trigger-button.error,
|
|
160
|
-
.trigger-button.error:hover
|
|
210
|
+
:host(:not([disabled])) .trigger-button.error:hover {
|
|
161
211
|
border-color: var(--rel-border-danger);
|
|
162
212
|
}
|
|
163
213
|
|
|
164
|
-
.trigger-button.error:focus-
|
|
214
|
+
.trigger-button.error:focus-within {
|
|
165
215
|
outline-color: var(--rel-border-danger);
|
|
166
216
|
border-color: var(--rel-border-danger);
|
|
167
217
|
}
|
|
@@ -185,15 +235,6 @@ export class RelTimePicker extends LitElement {
|
|
|
185
235
|
letter-spacing: var(--rel-body-lg-regular-letter-spacing);
|
|
186
236
|
}
|
|
187
237
|
|
|
188
|
-
.trigger-content {
|
|
189
|
-
flex: 1;
|
|
190
|
-
color: var(--rel-text-default);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
.trigger-content.placeholder {
|
|
194
|
-
color: var(--rel-text-placeholder);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
238
|
.trigger-icons {
|
|
198
239
|
display: flex;
|
|
199
240
|
align-items: center;
|
|
@@ -224,16 +265,36 @@ export class RelTimePicker extends LitElement {
|
|
|
224
265
|
height: 16px;
|
|
225
266
|
}
|
|
226
267
|
|
|
268
|
+
/* Panel (Figma "Time Picker"): fixed width per size, full-bleed content. */
|
|
227
269
|
.popover {
|
|
270
|
+
/* Reset native [popover] UA styles first (the panel is promoted to
|
|
271
|
+
the top layer so a drawer/sheet/modal ancestor — transformed,
|
|
272
|
+
overflow:hidden, or an equal-z-index sibling panel — can't clip it
|
|
273
|
+
or paint over it). While in the top layer, JS overrides the
|
|
274
|
+
anchored position below with inline fixed coordinates; the CSS
|
|
275
|
+
anchoring is the no-Popover-API fallback. */
|
|
276
|
+
margin: 0;
|
|
277
|
+
padding: 0;
|
|
278
|
+
height: auto;
|
|
279
|
+
inset: auto;
|
|
280
|
+
color: inherit;
|
|
228
281
|
position: absolute;
|
|
229
282
|
top: calc(100% + 4px);
|
|
230
283
|
left: 0;
|
|
231
|
-
z-index:
|
|
284
|
+
z-index: 2147483647;
|
|
232
285
|
background: var(--rel-bg-surface);
|
|
233
|
-
border: 1px solid var(--rel-border-
|
|
286
|
+
border: 1px solid var(--rel-border-subtle);
|
|
234
287
|
border-radius: var(--rel-border-radius-sm);
|
|
235
288
|
box-shadow: var(--rel-shadows-shadow-300);
|
|
236
|
-
|
|
289
|
+
/* Never wider or taller than the viewport (small screens). border-box
|
|
290
|
+
so the Figma widths and viewport caps include the 1px borders. */
|
|
291
|
+
box-sizing: border-box;
|
|
292
|
+
overflow-x: hidden;
|
|
293
|
+
overflow-y: auto;
|
|
294
|
+
min-width: min(220px, calc(100vw - 16px));
|
|
295
|
+
width: 250px;
|
|
296
|
+
max-width: calc(100vw - 16px);
|
|
297
|
+
max-height: calc(100vh - 16px);
|
|
237
298
|
display: none;
|
|
238
299
|
}
|
|
239
300
|
|
|
@@ -241,94 +302,122 @@ export class RelTimePicker extends LitElement {
|
|
|
241
302
|
display: block;
|
|
242
303
|
}
|
|
243
304
|
|
|
244
|
-
/*
|
|
245
|
-
.
|
|
305
|
+
/* Typed-shortcut hints ("N → Now"); each chip applies its value. */
|
|
306
|
+
.shortcut-row {
|
|
246
307
|
display: flex;
|
|
247
|
-
|
|
248
|
-
gap: var(--rel-spacing-
|
|
249
|
-
padding
|
|
250
|
-
margin-bottom: var(--rel-spacing-sm);
|
|
308
|
+
flex-wrap: wrap;
|
|
309
|
+
gap: var(--rel-spacing-2xs);
|
|
310
|
+
padding: var(--rel-spacing-2xs) var(--rel-spacing-xs);
|
|
251
311
|
border-bottom: 1px solid var(--rel-border-subtle);
|
|
252
312
|
}
|
|
253
313
|
|
|
254
|
-
.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
314
|
+
.shortcut-chip {
|
|
315
|
+
display: inline-flex;
|
|
316
|
+
align-items: center;
|
|
317
|
+
gap: var(--rel-spacing-2xs);
|
|
318
|
+
border: none;
|
|
319
|
+
background: transparent;
|
|
320
|
+
border-radius: var(--rel-border-radius-xs);
|
|
321
|
+
padding: var(--rel-spacing-3xs) var(--rel-spacing-2xs);
|
|
322
|
+
cursor: pointer;
|
|
323
|
+
color: var(--rel-text-default);
|
|
324
|
+
transition: background 0.15s ease;
|
|
325
|
+
font-family: var(--rel-body-xs-regular-font-family);
|
|
326
|
+
font-weight: var(--rel-body-xs-regular-font-weight);
|
|
327
|
+
font-size: var(--rel-body-xs-regular-font-size);
|
|
328
|
+
line-height: var(--rel-body-xs-regular-line-height);
|
|
329
|
+
letter-spacing: var(--rel-body-xs-regular-letter-spacing);
|
|
262
330
|
}
|
|
263
331
|
|
|
264
|
-
.
|
|
265
|
-
|
|
266
|
-
font-family: var(--rel-body-md-semibold-font-family);
|
|
267
|
-
font-weight: var(--rel-body-md-semibold-font-weight);
|
|
268
|
-
font-size: var(--rel-body-md-semibold-font-size);
|
|
269
|
-
line-height: var(--rel-body-md-semibold-line-height);
|
|
270
|
-
letter-spacing: var(--rel-body-md-semibold-letter-spacing);
|
|
332
|
+
.shortcut-chip:hover {
|
|
333
|
+
background: var(--rel-bg-neutral-elevated-1);
|
|
271
334
|
}
|
|
272
335
|
|
|
273
|
-
.
|
|
274
|
-
|
|
336
|
+
.shortcut-chip:focus-visible {
|
|
337
|
+
outline: 2px solid var(--rel-border-focus);
|
|
338
|
+
outline-offset: -2px;
|
|
275
339
|
}
|
|
276
340
|
|
|
277
|
-
.
|
|
278
|
-
|
|
279
|
-
|
|
341
|
+
.shortcut-chip kbd {
|
|
342
|
+
border: 1px solid var(--rel-border-default);
|
|
343
|
+
border-bottom-width: 2px;
|
|
344
|
+
border-radius: var(--rel-border-radius-xs);
|
|
345
|
+
background: var(--rel-bg-surface-elevated);
|
|
346
|
+
padding: 0 var(--rel-spacing-3xs);
|
|
347
|
+
min-width: 18px;
|
|
348
|
+
text-align: center;
|
|
349
|
+
font-family: inherit;
|
|
350
|
+
font-size: inherit;
|
|
351
|
+
color: var(--rel-text-label);
|
|
280
352
|
}
|
|
281
353
|
|
|
282
|
-
/*
|
|
283
|
-
|
|
284
|
-
.column {
|
|
354
|
+
/* Full-bleed column row with 1px dividers (Figma "Columns"). */
|
|
355
|
+
.columns {
|
|
285
356
|
display: flex;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
min-width: 64px;
|
|
357
|
+
align-items: stretch;
|
|
358
|
+
height: 260px;
|
|
289
359
|
}
|
|
290
360
|
|
|
291
|
-
.column-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
text-transform: uppercase;
|
|
296
|
-
font-family: var(--rel-body-xs-semibold-font-family);
|
|
297
|
-
font-weight: var(--rel-body-xs-semibold-font-weight);
|
|
298
|
-
font-size: var(--rel-body-xs-semibold-font-size);
|
|
299
|
-
line-height: var(--rel-body-xs-semibold-line-height);
|
|
300
|
-
letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
|
|
361
|
+
.column-divider {
|
|
362
|
+
flex-shrink: 0;
|
|
363
|
+
width: 1px;
|
|
364
|
+
background: var(--rel-border-subtle);
|
|
301
365
|
}
|
|
302
366
|
|
|
367
|
+
/* Each column scrolls independently. The 4px rail is always reserved
|
|
368
|
+
(scrollbar-gutter) so columns stay aligned whether or not they
|
|
369
|
+
overflow; the thumb only appears on overflow. */
|
|
303
370
|
.column-list {
|
|
371
|
+
flex: 1;
|
|
372
|
+
min-width: 0;
|
|
304
373
|
display: flex;
|
|
305
374
|
flex-direction: column;
|
|
306
|
-
|
|
307
|
-
|
|
375
|
+
/* Positioned so option offsetTop is list-relative — the in-column
|
|
376
|
+
scroll math depends on it (we never scrollIntoView: it scrolls every
|
|
377
|
+
ancestor, page included). */
|
|
378
|
+
position: relative;
|
|
308
379
|
overflow-y: auto;
|
|
309
|
-
padding: var(--rel-spacing-
|
|
380
|
+
padding: var(--rel-spacing-2xs);
|
|
310
381
|
scrollbar-width: thin;
|
|
382
|
+
scrollbar-color: var(--rel-border-default) transparent;
|
|
383
|
+
scrollbar-gutter: stable;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.column-list::-webkit-scrollbar {
|
|
387
|
+
width: 4px;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.column-list::-webkit-scrollbar-track {
|
|
391
|
+
background: transparent;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.column-list::-webkit-scrollbar-thumb {
|
|
395
|
+
background: var(--rel-border-default);
|
|
396
|
+
border-radius: var(--rel-border-radius-full-rounded);
|
|
311
397
|
}
|
|
312
398
|
|
|
399
|
+
/* Option cell (Figma ".Time Option"): selected uses the neutral
|
|
400
|
+
elevated-2 fill + medium weight, NOT the brand color. */
|
|
313
401
|
.option-button {
|
|
314
402
|
border: none;
|
|
315
403
|
background: transparent;
|
|
316
|
-
border-radius: var(--rel-border-radius-
|
|
404
|
+
border-radius: var(--rel-border-radius-xs);
|
|
317
405
|
color: var(--rel-text-default);
|
|
318
406
|
cursor: pointer;
|
|
319
|
-
padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
|
|
320
|
-
text-align: center;
|
|
321
407
|
flex-shrink: 0;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
408
|
+
height: 36px;
|
|
409
|
+
padding: 0 var(--rel-spacing-xs);
|
|
410
|
+
text-align: center;
|
|
411
|
+
transition: background 0.15s ease;
|
|
412
|
+
font-family: var(--rel-body-md-regular-font-family);
|
|
413
|
+
font-weight: var(--rel-body-md-regular-font-weight);
|
|
414
|
+
font-size: var(--rel-body-md-regular-font-size);
|
|
415
|
+
line-height: var(--rel-body-md-regular-line-height);
|
|
416
|
+
letter-spacing: var(--rel-body-md-regular-letter-spacing);
|
|
328
417
|
}
|
|
329
418
|
|
|
330
419
|
.option-button:hover:not(:disabled):not(.selected) {
|
|
331
|
-
background: var(--rel-bg-
|
|
420
|
+
background: var(--rel-bg-neutral-elevated-1);
|
|
332
421
|
}
|
|
333
422
|
|
|
334
423
|
.option-button:focus-visible {
|
|
@@ -338,9 +427,9 @@ export class RelTimePicker extends LitElement {
|
|
|
338
427
|
}
|
|
339
428
|
|
|
340
429
|
.option-button.selected {
|
|
341
|
-
background: var(--rel-
|
|
342
|
-
color: var(--rel-
|
|
343
|
-
font-weight: var(--rel-body-md-
|
|
430
|
+
background: var(--rel-bg-neutral-elevated-2);
|
|
431
|
+
color: var(--rel-text-default);
|
|
432
|
+
font-weight: var(--rel-body-md-medium-font-weight);
|
|
344
433
|
}
|
|
345
434
|
|
|
346
435
|
.option-button:disabled {
|
|
@@ -353,14 +442,15 @@ export class RelTimePicker extends LitElement {
|
|
|
353
442
|
background: transparent;
|
|
354
443
|
}
|
|
355
444
|
|
|
356
|
-
/* Footer (
|
|
445
|
+
/* Footer (Figma "Footer"): 8px padding + 8px extra left so the "Now"
|
|
446
|
+
link aligns with the buttons' inner padding. Buttons are always S. */
|
|
357
447
|
.time-footer {
|
|
358
448
|
display: flex;
|
|
359
449
|
align-items: center;
|
|
360
450
|
justify-content: space-between;
|
|
361
|
-
gap: var(--rel-spacing-
|
|
362
|
-
padding
|
|
363
|
-
|
|
451
|
+
gap: var(--rel-spacing-xs);
|
|
452
|
+
padding: var(--rel-spacing-xs);
|
|
453
|
+
padding-left: calc(var(--rel-spacing-xs) * 2);
|
|
364
454
|
border-top: 1px solid var(--rel-border-subtle);
|
|
365
455
|
}
|
|
366
456
|
|
|
@@ -370,6 +460,56 @@ export class RelTimePicker extends LitElement {
|
|
|
370
460
|
gap: var(--rel-spacing-xs);
|
|
371
461
|
}
|
|
372
462
|
|
|
463
|
+
/* The design narrows OK below the DS small min-width (60 vs 80) so the
|
|
464
|
+
compact footer fits the 220px panel. */
|
|
465
|
+
.footer-right rel-button[variant="primary"] {
|
|
466
|
+
--rel-button-min-width: 60px;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/* Panel size variants (Figma S/M/L): width 220/250/280, option height
|
|
470
|
+
32/36/40, body-sm/md/lg type. Medium is the base above. */
|
|
471
|
+
.time-picker-wrapper.small .popover {
|
|
472
|
+
width: 220px;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.time-picker-wrapper.small .columns {
|
|
476
|
+
height: 232px;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.time-picker-wrapper.small .option-button {
|
|
480
|
+
height: 32px;
|
|
481
|
+
font-family: var(--rel-body-sm-regular-font-family);
|
|
482
|
+
font-weight: var(--rel-body-sm-regular-font-weight);
|
|
483
|
+
font-size: var(--rel-body-sm-regular-font-size);
|
|
484
|
+
line-height: var(--rel-body-sm-regular-line-height);
|
|
485
|
+
letter-spacing: var(--rel-body-sm-regular-letter-spacing);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.time-picker-wrapper.small .option-button.selected {
|
|
489
|
+
font-weight: var(--rel-body-sm-medium-font-weight);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.time-picker-wrapper.large .popover {
|
|
493
|
+
width: 280px;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.time-picker-wrapper.large .columns {
|
|
497
|
+
height: 288px;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.time-picker-wrapper.large .option-button {
|
|
501
|
+
height: 40px;
|
|
502
|
+
font-family: var(--rel-body-lg-regular-font-family);
|
|
503
|
+
font-weight: var(--rel-body-lg-regular-font-weight);
|
|
504
|
+
font-size: var(--rel-body-lg-regular-font-size);
|
|
505
|
+
line-height: var(--rel-body-lg-regular-line-height);
|
|
506
|
+
letter-spacing: var(--rel-body-lg-regular-letter-spacing);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.time-picker-wrapper.large .option-button.selected {
|
|
510
|
+
font-weight: var(--rel-body-lg-medium-font-weight);
|
|
511
|
+
}
|
|
512
|
+
|
|
373
513
|
@media (prefers-reduced-motion: reduce) {
|
|
374
514
|
.trigger-button,
|
|
375
515
|
.icon-button,
|
|
@@ -389,13 +529,17 @@ export class RelTimePicker extends LitElement {
|
|
|
389
529
|
super.disconnectedCallback();
|
|
390
530
|
document.removeEventListener("click", this.handleClickOutside);
|
|
391
531
|
document.removeEventListener("keydown", this.handleEscapeKey);
|
|
532
|
+
window.removeEventListener("resize", this.onReposition);
|
|
533
|
+
window.removeEventListener("scroll", this.onReposition, true);
|
|
534
|
+
this.hideTopLayer();
|
|
392
535
|
}
|
|
393
536
|
|
|
394
537
|
private handleClickOutside = (e: MouseEvent) => {
|
|
395
538
|
if (!this.open) return;
|
|
396
539
|
const path = e.composedPath();
|
|
397
540
|
if (!path.includes(this)) {
|
|
398
|
-
|
|
541
|
+
// Don't refocus the field — the user clicked somewhere else on purpose.
|
|
542
|
+
this.cancelPopover(false);
|
|
399
543
|
}
|
|
400
544
|
};
|
|
401
545
|
|
|
@@ -405,49 +549,163 @@ export class RelTimePicker extends LitElement {
|
|
|
405
549
|
}
|
|
406
550
|
};
|
|
407
551
|
|
|
552
|
+
// `open` is a public reflected property (consumers may set it directly,
|
|
553
|
+
// not only via the trigger), so top-layer promotion syncs here rather
|
|
554
|
+
// than inside toggle/cancel.
|
|
408
555
|
updated(changedProperties: Map<string, any>) {
|
|
409
|
-
if (changedProperties.has("open")
|
|
556
|
+
if (changedProperties.has("open")) {
|
|
557
|
+
if (!this.open) {
|
|
558
|
+
window.removeEventListener("resize", this.onReposition);
|
|
559
|
+
window.removeEventListener("scroll", this.onReposition, true);
|
|
560
|
+
this.hideTopLayer();
|
|
561
|
+
this.focusPanelOnOpen = true;
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
410
564
|
this.resetDraftFromValue();
|
|
411
|
-
|
|
412
|
-
|
|
565
|
+
this.showTopLayer();
|
|
566
|
+
window.addEventListener("resize", this.onReposition);
|
|
567
|
+
window.addEventListener("scroll", this.onReposition, true);
|
|
568
|
+
// Once the popover has rendered: center each column's selection, and —
|
|
569
|
+
// unless the open came from clicking into the text input (typing must
|
|
570
|
+
// keep working) — focus the selected hour without scrolling the page.
|
|
571
|
+
const focusPanel = this.focusPanelOnOpen;
|
|
572
|
+
this.focusPanelOnOpen = true;
|
|
413
573
|
setTimeout(() => {
|
|
414
574
|
this.scrollColumnsToSelection();
|
|
415
|
-
|
|
416
|
-
this.columnsElement?.querySelector<HTMLButtonElement>(
|
|
417
|
-
".option-button.selected:not(:disabled)"
|
|
418
|
-
) ??
|
|
419
|
-
this.columnsElement?.querySelector<HTMLButtonElement>(
|
|
420
|
-
".option-button:not(:disabled)"
|
|
421
|
-
);
|
|
422
|
-
target?.focus();
|
|
575
|
+
if (focusPanel) this.focusPanelSelection();
|
|
423
576
|
}, 0);
|
|
424
577
|
}
|
|
425
578
|
}
|
|
426
579
|
|
|
580
|
+
/* A drawer/sheet/modal ancestor breaks the anchored panel two ways: a
|
|
581
|
+
* transform makes it the containing block for position:fixed descendants
|
|
582
|
+
* (and overflow:hidden clips absolute ones), and an equal-z-index panel
|
|
583
|
+
* later in the DOM paints over the columns. Promoting the panel to the
|
|
584
|
+
* browser top layer sidesteps both — same fix as rel-popover /
|
|
585
|
+
* rel-autocomplete / rel-select / rel-range-picker / rel-date-picker.
|
|
586
|
+
* `manual` keeps outside-click and Escape handling under this component's
|
|
587
|
+
* own control. While in the top layer the CSS anchoring is meaningless, so
|
|
588
|
+
* the panel gets inline fixed coordinates from updatePopoverPosition()
|
|
589
|
+
* (which also replaces the old .open-up CSS flip). */
|
|
590
|
+
|
|
591
|
+
private showTopLayer() {
|
|
592
|
+
const el = this.popoverElement;
|
|
593
|
+
if (el && typeof el.showPopover === "function") {
|
|
594
|
+
try {
|
|
595
|
+
if (!el.matches(":popover-open")) el.showPopover();
|
|
596
|
+
} catch {
|
|
597
|
+
/* older engines / detached element — CSS fallback applies */
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
requestAnimationFrame(() => this.updatePopoverPosition());
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private hideTopLayer() {
|
|
604
|
+
const el = this.popoverElement;
|
|
605
|
+
if (!el) return;
|
|
606
|
+
if (typeof el.hidePopover === "function") {
|
|
607
|
+
try {
|
|
608
|
+
if (el.matches(":popover-open")) el.hidePopover();
|
|
609
|
+
} catch {
|
|
610
|
+
/* already hidden or unsupported — nothing to do */
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
el.style.position = "";
|
|
614
|
+
el.style.top = "";
|
|
615
|
+
el.style.left = "";
|
|
616
|
+
el.style.maxHeight = "";
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private onReposition = () => {
|
|
620
|
+
if (this.open) this.updatePopoverPosition();
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
private updatePopoverPosition = () => {
|
|
624
|
+
const panel = this.popoverElement;
|
|
625
|
+
const anchor = this.triggerButton;
|
|
626
|
+
if (!panel || !anchor || !this.open) return;
|
|
627
|
+
|
|
628
|
+
const t = anchor.getBoundingClientRect();
|
|
629
|
+
// Measure the natural height, clearing any cap from a previous pass.
|
|
630
|
+
panel.style.maxHeight = "";
|
|
631
|
+
const c = panel.getBoundingClientRect();
|
|
632
|
+
|
|
633
|
+
// Below the trigger; flip above when there's no room below. Either side
|
|
634
|
+
// keeps the same 4px gap to the field (above, the panel may cover the
|
|
635
|
+
// label) and the panel shrinks to the available space — columns scroll —
|
|
636
|
+
// rather than sliding over the trigger. 8px viewport margin on every edge.
|
|
637
|
+
const spaceBelow = Math.max(0, window.innerHeight - 8 - (t.bottom + 4));
|
|
638
|
+
const spaceAbove = Math.max(0, t.top - 4 - 8);
|
|
639
|
+
let top: number;
|
|
640
|
+
if (c.height <= spaceBelow || spaceBelow >= spaceAbove) {
|
|
641
|
+
panel.style.maxHeight = `${Math.min(c.height, spaceBelow)}px`;
|
|
642
|
+
top = t.bottom + 4;
|
|
643
|
+
} else {
|
|
644
|
+
const height = Math.min(c.height, spaceAbove);
|
|
645
|
+
panel.style.maxHeight = `${height}px`;
|
|
646
|
+
top = t.top - 4 - height;
|
|
647
|
+
}
|
|
648
|
+
let left = t.left;
|
|
649
|
+
if (left + c.width > window.innerWidth - 8) {
|
|
650
|
+
left = window.innerWidth - c.width - 8;
|
|
651
|
+
}
|
|
652
|
+
left = Math.max(8, left);
|
|
653
|
+
|
|
654
|
+
// Without top-layer support a transformed ancestor may be the containing
|
|
655
|
+
// block for position:fixed, shifting the panel away from the viewport
|
|
656
|
+
// coordinates computed above. Apply, re-measure, and cancel the offset.
|
|
657
|
+
panel.style.position = "fixed";
|
|
658
|
+
panel.style.top = `${top}px`;
|
|
659
|
+
panel.style.left = `${left}px`;
|
|
660
|
+
const applied = panel.getBoundingClientRect();
|
|
661
|
+
top -= applied.top - top;
|
|
662
|
+
left -= applied.left - left;
|
|
663
|
+
panel.style.top = `${top}px`;
|
|
664
|
+
panel.style.left = `${left}px`;
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
/** Focus the selected (or first enabled) option in the columns. */
|
|
668
|
+
private focusPanelSelection() {
|
|
669
|
+
const target =
|
|
670
|
+
this.columnsElement?.querySelector<HTMLButtonElement>(
|
|
671
|
+
".option-button.selected:not(:disabled)"
|
|
672
|
+
) ??
|
|
673
|
+
this.columnsElement?.querySelector<HTMLButtonElement>(
|
|
674
|
+
".option-button:not(:disabled)"
|
|
675
|
+
);
|
|
676
|
+
target?.focus({ preventScroll: true });
|
|
677
|
+
}
|
|
678
|
+
|
|
427
679
|
// ── Parsing / formatting ──
|
|
428
680
|
|
|
429
|
-
private parseTime(
|
|
681
|
+
private parseTime(
|
|
682
|
+
time: string | null
|
|
683
|
+
): { hour: number; minute: number; second: number } | null {
|
|
430
684
|
if (!time) return null;
|
|
431
|
-
const match = time.match(/^(\d{1,2}):(\d{2})
|
|
685
|
+
const match = time.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
|
|
432
686
|
if (!match) return null;
|
|
433
687
|
const hour = parseInt(match[1], 10);
|
|
434
688
|
const minute = parseInt(match[2], 10);
|
|
435
|
-
|
|
436
|
-
|
|
689
|
+
const second = match[3] ? parseInt(match[3], 10) : 0;
|
|
690
|
+
if (hour > 23 || minute > 59 || second > 59) return null;
|
|
691
|
+
return { hour, minute, second };
|
|
437
692
|
}
|
|
438
693
|
|
|
439
|
-
private formatValue(hour: number, minute: number): string {
|
|
440
|
-
|
|
694
|
+
private formatValue(hour: number, minute: number, second = 0): string {
|
|
695
|
+
const hh = String(hour).padStart(2, "0");
|
|
696
|
+
const mm = String(minute).padStart(2, "0");
|
|
697
|
+
return this.seconds ? `${hh}:${mm}:${String(second).padStart(2, "0")}` : `${hh}:${mm}`;
|
|
441
698
|
}
|
|
442
699
|
|
|
443
|
-
private formatDisplay(hour: number, minute: number): string {
|
|
700
|
+
private formatDisplay(hour: number, minute: number, second = 0): string {
|
|
444
701
|
const mm = String(minute).padStart(2, "0");
|
|
702
|
+
const ss = this.seconds ? `:${String(second).padStart(2, "0")}` : "";
|
|
445
703
|
if (this.format === "24h") {
|
|
446
|
-
return `${String(hour).padStart(2, "0")}:${mm}`;
|
|
704
|
+
return `${String(hour).padStart(2, "0")}:${mm}${ss}`;
|
|
447
705
|
}
|
|
448
706
|
const period = hour < 12 ? "AM" : "PM";
|
|
449
707
|
const hour12 = hour % 12 === 0 ? 12 : hour % 12;
|
|
450
|
-
return `${hour12}:${mm} ${period}`;
|
|
708
|
+
return `${hour12}:${mm}${ss} ${period}`;
|
|
451
709
|
}
|
|
452
710
|
|
|
453
711
|
private toMinutes(hour: number, minute: number): number {
|
|
@@ -504,13 +762,22 @@ export class RelTimePicker extends LitElement {
|
|
|
504
762
|
hour24: h,
|
|
505
763
|
}));
|
|
506
764
|
}
|
|
507
|
-
// 12-hour clock,
|
|
765
|
+
// 12-hour clock, 01 … 12 as in the Figma "Hour 12" column ("12" maps to
|
|
766
|
+
// 00:xx AM / 12:xx PM).
|
|
508
767
|
const period = this.draftPeriod ?? "AM";
|
|
509
768
|
const base = period === "AM" ? 0 : 12;
|
|
510
|
-
return Array.from({ length: 12 }, (_, i) =>
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
769
|
+
return Array.from({ length: 12 }, (_, i) => {
|
|
770
|
+
const display = i + 1;
|
|
771
|
+
return {
|
|
772
|
+
label: String(display).padStart(2, "0"),
|
|
773
|
+
hour24: base + (display % 12),
|
|
774
|
+
};
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** Seconds shown in the seconds column (always step 1). */
|
|
779
|
+
private getSecondOptions(): number[] {
|
|
780
|
+
return Array.from({ length: 60 }, (_, s) => s);
|
|
514
781
|
}
|
|
515
782
|
|
|
516
783
|
private get draftPeriod(): "AM" | "PM" | null {
|
|
@@ -522,6 +789,7 @@ export class RelTimePicker extends LitElement {
|
|
|
522
789
|
const parsed = this.parseTime(this.value);
|
|
523
790
|
this.draftHour = parsed ? parsed.hour : null;
|
|
524
791
|
this.draftMinute = parsed ? parsed.minute : null;
|
|
792
|
+
this.draftSecond = parsed ? parsed.second : null;
|
|
525
793
|
}
|
|
526
794
|
|
|
527
795
|
/** Snap the draft minute to the nearest valid step minute for the hour. */
|
|
@@ -555,9 +823,9 @@ export class RelTimePicker extends LitElement {
|
|
|
555
823
|
}
|
|
556
824
|
|
|
557
825
|
/** Close discarding the draft (Cancel / Escape / outside click). */
|
|
558
|
-
private cancelPopover() {
|
|
826
|
+
private cancelPopover(refocus = true) {
|
|
559
827
|
this.open = false;
|
|
560
|
-
this.
|
|
828
|
+
if (refocus) this.triggerInput?.focus();
|
|
561
829
|
this.dispatchEvent(
|
|
562
830
|
new CustomEvent("rel-open-change", {
|
|
563
831
|
detail: { open: false },
|
|
@@ -569,8 +837,20 @@ export class RelTimePicker extends LitElement {
|
|
|
569
837
|
|
|
570
838
|
private applySelection() {
|
|
571
839
|
if (this.draftHour === null) return;
|
|
572
|
-
const
|
|
840
|
+
const hour = this.draftHour;
|
|
841
|
+
const minute = this.draftMinute ?? 0;
|
|
842
|
+
const second = this.draftSecond ?? 0;
|
|
843
|
+
this.commitTime(hour, minute, second);
|
|
844
|
+
announce(`${this.formatDisplay(hour, minute, second)} selected`);
|
|
845
|
+
this.cancelPopover();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** Commit a time: set value, reset typed text/error, fire rel-change. */
|
|
849
|
+
private commitTime(hour: number, minute: number, second: number) {
|
|
850
|
+
const newValue = this.formatValue(hour, minute, second);
|
|
573
851
|
this.value = newValue;
|
|
852
|
+
this.inputText = null;
|
|
853
|
+
this.inputError = null;
|
|
574
854
|
this.dispatchEvent(
|
|
575
855
|
new CustomEvent("rel-change", {
|
|
576
856
|
detail: { value: newValue },
|
|
@@ -578,7 +858,6 @@ export class RelTimePicker extends LitElement {
|
|
|
578
858
|
composed: true,
|
|
579
859
|
})
|
|
580
860
|
);
|
|
581
|
-
this.cancelPopover();
|
|
582
861
|
}
|
|
583
862
|
|
|
584
863
|
/** Set the draft to the current time, snapped to the minute step and range. */
|
|
@@ -586,6 +865,7 @@ export class RelTimePicker extends LitElement {
|
|
|
586
865
|
const now = new Date();
|
|
587
866
|
const step = this.minuteStepSafe;
|
|
588
867
|
this.draftHour = now.getHours();
|
|
868
|
+
this.draftSecond = this.seconds ? now.getSeconds() : null;
|
|
589
869
|
this.draftMinute = Math.round(now.getMinutes() / step) * step;
|
|
590
870
|
if (this.draftMinute >= 60) {
|
|
591
871
|
this.draftMinute = 0;
|
|
@@ -610,6 +890,9 @@ export class RelTimePicker extends LitElement {
|
|
|
610
890
|
this.value = null;
|
|
611
891
|
this.draftHour = null;
|
|
612
892
|
this.draftMinute = null;
|
|
893
|
+
this.draftSecond = null;
|
|
894
|
+
this.inputText = null;
|
|
895
|
+
this.inputError = null;
|
|
613
896
|
this.dispatchEvent(
|
|
614
897
|
new CustomEvent("rel-change", {
|
|
615
898
|
detail: { value: null },
|
|
@@ -619,6 +902,73 @@ export class RelTimePicker extends LitElement {
|
|
|
619
902
|
);
|
|
620
903
|
}
|
|
621
904
|
|
|
905
|
+
// ── Typed keyboard entry ──
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Parse and commit the typed text (Enter / blur). Invalid or out-of-range
|
|
909
|
+
* text keeps the field in an error state with the text preserved for
|
|
910
|
+
* correction; the committed value is untouched (Escape reverts the text).
|
|
911
|
+
*/
|
|
912
|
+
private commitTypedValue() {
|
|
913
|
+
if (this.inputText === null) return; // nothing typed since last commit
|
|
914
|
+
const text = this.inputText.trim();
|
|
915
|
+
|
|
916
|
+
if (!text) {
|
|
917
|
+
// Field cleared by keyboard — behaves like the clear button.
|
|
918
|
+
this.inputText = null;
|
|
919
|
+
this.inputError = null;
|
|
920
|
+
if (this.value !== null) {
|
|
921
|
+
this.value = null;
|
|
922
|
+
this.dispatchEvent(
|
|
923
|
+
new CustomEvent("rel-change", {
|
|
924
|
+
detail: { value: null },
|
|
925
|
+
bubbles: true,
|
|
926
|
+
composed: true,
|
|
927
|
+
})
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
if (this.open) this.cancelPopover();
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const parsed = parseTimeInput(text);
|
|
935
|
+
if (!parsed) {
|
|
936
|
+
this.inputError = "Invalid time";
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (parsed.kind === "now") {
|
|
941
|
+
this.applyNowShortcut();
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (!this.isTimeInRange(parsed.hour, parsed.minute)) {
|
|
946
|
+
this.inputError = "Time out of range";
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Typed entry accepts exact minutes/seconds (not limited to minuteStep).
|
|
951
|
+
this.commitTime(parsed.hour, parsed.minute, this.seconds ? parsed.second : 0);
|
|
952
|
+
if (this.open) this.cancelPopover();
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/** "Now", committed directly (N shortcut / popover chip), clamped to range. */
|
|
956
|
+
private applyNowShortcut() {
|
|
957
|
+
const now = new Date();
|
|
958
|
+
let hour = now.getHours();
|
|
959
|
+
let minute = now.getMinutes();
|
|
960
|
+
const total = this.toMinutes(hour, minute);
|
|
961
|
+
if (total < this.minBoundMinutes) {
|
|
962
|
+
hour = Math.floor(this.minBoundMinutes / 60);
|
|
963
|
+
minute = this.minBoundMinutes % 60;
|
|
964
|
+
} else if (total > this.maxBoundMinutes) {
|
|
965
|
+
hour = Math.floor(this.maxBoundMinutes / 60);
|
|
966
|
+
minute = this.maxBoundMinutes % 60;
|
|
967
|
+
}
|
|
968
|
+
this.commitTime(hour, minute, this.seconds ? now.getSeconds() : 0);
|
|
969
|
+
if (this.open) this.cancelPopover();
|
|
970
|
+
}
|
|
971
|
+
|
|
622
972
|
// ── Column selection ──
|
|
623
973
|
|
|
624
974
|
private selectHour(hour24: number) {
|
|
@@ -643,6 +993,22 @@ export class RelTimePicker extends LitElement {
|
|
|
643
993
|
this.draftMinute = minute;
|
|
644
994
|
}
|
|
645
995
|
|
|
996
|
+
private selectSecond(second: number) {
|
|
997
|
+
if (this.draftHour === null) {
|
|
998
|
+
// Second picked first — default the hour to the first enabled one.
|
|
999
|
+
for (let h = 0; h < 24; h++) {
|
|
1000
|
+
if (this.isHourEnabled(h)) {
|
|
1001
|
+
this.draftHour = h;
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (this.draftHour === null) return;
|
|
1006
|
+
}
|
|
1007
|
+
if (this.draftMinute === null) this.draftMinute = 0;
|
|
1008
|
+
this.draftSecond = second;
|
|
1009
|
+
this.clampDraftToRange();
|
|
1010
|
+
}
|
|
1011
|
+
|
|
646
1012
|
private selectPeriod(period: "AM" | "PM") {
|
|
647
1013
|
if (!this.isPeriodEnabled(period)) return;
|
|
648
1014
|
const base = period === "AM" ? 0 : 12;
|
|
@@ -663,25 +1029,111 @@ export class RelTimePicker extends LitElement {
|
|
|
663
1029
|
this.scrollColumnsToSelection();
|
|
664
1030
|
}
|
|
665
1031
|
|
|
1032
|
+
/**
|
|
1033
|
+
* Scrolls the column's own scroll container only. scrollIntoView is off
|
|
1034
|
+
* limits here: it walks ALL scrollable ancestors, so it yanked the page
|
|
1035
|
+
* up/down whenever a column recentred (user-reported jump bug).
|
|
1036
|
+
*/
|
|
1037
|
+
private scrollOptionInColumn(btn: HTMLElement, block: "center" | "nearest") {
|
|
1038
|
+
const list = btn.parentElement;
|
|
1039
|
+
if (!list) return;
|
|
1040
|
+
const top = btn.offsetTop;
|
|
1041
|
+
const height = btn.offsetHeight;
|
|
1042
|
+
if (block === "center") {
|
|
1043
|
+
list.scrollTop = top - (list.clientHeight - height) / 2;
|
|
1044
|
+
} else if (top < list.scrollTop) {
|
|
1045
|
+
list.scrollTop = top;
|
|
1046
|
+
} else if (top + height > list.scrollTop + list.clientHeight) {
|
|
1047
|
+
list.scrollTop = top + height - list.clientHeight;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
666
1051
|
private scrollColumnsToSelection() {
|
|
667
1052
|
this.updateComplete.then(() => {
|
|
668
1053
|
this.columnsElement
|
|
669
1054
|
?.querySelectorAll<HTMLButtonElement>(".option-button.selected")
|
|
670
|
-
.forEach((btn) =>
|
|
1055
|
+
.forEach((btn) => this.scrollOptionInColumn(btn, "center"));
|
|
671
1056
|
});
|
|
672
1057
|
}
|
|
673
1058
|
|
|
674
1059
|
// ── Keyboard ──
|
|
675
1060
|
|
|
676
|
-
private
|
|
677
|
-
|
|
1061
|
+
private handleInputInput(e: Event) {
|
|
1062
|
+
this.inputText = (e.target as HTMLInputElement).value;
|
|
1063
|
+
if (this.inputError) this.inputError = null;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
private handleInputKeydown(e: KeyboardEvent) {
|
|
1067
|
+
if (e.key === "Enter") {
|
|
1068
|
+
e.preventDefault();
|
|
1069
|
+
if (this.inputText === null) {
|
|
1070
|
+
// Nothing typed — Enter toggles the popover (previous trigger behavior).
|
|
1071
|
+
if (!this.open) {
|
|
1072
|
+
this.toggleTimePopover();
|
|
1073
|
+
} else {
|
|
1074
|
+
this.applySelection();
|
|
1075
|
+
}
|
|
1076
|
+
} else {
|
|
1077
|
+
this.commitTypedValue();
|
|
1078
|
+
}
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (e.key === "ArrowDown") {
|
|
678
1082
|
e.preventDefault();
|
|
679
1083
|
if (!this.open) {
|
|
680
1084
|
this.toggleTimePopover();
|
|
1085
|
+
} else {
|
|
1086
|
+
this.focusPanelSelection();
|
|
681
1087
|
}
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (e.key === "Escape" && this.inputText !== null) {
|
|
1091
|
+
// Revert the typed text; the document listener closes the popover.
|
|
1092
|
+
this.inputText = null;
|
|
1093
|
+
this.inputError = null;
|
|
682
1094
|
}
|
|
683
1095
|
}
|
|
684
1096
|
|
|
1097
|
+
/** Clicking anywhere on the field opens the popover, keeping focus typed-in. */
|
|
1098
|
+
private handleFieldClick() {
|
|
1099
|
+
if (this.open || this.disabled || this.readonly) return;
|
|
1100
|
+
this.focusPanelOnOpen = false;
|
|
1101
|
+
this.toggleTimePopover();
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/** The clock icon toggles the popover with the previous focus behavior. */
|
|
1105
|
+
private handleIconClick(e: Event) {
|
|
1106
|
+
e.stopPropagation();
|
|
1107
|
+
if (this.open) {
|
|
1108
|
+
this.cancelPopover();
|
|
1109
|
+
} else {
|
|
1110
|
+
this.toggleTimePopover();
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/** Commit typed text when focus leaves the component entirely. */
|
|
1115
|
+
private handleInputBlur = (e: FocusEvent) => {
|
|
1116
|
+
this.dispatchEvent(new CustomEvent("rel-blur", { bubbles: true, composed: true }));
|
|
1117
|
+
const related = e.relatedTarget as Node | null;
|
|
1118
|
+
if (related && (this.renderRoot.contains(related) || this.contains(related))) {
|
|
1119
|
+
return; // focus moved into the panel/icons — their own handlers commit
|
|
1120
|
+
}
|
|
1121
|
+
this.commitTypedValue();
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
/** N inside the panel (columns/footer focus) applies Now directly. */
|
|
1125
|
+
private handlePanelKeydown = (e: KeyboardEvent) => {
|
|
1126
|
+
if (
|
|
1127
|
+
(e.key === "n" || e.key === "N") &&
|
|
1128
|
+
!e.ctrlKey &&
|
|
1129
|
+
!e.metaKey &&
|
|
1130
|
+
!e.altKey
|
|
1131
|
+
) {
|
|
1132
|
+
e.preventDefault();
|
|
1133
|
+
this.applyNowShortcut();
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
|
|
685
1137
|
/**
|
|
686
1138
|
* Column navigation: Up/Down move within the column (and select), Left/Right
|
|
687
1139
|
* jump between columns, Home/End go to the first/last option, Enter applies.
|
|
@@ -729,14 +1181,15 @@ export class RelTimePicker extends LitElement {
|
|
|
729
1181
|
|
|
730
1182
|
if (target && target !== buttons[index]) {
|
|
731
1183
|
target.click();
|
|
732
|
-
target.focus();
|
|
733
|
-
|
|
1184
|
+
target.focus({ preventScroll: true });
|
|
1185
|
+
this.scrollOptionInColumn(target, "nearest");
|
|
734
1186
|
}
|
|
735
1187
|
}
|
|
736
1188
|
|
|
737
1189
|
private focusAdjacentColumn(from: TimeColumn, direction: 1 | -1) {
|
|
738
|
-
const order: TimeColumn[] =
|
|
739
|
-
|
|
1190
|
+
const order: TimeColumn[] = ["hour", "minute"];
|
|
1191
|
+
if (this.seconds) order.push("second");
|
|
1192
|
+
if (this.format === "12h") order.push("period");
|
|
740
1193
|
const nextIndex = order.indexOf(from) + direction;
|
|
741
1194
|
if (nextIndex < 0 || nextIndex >= order.length) return;
|
|
742
1195
|
const lists = this.columnsElement?.querySelectorAll(".column-list");
|
|
@@ -744,26 +1197,27 @@ export class RelTimePicker extends LitElement {
|
|
|
744
1197
|
const target =
|
|
745
1198
|
list?.querySelector<HTMLButtonElement>(".option-button.selected:not(:disabled)") ??
|
|
746
1199
|
list?.querySelector<HTMLButtonElement>(".option-button:not(:disabled)");
|
|
747
|
-
target
|
|
748
|
-
|
|
1200
|
+
if (target) {
|
|
1201
|
+
target.focus({ preventScroll: true });
|
|
1202
|
+
this.scrollOptionInColumn(target, "nearest");
|
|
1203
|
+
}
|
|
749
1204
|
}
|
|
750
1205
|
|
|
751
1206
|
// ── Render ──
|
|
752
1207
|
|
|
753
1208
|
render() {
|
|
754
|
-
const isError = this.error || this.invalid;
|
|
755
|
-
const belowText = isError
|
|
1209
|
+
const isError = this.error || this.invalid || this.inputError !== null;
|
|
1210
|
+
const belowText = isError
|
|
1211
|
+
? this.errorMessage ?? this.inputError ?? this.helperText
|
|
1212
|
+
: this.helperText;
|
|
756
1213
|
const describedById = belowText ? "time-picker-desc" : undefined;
|
|
757
1214
|
const parsedValue = this.parseTime(this.value);
|
|
758
1215
|
const displayText = parsedValue
|
|
759
|
-
? this.formatDisplay(parsedValue.hour, parsedValue.minute)
|
|
1216
|
+
? this.formatDisplay(parsedValue.hour, parsedValue.minute, parsedValue.second)
|
|
760
1217
|
: "";
|
|
761
|
-
const draftDisplay =
|
|
762
|
-
this.draftHour !== null
|
|
763
|
-
? this.formatDisplay(this.draftHour, this.draftMinute ?? 0)
|
|
764
|
-
: "";
|
|
765
1218
|
const hourOptions = this.getHourOptions();
|
|
766
1219
|
const minuteOptions = this.getMinuteOptions();
|
|
1220
|
+
const secondOptions = this.seconds ? this.getSecondOptions() : [];
|
|
767
1221
|
const draftPeriod = this.draftPeriod;
|
|
768
1222
|
const wrapperClasses = [
|
|
769
1223
|
"time-picker-wrapper",
|
|
@@ -788,33 +1242,39 @@ export class RelTimePicker extends LitElement {
|
|
|
788
1242
|
? html`<input type="hidden" name=${this.name} .value=${this.value ?? ""} />`
|
|
789
1243
|
: nothing}
|
|
790
1244
|
|
|
791
|
-
<
|
|
1245
|
+
<div
|
|
792
1246
|
part="trigger"
|
|
793
1247
|
class="trigger-button ${isError ? "error" : ""}"
|
|
794
|
-
|
|
795
|
-
?disabled=${this.disabled}
|
|
796
|
-
@click=${this.toggleTimePopover}
|
|
797
|
-
@keydown=${this.handleTriggerKeydown}
|
|
798
|
-
@focus=${() =>
|
|
799
|
-
this.dispatchEvent(
|
|
800
|
-
new CustomEvent("rel-focus", { bubbles: true, composed: true })
|
|
801
|
-
)}
|
|
802
|
-
@blur=${() =>
|
|
803
|
-
this.dispatchEvent(
|
|
804
|
-
new CustomEvent("rel-blur", { bubbles: true, composed: true })
|
|
805
|
-
)}
|
|
806
|
-
aria-label=${this.label || "Select time"}
|
|
807
|
-
aria-haspopup="dialog"
|
|
808
|
-
aria-expanded=${this.open}
|
|
809
|
-
aria-required=${this.required}
|
|
810
|
-
aria-invalid=${isError}
|
|
811
|
-
aria-readonly=${this.readonly}
|
|
812
|
-
aria-describedby=${describedById ?? nothing}
|
|
813
|
-
aria-labelledby=${this.label ? "time-picker-label" : nothing}
|
|
1248
|
+
@click=${this.handleFieldClick}
|
|
814
1249
|
>
|
|
815
|
-
<
|
|
816
|
-
|
|
817
|
-
|
|
1250
|
+
<input
|
|
1251
|
+
part="value"
|
|
1252
|
+
class="trigger-input"
|
|
1253
|
+
type="text"
|
|
1254
|
+
.value=${live(this.inputText ?? displayText)}
|
|
1255
|
+
placeholder=${this.placeholder}
|
|
1256
|
+
?disabled=${this.disabled}
|
|
1257
|
+
?readonly=${this.readonly}
|
|
1258
|
+
autocomplete="off"
|
|
1259
|
+
spellcheck="false"
|
|
1260
|
+
enterkeyhint="done"
|
|
1261
|
+
@input=${this.handleInputInput}
|
|
1262
|
+
@keydown=${this.handleInputKeydown}
|
|
1263
|
+
@focus=${() =>
|
|
1264
|
+
this.dispatchEvent(
|
|
1265
|
+
new CustomEvent("rel-focus", { bubbles: true, composed: true })
|
|
1266
|
+
)}
|
|
1267
|
+
@blur=${this.handleInputBlur}
|
|
1268
|
+
role="combobox"
|
|
1269
|
+
aria-label=${this.label || "Select time"}
|
|
1270
|
+
aria-haspopup="dialog"
|
|
1271
|
+
aria-expanded=${this.open}
|
|
1272
|
+
aria-controls="time-picker-panel"
|
|
1273
|
+
aria-required=${this.required}
|
|
1274
|
+
aria-invalid=${isError}
|
|
1275
|
+
aria-describedby=${describedById ?? nothing}
|
|
1276
|
+
aria-labelledby=${this.label ? "time-picker-label" : nothing}
|
|
1277
|
+
/>
|
|
818
1278
|
<span class="trigger-icons">
|
|
819
1279
|
${this.clearable && this.value
|
|
820
1280
|
? html`
|
|
@@ -835,126 +1295,158 @@ export class RelTimePicker extends LitElement {
|
|
|
835
1295
|
</button>
|
|
836
1296
|
`
|
|
837
1297
|
: nothing}
|
|
838
|
-
<
|
|
1298
|
+
<button
|
|
1299
|
+
class="icon-button"
|
|
1300
|
+
type="button"
|
|
1301
|
+
aria-label="Open time panel"
|
|
1302
|
+
aria-expanded=${this.open}
|
|
1303
|
+
?disabled=${this.disabled}
|
|
1304
|
+
@click=${this.handleIconClick}
|
|
1305
|
+
>
|
|
1306
|
+
<rel-icon part="icon" name="time-filled" size="sm"></rel-icon>
|
|
1307
|
+
</button>
|
|
839
1308
|
</span>
|
|
840
|
-
</
|
|
1309
|
+
</div>
|
|
841
1310
|
|
|
842
1311
|
<div
|
|
843
1312
|
part="panel"
|
|
1313
|
+
id="time-picker-panel"
|
|
844
1314
|
class="popover ${this.open ? "open" : ""}"
|
|
1315
|
+
popover="manual"
|
|
845
1316
|
role="dialog"
|
|
846
1317
|
aria-label="Choose time"
|
|
1318
|
+
@keydown=${this.handlePanelKeydown}
|
|
847
1319
|
>
|
|
848
|
-
<div class="
|
|
849
|
-
<
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1320
|
+
<div part="shortcuts" class="shortcut-row">
|
|
1321
|
+
<button
|
|
1322
|
+
class="shortcut-chip"
|
|
1323
|
+
type="button"
|
|
1324
|
+
title="Type N in the field for the current time"
|
|
1325
|
+
@click=${() => this.applyNowShortcut()}
|
|
1326
|
+
>
|
|
1327
|
+
<kbd aria-hidden="true">N</kbd><span>Now</span>
|
|
1328
|
+
</button>
|
|
853
1329
|
</div>
|
|
854
|
-
|
|
855
1330
|
<div class="columns">
|
|
856
|
-
<div class="column">
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
`
|
|
880
|
-
)}
|
|
881
|
-
</div>
|
|
1331
|
+
<div class="column-list" role="listbox" aria-label="Hours">
|
|
1332
|
+
${hourOptions.map(
|
|
1333
|
+
(opt, i) => html`
|
|
1334
|
+
<button
|
|
1335
|
+
part="option"
|
|
1336
|
+
class="option-button ${this.draftHour === opt.hour24
|
|
1337
|
+
? "selected"
|
|
1338
|
+
: ""}"
|
|
1339
|
+
type="button"
|
|
1340
|
+
role="option"
|
|
1341
|
+
aria-selected=${this.draftHour === opt.hour24}
|
|
1342
|
+
?disabled=${!this.isHourEnabled(opt.hour24)}
|
|
1343
|
+
@click=${() => this.selectHour(opt.hour24)}
|
|
1344
|
+
@keydown=${(e: KeyboardEvent) => this.handleOptionKeydown(e, "hour")}
|
|
1345
|
+
tabindex=${this.draftHour === opt.hour24 ||
|
|
1346
|
+
(this.draftHour === null && i === 0)
|
|
1347
|
+
? 0
|
|
1348
|
+
: -1}
|
|
1349
|
+
>
|
|
1350
|
+
${opt.label}
|
|
1351
|
+
</button>
|
|
1352
|
+
`
|
|
1353
|
+
)}
|
|
882
1354
|
</div>
|
|
883
1355
|
|
|
884
|
-
<div class="column">
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
</div>
|
|
1356
|
+
<div class="column-divider" aria-hidden="true"></div>
|
|
1357
|
+
|
|
1358
|
+
<div class="column-list" role="listbox" aria-label="Minutes">
|
|
1359
|
+
${minuteOptions.map(
|
|
1360
|
+
(m, i) => html`
|
|
1361
|
+
<button
|
|
1362
|
+
part="option"
|
|
1363
|
+
class="option-button ${this.draftMinute === m ? "selected" : ""}"
|
|
1364
|
+
type="button"
|
|
1365
|
+
role="option"
|
|
1366
|
+
aria-selected=${this.draftMinute === m}
|
|
1367
|
+
?disabled=${this.draftHour !== null &&
|
|
1368
|
+
!this.isTimeInRange(this.draftHour, m)}
|
|
1369
|
+
@click=${() => this.selectMinute(m)}
|
|
1370
|
+
@keydown=${(e: KeyboardEvent) =>
|
|
1371
|
+
this.handleOptionKeydown(e, "minute")}
|
|
1372
|
+
tabindex=${this.draftMinute === m ||
|
|
1373
|
+
(this.draftMinute === null && i === 0)
|
|
1374
|
+
? 0
|
|
1375
|
+
: -1}
|
|
1376
|
+
>
|
|
1377
|
+
${String(m).padStart(2, "0")}
|
|
1378
|
+
</button>
|
|
1379
|
+
`
|
|
1380
|
+
)}
|
|
910
1381
|
</div>
|
|
911
1382
|
|
|
1383
|
+
${this.seconds
|
|
1384
|
+
? html`
|
|
1385
|
+
<div class="column-divider" aria-hidden="true"></div>
|
|
1386
|
+
<div class="column-list" role="listbox" aria-label="Seconds">
|
|
1387
|
+
${secondOptions.map(
|
|
1388
|
+
(s, i) => html`
|
|
1389
|
+
<button
|
|
1390
|
+
part="option"
|
|
1391
|
+
class="option-button ${this.draftSecond === s
|
|
1392
|
+
? "selected"
|
|
1393
|
+
: ""}"
|
|
1394
|
+
type="button"
|
|
1395
|
+
role="option"
|
|
1396
|
+
aria-selected=${this.draftSecond === s}
|
|
1397
|
+
@click=${() => this.selectSecond(s)}
|
|
1398
|
+
@keydown=${(e: KeyboardEvent) =>
|
|
1399
|
+
this.handleOptionKeydown(e, "second")}
|
|
1400
|
+
tabindex=${this.draftSecond === s ||
|
|
1401
|
+
(this.draftSecond === null && i === 0)
|
|
1402
|
+
? 0
|
|
1403
|
+
: -1}
|
|
1404
|
+
>
|
|
1405
|
+
${String(s).padStart(2, "0")}
|
|
1406
|
+
</button>
|
|
1407
|
+
`
|
|
1408
|
+
)}
|
|
1409
|
+
</div>
|
|
1410
|
+
`
|
|
1411
|
+
: nothing}
|
|
912
1412
|
${this.format === "12h"
|
|
913
1413
|
? html`
|
|
914
|
-
<div class="column">
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
)}
|
|
938
|
-
</div>
|
|
1414
|
+
<div class="column-divider" aria-hidden="true"></div>
|
|
1415
|
+
<div class="column-list" role="listbox" aria-label="AM or PM">
|
|
1416
|
+
${(["AM", "PM"] as const).map(
|
|
1417
|
+
(p) => html`
|
|
1418
|
+
<button
|
|
1419
|
+
part="option"
|
|
1420
|
+
class="option-button ${draftPeriod === p ? "selected" : ""}"
|
|
1421
|
+
type="button"
|
|
1422
|
+
role="option"
|
|
1423
|
+
aria-selected=${draftPeriod === p}
|
|
1424
|
+
?disabled=${!this.isPeriodEnabled(p)}
|
|
1425
|
+
@click=${() => this.selectPeriod(p)}
|
|
1426
|
+
@keydown=${(e: KeyboardEvent) =>
|
|
1427
|
+
this.handleOptionKeydown(e, "period")}
|
|
1428
|
+
tabindex=${draftPeriod === p ||
|
|
1429
|
+
(draftPeriod === null && p === "AM")
|
|
1430
|
+
? 0
|
|
1431
|
+
: -1}
|
|
1432
|
+
>
|
|
1433
|
+
${p}
|
|
1434
|
+
</button>
|
|
1435
|
+
`
|
|
1436
|
+
)}
|
|
939
1437
|
</div>
|
|
940
1438
|
`
|
|
941
1439
|
: nothing}
|
|
942
1440
|
</div>
|
|
943
1441
|
|
|
944
1442
|
<div part="footer" class="time-footer">
|
|
945
|
-
<rel-button
|
|
946
|
-
variant="secondary"
|
|
947
|
-
size="small"
|
|
948
|
-
border="corner-radius"
|
|
949
|
-
@click=${() => this.selectNow()}
|
|
950
|
-
>
|
|
1443
|
+
<rel-button variant="link" size="small" @click=${() => this.selectNow()}>
|
|
951
1444
|
Now
|
|
952
1445
|
</rel-button>
|
|
953
1446
|
<div class="footer-right">
|
|
954
1447
|
<rel-button
|
|
955
|
-
variant="
|
|
1448
|
+
variant="ghost"
|
|
956
1449
|
size="small"
|
|
957
|
-
border="corner-radius"
|
|
958
1450
|
@click=${() => this.cancelPopover()}
|
|
959
1451
|
>
|
|
960
1452
|
Cancel
|
|
@@ -963,25 +1455,21 @@ export class RelTimePicker extends LitElement {
|
|
|
963
1455
|
variant="primary"
|
|
964
1456
|
tone="default"
|
|
965
1457
|
size="small"
|
|
966
|
-
border="corner-radius"
|
|
967
1458
|
?disabled=${this.draftHour === null}
|
|
968
1459
|
@click=${() => this.applySelection()}
|
|
969
1460
|
>
|
|
970
|
-
|
|
1461
|
+
OK
|
|
971
1462
|
</rel-button>
|
|
972
1463
|
</div>
|
|
973
1464
|
</div>
|
|
974
1465
|
</div>
|
|
975
1466
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
${belowText}
|
|
983
|
-
</div>`
|
|
984
|
-
: nothing}
|
|
1467
|
+
<div
|
|
1468
|
+
part="helper"
|
|
1469
|
+
class="time-picker-support ${isError ? "error" : ""} ${belowText ? "" : "empty"}"
|
|
1470
|
+
id="time-picker-desc"
|
|
1471
|
+
aria-live="polite"
|
|
1472
|
+
>${belowText ?? nothing}</div>
|
|
985
1473
|
</div>
|
|
986
1474
|
`;
|
|
987
1475
|
}
|