@reliability-design/web 1.4.3 → 1.4.5
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-autocomplete/README.md +1 -0
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +56 -1
- package/dist/components/rel-button/rel-button.ts +18 -2
- package/dist/components/rel-calendar/rel-calendar.ts +23 -1
- package/dist/components/rel-carousel/rel-carousel.ts +20 -0
- package/dist/components/rel-checkbox/README.md +69 -22
- package/dist/components/rel-checkbox/rel-checkbox.ts +119 -9
- package/dist/components/rel-chip/rel-chip.ts +29 -0
- package/dist/components/rel-date-picker/rel-date-picker.ts +156 -13
- package/dist/components/rel-drawer/rel-drawer.ts +22 -58
- package/dist/components/rel-menu/rel-menu.ts +124 -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 +3 -3
- package/dist/components/rel-pagination/README.md +14 -0
- package/dist/components/rel-pagination/rel-pagination.ts +30 -14
- package/dist/components/rel-popover/rel-popover.ts +31 -3
- package/dist/components/rel-range-picker/rel-range-picker.ts +251 -3
- package/dist/components/rel-resizable/rel-resizable-group.ts +20 -0
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +50 -12
- package/dist/components/rel-select/README.md +1 -0
- package/dist/components/rel-select/rel-select.ts +1385 -1170
- package/dist/components/rel-sheet/rel-sheet.ts +22 -1
- package/dist/components/rel-sidebar/rel-sidebar.ts +43 -39
- package/dist/components/rel-stepper/rel-stepper.ts +44 -1
- package/dist/components/rel-tab/rel-tab.ts +32 -7
- package/dist/components/rel-time-picker/rel-time-picker.ts +338 -206
- package/dist/components/rel-tooltip/rel-tooltip.ts +30 -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/UPLOAD-DESIGN-CHANGES.md +134 -0
- package/dist/components/rel-upload/rel-upload-actions.ts +163 -0
- package/dist/components/rel-upload/rel-upload.ts +943 -0
- package/dist/index.d.mts +491 -18
- package/dist/index.d.ts +491 -18
- package/dist/index.js +2748 -410
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2953 -627
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import { customElement, property, state, query } from "lit/decorators.js";
|
|
|
3
3
|
import "@reliability-design/icons/rel-icon";
|
|
4
4
|
import "../rel-button/rel-button";
|
|
5
5
|
|
|
6
|
-
type TimeColumn = "hour" | "minute" | "period";
|
|
6
|
+
type TimeColumn = "hour" | "minute" | "second" | "period";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* `<rel-time-picker>` — time selection field.
|
|
@@ -13,12 +13,18 @@ type TimeColumn = "hour" | "minute" | "period";
|
|
|
13
13
|
* column selections are held in draft state until Apply is clicked; Cancel,
|
|
14
14
|
* Escape, or an outside click discards the draft.
|
|
15
15
|
*
|
|
16
|
-
* The committed `value` is ALWAYS 24-hour "HH:mm"
|
|
17
|
-
* `format`, so consumers get
|
|
16
|
+
* The committed `value` is ALWAYS 24-hour "HH:mm" (or "HH:mm:ss" when
|
|
17
|
+
* `seconds` is enabled) regardless of the display `format`, so consumers get
|
|
18
|
+
* a consistent machine-readable string.
|
|
19
|
+
*
|
|
20
|
+
* Panel visuals follow the Figma "Time Picker" component (Reliability DS
|
|
21
|
+
* V1.0, node 8301:3771): fixed panel widths 220/250/280 by size, full-bleed
|
|
22
|
+
* columns with 1px dividers, neutral selected state, and a Now / Cancel / OK
|
|
23
|
+
* footer whose buttons are always size S.
|
|
18
24
|
*/
|
|
19
25
|
@customElement("rel-time-picker")
|
|
20
26
|
export class RelTimePicker extends LitElement {
|
|
21
|
-
/** Committed time, 24-hour "HH:mm" (e.g. "09:30", "17:05"). */
|
|
27
|
+
/** Committed time, 24-hour "HH:mm" or "HH:mm:ss" (e.g. "09:30", "17:05:30"). */
|
|
22
28
|
@property({ type: String, reflect: true }) value: string | null = null;
|
|
23
29
|
@property({ type: String }) placeholder = "Select time";
|
|
24
30
|
@property({ type: String }) label: string | null = null;
|
|
@@ -26,10 +32,12 @@ export class RelTimePicker extends LitElement {
|
|
|
26
32
|
@property({ type: Boolean }) required = false;
|
|
27
33
|
@property({ type: Boolean, reflect: true }) readonly = false;
|
|
28
34
|
|
|
29
|
-
/** Display format of the trigger text and hour column. Value stays
|
|
35
|
+
/** Display format of the trigger text and hour column. Value stays 24-hour. */
|
|
30
36
|
@property({ type: String }) format: "12h" | "24h" = "12h";
|
|
37
|
+
/** Shows the seconds column; committed value becomes "HH:mm:ss". */
|
|
38
|
+
@property({ type: Boolean, reflect: true }) seconds = false;
|
|
31
39
|
/** Minute granularity of the minutes column (1, 5, 10, 15, 30, …). */
|
|
32
|
-
@property({ type: Number, attribute: "minute-step" }) minuteStep =
|
|
40
|
+
@property({ type: Number, attribute: "minute-step" }) minuteStep = 5;
|
|
33
41
|
|
|
34
42
|
/** Earliest selectable time, "HH:mm" (inclusive). */
|
|
35
43
|
@property({ type: String, attribute: "min-time" }) minTime: string | null = null;
|
|
@@ -50,9 +58,12 @@ export class RelTimePicker extends LitElement {
|
|
|
50
58
|
@property({ type: String, reflect: true }) variant: "outlined" = "outlined";
|
|
51
59
|
@property({ type: Boolean, reflect: true }) fullWidth = false;
|
|
52
60
|
|
|
53
|
-
// Draft selection (hour is 0–23 internally; committed on
|
|
61
|
+
// Draft selection (hour is 0–23 internally; committed on OK only)
|
|
54
62
|
@state() private draftHour: number | null = null;
|
|
55
63
|
@state() private draftMinute: number | null = null;
|
|
64
|
+
@state() private draftSecond: number | null = null;
|
|
65
|
+
/** Panel opens above the field when there is no viewport space below. */
|
|
66
|
+
@state() private openUp = false;
|
|
56
67
|
|
|
57
68
|
@query(".trigger-button") private triggerButton?: HTMLButtonElement;
|
|
58
69
|
@query(".columns") private columnsElement?: HTMLElement;
|
|
@@ -224,16 +235,19 @@ export class RelTimePicker extends LitElement {
|
|
|
224
235
|
height: 16px;
|
|
225
236
|
}
|
|
226
237
|
|
|
238
|
+
/* Panel (Figma "Time Picker"): fixed width per size, full-bleed content. */
|
|
227
239
|
.popover {
|
|
228
240
|
position: absolute;
|
|
229
241
|
top: calc(100% + 4px);
|
|
230
242
|
left: 0;
|
|
231
243
|
z-index: 1000;
|
|
232
244
|
background: var(--rel-bg-surface);
|
|
233
|
-
border: 1px solid var(--rel-border-
|
|
245
|
+
border: 1px solid var(--rel-border-subtle);
|
|
234
246
|
border-radius: var(--rel-border-radius-sm);
|
|
235
247
|
box-shadow: var(--rel-shadows-shadow-300);
|
|
236
|
-
|
|
248
|
+
overflow: hidden;
|
|
249
|
+
min-width: 220px;
|
|
250
|
+
width: 250px;
|
|
237
251
|
display: none;
|
|
238
252
|
}
|
|
239
253
|
|
|
@@ -241,94 +255,79 @@ export class RelTimePicker extends LitElement {
|
|
|
241
255
|
display: block;
|
|
242
256
|
}
|
|
243
257
|
|
|
244
|
-
/*
|
|
245
|
-
.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
gap: var(--rel-spacing-sm);
|
|
249
|
-
padding-bottom: var(--rel-spacing-sm);
|
|
250
|
-
margin-bottom: var(--rel-spacing-sm);
|
|
251
|
-
border-bottom: 1px solid var(--rel-border-subtle);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
.time-readout-label {
|
|
255
|
-
color: var(--rel-text-placeholder);
|
|
256
|
-
text-transform: uppercase;
|
|
257
|
-
font-family: var(--rel-body-xs-semibold-font-family);
|
|
258
|
-
font-weight: var(--rel-body-xs-semibold-font-weight);
|
|
259
|
-
font-size: var(--rel-body-xs-semibold-font-size);
|
|
260
|
-
line-height: var(--rel-body-xs-semibold-line-height);
|
|
261
|
-
letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
.time-readout-value {
|
|
265
|
-
color: var(--rel-text-default);
|
|
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);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
.time-readout-value.empty {
|
|
274
|
-
color: var(--rel-text-placeholder);
|
|
258
|
+
/* Flip above the field when the viewport has no room below. */
|
|
259
|
+
.popover.open-up {
|
|
260
|
+
top: auto;
|
|
261
|
+
bottom: calc(100% + 4px);
|
|
275
262
|
}
|
|
276
263
|
|
|
264
|
+
/* Full-bleed column row with 1px dividers (Figma "Columns"). */
|
|
277
265
|
.columns {
|
|
278
266
|
display: flex;
|
|
279
|
-
|
|
267
|
+
align-items: stretch;
|
|
268
|
+
height: 260px;
|
|
280
269
|
}
|
|
281
270
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
flex-direction: column;
|
|
287
|
-
flex: 1;
|
|
288
|
-
min-width: 64px;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
.column-header {
|
|
292
|
-
text-align: center;
|
|
293
|
-
color: var(--rel-text-placeholder);
|
|
294
|
-
padding: var(--rel-spacing-xs);
|
|
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);
|
|
271
|
+
.column-divider {
|
|
272
|
+
flex-shrink: 0;
|
|
273
|
+
width: 1px;
|
|
274
|
+
background: var(--rel-border-subtle);
|
|
301
275
|
}
|
|
302
276
|
|
|
277
|
+
/* Each column scrolls independently. The 4px rail is always reserved
|
|
278
|
+
(scrollbar-gutter) so columns stay aligned whether or not they
|
|
279
|
+
overflow; the thumb only appears on overflow. */
|
|
303
280
|
.column-list {
|
|
281
|
+
flex: 1;
|
|
282
|
+
min-width: 0;
|
|
304
283
|
display: flex;
|
|
305
284
|
flex-direction: column;
|
|
306
|
-
|
|
307
|
-
|
|
285
|
+
/* Positioned so option offsetTop is list-relative — the in-column
|
|
286
|
+
scroll math depends on it (we never scrollIntoView: it scrolls every
|
|
287
|
+
ancestor, page included). */
|
|
288
|
+
position: relative;
|
|
308
289
|
overflow-y: auto;
|
|
309
|
-
padding: var(--rel-spacing-
|
|
290
|
+
padding: var(--rel-spacing-2xs);
|
|
310
291
|
scrollbar-width: thin;
|
|
292
|
+
scrollbar-color: var(--rel-border-default) transparent;
|
|
293
|
+
scrollbar-gutter: stable;
|
|
311
294
|
}
|
|
312
295
|
|
|
296
|
+
.column-list::-webkit-scrollbar {
|
|
297
|
+
width: 4px;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.column-list::-webkit-scrollbar-track {
|
|
301
|
+
background: transparent;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.column-list::-webkit-scrollbar-thumb {
|
|
305
|
+
background: var(--rel-border-default);
|
|
306
|
+
border-radius: var(--rel-border-radius-full-rounded);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* Option cell (Figma ".Time Option"): selected uses the neutral
|
|
310
|
+
elevated-2 fill + medium weight, NOT the brand color. */
|
|
313
311
|
.option-button {
|
|
314
312
|
border: none;
|
|
315
313
|
background: transparent;
|
|
316
|
-
border-radius: var(--rel-border-radius-
|
|
314
|
+
border-radius: var(--rel-border-radius-xs);
|
|
317
315
|
color: var(--rel-text-default);
|
|
318
316
|
cursor: pointer;
|
|
319
|
-
padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
|
|
320
|
-
text-align: center;
|
|
321
317
|
flex-shrink: 0;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
318
|
+
height: 36px;
|
|
319
|
+
padding: 0 var(--rel-spacing-xs);
|
|
320
|
+
text-align: center;
|
|
321
|
+
transition: background 0.15s ease;
|
|
322
|
+
font-family: var(--rel-body-md-regular-font-family);
|
|
323
|
+
font-weight: var(--rel-body-md-regular-font-weight);
|
|
324
|
+
font-size: var(--rel-body-md-regular-font-size);
|
|
325
|
+
line-height: var(--rel-body-md-regular-line-height);
|
|
326
|
+
letter-spacing: var(--rel-body-md-regular-letter-spacing);
|
|
328
327
|
}
|
|
329
328
|
|
|
330
329
|
.option-button:hover:not(:disabled):not(.selected) {
|
|
331
|
-
background: var(--rel-bg-
|
|
330
|
+
background: var(--rel-bg-neutral-elevated-1);
|
|
332
331
|
}
|
|
333
332
|
|
|
334
333
|
.option-button:focus-visible {
|
|
@@ -338,9 +337,9 @@ export class RelTimePicker extends LitElement {
|
|
|
338
337
|
}
|
|
339
338
|
|
|
340
339
|
.option-button.selected {
|
|
341
|
-
background: var(--rel-
|
|
342
|
-
color: var(--rel-
|
|
343
|
-
font-weight: var(--rel-body-md-
|
|
340
|
+
background: var(--rel-bg-neutral-elevated-2);
|
|
341
|
+
color: var(--rel-text-default);
|
|
342
|
+
font-weight: var(--rel-body-md-medium-font-weight);
|
|
344
343
|
}
|
|
345
344
|
|
|
346
345
|
.option-button:disabled {
|
|
@@ -353,14 +352,15 @@ export class RelTimePicker extends LitElement {
|
|
|
353
352
|
background: transparent;
|
|
354
353
|
}
|
|
355
354
|
|
|
356
|
-
/* Footer (
|
|
355
|
+
/* Footer (Figma "Footer"): 8px padding + 8px extra left so the "Now"
|
|
356
|
+
link aligns with the buttons' inner padding. Buttons are always S. */
|
|
357
357
|
.time-footer {
|
|
358
358
|
display: flex;
|
|
359
359
|
align-items: center;
|
|
360
360
|
justify-content: space-between;
|
|
361
|
-
gap: var(--rel-spacing-
|
|
362
|
-
padding
|
|
363
|
-
|
|
361
|
+
gap: var(--rel-spacing-xs);
|
|
362
|
+
padding: var(--rel-spacing-xs);
|
|
363
|
+
padding-left: calc(var(--rel-spacing-xs) * 2);
|
|
364
364
|
border-top: 1px solid var(--rel-border-subtle);
|
|
365
365
|
}
|
|
366
366
|
|
|
@@ -370,6 +370,56 @@ export class RelTimePicker extends LitElement {
|
|
|
370
370
|
gap: var(--rel-spacing-xs);
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
/* The design narrows OK below the DS small min-width (60 vs 80) so the
|
|
374
|
+
compact footer fits the 220px panel. */
|
|
375
|
+
.footer-right rel-button[variant="primary"] {
|
|
376
|
+
--rel-button-min-width: 60px;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* Panel size variants (Figma S/M/L): width 220/250/280, option height
|
|
380
|
+
32/36/40, body-sm/md/lg type. Medium is the base above. */
|
|
381
|
+
.time-picker-wrapper.small .popover {
|
|
382
|
+
width: 220px;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.time-picker-wrapper.small .columns {
|
|
386
|
+
height: 232px;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.time-picker-wrapper.small .option-button {
|
|
390
|
+
height: 32px;
|
|
391
|
+
font-family: var(--rel-body-sm-regular-font-family);
|
|
392
|
+
font-weight: var(--rel-body-sm-regular-font-weight);
|
|
393
|
+
font-size: var(--rel-body-sm-regular-font-size);
|
|
394
|
+
line-height: var(--rel-body-sm-regular-line-height);
|
|
395
|
+
letter-spacing: var(--rel-body-sm-regular-letter-spacing);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.time-picker-wrapper.small .option-button.selected {
|
|
399
|
+
font-weight: var(--rel-body-sm-medium-font-weight);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.time-picker-wrapper.large .popover {
|
|
403
|
+
width: 280px;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.time-picker-wrapper.large .columns {
|
|
407
|
+
height: 288px;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.time-picker-wrapper.large .option-button {
|
|
411
|
+
height: 40px;
|
|
412
|
+
font-family: var(--rel-body-lg-regular-font-family);
|
|
413
|
+
font-weight: var(--rel-body-lg-regular-font-weight);
|
|
414
|
+
font-size: var(--rel-body-lg-regular-font-size);
|
|
415
|
+
line-height: var(--rel-body-lg-regular-line-height);
|
|
416
|
+
letter-spacing: var(--rel-body-lg-regular-letter-spacing);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.time-picker-wrapper.large .option-button.selected {
|
|
420
|
+
font-weight: var(--rel-body-lg-medium-font-weight);
|
|
421
|
+
}
|
|
422
|
+
|
|
373
423
|
@media (prefers-reduced-motion: reduce) {
|
|
374
424
|
.trigger-button,
|
|
375
425
|
.icon-button,
|
|
@@ -406,11 +456,17 @@ export class RelTimePicker extends LitElement {
|
|
|
406
456
|
};
|
|
407
457
|
|
|
408
458
|
updated(changedProperties: Map<string, any>) {
|
|
409
|
-
if (changedProperties.has("open")
|
|
459
|
+
if (changedProperties.has("open")) {
|
|
460
|
+
if (!this.open) {
|
|
461
|
+
this.openUp = false;
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
410
464
|
this.resetDraftFromValue();
|
|
411
|
-
//
|
|
412
|
-
//
|
|
465
|
+
// Once the popover has rendered: flip it above the field if the
|
|
466
|
+
// viewport lacks room below, center each column's selection, and focus
|
|
467
|
+
// the selected (or first enabled) hour without scrolling the page.
|
|
413
468
|
setTimeout(() => {
|
|
469
|
+
this.updatePanelPlacement();
|
|
414
470
|
this.scrollColumnsToSelection();
|
|
415
471
|
const target =
|
|
416
472
|
this.columnsElement?.querySelector<HTMLButtonElement>(
|
|
@@ -419,35 +475,51 @@ export class RelTimePicker extends LitElement {
|
|
|
419
475
|
this.columnsElement?.querySelector<HTMLButtonElement>(
|
|
420
476
|
".option-button:not(:disabled)"
|
|
421
477
|
);
|
|
422
|
-
target?.focus();
|
|
478
|
+
target?.focus({ preventScroll: true });
|
|
423
479
|
}, 0);
|
|
424
480
|
}
|
|
425
481
|
}
|
|
426
482
|
|
|
483
|
+
/** Open upward when the panel doesn't fit below but does fit above. */
|
|
484
|
+
private updatePanelPlacement() {
|
|
485
|
+
const panel = this.renderRoot.querySelector<HTMLElement>(".popover");
|
|
486
|
+
if (!panel) return;
|
|
487
|
+
const rect = this.getBoundingClientRect();
|
|
488
|
+
const panelHeight = panel.offsetHeight;
|
|
489
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
490
|
+
this.openUp = spaceBelow < panelHeight + 8 && rect.top > panelHeight + 8;
|
|
491
|
+
}
|
|
492
|
+
|
|
427
493
|
// ── Parsing / formatting ──
|
|
428
494
|
|
|
429
|
-
private parseTime(
|
|
495
|
+
private parseTime(
|
|
496
|
+
time: string | null
|
|
497
|
+
): { hour: number; minute: number; second: number } | null {
|
|
430
498
|
if (!time) return null;
|
|
431
|
-
const match = time.match(/^(\d{1,2}):(\d{2})
|
|
499
|
+
const match = time.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
|
|
432
500
|
if (!match) return null;
|
|
433
501
|
const hour = parseInt(match[1], 10);
|
|
434
502
|
const minute = parseInt(match[2], 10);
|
|
435
|
-
|
|
436
|
-
|
|
503
|
+
const second = match[3] ? parseInt(match[3], 10) : 0;
|
|
504
|
+
if (hour > 23 || minute > 59 || second > 59) return null;
|
|
505
|
+
return { hour, minute, second };
|
|
437
506
|
}
|
|
438
507
|
|
|
439
|
-
private formatValue(hour: number, minute: number): string {
|
|
440
|
-
|
|
508
|
+
private formatValue(hour: number, minute: number, second = 0): string {
|
|
509
|
+
const hh = String(hour).padStart(2, "0");
|
|
510
|
+
const mm = String(minute).padStart(2, "0");
|
|
511
|
+
return this.seconds ? `${hh}:${mm}:${String(second).padStart(2, "0")}` : `${hh}:${mm}`;
|
|
441
512
|
}
|
|
442
513
|
|
|
443
|
-
private formatDisplay(hour: number, minute: number): string {
|
|
514
|
+
private formatDisplay(hour: number, minute: number, second = 0): string {
|
|
444
515
|
const mm = String(minute).padStart(2, "0");
|
|
516
|
+
const ss = this.seconds ? `:${String(second).padStart(2, "0")}` : "";
|
|
445
517
|
if (this.format === "24h") {
|
|
446
|
-
return `${String(hour).padStart(2, "0")}:${mm}`;
|
|
518
|
+
return `${String(hour).padStart(2, "0")}:${mm}${ss}`;
|
|
447
519
|
}
|
|
448
520
|
const period = hour < 12 ? "AM" : "PM";
|
|
449
521
|
const hour12 = hour % 12 === 0 ? 12 : hour % 12;
|
|
450
|
-
return `${hour12}:${mm} ${period}`;
|
|
522
|
+
return `${hour12}:${mm}${ss} ${period}`;
|
|
451
523
|
}
|
|
452
524
|
|
|
453
525
|
private toMinutes(hour: number, minute: number): number {
|
|
@@ -504,13 +576,22 @@ export class RelTimePicker extends LitElement {
|
|
|
504
576
|
hour24: h,
|
|
505
577
|
}));
|
|
506
578
|
}
|
|
507
|
-
// 12-hour clock,
|
|
579
|
+
// 12-hour clock, 01 … 12 as in the Figma "Hour 12" column ("12" maps to
|
|
580
|
+
// 00:xx AM / 12:xx PM).
|
|
508
581
|
const period = this.draftPeriod ?? "AM";
|
|
509
582
|
const base = period === "AM" ? 0 : 12;
|
|
510
|
-
return Array.from({ length: 12 }, (_, i) =>
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
583
|
+
return Array.from({ length: 12 }, (_, i) => {
|
|
584
|
+
const display = i + 1;
|
|
585
|
+
return {
|
|
586
|
+
label: String(display).padStart(2, "0"),
|
|
587
|
+
hour24: base + (display % 12),
|
|
588
|
+
};
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Seconds shown in the seconds column (always step 1). */
|
|
593
|
+
private getSecondOptions(): number[] {
|
|
594
|
+
return Array.from({ length: 60 }, (_, s) => s);
|
|
514
595
|
}
|
|
515
596
|
|
|
516
597
|
private get draftPeriod(): "AM" | "PM" | null {
|
|
@@ -522,6 +603,7 @@ export class RelTimePicker extends LitElement {
|
|
|
522
603
|
const parsed = this.parseTime(this.value);
|
|
523
604
|
this.draftHour = parsed ? parsed.hour : null;
|
|
524
605
|
this.draftMinute = parsed ? parsed.minute : null;
|
|
606
|
+
this.draftSecond = parsed ? parsed.second : null;
|
|
525
607
|
}
|
|
526
608
|
|
|
527
609
|
/** Snap the draft minute to the nearest valid step minute for the hour. */
|
|
@@ -569,7 +651,11 @@ export class RelTimePicker extends LitElement {
|
|
|
569
651
|
|
|
570
652
|
private applySelection() {
|
|
571
653
|
if (this.draftHour === null) return;
|
|
572
|
-
const newValue = this.formatValue(
|
|
654
|
+
const newValue = this.formatValue(
|
|
655
|
+
this.draftHour,
|
|
656
|
+
this.draftMinute ?? 0,
|
|
657
|
+
this.draftSecond ?? 0
|
|
658
|
+
);
|
|
573
659
|
this.value = newValue;
|
|
574
660
|
this.dispatchEvent(
|
|
575
661
|
new CustomEvent("rel-change", {
|
|
@@ -586,6 +672,7 @@ export class RelTimePicker extends LitElement {
|
|
|
586
672
|
const now = new Date();
|
|
587
673
|
const step = this.minuteStepSafe;
|
|
588
674
|
this.draftHour = now.getHours();
|
|
675
|
+
this.draftSecond = this.seconds ? now.getSeconds() : null;
|
|
589
676
|
this.draftMinute = Math.round(now.getMinutes() / step) * step;
|
|
590
677
|
if (this.draftMinute >= 60) {
|
|
591
678
|
this.draftMinute = 0;
|
|
@@ -610,6 +697,7 @@ export class RelTimePicker extends LitElement {
|
|
|
610
697
|
this.value = null;
|
|
611
698
|
this.draftHour = null;
|
|
612
699
|
this.draftMinute = null;
|
|
700
|
+
this.draftSecond = null;
|
|
613
701
|
this.dispatchEvent(
|
|
614
702
|
new CustomEvent("rel-change", {
|
|
615
703
|
detail: { value: null },
|
|
@@ -643,6 +731,22 @@ export class RelTimePicker extends LitElement {
|
|
|
643
731
|
this.draftMinute = minute;
|
|
644
732
|
}
|
|
645
733
|
|
|
734
|
+
private selectSecond(second: number) {
|
|
735
|
+
if (this.draftHour === null) {
|
|
736
|
+
// Second picked first — default the hour to the first enabled one.
|
|
737
|
+
for (let h = 0; h < 24; h++) {
|
|
738
|
+
if (this.isHourEnabled(h)) {
|
|
739
|
+
this.draftHour = h;
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (this.draftHour === null) return;
|
|
744
|
+
}
|
|
745
|
+
if (this.draftMinute === null) this.draftMinute = 0;
|
|
746
|
+
this.draftSecond = second;
|
|
747
|
+
this.clampDraftToRange();
|
|
748
|
+
}
|
|
749
|
+
|
|
646
750
|
private selectPeriod(period: "AM" | "PM") {
|
|
647
751
|
if (!this.isPeriodEnabled(period)) return;
|
|
648
752
|
const base = period === "AM" ? 0 : 12;
|
|
@@ -663,11 +767,30 @@ export class RelTimePicker extends LitElement {
|
|
|
663
767
|
this.scrollColumnsToSelection();
|
|
664
768
|
}
|
|
665
769
|
|
|
770
|
+
/**
|
|
771
|
+
* Scrolls the column's own scroll container only. scrollIntoView is off
|
|
772
|
+
* limits here: it walks ALL scrollable ancestors, so it yanked the page
|
|
773
|
+
* up/down whenever a column recentred (user-reported jump bug).
|
|
774
|
+
*/
|
|
775
|
+
private scrollOptionInColumn(btn: HTMLElement, block: "center" | "nearest") {
|
|
776
|
+
const list = btn.parentElement;
|
|
777
|
+
if (!list) return;
|
|
778
|
+
const top = btn.offsetTop;
|
|
779
|
+
const height = btn.offsetHeight;
|
|
780
|
+
if (block === "center") {
|
|
781
|
+
list.scrollTop = top - (list.clientHeight - height) / 2;
|
|
782
|
+
} else if (top < list.scrollTop) {
|
|
783
|
+
list.scrollTop = top;
|
|
784
|
+
} else if (top + height > list.scrollTop + list.clientHeight) {
|
|
785
|
+
list.scrollTop = top + height - list.clientHeight;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
666
789
|
private scrollColumnsToSelection() {
|
|
667
790
|
this.updateComplete.then(() => {
|
|
668
791
|
this.columnsElement
|
|
669
792
|
?.querySelectorAll<HTMLButtonElement>(".option-button.selected")
|
|
670
|
-
.forEach((btn) =>
|
|
793
|
+
.forEach((btn) => this.scrollOptionInColumn(btn, "center"));
|
|
671
794
|
});
|
|
672
795
|
}
|
|
673
796
|
|
|
@@ -729,14 +852,15 @@ export class RelTimePicker extends LitElement {
|
|
|
729
852
|
|
|
730
853
|
if (target && target !== buttons[index]) {
|
|
731
854
|
target.click();
|
|
732
|
-
target.focus();
|
|
733
|
-
|
|
855
|
+
target.focus({ preventScroll: true });
|
|
856
|
+
this.scrollOptionInColumn(target, "nearest");
|
|
734
857
|
}
|
|
735
858
|
}
|
|
736
859
|
|
|
737
860
|
private focusAdjacentColumn(from: TimeColumn, direction: 1 | -1) {
|
|
738
|
-
const order: TimeColumn[] =
|
|
739
|
-
|
|
861
|
+
const order: TimeColumn[] = ["hour", "minute"];
|
|
862
|
+
if (this.seconds) order.push("second");
|
|
863
|
+
if (this.format === "12h") order.push("period");
|
|
740
864
|
const nextIndex = order.indexOf(from) + direction;
|
|
741
865
|
if (nextIndex < 0 || nextIndex >= order.length) return;
|
|
742
866
|
const lists = this.columnsElement?.querySelectorAll(".column-list");
|
|
@@ -744,8 +868,10 @@ export class RelTimePicker extends LitElement {
|
|
|
744
868
|
const target =
|
|
745
869
|
list?.querySelector<HTMLButtonElement>(".option-button.selected:not(:disabled)") ??
|
|
746
870
|
list?.querySelector<HTMLButtonElement>(".option-button:not(:disabled)");
|
|
747
|
-
target
|
|
748
|
-
|
|
871
|
+
if (target) {
|
|
872
|
+
target.focus({ preventScroll: true });
|
|
873
|
+
this.scrollOptionInColumn(target, "nearest");
|
|
874
|
+
}
|
|
749
875
|
}
|
|
750
876
|
|
|
751
877
|
// ── Render ──
|
|
@@ -756,14 +882,11 @@ export class RelTimePicker extends LitElement {
|
|
|
756
882
|
const describedById = belowText ? "time-picker-desc" : undefined;
|
|
757
883
|
const parsedValue = this.parseTime(this.value);
|
|
758
884
|
const displayText = parsedValue
|
|
759
|
-
? this.formatDisplay(parsedValue.hour, parsedValue.minute)
|
|
885
|
+
? this.formatDisplay(parsedValue.hour, parsedValue.minute, parsedValue.second)
|
|
760
886
|
: "";
|
|
761
|
-
const draftDisplay =
|
|
762
|
-
this.draftHour !== null
|
|
763
|
-
? this.formatDisplay(this.draftHour, this.draftMinute ?? 0)
|
|
764
|
-
: "";
|
|
765
887
|
const hourOptions = this.getHourOptions();
|
|
766
888
|
const minuteOptions = this.getMinuteOptions();
|
|
889
|
+
const secondOptions = this.seconds ? this.getSecondOptions() : [];
|
|
767
890
|
const draftPeriod = this.draftPeriod;
|
|
768
891
|
const wrapperClasses = [
|
|
769
892
|
"time-picker-wrapper",
|
|
@@ -841,120 +964,130 @@ export class RelTimePicker extends LitElement {
|
|
|
841
964
|
|
|
842
965
|
<div
|
|
843
966
|
part="panel"
|
|
844
|
-
class="popover ${this.open ? "open" : ""}"
|
|
967
|
+
class="popover ${this.open ? "open" : ""} ${this.openUp ? "open-up" : ""}"
|
|
845
968
|
role="dialog"
|
|
846
969
|
aria-label="Choose time"
|
|
847
970
|
>
|
|
848
|
-
<div class="time-readout">
|
|
849
|
-
<span class="time-readout-label">Time</span>
|
|
850
|
-
<span class="time-readout-value ${draftDisplay ? "" : "empty"}">
|
|
851
|
-
${draftDisplay || (this.format === "24h" ? "--:--" : "--:-- --")}
|
|
852
|
-
</span>
|
|
853
|
-
</div>
|
|
854
|
-
|
|
855
971
|
<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>
|
|
972
|
+
<div class="column-list" role="listbox" aria-label="Hours">
|
|
973
|
+
${hourOptions.map(
|
|
974
|
+
(opt, i) => html`
|
|
975
|
+
<button
|
|
976
|
+
part="option"
|
|
977
|
+
class="option-button ${this.draftHour === opt.hour24
|
|
978
|
+
? "selected"
|
|
979
|
+
: ""}"
|
|
980
|
+
type="button"
|
|
981
|
+
role="option"
|
|
982
|
+
aria-selected=${this.draftHour === opt.hour24}
|
|
983
|
+
?disabled=${!this.isHourEnabled(opt.hour24)}
|
|
984
|
+
@click=${() => this.selectHour(opt.hour24)}
|
|
985
|
+
@keydown=${(e: KeyboardEvent) => this.handleOptionKeydown(e, "hour")}
|
|
986
|
+
tabindex=${this.draftHour === opt.hour24 ||
|
|
987
|
+
(this.draftHour === null && i === 0)
|
|
988
|
+
? 0
|
|
989
|
+
: -1}
|
|
990
|
+
>
|
|
991
|
+
${opt.label}
|
|
992
|
+
</button>
|
|
993
|
+
`
|
|
994
|
+
)}
|
|
882
995
|
</div>
|
|
883
996
|
|
|
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>
|
|
997
|
+
<div class="column-divider" aria-hidden="true"></div>
|
|
998
|
+
|
|
999
|
+
<div class="column-list" role="listbox" aria-label="Minutes">
|
|
1000
|
+
${minuteOptions.map(
|
|
1001
|
+
(m, i) => html`
|
|
1002
|
+
<button
|
|
1003
|
+
part="option"
|
|
1004
|
+
class="option-button ${this.draftMinute === m ? "selected" : ""}"
|
|
1005
|
+
type="button"
|
|
1006
|
+
role="option"
|
|
1007
|
+
aria-selected=${this.draftMinute === m}
|
|
1008
|
+
?disabled=${this.draftHour !== null &&
|
|
1009
|
+
!this.isTimeInRange(this.draftHour, m)}
|
|
1010
|
+
@click=${() => this.selectMinute(m)}
|
|
1011
|
+
@keydown=${(e: KeyboardEvent) =>
|
|
1012
|
+
this.handleOptionKeydown(e, "minute")}
|
|
1013
|
+
tabindex=${this.draftMinute === m ||
|
|
1014
|
+
(this.draftMinute === null && i === 0)
|
|
1015
|
+
? 0
|
|
1016
|
+
: -1}
|
|
1017
|
+
>
|
|
1018
|
+
${String(m).padStart(2, "0")}
|
|
1019
|
+
</button>
|
|
1020
|
+
`
|
|
1021
|
+
)}
|
|
910
1022
|
</div>
|
|
911
1023
|
|
|
1024
|
+
${this.seconds
|
|
1025
|
+
? html`
|
|
1026
|
+
<div class="column-divider" aria-hidden="true"></div>
|
|
1027
|
+
<div class="column-list" role="listbox" aria-label="Seconds">
|
|
1028
|
+
${secondOptions.map(
|
|
1029
|
+
(s, i) => html`
|
|
1030
|
+
<button
|
|
1031
|
+
part="option"
|
|
1032
|
+
class="option-button ${this.draftSecond === s
|
|
1033
|
+
? "selected"
|
|
1034
|
+
: ""}"
|
|
1035
|
+
type="button"
|
|
1036
|
+
role="option"
|
|
1037
|
+
aria-selected=${this.draftSecond === s}
|
|
1038
|
+
@click=${() => this.selectSecond(s)}
|
|
1039
|
+
@keydown=${(e: KeyboardEvent) =>
|
|
1040
|
+
this.handleOptionKeydown(e, "second")}
|
|
1041
|
+
tabindex=${this.draftSecond === s ||
|
|
1042
|
+
(this.draftSecond === null && i === 0)
|
|
1043
|
+
? 0
|
|
1044
|
+
: -1}
|
|
1045
|
+
>
|
|
1046
|
+
${String(s).padStart(2, "0")}
|
|
1047
|
+
</button>
|
|
1048
|
+
`
|
|
1049
|
+
)}
|
|
1050
|
+
</div>
|
|
1051
|
+
`
|
|
1052
|
+
: nothing}
|
|
912
1053
|
${this.format === "12h"
|
|
913
1054
|
? 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>
|
|
1055
|
+
<div class="column-divider" aria-hidden="true"></div>
|
|
1056
|
+
<div class="column-list" role="listbox" aria-label="AM or PM">
|
|
1057
|
+
${(["AM", "PM"] as const).map(
|
|
1058
|
+
(p) => html`
|
|
1059
|
+
<button
|
|
1060
|
+
part="option"
|
|
1061
|
+
class="option-button ${draftPeriod === p ? "selected" : ""}"
|
|
1062
|
+
type="button"
|
|
1063
|
+
role="option"
|
|
1064
|
+
aria-selected=${draftPeriod === p}
|
|
1065
|
+
?disabled=${!this.isPeriodEnabled(p)}
|
|
1066
|
+
@click=${() => this.selectPeriod(p)}
|
|
1067
|
+
@keydown=${(e: KeyboardEvent) =>
|
|
1068
|
+
this.handleOptionKeydown(e, "period")}
|
|
1069
|
+
tabindex=${draftPeriod === p ||
|
|
1070
|
+
(draftPeriod === null && p === "AM")
|
|
1071
|
+
? 0
|
|
1072
|
+
: -1}
|
|
1073
|
+
>
|
|
1074
|
+
${p}
|
|
1075
|
+
</button>
|
|
1076
|
+
`
|
|
1077
|
+
)}
|
|
939
1078
|
</div>
|
|
940
1079
|
`
|
|
941
1080
|
: nothing}
|
|
942
1081
|
</div>
|
|
943
1082
|
|
|
944
1083
|
<div part="footer" class="time-footer">
|
|
945
|
-
<rel-button
|
|
946
|
-
variant="secondary"
|
|
947
|
-
size="small"
|
|
948
|
-
border="corner-radius"
|
|
949
|
-
@click=${() => this.selectNow()}
|
|
950
|
-
>
|
|
1084
|
+
<rel-button variant="link" size="small" @click=${() => this.selectNow()}>
|
|
951
1085
|
Now
|
|
952
1086
|
</rel-button>
|
|
953
1087
|
<div class="footer-right">
|
|
954
1088
|
<rel-button
|
|
955
|
-
variant="
|
|
1089
|
+
variant="ghost"
|
|
956
1090
|
size="small"
|
|
957
|
-
border="corner-radius"
|
|
958
1091
|
@click=${() => this.cancelPopover()}
|
|
959
1092
|
>
|
|
960
1093
|
Cancel
|
|
@@ -963,11 +1096,10 @@ export class RelTimePicker extends LitElement {
|
|
|
963
1096
|
variant="primary"
|
|
964
1097
|
tone="default"
|
|
965
1098
|
size="small"
|
|
966
|
-
border="corner-radius"
|
|
967
1099
|
?disabled=${this.draftHour === null}
|
|
968
1100
|
@click=${() => this.applySelection()}
|
|
969
1101
|
>
|
|
970
|
-
|
|
1102
|
+
OK
|
|
971
1103
|
</rel-button>
|
|
972
1104
|
</div>
|
|
973
1105
|
</div>
|