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.
Files changed (39) hide show
  1. package/dist/components/DropZone/DropZone.d.ts +21 -0
  2. package/dist/components/DropZone/DropZone.stories.d.ts +52 -0
  3. package/dist/components/DropZone/__tests__/DropZone.test.d.ts +1 -0
  4. package/dist/components/DropZone/index.d.ts +2 -0
  5. package/dist/components/Skeleton/Skeleton.d.ts +35 -0
  6. package/dist/components/Skeleton/Skeleton.stories.d.ts +27 -0
  7. package/dist/components/Skeleton/SkeletonAvatar.d.ts +7 -0
  8. package/dist/components/Skeleton/SkeletonInput.d.ts +4 -0
  9. package/dist/components/Skeleton/SkeletonTableRow.d.ts +9 -0
  10. package/dist/components/Skeleton/SkeletonText.d.ts +4 -0
  11. package/dist/components/Skeleton/__tests__/Skeleton.test.d.ts +1 -0
  12. package/dist/components/Skeleton/index.d.ts +6 -0
  13. package/dist/components/index.d.ts +4 -0
  14. package/dist/hooks/__tests__/useDraggable.test.d.ts +1 -0
  15. package/dist/hooks/index.d.ts +4 -0
  16. package/dist/hooks/useDraggable.d.ts +31 -0
  17. package/dist/hooks/useDropZone.d.ts +57 -0
  18. package/dist/index.js +7229 -6665
  19. package/lib/components/DropZone/DropZone.mdx +72 -0
  20. package/lib/components/DropZone/DropZone.stories.tsx +139 -0
  21. package/lib/components/DropZone/DropZone.tsx +213 -0
  22. package/lib/components/DropZone/__tests__/DropZone.test.tsx +378 -0
  23. package/lib/components/DropZone/__tests__/__snapshots__/DropZone.test.tsx.snap +46 -0
  24. package/lib/components/DropZone/index.ts +2 -0
  25. package/lib/components/Skeleton/Skeleton.mdx +34 -0
  26. package/lib/components/Skeleton/Skeleton.stories.tsx +77 -0
  27. package/lib/components/Skeleton/Skeleton.tsx +158 -0
  28. package/lib/components/Skeleton/SkeletonAvatar.tsx +20 -0
  29. package/lib/components/Skeleton/SkeletonInput.tsx +13 -0
  30. package/lib/components/Skeleton/SkeletonTableRow.tsx +44 -0
  31. package/lib/components/Skeleton/SkeletonText.tsx +13 -0
  32. package/lib/components/Skeleton/__tests__/Skeleton.test.tsx +86 -0
  33. package/lib/components/Skeleton/index.ts +6 -0
  34. package/lib/components/index.ts +13 -0
  35. package/lib/hooks/__tests__/useDraggable.test.tsx +184 -0
  36. package/lib/hooks/index.ts +19 -0
  37. package/lib/hooks/useDraggable.ts +243 -0
  38. package/lib/hooks/useDropZone.ts +418 -0
  39. package/package.json +1 -1
@@ -0,0 +1,72 @@
1
+ import * as DropZoneStories from './DropZone.stories';
2
+ import {
3
+ Meta,
4
+ Title,
5
+ Subtitle,
6
+ Description,
7
+ Canvas,
8
+ Controls,
9
+ ArgTypes,
10
+ } from '@storybook/addon-docs/blocks';
11
+
12
+ export const usage = {
13
+ default: `<DropZone
14
+ acceptedTypes='application/x-ucl-uikit-table-cell'
15
+ onDrop={(drop) => console.log(drop.data)}
16
+ />`,
17
+ sourceAndTarget: `const TABLE_CELL_TYPE = 'application/x-ucl-uikit-table-cell';
18
+
19
+ const { getDragProps } = useDraggable({
20
+ type: TABLE_CELL_TYPE,
21
+ data: { row: 'Research proposal', column: 'Status', value: 'Approved' },
22
+ });
23
+
24
+ <Table.Cell {...getDragProps()}>Approved</Table.Cell>
25
+
26
+ <DropZone
27
+ acceptedTypes={TABLE_CELL_TYPE}
28
+ onDrop={(drop) => setDroppedCell(drop.data)}
29
+ />`,
30
+ };
31
+
32
+ <Meta of={DropZoneStories} />
33
+ <Title />
34
+ <Subtitle>A themed drop target built on the public useDropZone hook</Subtitle>
35
+
36
+ <Description>
37
+ `DropZone` accepts generic drag data. Use it with `useDraggable` for
38
+ UIKit-managed drag data, or pass native drag handlers when integrating with
39
+ another drag source. UIKit-managed draggables can be picked up with Space or
40
+ Enter, cancelled with Escape, and dropped on a focused drop target with Space
41
+ or Enter.
42
+ </Description>
43
+
44
+ <Canvas
45
+ of={DropZoneStories.Default}
46
+ source={{ code: usage.default }}
47
+ />
48
+
49
+ ## Source And Target
50
+
51
+ Use `useDraggable` on a source element and `DropZone` on the target.
52
+
53
+ For workflows such as list reordering, provide an equivalent button or menu
54
+ action when the drag operation is not the only reasonable way to complete the
55
+ task.
56
+
57
+ <Canvas
58
+ of={DropZoneStories.WithTableCell}
59
+ source={{ code: usage.sourceAndTarget }}
60
+ />
61
+
62
+ ## States
63
+
64
+ <Canvas of={DropZoneStories.Disabled} />
65
+
66
+ ## Props
67
+
68
+ <ArgTypes of={DropZoneStories} />
69
+
70
+ ## Interactive Controls
71
+
72
+ <Controls of={DropZoneStories.Default} />
@@ -0,0 +1,139 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { useState } from 'react';
3
+ import { css } from '@emotion/css';
4
+ import DropZone from './DropZone';
5
+ import Text from '../Text';
6
+ import { useDraggable } from '../../hooks';
7
+
8
+ const TABLE_CELL_TYPE = 'application/x-ucl-uikit-table-cell';
9
+
10
+ interface TableCellDragData {
11
+ column: string;
12
+ row: string;
13
+ value: string;
14
+ }
15
+
16
+ const meta = {
17
+ title: 'Components/DropZone',
18
+ component: DropZone,
19
+ parameters: {
20
+ layout: 'centered',
21
+ },
22
+ argTypes: {
23
+ acceptedTypes: {
24
+ control: { type: 'object' },
25
+ },
26
+ disabled: {
27
+ control: { type: 'boolean' },
28
+ },
29
+ dropEffect: {
30
+ options: ['copy', 'move', 'link'],
31
+ control: { type: 'select' },
32
+ },
33
+ testId: {
34
+ control: { type: 'text' },
35
+ },
36
+ className: {
37
+ control: false,
38
+ },
39
+ children: {
40
+ control: false,
41
+ },
42
+ onDrop: {
43
+ action: 'drop',
44
+ },
45
+ onReject: {
46
+ action: 'reject',
47
+ },
48
+ },
49
+ args: {
50
+ acceptedTypes: TABLE_CELL_TYPE,
51
+ },
52
+ tags: ['autodocs'],
53
+ } satisfies Meta<typeof DropZone>;
54
+
55
+ export default meta;
56
+ type Story = StoryObj<typeof meta>;
57
+
58
+ const storyContainerStyle = css`
59
+ display: grid;
60
+ grid-template-columns: minmax(180px, 1fr) minmax(260px, 1.5fr);
61
+ gap: 24px;
62
+ width: min(720px, 90vw);
63
+ `;
64
+
65
+ const draggableCellStyle = css`
66
+ display: inline-flex;
67
+ align-items: center;
68
+ min-height: 40px;
69
+ padding: 0 12px;
70
+ border: 1px solid #d8dde8;
71
+ border-radius: 4px;
72
+ cursor: grab;
73
+ user-select: none;
74
+ `;
75
+
76
+ const DraggableCell = ({ data }: { data: TableCellDragData }) => {
77
+ const { getDragProps, isDragging } = useDraggable({
78
+ data,
79
+ type: TABLE_CELL_TYPE,
80
+ });
81
+
82
+ return (
83
+ <span
84
+ {...getDragProps({
85
+ className: draggableCellStyle,
86
+ })}
87
+ aria-label={`Drag ${data.value}`}
88
+ >
89
+ {isDragging ? 'Dragging...' : data.value}
90
+ </span>
91
+ );
92
+ };
93
+
94
+ const TableCellDropExample = () => {
95
+ const [droppedCell, setDroppedCell] = useState<TableCellDragData | null>(
96
+ null
97
+ );
98
+
99
+ return (
100
+ <div className={storyContainerStyle}>
101
+ <DraggableCell
102
+ data={{
103
+ column: 'Status',
104
+ row: 'Research proposal',
105
+ value: 'Approved',
106
+ }}
107
+ />
108
+ <DropZone<TableCellDragData>
109
+ acceptedTypes={TABLE_CELL_TYPE}
110
+ onDrop={(drop) => setDroppedCell(drop.data)}
111
+ >
112
+ {({ isDragActive }) => (
113
+ <Text level='md-semibold'>
114
+ {droppedCell
115
+ ? `${droppedCell.row}: ${droppedCell.value}`
116
+ : isDragActive
117
+ ? 'Release to drop the table cell'
118
+ : 'Drop a table cell here'}
119
+ </Text>
120
+ )}
121
+ </DropZone>
122
+ </div>
123
+ );
124
+ };
125
+
126
+ export const Default: Story = {
127
+ render: (args) => <DropZone {...args} />,
128
+ };
129
+
130
+ export const WithTableCell: Story = {
131
+ render: () => <TableCellDropExample />,
132
+ };
133
+
134
+ export const Disabled: Story = {
135
+ args: {
136
+ disabled: true,
137
+ },
138
+ render: (args) => <DropZone {...args} />,
139
+ };
@@ -0,0 +1,213 @@
1
+ import { HTMLAttributes, ReactNode, Ref, memo } from 'react';
2
+ import { css, cx } from '@emotion/css';
3
+ import useTheme from '../../theme/useTheme';
4
+ import useDropZone, {
5
+ DropZoneAcceptedTypes,
6
+ DropZoneDrop,
7
+ DropZoneRejection,
8
+ UseDropZoneOptions,
9
+ } from '../../hooks/useDropZone';
10
+ import marginsStyle, { MarginProps } from '../common/marginsStyle';
11
+ import Icon from '../Icon';
12
+ import Paragraph from '../Paragraph';
13
+ import Text from '../Text';
14
+
15
+ export const NAME = 'ucl-uikit-drop-zone';
16
+
17
+ export interface DropZoneRenderProps<TData = unknown> {
18
+ activeTypes: string[];
19
+ isDragAccept: boolean;
20
+ isDragActive: boolean;
21
+ isDragReject: boolean;
22
+ lastDrop: DropZoneDrop<TData> | null;
23
+ lastRejection: DropZoneRejection<TData> | null;
24
+ }
25
+
26
+ export interface DropZoneBaseProps<TData = unknown>
27
+ extends
28
+ Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onDrop'>,
29
+ UseDropZoneOptions<TData> {
30
+ children?: ReactNode | ((props: DropZoneRenderProps<TData>) => ReactNode);
31
+ ref?: Ref<HTMLDivElement>;
32
+ testId?: string;
33
+ }
34
+
35
+ export type DropZoneProps<TData = unknown> = DropZoneBaseProps<TData> &
36
+ MarginProps;
37
+
38
+ const getDefaultDescription = (acceptedTypes?: DropZoneAcceptedTypes) => {
39
+ if (!acceptedTypes) return 'Drop an item here';
40
+
41
+ const values =
42
+ typeof acceptedTypes === 'string'
43
+ ? acceptedTypes.split(',')
44
+ : Array.from(acceptedTypes);
45
+ const normalizedValues = values.map((value) => value.trim()).filter(Boolean);
46
+
47
+ return normalizedValues.length > 0
48
+ ? `Drop ${normalizedValues.join(', ')} here`
49
+ : 'Drop an item here';
50
+ };
51
+
52
+ const DropZone = <TData = unknown,>({
53
+ 'aria-label': ariaLabel,
54
+ 'aria-labelledby': ariaLabelledBy,
55
+ acceptedTypes,
56
+ canDrop,
57
+ children,
58
+ className,
59
+ disabled = false,
60
+ dropEffect = 'move',
61
+ keyboardDrop,
62
+ onDrop,
63
+ onReject,
64
+ parseData,
65
+ ref,
66
+ role,
67
+ testId = NAME,
68
+ ...props
69
+ }: DropZoneProps<TData>) => {
70
+ const [theme] = useTheme();
71
+ const {
72
+ activeTypes,
73
+ getRootProps,
74
+ isDragAccept,
75
+ isDragActive,
76
+ isDragReject,
77
+ lastDrop,
78
+ lastRejection,
79
+ } = useDropZone<TData>({
80
+ acceptedTypes,
81
+ canDrop,
82
+ disabled,
83
+ dropEffect,
84
+ keyboardDrop,
85
+ onDrop,
86
+ onReject,
87
+ parseData,
88
+ });
89
+
90
+ const baseStyle = css`
91
+ box-sizing: border-box;
92
+ display: flex;
93
+ flex-direction: column;
94
+ align-items: center;
95
+ justify-content: center;
96
+ gap: ${theme.padding.p8};
97
+ width: 100%;
98
+ min-height: 160px;
99
+ padding: ${theme.padding.p24};
100
+ color: ${theme.colour.text.default};
101
+ background-color: ${theme.colour.surface.default};
102
+ border: ${theme.border.b2} dashed ${theme.colour.border.default};
103
+ border-radius: ${theme.radius.r4};
104
+ outline: none;
105
+ text-align: center;
106
+
107
+ &:focus-visible {
108
+ box-shadow: ${theme.boxShadow.focus};
109
+ }
110
+ `;
111
+
112
+ const activeStyle = css`
113
+ background-color: ${theme.colour.surface.brand};
114
+ border-color: ${theme.colour.border.brand};
115
+ `;
116
+
117
+ const acceptStyle = css`
118
+ border-color: ${theme.colour.border.success};
119
+ `;
120
+
121
+ const rejectStyle = css`
122
+ background-color: ${theme.colour.surface.critical};
123
+ border-color: ${theme.colour.border.critical};
124
+ `;
125
+
126
+ const disabledStyle = css`
127
+ color: ${theme.colour.text.disabled};
128
+ background-color: ${theme.colour.fill.disabled};
129
+ border-color: ${theme.colour.border.disabled};
130
+ cursor: not-allowed;
131
+ `;
132
+
133
+ const iconStyle = css`
134
+ color: ${
135
+ disabled
136
+ ? theme.colour.icon.disabled
137
+ : isDragReject
138
+ ? theme.colour.icon.critical
139
+ : isDragAccept
140
+ ? theme.colour.icon.success
141
+ : theme.colour.icon.brand
142
+ };
143
+ `;
144
+
145
+ const style = cx(
146
+ NAME,
147
+ baseStyle,
148
+ marginsStyle(props, theme),
149
+ isDragActive && activeStyle,
150
+ isDragAccept && acceptStyle,
151
+ isDragReject && rejectStyle,
152
+ disabled && disabledStyle,
153
+ className
154
+ );
155
+
156
+ const renderProps: DropZoneRenderProps<TData> = {
157
+ activeTypes,
158
+ isDragAccept,
159
+ isDragActive,
160
+ isDragReject,
161
+ lastDrop,
162
+ lastRejection,
163
+ };
164
+
165
+ return (
166
+ <div
167
+ data-testid={testId}
168
+ {...getRootProps({
169
+ ...props,
170
+ 'aria-label': ariaLabel ?? (ariaLabelledBy ? undefined : 'Drop zone'),
171
+ 'aria-labelledby': ariaLabelledBy,
172
+ ref,
173
+ role: role ?? 'group',
174
+ className: style,
175
+ })}
176
+ >
177
+ {typeof children === 'function' ? (
178
+ children(renderProps)
179
+ ) : (
180
+ <>
181
+ <Icon.Move
182
+ aria-hidden='true'
183
+ className={iconStyle}
184
+ />
185
+ {children ?? (
186
+ <>
187
+ <Paragraph
188
+ as='div'
189
+ level='md-semibold'
190
+ noMargins
191
+ >
192
+ {isDragReject
193
+ ? 'This item cannot be dropped here'
194
+ : isDragActive
195
+ ? 'Release to drop'
196
+ : getDefaultDescription(acceptedTypes)}
197
+ </Paragraph>
198
+ <Text level='sm'>
199
+ {isDragReject
200
+ ? 'This drop target does not accept that item'
201
+ : 'Drag a compatible item into this area'}
202
+ </Text>
203
+ </>
204
+ )}
205
+ </>
206
+ )}
207
+ </div>
208
+ );
209
+ };
210
+
211
+ const MemoizedDropZone = memo(DropZone) as typeof DropZone;
212
+
213
+ export default MemoizedDropZone;