@jetbrains/ring-ui 8.0.0-beta.3 → 8.0.0-beta.5
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/button-group/button-group.css +5 -5
- package/components/checkbox/checkbox.d.ts +1 -1
- package/components/collapsible-group/collapsible-group.css +138 -0
- package/components/collapsible-group/collapsible-group.d.ts +20 -0
- package/components/collapsible-group/collapsible-group.js +73 -0
- package/components/date-picker/consts.d.ts +1 -0
- package/components/date-picker/date-input.js +6 -2
- package/components/editable-heading/editable-heading.js +14 -14
- package/components/expand/collapsible-group.css +23 -89
- package/components/expand/collapsible-group.d.ts +8 -20
- package/components/expand/collapsible-group.js +14 -73
- package/components/global/compose-refs.d.ts +2 -1
- package/components/global/compose-refs.js +24 -8
- package/components/global/focus-with-temporary-tabindex.d.ts +11 -0
- package/components/global/focus-with-temporary-tabindex.js +21 -0
- package/components/global/intersection-observer-context.d.ts +29 -9
- package/components/global/intersection-observer-context.js +43 -24
- package/components/global/is-within-interactive-element.d.ts +6 -0
- package/components/global/is-within-interactive-element.js +26 -0
- package/components/global/is-within-navigable-element.d.ts +6 -0
- package/components/global/is-within-navigable-element.js +27 -0
- package/components/global/parse-css-duration.d.ts +5 -0
- package/components/global/parse-css-duration.js +13 -0
- package/components/global/schedule-with-cleanup.d.ts +12 -0
- package/components/global/schedule-with-cleanup.js +34 -0
- package/components/global/table-selection.js +1 -1
- 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/input/input.d.ts +1 -1
- package/components/legacy-table/row.d.ts +1 -1
- package/components/popup/popup.d.ts +2 -0
- package/components/popup/popup.js +2 -2
- package/components/select/select-popup.d.ts +1 -1
- package/components/select/select.d.ts +1 -1
- package/components/table/default-item-renderer.d.ts +29 -10
- package/components/table/default-item-renderer.js +53 -19
- 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 -0
- package/components/table/internal/table-header.js +143 -0
- package/components/table/internal/virtualization.d.ts +41 -0
- package/components/table/{table-virtualize.js → internal/virtualization.js} +74 -37
- package/components/table/item-virtualization.d.ts +37 -0
- package/components/table/item-virtualization.js +26 -0
- 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 +8 -6
- package/components/table/table-const.js +7 -5
- package/components/table/table-primitives.d.ts +52 -16
- package/components/table/table-primitives.js +15 -60
- package/components/table/table-props.d.ts +304 -0
- package/components/table/table-props.js +1 -0
- package/components/table/table.css +179 -67
- package/components/table/table.d.ts +231 -209
- package/components/table/table.js +327 -2
- package/components/user-card/smart-user-card-tooltip.d.ts +1 -1
- package/components/util-stories.d.ts +26 -0
- package/components/util-stories.js +61 -0
- package/package.json +35 -36
- package/components/date-picker/use-intersection-observer.d.ts +0 -6
- package/components/date-picker/use-intersection-observer.js +0 -48
- package/components/global/composeRefs.d.ts +0 -6
- package/components/global/composeRefs.js +0 -7
- package/components/table/table-component.d.ts +0 -80
- package/components/table/table-component.js +0 -130
- package/components/table/table-row-focus.d.ts +0 -4
- package/components/table/table-row-focus.js +0 -42
- package/components/table/table-virtualize.d.ts +0 -32
|
@@ -1,40 +1,74 @@
|
|
|
1
|
-
import { use, useRef } from 'react';
|
|
1
|
+
import { use, useCallback, useRef } from 'react';
|
|
2
2
|
import classNames from 'classnames';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { TablePropsContext } from './table-const';
|
|
4
|
+
import { useComposedRef } from '../global/compose-refs';
|
|
5
|
+
import { useItemVirtualization } from './item-virtualization';
|
|
6
6
|
import { TableCell, TableRow } from './table-primitives';
|
|
7
|
+
import { ReorderAnimationContext } from './internal/reorder-animation-context';
|
|
8
|
+
import { useReorderItemLayout } from './reorder-item-layout';
|
|
7
9
|
import styles from './table.css';
|
|
8
|
-
const INDENT_SIZE = 24;
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
+
* Standard component for rendering a table row.
|
|
12
|
+
*
|
|
13
|
+
* Renders an item using the table's column definitions and lets you
|
|
14
|
+
* configure item-scoped behavior such as selection, keyboard navigation,
|
|
15
|
+
* event handlers, `className`, and `ref`.
|
|
16
|
+
*
|
|
17
|
+
* Note that row-level click and keyboard handlers are not discoverable
|
|
18
|
+
* by assistive technologies. Make sure that any functionality relying on
|
|
19
|
+
* them (such as selection or expand/collapse) is also available through
|
|
20
|
+
* accessible controls, such as checkboxes or buttons with accessible
|
|
21
|
+
* labels.
|
|
11
22
|
*/
|
|
12
|
-
export function DefaultItemRenderer({ index, keyboardFocusable, clickable, selected, level, ref: userRef, className, ...restProps }) {
|
|
23
|
+
export function DefaultItemRenderer({ index, keyboardFocusable, clickable, selected, level, noItemVirtualization, noReorderLayout, ref: userRef, className, ...restProps }) {
|
|
13
24
|
const localRef = useRef(null);
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
const composedRef = useComposedRef(userRef, localRef);
|
|
26
|
+
useItemVirtualization({
|
|
27
|
+
index,
|
|
28
|
+
refs: localRef,
|
|
29
|
+
onIntersectionChange: useCallback(([isIntersecting], _i, [element]) => isIntersecting === false &&
|
|
30
|
+
!noItemVirtualization &&
|
|
31
|
+
element?.isConnected &&
|
|
32
|
+
!element.contains(document.activeElement) &&
|
|
33
|
+
!element.previousElementSibling?.contains(document.activeElement) &&
|
|
34
|
+
!element.nextElementSibling?.contains(document.activeElement)
|
|
35
|
+
? element.getBoundingClientRect().height
|
|
36
|
+
: undefined, [noItemVirtualization]),
|
|
37
|
+
});
|
|
38
|
+
useReorderItemLayout({
|
|
39
|
+
disabled: noReorderLayout,
|
|
40
|
+
index,
|
|
41
|
+
getBounds: () => {
|
|
42
|
+
const r = localRef.current?.getBoundingClientRect();
|
|
43
|
+
return { start: r?.top ?? 0, end: r?.bottom ?? 0 };
|
|
44
|
+
},
|
|
19
45
|
});
|
|
20
46
|
const tableProps = use(TablePropsContext);
|
|
21
47
|
if (!tableProps) {
|
|
22
48
|
return null;
|
|
23
49
|
}
|
|
50
|
+
const { reorderAnimation } = use(ReorderAnimationContext);
|
|
51
|
+
const isAnimatedItem = reorderAnimation?.direction === 'items' && reorderAnimation.index === index;
|
|
52
|
+
const animatedColumnIndex = reorderAnimation?.direction === 'columns' ? reorderAnimation.index : undefined;
|
|
53
|
+
const animateClassName = reorderAnimation?.className;
|
|
24
54
|
const { data, columns } = tableProps;
|
|
25
55
|
const item = data[index];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
56
|
+
const indentSize = 24;
|
|
57
|
+
return (<TableRow ref={composedRef} keyboardFocusable={keyboardFocusable} className={classNames(className, clickable && styles.clickableRow, selected && styles.selectedRow, isAnimatedItem && animateClassName)} {...restProps}>
|
|
58
|
+
{columns.map((column, columnIndex) => {
|
|
59
|
+
const { key, tdClassName, indent } = column;
|
|
60
|
+
return (<TableCell key={key} className={classNames(columnIndex === animatedColumnIndex && animateClassName, typeof tdClassName === 'function' ? tdClassName(item, index, data) : tdClassName)} style={indent && level != null && level > 0 ? { paddingInlineStart: `${level * indentSize}px` } : undefined}>
|
|
61
|
+
{column.renderCell?.(item, index, data) ?? getDefaultCellValue(item, columnIndex, key)}
|
|
62
|
+
</TableCell>);
|
|
63
|
+
})}
|
|
30
64
|
</TableRow>);
|
|
31
65
|
}
|
|
32
|
-
function getDefaultCellValue(item, columnIndex) {
|
|
66
|
+
function getDefaultCellValue(item, columnIndex, columnKey) {
|
|
33
67
|
if (Array.isArray(item)) {
|
|
34
|
-
return String(item[columnIndex]
|
|
68
|
+
return String(item[columnIndex]);
|
|
35
69
|
}
|
|
36
70
|
if (item !== null && typeof item === 'object') {
|
|
37
|
-
return String(
|
|
71
|
+
return String(item[String(columnKey)]);
|
|
38
72
|
}
|
|
39
73
|
if (columnIndex === 0) {
|
|
40
74
|
return String(item);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import type { Column } from '../table-props';
|
|
3
|
+
import type { ReorderAnimation } from '../reorder-animation';
|
|
4
|
+
export interface ReorderSpec {
|
|
5
|
+
direction: 'columns' | 'items';
|
|
6
|
+
fromIndex: number;
|
|
7
|
+
insertionIndex: number;
|
|
8
|
+
}
|
|
9
|
+
interface ReorderAnimationContextValue {
|
|
10
|
+
reorderAnimation: ReorderAnimation | null;
|
|
11
|
+
expectReorder: (reorderSpec: ReorderSpec) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare const ReorderAnimationContext: import("react").Context<ReorderAnimationContextValue>;
|
|
14
|
+
export declare function useReorderAnimationContextValue<T>({ noColumnReorderAnimation, noItemReorderAnimation, tableRef, data, columns, }: {
|
|
15
|
+
noColumnReorderAnimation: boolean | undefined;
|
|
16
|
+
noItemReorderAnimation: boolean | undefined;
|
|
17
|
+
tableRef: RefObject<HTMLTableElement | null>;
|
|
18
|
+
data: readonly T[];
|
|
19
|
+
columns: readonly Column<T>[];
|
|
20
|
+
}): ReorderAnimationContextValue;
|
|
21
|
+
export {};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function TableHeader<T>(): import("react").JSX.Element | null;
|