@toolpath/ui 0.1.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/LICENSE +21 -0
- package/README.md +44 -0
- package/dist/index.d.ts +1174 -0
- package/dist/index.js +5934 -0
- package/package.json +68 -0
- package/src/attribution/action.tsx +13 -0
- package/src/attribution/at.tsx +28 -0
- package/src/attribution/attribution.tsx +16 -0
- package/src/attribution/by.tsx +21 -0
- package/src/attribution/format-time.ts +48 -0
- package/src/attribution/index.ts +20 -0
- package/src/badge/badge.tsx +38 -0
- package/src/badge/index.ts +1 -0
- package/src/breadcrumbs/breadcrumbs-context.tsx +26 -0
- package/src/breadcrumbs/breadcrumbs.tsx +24 -0
- package/src/breadcrumbs/index.ts +12 -0
- package/src/breadcrumbs/item.tsx +85 -0
- package/src/button/animated-ellipsis.tsx +44 -0
- package/src/button/button.tsx +168 -0
- package/src/button/index.ts +1 -0
- package/src/callout/callout.tsx +202 -0
- package/src/callout/index.ts +2 -0
- package/src/callout/use-dismissed-callouts.ts +78 -0
- package/src/card/card.tsx +19 -0
- package/src/card/index.ts +2 -0
- package/src/checkbox/checkbox.tsx +76 -0
- package/src/checkbox/index.ts +1 -0
- package/src/combobox/button.tsx +99 -0
- package/src/combobox/combobox-context.tsx +42 -0
- package/src/combobox/combobox.tsx +60 -0
- package/src/combobox/empty.tsx +21 -0
- package/src/combobox/group-label.tsx +20 -0
- package/src/combobox/group.tsx +11 -0
- package/src/combobox/icon.tsx +11 -0
- package/src/combobox/index.tsx +64 -0
- package/src/combobox/input.tsx +77 -0
- package/src/combobox/item-indicator.tsx +16 -0
- package/src/combobox/item.tsx +58 -0
- package/src/combobox/list.tsx +19 -0
- package/src/combobox/popover.tsx +62 -0
- package/src/combobox/trigger.tsx +33 -0
- package/src/combobox/value.tsx +12 -0
- package/src/common/index.ts +1 -0
- package/src/common/svg.tsx +17 -0
- package/src/dialog/actions.tsx +17 -0
- package/src/dialog/confirm-button.tsx +25 -0
- package/src/dialog/dialog-context.tsx +21 -0
- package/src/dialog/dialog.tsx +37 -0
- package/src/dialog/dismiss-button.tsx +29 -0
- package/src/dialog/index.ts +37 -0
- package/src/dialog/message.tsx +20 -0
- package/src/dialog/provider.tsx +102 -0
- package/src/dialog/title.tsx +20 -0
- package/src/editable-cell/editable-cell.tsx +307 -0
- package/src/editable-cell/index.ts +7 -0
- package/src/editable-cell/use-editable-grid.tsx +296 -0
- package/src/field/field.tsx +22 -0
- package/src/field/index.ts +2 -0
- package/src/helpers/cn.ts +6 -0
- package/src/helpers/index.ts +1 -0
- package/src/icon-button/icon-button.tsx +126 -0
- package/src/icon-button/index.ts +2 -0
- package/src/index.ts +32 -0
- package/src/input/index.ts +1 -0
- package/src/input/input.tsx +276 -0
- package/src/link/index.ts +4 -0
- package/src/link/link-context.tsx +33 -0
- package/src/link/link.tsx +24 -0
- package/src/menu/divider.tsx +17 -0
- package/src/menu/index.ts +40 -0
- package/src/menu/item.tsx +149 -0
- package/src/menu/menu-context.tsx +24 -0
- package/src/menu/menu.tsx +14 -0
- package/src/menu/popover.tsx +69 -0
- package/src/menu/submenu-trigger.tsx +48 -0
- package/src/menu/submenu.tsx +20 -0
- package/src/menu/trigger.tsx +43 -0
- package/src/notification/index.ts +16 -0
- package/src/notification/list.tsx +123 -0
- package/src/notification/provider.tsx +75 -0
- package/src/page-header/index.ts +2 -0
- package/src/page-header/page-header.tsx +14 -0
- package/src/pagination/index.ts +1 -0
- package/src/pagination/pagination.tsx +136 -0
- package/src/panels/collapsed.tsx +32 -0
- package/src/panels/group.tsx +27 -0
- package/src/panels/index.ts +57 -0
- package/src/panels/layout-context.ts +5 -0
- package/src/panels/orientation-context.tsx +5 -0
- package/src/panels/panel.tsx +95 -0
- package/src/panels/separator.tsx +44 -0
- package/src/pin/index.ts +1 -0
- package/src/pin/pin.tsx +99 -0
- package/src/progress-bar/index.ts +1 -0
- package/src/progress-bar/progress-bar.tsx +48 -0
- package/src/quantity-input/index.ts +1 -0
- package/src/quantity-input/quantity-input.tsx +158 -0
- package/src/scroll-area/index.ts +1 -0
- package/src/scroll-area/scroll-area.tsx +82 -0
- package/src/slider/index.ts +1 -0
- package/src/slider/slider.tsx +75 -0
- package/src/star/index.ts +1 -0
- package/src/star/star.tsx +99 -0
- package/src/switch/index.ts +1 -0
- package/src/switch/switch.tsx +33 -0
- package/src/table/body.tsx +15 -0
- package/src/table/cell-context.tsx +48 -0
- package/src/table/cell.tsx +83 -0
- package/src/table/group-empty-state.tsx +28 -0
- package/src/table/group-header.tsx +62 -0
- package/src/table/header-cell.tsx +158 -0
- package/src/table/header-row.tsx +28 -0
- package/src/table/index.ts +56 -0
- package/src/table/is-group-empty-state.ts +17 -0
- package/src/table/is-group-header.ts +5 -0
- package/src/table/row-context.tsx +61 -0
- package/src/table/row-disabled-context.tsx +7 -0
- package/src/table/row.tsx +98 -0
- package/src/table/table-context.tsx +127 -0
- package/src/table/table-empty.tsx +57 -0
- package/src/table/table.tsx +547 -0
- package/src/table/tree-icon.tsx +147 -0
- package/src/table/use-checkbox.ts +200 -0
- package/src/table/use-column-layout.ts +117 -0
- package/src/table/use-groups.ts +134 -0
- package/src/table/use-keyboard-navigation.ts +274 -0
- package/src/table/use-sort-functions.ts +90 -0
- package/src/tabs/index.ts +20 -0
- package/src/tabs/list.tsx +29 -0
- package/src/tabs/tab.tsx +141 -0
- package/src/tabs/tabs-context.tsx +38 -0
- package/src/tabs/tabs.tsx +27 -0
- package/src/textarea/index.ts +1 -0
- package/src/textarea/textarea.tsx +88 -0
- package/src/toast/index.ts +19 -0
- package/src/toast/list.tsx +133 -0
- package/src/toast/provider.tsx +36 -0
- package/src/toggle/index.ts +17 -0
- package/src/toggle/indicator.tsx +27 -0
- package/src/toggle/item.tsx +54 -0
- package/src/toggle/toggle-context.tsx +68 -0
- package/src/toggle/toggle.tsx +124 -0
- package/src/toggle/use-indicator.tsx +69 -0
- package/src/tooltip/index.ts +1 -0
- package/src/tooltip/tooltip.tsx +122 -0
- package/theme.css +70 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Identifier, TableNode } from '@table-library/react-table-library/types/table'
|
|
3
|
+
import { CheckboxChangeEvent } from '../checkbox'
|
|
4
|
+
|
|
5
|
+
export type UseCheckboxReturn = {
|
|
6
|
+
isChecked: (item: TableNode) => boolean
|
|
7
|
+
isIndeterminate: (item: TableNode) => boolean
|
|
8
|
+
handleCheck: (item: TableNode, checked: boolean, event?: CheckboxChangeEvent) => void
|
|
9
|
+
checkedItems: Array<Identifier>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const useCheckbox = (
|
|
13
|
+
items: Array<TableNode>,
|
|
14
|
+
initialState?: Array<Identifier>,
|
|
15
|
+
): UseCheckboxReturn => {
|
|
16
|
+
const [checkedRows, setCheckedRows] = useState<Array<Identifier>>(initialState || [])
|
|
17
|
+
const [lastCheckedId, setLastCheckedId] = useState<Identifier | null>(null)
|
|
18
|
+
|
|
19
|
+
// Flatten the tree into a list in visual order
|
|
20
|
+
const flattenItems = (nodes: Array<TableNode>): Array<TableNode> => {
|
|
21
|
+
return nodes.flatMap((node) => [node, ...flattenItems(node.nodes ?? [])])
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Find an item by ID in the tree
|
|
25
|
+
const findItemById = (items: Array<TableNode>, id: Identifier): TableNode | undefined => {
|
|
26
|
+
return items.reduce<TableNode | undefined>((found, item) => {
|
|
27
|
+
if (found) {
|
|
28
|
+
return found
|
|
29
|
+
}
|
|
30
|
+
if (item.id === id) {
|
|
31
|
+
return item
|
|
32
|
+
}
|
|
33
|
+
if (item.nodes?.length) {
|
|
34
|
+
return findItemById(item.nodes, id)
|
|
35
|
+
}
|
|
36
|
+
return undefined
|
|
37
|
+
}, undefined)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Recursively get all descendant IDs from a TreeItem
|
|
41
|
+
const getAllDescendantIds = (item: TableNode): Array<Identifier> => {
|
|
42
|
+
if (!item.nodes?.length) {
|
|
43
|
+
return []
|
|
44
|
+
}
|
|
45
|
+
return item.nodes
|
|
46
|
+
.filter((child) => child && child.id != null)
|
|
47
|
+
.flatMap((child) => [child.id, ...getAllDescendantIds(child)])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Get all ancestor IDs for an item
|
|
51
|
+
const getAllAncestorIds = (
|
|
52
|
+
items: Array<TableNode>,
|
|
53
|
+
targetId: Identifier,
|
|
54
|
+
path: Array<Identifier> = [],
|
|
55
|
+
): Array<Identifier> => {
|
|
56
|
+
return items.reduce<Array<Identifier>>((result, item) => {
|
|
57
|
+
if (result.length > 0) {
|
|
58
|
+
return result
|
|
59
|
+
}
|
|
60
|
+
if (item.id === targetId) {
|
|
61
|
+
return path
|
|
62
|
+
}
|
|
63
|
+
if (item.nodes?.length) {
|
|
64
|
+
const found = getAllAncestorIds(item.nodes, targetId, [...path, item.id])
|
|
65
|
+
if (found.length > 0 || item.nodes.some((n) => n.id === targetId)) {
|
|
66
|
+
return found.length > 0 ? found : [...path, item.id]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result
|
|
70
|
+
}, [])
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Get ancestors that should be auto-checked (all their descendants are checked)
|
|
74
|
+
const getAncestorsToAutoCheck = (
|
|
75
|
+
items: Array<TableNode>,
|
|
76
|
+
targetId: Identifier,
|
|
77
|
+
newCheckedIds: Array<Identifier>,
|
|
78
|
+
): Array<Identifier> => {
|
|
79
|
+
const ancestorIds = getAllAncestorIds(items, targetId)
|
|
80
|
+
|
|
81
|
+
// Walk from innermost ancestor to outermost, stop when one fails
|
|
82
|
+
const result = ancestorIds
|
|
83
|
+
.slice()
|
|
84
|
+
.reverse()
|
|
85
|
+
.reduce<{ stopped: boolean; ids: Array<Identifier> }>(
|
|
86
|
+
(acc, ancestorId) => {
|
|
87
|
+
if (acc.stopped) {
|
|
88
|
+
return acc
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ancestor = findItemById(items, ancestorId)
|
|
92
|
+
if (!ancestor) {
|
|
93
|
+
return acc
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const descendantIds = getAllDescendantIds(ancestor)
|
|
97
|
+
const allDescendantsChecked = descendantIds.every(
|
|
98
|
+
(id) => newCheckedIds.includes(id) || acc.ids.includes(id),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if (allDescendantsChecked) {
|
|
102
|
+
return { stopped: false, ids: [...acc.ids, ancestorId] }
|
|
103
|
+
}
|
|
104
|
+
// If this ancestor isn't fully checked, outer ancestors won't be either
|
|
105
|
+
return { stopped: true, ids: acc.ids }
|
|
106
|
+
},
|
|
107
|
+
{ stopped: false, ids: [] },
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return result.ids
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const getDescendantCheckState = (item: TableNode) => {
|
|
114
|
+
const originalItem = findItemById(items, item.id)
|
|
115
|
+
const descendantIds = originalItem ? getAllDescendantIds(originalItem) : []
|
|
116
|
+
const checkedCount = descendantIds.filter((id) => checkedRows.includes(id)).length
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
hasChildren: descendantIds.length > 0,
|
|
120
|
+
allChecked: descendantIds.length > 0 && checkedCount === descendantIds.length,
|
|
121
|
+
someChecked: checkedCount > 0 && checkedCount < descendantIds.length,
|
|
122
|
+
descendantIds,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const isChecked = (item: TableNode): boolean => {
|
|
127
|
+
const { allChecked } = getDescendantCheckState(item)
|
|
128
|
+
// If all descendants are checked, treat parent as checked too
|
|
129
|
+
return checkedRows.includes(item.id) || allChecked
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const isIndeterminate = (item: TableNode): boolean => {
|
|
133
|
+
const { hasChildren, someChecked } = getDescendantCheckState(item)
|
|
134
|
+
// Indeterminate only when some (but not all/none) descendants are checked
|
|
135
|
+
return hasChildren && !isChecked(item) && someChecked
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const handleCheck = (item: TableNode, checked: boolean, event?: CheckboxChangeEvent) => {
|
|
139
|
+
const shiftPressed = event?.shiftKey
|
|
140
|
+
|
|
141
|
+
// Handle shift+click range selection
|
|
142
|
+
if (shiftPressed && lastCheckedId !== null && checked) {
|
|
143
|
+
const flatList = flattenItems(items)
|
|
144
|
+
const lastIndex = flatList.findIndex((n) => n.id === lastCheckedId)
|
|
145
|
+
const currentIndex = flatList.findIndex((n) => n.id === item.id)
|
|
146
|
+
|
|
147
|
+
if (lastIndex !== -1 && currentIndex !== -1) {
|
|
148
|
+
const start = Math.min(lastIndex, currentIndex)
|
|
149
|
+
const end = Math.max(lastIndex, currentIndex)
|
|
150
|
+
const itemsInRange = flatList.slice(start, end + 1)
|
|
151
|
+
|
|
152
|
+
setCheckedRows((current) => {
|
|
153
|
+
return itemsInRange.reduce(
|
|
154
|
+
(newCheckedIds, rangeItem) => {
|
|
155
|
+
const { descendantIds } = getDescendantCheckState(rangeItem)
|
|
156
|
+
const allIds = [rangeItem.id, ...descendantIds]
|
|
157
|
+
const idsToAdd = allIds.filter((id) => !newCheckedIds.includes(id))
|
|
158
|
+
const withDescendants = [...newCheckedIds, ...idsToAdd]
|
|
159
|
+
|
|
160
|
+
// Check if any ancestors should also be auto-checked
|
|
161
|
+
const ancestorsToCheck = getAncestorsToAutoCheck(
|
|
162
|
+
items,
|
|
163
|
+
rangeItem.id,
|
|
164
|
+
withDescendants,
|
|
165
|
+
).filter((id) => !withDescendants.includes(id))
|
|
166
|
+
return [...withDescendants, ...ancestorsToCheck]
|
|
167
|
+
},
|
|
168
|
+
[...current],
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
setLastCheckedId(item.id)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const { descendantIds } = getDescendantCheckState(item)
|
|
178
|
+
const allIds = [item.id, ...descendantIds]
|
|
179
|
+
const ancestorIds = getAllAncestorIds(items, item.id)
|
|
180
|
+
setCheckedRows((current) => {
|
|
181
|
+
if (checked) {
|
|
182
|
+
// Add this item and descendants (filter duplicates)
|
|
183
|
+
const idsToAdd = allIds.filter((id) => !current.includes(id))
|
|
184
|
+
const newCheckedIds = [...current, ...idsToAdd]
|
|
185
|
+
// Check if any ancestors should also be auto-checked
|
|
186
|
+
const ancestorsToCheck = getAncestorsToAutoCheck(items, item.id, newCheckedIds).filter(
|
|
187
|
+
(id) => !newCheckedIds.includes(id),
|
|
188
|
+
)
|
|
189
|
+
return [...newCheckedIds, ...ancestorsToCheck]
|
|
190
|
+
}
|
|
191
|
+
// When unchecking, also uncheck all ancestors
|
|
192
|
+
const idsToRemove = [...allIds, ...ancestorIds]
|
|
193
|
+
return current.filter((id) => !idsToRemove.includes(id))
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
setLastCheckedId(item.id)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { isChecked, isIndeterminate, handleCheck, checkedItems: checkedRows }
|
|
200
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Children, ReactElement, ReactNode, isValidElement, useEffect, useState } from 'react'
|
|
2
|
+
import { useTable } from './table-context'
|
|
3
|
+
|
|
4
|
+
interface ColumnProps {
|
|
5
|
+
width?: string | number
|
|
6
|
+
resize?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface HeaderProps {
|
|
10
|
+
children?: ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const useColumnLayout = (header: ReactElement<HeaderProps>, id?: string) => {
|
|
14
|
+
const { select, multiselect } = useTable()
|
|
15
|
+
const hasSelectionColumn = select || multiselect
|
|
16
|
+
const columns = Children.toArray(header.props.children).filter(
|
|
17
|
+
(child): child is ReactElement<ColumnProps> => isValidElement(child),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const formatWidth = (width: string | number | undefined): string => {
|
|
21
|
+
if (width === undefined) {
|
|
22
|
+
return 'minmax(0px, 1fr)'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (typeof width === 'number') {
|
|
26
|
+
return `${width}px`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return width
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const initialWidths: string = `${hasSelectionColumn ? '8px' : ''} ${columns
|
|
33
|
+
.map((child) => {
|
|
34
|
+
const width = child.props.width
|
|
35
|
+
return formatWidth(width)
|
|
36
|
+
})
|
|
37
|
+
.join(' ')}`
|
|
38
|
+
|
|
39
|
+
const [columnLayout, setColumnLayout] = useState<string>(initialWidths)
|
|
40
|
+
|
|
41
|
+
// Restore layout from localStorage when columns are available
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
// Don't try to restore layout if columns haven't loaded yet
|
|
44
|
+
// (columns are often loaded asynchronously)
|
|
45
|
+
if (columns.length === 0) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If no id, just use the default widths from props
|
|
50
|
+
if (!id) {
|
|
51
|
+
setColumnLayout(initialWidths)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const storageKey = `table-${id}`
|
|
56
|
+
const storedLayout = localStorage.getItem(storageKey)
|
|
57
|
+
|
|
58
|
+
// No stored layout - use the default widths from props
|
|
59
|
+
if (!storedLayout) {
|
|
60
|
+
setColumnLayout(initialWidths)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const storedColumns = storedLayout.match(/(?:minmax\([^()]*\)|[^\s]+)/g)
|
|
65
|
+
|
|
66
|
+
// Column count mismatch - clear storage and use default widths
|
|
67
|
+
if (!storedColumns || storedColumns.length !== columns.length + (hasSelectionColumn ? 1 : 0)) {
|
|
68
|
+
localStorage.removeItem(storageKey)
|
|
69
|
+
setColumnLayout(initialWidths)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Restore from localStorage
|
|
74
|
+
setColumnLayout(storedLayout)
|
|
75
|
+
}, [columns.length, hasSelectionColumn, id, initialWidths])
|
|
76
|
+
|
|
77
|
+
// On layout change, save to local storage.
|
|
78
|
+
const handleLayoutChange = (newLayout: string) => {
|
|
79
|
+
if (!id) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const storageKey = `table-${id}`
|
|
84
|
+
|
|
85
|
+
const newLayoutArray = newLayout.match(/(?:minmax\([^()]*\)|[^\s]+)/g)
|
|
86
|
+
if (!newLayoutArray) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (hasSelectionColumn) {
|
|
91
|
+
newLayoutArray.shift()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let newLayoutString = newLayoutArray
|
|
95
|
+
.map((width, index) => {
|
|
96
|
+
const column = columns[index]
|
|
97
|
+
const columnWidth = column?.props?.width
|
|
98
|
+
const resizeable = column?.props?.resize !== false
|
|
99
|
+
|
|
100
|
+
return resizeable || !columnWidth
|
|
101
|
+
? width.includes('minmax')
|
|
102
|
+
? width
|
|
103
|
+
: `minmax(${width}, 1fr)`
|
|
104
|
+
: formatWidth(columnWidth)
|
|
105
|
+
})
|
|
106
|
+
.join(' ')
|
|
107
|
+
|
|
108
|
+
if (hasSelectionColumn) {
|
|
109
|
+
newLayoutString = `8px ${newLayoutString}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
localStorage.setItem(storageKey, newLayoutString)
|
|
113
|
+
setColumnLayout(newLayoutString)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { handleLayoutChange, columnLayout }
|
|
117
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { isNumber } from 'lodash-es'
|
|
2
|
+
import { ReactNode, RefObject, useCallback, useMemo, useState } from 'react'
|
|
3
|
+
import { createGroupEmptyStateMarker } from './is-group-empty-state'
|
|
4
|
+
import { getRowHeight } from './table'
|
|
5
|
+
import { useTable } from './table-context'
|
|
6
|
+
|
|
7
|
+
export const useGroups = (
|
|
8
|
+
data: Array<any>,
|
|
9
|
+
tableRef: RefObject<any>,
|
|
10
|
+
grouped?: {
|
|
11
|
+
counts: Array<number>
|
|
12
|
+
header: (index: number) => ReactNode
|
|
13
|
+
emptyState?: (index: number) => ReactNode
|
|
14
|
+
},
|
|
15
|
+
) => {
|
|
16
|
+
const { collapsedGroups, density } = useTable()
|
|
17
|
+
const rowHeight = getRowHeight(density)
|
|
18
|
+
const [firstIndex, setFirstIndex] = useState(0)
|
|
19
|
+
|
|
20
|
+
if (grouped) {
|
|
21
|
+
if (!Array.isArray(grouped.counts)) {
|
|
22
|
+
throw new Error('Grouped counts must be an array.')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const totalGroupedItems = grouped.counts.reduce((prev, current) => prev + current, 0)
|
|
26
|
+
if (totalGroupedItems !== data?.length) {
|
|
27
|
+
throw new Error('All items must be assigned to a group.')
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const groupedData = useMemo(() => {
|
|
32
|
+
if (!grouped) {
|
|
33
|
+
return data || []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let offset = 0
|
|
37
|
+
const groups = grouped.counts.map((count, i) => {
|
|
38
|
+
const isCollapsed = collapsedGroups?.includes(i)
|
|
39
|
+
const rows = !isCollapsed ? data.slice(offset, offset + count) : []
|
|
40
|
+
|
|
41
|
+
const group = [grouped.header(i), ...rows]
|
|
42
|
+
|
|
43
|
+
if (count === 0 && !isCollapsed && grouped.emptyState) {
|
|
44
|
+
group.push(createGroupEmptyStateMarker(), createGroupEmptyStateMarker(true))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
offset += count
|
|
48
|
+
return group
|
|
49
|
+
})
|
|
50
|
+
return groups.flat()
|
|
51
|
+
}, [data, grouped, collapsedGroups])
|
|
52
|
+
|
|
53
|
+
const groupIndexByItemIndex = useMemo(() => {
|
|
54
|
+
if (!grouped) {
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const items: Array<number> = []
|
|
59
|
+
grouped.counts.forEach((count, i) => {
|
|
60
|
+
if (!collapsedGroups?.includes(i)) {
|
|
61
|
+
const hasEmptyState = count === 0 && grouped.emptyState
|
|
62
|
+
for (let j = 0; j < count + 1 + (hasEmptyState ? 2 : 0); j++) {
|
|
63
|
+
items.push(i)
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
items.push(i)
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return items
|
|
71
|
+
}, [grouped, collapsedGroups])
|
|
72
|
+
|
|
73
|
+
const getGroupIndex = useCallback(
|
|
74
|
+
(rowIndex: number): number => {
|
|
75
|
+
return groupIndexByItemIndex[rowIndex] ?? 0
|
|
76
|
+
},
|
|
77
|
+
[groupIndexByItemIndex],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const currentGroupIndex = useMemo(() => {
|
|
81
|
+
if (!grouped) {
|
|
82
|
+
return undefined
|
|
83
|
+
}
|
|
84
|
+
return getGroupIndex(firstIndex)
|
|
85
|
+
}, [grouped, firstIndex, getGroupIndex])
|
|
86
|
+
|
|
87
|
+
const currentGroup = useMemo(() => {
|
|
88
|
+
if (!grouped || grouped.counts.length === 0) {
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
const groupIndex = getGroupIndex(firstIndex)
|
|
92
|
+
return grouped.header(groupIndex)
|
|
93
|
+
}, [grouped, firstIndex, getGroupIndex])
|
|
94
|
+
|
|
95
|
+
const handleScroll = useCallback(() => {
|
|
96
|
+
if (!grouped || !tableRef.current) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
const scrollTop: number = Number(
|
|
100
|
+
tableRef.current.querySelector('[data-table-library_table] > div > div')?.scrollTop,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if (scrollTop >= 0 && scrollTop <= rowHeight) {
|
|
104
|
+
setFirstIndex(0)
|
|
105
|
+
} else {
|
|
106
|
+
try {
|
|
107
|
+
groupedData.reduce((prev, _, currentIndex) => {
|
|
108
|
+
const height = rowHeight
|
|
109
|
+
const start = isNumber(prev) ? prev : rowHeight
|
|
110
|
+
const end = start + height
|
|
111
|
+
|
|
112
|
+
if (scrollTop >= start && scrollTop <= end) {
|
|
113
|
+
setFirstIndex(currentIndex)
|
|
114
|
+
throw 'BREAK'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return end
|
|
118
|
+
})
|
|
119
|
+
} catch (e) {
|
|
120
|
+
if (e !== 'BREAK') {
|
|
121
|
+
throw e
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}, [grouped, tableRef, groupedData, rowHeight])
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
data: groupedData,
|
|
129
|
+
handleScroll,
|
|
130
|
+
currentGroup,
|
|
131
|
+
getGroupIndex,
|
|
132
|
+
currentGroupIndex,
|
|
133
|
+
}
|
|
134
|
+
}
|