@pharos-ds/core 1.0.9 → 1.0.10-canary.8c2af6f
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/CHANGELOG.md +4 -0
- package/dist/SideNav/SideNav.d.ts +5 -1
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +8 -2
- package/dist/pharos.umd.js +12 -12
- package/dist/pharos.umd.js.map +3 -3
- package/package.json +1 -1
- package/src/SideNav/SideNav.doc.mjs +13 -4
- package/src/SideNav/SideNav.test.tsx +65 -0
- package/src/SideNav/SideNav.tsx +11 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pharos-ds/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10-canary.8c2af6f",
|
|
4
4
|
"displayName": "Pharos Core",
|
|
5
5
|
"description": "Pharos component library. Accessible, themeable React components with spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Caio Silvestre",
|
|
@@ -25,7 +25,7 @@ export const docs = {
|
|
|
25
25
|
{className: 'pharos-side-nav-section', states: ['collapsed']},
|
|
26
26
|
],
|
|
27
27
|
},
|
|
28
|
-
description: 'Container with five zones: header, topContent, children (scrollable), footer, and footerIcons. Supports collapsible mode.',
|
|
28
|
+
description: 'Container with five zones: header, topContent, children (scrollable), footer, and footerIcons. Supports collapsible mode: the collapsed rail shows only item icons, footerIcons, and the collapse button (footer is hidden).',
|
|
29
29
|
props: [
|
|
30
30
|
{
|
|
31
31
|
name: 'header',
|
|
@@ -71,7 +71,7 @@ export const docs = {
|
|
|
71
71
|
{
|
|
72
72
|
name: 'footer',
|
|
73
73
|
type: 'ReactNode',
|
|
74
|
-
description: 'Footer area above the icon bar.',
|
|
74
|
+
description: 'Footer area above the icon bar. Hidden while the SideNav is collapsed to the icon rail, since full-width content does not fit; put actions that must stay reachable in footerIcons.',
|
|
75
75
|
slotElements: [
|
|
76
76
|
{
|
|
77
77
|
__element: 'Text',
|
|
@@ -85,7 +85,7 @@ export const docs = {
|
|
|
85
85
|
{
|
|
86
86
|
name: 'footerIcons',
|
|
87
87
|
type: 'ReactNode',
|
|
88
|
-
description: 'Footer icon bar.',
|
|
88
|
+
description: 'Footer icon bar. Stays visible in the collapsed rail, next to the collapse button.',
|
|
89
89
|
slotElements: [
|
|
90
90
|
{
|
|
91
91
|
__element: 'Icon',
|
|
@@ -99,7 +99,7 @@ export const docs = {
|
|
|
99
99
|
{
|
|
100
100
|
name: 'collapsible',
|
|
101
101
|
type: 'boolean | { defaultIsCollapsed?: boolean; isCollapsed?: boolean; onCollapsedChange?: (isCollapsed: boolean) => void; hasButton?: boolean; buttonLabel?: string }',
|
|
102
|
-
description: 'Enables collapse behavior. true for uncontrolled with default toggle button, or an object for controlled mode and advanced config (defaultIsCollapsed, isCollapsed + onCollapsedChange, hasButton, buttonLabel).',
|
|
102
|
+
description: 'Enables collapse behavior. true for uncontrolled with default toggle button, or an object for controlled mode and advanced config (defaultIsCollapsed, isCollapsed + onCollapsedChange, hasButton, buttonLabel). When collapsed, the rail shows item icons only (items without icons are hidden), keeps footerIcons and the collapse button, and does not render footer. Read the state inside the nav with useSideNavCollapse().',
|
|
103
103
|
default: 'false',
|
|
104
104
|
},
|
|
105
105
|
{
|
|
@@ -131,6 +131,9 @@ export const docs = {
|
|
|
131
131
|
bestPractices: [
|
|
132
132
|
{guidance: true, description: 'Use sections to group related navigation items and help users scan for their destination.'},
|
|
133
133
|
{guidance: true, description: 'Pair outline and filled icon variants so the selected state is visually distinct.'},
|
|
134
|
+
{guidance: true, description: 'Make the SideNav of an app shell collapsible so users can fold it into an icon rail; control it with collapsible={{isCollapsed, onCollapsedChange}} to persist the choice.'},
|
|
135
|
+
{guidance: true, description: 'Give every top-level SideNavItem an icon: the collapsed rail hides items without one. Swap the header for a compact mark (e.g. a symbol logo) by reading useSideNavCollapse().isCollapsed.'},
|
|
136
|
+
{guidance: true, description: 'Put actions that must stay reachable in the rail (settings, account, theme, sign out) in footerIcons as icon buttons; footer is hidden while collapsed, so use it only for full-width extras such as promo cards or density controls.'},
|
|
134
137
|
{guidance: false, description: 'Include a SideNavHeading when a TopNav is already providing app identity; this duplicates branding.'},
|
|
135
138
|
{guidance: false, description: 'Use for filtering content; use tabs or filter buttons instead.'},
|
|
136
139
|
],
|
|
@@ -150,6 +153,9 @@ export const docsZh = {
|
|
|
150
153
|
bestPractices: [
|
|
151
154
|
{guidance: true, description: 'Use sections to group related navigation items and help users scan for their destination.'},
|
|
152
155
|
{guidance: true, description: 'Pair outline and filled icon variants so the selected state is visually distinct.'},
|
|
156
|
+
{guidance: true, description: 'Make the SideNav of an app shell collapsible so users can fold it into an icon rail; control it with collapsible={{isCollapsed, onCollapsedChange}} to persist the choice.'},
|
|
157
|
+
{guidance: true, description: 'Give every top-level SideNavItem an icon: the collapsed rail hides items without one. Swap the header for a compact mark (e.g. a symbol logo) by reading useSideNavCollapse().isCollapsed.'},
|
|
158
|
+
{guidance: true, description: 'Put actions that must stay reachable in the rail (settings, account, theme, sign out) in footerIcons as icon buttons; footer is hidden while collapsed, so use it only for full-width extras such as promo cards or density controls.'},
|
|
153
159
|
{guidance: false, description: 'Include a SideNavHeading when a TopNav is already providing app identity; this duplicates branding.'},
|
|
154
160
|
{guidance: false, description: 'Use for filtering content; use tabs or filter buttons instead.'},
|
|
155
161
|
],
|
|
@@ -169,6 +175,9 @@ export const docsDense = {
|
|
|
169
175
|
bestPractices: [
|
|
170
176
|
{guidance: true, description: 'Use sections to group related navigation items and help users scan for their destination.'},
|
|
171
177
|
{guidance: true, description: 'Pair outline and filled icon variants so the selected state is visually distinct.'},
|
|
178
|
+
{guidance: true, description: 'Make the SideNav of an app shell collapsible so users can fold it into an icon rail; control it with collapsible={{isCollapsed, onCollapsedChange}} to persist the choice.'},
|
|
179
|
+
{guidance: true, description: 'Give every top-level SideNavItem an icon: the collapsed rail hides items without one. Swap the header for a compact mark (e.g. a symbol logo) by reading useSideNavCollapse().isCollapsed.'},
|
|
180
|
+
{guidance: true, description: 'Put actions that must stay reachable in the rail (settings, account, theme, sign out) in footerIcons as icon buttons; footer is hidden while collapsed, so use it only for full-width extras such as promo cards or density controls.'},
|
|
172
181
|
{guidance: false, description: 'Include a SideNavHeading when a TopNav is already providing app identity; this duplicates branding.'},
|
|
173
182
|
{guidance: false, description: 'Use for filtering content; use tabs or filter buttons instead.'},
|
|
174
183
|
],
|
|
@@ -183,6 +183,71 @@ describe('SideNav', () => {
|
|
|
183
183
|
expect(nav.children).toHaveLength(1);
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
describe('footer in the collapsed rail', () => {
|
|
187
|
+
it('hides footer but keeps footerIcons and the collapse button when collapsed', () => {
|
|
188
|
+
render(
|
|
189
|
+
<SideNav
|
|
190
|
+
collapsible={{defaultIsCollapsed: true}}
|
|
191
|
+
footer={<span data-testid="footer">Footer</span>}
|
|
192
|
+
footerIcons={<span data-testid="icons">Icons</span>}>
|
|
193
|
+
<SideNavItem label="Dashboard" icon={StubIcon} />
|
|
194
|
+
</SideNav>,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(screen.queryByTestId('footer')).not.toBeInTheDocument();
|
|
198
|
+
expect(screen.getByTestId('icons')).toBeInTheDocument();
|
|
199
|
+
expect(
|
|
200
|
+
screen.getByRole('button', {name: 'Expand sidebar'}),
|
|
201
|
+
).toBeInTheDocument();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('shows footer again after expanding the rail', async () => {
|
|
205
|
+
const user = userEvent.setup();
|
|
206
|
+
render(
|
|
207
|
+
<SideNav
|
|
208
|
+
collapsible={{defaultIsCollapsed: true}}
|
|
209
|
+
footer={<span data-testid="footer">Footer</span>}>
|
|
210
|
+
<SideNavItem label="Dashboard" icon={StubIcon} />
|
|
211
|
+
</SideNav>,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(screen.queryByTestId('footer')).not.toBeInTheDocument();
|
|
215
|
+
await user.click(screen.getByRole('button', {name: 'Expand sidebar'}));
|
|
216
|
+
expect(screen.getByTestId('footer')).toBeInTheDocument();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('does not leave an empty footer container when only footer is set and the rail has no button', () => {
|
|
220
|
+
render(
|
|
221
|
+
<SideNav
|
|
222
|
+
data-testid="nav"
|
|
223
|
+
collapsible={{isCollapsed: true, hasButton: false}}
|
|
224
|
+
footer={<span data-testid="footer">Footer</span>}>
|
|
225
|
+
Content
|
|
226
|
+
</SideNav>,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
expect(screen.queryByTestId('footer')).not.toBeInTheDocument();
|
|
230
|
+
expect(screen.getByTestId('nav').children).toHaveLength(1);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('keeps rendering footer when the SideNav is expanded or not collapsible', () => {
|
|
234
|
+
const {unmount} = render(
|
|
235
|
+
<SideNav collapsible footer={<span data-testid="footer">Footer</span>}>
|
|
236
|
+
Content
|
|
237
|
+
</SideNav>,
|
|
238
|
+
);
|
|
239
|
+
expect(screen.getByTestId('footer')).toBeInTheDocument();
|
|
240
|
+
unmount();
|
|
241
|
+
|
|
242
|
+
render(
|
|
243
|
+
<SideNav footer={<span data-testid="footer">Footer</span>}>
|
|
244
|
+
Content
|
|
245
|
+
</SideNav>,
|
|
246
|
+
);
|
|
247
|
+
expect(screen.getByTestId('footer')).toBeInTheDocument();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
186
251
|
it('fires a consumer onClick on the collapse button in addition to toggling', async () => {
|
|
187
252
|
const user = userEvent.setup();
|
|
188
253
|
const onClick = vi.fn();
|
package/src/SideNav/SideNav.tsx
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Sidebar navigation container with five zones: header + topContent (sticky together),
|
|
12
12
|
* children (scrollable), footer, and footerIcons (sticky bottom).
|
|
13
|
+
* When collapsed to the icon rail, `footer` is hidden; `footerIcons` and the
|
|
14
|
+
* collapse button remain.
|
|
13
15
|
*
|
|
14
16
|
* Supports optional resize via drag handle at the inline-end edge.
|
|
15
17
|
*
|
|
@@ -212,7 +214,9 @@ export interface SideNavProps extends BaseProps<HTMLElement> {
|
|
|
212
214
|
*/
|
|
213
215
|
children: ReactNode;
|
|
214
216
|
/**
|
|
215
|
-
* Footer area above icon bar (e.g., promo cards).
|
|
217
|
+
* Footer area above icon bar (e.g., promo cards). Not rendered while the
|
|
218
|
+
* sidebar is collapsed to the icon rail (full-width content doesn't fit);
|
|
219
|
+
* use `footerIcons` for actions that must stay visible in the rail.
|
|
216
220
|
*/
|
|
217
221
|
footer?: ReactNode;
|
|
218
222
|
/**
|
|
@@ -484,7 +488,11 @@ export function SideNav({
|
|
|
484
488
|
// Default mode — full sidebar
|
|
485
489
|
// =========================================================================
|
|
486
490
|
const hasStickyTop = !!(header || topContent);
|
|
487
|
-
|
|
491
|
+
// In the collapsed rail, `footer` is not rendered: its content is laid out
|
|
492
|
+
// full-width (promo cards, segmented controls, account blocks) and doesn't
|
|
493
|
+
// fit the icon-only rail. `footerIcons` and the collapse button stay.
|
|
494
|
+
const visibleFooter = collapsed ? null : footer;
|
|
495
|
+
const hasStickyBottom = !!(visibleFooter || footerIcons);
|
|
488
496
|
// The built-in collapse button only renders when collapse is enabled and it
|
|
489
497
|
// hasn't been opted out via `collapsible.hasButton: false` (e.g. when the
|
|
490
498
|
// consumer places a SideNavCollapseButton in the header instead).
|
|
@@ -537,7 +545,7 @@ export function SideNav({
|
|
|
537
545
|
styles.stickyBottom,
|
|
538
546
|
collapsed && styles.stickyBottomCollapsed,
|
|
539
547
|
)}>
|
|
540
|
-
{
|
|
548
|
+
{visibleFooter}
|
|
541
549
|
<div
|
|
542
550
|
{...stylex.props(
|
|
543
551
|
styles.footerRow,
|