@lyeve-labs/ui-kit 0.38.2 → 0.38.3
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/TimePicker.svelte +87 -27
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
toISOTime,
|
|
20
20
|
} from '../internal/time.js';
|
|
21
21
|
import type { TimeParts, TimeSegment } from '../internal/time.js';
|
|
22
|
+
import { HIT_AREA } from '../internal/touch.js';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* A time field built from one input per segment, and deliberately not from a
|
|
@@ -386,6 +387,46 @@
|
|
|
386
387
|
commit();
|
|
387
388
|
}
|
|
388
389
|
|
|
390
|
+
/** Whether the press now under way started inside a segment. */
|
|
391
|
+
let pressInSegment = false;
|
|
392
|
+
|
|
393
|
+
function onGroupPointerdown(e: PointerEvent): void {
|
|
394
|
+
pressInSegment = !!(e.target as Element | null)?.closest('[data-segment]');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* A press on the field's padding, on a colon or on a segment's hit box
|
|
399
|
+
* focuses the nearest segment, as a native time input does. The group is
|
|
400
|
+
* 44px tall under a finger and its segments are 20px, so without this a tap
|
|
401
|
+
* on most of the field focused nothing. A mouse gets the same, because the
|
|
402
|
+
* box it clicks is the box that looks like the field.
|
|
403
|
+
*/
|
|
404
|
+
function onGroupClick(e: MouseEvent): void {
|
|
405
|
+
// A drag that selects digits in a segment and ends on the padding fires
|
|
406
|
+
// its click on the group, the nearest element both ends share. Where the
|
|
407
|
+
// press started tells that drag from a press on the padding. Which
|
|
408
|
+
// element holds focus does not: iOS Safari keeps a focused input focused
|
|
409
|
+
// when a finger taps something that cannot take focus, so a guard on
|
|
410
|
+
// focus would refuse a tap on the next segment's box. A click from the
|
|
411
|
+
// keyboard has no press before it, and the flag is reset by every click.
|
|
412
|
+
const startedInSegment = pressInSegment;
|
|
413
|
+
pressInSegment = false;
|
|
414
|
+
if (disabled || !group) return;
|
|
415
|
+
if ((e.target as Element | null)?.closest('[data-segment]')) return;
|
|
416
|
+
if (startedInSegment) return;
|
|
417
|
+
let nearest: HTMLElement | null = null;
|
|
418
|
+
let best = Infinity;
|
|
419
|
+
for (const el of group.querySelectorAll<HTMLElement>('[data-segment]')) {
|
|
420
|
+
const box = el.getBoundingClientRect();
|
|
421
|
+
const distance = Math.max(box.left - e.clientX, 0, e.clientX - box.right);
|
|
422
|
+
if (distance < best) {
|
|
423
|
+
best = distance;
|
|
424
|
+
nearest = el;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
nearest?.focus();
|
|
428
|
+
}
|
|
429
|
+
|
|
389
430
|
function onMeridiemKeydown(e: KeyboardEvent): void {
|
|
390
431
|
// Left and right belong to the field: they walk the segments. Up and down
|
|
391
432
|
// step the value as they do on every other segment, and the first letter
|
|
@@ -414,44 +455,63 @@
|
|
|
414
455
|
</label>
|
|
415
456
|
{/if}
|
|
416
457
|
|
|
458
|
+
<!--
|
|
459
|
+
Under a finger each segment takes a 44px hit box round its 28px or 32px
|
|
460
|
+
visual. The segments spread apart there so their centers sit 44px apart
|
|
461
|
+
and no box takes its neighbor's: a 4px gap each side of a colon held at
|
|
462
|
+
8px, and 10px before AM or PM. The field grows 8px wider, more with
|
|
463
|
+
seconds or AM and PM, and a mouse sees it as it was.
|
|
464
|
+
|
|
465
|
+
The press and the click on the group only move focus to a segment, which
|
|
466
|
+
Tab and the label already reach, so they need no key handler of their own.
|
|
467
|
+
-->
|
|
468
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
|
417
469
|
<div
|
|
418
470
|
bind:this={group}
|
|
419
471
|
role="group"
|
|
420
472
|
aria-labelledby={label ? `${fieldId}-label` : undefined}
|
|
421
473
|
aria-label={label ? undefined : 'Time'}
|
|
422
474
|
aria-describedby={describedBy(fieldId, error, hint)}
|
|
423
|
-
|
|
475
|
+
onpointerdown={onGroupPointerdown}
|
|
476
|
+
onclick={onGroupClick}
|
|
477
|
+
class="{CONTROL_SEGMENTED} coarse:gap-1 {segmentedBorder(!!error)} {disabled
|
|
424
478
|
? 'cursor-not-allowed opacity-50'
|
|
425
479
|
: ''}"
|
|
426
480
|
>
|
|
427
481
|
{#each digitSegments as segment, i (segment)}
|
|
428
482
|
{#if i > 0}
|
|
429
|
-
<span aria-hidden="true" class="text-faint">:</span>
|
|
483
|
+
<span aria-hidden="true" class="text-faint coarse:w-2 coarse:text-center">:</span>
|
|
430
484
|
{/if}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
485
|
+
<!-- An input draws no pseudo-element, so the hit box sits on a span
|
|
486
|
+
round it, and the group's click handler focuses the input a tap on
|
|
487
|
+
the box lands beside. A label would do it without script, but it
|
|
488
|
+
would be a second label in the field for every segment. -->
|
|
489
|
+
<span data-segment-box class="{HIT_AREA} inline-flex">
|
|
490
|
+
<input
|
|
491
|
+
data-segment={segment}
|
|
492
|
+
id="{fieldId}-{segment}"
|
|
493
|
+
type="text"
|
|
494
|
+
inputmode="numeric"
|
|
495
|
+
role="spinbutton"
|
|
496
|
+
autocomplete="off"
|
|
497
|
+
spellcheck="false"
|
|
498
|
+
maxlength="2"
|
|
499
|
+
{disabled}
|
|
500
|
+
value={shown(segment)}
|
|
501
|
+
aria-label={SEGMENT_LABEL[segment]}
|
|
502
|
+
aria-valuenow={shownNow(segment)}
|
|
503
|
+
aria-valuemin={segmentMin(segment)}
|
|
504
|
+
aria-valuemax={segmentMax(segment)}
|
|
505
|
+
aria-valuetext={valueText(segment)}
|
|
506
|
+
aria-invalid={error ? 'true' : undefined}
|
|
507
|
+
aria-required={required ? 'true' : undefined}
|
|
508
|
+
onkeydown={(e) => onSegmentKeydown(segment, e)}
|
|
509
|
+
oninput={(e) => onSegmentInput(segment, e)}
|
|
510
|
+
onblur={() => onSegmentBlur(segment)}
|
|
511
|
+
onfocus={(e) => e.currentTarget.select()}
|
|
512
|
+
class="{CONTROL_SEGMENT} w-7"
|
|
513
|
+
/>
|
|
514
|
+
</span>
|
|
455
515
|
{/each}
|
|
456
516
|
|
|
457
517
|
{#if hour12}
|
|
@@ -473,7 +533,7 @@
|
|
|
473
533
|
aria-required={required ? 'true' : undefined}
|
|
474
534
|
onclick={() => setMeridiem(meridiem === 'AM' ? 'PM' : 'AM')}
|
|
475
535
|
onkeydown={onMeridiemKeydown}
|
|
476
|
-
class="{CONTROL_SEGMENT} ms-1 w-8 cursor-pointer"
|
|
536
|
+
class="{HIT_AREA} {CONTROL_SEGMENT} ms-1 coarse:ms-2.5 w-8 cursor-pointer"
|
|
477
537
|
>
|
|
478
538
|
{meridiem}
|
|
479
539
|
</button>
|
package/dist/index.d.ts
CHANGED
|
@@ -103,4 +103,4 @@ export { cn, type ClassValue } from './utils/cn.js';
|
|
|
103
103
|
export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setTheme, setThemePreference, systemTheme, themeBootScript, watchThemePreference, toggleTheme, watchSystemTheme, THEME_PREFERENCES, type Theme, type ThemePreference, } from './utils/theme.js';
|
|
104
104
|
export * as motion from './motion.js';
|
|
105
105
|
export type { Rung as MotionRung, Curve as MotionCurve } from './motion.js';
|
|
106
|
-
export declare const VERSION = "0.38.
|
|
106
|
+
export declare const VERSION = "0.38.3";
|
package/dist/index.js
CHANGED
|
@@ -116,4 +116,4 @@ export * as motion from './motion.js';
|
|
|
116
116
|
// ── Version ────────────────────────────────────────────────────────────────
|
|
117
117
|
// Generated from package.json by `pnpm version:sync`. Bump package.json, never
|
|
118
118
|
// this line. The build and the test suite fail when the two disagree.
|
|
119
|
-
export const VERSION = '0.38.
|
|
119
|
+
export const VERSION = '0.38.3';
|
package/package.json
CHANGED