uikit-react-public 0.45.5 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/DropZone/DropZone.d.ts +21 -0
- package/dist/components/DropZone/DropZone.stories.d.ts +52 -0
- package/dist/components/DropZone/__tests__/DropZone.test.d.ts +1 -0
- package/dist/components/DropZone/index.d.ts +2 -0
- package/dist/components/Skeleton/Skeleton.d.ts +35 -0
- package/dist/components/Skeleton/Skeleton.stories.d.ts +27 -0
- package/dist/components/Skeleton/SkeletonAvatar.d.ts +7 -0
- package/dist/components/Skeleton/SkeletonInput.d.ts +4 -0
- package/dist/components/Skeleton/SkeletonTableRow.d.ts +9 -0
- package/dist/components/Skeleton/SkeletonText.d.ts +4 -0
- package/dist/components/Skeleton/__tests__/Skeleton.test.d.ts +1 -0
- package/dist/components/Skeleton/index.d.ts +6 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/hooks/__tests__/useDraggable.test.d.ts +1 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/useDraggable.d.ts +31 -0
- package/dist/hooks/useDropZone.d.ts +57 -0
- package/dist/index.js +7229 -6665
- package/lib/components/DropZone/DropZone.mdx +72 -0
- package/lib/components/DropZone/DropZone.stories.tsx +139 -0
- package/lib/components/DropZone/DropZone.tsx +213 -0
- package/lib/components/DropZone/__tests__/DropZone.test.tsx +378 -0
- package/lib/components/DropZone/__tests__/__snapshots__/DropZone.test.tsx.snap +46 -0
- package/lib/components/DropZone/index.ts +2 -0
- package/lib/components/Skeleton/Skeleton.mdx +34 -0
- package/lib/components/Skeleton/Skeleton.stories.tsx +77 -0
- package/lib/components/Skeleton/Skeleton.tsx +158 -0
- package/lib/components/Skeleton/SkeletonAvatar.tsx +20 -0
- package/lib/components/Skeleton/SkeletonInput.tsx +13 -0
- package/lib/components/Skeleton/SkeletonTableRow.tsx +44 -0
- package/lib/components/Skeleton/SkeletonText.tsx +13 -0
- package/lib/components/Skeleton/__tests__/Skeleton.test.tsx +86 -0
- package/lib/components/Skeleton/index.ts +6 -0
- package/lib/components/index.ts +13 -0
- package/lib/hooks/__tests__/useDraggable.test.tsx +184 -0
- package/lib/hooks/index.ts +19 -0
- package/lib/hooks/useDraggable.ts +243 -0
- package/lib/hooks/useDropZone.ts +418 -0
- package/package.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Skeleton, SkeletonProps } from './Skeleton';
|
|
2
|
+
|
|
3
|
+
export interface SkeletonAvatarProps extends Omit<
|
|
4
|
+
SkeletonProps,
|
|
5
|
+
'variant' | 'width' | 'height' | 'count'
|
|
6
|
+
> {
|
|
7
|
+
/** Matches the `Avatar` size options. @default 72 */
|
|
8
|
+
size?: 32 | 48 | 56 | 72 | 80;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SkeletonAvatar = ({ size = 72, ...props }: SkeletonAvatarProps) => (
|
|
12
|
+
<Skeleton
|
|
13
|
+
variant='circle'
|
|
14
|
+
width={size}
|
|
15
|
+
height={size}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export default SkeletonAvatar;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Skeleton, SkeletonProps } from './Skeleton';
|
|
2
|
+
|
|
3
|
+
export type SkeletonInputProps = Omit<SkeletonProps, 'variant' | 'count'>;
|
|
4
|
+
|
|
5
|
+
const SkeletonInput = ({ height = 48, ...props }: SkeletonInputProps) => (
|
|
6
|
+
<Skeleton
|
|
7
|
+
variant='rect'
|
|
8
|
+
height={height}
|
|
9
|
+
{...props}
|
|
10
|
+
/>
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export default SkeletonInput;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { HTMLAttributes } from 'react';
|
|
2
|
+
import { css, cx } from '@emotion/css';
|
|
3
|
+
import { useTheme } from '../../theme';
|
|
4
|
+
import { Skeleton } from './Skeleton';
|
|
5
|
+
|
|
6
|
+
export const NAME = 'ucl-uikit-skeleton__table-row';
|
|
7
|
+
|
|
8
|
+
export interface SkeletonTableRowProps extends HTMLAttributes<HTMLTableRowElement> {
|
|
9
|
+
/** Number of cells to render. @default 4 */
|
|
10
|
+
columns?: number;
|
|
11
|
+
testId?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SkeletonTableRow = ({
|
|
15
|
+
columns = 4,
|
|
16
|
+
testId = NAME,
|
|
17
|
+
className,
|
|
18
|
+
...props
|
|
19
|
+
}: SkeletonTableRowProps) => {
|
|
20
|
+
const [theme] = useTheme();
|
|
21
|
+
|
|
22
|
+
const cellStyle = css`
|
|
23
|
+
padding: ${theme.padding.p12} ${theme.padding.p16};
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<tr
|
|
28
|
+
className={cx(NAME, className)}
|
|
29
|
+
data-testid={testId}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
{Array.from({ length: columns }, (_, index) => (
|
|
33
|
+
<td
|
|
34
|
+
key={index}
|
|
35
|
+
className={cellStyle}
|
|
36
|
+
>
|
|
37
|
+
<Skeleton variant='text' />
|
|
38
|
+
</td>
|
|
39
|
+
))}
|
|
40
|
+
</tr>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default SkeletonTableRow;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Skeleton, SkeletonProps } from './Skeleton';
|
|
2
|
+
|
|
3
|
+
export type SkeletonTextProps = Omit<SkeletonProps, 'variant'>;
|
|
4
|
+
|
|
5
|
+
const SkeletonText = ({ count = 4, ...props }: SkeletonTextProps) => (
|
|
6
|
+
<Skeleton
|
|
7
|
+
variant='text'
|
|
8
|
+
count={count}
|
|
9
|
+
{...props}
|
|
10
|
+
/>
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export default SkeletonText;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { ThemeContextProvider } from '../../../theme/useTheme';
|
|
4
|
+
import darkTheme from '../../../theme/dark/darkTheme';
|
|
5
|
+
import lightTheme from '../../../theme/light/lightTheme';
|
|
6
|
+
import Skeleton, { getSkeletonShimmerBackground } from '../Skeleton';
|
|
7
|
+
|
|
8
|
+
const wrap = (ui: React.ReactElement) =>
|
|
9
|
+
render(<ThemeContextProvider>{ui}</ThemeContextProvider>);
|
|
10
|
+
|
|
11
|
+
describe('Skeleton', () => {
|
|
12
|
+
test('renders one bar per count', () => {
|
|
13
|
+
const { getByTestId } = wrap(
|
|
14
|
+
<Skeleton
|
|
15
|
+
count={4}
|
|
16
|
+
testId='skeleton'
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
expect(getByTestId('skeleton').childElementCount).toBe(4);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('is hidden from assistive technology', () => {
|
|
24
|
+
const { getByTestId } = wrap(<Skeleton testId='skeleton' />);
|
|
25
|
+
|
|
26
|
+
expect(getByTestId('skeleton')).toHaveAttribute('aria-hidden', 'true');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('applies a custom class name', () => {
|
|
30
|
+
const { getByTestId } = wrap(
|
|
31
|
+
<Skeleton
|
|
32
|
+
className='custom-class'
|
|
33
|
+
testId='skeleton'
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(getByTestId('skeleton')).toHaveClass('custom-class');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('renders a 61.8% final line for multiline text', () => {
|
|
41
|
+
const { getByTestId } = wrap(
|
|
42
|
+
<Skeleton
|
|
43
|
+
count={4}
|
|
44
|
+
testId='skeleton'
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(getByTestId('skeleton').lastElementChild).toHaveStyle({
|
|
49
|
+
width: '61.8%',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('does not shorten a single text line', () => {
|
|
54
|
+
const { getByTestId } = wrap(<Skeleton testId='skeleton' />);
|
|
55
|
+
|
|
56
|
+
expect(getByTestId('skeleton').firstElementChild).not.toHaveAttribute(
|
|
57
|
+
'style'
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('Skeleton.TableRow renders one cell per column', () => {
|
|
62
|
+
const { getByTestId } = wrap(
|
|
63
|
+
<table>
|
|
64
|
+
<tbody>
|
|
65
|
+
<Skeleton.TableRow
|
|
66
|
+
columns={4}
|
|
67
|
+
testId='skeleton-row'
|
|
68
|
+
/>
|
|
69
|
+
</tbody>
|
|
70
|
+
</table>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(getByTestId('skeleton-row').querySelectorAll('td')).toHaveLength(4);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test.each([
|
|
77
|
+
['light', lightTheme],
|
|
78
|
+
['dark', darkTheme],
|
|
79
|
+
])('uses semantic theme colours for the %s shimmer', (_, theme) => {
|
|
80
|
+
const shimmer = getSkeletonShimmerBackground(theme);
|
|
81
|
+
|
|
82
|
+
expect(shimmer).toContain(theme.colour.bg.default);
|
|
83
|
+
expect(shimmer).toContain(theme.colour.surface.secondary);
|
|
84
|
+
expect(shimmer).toContain(theme.colour.fill.subtle);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { default } from './Skeleton';
|
|
2
|
+
export type { SkeletonProps, SkeletonVariant } from './Skeleton';
|
|
3
|
+
export type { SkeletonAvatarProps } from './SkeletonAvatar';
|
|
4
|
+
export type { SkeletonInputProps } from './SkeletonInput';
|
|
5
|
+
export type { SkeletonTableRowProps } from './SkeletonTableRow';
|
|
6
|
+
export type { SkeletonTextProps } from './SkeletonText';
|
package/lib/components/index.ts
CHANGED
|
@@ -56,6 +56,16 @@ export type {
|
|
|
56
56
|
ProgressBarVariant,
|
|
57
57
|
} from './ProgressBar';
|
|
58
58
|
|
|
59
|
+
export { default as Skeleton } from './Skeleton';
|
|
60
|
+
export type {
|
|
61
|
+
SkeletonProps,
|
|
62
|
+
SkeletonVariant,
|
|
63
|
+
SkeletonTextProps,
|
|
64
|
+
SkeletonAvatarProps,
|
|
65
|
+
SkeletonInputProps,
|
|
66
|
+
SkeletonTableRowProps,
|
|
67
|
+
} from './Skeleton';
|
|
68
|
+
|
|
59
69
|
export { default as Textarea } from './Textarea';
|
|
60
70
|
export type { TextareaProps } from './Textarea';
|
|
61
71
|
|
|
@@ -215,6 +225,9 @@ export type { CalendarProps, CalendarStatuses } from './Calendar';
|
|
|
215
225
|
|
|
216
226
|
export { default as WeekPicker } from './WeekPicker';
|
|
217
227
|
|
|
228
|
+
export { default as DropZone } from './DropZone';
|
|
229
|
+
export type { DropZoneProps, DropZoneRenderProps } from './DropZone';
|
|
230
|
+
|
|
218
231
|
export { default as FileInput } from './FileInput';
|
|
219
232
|
export type { FileInputProps } from './FileInput';
|
|
220
233
|
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test, vitest } from 'vitest';
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { UIKIT_DND_DATA_TYPE, useDraggable } from '..';
|
|
4
|
+
import { clearActiveKeyboardDrag, getActiveKeyboardDrag } from '../useDropZone';
|
|
5
|
+
|
|
6
|
+
const TABLE_CELL_TYPE = 'application/x-ucl-uikit-table-cell';
|
|
7
|
+
|
|
8
|
+
const createDataTransfer = () =>
|
|
9
|
+
({
|
|
10
|
+
effectAllowed: 'all',
|
|
11
|
+
setData: vitest.fn(),
|
|
12
|
+
setDragImage: vitest.fn(),
|
|
13
|
+
}) as unknown as DataTransfer;
|
|
14
|
+
|
|
15
|
+
const DraggableDemo = () => {
|
|
16
|
+
const { getDragProps, isDragging } = useDraggable({
|
|
17
|
+
data: {
|
|
18
|
+
column: 'Status',
|
|
19
|
+
row: 'Research proposal',
|
|
20
|
+
value: 'Approved',
|
|
21
|
+
},
|
|
22
|
+
type: TABLE_CELL_TYPE,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
data-testid='drag-source'
|
|
28
|
+
{...getDragProps()}
|
|
29
|
+
>
|
|
30
|
+
{isDragging ? 'Dragging' : 'Ready'}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const StringDraggableDemo = () => {
|
|
36
|
+
const { getDragProps } = useDraggable({
|
|
37
|
+
data: '123',
|
|
38
|
+
type: TABLE_CELL_TYPE,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
data-testid='string-drag-source'
|
|
44
|
+
{...getDragProps()}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const DisabledDraggableDemo = ({ disabled }: { disabled: boolean }) => {
|
|
50
|
+
const { getDragProps } = useDraggable({
|
|
51
|
+
data: 'Approved',
|
|
52
|
+
disabled,
|
|
53
|
+
type: TABLE_CELL_TYPE,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
data-testid='disabled-drag-source'
|
|
59
|
+
{...getDragProps()}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
describe('useDraggable', () => {
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
clearActiveKeyboardDrag();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('sets draggable data on drag start', () => {
|
|
70
|
+
const dataTransfer = createDataTransfer();
|
|
71
|
+
|
|
72
|
+
render(<DraggableDemo />);
|
|
73
|
+
|
|
74
|
+
fireEvent.dragStart(screen.getByTestId('drag-source'), {
|
|
75
|
+
dataTransfer,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(dataTransfer.effectAllowed).toBe('move');
|
|
79
|
+
expect(dataTransfer.setData).toHaveBeenCalledWith(
|
|
80
|
+
TABLE_CELL_TYPE,
|
|
81
|
+
JSON.stringify({
|
|
82
|
+
column: 'Status',
|
|
83
|
+
row: 'Research proposal',
|
|
84
|
+
value: 'Approved',
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
|
+
expect(dataTransfer.setData).toHaveBeenCalledWith(
|
|
88
|
+
UIKIT_DND_DATA_TYPE,
|
|
89
|
+
JSON.stringify({
|
|
90
|
+
data: JSON.stringify({
|
|
91
|
+
column: 'Status',
|
|
92
|
+
row: 'Research proposal',
|
|
93
|
+
value: 'Approved',
|
|
94
|
+
}),
|
|
95
|
+
type: TABLE_CELL_TYPE,
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
expect(screen.getByText('Dragging')).toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('clears dragging state on drag end', () => {
|
|
102
|
+
render(<DraggableDemo />);
|
|
103
|
+
|
|
104
|
+
fireEvent.dragStart(screen.getByTestId('drag-source'), {
|
|
105
|
+
dataTransfer: createDataTransfer(),
|
|
106
|
+
});
|
|
107
|
+
fireEvent.dragEnd(screen.getByTestId('drag-source'));
|
|
108
|
+
|
|
109
|
+
expect(screen.getByText('Ready')).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('serializes strings as JSON strings', () => {
|
|
113
|
+
const dataTransfer = createDataTransfer();
|
|
114
|
+
|
|
115
|
+
render(<StringDraggableDemo />);
|
|
116
|
+
|
|
117
|
+
fireEvent.dragStart(screen.getByTestId('string-drag-source'), {
|
|
118
|
+
dataTransfer,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(dataTransfer.setData).toHaveBeenCalledWith(
|
|
122
|
+
TABLE_CELL_TYPE,
|
|
123
|
+
JSON.stringify('123')
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('supports keyboard drag start and cancel', () => {
|
|
128
|
+
render(<DraggableDemo />);
|
|
129
|
+
|
|
130
|
+
fireEvent.keyDown(screen.getByTestId('drag-source'), { key: 'Enter' });
|
|
131
|
+
|
|
132
|
+
expect(screen.getByText('Dragging')).toBeInTheDocument();
|
|
133
|
+
|
|
134
|
+
fireEvent.keyDown(screen.getByTestId('drag-source'), { key: 'Escape' });
|
|
135
|
+
|
|
136
|
+
expect(screen.getByText('Ready')).toBeInTheDocument();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('does not replace an active keyboard drag', () => {
|
|
140
|
+
render(
|
|
141
|
+
<>
|
|
142
|
+
<DraggableDemo />
|
|
143
|
+
<StringDraggableDemo />
|
|
144
|
+
</>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
fireEvent.keyDown(screen.getByTestId('drag-source'), { key: 'Enter' });
|
|
148
|
+
fireEvent.keyDown(screen.getByTestId('string-drag-source'), {
|
|
149
|
+
key: 'Enter',
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(getActiveKeyboardDrag()?.data).toEqual({
|
|
153
|
+
column: 'Status',
|
|
154
|
+
row: 'Research proposal',
|
|
155
|
+
value: 'Approved',
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('clears an owned keyboard drag on unmount', () => {
|
|
160
|
+
const { unmount } = render(<DraggableDemo />);
|
|
161
|
+
|
|
162
|
+
fireEvent.keyDown(screen.getByTestId('drag-source'), { key: 'Enter' });
|
|
163
|
+
|
|
164
|
+
expect(getActiveKeyboardDrag()).not.toBeNull();
|
|
165
|
+
|
|
166
|
+
unmount();
|
|
167
|
+
|
|
168
|
+
expect(getActiveKeyboardDrag()).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('clears an owned keyboard drag when disabled', () => {
|
|
172
|
+
const { rerender } = render(<DisabledDraggableDemo disabled={false} />);
|
|
173
|
+
|
|
174
|
+
fireEvent.keyDown(screen.getByTestId('disabled-drag-source'), {
|
|
175
|
+
key: 'Enter',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(getActiveKeyboardDrag()).not.toBeNull();
|
|
179
|
+
|
|
180
|
+
rerender(<DisabledDraggableDemo disabled />);
|
|
181
|
+
|
|
182
|
+
expect(getActiveKeyboardDrag()).toBeNull();
|
|
183
|
+
});
|
|
184
|
+
});
|
package/lib/hooks/index.ts
CHANGED
|
@@ -1,2 +1,21 @@
|
|
|
1
1
|
export { useFocusTrap } from './useFocusTrap';
|
|
2
|
+
export { default as useDraggable } from './useDraggable';
|
|
3
|
+
export type {
|
|
4
|
+
DraggableProps,
|
|
5
|
+
UseDraggableOptions,
|
|
6
|
+
UseDraggableReturn,
|
|
7
|
+
} from './useDraggable';
|
|
8
|
+
export { default as useDropZone, UIKIT_DND_DATA_TYPE } from './useDropZone';
|
|
9
|
+
export type {
|
|
10
|
+
DragData,
|
|
11
|
+
DropZoneAcceptedTypes,
|
|
12
|
+
DropZoneDrop,
|
|
13
|
+
DropZoneEvent,
|
|
14
|
+
DropZoneRejection,
|
|
15
|
+
DropZoneRejectionReason,
|
|
16
|
+
DropZoneRootProps,
|
|
17
|
+
KeyboardDragData,
|
|
18
|
+
UseDropZoneOptions,
|
|
19
|
+
UseDropZoneReturn,
|
|
20
|
+
} from './useDropZone';
|
|
2
21
|
export { default as useMediaQuery } from './useMediaQuery';
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DragEvent,
|
|
3
|
+
HTMLAttributes,
|
|
4
|
+
KeyboardEvent,
|
|
5
|
+
Ref,
|
|
6
|
+
RefCallback,
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useId,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from 'react';
|
|
13
|
+
import {
|
|
14
|
+
clearActiveKeyboardDrag,
|
|
15
|
+
DragData,
|
|
16
|
+
getActiveKeyboardDrag,
|
|
17
|
+
setActiveKeyboardDrag,
|
|
18
|
+
subscribeToKeyboardDrag,
|
|
19
|
+
UIKIT_DND_DATA_TYPE,
|
|
20
|
+
} from './useDropZone';
|
|
21
|
+
|
|
22
|
+
export interface UseDraggableOptions<TData = unknown> {
|
|
23
|
+
data: TData;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
dragImage?: Element;
|
|
26
|
+
dragImageOffset?: {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
};
|
|
30
|
+
effectAllowed?: DataTransfer['effectAllowed'];
|
|
31
|
+
keyboardDrag?: boolean;
|
|
32
|
+
onDragEnd?: (event: DragEvent<HTMLElement>) => void;
|
|
33
|
+
onKeyboardDragCancel?: (dragData: DragData<TData>) => void;
|
|
34
|
+
onDragStart?: (
|
|
35
|
+
dragData: DragData<TData>,
|
|
36
|
+
event: DragEvent<HTMLElement>
|
|
37
|
+
) => void;
|
|
38
|
+
onKeyboardDragStart?: (dragData: DragData<TData>) => void;
|
|
39
|
+
serializeData?: (data: TData) => string;
|
|
40
|
+
type: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DraggableProps<
|
|
44
|
+
TElement extends HTMLElement,
|
|
45
|
+
> extends HTMLAttributes<TElement> {
|
|
46
|
+
draggable: boolean;
|
|
47
|
+
ref: RefCallback<TElement>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface UseDraggableReturn {
|
|
51
|
+
getDragProps: <TElement extends HTMLElement = HTMLElement>(
|
|
52
|
+
props?: HTMLAttributes<TElement> & { ref?: Ref<TElement> }
|
|
53
|
+
) => DraggableProps<TElement>;
|
|
54
|
+
isDragging: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const serializeDefaultData = <TData>(data: TData): string => {
|
|
58
|
+
const serializedData = JSON.stringify(data);
|
|
59
|
+
|
|
60
|
+
if (serializedData === undefined) {
|
|
61
|
+
throw new TypeError('Drag data must be JSON-serializable');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return serializedData;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const setRef = <TValue>(ref: Ref<TValue> | undefined, value: TValue) => {
|
|
68
|
+
if (!ref) return;
|
|
69
|
+
|
|
70
|
+
if (typeof ref === 'function') {
|
|
71
|
+
ref(value);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
ref.current = value;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const useDraggable = <TData = unknown>({
|
|
79
|
+
data,
|
|
80
|
+
disabled = false,
|
|
81
|
+
dragImage,
|
|
82
|
+
dragImageOffset = { x: 0, y: 0 },
|
|
83
|
+
effectAllowed = 'move',
|
|
84
|
+
keyboardDrag = true,
|
|
85
|
+
onDragEnd,
|
|
86
|
+
onKeyboardDragCancel,
|
|
87
|
+
onDragStart,
|
|
88
|
+
onKeyboardDragStart,
|
|
89
|
+
serializeData = serializeDefaultData,
|
|
90
|
+
type,
|
|
91
|
+
}: UseDraggableOptions<TData>): UseDraggableReturn => {
|
|
92
|
+
const dragId = useId();
|
|
93
|
+
const dragRef = useRef<HTMLElement | null>(null);
|
|
94
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
95
|
+
|
|
96
|
+
const getDragData = useCallback((): DragData<TData> => {
|
|
97
|
+
const rawData = serializeData(data);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
data,
|
|
101
|
+
rawData,
|
|
102
|
+
type,
|
|
103
|
+
};
|
|
104
|
+
}, [data, serializeData, type]);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const unsubscribe = subscribeToKeyboardDrag(() => {
|
|
108
|
+
if (getActiveKeyboardDrag()?.id !== dragId) {
|
|
109
|
+
setIsDragging(false);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
unsubscribe();
|
|
115
|
+
if (getActiveKeyboardDrag()?.id === dragId) {
|
|
116
|
+
clearActiveKeyboardDrag();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}, [dragId]);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (disabled && getActiveKeyboardDrag()?.id === dragId) {
|
|
123
|
+
clearActiveKeyboardDrag();
|
|
124
|
+
}
|
|
125
|
+
}, [disabled, dragId]);
|
|
126
|
+
|
|
127
|
+
const getDragProps = useCallback(
|
|
128
|
+
<TElement extends HTMLElement = HTMLElement>({
|
|
129
|
+
onDragEnd: userOnDragEnd,
|
|
130
|
+
onDragStart: userOnDragStart,
|
|
131
|
+
onKeyDown: userOnKeyDown,
|
|
132
|
+
ref: userRef,
|
|
133
|
+
tabIndex,
|
|
134
|
+
...props
|
|
135
|
+
}: HTMLAttributes<TElement> & { ref?: Ref<TElement> } = {}) => {
|
|
136
|
+
const ref: RefCallback<TElement> = (element) => {
|
|
137
|
+
dragRef.current = element;
|
|
138
|
+
setRef(userRef, element);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const onDragStartHandler = (event: DragEvent<TElement>) => {
|
|
142
|
+
userOnDragStart?.(event);
|
|
143
|
+
if (disabled || event.defaultPrevented) return;
|
|
144
|
+
|
|
145
|
+
const dragData = getDragData();
|
|
146
|
+
|
|
147
|
+
event.dataTransfer.effectAllowed = effectAllowed;
|
|
148
|
+
event.dataTransfer.setData(type, dragData.rawData ?? '');
|
|
149
|
+
event.dataTransfer.setData(
|
|
150
|
+
UIKIT_DND_DATA_TYPE,
|
|
151
|
+
JSON.stringify({
|
|
152
|
+
data: dragData.rawData,
|
|
153
|
+
type,
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (dragImage) {
|
|
158
|
+
event.dataTransfer.setDragImage(
|
|
159
|
+
dragImage,
|
|
160
|
+
dragImageOffset.x,
|
|
161
|
+
dragImageOffset.y
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
setIsDragging(true);
|
|
166
|
+
onDragStart?.(dragData, event);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const onDragEndHandler = (event: DragEvent<TElement>) => {
|
|
170
|
+
userOnDragEnd?.(event);
|
|
171
|
+
setIsDragging(false);
|
|
172
|
+
onDragEnd?.(event);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const onKeyDownHandler = (event: KeyboardEvent<TElement>) => {
|
|
176
|
+
userOnKeyDown?.(event);
|
|
177
|
+
if (disabled || !keyboardDrag || event.defaultPrevented) return;
|
|
178
|
+
|
|
179
|
+
if (event.target !== event.currentTarget) return;
|
|
180
|
+
|
|
181
|
+
if (event.key === 'Escape' && isDragging) {
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
const dragData = getDragData();
|
|
184
|
+
clearActiveKeyboardDrag();
|
|
185
|
+
setIsDragging(false);
|
|
186
|
+
onKeyboardDragCancel?.(dragData);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
(event.key !== 'Enter' && event.key !== ' ') ||
|
|
192
|
+
getActiveKeyboardDrag()
|
|
193
|
+
) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
|
|
199
|
+
const dragData = getDragData();
|
|
200
|
+
setActiveKeyboardDrag({
|
|
201
|
+
...dragData,
|
|
202
|
+
id: dragId,
|
|
203
|
+
rawData: dragData.rawData ?? '',
|
|
204
|
+
});
|
|
205
|
+
setIsDragging(true);
|
|
206
|
+
onKeyboardDragStart?.(dragData);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
...props,
|
|
211
|
+
ref,
|
|
212
|
+
draggable: !disabled,
|
|
213
|
+
tabIndex: disabled ? -1 : keyboardDrag ? (tabIndex ?? 0) : tabIndex,
|
|
214
|
+
onDragEnd: onDragEndHandler,
|
|
215
|
+
onDragStart: onDragStartHandler,
|
|
216
|
+
onKeyDown: onKeyDownHandler,
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
[
|
|
220
|
+
disabled,
|
|
221
|
+
dragImage,
|
|
222
|
+
dragImageOffset.x,
|
|
223
|
+
dragImageOffset.y,
|
|
224
|
+
effectAllowed,
|
|
225
|
+
getDragData,
|
|
226
|
+
isDragging,
|
|
227
|
+
keyboardDrag,
|
|
228
|
+
onKeyboardDragCancel,
|
|
229
|
+
onDragEnd,
|
|
230
|
+
onKeyboardDragStart,
|
|
231
|
+
onDragStart,
|
|
232
|
+
type,
|
|
233
|
+
dragId,
|
|
234
|
+
]
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
getDragProps,
|
|
239
|
+
isDragging,
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export default useDraggable;
|