@octanejs/aria 0.0.46 → 0.0.47
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/README.md +7 -6
- package/UPSTREAM.md +58 -69
- package/package.json +11 -11
- package/src/calendar/useCalendarGrid.tsx +64 -62
- package/src/collections/Document.ts +3 -0
- package/src/color/useColorArea.tsx +95 -69
- package/src/color/useColorWheel.tsx +38 -37
- package/src/combobox/{useComboBox.ts → useComboBox.tsx} +239 -259
- package/src/components/ComboBox.ts +35 -2
- package/src/components/Dialog.ts +12 -1
- package/src/components/DropZone.tsx +3 -2
- package/src/components/FileTrigger.tsx +1 -0
- package/src/components/GridList.ts +11 -2
- package/src/components/ListBox.ts +1 -1
- package/src/components/Menu.ts +1 -0
- package/src/components/Meter.ts +3 -1
- package/src/components/Popover.ts +20 -8
- package/src/components/PreviewTrigger.tsx +101 -0
- package/src/components/ProgressBar.ts +10 -2
- package/src/components/Select.ts +1 -1
- package/src/components/Table.ts +26 -15
- package/src/components/TokenField.tsx +395 -0
- package/src/components/Tooltip.ts +4 -6
- package/src/components/Tree.ts +14 -7
- package/src/components/Virtualizer.tsx +30 -14
- package/src/components/index.ts +22 -0
- package/src/components/utils.ts +1 -1
- package/src/datepicker/useDateField.tsx +2 -11
- package/src/datepicker/useDatePickerGroup.tsx +44 -33
- package/src/datepicker/useDateSegment.tsx +31 -30
- package/src/dialog/useDialog.ts +8 -1
- package/src/dnd/DragManager.tsx +5 -1
- package/src/dnd/useDrag.tsx +2 -1
- package/src/focus/FocusScope.ts +2 -2
- package/src/grid/useGrid.ts +3 -0
- package/src/grid/{useGridCell.ts → useGridCell.tsx} +121 -64
- package/src/grid/utils.ts +1 -0
- package/src/gridlist/{useGridListItem.ts → useGridListItem.tsx} +152 -111
- package/src/index.ts +22 -0
- package/src/interactions/createEventHandler.ts +11 -8
- package/src/interactions/createKeyboardShortcutHandler.tsx +218 -0
- package/src/interactions/textSelection.ts +3 -3
- package/src/interactions/useContextMenu.tsx +125 -0
- package/src/interactions/useFocusVisible.ts +4 -4
- package/src/interactions/useKeyboard.ts +50 -2
- package/src/interactions/useLongPress.ts +33 -22
- package/src/interactions/useMove.ts +6 -6
- package/src/interactions/usePress.ts +8 -0
- package/src/interactions/utils.ts +10 -10
- package/src/intl/combobox/fi-FI.json +2 -2
- package/src/intl/previewtrigger/ar-AE.json +3 -0
- package/src/intl/previewtrigger/bg-BG.json +3 -0
- package/src/intl/previewtrigger/cs-CZ.json +3 -0
- package/src/intl/previewtrigger/da-DK.json +3 -0
- package/src/intl/previewtrigger/de-DE.json +3 -0
- package/src/intl/previewtrigger/el-GR.json +3 -0
- package/src/intl/previewtrigger/en-US.json +3 -0
- package/src/intl/previewtrigger/es-ES.json +3 -0
- package/src/intl/previewtrigger/et-EE.json +3 -0
- package/src/intl/previewtrigger/fi-FI.json +3 -0
- package/src/intl/previewtrigger/fr-FR.json +3 -0
- package/src/intl/previewtrigger/he-IL.json +3 -0
- package/src/intl/previewtrigger/hr-HR.json +3 -0
- package/src/intl/previewtrigger/hu-HU.json +3 -0
- package/src/intl/previewtrigger/index.ts +73 -0
- package/src/intl/previewtrigger/it-IT.json +3 -0
- package/src/intl/previewtrigger/ja-JP.json +3 -0
- package/src/intl/previewtrigger/ko-KR.json +3 -0
- package/src/intl/previewtrigger/lt-LT.json +3 -0
- package/src/intl/previewtrigger/lv-LV.json +3 -0
- package/src/intl/previewtrigger/nb-NO.json +3 -0
- package/src/intl/previewtrigger/nl-NL.json +3 -0
- package/src/intl/previewtrigger/pl-PL.json +3 -0
- package/src/intl/previewtrigger/pt-BR.json +3 -0
- package/src/intl/previewtrigger/pt-PT.json +3 -0
- package/src/intl/previewtrigger/ro-RO.json +3 -0
- package/src/intl/previewtrigger/ru-RU.json +3 -0
- package/src/intl/previewtrigger/sk-SK.json +3 -0
- package/src/intl/previewtrigger/sl-SI.json +3 -0
- package/src/intl/previewtrigger/sr-SP.json +3 -0
- package/src/intl/previewtrigger/sv-SE.json +3 -0
- package/src/intl/previewtrigger/tr-TR.json +3 -0
- package/src/intl/previewtrigger/uk-UA.json +3 -0
- package/src/intl/previewtrigger/zh-CN.json +3 -0
- package/src/intl/previewtrigger/zh-TW.json +3 -0
- package/src/intl/table/ar-AE.json +3 -3
- package/src/intl/table/bg-BG.json +3 -3
- package/src/intl/table/cs-CZ.json +3 -3
- package/src/intl/table/da-DK.json +3 -3
- package/src/intl/table/de-DE.json +3 -3
- package/src/intl/table/el-GR.json +3 -3
- package/src/intl/table/es-ES.json +3 -3
- package/src/intl/table/et-EE.json +3 -3
- package/src/intl/table/fi-FI.json +3 -3
- package/src/intl/table/fr-FR.json +3 -3
- package/src/intl/table/he-IL.json +3 -3
- package/src/intl/table/hr-HR.json +3 -3
- package/src/intl/table/hu-HU.json +3 -3
- package/src/intl/table/it-IT.json +3 -3
- package/src/intl/table/ja-JP.json +3 -3
- package/src/intl/table/ko-KR.json +3 -3
- package/src/intl/table/lt-LT.json +3 -3
- package/src/intl/table/lv-LV.json +3 -3
- package/src/intl/table/nb-NO.json +3 -3
- package/src/intl/table/nl-NL.json +3 -3
- package/src/intl/table/pl-PL.json +3 -3
- package/src/intl/table/pt-BR.json +3 -3
- package/src/intl/table/pt-PT.json +3 -3
- package/src/intl/table/ro-RO.json +3 -3
- package/src/intl/table/ru-RU.json +3 -3
- package/src/intl/table/sk-SK.json +3 -3
- package/src/intl/table/sl-SI.json +3 -3
- package/src/intl/table/sr-SP.json +3 -3
- package/src/intl/table/sv-SE.json +3 -3
- package/src/intl/table/tr-TR.json +3 -3
- package/src/intl/table/uk-UA.json +3 -3
- package/src/intl/table/zh-CN.json +4 -4
- package/src/intl/table/zh-TW.json +3 -3
- package/src/intl/tree/es-ES.json +1 -1
- package/src/link/useLink.ts +2 -0
- package/src/menu/useMenuItem.ts +33 -36
- package/src/menu/useMenuTrigger.tsx +217 -0
- package/src/menu/{useSubmenuTrigger.ts → useSubmenuTrigger.tsx} +112 -152
- package/src/numberfield/{useNumberField.ts → useNumberField.tsx} +185 -239
- package/src/overlays/Overlay.ts +5 -1
- package/src/overlays/ariaHideOutside.ts +6 -8
- package/src/overlays/useCloseOnScroll.ts +7 -5
- package/src/overlays/{useOverlay.ts → useOverlay.tsx} +72 -84
- package/src/overlays/usePopover.ts +10 -5
- package/src/overlays/usePreventScroll.ts +4 -4
- package/src/progress/useProgressBar.ts +2 -1
- package/src/radio/{useRadioGroup.ts → useRadioGroup.tsx} +83 -80
- package/src/searchfield/useSearchField.tsx +147 -0
- package/src/select/useSelect.ts +20 -20
- package/src/selection/useSelectableCollection.tsx +751 -0
- package/src/selection/useSelectableItem.ts +48 -11
- package/src/slider/{useSliderThumb.ts → useSliderThumb.tsx} +116 -159
- package/src/spinbutton/useSpinButton.tsx +330 -0
- package/src/stately/combobox/useComboBoxState.ts +1 -1
- package/src/stately/grid/useGridState.ts +9 -10
- package/src/stately/index.ts +11 -0
- package/src/stately/menu/useMenuTriggerState.ts +1 -1
- package/src/stately/menu/useSubmenuTriggerState.ts +2 -0
- package/src/stately/overlays/useOverlayTriggerState.ts +12 -1
- package/src/stately/radio/useRadioGroupState.ts +5 -1
- package/src/stately/table/TableUtils.ts +15 -1
- package/src/stately/table/useTableState.ts +7 -0
- package/src/stately/tokenfield/TokenFieldValue.tsx +440 -0
- package/src/stately/tokenfield/useTokenFieldState.tsx +57 -0
- package/src/stately/tooltip/useTooltipTriggerState.ts +16 -7
- package/src/stately/virtualizer/Layout.tsx +2 -0
- package/src/stately/virtualizer/ScrollAnchor.tsx +338 -0
- package/src/stately/virtualizer/Virtualizer.tsx +52 -1
- package/src/stately/virtualizer/types.tsx +2 -0
- package/src/table/useTableCell.ts +12 -0
- package/src/table/useTableColumnHeader.ts +8 -1
- package/src/table/useTableColumnResize.tsx +354 -0
- package/src/tag/useTag.ts +22 -15
- package/src/textfield/useTextField.ts +2 -0
- package/src/tokenfield/useToken.tsx +64 -0
- package/src/tokenfield/useTokenField.tsx +849 -0
- package/src/tooltip/usePreviewTrigger.tsx +266 -0
- package/src/tooltip/useSafeArea.tsx +171 -0
- package/src/upstream-exports/react-aria/mergeRefs.tsx +3 -0
- package/src/upstream-exports/react-aria/usePreviewTrigger.tsx +18 -0
- package/src/upstream-exports/react-aria/useTokenField.tsx +22 -0
- package/src/upstream-exports/react-stately/private/collections/getChildNodes.tsx +8 -0
- package/src/upstream-exports/react-stately/private/collections/getItemCount.tsx +2 -0
- package/src/upstream-exports/react-stately/private/grid/GridCollection.tsx +7 -0
- package/src/upstream-exports/react-stately/private/grid/useGridState.tsx +6 -0
- package/src/upstream-exports/react-stately/private/utils/number.tsx +2 -0
- package/src/upstream-exports/react-stately/useComboBoxState.tsx +23 -0
- package/src/upstream-exports/react-stately/useListState.tsx +15 -0
- package/src/upstream-exports/react-stately/useMenuTriggerState.tsx +26 -0
- package/src/upstream-exports/react-stately/useMultipleSelectionState.tsx +21 -0
- package/src/upstream-exports/react-stately/useNumberFieldState.tsx +20 -0
- package/src/upstream-exports/react-stately/useOverlayTriggerState.tsx +20 -0
- package/src/upstream-exports/react-stately/useRadioGroupState.tsx +16 -0
- package/src/upstream-exports/react-stately/useSearchFieldState.tsx +19 -0
- package/src/upstream-exports/react-stately/useSliderState.tsx +20 -0
- package/src/upstream-exports/react-stately/useTableState.tsx +45 -0
- package/src/upstream-exports/react-stately/useTokenFieldState.tsx +25 -0
- package/src/upstream-exports/react-stately/useTooltipTriggerState.tsx +20 -0
- package/src/upstream-exports/react-stately/useTreeState.tsx +14 -0
- package/src/utils/domHelpers.ts +74 -17
- package/src/utils/isVirtualEvent.ts +2 -1
- package/src/utils/openLink.ts +8 -3
- package/src/utils/platform.ts +7 -3
- package/src/utils/scrollIntoView.ts +4 -4
- package/src/utils/shadowdom/DOMFunctions.ts +42 -2
- package/src/utils/useId.ts +11 -2
- package/src/utils/useLoadMoreSentinel.ts +8 -3
- package/src/utils/useViewportSize.ts +3 -3
- package/src/virtualizer/ScrollView.tsx +13 -4
- package/src/virtualizer/VirtualizerItem.tsx +3 -1
- package/src/virtualizer/useVirtualizerItem.tsx +43 -3
- package/src/menu/useMenuTrigger.ts +0 -189
- package/src/searchfield/useSearchField.ts +0 -156
- package/src/selection/useSelectableCollection.ts +0 -750
- package/src/spinbutton/useSpinButton.ts +0 -380
- package/src/table/useTableColumnResize.ts +0 -374
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/** @jsxImportSource octane */
|
|
2
|
+
// Ported from adobe/react-spectrum@5ecb3333001313e83898cd07644227897e3bae1f (packages/react-stately/src/tokenfield/TokenFieldValue.ts).
|
|
3
|
+
/*
|
|
4
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
5
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
7
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
10
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
11
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
12
|
+
* governing permissions and limitations under the License.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type TokenFieldSegment<T = any> = TextSegment | TokenSegment<T>;
|
|
16
|
+
|
|
17
|
+
export interface TextSegment {
|
|
18
|
+
type: 'text';
|
|
19
|
+
text: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TokenSegment<T = any> {
|
|
23
|
+
type: 'token';
|
|
24
|
+
text: string;
|
|
25
|
+
/** An arbitrary value associated with the token. */
|
|
26
|
+
value?: T;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Position {
|
|
30
|
+
/** Index of the segment in the list. */
|
|
31
|
+
index: number;
|
|
32
|
+
/** Text offset within the segment in UTF-16 code units. */
|
|
33
|
+
offset: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
enum Direction {
|
|
37
|
+
Forward = 1,
|
|
38
|
+
Backward = -1,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TokenFieldValueOptions {
|
|
42
|
+
caretPosition?: Position | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A list of segments containing editable text and non-editable tokens.
|
|
47
|
+
*/
|
|
48
|
+
export class TokenFieldValue<T = any> {
|
|
49
|
+
static readonly Direction = Direction;
|
|
50
|
+
|
|
51
|
+
/** The text and token segments in the list. */
|
|
52
|
+
readonly segments: readonly TokenFieldSegment<T>[];
|
|
53
|
+
/** The caret position. */
|
|
54
|
+
caretPosition: Position = { index: 0, offset: 0 };
|
|
55
|
+
// Linked list representing the undo/redo history.
|
|
56
|
+
private previous: this | null = null;
|
|
57
|
+
private next: this | null = null;
|
|
58
|
+
private isCoalescing = true;
|
|
59
|
+
|
|
60
|
+
/** Create a new list with the given segments. */
|
|
61
|
+
constructor(tokens: readonly TokenFieldSegment<T>[], options?: TokenFieldValueOptions) {
|
|
62
|
+
this.segments = tokens;
|
|
63
|
+
this.caretPosition = options?.caretPosition ?? { index: 0, offset: 0 };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protected createFieldValue(segments: readonly TokenFieldSegment<T>[]): this {
|
|
67
|
+
const Constructor = this.constructor as new (
|
|
68
|
+
segments: readonly TokenFieldSegment<T>[],
|
|
69
|
+
options?: TokenFieldValueOptions,
|
|
70
|
+
) => this;
|
|
71
|
+
return new Constructor(segments);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Create a new list with the caret position set to the given position. */
|
|
75
|
+
withCaretPosition(caretPosition: Position): this {
|
|
76
|
+
if (
|
|
77
|
+
this.caretPosition.index === caretPosition.index &&
|
|
78
|
+
this.caretPosition.offset === caretPosition.offset
|
|
79
|
+
) {
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let result = this.createFieldValue(this.segments);
|
|
84
|
+
result.caretPosition = caretPosition;
|
|
85
|
+
result.previous = this.previous;
|
|
86
|
+
result.next = this.next;
|
|
87
|
+
result.isCoalescing = this.isCoalescing;
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private splitSegment(
|
|
92
|
+
segment: TokenFieldSegment | undefined,
|
|
93
|
+
offset: number,
|
|
94
|
+
): [TokenFieldSegment | null, TokenFieldSegment | null] {
|
|
95
|
+
if (!segment) {
|
|
96
|
+
let empty = this.createTextSegment('');
|
|
97
|
+
return [offset > 0 ? empty : null, offset > 0 ? null : empty];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (segment.type === 'token') {
|
|
101
|
+
return [offset > 0 ? { ...segment } : null, offset > 0 ? null : { ...segment }];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return [
|
|
105
|
+
offset > 0 ? { type: 'text', text: segment.text.slice(0, offset) } : null,
|
|
106
|
+
offset < segment.text.length ? this.createTextSegment(segment.text.slice(offset)) : null,
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private createTextSegment(text: string): TextSegment {
|
|
111
|
+
return { type: 'text', text };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
protected tokenize(text: string): TokenFieldSegment<T>[] {
|
|
115
|
+
return [this.createTextSegment(text)];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private clampPosition(position: Position): Position {
|
|
119
|
+
if (this.segments.length > 0 && position.index >= this.segments.length) {
|
|
120
|
+
return {
|
|
121
|
+
index: this.segments.length - 1,
|
|
122
|
+
offset: this.segments[this.segments.length - 1].text.length,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (position.index < 0) {
|
|
127
|
+
return { index: 0, offset: 0 };
|
|
128
|
+
}
|
|
129
|
+
return position;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Replace the text between two positions with new text. */
|
|
133
|
+
replaceRange(start: Position, end: Position, text: string, coalesce = true): this {
|
|
134
|
+
return this.replaceRangeWithSegments(
|
|
135
|
+
start,
|
|
136
|
+
end,
|
|
137
|
+
text.length > 0 ? [this.createTextSegment(text)] : [],
|
|
138
|
+
coalesce,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Replace the text between two positions with new segments. */
|
|
143
|
+
replaceRangeWithSegments(
|
|
144
|
+
start: Position,
|
|
145
|
+
end: Position,
|
|
146
|
+
insert: TokenFieldSegment<T>[],
|
|
147
|
+
coalesce = true,
|
|
148
|
+
): this {
|
|
149
|
+
start = this.clampPosition(start);
|
|
150
|
+
end = this.clampPosition(end);
|
|
151
|
+
let startSegment = this.segments[start.index];
|
|
152
|
+
let endSegment = this.segments[end.index];
|
|
153
|
+
let [startSplit] = this.splitSegment(startSegment, start.offset);
|
|
154
|
+
let [, endSplit] = this.splitSegment(endSegment, end.offset);
|
|
155
|
+
|
|
156
|
+
let newSegments = this.segments.slice(0, start.index);
|
|
157
|
+
if (startSplit) {
|
|
158
|
+
appendSegments(newSegments, [startSplit]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (insert.length) {
|
|
162
|
+
appendSegments(newSegments, insert, (text) => this.tokenize(text));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let lastSegment = newSegments[newSegments.length - 1];
|
|
166
|
+
let lastIsText = lastSegment && lastSegment.type === 'text';
|
|
167
|
+
let caret = {
|
|
168
|
+
index: lastIsText ? newSegments.length - 1 : newSegments.length,
|
|
169
|
+
offset: lastIsText ? lastSegment.text.length : 0,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (endSplit) {
|
|
173
|
+
appendSegments(newSegments, [endSplit]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
appendSegments(newSegments, this.segments.slice(end.index + 1));
|
|
177
|
+
|
|
178
|
+
let segments = this.createFieldValue(newSegments);
|
|
179
|
+
segments.caretPosition = caret;
|
|
180
|
+
segments.isCoalescing = coalesce;
|
|
181
|
+
if (this.isCoalescing && coalesce && this.previous) {
|
|
182
|
+
segments.previous = this.previous;
|
|
183
|
+
segments.previous.next = segments;
|
|
184
|
+
} else {
|
|
185
|
+
segments.previous = this;
|
|
186
|
+
this.caretPosition = end;
|
|
187
|
+
this.next = segments;
|
|
188
|
+
}
|
|
189
|
+
return segments;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Find the boundary before or after a position using an Intl.Segmenter. */
|
|
193
|
+
findBoundaryWithSegmenter(
|
|
194
|
+
position: Position,
|
|
195
|
+
segmenter: Intl.Segmenter,
|
|
196
|
+
direction: Direction,
|
|
197
|
+
): Position | null {
|
|
198
|
+
position = this.clampPosition(position);
|
|
199
|
+
for (let i = position.index; i >= 0 && i < this.segments.length; i += direction) {
|
|
200
|
+
let segment = this.segments[i];
|
|
201
|
+
switch (segment.type) {
|
|
202
|
+
case 'token':
|
|
203
|
+
if (
|
|
204
|
+
i !== position.index ||
|
|
205
|
+
(direction === Direction.Backward ? position.offset > 0 : position.offset === 0)
|
|
206
|
+
) {
|
|
207
|
+
let index = i + direction;
|
|
208
|
+
return {
|
|
209
|
+
index: index >= 0 ? index : 0,
|
|
210
|
+
offset:
|
|
211
|
+
direction === Direction.Backward && index >= 0
|
|
212
|
+
? this.segments[index].text.length
|
|
213
|
+
: 0,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
case 'text': {
|
|
218
|
+
let offset = direction === Direction.Backward ? segment.text.length : 0;
|
|
219
|
+
if (i === position.index) {
|
|
220
|
+
offset = position.offset;
|
|
221
|
+
}
|
|
222
|
+
if (direction === Direction.Backward) {
|
|
223
|
+
offset--;
|
|
224
|
+
}
|
|
225
|
+
if (offset < 0 || offset >= segment.text.length) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let part = segmenter.segment(segment.text).containing(offset);
|
|
230
|
+
while (part && part.isWordLike === false) {
|
|
231
|
+
offset += direction;
|
|
232
|
+
part = segmenter.segment(segment.text).containing(offset);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (part) {
|
|
236
|
+
return {
|
|
237
|
+
index: i,
|
|
238
|
+
offset:
|
|
239
|
+
direction === Direction.Backward ? part.index : part.index + part.segment.length,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Find a line boundary before or after a position. */
|
|
251
|
+
findLineBoundary(position: Position, direction: Direction): Position | null {
|
|
252
|
+
let res = this.findText(position, direction, '\n');
|
|
253
|
+
if (res) {
|
|
254
|
+
return res;
|
|
255
|
+
}
|
|
256
|
+
return direction === Direction.Backward
|
|
257
|
+
? { index: 0, offset: 0 }
|
|
258
|
+
: {
|
|
259
|
+
index: this.segments.length - 1,
|
|
260
|
+
offset: this.segments[this.segments.length - 1].text.length,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Find a string or regular expression match before or after a position. */
|
|
265
|
+
findText(position: Position, direction: Direction, search: string | RegExp): Position | null {
|
|
266
|
+
if (this.segments.length === 0) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (let i = position.index; i >= 0 && i < this.segments.length; i += direction) {
|
|
271
|
+
let segment = this.segments[i];
|
|
272
|
+
if (segment.type !== 'text') {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
let offset = findInText(
|
|
276
|
+
segment.text,
|
|
277
|
+
search,
|
|
278
|
+
direction,
|
|
279
|
+
i === position.index ? position.offset : undefined,
|
|
280
|
+
);
|
|
281
|
+
if (offset >= 0) {
|
|
282
|
+
return {
|
|
283
|
+
index: i,
|
|
284
|
+
offset: offset,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Delete text at a position using a segmenter. */
|
|
293
|
+
delete(
|
|
294
|
+
position: Position,
|
|
295
|
+
segmenter: Intl.Segmenter,
|
|
296
|
+
direction: Direction,
|
|
297
|
+
coalesce = true,
|
|
298
|
+
): this {
|
|
299
|
+
let boundary = this.findBoundaryWithSegmenter(position, segmenter, direction);
|
|
300
|
+
if (boundary) {
|
|
301
|
+
return this.replaceRange(
|
|
302
|
+
direction === Direction.Backward ? boundary : position,
|
|
303
|
+
direction === Direction.Backward ? position : boundary,
|
|
304
|
+
'',
|
|
305
|
+
coalesce,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
this.caretPosition = position;
|
|
310
|
+
return this;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Delete text to the next or previous line break. */
|
|
314
|
+
deleteLine(position: Position, direction: Direction, coalesce = true): this {
|
|
315
|
+
if (this.segments.length === 0) {
|
|
316
|
+
return this;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let boundary = this.findLineBoundary(position, direction);
|
|
320
|
+
if (boundary) {
|
|
321
|
+
return this.replaceRange(
|
|
322
|
+
direction === Direction.Backward ? boundary : position,
|
|
323
|
+
direction === Direction.Backward ? position : boundary,
|
|
324
|
+
'',
|
|
325
|
+
coalesce,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return this;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Create a new list containing a subset of the segments. */
|
|
333
|
+
slice(start: Position, end: Position): this {
|
|
334
|
+
start = this.clampPosition(start);
|
|
335
|
+
end = this.clampPosition(end);
|
|
336
|
+
if (start.index === end.index && start.offset === end.offset) {
|
|
337
|
+
return this.createFieldValue([]);
|
|
338
|
+
}
|
|
339
|
+
if (start.index === end.index) {
|
|
340
|
+
let segment = this.segments[start.index];
|
|
341
|
+
if (segment.type === 'text') {
|
|
342
|
+
return this.createFieldValue([
|
|
343
|
+
{ type: 'text', text: segment.text.slice(start.offset, end.offset) },
|
|
344
|
+
]);
|
|
345
|
+
}
|
|
346
|
+
return this.createFieldValue([segment]);
|
|
347
|
+
}
|
|
348
|
+
let startSegment = this.segments[start.index];
|
|
349
|
+
let endSegment = this.segments[end.index];
|
|
350
|
+
let [, startSplit] = this.splitSegment(startSegment, start.offset);
|
|
351
|
+
let [endSplit] = this.splitSegment(endSegment, end.offset);
|
|
352
|
+
|
|
353
|
+
let result: TokenFieldSegment<T>[] = [];
|
|
354
|
+
if (startSplit) {
|
|
355
|
+
result.push(startSplit);
|
|
356
|
+
}
|
|
357
|
+
result.push(...this.segments.slice(start.index + 1, end.index));
|
|
358
|
+
if (endSplit) {
|
|
359
|
+
result.push(endSplit);
|
|
360
|
+
}
|
|
361
|
+
return this.createFieldValue(result);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Convert the list to a string. */
|
|
365
|
+
toString(): string {
|
|
366
|
+
return this.segments.map((seg) => seg.text).join('');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Returns the previous list in the undo history. */
|
|
370
|
+
undo(): this {
|
|
371
|
+
return this.previous ?? this;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Returns the next list in the redo history. */
|
|
375
|
+
redo(): this {
|
|
376
|
+
return this.next ?? this;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** End coalescing undo/redo history. */
|
|
380
|
+
endCoalescing(): void {
|
|
381
|
+
this.isCoalescing = false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function findInText(
|
|
386
|
+
text: string,
|
|
387
|
+
search: string | RegExp,
|
|
388
|
+
direction: Direction,
|
|
389
|
+
fromOffset?: number,
|
|
390
|
+
): number {
|
|
391
|
+
if (typeof search === 'string') {
|
|
392
|
+
if (direction === Direction.Backward) {
|
|
393
|
+
return text.lastIndexOf(search, fromOffset !== undefined ? fromOffset - 1 : text.length - 1);
|
|
394
|
+
}
|
|
395
|
+
return text.indexOf(search, fromOffset ?? 0);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (direction === Direction.Forward) {
|
|
399
|
+
let start = fromOffset ?? 0;
|
|
400
|
+
let index = text.slice(start).search(search);
|
|
401
|
+
return index >= 0 ? start + index : -1;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let limit = fromOffset !== undefined ? fromOffset : text.length;
|
|
405
|
+
if (limit < 0) {
|
|
406
|
+
return -1;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let re = search.flags.includes('g') ? search : new RegExp(search.source, search.flags + 'g');
|
|
410
|
+
let matches = Array.from(text.slice(0, limit).matchAll(re));
|
|
411
|
+
return matches.at(-1)?.index ?? -1;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function appendSegments(
|
|
415
|
+
segments: TokenFieldSegment[],
|
|
416
|
+
insert: TokenFieldSegment[],
|
|
417
|
+
tokenize?: (text: string) => TokenFieldSegment[],
|
|
418
|
+
): TokenFieldSegment[] {
|
|
419
|
+
for (let segment of insert) {
|
|
420
|
+
if (segment.type === 'text' && segment.text.length === 0) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
let last = segments[segments.length - 1];
|
|
425
|
+
if (last && last.type === 'text' && segment.type === 'text') {
|
|
426
|
+
if (tokenize) {
|
|
427
|
+
let tokenized = tokenize(last.text + segment.text);
|
|
428
|
+
segments.splice(segments.length - 1, 1, ...tokenized);
|
|
429
|
+
} else {
|
|
430
|
+
segments[segments.length - 1] = { type: 'text', text: last.text + segment.text };
|
|
431
|
+
}
|
|
432
|
+
} else if (tokenize && segment.type === 'text') {
|
|
433
|
+
let tokenized = tokenize(segment.text);
|
|
434
|
+
segments.push(...tokenized);
|
|
435
|
+
} else {
|
|
436
|
+
segments.push(segment);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return segments;
|
|
440
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** @jsxImportSource octane */
|
|
2
|
+
// Ported from adobe/react-spectrum@5ecb3333001313e83898cd07644227897e3bae1f (packages/react-stately/src/tokenfield/useTokenFieldState.ts).
|
|
3
|
+
/*
|
|
4
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
5
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
7
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
10
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
11
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
12
|
+
* governing permissions and limitations under the License.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { TokenFieldValue } from './TokenFieldValue';
|
|
16
|
+
import { useControlledState } from '../utils/useControlledState';
|
|
17
|
+
import { useState } from '../../compat/react';
|
|
18
|
+
import { ValueBase } from '@react-types/shared';
|
|
19
|
+
|
|
20
|
+
export interface TokenFieldProps<
|
|
21
|
+
T extends TokenFieldValue = TokenFieldValue,
|
|
22
|
+
> extends ValueBase<T> {}
|
|
23
|
+
|
|
24
|
+
export interface TokenFieldState<T extends TokenFieldValue = TokenFieldValue> {
|
|
25
|
+
/** The current value of the token field. */
|
|
26
|
+
value: T;
|
|
27
|
+
/** Sets the value of the token field. */
|
|
28
|
+
setValue: (fn: T | ((value: T) => T)) => void;
|
|
29
|
+
/** Whether the token field is composing. */
|
|
30
|
+
isComposing: boolean;
|
|
31
|
+
/** Sets the composing state of the token field. */
|
|
32
|
+
setComposing: (isComposing: boolean) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Provides state management for a token field. Tracks the field value and the
|
|
37
|
+
* composition state.
|
|
38
|
+
*/
|
|
39
|
+
export function useTokenFieldState<T extends TokenFieldValue = TokenFieldValue>(
|
|
40
|
+
props: TokenFieldProps<T>,
|
|
41
|
+
): TokenFieldState<T> {
|
|
42
|
+
let {
|
|
43
|
+
value: valueProp,
|
|
44
|
+
defaultValue: defaultValueProp = new TokenFieldValue([]),
|
|
45
|
+
onChange,
|
|
46
|
+
} = props;
|
|
47
|
+
|
|
48
|
+
let [value, setValue] = useControlledState(valueProp as any, defaultValueProp, onChange);
|
|
49
|
+
let [isComposing, setComposing] = useState(false);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
value,
|
|
53
|
+
setValue,
|
|
54
|
+
isComposing,
|
|
55
|
+
setComposing,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// convention; the module-level warmup/cooldown timers and global tooltip registry are
|
|
4
4
|
// ported verbatim; upstream's explicit useEffect dep arrays are kept verbatim with a
|
|
5
5
|
// trailing subSlot.
|
|
6
|
-
import { useEffect, useMemo, useRef } from 'octane';
|
|
6
|
+
import { useEffect, useMemo, useRef, useState } from 'octane';
|
|
7
7
|
|
|
8
8
|
import { S, splitSlot, subSlot } from '../../internal';
|
|
9
9
|
import {
|
|
@@ -54,6 +54,11 @@ const TOOLTIP_COOLDOWN = 500;
|
|
|
54
54
|
export interface TooltipTriggerState {
|
|
55
55
|
/** Whether the tooltip is currently showing. */
|
|
56
56
|
isOpen: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Whether the tooltip is being shown or hidden without an animation. This is true while the
|
|
59
|
+
* global warmup timer is active, i.e. when quickly moving between tooltips.
|
|
60
|
+
*/
|
|
61
|
+
shouldSkipAnimation: boolean;
|
|
57
62
|
/**
|
|
58
63
|
* Shows the tooltip. By default, the tooltip becomes visible after a delay
|
|
59
64
|
* depending on a global warmup timer. The `immediate` option shows the
|
|
@@ -90,6 +95,7 @@ export function useTooltipTriggerState(...args: any[]): TooltipTriggerState {
|
|
|
90
95
|
|
|
91
96
|
let { delay = TOOLTIP_DELAY, closeDelay = TOOLTIP_COOLDOWN } = props;
|
|
92
97
|
let { isOpen, open, close } = useOverlayTriggerState(props, subSlot(slot, 'overlay'));
|
|
98
|
+
let [shouldSkipAnimation, setIsInstant] = useState(false, subSlot(slot, 'skipAnimation'));
|
|
93
99
|
let id = useMemo(() => `${++tooltipId}`, [], subSlot(slot, 'id'));
|
|
94
100
|
let closeTimeout = useRef<ReturnType<typeof setTimeout> | null>(
|
|
95
101
|
null,
|
|
@@ -104,19 +110,20 @@ export function useTooltipTriggerState(...args: any[]): TooltipTriggerState {
|
|
|
104
110
|
let closeOpenTooltips = () => {
|
|
105
111
|
for (let hideTooltipId in tooltips) {
|
|
106
112
|
if (hideTooltipId !== id) {
|
|
107
|
-
tooltips[hideTooltipId](true);
|
|
113
|
+
tooltips[hideTooltipId](true, true);
|
|
108
114
|
delete tooltips[hideTooltipId];
|
|
109
115
|
}
|
|
110
116
|
}
|
|
111
117
|
};
|
|
112
118
|
|
|
113
|
-
let showTooltip = () => {
|
|
119
|
+
let showTooltip = (instant?: boolean) => {
|
|
114
120
|
if (closeTimeout.current) {
|
|
115
121
|
clearTimeout(closeTimeout.current);
|
|
116
122
|
}
|
|
117
123
|
closeTimeout.current = null;
|
|
118
124
|
closeOpenTooltips();
|
|
119
125
|
ensureTooltipEntry();
|
|
126
|
+
setIsInstant(!!instant);
|
|
120
127
|
globalWarmedUp = true;
|
|
121
128
|
open();
|
|
122
129
|
if (globalWarmUpTimeout) {
|
|
@@ -129,7 +136,8 @@ export function useTooltipTriggerState(...args: any[]): TooltipTriggerState {
|
|
|
129
136
|
}
|
|
130
137
|
};
|
|
131
138
|
|
|
132
|
-
let hideTooltip = (immediate?: boolean) => {
|
|
139
|
+
let hideTooltip = (immediate?: boolean, instant?: boolean) => {
|
|
140
|
+
setIsInstant(!!instant);
|
|
133
141
|
if (immediate || closeDelay <= 0) {
|
|
134
142
|
if (closeTimeout.current) {
|
|
135
143
|
clearTimeout(closeTimeout.current);
|
|
@@ -173,10 +181,10 @@ export function useTooltipTriggerState(...args: any[]): TooltipTriggerState {
|
|
|
173
181
|
globalWarmUpTimeout = setTimeout(() => {
|
|
174
182
|
globalWarmUpTimeout = null;
|
|
175
183
|
globalWarmedUp = true;
|
|
176
|
-
showTooltip();
|
|
184
|
+
showTooltip(false);
|
|
177
185
|
}, delay);
|
|
178
186
|
} else if (!isOpen) {
|
|
179
|
-
showTooltip();
|
|
187
|
+
showTooltip(true);
|
|
180
188
|
}
|
|
181
189
|
};
|
|
182
190
|
|
|
@@ -206,11 +214,12 @@ export function useTooltipTriggerState(...args: any[]): TooltipTriggerState {
|
|
|
206
214
|
|
|
207
215
|
return {
|
|
208
216
|
isOpen,
|
|
217
|
+
shouldSkipAnimation,
|
|
209
218
|
open: (immediate) => {
|
|
210
219
|
if (!immediate && delay > 0 && !closeTimeout.current) {
|
|
211
220
|
warmupTooltip();
|
|
212
221
|
} else {
|
|
213
|
-
showTooltip();
|
|
222
|
+
showTooltip(globalWarmedUp);
|
|
214
223
|
}
|
|
215
224
|
},
|
|
216
225
|
close: hideTooltip,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource octane */
|
|
2
|
+
import type { ScrollAnchorInfo } from './ScrollAnchor';
|
|
2
3
|
// Ported from adobe/react-spectrum@1c84a49a1faf50b571c84e00bcf9c60b22ddd03e (packages/react-stately/src/virtualizer/Layout.ts).
|
|
3
4
|
/*
|
|
4
5
|
* Copyright 2020 Adobe. All rights reserved.
|
|
@@ -33,6 +34,7 @@ import { Virtualizer } from './Virtualizer';
|
|
|
33
34
|
export abstract class Layout<T extends object = Node<any>, O = any> implements LayoutDelegate {
|
|
34
35
|
/** The Virtualizer the layout is currently attached to. */
|
|
35
36
|
virtualizer: Virtualizer<T, any> | null = null;
|
|
37
|
+
UNSTABLE_getScrollAnchorInfo?(options?: O): ScrollAnchorInfo | null;
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* Returns an array of `LayoutInfo` objects which are inside the given rectangle.
|