@hyphen/hyphen-components 7.2.0 → 7.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyphen/hyphen-components",
3
- "version": "7.2.0",
3
+ "version": "7.3.1",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "@hyphen"
@@ -124,7 +124,7 @@
124
124
  "rollup-plugin-scss": "^4.0.0",
125
125
  "sass": "^1.77.8",
126
126
  "sass-loader": "^13.3.3",
127
- "semantic-release": "^23.1.1",
127
+ "semantic-release": "^25.0.2",
128
128
  "size-limit": "^11.2.0",
129
129
  "storybook": "^9.1.3",
130
130
  "ts-jest": "^29.4.6",
@@ -184,14 +184,116 @@ export const SidebarExample = () => {
184
184
  </Box>
185
185
  )}
186
186
  <Card height="100" padding="2xl">
187
- content
187
+ <SidebarTrigger />
188
+ </Card>
189
+ </SidebarInset>
190
+ </SidebarProvider>
191
+ </ResponsiveProvider>
192
+ );
193
+ };
194
+
195
+ export const SidebarRightExample = () => {
196
+ const [activeTeam, setActiveTeam] = React.useState(data.teams[0]);
197
+ const isMobile = useIsMobile();
198
+
199
+ const STORAGE_KEY = 'sidebar_right_expanded_storybook';
200
+
201
+ const startExpanded = localStorage.getItem(STORAGE_KEY) !== 'false';
202
+
203
+ return (
204
+ <ResponsiveProvider>
205
+ <SidebarProvider
206
+ storageKey={{ right: STORAGE_KEY }}
207
+ defaultOpen={startExpanded}
208
+ >
209
+ <SidebarInset>
210
+ {isMobile && (
211
+ <Box direction="row" gap="sm" alignItems="center">
212
+ <SidebarTrigger side="right" iconName="cpu" />
213
+ <CreateMenu />
214
+ </Box>
215
+ )}
216
+ <Card height="100" padding="2xl">
217
+ <Box alignItems="flex-end" width="100">
218
+ <SidebarTrigger side="right" iconName="cpu" />
219
+ </Box>
220
+ </Card>
221
+ </SidebarInset>
222
+ <Sidebar side="right" collapsible="offcanvas">
223
+ <NavHeader activeTeam={activeTeam} setActiveTeam={setActiveTeam} />
224
+ <SidebarContent>
225
+ <Box width="100" background="inverse" color="inverse">
226
+ content
227
+ </Box>
228
+ </SidebarContent>
229
+ <NavFooter />
230
+ <SidebarRail />
231
+ </Sidebar>
232
+ </SidebarProvider>
233
+ </ResponsiveProvider>
234
+ );
235
+ };
236
+
237
+ export const SidebarBothSides = () => {
238
+ const [activeTeam, setActiveTeam] = React.useState(data.teams[0]);
239
+ const isMobile = useIsMobile();
240
+
241
+ const STORAGE_KEY = 'sidebar_dual_expanded_storybook';
242
+
243
+ return (
244
+ <ResponsiveProvider>
245
+ <SidebarProvider storageKey={STORAGE_KEY} defaultOpen>
246
+ <Sidebar side="left" collapsible="icon">
247
+ <NavHeader activeTeam={activeTeam} setActiveTeam={setActiveTeam} />
248
+ <SidebarContent>
249
+ <NavMain items={data.items} />
250
+ <NavFavorites favorites={data.favorites} />
251
+ </SidebarContent>
252
+ <NavFooter />
253
+ <SidebarRail />
254
+ </Sidebar>
255
+ <SidebarInset>
256
+ {isMobile && (
257
+ <Box width="100" direction="row" gap="sm" alignItems="center">
258
+ <Box flex="auto" direction="row" gap="sm" alignItems="center">
259
+ <SidebarTrigger side="left" />
260
+ <CreateMenu />
261
+ </Box>
262
+ <SidebarTrigger side="right" iconName="cpu" />
263
+ </Box>
264
+ )}
265
+ <Card height="100" padding="2xl">
266
+ <SidebarTrigger />
267
+ <SidebarTrigger side="right" iconName="cpu" />
268
+ <ContextContents />
188
269
  </Card>
189
270
  </SidebarInset>
271
+ <Sidebar side="right" collapsible="offcanvas">
272
+ <NavHeader activeTeam={activeTeam} setActiveTeam={setActiveTeam} />
273
+ <SidebarContent>
274
+ <NavMain items={data.items} />
275
+ <NavFavorites favorites={data.favorites} />
276
+ </SidebarContent>
277
+ <NavFooter />
278
+ <SidebarRail />
279
+ </Sidebar>
190
280
  </SidebarProvider>
191
281
  </ResponsiveProvider>
192
282
  );
193
283
  };
194
284
 
285
+ const ContextContents = () => {
286
+ const leftState = useSidebar('left');
287
+ const rightState = useSidebar('right');
288
+
289
+ return (
290
+ <Box direction="column" gap="sm">
291
+ <span>Left: {leftState.state}</span>
292
+ <span>Right: {rightState.state}</span>
293
+ </Box>
294
+ );
295
+ };
296
+
195
297
  export const SidebarCollapsed = () => {
196
298
  const STORAGE_KEY = 'sidebar_collapsed';
197
299
 
@@ -521,6 +623,7 @@ function CreateMenu() {
521
623
  background="primary"
522
624
  cursor="pointer"
523
625
  shadow="xs"
626
+ aria-label="Create new item..."
524
627
  >
525
628
  <Icon name="add" />
526
629
  </Box>
@@ -1,6 +1,11 @@
1
1
  import { fireEvent, render, screen } from '@testing-library/react';
2
2
  import React from 'react';
3
- import { Sidebar, SidebarProvider, SidebarTrigger } from './Sidebar';
3
+ import {
4
+ Sidebar,
5
+ SidebarProvider,
6
+ SidebarTrigger,
7
+ useSidebar,
8
+ } from './Sidebar';
4
9
 
5
10
  jest.mock('../../hooks/useIsMobile/useIsMobile', () => ({
6
11
  useIsMobile: () => false,
@@ -13,14 +18,184 @@ describe('Sidebar', () => {
13
18
  <Sidebar data-testid="sidebar">
14
19
  <div>Content</div>
15
20
  </Sidebar>
16
- <SidebarTrigger />
21
+ <SidebarTrigger side="left" />
17
22
  </SidebarProvider>
18
23
  );
19
24
 
20
25
  const sidebar = document.querySelector('[data-state]') as HTMLElement;
21
26
  expect(sidebar).toHaveAttribute('data-state', 'expanded');
22
27
 
23
- fireEvent.click(screen.getByLabelText('toggle sidebar'));
28
+ fireEvent.click(screen.getByLabelText('Toggle left sidebar'));
24
29
  expect(sidebar).toHaveAttribute('data-state', 'collapsed');
25
30
  });
31
+
32
+ test('supports right side placement', () => {
33
+ render(
34
+ <SidebarProvider>
35
+ <Sidebar side="right">
36
+ <div>Content</div>
37
+ </Sidebar>
38
+ </SidebarProvider>
39
+ );
40
+
41
+ const sidebar = document.querySelector(
42
+ '[data-side="right"]'
43
+ ) as HTMLElement;
44
+ expect(sidebar).toBeInTheDocument();
45
+ });
46
+
47
+ test('tracks left and right sidebars independently', () => {
48
+ render(
49
+ <SidebarProvider>
50
+ <Sidebar side="left">
51
+ <div>Left</div>
52
+ </Sidebar>
53
+ <Sidebar side="right">
54
+ <div>Right</div>
55
+ </Sidebar>
56
+ <SidebarTrigger side="left" data-testid="left-trigger" />
57
+ <SidebarTrigger side="right" data-testid="right-trigger" />
58
+ </SidebarProvider>
59
+ );
60
+
61
+ const leftSidebar = document.querySelector(
62
+ '[data-side="left"]'
63
+ ) as HTMLElement;
64
+ const rightSidebar = document.querySelector(
65
+ '[data-side="right"]'
66
+ ) as HTMLElement;
67
+
68
+ expect(leftSidebar).toHaveAttribute('data-state', 'expanded');
69
+ expect(rightSidebar).toHaveAttribute('data-state', 'expanded');
70
+
71
+ fireEvent.click(screen.getByTestId('left-trigger'));
72
+ expect(leftSidebar).toHaveAttribute('data-state', 'collapsed');
73
+ expect(rightSidebar).toHaveAttribute('data-state', 'expanded');
74
+
75
+ fireEvent.click(screen.getByTestId('right-trigger'));
76
+ expect(rightSidebar).toHaveAttribute('data-state', 'collapsed');
77
+ });
78
+
79
+ test('toggles left sidebar with keyboard shortcut', () => {
80
+ render(
81
+ <SidebarProvider>
82
+ <Sidebar side="left">
83
+ <div>Left</div>
84
+ </Sidebar>
85
+ <Sidebar side="right">
86
+ <div>Right</div>
87
+ </Sidebar>
88
+ <SidebarTrigger side="left" data-testid="left-trigger" />
89
+ <SidebarTrigger side="right" data-testid="right-trigger" />
90
+ </SidebarProvider>
91
+ );
92
+
93
+ const leftSidebar = document.querySelector(
94
+ '[data-side="left"]'
95
+ ) as HTMLElement;
96
+
97
+ expect(leftSidebar).toHaveAttribute('data-state', 'expanded');
98
+
99
+ fireEvent.keyDown(window, { key: '[' });
100
+ expect(leftSidebar).toHaveAttribute('data-state', 'collapsed');
101
+ });
102
+
103
+ test('toggles right sidebar with keyboard shortcut', () => {
104
+ render(
105
+ <SidebarProvider>
106
+ <Sidebar side="left">
107
+ <div>Left</div>
108
+ </Sidebar>
109
+ <Sidebar side="right">
110
+ <div>Right</div>
111
+ </Sidebar>
112
+ <SidebarTrigger side="left" data-testid="left-trigger" />
113
+ <SidebarTrigger side="right" data-testid="right-trigger" />
114
+ </SidebarProvider>
115
+ );
116
+
117
+ const rightSidebar = document.querySelector(
118
+ '[data-side="right"]'
119
+ ) as HTMLElement;
120
+
121
+ expect(rightSidebar).toHaveAttribute('data-state', 'expanded');
122
+
123
+ fireEvent.keyDown(window, { key: ']' });
124
+ expect(rightSidebar).toHaveAttribute('data-state', 'collapsed');
125
+ });
126
+
127
+ test('calls onOpenChange callback when sidebar state changes', () => {
128
+ const onOpenChange = jest.fn();
129
+ render(
130
+ <SidebarProvider onOpenChange={onOpenChange}>
131
+ <Sidebar side="left" />
132
+ <SidebarTrigger side="left" data-testid="left-trigger" />
133
+ </SidebarProvider>
134
+ );
135
+ fireEvent.click(screen.getByTestId('left-trigger'));
136
+ expect(onOpenChange).toHaveBeenCalledWith(false, 'left');
137
+ fireEvent.click(screen.getByTestId('left-trigger'));
138
+ expect(onOpenChange).toHaveBeenCalledWith(true, 'left');
139
+ });
140
+
141
+ test.each([
142
+ ['input', <input aria-label="input-field" />],
143
+ ['textarea', <textarea aria-label="textarea-field" />],
144
+ ['select', <select aria-label="select-field" />],
145
+ ['contenteditable', <div aria-label="editable-field" contentEditable />],
146
+ ])('ignores keyboard shortcuts for %s elements', (label, field) => {
147
+ render(
148
+ <SidebarProvider>
149
+ <Sidebar side="left">
150
+ <div>Left</div>
151
+ </Sidebar>
152
+ {field}
153
+ </SidebarProvider>
154
+ );
155
+
156
+ const leftSidebar = document.querySelector(
157
+ '[data-side="left"]'
158
+ ) as HTMLElement;
159
+
160
+ expect(leftSidebar).toHaveAttribute('data-state', 'expanded');
161
+
162
+ const target = screen.getByLabelText(/field/) as HTMLElement;
163
+ if (label === 'contenteditable') {
164
+ Object.defineProperty(target, 'isContentEditable', {
165
+ configurable: true,
166
+ value: true,
167
+ });
168
+ }
169
+ fireEvent.keyDown(target, { key: '[' });
170
+ expect(leftSidebar).toHaveAttribute('data-state', 'expanded');
171
+ });
172
+
173
+ test('avoids re-rendering right consumers when left toggles', () => {
174
+ const onRender = jest.fn();
175
+ const RightConsumer = React.memo(
176
+ ({ onRender: onRenderProp }: { onRender: jest.Mock }) => {
177
+ useSidebar('right');
178
+ onRenderProp();
179
+ return null;
180
+ }
181
+ );
182
+
183
+ render(
184
+ <SidebarProvider>
185
+ <Sidebar side="left" />
186
+ <Sidebar side="right" />
187
+ <RightConsumer onRender={onRender} />
188
+ <SidebarTrigger side="left" data-testid="left-trigger" />
189
+ <SidebarTrigger side="right" data-testid="right-trigger" />
190
+ </SidebarProvider>
191
+ );
192
+
193
+ expect(onRender).toHaveBeenCalledTimes(1);
194
+
195
+ fireEvent.click(screen.getByTestId('left-trigger'));
196
+ expect(onRender).toHaveBeenCalledTimes(1);
197
+
198
+ fireEvent.click(screen.getByTestId('right-trigger'));
199
+ expect(onRender).toHaveBeenCalledTimes(2);
200
+ });
26
201
  });