@duro-app/ui 0.38.0 → 0.38.1
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/SideNav/SideNav.d.ts.map +1 -1
- package/dist/components/SideNav/SideNav.stories.d.ts +1 -0
- package/dist/components/SideNav/SideNav.stories.d.ts.map +1 -1
- package/dist/components/SideNav/SideNavContext.d.ts +4 -0
- package/dist/components/SideNav/SideNavContext.d.ts.map +1 -1
- package/dist/index.js +1584 -1579
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/SideNav/SideNav.stories.tsx +22 -0
- package/src/components/SideNav/SideNav.tsx +31 -18
- package/src/components/SideNav/SideNavContext.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@duro-app/tokens": "^0.38.
|
|
57
|
+
"@duro-app/tokens": "^0.38.1"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@babel/preset-typescript": "^7.28.0",
|
|
@@ -58,6 +58,28 @@ export const Collapsed: Story = {
|
|
|
58
58
|
},
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// Regression: a `defaultExpanded` group that ALSO contains the active item used
|
|
62
|
+
// to be toggled twice on mount (once by defaultExpanded, once by auto-expand of
|
|
63
|
+
// the active group) and end up collapsed, hiding the active item. It must stay
|
|
64
|
+
// open.
|
|
65
|
+
export const DefaultExpandedWithActiveItem: Story = {
|
|
66
|
+
render: (args) => (
|
|
67
|
+
<SideNav.Root {...args} defaultValue="security">
|
|
68
|
+
<SideNav.Group label="Settings" defaultExpanded>
|
|
69
|
+
<SideNav.Item value="profile">Profile</SideNav.Item>
|
|
70
|
+
<SideNav.Item value="security">Security</SideNav.Item>
|
|
71
|
+
</SideNav.Group>
|
|
72
|
+
</SideNav.Root>
|
|
73
|
+
),
|
|
74
|
+
play: async ({canvas}) => {
|
|
75
|
+
const trigger = canvas.getByText('Settings')
|
|
76
|
+
await expect(trigger).toHaveAttribute('aria-expanded', 'true')
|
|
77
|
+
const active = canvas.getByText('Security')
|
|
78
|
+
await expect(active).toBeInTheDocument()
|
|
79
|
+
await expect(active).toHaveAttribute('aria-current', 'page')
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
61
83
|
export const Interactive: Story = {
|
|
62
84
|
render: (args) => (
|
|
63
85
|
<SideNav.Root {...args}>
|
|
@@ -38,6 +38,19 @@ function Root({children, value: controlledValue, defaultValue, onValueChange}: R
|
|
|
38
38
|
})
|
|
39
39
|
}, [])
|
|
40
40
|
|
|
41
|
+
// Idempotent expand — never closes an already-open group. The mount effects
|
|
42
|
+
// in Group (defaultExpanded + auto-expand-active) use this instead of
|
|
43
|
+
// toggleGroup so a group that qualifies on both counts stays open rather than
|
|
44
|
+
// being toggled open-then-closed.
|
|
45
|
+
const expandGroup = useCallback((group: string) => {
|
|
46
|
+
setExpandedGroups((prev) => {
|
|
47
|
+
if (prev.has(group)) return prev
|
|
48
|
+
const next = new Set(prev)
|
|
49
|
+
next.add(group)
|
|
50
|
+
return next
|
|
51
|
+
})
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
41
54
|
const registerItem = useCallback((value: string) => {
|
|
42
55
|
if (!orderRef.current.includes(value)) {
|
|
43
56
|
orderRef.current.push(value)
|
|
@@ -47,19 +60,17 @@ function Root({children, value: controlledValue, defaultValue, onValueChange}: R
|
|
|
47
60
|
}
|
|
48
61
|
}, [])
|
|
49
62
|
|
|
50
|
-
// Auto-expand group containing active value
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
if (activeValue) {
|
|
53
|
-
setExpandedGroups((prev) => {
|
|
54
|
-
// We don't know which group it belongs to here — Group handles this
|
|
55
|
-
return prev
|
|
56
|
-
})
|
|
57
|
-
}
|
|
58
|
-
}, [activeValue])
|
|
59
|
-
|
|
60
63
|
return (
|
|
61
64
|
<SideNavContext.Provider
|
|
62
|
-
value={{
|
|
65
|
+
value={{
|
|
66
|
+
activeValue,
|
|
67
|
+
onSelect,
|
|
68
|
+
expandedGroups,
|
|
69
|
+
toggleGroup,
|
|
70
|
+
expandGroup,
|
|
71
|
+
registerItem,
|
|
72
|
+
orderRef,
|
|
73
|
+
}}
|
|
63
74
|
>
|
|
64
75
|
<html.nav role="navigation" style={styles.root}>
|
|
65
76
|
{children}
|
|
@@ -79,25 +90,27 @@ interface GroupProps {
|
|
|
79
90
|
|
|
80
91
|
function Group({children, label, groupKey, defaultExpanded}: GroupProps) {
|
|
81
92
|
const key = groupKey ?? label
|
|
82
|
-
const {expandedGroups, toggleGroup, activeValue} = useSideNav()
|
|
93
|
+
const {expandedGroups, toggleGroup, expandGroup, activeValue} = useSideNav()
|
|
83
94
|
const isExpanded = expandedGroups.has(key)
|
|
84
95
|
const groupRef = useRef<HTMLDivElement>(null)
|
|
85
96
|
|
|
86
|
-
// Auto-expand if this group contains the active item
|
|
97
|
+
// Auto-expand if this group contains the active item. Uses expandGroup (not
|
|
98
|
+
// toggleGroup) so it never races the defaultExpanded effect into a closed
|
|
99
|
+
// state when the active item lives in a defaultExpanded group.
|
|
87
100
|
useEffect(() => {
|
|
88
101
|
if (!activeValue || expandedGroups.has(key)) return
|
|
89
102
|
const el = groupRef.current
|
|
90
103
|
if (!el) return
|
|
91
104
|
const activeBtn = el.querySelector(`[data-nav-value="${activeValue}"]`)
|
|
92
105
|
if (activeBtn) {
|
|
93
|
-
|
|
106
|
+
expandGroup(key)
|
|
94
107
|
}
|
|
95
|
-
}, [activeValue, key, expandedGroups,
|
|
108
|
+
}, [activeValue, key, expandedGroups, expandGroup])
|
|
96
109
|
|
|
97
|
-
// Expand on first render if defaultExpanded
|
|
110
|
+
// Expand on first render if defaultExpanded (idempotent add, not a toggle).
|
|
98
111
|
useEffect(() => {
|
|
99
|
-
if (defaultExpanded
|
|
100
|
-
|
|
112
|
+
if (defaultExpanded) {
|
|
113
|
+
expandGroup(key)
|
|
101
114
|
}
|
|
102
115
|
// Only run on mount
|
|
103
116
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -5,6 +5,10 @@ export interface SideNavContextValue {
|
|
|
5
5
|
onSelect: (value: string) => void
|
|
6
6
|
expandedGroups: Set<string>
|
|
7
7
|
toggleGroup: (group: string) => void
|
|
8
|
+
/** Idempotent expand (add-only) — used by auto-expand/defaultExpanded so a
|
|
9
|
+
* group that qualifies twice (e.g. defaultExpanded AND holding the active
|
|
10
|
+
* item) doesn't get toggled back closed. `toggleGroup` stays for user clicks. */
|
|
11
|
+
expandGroup: (group: string) => void
|
|
8
12
|
registerItem: (value: string) => () => void
|
|
9
13
|
orderRef: React.RefObject<string[]>
|
|
10
14
|
}
|