Skip to content

Commit e86732c

Browse files
committed
Merge branch 'feat/tree-view' into tree-view/more-a11y
2 parents 83b0e5d + 22e9fea commit e86732c

File tree

11 files changed

+424
-542
lines changed

11 files changed

+424
-542
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import React from 'react'
2+
3+
import type { Column, ItemKey, SectionItem } from '../types.js'
4+
5+
import { Row } from '../Row/index.js'
6+
7+
interface ItemWithChildrenProps {
8+
columns: Column[]
9+
dropContextName: string
10+
firstCellRef?: React.RefObject<HTMLDivElement>
11+
firstCellWidth: number
12+
firstCellXOffset: number
13+
focusedItemIndex?: number
14+
hasSelectedAncestor?: boolean
15+
hoveredItemKey: ItemKey | null
16+
isDragging: boolean
17+
isLastItemOfRoot?: boolean
18+
items: SectionItem[]
19+
level?: number
20+
loadingItemKeys?: Set<ItemKey>
21+
onDroppableHover: (params: {
22+
hoveredItemKey?: ItemKey
23+
placement?: string
24+
targetItem: null | SectionItem
25+
}) => void
26+
onFocusChange: (focusedIndex: number) => void
27+
onItemDrag: (params: { event: PointerEvent; item: null | SectionItem }) => void
28+
onItemKeyPress: (params: { event: React.KeyboardEvent; item: SectionItem }) => void
29+
onSelectionChange: ({
30+
itemKey,
31+
options,
32+
}: {
33+
itemKey: ItemKey
34+
options: {
35+
ctrlKey: boolean
36+
metaKey: boolean
37+
shiftKey: boolean
38+
}
39+
}) => void
40+
openItemKeys?: Set<ItemKey>
41+
parentIndex?: number
42+
parentItems?: SectionItem[]
43+
segmentWidth: number
44+
selectedItemKeys?: Set<ItemKey>
45+
targetParentItemKey: ItemKey | null
46+
toggleItemExpand: (itemKey: ItemKey) => void
47+
}
48+
49+
export const NestedItems: React.FC<ItemWithChildrenProps> = ({
50+
columns,
51+
dropContextName,
52+
firstCellRef,
53+
firstCellWidth,
54+
firstCellXOffset,
55+
focusedItemIndex,
56+
hasSelectedAncestor = false,
57+
hoveredItemKey,
58+
isDragging,
59+
isLastItemOfRoot = false,
60+
items,
61+
level = 0,
62+
loadingItemKeys,
63+
onDroppableHover,
64+
onFocusChange,
65+
onItemDrag,
66+
onItemKeyPress,
67+
onSelectionChange,
68+
openItemKeys,
69+
parentIndex = 0,
70+
parentItems = [],
71+
segmentWidth,
72+
selectedItemKeys = new Set<ItemKey>(),
73+
targetParentItemKey,
74+
toggleItemExpand,
75+
}) => {
76+
// Helper to count all rows recursively, only counting visible (open) rows
77+
const countItems = (items: SectionItem[]): number => {
78+
return items.reduce((count, item) => {
79+
const isOpen = openItemKeys?.has(item.itemKey)
80+
return count + 1 + (item.rows && isOpen ? countItems(item.rows) : 0)
81+
}, 0)
82+
}
83+
84+
// Calculate absolute row index for each row before render
85+
const getAbsoluteItemIndex = (index: number): number => {
86+
let offset = parentIndex
87+
for (let i = 0; i < index; i++) {
88+
offset += 1
89+
const isOpen = openItemKeys?.has(items[i].itemKey)
90+
if (items[i].rows && isOpen) {
91+
offset += countItems(items[i].rows || [])
92+
}
93+
}
94+
return offset
95+
}
96+
97+
return (
98+
<>
99+
{items.map((sectionItem, sectionItemIndex: number) => {
100+
const absoluteItemIndex = getAbsoluteItemIndex(sectionItemIndex)
101+
const isLastSiblingItem = items.length - 1 === sectionItemIndex
102+
const hasNestedItems =
103+
Boolean(sectionItem?.rows?.length) && openItemKeys?.has(sectionItem.itemKey)
104+
105+
const isItemSelected = selectedItemKeys.has(sectionItem.itemKey)
106+
const isInvalidTarget = hasSelectedAncestor || isItemSelected
107+
const isItemAtRootLevel = level === 0 || (isLastSiblingItem && isLastItemOfRoot)
108+
const isFirstItemAtRootLevel = level === 0 && sectionItemIndex === 0
109+
110+
// Calculate drop target items based on position in hierarchy
111+
let targetItems: (null | SectionItem)[] = []
112+
113+
if (level === 0) {
114+
targetItems = hasNestedItems ? [sectionItem] : []
115+
} else if (isLastSiblingItem) {
116+
targetItems = hasNestedItems
117+
? [sectionItem]
118+
: isItemAtRootLevel
119+
? [...parentItems]
120+
: [parentItems[parentItems.length - 2], parentItems[parentItems.length - 1]].filter(
121+
Boolean,
122+
)
123+
} else {
124+
targetItems = hasNestedItems ? [sectionItem] : [parentItems[parentItems.length - 1]]
125+
}
126+
127+
// Allow dropping at root level for last item without nested items
128+
if (isItemAtRootLevel && !hasNestedItems) {
129+
targetItems = [null, ...targetItems]
130+
}
131+
132+
const startOffset =
133+
isLastSiblingItem && !hasNestedItems
134+
? firstCellWidth + segmentWidth * (level - targetItems.length + 1) + 28
135+
: firstCellWidth + segmentWidth * (hasNestedItems ? level + 1 : level) + 28
136+
137+
return (
138+
<React.Fragment key={sectionItem.itemKey}>
139+
<Row
140+
absoluteIndex={absoluteItemIndex}
141+
columns={columns}
142+
dropContextName={dropContextName}
143+
firstCellRef={level === 0 && sectionItemIndex === 0 ? firstCellRef : undefined}
144+
firstCellWidth={firstCellWidth}
145+
firstCellXOffset={firstCellXOffset}
146+
hasSelectedAncestor={hasSelectedAncestor}
147+
isDragging={isDragging}
148+
isFirstRootItem={isFirstItemAtRootLevel}
149+
isFocused={focusedItemIndex !== undefined && focusedItemIndex === absoluteItemIndex}
150+
isHovered={hoveredItemKey === sectionItem.itemKey}
151+
isInvalidTarget={isInvalidTarget}
152+
isSelected={isItemSelected}
153+
item={sectionItem}
154+
level={level}
155+
loadingItemKeys={loadingItemKeys}
156+
onClick={onSelectionChange}
157+
onDrag={onItemDrag}
158+
onDroppableHover={onDroppableHover}
159+
onFocusChange={onFocusChange}
160+
onKeyPress={onItemKeyPress}
161+
openItemKeys={openItemKeys}
162+
segmentWidth={segmentWidth}
163+
selectedItemKeys={selectedItemKeys}
164+
startOffset={startOffset}
165+
targetItems={targetItems}
166+
targetParentItemKey={targetParentItemKey}
167+
toggleExpand={toggleItemExpand}
168+
/>
169+
170+
{hasNestedItems && sectionItem.rows && (
171+
<NestedItems
172+
columns={columns}
173+
dropContextName={dropContextName}
174+
firstCellWidth={firstCellWidth}
175+
firstCellXOffset={firstCellXOffset}
176+
focusedItemIndex={focusedItemIndex}
177+
hasSelectedAncestor={hasSelectedAncestor || isItemSelected}
178+
hoveredItemKey={hoveredItemKey}
179+
isDragging={isDragging}
180+
isLastItemOfRoot={isItemAtRootLevel}
181+
items={sectionItem.rows}
182+
level={level + 1}
183+
loadingItemKeys={loadingItemKeys}
184+
onDroppableHover={onDroppableHover}
185+
onFocusChange={onFocusChange}
186+
onItemDrag={onItemDrag}
187+
onItemKeyPress={onItemKeyPress}
188+
onSelectionChange={onSelectionChange}
189+
openItemKeys={openItemKeys}
190+
parentIndex={absoluteItemIndex + 1}
191+
parentItems={[...parentItems, sectionItem]}
192+
segmentWidth={segmentWidth}
193+
selectedItemKeys={selectedItemKeys}
194+
targetParentItemKey={targetParentItemKey}
195+
toggleItemExpand={toggleItemExpand}
196+
/>
197+
)}
198+
</React.Fragment>
199+
)
200+
})}
201+
</>
202+
)
203+
}

packages/ui/src/elements/TreeView/NestedSectionsTable/Row/index.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
}
4141

4242
.nested-sections-table-row {
43+
--cell-inline-padding-start: calc(var(--base) * 0.6);
44+
--cell-inline-padding-end: calc(var(--base) * 0.6);
45+
4346
display: table-row;
4447
min-width: 100%;
4548
border-collapse: collapse;

0 commit comments

Comments
 (0)