bits-ui 2.16.1 → 2.16.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/bits/time-field/time-field.svelte.d.ts +4 -1
- package/dist/bits/time-field/time-field.svelte.js +17 -0
- package/dist/bits/tooltip/tooltip.svelte.js +10 -4
- package/dist/internal/floating-svelte/use-floating.svelte.js +1 -1
- package/dist/internal/safe-polygon.svelte.d.ts +1 -0
- package/dist/internal/safe-polygon.svelte.js +53 -10
- package/dist/internal/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Updater } from "svelte/store";
|
|
2
2
|
import { Time } from "@internationalized/date";
|
|
3
3
|
import { type WritableBox, DOMContext, type ReadableBoxedValues, type WritableBoxedValues } from "svelte-toolbelt";
|
|
4
|
-
import type { BitsFocusEvent, BitsKeyboardEvent, BitsMouseEvent, RefAttachment, WithRefOpts } from "../../internal/types.js";
|
|
4
|
+
import type { BitsFocusEvent, BitsInputEvent, BitsKeyboardEvent, BitsMouseEvent, RefAttachment, WithRefOpts } from "../../internal/types.js";
|
|
5
5
|
import type { TimeSegmentObj, SegmentPart, HourCycle, TimeValidator, TimeOnInvalid, EditableTimeSegmentPart } from "../../shared/date/types.js";
|
|
6
6
|
import { type TimeFormatter } from "../../internal/date-time/formatter.js";
|
|
7
7
|
import { type Announcer } from "../../internal/date-time/announcer.js";
|
|
@@ -288,6 +288,7 @@ declare class TimeFieldDayPeriodSegmentState {
|
|
|
288
288
|
readonly root: TimeFieldRootState;
|
|
289
289
|
readonly attachment: RefAttachment;
|
|
290
290
|
constructor(opts: TimeFieldDayPeriodSegmentStateOpts, root: TimeFieldRootState);
|
|
291
|
+
onbeforeinput(e: BitsInputEvent): void;
|
|
291
292
|
onkeydown(e: BitsKeyboardEvent): void;
|
|
292
293
|
readonly props: {
|
|
293
294
|
"aria-invalid": "true" | undefined;
|
|
@@ -305,6 +306,7 @@ declare class TimeFieldDayPeriodSegmentState {
|
|
|
305
306
|
"aria-valuenow": number;
|
|
306
307
|
"aria-valuetext": string;
|
|
307
308
|
onkeydown: (e: BitsKeyboardEvent) => void;
|
|
309
|
+
onbeforeinput: (e: BitsInputEvent) => void;
|
|
308
310
|
onclick: (e: BitsMouseEvent) => void;
|
|
309
311
|
role: string;
|
|
310
312
|
contenteditable: string;
|
|
@@ -335,6 +337,7 @@ declare class TimeFieldDayPeriodSegmentState {
|
|
|
335
337
|
"aria-valuenow": number;
|
|
336
338
|
"aria-valuetext": string;
|
|
337
339
|
onkeydown: (e: BitsKeyboardEvent) => void;
|
|
340
|
+
onbeforeinput: (e: BitsInputEvent) => void;
|
|
338
341
|
onclick: (e: BitsMouseEvent) => void;
|
|
339
342
|
role: string;
|
|
340
343
|
spellcheck: boolean;
|
|
@@ -836,6 +836,22 @@ class TimeFieldDayPeriodSegmentState {
|
|
|
836
836
|
this.#announcer = this.root.announcer;
|
|
837
837
|
this.attachment = attachRef(opts.ref, (v) => (this.root.dayPeriodNode = v));
|
|
838
838
|
this.onkeydown = this.onkeydown.bind(this);
|
|
839
|
+
this.onbeforeinput = this.onbeforeinput.bind(this);
|
|
840
|
+
}
|
|
841
|
+
onbeforeinput(e) {
|
|
842
|
+
if (this.root.disabled.current)
|
|
843
|
+
return;
|
|
844
|
+
const data = typeof e.data === "string" ? e.data.trim().toLowerCase() : "";
|
|
845
|
+
if (!data)
|
|
846
|
+
return;
|
|
847
|
+
e.preventDefault();
|
|
848
|
+
if (data === "a" || data === "p") {
|
|
849
|
+
this.root.updateSegment("dayPeriod", () => {
|
|
850
|
+
const next = data === "a" ? "AM" : "PM";
|
|
851
|
+
this.#announcer.announce(next);
|
|
852
|
+
return next;
|
|
853
|
+
});
|
|
854
|
+
}
|
|
839
855
|
}
|
|
840
856
|
onkeydown(e) {
|
|
841
857
|
if (e.ctrlKey || e.metaKey || this.root.disabled.current)
|
|
@@ -894,6 +910,7 @@ class TimeFieldDayPeriodSegmentState {
|
|
|
894
910
|
"aria-valuenow": valueNow,
|
|
895
911
|
"aria-valuetext": valueText,
|
|
896
912
|
onkeydown: this.onkeydown,
|
|
913
|
+
onbeforeinput: this.onbeforeinput,
|
|
897
914
|
onclick: this.root.handleSegmentClick,
|
|
898
915
|
...this.root.getBaseSegmentAttrs("dayPeriod", this.opts.id.current),
|
|
899
916
|
...this.attachment,
|
|
@@ -524,14 +524,19 @@ export class TooltipTriggerState {
|
|
|
524
524
|
const relatedTarget = e.relatedTarget;
|
|
525
525
|
// when moving to a sibling trigger and skip delay is active, don't close —
|
|
526
526
|
// the sibling's enter handler will switch the active trigger instantly.
|
|
527
|
-
// if skipDelayDuration is 0 there's no grace period, so
|
|
528
|
-
//
|
|
529
|
-
if (isElement(relatedTarget)
|
|
527
|
+
// if skipDelayDuration is 0 there's no grace period, so close now and let
|
|
528
|
+
// the sibling wait through the full delay (and re-animate).
|
|
529
|
+
if (isElement(relatedTarget)) {
|
|
530
530
|
for (const record of root.registry.triggers.values()) {
|
|
531
|
-
if (record.node
|
|
531
|
+
if (record.node !== relatedTarget)
|
|
532
|
+
continue;
|
|
533
|
+
if (root.provider.opts.skipDelayDuration.current > 0) {
|
|
532
534
|
this.#hasPointerMoveOpened = false;
|
|
533
535
|
return;
|
|
534
536
|
}
|
|
537
|
+
root.handleClose();
|
|
538
|
+
this.#hasPointerMoveOpened = false;
|
|
539
|
+
return;
|
|
535
540
|
}
|
|
536
541
|
}
|
|
537
542
|
root.onTriggerLeave();
|
|
@@ -606,6 +611,7 @@ export class TooltipContentState {
|
|
|
606
611
|
triggerNode: () => this.root.triggerNode,
|
|
607
612
|
contentNode: () => this.root.contentNode,
|
|
608
613
|
enabled: () => this.root.opts.open.current && !this.root.disableHoverableContent,
|
|
614
|
+
transitIntentTimeout: 180,
|
|
609
615
|
ignoredTargets: () => {
|
|
610
616
|
// only skip closing for sibling triggers when there's a skip-delay grace period;
|
|
611
617
|
// with skipDelayDuration=0 the close+reopen is intentional (full delay + re-animation)
|
|
@@ -105,7 +105,7 @@ export function useFloating(options) {
|
|
|
105
105
|
whileElementsMountedCleanup = whileElementsMountedOption(reference.current, floating.current, update);
|
|
106
106
|
}
|
|
107
107
|
function reset() {
|
|
108
|
-
if (!openOption) {
|
|
108
|
+
if (!openOption && floating.current === null) {
|
|
109
109
|
isPositioned = false;
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -5,6 +5,7 @@ export interface SafePolygonOptions {
|
|
|
5
5
|
contentNode: Getter<HTMLElement | null>;
|
|
6
6
|
onPointerExit: () => void;
|
|
7
7
|
buffer?: number;
|
|
8
|
+
transitIntentTimeout?: number;
|
|
8
9
|
/** nodes that should not trigger a close when they become the relatedTarget on trigger leave (e.g. sibling triggers in singleton mode) */
|
|
9
10
|
ignoredTargets?: Getter<HTMLElement[]>;
|
|
10
11
|
}
|
|
@@ -42,11 +42,15 @@ function getSide(triggerRect, contentRect) {
|
|
|
42
42
|
export class SafePolygon {
|
|
43
43
|
#opts;
|
|
44
44
|
#buffer;
|
|
45
|
+
#transitIntentTimeout;
|
|
45
46
|
// tracks the cursor position when leaving trigger or content
|
|
46
47
|
#exitPoint = null;
|
|
47
48
|
// tracks what we're moving toward: "content" when leaving trigger, "trigger" when leaving content
|
|
48
49
|
#exitTarget = null;
|
|
50
|
+
#transitTargets = [];
|
|
51
|
+
#trackedTriggerNode = null;
|
|
49
52
|
#leaveFallbackRafId = null;
|
|
53
|
+
#transitIntentTimeoutId = null;
|
|
50
54
|
#cancelLeaveFallback() {
|
|
51
55
|
if (this.#leaveFallbackRafId !== null) {
|
|
52
56
|
cancelAnimationFrame(this.#leaveFallbackRafId);
|
|
@@ -63,14 +67,42 @@ export class SafePolygon {
|
|
|
63
67
|
this.#opts.onPointerExit();
|
|
64
68
|
});
|
|
65
69
|
}
|
|
70
|
+
#cancelTransitIntentTimeout() {
|
|
71
|
+
if (this.#transitIntentTimeoutId !== null) {
|
|
72
|
+
clearTimeout(this.#transitIntentTimeoutId);
|
|
73
|
+
this.#transitIntentTimeoutId = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
#scheduleTransitIntentTimeout() {
|
|
77
|
+
if (this.#transitIntentTimeout === null)
|
|
78
|
+
return;
|
|
79
|
+
this.#cancelTransitIntentTimeout();
|
|
80
|
+
this.#transitIntentTimeoutId = window.setTimeout(() => {
|
|
81
|
+
this.#transitIntentTimeoutId = null;
|
|
82
|
+
if (!this.#exitPoint || !this.#exitTarget)
|
|
83
|
+
return;
|
|
84
|
+
this.#clearTracking();
|
|
85
|
+
this.#opts.onPointerExit();
|
|
86
|
+
}, this.#transitIntentTimeout);
|
|
87
|
+
}
|
|
66
88
|
constructor(opts) {
|
|
67
89
|
this.#opts = opts;
|
|
68
90
|
this.#buffer = opts.buffer ?? 1;
|
|
91
|
+
const transitIntentTimeout = opts.transitIntentTimeout;
|
|
92
|
+
this.#transitIntentTimeout =
|
|
93
|
+
typeof transitIntentTimeout === "number" && transitIntentTimeout > 0
|
|
94
|
+
? transitIntentTimeout
|
|
95
|
+
: null;
|
|
69
96
|
watch([opts.triggerNode, opts.contentNode, opts.enabled], ([triggerNode, contentNode, enabled]) => {
|
|
70
97
|
if (!triggerNode || !contentNode || !enabled) {
|
|
98
|
+
this.#trackedTriggerNode = null;
|
|
71
99
|
this.#clearTracking();
|
|
72
100
|
return;
|
|
73
101
|
}
|
|
102
|
+
if (this.#trackedTriggerNode && this.#trackedTriggerNode !== triggerNode) {
|
|
103
|
+
this.#clearTracking();
|
|
104
|
+
}
|
|
105
|
+
this.#trackedTriggerNode = triggerNode;
|
|
74
106
|
const doc = getDocument(triggerNode);
|
|
75
107
|
const handlePointerMove = (e) => {
|
|
76
108
|
this.#onPointerMove([e.clientX, e.clientY], triggerNode, contentNode);
|
|
@@ -89,16 +121,13 @@ export class SafePolygon {
|
|
|
89
121
|
ignoredTargets.some((n) => n === target || n.contains(target))) {
|
|
90
122
|
return;
|
|
91
123
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.#opts.onPointerExit();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
124
|
+
this.#transitTargets =
|
|
125
|
+
isElement(target) && ignoredTargets.length > 0
|
|
126
|
+
? ignoredTargets.filter((n) => target.contains(n))
|
|
127
|
+
: [];
|
|
128
|
+
// for unrelated elements, defer close decisions to pointer geometry checks.
|
|
129
|
+
// this allows the cursor to pass through intermediate elements on the way
|
|
130
|
+
// to content without immediately closing.
|
|
102
131
|
this.#exitPoint = [e.clientX, e.clientY];
|
|
103
132
|
this.#exitTarget = "content";
|
|
104
133
|
this.#scheduleLeaveFallback();
|
|
@@ -140,6 +169,7 @@ export class SafePolygon {
|
|
|
140
169
|
if (!this.#exitPoint || !this.#exitTarget)
|
|
141
170
|
return;
|
|
142
171
|
this.#cancelLeaveFallback();
|
|
172
|
+
this.#scheduleTransitIntentTimeout();
|
|
143
173
|
const triggerRect = triggerNode.getBoundingClientRect();
|
|
144
174
|
const contentRect = contentNode.getBoundingClientRect();
|
|
145
175
|
// check if pointer reached the target
|
|
@@ -151,6 +181,17 @@ export class SafePolygon {
|
|
|
151
181
|
this.#clearTracking();
|
|
152
182
|
return;
|
|
153
183
|
}
|
|
184
|
+
if (this.#exitTarget === "content" && this.#transitTargets.length > 0) {
|
|
185
|
+
for (const transitTarget of this.#transitTargets) {
|
|
186
|
+
const transitRect = transitTarget.getBoundingClientRect();
|
|
187
|
+
if (isInsideRect(clientPoint, transitRect))
|
|
188
|
+
return;
|
|
189
|
+
const transitSide = getSide(triggerRect, transitRect);
|
|
190
|
+
const transitCorridor = this.#getCorridorPolygon(triggerRect, transitRect, transitSide);
|
|
191
|
+
if (transitCorridor && isPointInPolygon(clientPoint, transitCorridor))
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
154
195
|
// check if pointer is in the rectangular corridor between trigger and content
|
|
155
196
|
const side = getSide(triggerRect, contentRect);
|
|
156
197
|
const corridorPoly = this.#getCorridorPolygon(triggerRect, contentRect, side);
|
|
@@ -170,7 +211,9 @@ export class SafePolygon {
|
|
|
170
211
|
#clearTracking() {
|
|
171
212
|
this.#exitPoint = null;
|
|
172
213
|
this.#exitTarget = null;
|
|
214
|
+
this.#transitTargets = [];
|
|
173
215
|
this.#cancelLeaveFallback();
|
|
216
|
+
this.#cancelTransitIntentTimeout();
|
|
174
217
|
}
|
|
175
218
|
/**
|
|
176
219
|
* Creates a rectangular corridor between trigger and content
|
package/dist/internal/types.d.ts
CHANGED
|
@@ -90,4 +90,5 @@ export type BitsPointerEvent<T extends HTMLElement = HTMLElement> = BitsEvent<Po
|
|
|
90
90
|
export type BitsKeyboardEvent<T extends HTMLElement = HTMLElement> = BitsEvent<KeyboardEvent, T>;
|
|
91
91
|
export type BitsMouseEvent<T extends HTMLElement = HTMLElement> = BitsEvent<MouseEvent, T>;
|
|
92
92
|
export type BitsFocusEvent<T extends HTMLElement = HTMLElement> = BitsEvent<FocusEvent, T>;
|
|
93
|
+
export type BitsInputEvent<T extends HTMLElement = HTMLElement> = BitsEvent<InputEvent, T>;
|
|
93
94
|
export type RefAttachment<T extends HTMLElement = HTMLElement> = ReturnType<typeof attachRef<T>>;
|