@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pharos-ds/core",
3
- "version": "1.0.9",
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();
@@ -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
- const hasStickyBottom = !!(footer || footerIcons);
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
- {footer}
548
+ {visibleFooter}
541
549
  <div
542
550
  {...stylex.props(
543
551
  styles.footerRow,