@pharos-ds/core 1.0.5 → 1.0.7
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 +18 -0
- package/dist/SideNav/SideNavSection.d.ts +21 -1
- package/dist/SideNav/SideNavSection.d.ts.map +1 -1
- package/dist/SideNav/SideNavSection.js +105 -25
- package/dist/pharos.css +1 -0
- package/dist/pharos.umd.js +11 -11
- package/dist/pharos.umd.js.map +3 -3
- package/package.json +1 -1
- package/src/SideNav/SideNav.doc.mjs +1 -1
- package/src/SideNav/SideNav.test.tsx +183 -0
- package/src/SideNav/SideNavSection.doc.mjs +13 -0
- package/src/SideNav/SideNavSection.tsx +222 -17
package/package.json
CHANGED
|
@@ -22,7 +22,7 @@ export const docs = {
|
|
|
22
22
|
{className: 'pharos-side-nav', visualProps: ['mode']},
|
|
23
23
|
{className: 'pharos-side-nav-heading'},
|
|
24
24
|
{className: 'pharos-side-nav-item', visualProps: ['size'], states: ['selected']},
|
|
25
|
-
{className: 'pharos-side-nav-section'},
|
|
25
|
+
{className: 'pharos-side-nav-section', states: ['collapsed']},
|
|
26
26
|
],
|
|
27
27
|
},
|
|
28
28
|
description: 'Container with five zones: header, topContent, children (scrollable), footer, and footerIcons. Supports collapsible mode.',
|
|
@@ -918,6 +918,189 @@ describe('SideNavSection', () => {
|
|
|
918
918
|
});
|
|
919
919
|
});
|
|
920
920
|
|
|
921
|
+
describe('SideNavSection — collapsible', () => {
|
|
922
|
+
const items = (
|
|
923
|
+
<>
|
|
924
|
+
<SideNavItem label="Dashboard" />
|
|
925
|
+
<SideNavItem label="Reports" />
|
|
926
|
+
</>
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
function getItemsRegion(button: HTMLElement) {
|
|
930
|
+
const id = button.getAttribute('aria-controls');
|
|
931
|
+
expect(id).toBeTruthy();
|
|
932
|
+
return document.getElementById(id!)!;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
it('is not collapsible by default (no toggle button)', () => {
|
|
936
|
+
render(<SideNavSection title="Main">{items}</SideNavSection>);
|
|
937
|
+
expect(
|
|
938
|
+
screen.queryByRole('button', {name: 'Main'}),
|
|
939
|
+
).not.toBeInTheDocument();
|
|
940
|
+
expect(document.querySelector('[aria-expanded]')).toBeNull();
|
|
941
|
+
expect(document.querySelector('[inert]')).toBeNull();
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it('renders the title as a toggle button named by the title', () => {
|
|
945
|
+
render(
|
|
946
|
+
<SideNavSection title="Main" collapsible>
|
|
947
|
+
{items}
|
|
948
|
+
</SideNavSection>,
|
|
949
|
+
);
|
|
950
|
+
const button = screen.getByRole('button', {name: 'Main'});
|
|
951
|
+
expect(button).toHaveAttribute('type', 'button');
|
|
952
|
+
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
953
|
+
expect(button).not.toHaveAttribute('aria-label');
|
|
954
|
+
const region = getItemsRegion(button);
|
|
955
|
+
expect(region).toContainElement(screen.getByText('Dashboard'));
|
|
956
|
+
expect(region).not.toHaveAttribute('inert');
|
|
957
|
+
expect(region).not.toHaveAttribute('aria-hidden');
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
it('keeps role="group" labelled by the title', () => {
|
|
961
|
+
render(
|
|
962
|
+
<SideNavSection title="Main" collapsible>
|
|
963
|
+
{items}
|
|
964
|
+
</SideNavSection>,
|
|
965
|
+
);
|
|
966
|
+
const group = screen.getByRole('group');
|
|
967
|
+
const label = document.getElementById(
|
|
968
|
+
group.getAttribute('aria-labelledby')!,
|
|
969
|
+
);
|
|
970
|
+
expect(label).toHaveTextContent('Main');
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
it('toggles aria-expanded, inert and aria-hidden on click', async () => {
|
|
974
|
+
const user = userEvent.setup();
|
|
975
|
+
render(
|
|
976
|
+
<SideNavSection title="Main" collapsible data-testid="section">
|
|
977
|
+
{items}
|
|
978
|
+
</SideNavSection>,
|
|
979
|
+
);
|
|
980
|
+
const button = screen.getByRole('button', {name: 'Main'});
|
|
981
|
+
const region = getItemsRegion(button);
|
|
982
|
+
|
|
983
|
+
await user.click(button);
|
|
984
|
+
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
985
|
+
expect(region).toHaveAttribute('inert');
|
|
986
|
+
expect(region).toHaveAttribute('aria-hidden', 'true');
|
|
987
|
+
expect(screen.getByTestId('section')).toHaveAttribute(
|
|
988
|
+
'data-collapsed',
|
|
989
|
+
'collapsed',
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
await user.click(button);
|
|
993
|
+
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
994
|
+
expect(region).not.toHaveAttribute('inert');
|
|
995
|
+
expect(region).not.toHaveAttribute('aria-hidden');
|
|
996
|
+
expect(screen.getByTestId('section')).not.toHaveAttribute('data-collapsed');
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
it('starts collapsed with defaultIsCollapsed', () => {
|
|
1000
|
+
render(
|
|
1001
|
+
<SideNavSection title="Main" collapsible={{defaultIsCollapsed: true}}>
|
|
1002
|
+
{items}
|
|
1003
|
+
</SideNavSection>,
|
|
1004
|
+
);
|
|
1005
|
+
const button = screen.getByRole('button', {name: 'Main'});
|
|
1006
|
+
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
1007
|
+
expect(getItemsRegion(button)).toHaveAttribute('inert');
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it('controlled mode calls onCollapsedChange and respects the prop', async () => {
|
|
1011
|
+
const user = userEvent.setup();
|
|
1012
|
+
const onCollapsedChange = vi.fn();
|
|
1013
|
+
const {rerender} = render(
|
|
1014
|
+
<SideNavSection
|
|
1015
|
+
title="Main"
|
|
1016
|
+
collapsible={{isCollapsed: false, onCollapsedChange}}>
|
|
1017
|
+
{items}
|
|
1018
|
+
</SideNavSection>,
|
|
1019
|
+
);
|
|
1020
|
+
const button = screen.getByRole('button', {name: 'Main'});
|
|
1021
|
+
|
|
1022
|
+
await user.click(button);
|
|
1023
|
+
expect(onCollapsedChange).toHaveBeenCalledWith(true);
|
|
1024
|
+
// Prop did not change, so the section stays expanded
|
|
1025
|
+
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
1026
|
+
|
|
1027
|
+
rerender(
|
|
1028
|
+
<SideNavSection
|
|
1029
|
+
title="Main"
|
|
1030
|
+
collapsible={{isCollapsed: true, onCollapsedChange}}>
|
|
1031
|
+
{items}
|
|
1032
|
+
</SideNavSection>,
|
|
1033
|
+
);
|
|
1034
|
+
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
1035
|
+
expect(getItemsRegion(button)).toHaveAttribute('inert');
|
|
1036
|
+
|
|
1037
|
+
await user.click(button);
|
|
1038
|
+
expect(onCollapsedChange).toHaveBeenLastCalledWith(false);
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
it('toggles with Enter and Space (native button)', async () => {
|
|
1042
|
+
const user = userEvent.setup();
|
|
1043
|
+
render(
|
|
1044
|
+
<SideNavSection title="Main" collapsible>
|
|
1045
|
+
{items}
|
|
1046
|
+
</SideNavSection>,
|
|
1047
|
+
);
|
|
1048
|
+
const button = screen.getByRole('button', {name: 'Main'});
|
|
1049
|
+
button.focus();
|
|
1050
|
+
|
|
1051
|
+
await user.keyboard('{Enter}');
|
|
1052
|
+
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
1053
|
+
await user.keyboard(' ');
|
|
1054
|
+
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
it('keeps endContent outside the toggle button', () => {
|
|
1058
|
+
render(
|
|
1059
|
+
<SideNavSection
|
|
1060
|
+
title="Main"
|
|
1061
|
+
collapsible
|
|
1062
|
+
endContent={
|
|
1063
|
+
<button type="button" data-testid="section-action">
|
|
1064
|
+
+
|
|
1065
|
+
</button>
|
|
1066
|
+
}>
|
|
1067
|
+
{items}
|
|
1068
|
+
</SideNavSection>,
|
|
1069
|
+
);
|
|
1070
|
+
const toggle = screen.getByRole('button', {name: 'Main'});
|
|
1071
|
+
const action = screen.getByTestId('section-action');
|
|
1072
|
+
expect(toggle).not.toContainElement(action);
|
|
1073
|
+
expect(action.closest('button')).toBe(action);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
it('is ignored when the SideNav is rail-collapsed', () => {
|
|
1077
|
+
render(
|
|
1078
|
+
<SideNavCollapseContext
|
|
1079
|
+
value={{isCollapsed: true, toggle: () => {}, isCollapsible: true}}>
|
|
1080
|
+
<SideNavSection title="Main" collapsible={{defaultIsCollapsed: true}}>
|
|
1081
|
+
<SideNavItem label="Dashboard" icon={StubIcon} />
|
|
1082
|
+
</SideNavSection>
|
|
1083
|
+
</SideNavCollapseContext>,
|
|
1084
|
+
);
|
|
1085
|
+
expect(document.querySelector('[aria-expanded]')).toBeNull();
|
|
1086
|
+
expect(document.querySelector('[inert]')).toBeNull();
|
|
1087
|
+
expect(screen.getByLabelText('Dashboard')).toBeInTheDocument();
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
it('is ignored when the header is hidden', () => {
|
|
1091
|
+
render(
|
|
1092
|
+
<SideNavSection
|
|
1093
|
+
title="Main"
|
|
1094
|
+
isHeaderHidden
|
|
1095
|
+
collapsible={{defaultIsCollapsed: true}}>
|
|
1096
|
+
{items}
|
|
1097
|
+
</SideNavSection>,
|
|
1098
|
+
);
|
|
1099
|
+
expect(document.querySelector('[aria-expanded]')).toBeNull();
|
|
1100
|
+
expect(document.querySelector('[inert]')).toBeNull();
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1103
|
+
|
|
921
1104
|
// =============================================================================
|
|
922
1105
|
// Resizable
|
|
923
1106
|
// =============================================================================
|
|
@@ -59,6 +59,12 @@ export const docs = {
|
|
|
59
59
|
description: 'Visually hides the section header while keeping it accessible to screen readers.',
|
|
60
60
|
default: 'false',
|
|
61
61
|
},
|
|
62
|
+
{
|
|
63
|
+
name: 'collapsible',
|
|
64
|
+
type: 'boolean | { defaultIsCollapsed?: boolean; isCollapsed?: boolean; onCollapsedChange?: (isCollapsed: boolean) => void }',
|
|
65
|
+
description: 'Lets users collapse the section items by clicking the title. The title area becomes a toggle button (aria-expanded, chevron) and endContent stays outside it. true for uncontrolled (starts expanded), or an object with defaultIsCollapsed, or isCollapsed + onCollapsedChange for controlled mode. Ignored when isHeaderHidden is set or the SideNav is collapsed to its icon rail.',
|
|
66
|
+
default: 'false',
|
|
67
|
+
},
|
|
62
68
|
{
|
|
63
69
|
name: 'xstyle',
|
|
64
70
|
type: 'StyleXStyles',
|
|
@@ -100,6 +106,12 @@ export const docsZh = {
|
|
|
100
106
|
description: '视觉上隐藏分组头部,同时保持屏幕阅读器可访问。',
|
|
101
107
|
default: 'false',
|
|
102
108
|
},
|
|
109
|
+
{
|
|
110
|
+
name: 'collapsible',
|
|
111
|
+
type: 'boolean | { defaultIsCollapsed?: boolean; isCollapsed?: boolean; onCollapsedChange?: (isCollapsed: boolean) => void }',
|
|
112
|
+
description: '允许点击标题折叠分组项目。标题区域变为切换按钮(aria-expanded,带箭头),endContent 保持在按钮之外。true 为非受控模式(默认展开),或传入对象:defaultIsCollapsed,或 isCollapsed + onCollapsedChange 受控模式。设置 isHeaderHidden 或 SideNav 折叠为图标栏时忽略。',
|
|
113
|
+
default: 'false',
|
|
114
|
+
},
|
|
103
115
|
],
|
|
104
116
|
};
|
|
105
117
|
|
|
@@ -114,6 +126,7 @@ export const docsDense = {
|
|
|
114
126
|
children: 'Section items.',
|
|
115
127
|
endContent: 'Right-side content in section header.',
|
|
116
128
|
isHeaderHidden: 'Visually hides section header while keeping accessible to screen readers.',
|
|
129
|
+
collapsible: 'true | {defaultIsCollapsed?, isCollapsed?, onCollapsedChange?}. Title becomes toggle button collapsing items; endContent stays outside. Ignored if header hidden or nav rail-collapsed.',
|
|
117
130
|
xstyle: 'StyleX styles for layout customization.',
|
|
118
131
|
},
|
|
119
132
|
};
|
|
@@ -18,17 +18,27 @@
|
|
|
18
18
|
* - /packages/cli/templates/blocks/components/SideNav/ (showcase blocks)
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import React, {
|
|
21
|
+
import React, {
|
|
22
|
+
useCallback,
|
|
23
|
+
useId,
|
|
24
|
+
useMemo,
|
|
25
|
+
useState,
|
|
26
|
+
type ReactNode,
|
|
27
|
+
} from 'react';
|
|
22
28
|
import * as stylex from '@stylexjs/stylex';
|
|
23
29
|
import {
|
|
24
30
|
colorVars,
|
|
25
31
|
spacingVars,
|
|
26
32
|
fontWeightVars,
|
|
27
33
|
typeScaleVars,
|
|
34
|
+
durationVars,
|
|
35
|
+
easeVars,
|
|
36
|
+
radiusVars,
|
|
28
37
|
} from '../theme/tokens.stylex';
|
|
29
38
|
import {mergeProps} from '../utils';
|
|
30
39
|
import type {BaseProps} from '../BaseProps';
|
|
31
40
|
import {useSideNavCollapse} from './SideNavCollapseContext';
|
|
41
|
+
import {getIcon} from '../Icon/globalIconRegistry';
|
|
32
42
|
import {themeProps} from '../utils/themeProps';
|
|
33
43
|
// =============================================================================
|
|
34
44
|
// Styles
|
|
@@ -84,6 +94,92 @@ const styles = stylex.create({
|
|
|
84
94
|
flexDirection: 'column',
|
|
85
95
|
gap: spacingVars['--spacing-0-5'],
|
|
86
96
|
},
|
|
97
|
+
|
|
98
|
+
// ── Collapsible section ──
|
|
99
|
+
// The header row loses its own padding so the toggle button can own it
|
|
100
|
+
// (giving the hover overlay the full title area) while the text stays
|
|
101
|
+
// aligned exactly where the static header puts it.
|
|
102
|
+
headerCollapsible: {
|
|
103
|
+
paddingInline: 0,
|
|
104
|
+
paddingBlock: 0,
|
|
105
|
+
},
|
|
106
|
+
headerToggle: {
|
|
107
|
+
display: 'flex',
|
|
108
|
+
alignItems: 'center',
|
|
109
|
+
gap: spacingVars['--spacing-2'],
|
|
110
|
+
flex: 1,
|
|
111
|
+
minWidth: 0,
|
|
112
|
+
width: '100%',
|
|
113
|
+
paddingInline: spacingVars['--spacing-2'],
|
|
114
|
+
paddingBlock: spacingVars['--spacing-1'],
|
|
115
|
+
margin: 0,
|
|
116
|
+
borderWidth: 0,
|
|
117
|
+
borderStyle: 'none',
|
|
118
|
+
borderRadius: radiusVars['--radius-element'],
|
|
119
|
+
backgroundColor: 'transparent',
|
|
120
|
+
color: 'inherit',
|
|
121
|
+
fontFamily: 'inherit',
|
|
122
|
+
fontSize: 'inherit',
|
|
123
|
+
fontWeight: 'inherit',
|
|
124
|
+
lineHeight: 'inherit',
|
|
125
|
+
textAlign: 'start',
|
|
126
|
+
cursor: 'pointer',
|
|
127
|
+
userSelect: 'none',
|
|
128
|
+
':hover': {
|
|
129
|
+
'@media (hover: hover)': {
|
|
130
|
+
backgroundColor: colorVars['--color-overlay-hover'],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
':active': {
|
|
134
|
+
backgroundColor: colorVars['--color-overlay-pressed'],
|
|
135
|
+
},
|
|
136
|
+
outline: {
|
|
137
|
+
default: 'none',
|
|
138
|
+
':focus-visible': `2px solid ${colorVars['--color-accent']}`,
|
|
139
|
+
},
|
|
140
|
+
outlineOffset: {
|
|
141
|
+
default: '0',
|
|
142
|
+
':focus-visible': '-2px',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
endContentCollapsible: {
|
|
146
|
+
paddingInlineEnd: spacingVars['--spacing-2'],
|
|
147
|
+
},
|
|
148
|
+
expandChevron: {
|
|
149
|
+
display: 'inline-flex',
|
|
150
|
+
alignItems: 'center',
|
|
151
|
+
justifyContent: 'center',
|
|
152
|
+
width: spacingVars['--spacing-6'],
|
|
153
|
+
height: spacingVars['--spacing-6'],
|
|
154
|
+
color: colorVars['--color-text-secondary'],
|
|
155
|
+
transitionProperty: 'transform',
|
|
156
|
+
transitionDuration: {
|
|
157
|
+
default: durationVars['--duration-fast'],
|
|
158
|
+
'@media (prefers-reduced-motion: reduce)': '0s',
|
|
159
|
+
},
|
|
160
|
+
transitionTimingFunction: easeVars['--ease-standard'],
|
|
161
|
+
flexShrink: 0,
|
|
162
|
+
},
|
|
163
|
+
expandChevronExpanded: {
|
|
164
|
+
transform: 'rotate(180deg)',
|
|
165
|
+
},
|
|
166
|
+
itemsCollapsible: {
|
|
167
|
+
display: 'grid',
|
|
168
|
+
gridTemplateRows: '1fr',
|
|
169
|
+
transitionProperty: 'grid-template-rows',
|
|
170
|
+
transitionDuration: {
|
|
171
|
+
default: durationVars['--duration-medium'],
|
|
172
|
+
'@media (prefers-reduced-motion: reduce)': '0s',
|
|
173
|
+
},
|
|
174
|
+
transitionTimingFunction: easeVars['--ease-standard'],
|
|
175
|
+
},
|
|
176
|
+
itemsCollapsed: {
|
|
177
|
+
gridTemplateRows: '0fr',
|
|
178
|
+
},
|
|
179
|
+
itemsInner: {
|
|
180
|
+
overflow: 'hidden',
|
|
181
|
+
minHeight: 0,
|
|
182
|
+
},
|
|
87
183
|
});
|
|
88
184
|
|
|
89
185
|
// =============================================================================
|
|
@@ -114,6 +210,28 @@ export interface SideNavSectionProps extends BaseProps<HTMLDivElement> {
|
|
|
114
210
|
* @default false
|
|
115
211
|
*/
|
|
116
212
|
isHeaderHidden?: boolean;
|
|
213
|
+
/**
|
|
214
|
+
* Lets the user collapse the section's items by clicking its title.
|
|
215
|
+
* The title area becomes a toggle button (`aria-expanded`) with a chevron;
|
|
216
|
+
* `endContent` stays outside the button.
|
|
217
|
+
*
|
|
218
|
+
* - `true` — collapsible with defaults (starts expanded)
|
|
219
|
+
* - Object — controlled/configured:
|
|
220
|
+
* - `defaultIsCollapsed` — start collapsed (default: false)
|
|
221
|
+
* - `isCollapsed` + `onCollapsedChange` — controlled mode
|
|
222
|
+
*
|
|
223
|
+
* Ignored when `isHeaderHidden` is set or the SideNav itself is collapsed
|
|
224
|
+
* to its icon rail (items stay visible so their icons remain reachable).
|
|
225
|
+
*
|
|
226
|
+
* @default false
|
|
227
|
+
*/
|
|
228
|
+
collapsible?:
|
|
229
|
+
| boolean
|
|
230
|
+
| {
|
|
231
|
+
defaultIsCollapsed?: boolean;
|
|
232
|
+
isCollapsed?: boolean;
|
|
233
|
+
onCollapsedChange?: (isCollapsed: boolean) => void;
|
|
234
|
+
};
|
|
117
235
|
}
|
|
118
236
|
|
|
119
237
|
// =============================================================================
|
|
@@ -141,6 +259,7 @@ export function SideNavSection({
|
|
|
141
259
|
children,
|
|
142
260
|
endContent,
|
|
143
261
|
isHeaderHidden = false,
|
|
262
|
+
collapsible,
|
|
144
263
|
xstyle,
|
|
145
264
|
className,
|
|
146
265
|
style,
|
|
@@ -150,23 +269,83 @@ export function SideNavSection({
|
|
|
150
269
|
const {isCollapsed} = useSideNavCollapse();
|
|
151
270
|
const id = useId();
|
|
152
271
|
const titleId = `${id}-title`;
|
|
272
|
+
const itemsId = `${id}-items`;
|
|
273
|
+
|
|
274
|
+
// Section collapse state (mirrors SideNavItem's `collapsible`)
|
|
275
|
+
const collapsibleConfig = useMemo(
|
|
276
|
+
() => (typeof collapsible === 'object' ? collapsible : {}),
|
|
277
|
+
[collapsible],
|
|
278
|
+
);
|
|
279
|
+
const controlledCollapsed = collapsibleConfig.isCollapsed;
|
|
280
|
+
const isControlled = controlledCollapsed !== undefined;
|
|
281
|
+
const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState(
|
|
282
|
+
collapsibleConfig.defaultIsCollapsed ?? false,
|
|
283
|
+
);
|
|
284
|
+
const isSectionCollapsed = isControlled
|
|
285
|
+
? controlledCollapsed
|
|
286
|
+
: uncontrolledCollapsed;
|
|
153
287
|
|
|
154
|
-
const
|
|
288
|
+
const toggleSectionCollapse = useCallback(() => {
|
|
289
|
+
const next = !isSectionCollapsed;
|
|
290
|
+
if (!isControlled) {
|
|
291
|
+
setUncontrolledCollapsed(next);
|
|
292
|
+
}
|
|
293
|
+
collapsibleConfig.onCollapsedChange?.(next);
|
|
294
|
+
}, [isSectionCollapsed, isControlled, collapsibleConfig]);
|
|
295
|
+
|
|
296
|
+
const shouldHideHeader = isHeaderHidden || isCollapsed;
|
|
297
|
+
// Collapse only applies while the header is visible: a hidden header has
|
|
298
|
+
// no toggle, and the icon rail must keep every item reachable.
|
|
299
|
+
const isSectionCollapsible =
|
|
300
|
+
collapsible != null && collapsible !== false && !shouldHideHeader;
|
|
301
|
+
const isItemsCollapsed = isSectionCollapsible && isSectionCollapsed;
|
|
302
|
+
|
|
303
|
+
const titleContent = (
|
|
304
|
+
<span {...stylex.props(styles.titleContainer)}>
|
|
305
|
+
<span id={titleId} {...stylex.props(styles.title)}>
|
|
306
|
+
{title}
|
|
307
|
+
</span>
|
|
308
|
+
{subtitle && <span {...stylex.props(styles.subtitle)}>{subtitle}</span>}
|
|
309
|
+
</span>
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const endContentElement = endContent && (
|
|
313
|
+
<span
|
|
314
|
+
{...stylex.props(
|
|
315
|
+
styles.endContent,
|
|
316
|
+
isSectionCollapsible && styles.endContentCollapsible,
|
|
317
|
+
)}>
|
|
318
|
+
{endContent}
|
|
319
|
+
</span>
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const headerContent = isSectionCollapsible ? (
|
|
155
323
|
<>
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
324
|
+
<button
|
|
325
|
+
type="button"
|
|
326
|
+
onClick={toggleSectionCollapse}
|
|
327
|
+
aria-expanded={!isSectionCollapsed}
|
|
328
|
+
aria-controls={itemsId}
|
|
329
|
+
{...stylex.props(styles.headerToggle)}>
|
|
330
|
+
{titleContent}
|
|
331
|
+
<span
|
|
332
|
+
aria-hidden
|
|
333
|
+
{...stylex.props(
|
|
334
|
+
styles.expandChevron,
|
|
335
|
+
!isSectionCollapsed && styles.expandChevronExpanded,
|
|
336
|
+
)}>
|
|
337
|
+
{getIcon('chevronDown')}
|
|
159
338
|
</span>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
339
|
+
</button>
|
|
340
|
+
{endContentElement}
|
|
341
|
+
</>
|
|
342
|
+
) : (
|
|
343
|
+
<>
|
|
344
|
+
{titleContent}
|
|
345
|
+
{endContentElement}
|
|
165
346
|
</>
|
|
166
347
|
);
|
|
167
348
|
|
|
168
|
-
const shouldHideHeader = isHeaderHidden || isCollapsed;
|
|
169
|
-
|
|
170
349
|
const visuallyHiddenStyle: React.CSSProperties = shouldHideHeader
|
|
171
350
|
? {
|
|
172
351
|
position: 'absolute',
|
|
@@ -185,7 +364,12 @@ export function SideNavSection({
|
|
|
185
364
|
<div
|
|
186
365
|
ref={ref}
|
|
187
366
|
{...mergeProps(
|
|
188
|
-
themeProps(
|
|
367
|
+
themeProps(
|
|
368
|
+
'side-nav-section',
|
|
369
|
+
isSectionCollapsible
|
|
370
|
+
? {collapsed: isSectionCollapsed ? 'collapsed' : null}
|
|
371
|
+
: undefined,
|
|
372
|
+
),
|
|
189
373
|
stylex.props(styles.root, xstyle),
|
|
190
374
|
className,
|
|
191
375
|
style,
|
|
@@ -195,12 +379,33 @@ export function SideNavSection({
|
|
|
195
379
|
aria-labelledby={titleId}
|
|
196
380
|
data-testid={testId}>
|
|
197
381
|
<div
|
|
198
|
-
{...mergeProps(
|
|
199
|
-
|
|
200
|
-
|
|
382
|
+
{...mergeProps(
|
|
383
|
+
stylex.props(
|
|
384
|
+
styles.header,
|
|
385
|
+
isSectionCollapsible && styles.headerCollapsible,
|
|
386
|
+
),
|
|
387
|
+
{
|
|
388
|
+
style: shouldHideHeader ? visuallyHiddenStyle : undefined,
|
|
389
|
+
},
|
|
390
|
+
)}>
|
|
201
391
|
{headerContent}
|
|
202
392
|
</div>
|
|
203
|
-
|
|
393
|
+
{isSectionCollapsible ? (
|
|
394
|
+
<div
|
|
395
|
+
id={itemsId}
|
|
396
|
+
aria-hidden={isItemsCollapsed || undefined}
|
|
397
|
+
inert={isItemsCollapsed ? true : undefined}
|
|
398
|
+
{...stylex.props(
|
|
399
|
+
styles.itemsCollapsible,
|
|
400
|
+
isItemsCollapsed && styles.itemsCollapsed,
|
|
401
|
+
)}>
|
|
402
|
+
<div {...stylex.props(styles.itemsInner)}>
|
|
403
|
+
<div {...stylex.props(styles.items)}>{children}</div>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
) : (
|
|
407
|
+
<div {...stylex.props(styles.items)}>{children}</div>
|
|
408
|
+
)}
|
|
204
409
|
</div>
|
|
205
410
|
);
|
|
206
411
|
}
|