@astryxdesign/core 0.7.7-canary.36f8ea1 → 0.7.7-canary.550ff21
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/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +8 -1
- package/dist/DateInput/MonthScroller.d.ts.map +1 -1
- package/dist/DateInput/MonthScroller.js +72 -2
- package/dist/Field/PanelSearchInput.d.ts.map +1 -1
- package/dist/Field/PanelSearchInput.js +8 -1
- package/dist/Field/textEntryTraits.d.ts +6 -6
- package/dist/Field/textEntryTraits.js +6 -6
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +20 -9
- package/dist/ProgressBar/ProgressBarTouch.d.ts +29 -0
- package/dist/ProgressBar/ProgressBarTouch.d.ts.map +1 -0
- package/dist/ProgressBar/ProgressBarTouch.js +91 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +9 -1
- package/dist/astryx.css +7 -0
- package/dist/gestures/GestureArena.d.ts +21 -2
- package/dist/gestures/GestureArena.d.ts.map +1 -1
- package/dist/gestures/GestureArena.js +19 -2
- package/dist/gestures/GestureHost.d.ts +51 -1
- package/dist/gestures/GestureHost.d.ts.map +1 -1
- package/dist/gestures/GestureHost.js +148 -7
- package/dist/motion/animator.d.ts.map +1 -1
- package/dist/motion/animator.js +63 -5
- package/dist/motion/paging.d.ts +58 -0
- package/dist/motion/paging.d.ts.map +1 -0
- package/dist/motion/paging.js +128 -0
- package/dist/motion/uikit/index.d.ts +4 -3
- package/dist/motion/uikit/index.d.ts.map +1 -1
- package/dist/motion/uikit/index.js +4 -3
- package/dist/motion/uikit/paging.d.ts +160 -0
- package/dist/motion/uikit/paging.d.ts.map +1 -0
- package/dist/motion/uikit/paging.js +162 -0
- package/dist/motion/uikit/progressBar.d.ts +59 -0
- package/dist/motion/uikit/progressBar.d.ts.map +1 -0
- package/dist/motion/uikit/progressBar.js +82 -0
- package/dist/motion/uikit/scroll.d.ts +0 -19
- package/dist/motion/uikit/scroll.d.ts.map +1 -1
- package/dist/motion/uikit/scroll.js +2 -21
- package/dist/utils/selectionLock.d.ts +43 -0
- package/dist/utils/selectionLock.d.ts.map +1 -1
- package/dist/utils/selectionLock.js +394 -6
- package/package.json +4 -4
- package/src/CommandPalette/CommandPaletteInput.tsx +6 -0
- package/src/DateInput/MonthScroller.tsx +63 -5
- package/src/Field/PanelSearchInput.tsx +6 -0
- package/src/Field/textEntryTraits.ts +6 -6
- package/src/ProgressBar/ProgressBar.doc.mjs +3 -3
- package/src/ProgressBar/ProgressBar.tsx +82 -11
- package/src/ProgressBar/ProgressBarTouch.ts +109 -0
- package/src/Typeahead/BaseTypeahead.tsx +7 -0
- package/src/gestures/GestureArena.ts +44 -4
- package/src/gestures/GestureHost.ts +184 -7
- package/src/gestures/README.md +19 -3
- package/src/motion/MEASUREMENTS.md +8 -1
- package/src/motion/README.md +6 -3
- package/src/motion/animator.ts +78 -6
- package/src/motion/paging.ts +138 -0
- package/src/motion/uikit/index.ts +4 -3
- package/src/motion/uikit/paging.ts +162 -0
- package/src/motion/uikit/progressBar.ts +82 -0
- package/src/motion/uikit/scroll.ts +2 -18
- package/src/utils/selectionLock.ts +501 -6
- package/dist/motion/selectPage.d.ts +0 -26
- package/dist/motion/selectPage.d.ts.map +0 -1
- package/dist/motion/selectPage.js +0 -55
- package/src/motion/selectPage.ts +0 -55
|
@@ -59,25 +59,6 @@ export declare const scroll: {
|
|
|
59
59
|
readonly duration: 0.5;
|
|
60
60
|
readonly bounce: 0;
|
|
61
61
|
};
|
|
62
|
-
readonly paging: {
|
|
63
|
-
/**
|
|
64
|
-
* Deceleration rate a paging release is projected with to pick the next
|
|
65
|
-
* page.
|
|
66
|
-
* @source GUESS — UIScrollView paging behaves like a fast scroll view;
|
|
67
|
-
* the rate it uses internally is not documented
|
|
68
|
-
* @measure M-PG1
|
|
69
|
-
*/
|
|
70
|
-
readonly projectionRate: 0.99;
|
|
71
|
-
/**
|
|
72
|
-
* The spring that carries a paging scroll view onto its page.
|
|
73
|
-
* @source GUESS — SwiftUI `.smooth` default until measured
|
|
74
|
-
* @measure M-PG1
|
|
75
|
-
*/
|
|
76
|
-
readonly snap: {
|
|
77
|
-
readonly duration: 0.5;
|
|
78
|
-
readonly bounce: 0;
|
|
79
|
-
};
|
|
80
|
-
};
|
|
81
62
|
/**
|
|
82
63
|
* `delaysContentTouches`: how long a scroll view holds a touch-down from
|
|
83
64
|
* its content while it decides whether the touch is a scroll (no highlight
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scroll.d.ts","sourceRoot":"","sources":["../../../src/motion/uikit/scroll.ts"],"names":[],"mappings":"AAyBA,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C,eAAO,MAAM,MAAM;;QAEf;;;;;;;;;;;;;;;;WAgBG;;QAEH;;;;;;;WAOG;;;;QAIH;;;;;;;;;;;WAWG;;;IAGL;;;;;;;;;;OAUG
|
|
1
|
+
{"version":3,"file":"scroll.d.ts","sourceRoot":"","sources":["../../../src/motion/uikit/scroll.ts"],"names":[],"mappings":"AAyBA,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C,eAAO,MAAM,MAAM;;QAEf;;;;;;;;;;;;;;;;WAgBG;;QAEH;;;;;;;WAOG;;;;QAIH;;;;;;;;;;;WAWG;;;IAGL;;;;;;;;;;OAUG;;;;;IAEH;;;;;;;;;;OAUG;;IAEH;;;;;;;;;;;;;;OAcG;;CAEK,CAAC;AAEX;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAK1C"}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* @file scroll.ts
|
|
5
5
|
* @input Nothing; data only
|
|
6
6
|
* @output Exports `scroll`, the UIScrollView constants: deceleration rates,
|
|
7
|
-
* the rubber-band coefficient, the bounce-back spring
|
|
8
|
-
*
|
|
7
|
+
* the rubber-band coefficient, the bounce-back spring and the content-touch
|
|
8
|
+
* delay
|
|
9
9
|
* @position The scroll domain of the UIKit constants table (motion/uikit);
|
|
10
10
|
* read by the motion math and components
|
|
11
11
|
*
|
|
@@ -84,25 +84,6 @@ export const scroll = {
|
|
|
84
84
|
duration: 0.5,
|
|
85
85
|
bounce: 0
|
|
86
86
|
},
|
|
87
|
-
paging: {
|
|
88
|
-
/**
|
|
89
|
-
* Deceleration rate a paging release is projected with to pick the next
|
|
90
|
-
* page.
|
|
91
|
-
* @source GUESS — UIScrollView paging behaves like a fast scroll view;
|
|
92
|
-
* the rate it uses internally is not documented
|
|
93
|
-
* @measure M-PG1
|
|
94
|
-
*/
|
|
95
|
-
projectionRate: 0.99,
|
|
96
|
-
/**
|
|
97
|
-
* The spring that carries a paging scroll view onto its page.
|
|
98
|
-
* @source GUESS — SwiftUI `.smooth` default until measured
|
|
99
|
-
* @measure M-PG1
|
|
100
|
-
*/
|
|
101
|
-
snap: {
|
|
102
|
-
duration: 0.5,
|
|
103
|
-
bounce: 0
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
87
|
/**
|
|
107
88
|
* `delaysContentTouches`: how long a scroll view holds a touch-down from
|
|
108
89
|
* its content while it decides whether the touch is a scroll (no highlight
|
|
@@ -10,4 +10,47 @@ export declare function locksSelectionFor(pointerType: string): boolean;
|
|
|
10
10
|
* any holder holds. Idempotent release.
|
|
11
11
|
*/
|
|
12
12
|
export declare function lockSelection(elements: Iterable<Element>, target: EventTarget | null): () => void;
|
|
13
|
+
/** How a drag's selection hold ends: at a lift, or at a cancel. */
|
|
14
|
+
export interface DragSelectionReleaseOptions {
|
|
15
|
+
/**
|
|
16
|
+
* The press was cancelled (a pointercancel, a blur, a detach), so no
|
|
17
|
+
* mouseup or click follows: the hold ends at once. Called again after a
|
|
18
|
+
* lift, it ends the hold's last task at once too.
|
|
19
|
+
*/
|
|
20
|
+
isCancel?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Refuse a new selection while a mouse or pen drag that a recognizer claimed
|
|
24
|
+
* runs, until the returned function runs. `kept` is the page's selection
|
|
25
|
+
* when the press began, and it stays. Anything that replaces it while the
|
|
26
|
+
* drag runs is put back to it. That includes the press's own collapse and a
|
|
27
|
+
* selection the drag started before its claim. So the drag takes no new
|
|
28
|
+
* selection and never clears an existing one. `elements` refuse a
|
|
29
|
+
* selection with inline styles, on every engine. The document refuses one
|
|
30
|
+
* too, because WebKit extends a drag's selection over text outside the
|
|
31
|
+
* elements without another `selectstart`. So a `selectstart` anywhere is
|
|
32
|
+
* refused, and the selection is put back. A `selectstart` does not leave its
|
|
33
|
+
* shadow tree. So the document refuses only one that starts outside every
|
|
34
|
+
* shadow tree, and a claiming element inside one refuses its own. The
|
|
35
|
+
* press's own `mouseup` and `click` come after its `pointerup` in the same
|
|
36
|
+
* task, and WebKit settles the drag's selection there. So the document's
|
|
37
|
+
* hold ends a task after a lift. A cancel ends it at once, because nothing
|
|
38
|
+
* follows a cancel. `onReleased` runs once the document's hold has ended.
|
|
39
|
+
* The page listeners exist only during a press. Idempotent.
|
|
40
|
+
*/
|
|
41
|
+
export declare function lockDragSelection(elements: Iterable<Element>, view: Window | null | undefined, kept: ReadonlyArray<AbstractRange>, onReleased?: () => void): (options?: DragSelectionReleaseOptions) => void;
|
|
42
|
+
/**
|
|
43
|
+
* The page's selection now, its ranges copied; none for a collapsed or empty
|
|
44
|
+
* one. A drag's hold keeps what this read when the press began.
|
|
45
|
+
*/
|
|
46
|
+
export declare function selectionRangesOf(view: Window | null | undefined): ReadonlyArray<AbstractRange>;
|
|
47
|
+
/**
|
|
48
|
+
* Whether a press at (`x`, `y`) lands on text a drag would select. That is a
|
|
49
|
+
* text field, or a character of non-empty text whose element allows a
|
|
50
|
+
* selection. A native button's label, slotted or not, is not selectable text,
|
|
51
|
+
* because the engine starts no selection from a drag inside a button. A point
|
|
52
|
+
* beside text, which a caret query snaps to the nearest character, lands on
|
|
53
|
+
* none. Where the engine has no caret query, nothing is selectable text.
|
|
54
|
+
*/
|
|
55
|
+
export declare function startsOnSelectableText(doc: Document, x: number, y: number, target: EventTarget | null): boolean;
|
|
13
56
|
//# sourceMappingURL=selectionLock.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selectionLock.d.ts","sourceRoot":"","sources":["../../src/utils/selectionLock.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"selectionLock.d.ts","sourceRoot":"","sources":["../../src/utils/selectionLock.ts"],"names":[],"mappings":"AAoDA;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE9D;AAWD;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,EAC3B,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,MAAM,IAAI,CA4BZ;AAED,mEAAmE;AACnE,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,EAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC/B,IAAI,EAAE,aAAa,CAAC,aAAa,CAAC,EAClC,UAAU,CAAC,EAAE,MAAM,IAAI,GACtB,CAAC,OAAO,CAAC,EAAE,2BAA2B,KAAK,IAAI,CA+EjD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9B,aAAa,CAAC,aAAa,CAAC,CAiB9B;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,QAAQ,EACb,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,OAAO,CA+CT"}
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file selectionLock.ts
|
|
5
5
|
* @input The elements a held touch contact presses, and the touch's target
|
|
6
|
-
* @output lockSelection
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* @output lockSelection and locksSelectionFor give a held finger's lock.
|
|
7
|
+
* lockDragSelection gives a claimed mouse or pen drag's hold, which keeps
|
|
8
|
+
* the page's selection. startsOnSelectableText and selectionRangesOf are
|
|
9
|
+
* what a mouse or pen press reads.
|
|
10
|
+
* @position Recognizers whose hold does something bring lockSelection; the
|
|
11
|
+
* gesture host imports the drag hold and the press reads.
|
|
11
12
|
*
|
|
12
13
|
* SYNC: When modified, update GestureHost.ts and utils/pressFeedback.ts; the
|
|
13
14
|
* iOS query matches utils/touchPresentation.ts.
|
|
@@ -27,7 +28,8 @@
|
|
|
27
28
|
//
|
|
28
29
|
// A mouse never takes the lock, so its released behavior stays (AST-017).
|
|
29
30
|
|
|
30
|
-
import { containsAlongFlatTree } from "./flatTree.js";
|
|
31
|
+
import { containsAlongFlatTree, parentAcrossShadow } from "./flatTree.js";
|
|
32
|
+
import { subscribeInput } from "./inputStream.js";
|
|
31
33
|
import { isIosWebKit } from "./touchQueries.js";
|
|
32
34
|
|
|
33
35
|
/** What a held finger must not start on a control or a cell. */
|
|
@@ -77,6 +79,392 @@ export function lockSelection(elements, target) {
|
|
|
77
79
|
}
|
|
78
80
|
};
|
|
79
81
|
}
|
|
82
|
+
return lockStyles(set);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** How a drag's selection hold ends: at a lift, or at a cancel. */
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Refuse a new selection while a mouse or pen drag that a recognizer claimed
|
|
89
|
+
* runs, until the returned function runs. `kept` is the page's selection
|
|
90
|
+
* when the press began, and it stays. Anything that replaces it while the
|
|
91
|
+
* drag runs is put back to it. That includes the press's own collapse and a
|
|
92
|
+
* selection the drag started before its claim. So the drag takes no new
|
|
93
|
+
* selection and never clears an existing one. `elements` refuse a
|
|
94
|
+
* selection with inline styles, on every engine. The document refuses one
|
|
95
|
+
* too, because WebKit extends a drag's selection over text outside the
|
|
96
|
+
* elements without another `selectstart`. So a `selectstart` anywhere is
|
|
97
|
+
* refused, and the selection is put back. A `selectstart` does not leave its
|
|
98
|
+
* shadow tree. So the document refuses only one that starts outside every
|
|
99
|
+
* shadow tree, and a claiming element inside one refuses its own. The
|
|
100
|
+
* press's own `mouseup` and `click` come after its `pointerup` in the same
|
|
101
|
+
* task, and WebKit settles the drag's selection there. So the document's
|
|
102
|
+
* hold ends a task after a lift. A cancel ends it at once, because nothing
|
|
103
|
+
* follows a cancel. `onReleased` runs once the document's hold has ended.
|
|
104
|
+
* The page listeners exist only during a press. Idempotent.
|
|
105
|
+
*/
|
|
106
|
+
export function lockDragSelection(elements, view, kept, onReleased) {
|
|
107
|
+
const doc = view?.document ?? null;
|
|
108
|
+
const roots = shadowRootsOfRanges(kept);
|
|
109
|
+
const restore = () => {
|
|
110
|
+
const selection = view?.getSelection();
|
|
111
|
+
// The focused field keeps its own selection (a drag never starts in one).
|
|
112
|
+
if (selection == null || isTextEntry(deepActiveElement(doc))) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const held = heldRanges(selection, roots);
|
|
116
|
+
if (kept.length === 0) {
|
|
117
|
+
if (held.some(range => !range.collapsed)) {
|
|
118
|
+
selection.removeAllRanges();
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// A render can remove what the kept selection held: then there is
|
|
123
|
+
// nothing to put back.
|
|
124
|
+
if (sameRanges(held, kept) || !kept.every(isValidRange)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
selectRanges(selection, kept);
|
|
128
|
+
};
|
|
129
|
+
restore();
|
|
130
|
+
const set = new Set(elements);
|
|
131
|
+
const refuse = event => {
|
|
132
|
+
if (!isTextEntry(event.target)) {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
for (const element of set) {
|
|
137
|
+
element.addEventListener('selectstart', refuse);
|
|
138
|
+
}
|
|
139
|
+
const stopDocument = view == null ? [] : [subscribeInput(view, 'selectstart', refuse, {
|
|
140
|
+
cancels: true
|
|
141
|
+
}), subscribeInput(view, 'selectionchange', restore)];
|
|
142
|
+
const unlockStyles = lockStyles(set);
|
|
143
|
+
let stage = 'held';
|
|
144
|
+
let timer = null;
|
|
145
|
+
const releaseDocument = () => {
|
|
146
|
+
if (stage === 'done') {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
stage = 'done';
|
|
150
|
+
if (timer != null) {
|
|
151
|
+
view?.clearTimeout(timer);
|
|
152
|
+
timer = null;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
restore();
|
|
156
|
+
} finally {
|
|
157
|
+
for (const stop of stopDocument) {
|
|
158
|
+
stop();
|
|
159
|
+
}
|
|
160
|
+
onReleased?.();
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
return ({
|
|
164
|
+
isCancel = false
|
|
165
|
+
} = {}) => {
|
|
166
|
+
if (stage === 'held') {
|
|
167
|
+
stage = 'lifted';
|
|
168
|
+
for (const element of set) {
|
|
169
|
+
element.removeEventListener('selectstart', refuse);
|
|
170
|
+
}
|
|
171
|
+
unlockStyles();
|
|
172
|
+
// A lift's own mouseup and click follow in its task and must still
|
|
173
|
+
// meet the hold; nothing follows a cancel, which ends it at once.
|
|
174
|
+
if (view == null || isCancel) {
|
|
175
|
+
releaseDocument();
|
|
176
|
+
} else {
|
|
177
|
+
timer = view.setTimeout(releaseDocument, 0);
|
|
178
|
+
}
|
|
179
|
+
} else if (stage === 'lifted' && isCancel) {
|
|
180
|
+
releaseDocument();
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The page's selection now, its ranges copied; none for a collapsed or empty
|
|
187
|
+
* one. A drag's hold keeps what this read when the press began.
|
|
188
|
+
*/
|
|
189
|
+
export function selectionRangesOf(view) {
|
|
190
|
+
const selection = view?.getSelection();
|
|
191
|
+
if (selection == null) {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
const composed = composedRangesOf(selection, []);
|
|
195
|
+
if (composed != null) {
|
|
196
|
+
return composed.filter(range => !range.collapsed).map(liveWhereItCan);
|
|
197
|
+
}
|
|
198
|
+
if (selection.isCollapsed) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
const ranges = [];
|
|
202
|
+
for (let index = 0; index < selection.rangeCount; index++) {
|
|
203
|
+
ranges.push(selection.getRangeAt(index).cloneRange());
|
|
204
|
+
}
|
|
205
|
+
return ranges;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Whether a press at (`x`, `y`) lands on text a drag would select. That is a
|
|
210
|
+
* text field, or a character of non-empty text whose element allows a
|
|
211
|
+
* selection. A native button's label, slotted or not, is not selectable text,
|
|
212
|
+
* because the engine starts no selection from a drag inside a button. A point
|
|
213
|
+
* beside text, which a caret query snaps to the nearest character, lands on
|
|
214
|
+
* none. Where the engine has no caret query, nothing is selectable text.
|
|
215
|
+
*/
|
|
216
|
+
export function startsOnSelectableText(doc, x, y, target) {
|
|
217
|
+
if (isTextEntry(target)) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
const caret = caretAt(doc, x, y, [...ownShadowRoot(target), ...openShadowRootsOf(target)]);
|
|
221
|
+
if (caret == null) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
const {
|
|
225
|
+
node,
|
|
226
|
+
offset
|
|
227
|
+
} = caret;
|
|
228
|
+
if (node.nodeType !== 3 || (node.textContent ?? '').trim() === '') {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
// Text directly under a shadow root takes its host's style.
|
|
232
|
+
const element = node.parentElement ?? (node.parentNode?.nodeType === 11 ? node.parentNode.host : null);
|
|
233
|
+
if (element == null || !allowsSelection(element) || isInButton(node)) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
const length = (node.textContent ?? '').length;
|
|
237
|
+
const range = doc.createRange();
|
|
238
|
+
if (typeof range.getClientRects !== 'function') {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
for (const start of [offset - 1, offset]) {
|
|
242
|
+
if (start < 0 || start >= length) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
range.setStart(node, start);
|
|
246
|
+
range.setEnd(node, start + 1);
|
|
247
|
+
for (const rect of Array.from(range.getClientRects())) {
|
|
248
|
+
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Whether `node` is inside a native button, along the flat tree. */
|
|
257
|
+
function isInButton(node) {
|
|
258
|
+
for (let at = node; at != null; at = parentAcrossShadow(at)) {
|
|
259
|
+
if (at.nodeType === 1 && at.localName === 'button') {
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The caret position the engine puts at a point, by either query. */
|
|
267
|
+
function caretAt(doc, x, y, shadowRoots) {
|
|
268
|
+
const queries = doc;
|
|
269
|
+
if (typeof queries.caretPositionFromPoint === 'function') {
|
|
270
|
+
// Without the roots, a point over shadow text answers at its host.
|
|
271
|
+
const position = shadowRoots.length > 0 ? queries.caretPositionFromPoint(x, y, {
|
|
272
|
+
shadowRoots: [...shadowRoots]
|
|
273
|
+
}) : queries.caretPositionFromPoint(x, y);
|
|
274
|
+
return position == null ? null : {
|
|
275
|
+
node: position.offsetNode,
|
|
276
|
+
offset: position.offset
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (typeof queries.caretRangeFromPoint === 'function') {
|
|
280
|
+
const range = queries.caretRangeFromPoint(x, y);
|
|
281
|
+
return range == null ? null : {
|
|
282
|
+
node: range.startContainer,
|
|
283
|
+
offset: range.startOffset
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Whether an element's text can be selected (`user-select` is not none). */
|
|
290
|
+
function allowsSelection(element) {
|
|
291
|
+
const style = element.ownerDocument.defaultView?.getComputedStyle(element);
|
|
292
|
+
if (style == null) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
return style.getPropertyValue('user-select') !== 'none' && style.getPropertyValue('-webkit-user-select') !== 'none';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The ranges `selection` holds, with boundaries inside `shadowRoots` where the
|
|
300
|
+
* engine reads composed ranges.
|
|
301
|
+
*/
|
|
302
|
+
function heldRanges(selection, shadowRoots) {
|
|
303
|
+
const composed = composedRangesOf(selection, shadowRoots);
|
|
304
|
+
if (composed != null) {
|
|
305
|
+
return composed;
|
|
306
|
+
}
|
|
307
|
+
const ranges = [];
|
|
308
|
+
for (let index = 0; index < selection.rangeCount; index++) {
|
|
309
|
+
ranges.push(selection.getRangeAt(index));
|
|
310
|
+
}
|
|
311
|
+
return ranges;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* A live copy of `range` when both boundaries share a root, so it follows
|
|
316
|
+
* DOM changes. A range across roots stays static: no live range holds it.
|
|
317
|
+
*/
|
|
318
|
+
function liveWhereItCan(range) {
|
|
319
|
+
const live = range.startContainer.ownerDocument?.createRange();
|
|
320
|
+
if (live == null || range.startContainer.getRootNode() !== range.endContainer.getRootNode()) {
|
|
321
|
+
return range;
|
|
322
|
+
}
|
|
323
|
+
live.setStart(range.startContainer, range.startOffset);
|
|
324
|
+
live.setEnd(range.endContainer, range.endOffset);
|
|
325
|
+
return live;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Whether both of `range`'s boundaries still exist in the document. */
|
|
329
|
+
function isValidRange(range) {
|
|
330
|
+
return isValidBoundary(range.startContainer, range.startOffset) && isValidBoundary(range.endContainer, range.endOffset);
|
|
331
|
+
}
|
|
332
|
+
function isValidBoundary(node, offset) {
|
|
333
|
+
const length = node.nodeType === 3 || node.nodeType === 4 || node.nodeType === 8 ? node.length : node.childNodes.length;
|
|
334
|
+
return node.isConnected && offset >= 0 && offset <= length;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Whether two range lists have the same boundaries, in order. */
|
|
338
|
+
function sameRanges(held, kept) {
|
|
339
|
+
return held.length === kept.length && kept.every((range, index) => {
|
|
340
|
+
const other = held[index];
|
|
341
|
+
return other.startContainer === range.startContainer && other.startOffset === range.startOffset && other.endContainer === range.endContainer && other.endOffset === range.endOffset;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Put `ranges` back as the selection. One range goes through
|
|
347
|
+
* `setBaseAndExtent`, which also reaches inside a shadow root.
|
|
348
|
+
*/
|
|
349
|
+
function selectRanges(selection, ranges) {
|
|
350
|
+
const [only] = ranges;
|
|
351
|
+
if (ranges.length === 1 && typeof selection.setBaseAndExtent === 'function') {
|
|
352
|
+
selection.setBaseAndExtent(only.startContainer, only.startOffset, only.endContainer, only.endOffset);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
selection.removeAllRanges();
|
|
356
|
+
for (const range of ranges) {
|
|
357
|
+
const copy = range.startContainer.ownerDocument?.createRange();
|
|
358
|
+
if (copy == null) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
copy.setStart(range.startContainer, range.startOffset);
|
|
362
|
+
copy.setEnd(range.endContainer, range.endOffset);
|
|
363
|
+
selection.addRange(copy);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The selection's composed ranges, reaching into each open shadow root a
|
|
369
|
+
* boundary lands on; null where the engine has no `getComposedRanges`.
|
|
370
|
+
* Without the roots, a selection inside a shadow root reads as one around
|
|
371
|
+
* its host.
|
|
372
|
+
*/
|
|
373
|
+
function composedRangesOf(selection, known) {
|
|
374
|
+
const query = selection.getComposedRanges;
|
|
375
|
+
if (typeof query !== 'function') {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
// Engines take the roots as a dictionary, or, in older releases, as
|
|
379
|
+
// arguments.
|
|
380
|
+
const read = roots => {
|
|
381
|
+
try {
|
|
382
|
+
return query.call(selection, {
|
|
383
|
+
shadowRoots: roots
|
|
384
|
+
});
|
|
385
|
+
} catch {
|
|
386
|
+
try {
|
|
387
|
+
return query.call(selection, ...roots);
|
|
388
|
+
} catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
const roots = [...known];
|
|
394
|
+
for (;;) {
|
|
395
|
+
const ranges = read([...roots]);
|
|
396
|
+
if (ranges == null) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
const found = new Set();
|
|
400
|
+
for (const range of ranges) {
|
|
401
|
+
for (const [node, offset] of [[range.startContainer, range.startOffset], [range.endContainer, range.endOffset]]) {
|
|
402
|
+
for (const child of [node.childNodes[offset], node.childNodes[offset - 1]]) {
|
|
403
|
+
const root = child?.nodeType === 1 ? child.shadowRoot : null;
|
|
404
|
+
if (root != null && !roots.includes(root)) {
|
|
405
|
+
found.add(root);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (found.size === 0) {
|
|
411
|
+
return ranges;
|
|
412
|
+
}
|
|
413
|
+
roots.push(...found);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** The open shadow roots on the root chains of `ranges`' boundaries. */
|
|
418
|
+
function shadowRootsOfRanges(ranges) {
|
|
419
|
+
const roots = new Set();
|
|
420
|
+
for (const range of ranges) {
|
|
421
|
+
for (const root of [...openShadowRootsOf(range.startContainer), ...openShadowRootsOf(range.endContainer)]) {
|
|
422
|
+
roots.add(root);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return [...roots];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* The open shadow root `target` hosts, if any. A press on text directly under
|
|
430
|
+
* a shadow root targets its host.
|
|
431
|
+
*/
|
|
432
|
+
function ownShadowRoot(target) {
|
|
433
|
+
const root = target?.nodeType === 1 ? target.shadowRoot : null;
|
|
434
|
+
return root == null ? [] : [root];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** The open shadow roots on `target`'s root chain, innermost first. */
|
|
438
|
+
function openShadowRootsOf(target) {
|
|
439
|
+
const roots = [];
|
|
440
|
+
let root = target != null && typeof target.nodeType === 'number' ? target.getRootNode() : null;
|
|
441
|
+
while (root != null && root.nodeType === 11) {
|
|
442
|
+
const host = root.host;
|
|
443
|
+
if (host == null) {
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
if (root.mode === 'open') {
|
|
447
|
+
roots.push(root);
|
|
448
|
+
}
|
|
449
|
+
root = host.getRootNode();
|
|
450
|
+
}
|
|
451
|
+
return roots;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** The focused element, inside open shadow roots too. */
|
|
455
|
+
function deepActiveElement(doc) {
|
|
456
|
+
let active = doc?.activeElement ?? null;
|
|
457
|
+
while (active?.shadowRoot?.activeElement != null) {
|
|
458
|
+
active = active.shadowRoot.activeElement;
|
|
459
|
+
}
|
|
460
|
+
return active;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* The selection-lock styles on `set`, refcounted per element; idempotent
|
|
465
|
+
* release.
|
|
466
|
+
*/
|
|
467
|
+
function lockStyles(set) {
|
|
80
468
|
const locked = [];
|
|
81
469
|
for (const element of set) {
|
|
82
470
|
const style = styleOf(element);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.7-canary.
|
|
3
|
+
"version": "0.7.7-canary.550ff21",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -860,9 +860,9 @@
|
|
|
860
860
|
"react-dom": ">=19.0.0"
|
|
861
861
|
},
|
|
862
862
|
"devDependencies": {
|
|
863
|
-
"@astryxdesign/a11y-spec": "0.7.7-canary.
|
|
864
|
-
"@astryxdesign/cli": "0.7.7-canary.
|
|
865
|
-
"@astryxdesign/interaction-harness": "0.7.7-canary.
|
|
863
|
+
"@astryxdesign/a11y-spec": "0.7.7-canary.550ff21",
|
|
864
|
+
"@astryxdesign/cli": "0.7.7-canary.550ff21",
|
|
865
|
+
"@astryxdesign/interaction-harness": "0.7.7-canary.550ff21",
|
|
866
866
|
"@babel/cli": "^7.29.7",
|
|
867
867
|
"@babel/core": "^7.29.7",
|
|
868
868
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
useComponentTranslator,
|
|
47
47
|
} from '../i18n/useComponentTranslator';
|
|
48
48
|
import english from '../i18n/generated-english/CommandPalette/CommandPaletteInput.generated';
|
|
49
|
+
import {textEntryTraitAttributes} from '../Field/textEntryTraits';
|
|
49
50
|
|
|
50
51
|
registerEnglishFallback(english);
|
|
51
52
|
const styles = stylex.create({
|
|
@@ -320,6 +321,11 @@ export function CommandPaletteInput({
|
|
|
320
321
|
? focusOutlineProps.focusVisible(styles.input)
|
|
321
322
|
: focusOutlineProps.suppressed(styles.input))}
|
|
322
323
|
type="text"
|
|
324
|
+
// Autocorrect would rewrite names and identifiers.
|
|
325
|
+
{...textEntryTraitAttributes({
|
|
326
|
+
enterKeyHint: 'search',
|
|
327
|
+
hasAutoCorrect: false,
|
|
328
|
+
})}
|
|
323
329
|
role={ctx ? 'combobox' : standaloneRole}
|
|
324
330
|
aria-expanded={
|
|
325
331
|
ctx
|
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
plainDateToISO,
|
|
63
63
|
DATE_FORMAT_WITH_WEEKDAY,
|
|
64
64
|
} from '../utils';
|
|
65
|
+
import {reducedMotionFade} from '../motion/reducedMotion';
|
|
65
66
|
import {dateInputTouchSizes, dateInputTouchGeometry} from './tokens.stylex';
|
|
66
67
|
import {useOwnScrollGesture} from './useOwnScrollGesture';
|
|
67
68
|
import {useScrollSettle} from './useScrollSettle';
|
|
@@ -431,8 +432,26 @@ export function MonthScroller({
|
|
|
431
432
|
);
|
|
432
433
|
}, [paneSize, initialMonthIndex, minMonthIndex, isRTL]);
|
|
433
434
|
|
|
435
|
+
// The Reduced Motion dip of a step on a touch screen, owned here so a
|
|
436
|
+
// second step or the unmount clears it.
|
|
437
|
+
const dipRef = useRef<{animation: Animation; timer: number} | null>(null);
|
|
438
|
+
const stopDip = useCallback(() => {
|
|
439
|
+
const dip = dipRef.current;
|
|
440
|
+
if (dip != null) {
|
|
441
|
+
window.clearTimeout(dip.timer);
|
|
442
|
+
dip.animation.cancel();
|
|
443
|
+
dipRef.current = null;
|
|
444
|
+
}
|
|
445
|
+
}, []);
|
|
446
|
+
// Synchronizes with the dip's timer and animation: neither outlives the
|
|
447
|
+
// scroller.
|
|
448
|
+
useEffect(() => stopDip, [stopDip]);
|
|
449
|
+
|
|
434
450
|
const scrollToMonth = useCallback(
|
|
435
451
|
(monthIndex: number, behavior: ScrollBehavior = 'smooth') => {
|
|
452
|
+
// Every step replaces a running dip, whichever way it moves. Otherwise
|
|
453
|
+
// the dip's landing would undo a jump or an immediate step made after it.
|
|
454
|
+
stopDip();
|
|
436
455
|
const scroller = scrollerRef.current;
|
|
437
456
|
if (scroller == null || paneSize === 0) {
|
|
438
457
|
return;
|
|
@@ -458,13 +477,52 @@ export function MonthScroller({
|
|
|
458
477
|
recenter(scroller, row);
|
|
459
478
|
return;
|
|
460
479
|
}
|
|
480
|
+
const left = scrollOffsetForRow(row, paneSize, isRTL);
|
|
481
|
+
// Under reduced motion the month changes without travel: at once on a fine
|
|
482
|
+
// primary pointer, under an opacity dip on a coarse one (reducedMotionFade).
|
|
483
|
+
const fade = behavior === 'smooth' ? reducedMotionFade(scroller) : null;
|
|
484
|
+
if (fade != null) {
|
|
485
|
+
// The window mounts around the target first, and the scroll lands in
|
|
486
|
+
// the layout effect below, as a far jump does. So nothing moves the
|
|
487
|
+
// window after the jump. If the window followed the jump, WebKit would
|
|
488
|
+
// re-snap it one pane further on every frame. A scroll that lands on
|
|
489
|
+
// the centre row reports nothing.
|
|
490
|
+
const land = () => {
|
|
491
|
+
// The landing is the newest steer. An earlier steer whose scroll
|
|
492
|
+
// never arrived would otherwise hide every scroll after it, a
|
|
493
|
+
// trackpad's included. Closing the wheels makes such a steer: it
|
|
494
|
+
// re-asserts the month already in view.
|
|
495
|
+
steeredRowRef.current = null;
|
|
496
|
+
if (row === centerRowRef.current) {
|
|
497
|
+
scroller.scrollTo({left, behavior: 'auto'});
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
pendingScrollRef.current = row;
|
|
501
|
+
centerRowRef.current = row;
|
|
502
|
+
setCenterRow(row);
|
|
503
|
+
};
|
|
504
|
+
if (fade.duration === 0) {
|
|
505
|
+
land();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
// A second step within the dip has replaced the first (above): only
|
|
509
|
+
// its scroll lands, at its own dip's middle.
|
|
510
|
+
dipRef.current = {
|
|
511
|
+
animation: scroller.animate(
|
|
512
|
+
[{opacity: 1}, {opacity: 0}, {opacity: 1}],
|
|
513
|
+
{duration: fade.duration, easing: fade.easing},
|
|
514
|
+
),
|
|
515
|
+
timer: window.setTimeout(() => {
|
|
516
|
+
dipRef.current = null;
|
|
517
|
+
land();
|
|
518
|
+
}, fade.duration / 2),
|
|
519
|
+
};
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
461
522
|
steeredRowRef.current = row;
|
|
462
|
-
scroller.scrollTo({
|
|
463
|
-
left: scrollOffsetForRow(row, paneSize, isRTL),
|
|
464
|
-
behavior,
|
|
465
|
-
});
|
|
523
|
+
scroller.scrollTo({left, behavior});
|
|
466
524
|
},
|
|
467
|
-
[paneSize, minMonthIndex, rowCount, isRTL, recenter],
|
|
525
|
+
[paneSize, minMonthIndex, rowCount, isRTL, recenter, stopDip],
|
|
468
526
|
);
|
|
469
527
|
|
|
470
528
|
// Deliberately un-keyed: it runs after every render, costs a null check, and
|