@jetbrains/ring-ui 8.0.0-beta.4 → 8.0.0-beta.6
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/components/alert/alert-actions.d.ts +4 -0
- package/components/alert/alert-actions.js +5 -0
- package/components/alert/alert-heading.d.ts +4 -0
- package/components/alert/alert-heading.js +6 -0
- package/components/alert/alert.css +63 -22
- package/components/alert/alert.d.ts +14 -1
- package/components/alert/alert.js +34 -19
- package/components/alert/container.css +1 -2
- package/components/alert-service/alert-service.d.ts +2 -1
- package/components/alert-service/alert-service.js +8 -4
- package/components/auth/auth-core.d.ts +5 -9
- package/components/auth/auth-core.js +56 -15
- package/components/banner/banner.css +3 -3
- package/components/button-group/button-group.css +5 -5
- package/components/collapse/collapse-content.d.ts +7 -0
- package/components/collapse/collapse-content.js +41 -6
- package/components/collapse/collapse-context.d.ts +1 -0
- package/components/collapse/collapse-context.js +1 -0
- package/components/collapse/collapse.d.ts +9 -0
- package/components/collapse/collapse.js +2 -1
- package/components/collapsible-group/collapsible-group.css +6 -0
- package/components/collapsible-group/collapsible-group.d.ts +14 -1
- package/components/collapsible-group/collapsible-group.js +4 -3
- package/components/date-picker/consts.d.ts +1 -0
- package/components/date-picker/date-input.js +6 -2
- package/components/expand/collapsible-group.css +4 -0
- package/components/global/variables.css +18 -18
- package/components/global/variables_dark.css +18 -18
- package/components/header/header.css +3 -3
- package/components/http/http.d.ts +2 -2
- package/components/http/http.js +2 -2
- package/components/i18n/i18n.d.ts +1 -0
- package/components/i18n/messages.json +1 -0
- package/components/table/default-item-renderer.d.ts +7 -1
- package/components/table/default-item-renderer.js +18 -5
- package/components/table/internal/reorder-animation-context.d.ts +21 -0
- package/components/table/internal/reorder-animation-context.js +60 -0
- package/components/table/internal/reorder-handle.d.ts +9 -0
- package/components/table/internal/reorder-handle.js +334 -0
- package/components/table/internal/reorder-layout-context.d.ts +16 -0
- package/components/table/internal/reorder-layout-context.js +69 -0
- package/components/table/internal/table-header.d.ts +1 -4
- package/components/table/internal/table-header.js +32 -246
- package/components/table/internal/{virtual-items.d.ts → virtualization.d.ts} +10 -3
- package/components/table/internal/{virtual-items.js → virtualization.js} +8 -6
- package/components/table/item-virtualization.d.ts +5 -3
- package/components/table/item-virtualization.js +4 -6
- package/components/table/reorder-animation.d.ts +37 -0
- package/components/table/reorder-animation.js +11 -0
- package/components/table/reorder-item-layout.d.ts +32 -0
- package/components/table/reorder-item-layout.js +23 -0
- package/components/table/table-const.d.ts +0 -32
- package/components/table/table-const.js +0 -7
- package/components/table/table-primitives.d.ts +43 -0
- package/components/table/table-primitives.js +9 -0
- package/components/table/table-props.d.ts +29 -5
- package/components/table/table.css +17 -10
- package/components/table/table.d.ts +31 -5
- package/components/table/table.js +64 -35
- package/components/tag/tag.css +3 -3
- package/components/user-card/smart-user-card-tooltip.d.ts +1 -1
- package/components/util-stories.d.ts +2 -2
- package/package.json +14 -14
- package/components/table/internal/column-animation.d.ts +0 -16
- package/components/table/internal/column-animation.js +0 -45
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createContext, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { parseCssDuration } from '../../global/parse-css-duration';
|
|
3
|
+
import { requestAnimationFrameWithCleanup, setTimeoutWithCleanup } from '../../global/schedule-with-cleanup';
|
|
4
|
+
import styles from '../table.css';
|
|
5
|
+
/** How long to wait for the data to change after {@link expectReorder} is called. */
|
|
6
|
+
const pendingReorderTimeoutMs = 1000;
|
|
7
|
+
export const ReorderAnimationContext = createContext({
|
|
8
|
+
reorderAnimation: null,
|
|
9
|
+
expectReorder: () => { },
|
|
10
|
+
});
|
|
11
|
+
export function useReorderAnimationContextValue({ noColumnReorderAnimation, noItemReorderAnimation, tableRef, data, columns, }) {
|
|
12
|
+
const [reorderAnimation, setReorderAnimation] = useState(null);
|
|
13
|
+
const pendingReorderRef = useRef(null);
|
|
14
|
+
function expectReorder(reorderSpec) {
|
|
15
|
+
const isColumn = reorderSpec.direction === 'columns';
|
|
16
|
+
if ((isColumn && noColumnReorderAnimation) || (!isColumn && noItemReorderAnimation))
|
|
17
|
+
return;
|
|
18
|
+
if (pendingReorderRef.current) {
|
|
19
|
+
window.clearTimeout(pendingReorderRef.current.timerId);
|
|
20
|
+
}
|
|
21
|
+
const timerId = window.setTimeout(() => {
|
|
22
|
+
pendingReorderRef.current = null;
|
|
23
|
+
}, pendingReorderTimeoutMs);
|
|
24
|
+
pendingReorderRef.current = { ...reorderSpec, timerId, columns, data };
|
|
25
|
+
}
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
return () => {
|
|
28
|
+
if (pendingReorderRef.current) {
|
|
29
|
+
window.clearTimeout(pendingReorderRef.current.timerId);
|
|
30
|
+
pendingReorderRef.current = null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}, []);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const table = tableRef.current;
|
|
36
|
+
const pendingReorder = pendingReorderRef.current;
|
|
37
|
+
if (!table || !pendingReorder)
|
|
38
|
+
return;
|
|
39
|
+
const { direction, data: pendingData, columns: pendingColumns } = pendingReorder;
|
|
40
|
+
const isColumn = direction === 'columns';
|
|
41
|
+
if ((isColumn && columns === pendingColumns) || (!isColumn && data === pendingData))
|
|
42
|
+
return;
|
|
43
|
+
pendingReorderRef.current = null;
|
|
44
|
+
const { fromIndex, insertionIndex } = pendingReorder;
|
|
45
|
+
// Moving forward shifts the array by one, hence -1
|
|
46
|
+
const index = fromIndex < insertionIndex ? insertionIndex - 1 : insertionIndex;
|
|
47
|
+
return requestAnimationFrameWithCleanup(() => setReorderAnimation(prev => prev == null ? { direction, index, phase: 'initial', className: styles.reorderAnimationInitial } : prev));
|
|
48
|
+
}, [data, columns, tableRef]);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (reorderAnimation?.phase === 'initial') {
|
|
51
|
+
return requestAnimationFrameWithCleanup(() => setReorderAnimation(prev => prev === reorderAnimation ? { ...prev, phase: 'fade-out', className: styles.reorderAnimationFadeOut } : prev));
|
|
52
|
+
}
|
|
53
|
+
if (reorderAnimation?.phase === 'fade-out') {
|
|
54
|
+
const fadeOutMs = parseCssDuration(window.getComputedStyle(tableRef.current).getPropertyValue('--reorder-animation-fade-out-duration'));
|
|
55
|
+
return setTimeoutWithCleanup(() => setReorderAnimation(prev => (prev === reorderAnimation ? null : prev)), fadeOutMs);
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}, [reorderAnimation, tableRef]);
|
|
59
|
+
return { reorderAnimation, expectReorder };
|
|
60
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ComponentPropsWithRef } from 'react';
|
|
2
|
+
import type { DragState } from '../table-primitives';
|
|
3
|
+
export declare function ReorderHandle<T>({ direction, index, noDragFrame, noHandleTranslate, onUserDrag, ref: userRef, className, onKeyDown, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onLostPointerCapture, ...restProps }: {
|
|
4
|
+
direction: 'columns' | 'items';
|
|
5
|
+
index: number;
|
|
6
|
+
noDragFrame?: boolean;
|
|
7
|
+
noHandleTranslate?: boolean;
|
|
8
|
+
onUserDrag?: (state: DragState) => void;
|
|
9
|
+
} & ComponentPropsWithRef<'button'>): import("react").JSX.Element;
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
import { use, useEffect, useEffectEvent, useRef, } from 'react';
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import dragIcon from '@jetbrains/icons/drag-12px';
|
|
5
|
+
import { TablePropsContext } from '../table-const';
|
|
6
|
+
import Icon from '../../icon';
|
|
7
|
+
import { useComposedRef } from '../../global/compose-refs';
|
|
8
|
+
import { parseCssDuration } from '../../global/parse-css-duration';
|
|
9
|
+
import { ReorderAnimationContext } from './reorder-animation-context';
|
|
10
|
+
import { ReorderLayoutContext } from './reorder-layout-context';
|
|
11
|
+
import styles from '../table.css';
|
|
12
|
+
const columnDragFrameAdjustmentPx = -1;
|
|
13
|
+
const itemDragFrameAdjustmentPx = -2;
|
|
14
|
+
const scrollAreaPx = 100;
|
|
15
|
+
const scrollStepPx = 100;
|
|
16
|
+
const scrollIntervalMs = 200;
|
|
17
|
+
export function ReorderHandle({ direction, index, noDragFrame, noHandleTranslate, onUserDrag, ref: userRef, className, onKeyDown, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onLostPointerCapture, ...restProps }) {
|
|
18
|
+
const localRef = useRef(null);
|
|
19
|
+
const composedRef = useComposedRef(localRef, userRef);
|
|
20
|
+
const isColumn = direction === 'columns';
|
|
21
|
+
const tableProps = use(TablePropsContext);
|
|
22
|
+
const data = tableProps?.data;
|
|
23
|
+
const columns = tableProps?.columns;
|
|
24
|
+
const canReorderItem = tableProps?.canReorderItem;
|
|
25
|
+
const onItemReorder = tableProps?.onItemReorder;
|
|
26
|
+
const onColumnReorder = tableProps?.onColumnReorder;
|
|
27
|
+
function canReorder(insertionIndex) {
|
|
28
|
+
const columnBeingReordered = columns?.[index];
|
|
29
|
+
if (isColumn && columnBeingReordered) {
|
|
30
|
+
const canReorderColumn = columnBeingReordered.canReorder;
|
|
31
|
+
if (canReorderColumn === true)
|
|
32
|
+
return true;
|
|
33
|
+
if (typeof canReorderColumn === 'function')
|
|
34
|
+
return canReorderColumn(columnBeingReordered, index, insertionIndex, columns);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (!isColumn && data) {
|
|
38
|
+
return canReorderItem ? canReorderItem(data[index], index, insertionIndex, data) : true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const { expectReorder } = use(ReorderAnimationContext);
|
|
43
|
+
function onReorder(insertionIndex) {
|
|
44
|
+
if (isColumn && columns) {
|
|
45
|
+
expectReorder({ direction, fromIndex: index, insertionIndex });
|
|
46
|
+
onColumnReorder?.(columns[index], index, insertionIndex, columns);
|
|
47
|
+
}
|
|
48
|
+
else if (!isColumn && data) {
|
|
49
|
+
expectReorder({ direction, fromIndex: index, insertionIndex });
|
|
50
|
+
onItemReorder?.(data[index], index, insertionIndex, data);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const activeDragRef = useRef(null);
|
|
54
|
+
function getDragFrame() {
|
|
55
|
+
return document.body.querySelector(`.${styles.dragFrame}`);
|
|
56
|
+
}
|
|
57
|
+
function getInsertionIndicator() {
|
|
58
|
+
return document.body.querySelector(`.${styles.insertionIndicator}`);
|
|
59
|
+
}
|
|
60
|
+
const { getItemBounds, getClosestInsertionPoint } = use(ReorderLayoutContext);
|
|
61
|
+
function renderDragFrame(clientX, clientY) {
|
|
62
|
+
const drag = activeDragRef.current;
|
|
63
|
+
if (noDragFrame || !drag)
|
|
64
|
+
return;
|
|
65
|
+
const { startX, startY, initialItemStart, initialItemEnd, indicatorStart, indicatorSize } = drag;
|
|
66
|
+
let dragFrame = getDragFrame();
|
|
67
|
+
if (!dragFrame) {
|
|
68
|
+
dragFrame = document.createElement('div');
|
|
69
|
+
dragFrame.className = styles.dragFrame;
|
|
70
|
+
const frameStart = `calc(max(0px, ${indicatorStart - 2}px))`;
|
|
71
|
+
const frameAcrossSize = `${initialItemEnd - initialItemStart}px`;
|
|
72
|
+
const frameAlongSize = indicatorSize;
|
|
73
|
+
dragFrame.style[isColumn ? 'top' : 'left'] = frameStart;
|
|
74
|
+
dragFrame.style[isColumn ? 'width' : 'height'] = frameAcrossSize;
|
|
75
|
+
dragFrame.style[isColumn ? 'height' : 'width'] = frameAlongSize;
|
|
76
|
+
document.body.appendChild(dragFrame);
|
|
77
|
+
}
|
|
78
|
+
if (isColumn) {
|
|
79
|
+
dragFrame.style.left = `${initialItemStart + clientX - startX + columnDragFrameAdjustmentPx}px`;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
dragFrame.style.top = `${initialItemStart + clientY - startY + itemDragFrameAdjustmentPx}px`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function translateButton(clientX, clientY) {
|
|
86
|
+
const btn = localRef.current;
|
|
87
|
+
const drag = activeDragRef.current;
|
|
88
|
+
if (noHandleTranslate || !btn || !drag)
|
|
89
|
+
return;
|
|
90
|
+
const { startX, startY, initialItemStart } = drag;
|
|
91
|
+
const offsetByPointerMove = isColumn ? clientX - startX : clientY - startY;
|
|
92
|
+
const { start: itemStart } = getItemBounds(index) ?? { start: 0, end: 0 };
|
|
93
|
+
const offsetByItemMove = itemStart - initialItemStart;
|
|
94
|
+
const offset = offsetByPointerMove - offsetByItemMove;
|
|
95
|
+
btn.style.transform = isColumn ? `translateX(${offset}px)` : `translateY(${offset}px)`;
|
|
96
|
+
}
|
|
97
|
+
function getClosestInsertionPointLocal(clientX, clientY) {
|
|
98
|
+
const clientOffset = isColumn ? clientX : clientY;
|
|
99
|
+
return getClosestInsertionPoint(clientOffset, canReorder);
|
|
100
|
+
}
|
|
101
|
+
function renderInsertionIndicator({ itemIndex, after }) {
|
|
102
|
+
const drag = activeDragRef.current;
|
|
103
|
+
if (!drag)
|
|
104
|
+
return;
|
|
105
|
+
const { indicatorStart, indicatorSize } = drag;
|
|
106
|
+
const itemBounds = getItemBounds(itemIndex);
|
|
107
|
+
if (!itemBounds)
|
|
108
|
+
return;
|
|
109
|
+
const { start: itemStart, end: itemEnd } = itemBounds;
|
|
110
|
+
let indicator = getInsertionIndicator();
|
|
111
|
+
if (!indicator) {
|
|
112
|
+
indicator = document.createElement('div');
|
|
113
|
+
indicator.className = styles.insertionIndicator;
|
|
114
|
+
indicator.style[isColumn ? 'top' : 'left'] = `${indicatorStart}px`;
|
|
115
|
+
indicator.style[isColumn ? 'height' : 'width'] = indicatorSize;
|
|
116
|
+
indicator.style[isColumn ? 'width' : 'height'] = '2px';
|
|
117
|
+
document.body.appendChild(indicator);
|
|
118
|
+
}
|
|
119
|
+
const itemOffset = `${(after ? itemEnd : itemStart) - 1}px`;
|
|
120
|
+
indicator.style[isColumn ? 'left' : 'top'] = itemOffset;
|
|
121
|
+
}
|
|
122
|
+
function cleanupDrag() {
|
|
123
|
+
if (activeDragRef.current) {
|
|
124
|
+
activeDragRef.current.cleanup();
|
|
125
|
+
activeDragRef.current = null;
|
|
126
|
+
}
|
|
127
|
+
const btn = localRef.current;
|
|
128
|
+
if (btn) {
|
|
129
|
+
btn.style.removeProperty('transform');
|
|
130
|
+
btn.style.removeProperty('transition');
|
|
131
|
+
}
|
|
132
|
+
getDragFrame()?.remove();
|
|
133
|
+
getInsertionIndicator()?.remove();
|
|
134
|
+
onUserDrag?.(undefined);
|
|
135
|
+
}
|
|
136
|
+
function animateNoChangeThenCleanup() {
|
|
137
|
+
const drag = activeDragRef.current;
|
|
138
|
+
if (drag?.state !== 'is-dragging')
|
|
139
|
+
return;
|
|
140
|
+
drag.state = 'ended-with-no-change';
|
|
141
|
+
drag.cleanup();
|
|
142
|
+
drag.cleanup = () => { };
|
|
143
|
+
const dragFrame = getDragFrame();
|
|
144
|
+
if (dragFrame) {
|
|
145
|
+
const { initialItemStart } = drag;
|
|
146
|
+
if (isColumn) {
|
|
147
|
+
dragFrame.style.left = `${initialItemStart + columnDragFrameAdjustmentPx}px`;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
dragFrame.style.top = `${initialItemStart + itemDragFrameAdjustmentPx}px`;
|
|
151
|
+
}
|
|
152
|
+
dragFrame.style.transition = 'left var(--ring-ease), top var(--ring-ease), opacity var(--ring-ease)';
|
|
153
|
+
dragFrame.style.opacity = '0';
|
|
154
|
+
}
|
|
155
|
+
const indicator = getInsertionIndicator();
|
|
156
|
+
if (indicator) {
|
|
157
|
+
indicator.style.opacity = '0';
|
|
158
|
+
indicator.style.transition = 'opacity var(--ring-ease)';
|
|
159
|
+
}
|
|
160
|
+
const btn = localRef.current;
|
|
161
|
+
if (btn) {
|
|
162
|
+
btn.style.transform = isColumn ? 'translateX(0)' : 'translateY(0)';
|
|
163
|
+
btn.style.transition = 'transform var(--ring-ease)';
|
|
164
|
+
}
|
|
165
|
+
onUserDrag?.('cancelled');
|
|
166
|
+
const ringEaseMs = parseCssDuration(window.getComputedStyle(document.documentElement).getPropertyValue('--ring-ease'));
|
|
167
|
+
setTimeout(cleanupDrag, ringEaseMs);
|
|
168
|
+
}
|
|
169
|
+
function handlePointerDown(e) {
|
|
170
|
+
onPointerDown?.(e);
|
|
171
|
+
if (e.defaultPrevented)
|
|
172
|
+
return;
|
|
173
|
+
const { clientX, clientY, pointerId, currentTarget } = e;
|
|
174
|
+
const { start: initialItemStart, end: initialItemEnd } = getItemBounds(index) ?? { start: 0, end: 0 };
|
|
175
|
+
let indicatorStart;
|
|
176
|
+
let indicatorSize;
|
|
177
|
+
if (isColumn) {
|
|
178
|
+
const thead = currentTarget.closest('thead');
|
|
179
|
+
const table = thead?.closest('table');
|
|
180
|
+
if (!thead || !table)
|
|
181
|
+
return;
|
|
182
|
+
const { top: headerTop } = thead.getBoundingClientRect();
|
|
183
|
+
indicatorStart = headerTop;
|
|
184
|
+
const { bottom: tableBottom } = table.getBoundingClientRect();
|
|
185
|
+
const visibleTableHeight = tableBottom - headerTop;
|
|
186
|
+
const viewportBottomRelativeToHeaderTop = window.innerHeight - headerTop;
|
|
187
|
+
indicatorSize = `min(${visibleTableHeight}px, calc(${viewportBottomRelativeToHeaderTop}px - .5rem))`;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
const tr = currentTarget.closest('tr');
|
|
191
|
+
const tbody = tr?.closest('tbody');
|
|
192
|
+
if (!tr || !tbody)
|
|
193
|
+
return;
|
|
194
|
+
const { left: itemLeft, right: itemRight } = tr.getBoundingClientRect();
|
|
195
|
+
indicatorStart = itemLeft;
|
|
196
|
+
const visibleItemWidth = itemRight - itemLeft;
|
|
197
|
+
const viewportRightRelativeToTableLeft = window.innerWidth - itemLeft;
|
|
198
|
+
indicatorSize = `min(${visibleItemWidth}px, calc(${viewportRightRelativeToTableLeft}px - .5rem))`;
|
|
199
|
+
}
|
|
200
|
+
function keydownListener(keyEvent) {
|
|
201
|
+
if (keyEvent.key === 'Escape') {
|
|
202
|
+
animateNoChangeThenCleanup();
|
|
203
|
+
keyEvent.stopPropagation();
|
|
204
|
+
keyEvent.preventDefault();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
currentTarget.setPointerCapture(pointerId);
|
|
208
|
+
document.addEventListener('keydown', keydownListener);
|
|
209
|
+
currentTarget.style.cursor = 'grabbing';
|
|
210
|
+
activeDragRef.current = {
|
|
211
|
+
state: 'is-dragging',
|
|
212
|
+
startX: clientX,
|
|
213
|
+
startY: clientY,
|
|
214
|
+
initialItemStart,
|
|
215
|
+
initialItemEnd,
|
|
216
|
+
indicatorStart,
|
|
217
|
+
indicatorSize,
|
|
218
|
+
cleanup: () => {
|
|
219
|
+
currentTarget.releasePointerCapture(pointerId);
|
|
220
|
+
document.removeEventListener('keydown', keydownListener);
|
|
221
|
+
currentTarget.style.removeProperty('cursor');
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
renderDragFrame(clientX, clientY);
|
|
225
|
+
onUserDrag?.('pointerdown');
|
|
226
|
+
e.preventDefault();
|
|
227
|
+
}
|
|
228
|
+
const scrollerRef = tableProps?.scrollerRef;
|
|
229
|
+
const lastScrolledRef = useRef(0);
|
|
230
|
+
function scrollThrottled(scrollDirection) {
|
|
231
|
+
const now = performance.now();
|
|
232
|
+
if (now > lastScrolledRef.current + scrollIntervalMs) {
|
|
233
|
+
lastScrolledRef.current = now;
|
|
234
|
+
const top = scrollDirection === 'up' ? -scrollStepPx : scrollStepPx;
|
|
235
|
+
const scroller = scrollerRef?.current ?? window;
|
|
236
|
+
scroller?.scrollBy({ top, behavior: 'smooth' });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function handlePointerMove(e) {
|
|
240
|
+
onPointerMove?.(e);
|
|
241
|
+
if (e.defaultPrevented)
|
|
242
|
+
return;
|
|
243
|
+
const drag = activeDragRef.current;
|
|
244
|
+
if (drag?.state !== 'is-dragging')
|
|
245
|
+
return;
|
|
246
|
+
const { clientX, clientY } = e;
|
|
247
|
+
renderDragFrame(clientX, clientY);
|
|
248
|
+
translateButton(clientX, clientY);
|
|
249
|
+
const insertionPoint = getClosestInsertionPointLocal(clientX, clientY);
|
|
250
|
+
if (insertionPoint)
|
|
251
|
+
renderInsertionIndicator(insertionPoint);
|
|
252
|
+
onUserDrag?.(isColumn ? clientX - drag.startX : clientY - drag.startY);
|
|
253
|
+
if (!isColumn) {
|
|
254
|
+
const scrollerRect = scrollerRef?.current?.getBoundingClientRect();
|
|
255
|
+
const scrollerTop = scrollerRect?.top ?? 0;
|
|
256
|
+
const scrollerBottom = scrollerRect ? Math.min(scrollerRect.bottom, window.innerHeight) : window.innerHeight;
|
|
257
|
+
if (clientY < scrollerTop + scrollAreaPx) {
|
|
258
|
+
scrollThrottled('up');
|
|
259
|
+
}
|
|
260
|
+
else if (clientY > scrollerBottom - scrollAreaPx) {
|
|
261
|
+
scrollThrottled('down');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function handlePointerUp(e) {
|
|
266
|
+
onPointerUp?.(e);
|
|
267
|
+
if (e.defaultPrevented)
|
|
268
|
+
return;
|
|
269
|
+
if (activeDragRef.current?.state !== 'is-dragging')
|
|
270
|
+
return;
|
|
271
|
+
const { clientX, clientY } = e;
|
|
272
|
+
const insertionPoint = getClosestInsertionPointLocal(clientX, clientY);
|
|
273
|
+
const insertionIndex = insertionPoint && insertionPoint.itemIndex + (insertionPoint.after ? 1 : 0);
|
|
274
|
+
if (insertionIndex == null || insertionIndex === index || insertionIndex === index + 1) {
|
|
275
|
+
animateNoChangeThenCleanup();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
cleanupDrag();
|
|
279
|
+
onReorder(insertionIndex);
|
|
280
|
+
}
|
|
281
|
+
function handleKeyDown(e) {
|
|
282
|
+
onKeyDown?.(e);
|
|
283
|
+
if (e.defaultPrevented)
|
|
284
|
+
return;
|
|
285
|
+
const left = isColumn && e.key === 'ArrowLeft';
|
|
286
|
+
const right = isColumn && e.key === 'ArrowRight';
|
|
287
|
+
const up = !isColumn && e.key === 'ArrowUp';
|
|
288
|
+
const down = !isColumn && e.key === 'ArrowDown';
|
|
289
|
+
if ((!left && !right && !up && !down) || !columns || !data)
|
|
290
|
+
return;
|
|
291
|
+
const backward = left || up;
|
|
292
|
+
const initialInsertionIndex = backward ? index - 1 : index + 2;
|
|
293
|
+
const maxInsertionIndex = isColumn ? columns.length : data.length;
|
|
294
|
+
const step = backward ? -1 : 1;
|
|
295
|
+
// eslint-disable-next-line yoda
|
|
296
|
+
for (let i = initialInsertionIndex; 0 <= i && i <= maxInsertionIndex; i += step) {
|
|
297
|
+
if (canReorder(i)) {
|
|
298
|
+
onReorder(i);
|
|
299
|
+
e.preventDefault();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function handlePointerCancel(e) {
|
|
305
|
+
onPointerCancel?.(e);
|
|
306
|
+
if (!e.defaultPrevented)
|
|
307
|
+
animateNoChangeThenCleanup();
|
|
308
|
+
}
|
|
309
|
+
function handleLostPointerCapture(e) {
|
|
310
|
+
onLostPointerCapture?.(e);
|
|
311
|
+
if (!e.defaultPrevented)
|
|
312
|
+
animateNoChangeThenCleanup();
|
|
313
|
+
}
|
|
314
|
+
const cleanupComponent = useEffectEvent(() => {
|
|
315
|
+
if (activeDragRef.current) {
|
|
316
|
+
cleanupDrag();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
return cleanupComponent;
|
|
321
|
+
}, []);
|
|
322
|
+
const hint = isColumn
|
|
323
|
+
? `Reorder column ${columns?.[index]?.name ?? String(columns?.[index]?.key)}.`
|
|
324
|
+
: `Reorder item ${index + 1}.`;
|
|
325
|
+
const description = isColumn
|
|
326
|
+
? 'Use Left and Right arrow keys to move the column.'
|
|
327
|
+
: 'Use Up and Down arrow keys to move the item.';
|
|
328
|
+
const shortcuts = isColumn ? 'ArrowLeft ArrowRight' : 'ArrowUp ArrowDown';
|
|
329
|
+
return (
|
|
330
|
+
// eslint-disable-next-line jsx-a11y/role-supports-aria-props
|
|
331
|
+
<button ref={composedRef} type='button' className={classNames(styles.tableButton, isColumn ? styles.columnReorderHandle : styles.itemReorderHandle, className)} aria-label={hint} aria-description={description} aria-keyshortcuts={shortcuts} onKeyDown={handleKeyDown} onPointerDown={handlePointerDown} onPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onPointerCancel={handlePointerCancel} onLostPointerCapture={handleLostPointerCapture} {...restProps}>
|
|
332
|
+
<Icon glyph={dragIcon} aria-hidden='true'/>
|
|
333
|
+
</button>);
|
|
334
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface ItemBounds {
|
|
2
|
+
start: number;
|
|
3
|
+
end: number;
|
|
4
|
+
}
|
|
5
|
+
export interface InsertionPoint {
|
|
6
|
+
itemIndex: number;
|
|
7
|
+
after: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface ReorderLayoutContextValue {
|
|
10
|
+
registerReorderItem(index: number, getBounds: () => ItemBounds): () => void;
|
|
11
|
+
getItemBounds(index: number): ItemBounds | undefined;
|
|
12
|
+
getClosestInsertionPoint(clientOffset: number, canReorder: (insertionIndex: number) => boolean): InsertionPoint | undefined;
|
|
13
|
+
}
|
|
14
|
+
export declare const ReorderLayoutContext: import("react").Context<ReorderLayoutContextValue>;
|
|
15
|
+
export declare function useReorderLayoutContextValue(): ReorderLayoutContextValue;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createContext, useRef } from 'react';
|
|
2
|
+
export const ReorderLayoutContext = createContext({
|
|
3
|
+
registerReorderItem: () => () => { },
|
|
4
|
+
getItemBounds: () => undefined,
|
|
5
|
+
getClosestInsertionPoint: () => undefined,
|
|
6
|
+
});
|
|
7
|
+
export function useReorderLayoutContextValue() {
|
|
8
|
+
const getBoundsByItemIndex = useRef([]);
|
|
9
|
+
function registerReorderItem(index, getBounds) {
|
|
10
|
+
getBoundsByItemIndex.current[index] = getBounds;
|
|
11
|
+
return () => {
|
|
12
|
+
delete getBoundsByItemIndex.current[index];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function getItemBounds(index) {
|
|
16
|
+
const getBounds = getBoundsByItemIndex.current[index];
|
|
17
|
+
return getBounds?.();
|
|
18
|
+
}
|
|
19
|
+
function getClosestInsertionPoint(clientOffset, canReorder) {
|
|
20
|
+
const candidates = getBoundsByItemIndex.current
|
|
21
|
+
.map((getBounds, itemIndex) => ({
|
|
22
|
+
itemIndex,
|
|
23
|
+
getBounds,
|
|
24
|
+
beforeAllowed: canReorder(itemIndex),
|
|
25
|
+
afterAllowed: canReorder(itemIndex + 1),
|
|
26
|
+
}))
|
|
27
|
+
.filter(({ beforeAllowed, afterAllowed }) => beforeAllowed || afterAllowed);
|
|
28
|
+
if (!candidates.length)
|
|
29
|
+
return undefined;
|
|
30
|
+
// Lazily computed closest insertion side and distance for each candidate
|
|
31
|
+
const closest = [];
|
|
32
|
+
function computeClosest(i) {
|
|
33
|
+
if (!closest[i]) {
|
|
34
|
+
const { getBounds, beforeAllowed, afterAllowed } = candidates[i];
|
|
35
|
+
const { start, end } = getBounds();
|
|
36
|
+
const beforeDist = Math.abs(clientOffset - start);
|
|
37
|
+
const afterDist = Math.abs(clientOffset - end);
|
|
38
|
+
if (!afterAllowed) {
|
|
39
|
+
closest[i] = { distance: beforeDist, after: false };
|
|
40
|
+
}
|
|
41
|
+
else if (!beforeAllowed) {
|
|
42
|
+
closest[i] = { distance: afterDist, after: true };
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const after = afterDist < beforeDist;
|
|
46
|
+
closest[i] = { distance: after ? afterDist : beforeDist, after };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return closest[i];
|
|
50
|
+
}
|
|
51
|
+
let l = 0;
|
|
52
|
+
let r = candidates.length - 1;
|
|
53
|
+
while (l < r) {
|
|
54
|
+
const m = Math.floor((l + r) / 2);
|
|
55
|
+
const { distance } = computeClosest(m);
|
|
56
|
+
if (l <= m - 1 && computeClosest(m - 1).distance < distance) {
|
|
57
|
+
r = m - 1;
|
|
58
|
+
}
|
|
59
|
+
else if (m + 1 <= r && computeClosest(m + 1).distance < distance) {
|
|
60
|
+
l = m + 1;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
return { itemIndex: candidates[m].itemIndex, after: computeClosest(m).after };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { itemIndex: candidates[l].itemIndex, after: computeClosest(l).after };
|
|
67
|
+
}
|
|
68
|
+
return { registerReorderItem, getItemBounds, getClosestInsertionPoint };
|
|
69
|
+
}
|
|
@@ -1,4 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function TableHeader<T>({ expectColumnReorder }: {
|
|
3
|
-
expectColumnReorder: ExpectColumnReorder;
|
|
4
|
-
}): import("react").JSX.Element | null;
|
|
1
|
+
export declare function TableHeader<T>(): import("react").JSX.Element | null;
|