@octanejs/aria 0.0.46 → 0.0.48
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} +243 -258
- 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} +128 -66
- package/src/grid/utils.ts +1 -0
- package/src/gridlist/{useGridListItem.ts → useGridListItem.tsx} +154 -112
- 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 +222 -0
- package/src/menu/{useSubmenuTrigger.ts → useSubmenuTrigger.tsx} +116 -151
- package/src/numberfield/{useNumberField.ts → useNumberField.tsx} +188 -238
- 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} +76 -83
- 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} +87 -79
- package/src/searchfield/useSearchField.tsx +152 -0
- package/src/select/useSelect.ts +20 -20
- package/src/selection/useSelectableCollection.tsx +755 -0
- package/src/selection/useSelectableItem.ts +48 -11
- package/src/slider/{useSliderThumb.ts → useSliderThumb.tsx} +120 -158
- package/src/spinbutton/useSpinButton.tsx +335 -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 +359 -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 +4 -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,338 @@
|
|
|
1
|
+
/** @jsxImportSource octane */
|
|
2
|
+
// Ported from adobe/react-spectrum@5ecb3333001313e83898cd07644227897e3bae1f (packages/react-stately/src/virtualizer/ScrollAnchor.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 { Key } from '@react-types/shared';
|
|
16
|
+
import { LayoutInfo } from './LayoutInfo';
|
|
17
|
+
import { Rect, RectCorner } from './Rect';
|
|
18
|
+
import { Size } from './Size';
|
|
19
|
+
|
|
20
|
+
export type ScrollAnchorAxis = 'x' | 'y';
|
|
21
|
+
export type ScrollAnchorEdge = 'start' | 'end';
|
|
22
|
+
export interface ScrollAnchor {
|
|
23
|
+
key: Key;
|
|
24
|
+
corner: RectCorner;
|
|
25
|
+
offset: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ScrollAnchorInfo {
|
|
29
|
+
/** Which edge of the content the viewport should stay anchored to. */
|
|
30
|
+
edge: 'start' | 'end';
|
|
31
|
+
/** Which axis `edge` refers to — 'y' for vertical lists, 'x' for horizontal. */
|
|
32
|
+
axis: 'x' | 'y';
|
|
33
|
+
/** Distance (px) from `edge` within which the viewport is considered "following" it. */
|
|
34
|
+
threshold: number;
|
|
35
|
+
/**
|
|
36
|
+
* Optional classifier excluding structural/ephemeral layout infos (e.g. loaders) from being
|
|
37
|
+
* selected as the anchor. Defaults to allowing any layoutInfo.
|
|
38
|
+
*/
|
|
39
|
+
isAnchorable?: (layoutInfo: LayoutInfo) => boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Minimum overlap an item must have with the viewport, along the scroll axis,
|
|
44
|
+
* to be eligible as a scroll anchor. Without this, an item that only overlaps the viewport
|
|
45
|
+
* by a sliver (e.g. 1px, essentially scrolled out of view) can still "win" the anchor
|
|
46
|
+
* tie-break over a substantially visible item.
|
|
47
|
+
*/
|
|
48
|
+
const MIN_ANCHOR_OVERLAP = 4;
|
|
49
|
+
|
|
50
|
+
function dimensionForAxis(axis: ScrollAnchorAxis): 'width' | 'height' {
|
|
51
|
+
return axis === 'x' ? 'width' : 'height';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Given a previously-captured anchor, computes the new viewport coordinate (along `axis`) needed
|
|
56
|
+
* to keep it at the same offset from the viewport's start.
|
|
57
|
+
*/
|
|
58
|
+
export function computeScrollAnchorTarget(
|
|
59
|
+
anchor: ScrollAnchor,
|
|
60
|
+
axis: ScrollAnchorAxis,
|
|
61
|
+
getLayoutInfo: (key: Key) => LayoutInfo | null,
|
|
62
|
+
visibleRect: Rect,
|
|
63
|
+
contentSize: Size,
|
|
64
|
+
): number | null {
|
|
65
|
+
let finalInfo = getLayoutInfo(anchor.key);
|
|
66
|
+
if (!finalInfo) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
let adjustment = finalInfo.rect[anchor.corner][axis] - visibleRect[axis] - anchor.offset;
|
|
70
|
+
if (adjustment === 0) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
let target = visibleRect[axis] + adjustment;
|
|
74
|
+
let dimension = dimensionForAxis(axis);
|
|
75
|
+
let max = Math.max(0, contentSize[dimension] - visibleRect[dimension]);
|
|
76
|
+
let clamped = Math.max(0, Math.min(max, target));
|
|
77
|
+
return clamped !== visibleRect[axis] ? clamped : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Picks the item to anchor scroll to: the one nearest the top of the viewport when
|
|
82
|
+
* anchoring to 'end', or nearest the bottom when anchoring to 'start'. Callers can
|
|
83
|
+
* exclude certain items (like loaders) with `isAnchorable`.
|
|
84
|
+
*/
|
|
85
|
+
export function captureScrollAnchor(
|
|
86
|
+
edge: ScrollAnchorEdge,
|
|
87
|
+
axis: ScrollAnchorAxis,
|
|
88
|
+
visibleRect: Rect,
|
|
89
|
+
visibleLayoutInfos: Iterable<[Key, LayoutInfo]>,
|
|
90
|
+
isAnchorable: (layoutInfo: LayoutInfo) => boolean = () => true,
|
|
91
|
+
): ScrollAnchor | null {
|
|
92
|
+
let dimension = dimensionForAxis(axis);
|
|
93
|
+
let best: ScrollAnchor | null = null;
|
|
94
|
+
for (let [key, layoutInfo] of visibleLayoutInfos) {
|
|
95
|
+
if (!layoutInfo || !isAnchorable(layoutInfo)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
let overlap = layoutInfo.rect.intersection(visibleRect)[dimension];
|
|
99
|
+
if (layoutInfo.rect.area > 0 && overlap >= MIN_ANCHOR_OVERLAP) {
|
|
100
|
+
let corner = layoutInfo.rect.getCornerInRect(visibleRect) ?? 'topLeft';
|
|
101
|
+
let offset = layoutInfo.rect[corner][axis] - visibleRect[axis];
|
|
102
|
+
let isBetter = !best || (edge === 'end' ? offset < best.offset : offset > best.offset);
|
|
103
|
+
if (isBetter) {
|
|
104
|
+
best = { key, corner, offset };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return best;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Returns the viewport coordinate (along `axis`) that pins the viewport to `edge` of the content. */
|
|
112
|
+
export function getEdgeSnapTarget(
|
|
113
|
+
edge: ScrollAnchorEdge,
|
|
114
|
+
axis: ScrollAnchorAxis,
|
|
115
|
+
contentSize: Size,
|
|
116
|
+
previousVisibleRect: Rect,
|
|
117
|
+
): number {
|
|
118
|
+
if (edge === 'start') {
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
let dimension = dimensionForAxis(axis);
|
|
122
|
+
return Math.max(0, contentSize[dimension] - previousVisibleRect[dimension]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Whether the viewport is currently within `threshold` px of the anchored edge — used by
|
|
127
|
+
* Virtualizer to compute wasNearAnchorEdge generically, without any layout-specific state.
|
|
128
|
+
*/
|
|
129
|
+
export function isNearEdge(
|
|
130
|
+
visibleRect: Rect,
|
|
131
|
+
contentSize: Size,
|
|
132
|
+
edge: ScrollAnchorEdge,
|
|
133
|
+
axis: ScrollAnchorAxis,
|
|
134
|
+
threshold: number,
|
|
135
|
+
): boolean {
|
|
136
|
+
if (edge === 'start') {
|
|
137
|
+
return visibleRect[axis] <= threshold;
|
|
138
|
+
}
|
|
139
|
+
let dimension = dimensionForAxis(axis);
|
|
140
|
+
let distanceFromEnd = contentSize[dimension] - (visibleRect[axis] + visibleRect[dimension]);
|
|
141
|
+
return distanceFromEnd <= threshold;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Works out the new scroll position after content changes. Tries to keep the anchor
|
|
146
|
+
* item where it was. If that doesn't apply, sticks the view to the edge instead, but
|
|
147
|
+
* only if the user was already near the edge and didn't just scroll away on their own.
|
|
148
|
+
*/
|
|
149
|
+
export function resolveScrollAdjustment(
|
|
150
|
+
edge: ScrollAnchorEdge,
|
|
151
|
+
axis: ScrollAnchorAxis,
|
|
152
|
+
anchor: ScrollAnchor | null,
|
|
153
|
+
wasNearAnchorEdge: boolean,
|
|
154
|
+
isScrolling: boolean,
|
|
155
|
+
itemSizeChanged: boolean,
|
|
156
|
+
contentSizeDelta: number,
|
|
157
|
+
getLayoutInfo: (key: Key) => LayoutInfo | null,
|
|
158
|
+
previousVisibleRect: Rect,
|
|
159
|
+
contentSize: Size,
|
|
160
|
+
): Rect | null {
|
|
161
|
+
let withTarget = (target: number): Rect =>
|
|
162
|
+
axis === 'x'
|
|
163
|
+
? new Rect(
|
|
164
|
+
target,
|
|
165
|
+
previousVisibleRect.y,
|
|
166
|
+
previousVisibleRect.width,
|
|
167
|
+
previousVisibleRect.height,
|
|
168
|
+
)
|
|
169
|
+
: new Rect(
|
|
170
|
+
previousVisibleRect.x,
|
|
171
|
+
target,
|
|
172
|
+
previousVisibleRect.width,
|
|
173
|
+
previousVisibleRect.height,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (anchor) {
|
|
177
|
+
let target = computeScrollAnchorTarget(
|
|
178
|
+
anchor,
|
|
179
|
+
axis,
|
|
180
|
+
getLayoutInfo,
|
|
181
|
+
previousVisibleRect,
|
|
182
|
+
contentSize,
|
|
183
|
+
);
|
|
184
|
+
if (target != null) {
|
|
185
|
+
return withTarget(target);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (wasNearAnchorEdge && !isScrolling && (!itemSizeChanged || contentSizeDelta > 0)) {
|
|
190
|
+
let target = withTarget(getEdgeSnapTarget(edge, axis, contentSize, previousVisibleRect));
|
|
191
|
+
return target.equals(previousVisibleRect) ? null : target;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface ResolveAfterLayoutOptions {
|
|
198
|
+
anchorInfo: ScrollAnchorInfo | null;
|
|
199
|
+
/** The anchor captured by `captureBeforeLayout` before this pass's `layout.update()` ran. */
|
|
200
|
+
anchor: ScrollAnchor | null;
|
|
201
|
+
/** The full post-layout visible layout infos, i.e. `virtualizer.getVisibleLayoutInfos()`. */
|
|
202
|
+
postLayoutInfos: Map<Key, LayoutInfo>;
|
|
203
|
+
previousVisibleRect: Rect;
|
|
204
|
+
previousContentSize: Size;
|
|
205
|
+
contentSize: Size;
|
|
206
|
+
itemSizeChanged: boolean;
|
|
207
|
+
isScrolling: boolean;
|
|
208
|
+
getLayoutInfo: (key: Key) => LayoutInfo | null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Tracks the cross-pass state needed to keep the viewport anchored to a layout's edge across
|
|
213
|
+
* relayouts.
|
|
214
|
+
*/
|
|
215
|
+
export class ScrollAnchorTracker {
|
|
216
|
+
private hasSnappedToEdge = false;
|
|
217
|
+
private hadEstimatedVisibleItems = false;
|
|
218
|
+
private wasNearAnchorEdge = false;
|
|
219
|
+
|
|
220
|
+
/** Resets all tracked state, e.g. when the virtualizer's layout instance changes. */
|
|
221
|
+
reset(): void {
|
|
222
|
+
this.hasSnappedToEdge = false;
|
|
223
|
+
this.hadEstimatedVisibleItems = false;
|
|
224
|
+
this.wasNearAnchorEdge = false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Captures the anchor from pre-layout view positions.
|
|
229
|
+
*/
|
|
230
|
+
captureBeforeLayout(
|
|
231
|
+
anchorInfo: ScrollAnchorInfo | null,
|
|
232
|
+
preLayoutInfos: Iterable<[Key, LayoutInfo]>,
|
|
233
|
+
visibleRect: Rect,
|
|
234
|
+
): ScrollAnchor | null {
|
|
235
|
+
if (!anchorInfo) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
return captureScrollAnchor(
|
|
239
|
+
anchorInfo.edge,
|
|
240
|
+
anchorInfo.axis,
|
|
241
|
+
visibleRect,
|
|
242
|
+
preLayoutInfos,
|
|
243
|
+
anchorInfo.isAnchorable,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Runs the full post-layout decision: updates the tracked state for the next pass, and
|
|
249
|
+
* returns the resolved scroll target, or null if nothing should change.
|
|
250
|
+
*/
|
|
251
|
+
resolveAfterLayout(options: ResolveAfterLayoutOptions): Rect | null {
|
|
252
|
+
let {
|
|
253
|
+
anchorInfo,
|
|
254
|
+
anchor,
|
|
255
|
+
postLayoutInfos,
|
|
256
|
+
previousVisibleRect,
|
|
257
|
+
previousContentSize,
|
|
258
|
+
contentSize,
|
|
259
|
+
itemSizeChanged,
|
|
260
|
+
isScrolling,
|
|
261
|
+
getLayoutInfo,
|
|
262
|
+
} = options;
|
|
263
|
+
|
|
264
|
+
if (!anchorInfo) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Read the previous pass's state into locals before any writes below overwrite it.
|
|
269
|
+
let wasSettlingLastPass = this.hadEstimatedVisibleItems;
|
|
270
|
+
let wasNearAnchorEdgeLastPass = this.wasNearAnchorEdge;
|
|
271
|
+
|
|
272
|
+
let hasEstimated = false;
|
|
273
|
+
for (let layoutInfo of postLayoutInfos.values()) {
|
|
274
|
+
if (layoutInfo.estimatedSize) {
|
|
275
|
+
hasEstimated = true;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
this.hadEstimatedVisibleItems = hasEstimated;
|
|
280
|
+
// Don't recheck "near edge?" mid-resize because it could look like a scroll that never happened.
|
|
281
|
+
// Reuse the answer from before the resizing started.
|
|
282
|
+
if (!wasSettlingLastPass) {
|
|
283
|
+
this.wasNearAnchorEdge = isNearEdge(
|
|
284
|
+
previousVisibleRect,
|
|
285
|
+
previousContentSize,
|
|
286
|
+
anchorInfo.edge,
|
|
287
|
+
anchorInfo.axis,
|
|
288
|
+
anchorInfo.threshold,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (previousVisibleRect.area === 0) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let dimension = anchorInfo.axis === 'x' ? 'width' : 'height';
|
|
297
|
+
let contentSizeDelta = contentSize[dimension] - previousContentSize[dimension];
|
|
298
|
+
let isFirstAnchoredLayout = !this.hasSnappedToEdge;
|
|
299
|
+
this.hasSnappedToEdge = true;
|
|
300
|
+
|
|
301
|
+
// Only modify scroll when content actually changed (or this is the first layout, which always snaps)
|
|
302
|
+
if (!(isFirstAnchoredLayout || contentSizeDelta !== 0 || itemSizeChanged)) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let wasNearAnchorEdge =
|
|
307
|
+
isFirstAnchoredLayout ||
|
|
308
|
+
(wasSettlingLastPass && wasNearAnchorEdgeLastPass) ||
|
|
309
|
+
isNearEdge(
|
|
310
|
+
previousVisibleRect,
|
|
311
|
+
previousContentSize,
|
|
312
|
+
anchorInfo.edge,
|
|
313
|
+
anchorInfo.axis,
|
|
314
|
+
anchorInfo.threshold,
|
|
315
|
+
);
|
|
316
|
+
// A first-ever layout always snaps to the edge, even if the raw distance check says
|
|
317
|
+
// otherwise. Save that real decision here so later passes in this cascade reuse it.
|
|
318
|
+
if (!wasSettlingLastPass) {
|
|
319
|
+
this.wasNearAnchorEdge = wasNearAnchorEdge;
|
|
320
|
+
}
|
|
321
|
+
// Skip restoring to the captured anchor while still resizing because items above it are also still growing,
|
|
322
|
+
// and following it would fall short of the edge.
|
|
323
|
+
let effectiveAnchor =
|
|
324
|
+
isFirstAnchoredLayout || (wasSettlingLastPass && wasNearAnchorEdgeLastPass) ? null : anchor;
|
|
325
|
+
return resolveScrollAdjustment(
|
|
326
|
+
anchorInfo.edge,
|
|
327
|
+
anchorInfo.axis,
|
|
328
|
+
effectiveAnchor,
|
|
329
|
+
wasNearAnchorEdge,
|
|
330
|
+
isScrolling,
|
|
331
|
+
itemSizeChanged,
|
|
332
|
+
contentSizeDelta,
|
|
333
|
+
getLayoutInfo,
|
|
334
|
+
previousVisibleRect,
|
|
335
|
+
contentSize,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -26,6 +26,7 @@ import { LayoutInfo } from './LayoutInfo';
|
|
|
26
26
|
import { OverscanManager } from './OverscanManager';
|
|
27
27
|
import { Point } from './Point';
|
|
28
28
|
import { Rect } from './Rect';
|
|
29
|
+
import { ScrollAnchor, ScrollAnchorTracker } from './ScrollAnchor';
|
|
29
30
|
import { Size } from './Size';
|
|
30
31
|
|
|
31
32
|
interface VirtualizerOptions<T extends object, V> {
|
|
@@ -78,6 +79,7 @@ export class Virtualizer<T extends object, V> {
|
|
|
78
79
|
private _invalidationContext: InvalidationContext;
|
|
79
80
|
private _overscanManager: OverscanManager;
|
|
80
81
|
|
|
82
|
+
private _scrollAnchor: ScrollAnchorTracker;
|
|
81
83
|
constructor(options: VirtualizerOptions<T, V>) {
|
|
82
84
|
this.delegate = options.delegate;
|
|
83
85
|
this.collection = options.collection;
|
|
@@ -92,6 +94,7 @@ export class Virtualizer<T extends object, V> {
|
|
|
92
94
|
this._isScrolling = false;
|
|
93
95
|
this._invalidationContext = {};
|
|
94
96
|
this._overscanManager = new OverscanManager();
|
|
97
|
+
this._scrollAnchor = new ScrollAnchorTracker();
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
/** Returns whether the given key, or an ancestor, is persisted. */
|
|
@@ -175,11 +178,50 @@ export class Virtualizer<T extends object, V> {
|
|
|
175
178
|
|
|
176
179
|
private relayout(context: InvalidationContext = {}) {
|
|
177
180
|
// Update the layout
|
|
181
|
+
let anchorInfo = this.layout.UNSTABLE_getScrollAnchorInfo?.(context.layoutOptions) ?? null;
|
|
182
|
+
|
|
183
|
+
// Capture scroll anchor from current (pre-layout) view positions.
|
|
184
|
+
// On first render _visibleViews is empty so no anchor will be found.
|
|
185
|
+
let anchor: ScrollAnchor | null = null;
|
|
186
|
+
if (anchorInfo) {
|
|
187
|
+
let preLayoutInfos: [Key, LayoutInfo][] = [];
|
|
188
|
+
for (let [key, view] of this._visibleViews) {
|
|
189
|
+
let layoutInfo = this.layout.getLayoutInfo(key) ?? view.layoutInfo;
|
|
190
|
+
if (layoutInfo) {
|
|
191
|
+
preLayoutInfos.push([key, layoutInfo]);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
anchor = this._scrollAnchor.captureBeforeLayout(anchorInfo, preLayoutInfos, this.visibleRect);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let previousContentSize = this.contentSize;
|
|
198
|
+
let previousVisibleRect = this.visibleRect;
|
|
178
199
|
this.layout.update(context);
|
|
179
|
-
|
|
200
|
+
|
|
201
|
+
let rawContentSize = this.layout.getContentSize();
|
|
202
|
+
(this as Mutable<this>).contentSize = new Size(rawContentSize.width, rawContentSize.height);
|
|
203
|
+
|
|
204
|
+
let target = this._scrollAnchor.resolveAfterLayout({
|
|
205
|
+
anchorInfo,
|
|
206
|
+
anchor,
|
|
207
|
+
postLayoutInfos: anchorInfo ? this.getVisibleLayoutInfos() : new Map(),
|
|
208
|
+
previousVisibleRect,
|
|
209
|
+
previousContentSize,
|
|
210
|
+
contentSize: this.contentSize,
|
|
211
|
+
itemSizeChanged: context.itemSizeChanged ?? false,
|
|
212
|
+
isScrolling: this._isScrolling,
|
|
213
|
+
getLayoutInfo: (key: Key) => this.layout.getLayoutInfo(key),
|
|
214
|
+
});
|
|
180
215
|
|
|
181
216
|
// Constrain scroll position.
|
|
182
217
|
// If the content changed, scroll to the top.
|
|
218
|
+
if (target) {
|
|
219
|
+
// Queues a new render cycle. Return early to skip updateSubviews — running it now
|
|
220
|
+
// would position views against the old visibleRect, causing a flash before the
|
|
221
|
+
// incoming relayout corrects them.
|
|
222
|
+
this.delegate.setVisibleRect(target);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
183
225
|
let visibleRect = this.visibleRect;
|
|
184
226
|
let contentOffsetX = context.contentChanged ? 0 : visibleRect.x;
|
|
185
227
|
let contentOffsetY = context.contentChanged ? 0 : visibleRect.y;
|
|
@@ -289,6 +331,8 @@ export class Virtualizer<T extends object, V> {
|
|
|
289
331
|
let needsLayout = false;
|
|
290
332
|
let offsetChanged = false;
|
|
291
333
|
let sizeChanged = false;
|
|
334
|
+
let widthChanged = false;
|
|
335
|
+
let heightChanged = false;
|
|
292
336
|
let itemSizeChanged = false;
|
|
293
337
|
let layoutOptionsChanged = false;
|
|
294
338
|
let needsUpdate = false;
|
|
@@ -305,6 +349,7 @@ export class Virtualizer<T extends object, V> {
|
|
|
305
349
|
|
|
306
350
|
opts.layout.virtualizer = this;
|
|
307
351
|
mutableThis.layout = opts.layout;
|
|
352
|
+
this._scrollAnchor.reset();
|
|
308
353
|
needsLayout = true;
|
|
309
354
|
}
|
|
310
355
|
|
|
@@ -335,6 +380,8 @@ export class Virtualizer<T extends object, V> {
|
|
|
335
380
|
if (shouldInvalidate) {
|
|
336
381
|
offsetChanged = !opts.visibleRect.pointEquals(this.visibleRect);
|
|
337
382
|
sizeChanged = !this.size.equals(opts.size);
|
|
383
|
+
widthChanged = this.size.width !== opts.size.width;
|
|
384
|
+
heightChanged = this.size.height !== opts.size.height;
|
|
338
385
|
needsLayout = true;
|
|
339
386
|
} else {
|
|
340
387
|
needsUpdate = true;
|
|
@@ -347,6 +394,8 @@ export class Virtualizer<T extends object, V> {
|
|
|
347
394
|
if (opts.invalidationContext !== this._invalidationContext) {
|
|
348
395
|
if (opts.invalidationContext) {
|
|
349
396
|
sizeChanged ||= opts.invalidationContext.sizeChanged || false;
|
|
397
|
+
widthChanged ||= opts.invalidationContext.widthChanged || false;
|
|
398
|
+
heightChanged ||= opts.invalidationContext.heightChanged || false;
|
|
350
399
|
offsetChanged ||= opts.invalidationContext.offsetChanged || false;
|
|
351
400
|
itemSizeChanged ||= opts.invalidationContext.itemSizeChanged || false;
|
|
352
401
|
layoutOptionsChanged ||=
|
|
@@ -374,6 +423,8 @@ export class Virtualizer<T extends object, V> {
|
|
|
374
423
|
this.relayout({
|
|
375
424
|
offsetChanged,
|
|
376
425
|
sizeChanged,
|
|
426
|
+
widthChanged,
|
|
427
|
+
heightChanged,
|
|
377
428
|
itemSizeChanged,
|
|
378
429
|
layoutOptionsChanged,
|
|
379
430
|
layoutOptions: this._invalidationContext.layoutOptions,
|
|
@@ -21,6 +21,8 @@ export interface InvalidationContext<O = any> {
|
|
|
21
21
|
contentChanged?: boolean;
|
|
22
22
|
offsetChanged?: boolean;
|
|
23
23
|
sizeChanged?: boolean;
|
|
24
|
+
widthChanged?: boolean;
|
|
25
|
+
heightChanged?: boolean;
|
|
24
26
|
itemSizeChanged?: boolean;
|
|
25
27
|
layoutOptionsChanged?: boolean;
|
|
26
28
|
layoutOptions?: O;
|
|
@@ -32,6 +32,18 @@ export interface AriaTableCellProps {
|
|
|
32
32
|
* @deprecated
|
|
33
33
|
*/
|
|
34
34
|
onAction?: () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Whether the cell or its first focusable child element should be focused when the cell is
|
|
37
|
+
* focused. Defaults to 'child' in arrow keyboard navigation mode and 'cell' in
|
|
38
|
+
* tab keyboard navigation mode.
|
|
39
|
+
*/
|
|
40
|
+
focusMode?: 'child' | 'cell';
|
|
41
|
+
/**
|
|
42
|
+
* Whether the cell should support arrow key navigation even when the containing table uses
|
|
43
|
+
* tab keyboard navigation.Allows users to navigate between rows and cells with arrow keys while
|
|
44
|
+
* focus is on an interactive child element within the cell.
|
|
45
|
+
*/
|
|
46
|
+
allowsArrowNavigation?: boolean;
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
export interface TableCellAria {
|
|
@@ -38,6 +38,13 @@ export interface AriaTableColumnHeaderProps<T> {
|
|
|
38
38
|
* virtual scroller.
|
|
39
39
|
*/
|
|
40
40
|
isVirtualized?: boolean;
|
|
41
|
+
focusMode?: 'child' | 'cell';
|
|
42
|
+
/**
|
|
43
|
+
* Whether the column header should support arrow key navigation even when the containing table
|
|
44
|
+
* uses tab keyboard navigation. Allows users to navigate between columns with arrow keys while
|
|
45
|
+
* focus is on an interactive child element within the cell.
|
|
46
|
+
*/
|
|
47
|
+
allowsArrowNavigation?: boolean;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
export interface TableColumnHeaderAria {
|
|
@@ -77,7 +84,7 @@ export function useTableColumnHeader(...args: any[]): TableColumnHeaderAria {
|
|
|
77
84
|
let allowsSorting = node.props.allowsSorting;
|
|
78
85
|
// if there are no focusable children, the column header will focus the cell
|
|
79
86
|
let { gridCellProps } = useGridCell(
|
|
80
|
-
{
|
|
87
|
+
{ focusMode: 'child', ...props },
|
|
81
88
|
state,
|
|
82
89
|
ref,
|
|
83
90
|
subSlot(slot, 'gridCell'),
|