@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.
@@ -23,35 +23,225 @@ import {
23
23
 
24
24
  const SIDEBAR_WIDTH = '16rem';
25
25
  const SIDEBAR_WIDTH_ICON = '44px';
26
- const SIDEBAR_KEYBOARD_SHORTCUT = '[';
26
+ const SIDEBAR_KEYBOARD_SHORTCUT_LEFT = '[';
27
+ const SIDEBAR_KEYBOARD_SHORTCUT_RIGHT = ']';
27
28
 
28
- interface SidebarContextProps {
29
+ type SidebarSide = 'left' | 'right';
30
+
31
+ type SidebarOpenState = Record<SidebarSide, boolean>;
32
+
33
+ type SidebarOpenValue = boolean | Partial<SidebarOpenState>;
34
+
35
+ type SidebarStorageKey = string | Partial<Record<SidebarSide, string>>;
36
+
37
+ interface SidebarContextSideState {
29
38
  state: 'expanded' | 'collapsed';
30
39
  open: boolean;
31
- setOpen: (open: boolean) => void;
40
+ setOpen: (open: boolean | ((open: boolean) => boolean)) => void;
32
41
  openMobile: boolean;
33
- setOpenMobile: (open: boolean) => void;
34
- isMobile: boolean;
42
+ setOpenMobile: (open: boolean | ((open: boolean) => boolean)) => void;
35
43
  toggleSidebar: () => void;
36
44
  }
37
45
 
38
- const SidebarContext = React.createContext<SidebarContextProps | null>(null);
46
+ const SidebarIsMobileContext = React.createContext<boolean | null>(null);
47
+ const SidebarLeftContext = React.createContext<SidebarContextSideState | null>(
48
+ null
49
+ );
50
+ const SidebarRightContext = React.createContext<SidebarContextSideState | null>(
51
+ null
52
+ );
53
+ const SidebarSideContext = React.createContext<SidebarSide>('left');
54
+
55
+ const resolveSideValue = (
56
+ value: SidebarOpenValue | undefined,
57
+ side: SidebarSide,
58
+ fallback: boolean
59
+ ) => {
60
+ if (typeof value === 'boolean') {
61
+ return value;
62
+ }
63
+
64
+ if (value && typeof value === 'object' && typeof value[side] === 'boolean') {
65
+ return value[side] as boolean;
66
+ }
67
+
68
+ return fallback;
69
+ };
70
+
71
+ const resolveControlledOpen = (
72
+ value: SidebarOpenValue | undefined,
73
+ side: SidebarSide
74
+ ) => {
75
+ if (typeof value === 'boolean') {
76
+ return value;
77
+ }
78
+
79
+ if (value && typeof value === 'object' && typeof value[side] === 'boolean') {
80
+ return value[side] as boolean;
81
+ }
82
+
83
+ return undefined;
84
+ };
85
+
86
+ const resolveStorageKey = (
87
+ storageKey: SidebarStorageKey,
88
+ side: SidebarSide
89
+ ) => {
90
+ if (typeof storageKey === 'string') {
91
+ return side === 'left' ? storageKey : `${storageKey}_right`;
92
+ }
93
+
94
+ if (storageKey && typeof storageKey === 'object') {
95
+ return (
96
+ storageKey[side] ??
97
+ (side === 'left' ? 'sidebar_expanded' : 'sidebar_expanded_right')
98
+ );
99
+ }
100
+
101
+ return side === 'left' ? 'sidebar_expanded' : 'sidebar_expanded_right';
102
+ };
103
+
104
+ const getSidebarWidth = (
105
+ state: SidebarContextSideState['state'],
106
+ collapsible: 'offcanvas' | 'icon' | 'none'
107
+ ) =>
108
+ state === 'collapsed' && collapsible === 'icon'
109
+ ? 'var(--sidebar-width-icon)'
110
+ : state === 'collapsed'
111
+ ? '0'
112
+ : 'var(--sidebar-width)';
113
+
114
+ const getSidebarOffsetStyles = (
115
+ side: SidebarSide,
116
+ state: SidebarContextSideState['state'],
117
+ collapsible: 'offcanvas' | 'icon' | 'none'
118
+ ) => {
119
+ const isVisible = state === 'expanded' || collapsible === 'icon';
120
+ return {
121
+ left:
122
+ side === 'left'
123
+ ? isVisible
124
+ ? '0'
125
+ : 'calc(var(--sidebar-width)*-1)'
126
+ : undefined,
127
+ right:
128
+ side === 'right'
129
+ ? isVisible
130
+ ? '0'
131
+ : 'calc(var(--sidebar-width)*-1)'
132
+ : undefined,
133
+ };
134
+ };
135
+
136
+ const useSidebarSideState = ({
137
+ side,
138
+ isMobile,
139
+ defaultOpen,
140
+ openProp,
141
+ onOpenChange,
142
+ storageKey,
143
+ lastToggledSideRef,
144
+ }: {
145
+ side: SidebarSide;
146
+ isMobile: boolean;
147
+ defaultOpen: SidebarOpenValue | undefined;
148
+ openProp: SidebarOpenValue | undefined;
149
+ onOpenChange?: (open: boolean, side?: SidebarSide) => void;
150
+ storageKey: SidebarStorageKey;
151
+ lastToggledSideRef: React.MutableRefObject<SidebarSide>;
152
+ }): SidebarContextSideState => {
153
+ const defaultFallback = typeof defaultOpen === 'boolean' ? defaultOpen : true;
154
+ const initialDefaultOpen = resolveSideValue(
155
+ defaultOpen,
156
+ side,
157
+ defaultFallback
158
+ );
159
+ const controlledOpen = resolveControlledOpen(openProp, side);
160
+ const isControlled = typeof controlledOpen === 'boolean';
161
+
162
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(
163
+ controlledOpen ?? initialDefaultOpen
164
+ );
165
+ const [openMobile, setOpenMobile] = useState(() =>
166
+ isMobile ? false : controlledOpen ?? initialDefaultOpen
167
+ );
168
+
169
+ const open = controlledOpen ?? uncontrolledOpen;
170
+
171
+ useEffect(() => {
172
+ if (isMobile) {
173
+ setOpenMobile(false);
174
+ } else {
175
+ setUncontrolledOpen(controlledOpen ?? initialDefaultOpen);
176
+ }
177
+ }, [isMobile, controlledOpen, initialDefaultOpen]);
178
+
179
+ const setOpen = useCallback(
180
+ (value: boolean | ((value: boolean) => boolean)) => {
181
+ const newOpenState = typeof value === 'function' ? value(open) : value;
182
+ if (newOpenState === open) {
183
+ return;
184
+ }
185
+
186
+ if (!isControlled) {
187
+ setUncontrolledOpen(newOpenState);
188
+ }
189
+
190
+ onOpenChange?.(newOpenState, side);
191
+
192
+ const key = resolveStorageKey(storageKey, side);
193
+ localStorage.setItem(key, `${newOpenState}`);
194
+ },
195
+ [open, isControlled, onOpenChange, side, storageKey]
196
+ );
197
+
198
+ const toggleSidebar = useCallback(() => {
199
+ lastToggledSideRef.current = side;
200
+ isMobile ? setOpenMobile((value) => !value) : setOpen((value) => !value);
201
+ }, [isMobile, setOpen, side, lastToggledSideRef]);
202
+
203
+ const state = open ? 'expanded' : 'collapsed';
204
+
205
+ return useMemo(
206
+ () => ({
207
+ state,
208
+ open,
209
+ setOpen,
210
+ openMobile,
211
+ setOpenMobile,
212
+ toggleSidebar,
213
+ }),
214
+ [state, open, setOpen, openMobile, setOpenMobile, toggleSidebar]
215
+ );
216
+ };
39
217
 
40
- function useSidebar() {
41
- const context = React.useContext(SidebarContext);
42
- if (!context) {
218
+ function useSidebar(sideOverride?: SidebarSide) {
219
+ const isMobile = React.useContext(SidebarIsMobileContext);
220
+ if (typeof isMobile !== 'boolean') {
43
221
  throw new Error('useSidebar must be used within a SidebarProvider.');
44
222
  }
45
- return context;
223
+ const contextSide = React.useContext(SidebarSideContext);
224
+ const side = sideOverride ?? contextSide;
225
+ const sideContext = React.useContext(
226
+ side === 'left' ? SidebarLeftContext : SidebarRightContext
227
+ );
228
+ if (!sideContext) {
229
+ throw new Error('useSidebar must be used within a SidebarProvider.');
230
+ }
231
+ return {
232
+ ...sideContext,
233
+ isMobile,
234
+ side,
235
+ };
46
236
  }
47
237
 
48
238
  const SidebarProvider = forwardRef<
49
239
  HTMLDivElement,
50
240
  React.ComponentProps<'div'> & {
51
- defaultOpen?: boolean;
52
- open?: boolean;
53
- storageKey?: string;
54
- onOpenChange?: (open: boolean) => void;
241
+ defaultOpen?: SidebarOpenValue;
242
+ open?: SidebarOpenValue;
243
+ storageKey?: SidebarStorageKey;
244
+ onOpenChange?: (open: boolean, side?: SidebarSide) => void;
55
245
  }
56
246
  >(
57
247
  (
@@ -68,97 +258,103 @@ const SidebarProvider = forwardRef<
68
258
  ref
69
259
  ) => {
70
260
  const isMobile = useIsMobile();
71
- const [openMobile, setOpenMobile] = useState(() =>
72
- isMobile ? false : openProp ?? defaultOpen
73
- );
74
-
75
- // Manages sidebar open state with a fallback to internal state when openProp is not provided
76
- const [_open, _setOpen] = useState(openProp ?? defaultOpen);
77
- const open = openProp ?? _open;
261
+ const lastToggledSideRef = React.useRef<SidebarSide>('left');
262
+ const leftToggleRef = React.useRef<
263
+ SidebarContextSideState['toggleSidebar'] | null
264
+ >(null);
265
+ const rightToggleRef = React.useRef<
266
+ SidebarContextSideState['toggleSidebar'] | null
267
+ >(null);
268
+ const leftState = useSidebarSideState({
269
+ side: 'left',
270
+ isMobile,
271
+ defaultOpen,
272
+ openProp,
273
+ onOpenChange: setOpenProp,
274
+ storageKey,
275
+ lastToggledSideRef,
276
+ });
277
+ const rightState = useSidebarSideState({
278
+ side: 'right',
279
+ isMobile,
280
+ defaultOpen,
281
+ openProp,
282
+ onOpenChange: setOpenProp,
283
+ storageKey,
284
+ lastToggledSideRef,
285
+ });
78
286
 
79
- // Update open state when openProp or isMobile changes
80
287
  useEffect(() => {
81
- if (isMobile) {
82
- setOpenMobile(false); // Always start closed on mobile
83
- } else {
84
- _setOpen(openProp ?? defaultOpen); // Use desktop state
85
- }
86
- }, [isMobile, openProp, defaultOpen]);
87
-
88
- const setOpen = useCallback(
89
- (value: boolean | ((value: boolean) => boolean)) => {
90
- const newOpenState = typeof value === 'function' ? value(open) : value;
91
-
92
- if (newOpenState !== open) {
93
- if (setOpenProp) {
94
- setOpenProp(newOpenState);
95
- } else {
96
- _setOpen(newOpenState);
97
- }
98
-
99
- localStorage.setItem(storageKey, `${newOpenState}`);
100
- }
101
- },
102
- [setOpenProp, open, storageKey]
103
- );
288
+ leftToggleRef.current = leftState.toggleSidebar;
289
+ }, [leftState.toggleSidebar]);
104
290
 
105
- // Toggle sidebar based on screen type
106
- const toggleSidebar = useCallback(() => {
107
- isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
108
- }, [isMobile, setOpen, setOpenMobile]);
291
+ useEffect(() => {
292
+ rightToggleRef.current = rightState.toggleSidebar;
293
+ }, [rightState.toggleSidebar]);
109
294
 
110
- // Keydown event handler for toggling sidebar
111
295
  useEffect(() => {
112
296
  const handleKeyDown = (event: KeyboardEvent) => {
113
- if (event.key === SIDEBAR_KEYBOARD_SHORTCUT) {
114
- event.preventDefault();
115
- toggleSidebar();
297
+ const target = event.target as HTMLElement | null;
298
+ if (
299
+ target &&
300
+ (target.tagName === 'INPUT' ||
301
+ target.tagName === 'TEXTAREA' ||
302
+ target.tagName === 'SELECT' ||
303
+ target.isContentEditable)
304
+ ) {
305
+ return;
306
+ }
307
+ const shortcutSide =
308
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT_LEFT
309
+ ? 'left'
310
+ : event.key === SIDEBAR_KEYBOARD_SHORTCUT_RIGHT
311
+ ? 'right'
312
+ : null;
313
+
314
+ if (!shortcutSide) {
315
+ return;
116
316
  }
317
+
318
+ event.preventDefault();
319
+
320
+ const toggleSidebar =
321
+ shortcutSide === 'left'
322
+ ? leftToggleRef.current
323
+ : rightToggleRef.current;
324
+ toggleSidebar?.();
117
325
  };
118
326
 
119
327
  window.addEventListener('keydown', handleKeyDown);
120
328
  return () => window.removeEventListener('keydown', handleKeyDown);
121
- }, [toggleSidebar]);
122
-
123
- // Assign state for data attributes
124
- const state = open ? 'expanded' : 'collapsed';
125
-
126
- const contextValue = useMemo<SidebarContextProps>(
127
- () => ({
128
- state,
129
- open,
130
- setOpen,
131
- isMobile,
132
- openMobile,
133
- setOpenMobile,
134
- toggleSidebar,
135
- }),
136
- [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
137
- );
329
+ }, []);
138
330
 
139
331
  return (
140
- <SidebarContext.Provider value={contextValue}>
141
- <TooltipProvider delayDuration={0}>
142
- <div
143
- style={
144
- {
145
- '--sidebar-width': SIDEBAR_WIDTH,
146
- '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
147
- minBlockSize: '100svh',
148
- ...style,
149
- } as React.CSSProperties
150
- }
151
- className={classNames(
152
- 'display-flex w-100 background-color-secondary',
153
- className
154
- )}
155
- ref={ref}
156
- {...props}
157
- >
158
- {children}
159
- </div>
160
- </TooltipProvider>
161
- </SidebarContext.Provider>
332
+ <SidebarIsMobileContext.Provider value={isMobile}>
333
+ <SidebarLeftContext.Provider value={leftState}>
334
+ <SidebarRightContext.Provider value={rightState}>
335
+ <TooltipProvider delayDuration={0}>
336
+ <div
337
+ style={
338
+ {
339
+ '--sidebar-width': SIDEBAR_WIDTH,
340
+ '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
341
+ minBlockSize: '100svh',
342
+ ...style,
343
+ } as React.CSSProperties
344
+ }
345
+ className={classNames(
346
+ 'display-flex w-100 background-color-secondary',
347
+ className
348
+ )}
349
+ ref={ref}
350
+ {...props}
351
+ >
352
+ {children}
353
+ </div>
354
+ </TooltipProvider>
355
+ </SidebarRightContext.Provider>
356
+ </SidebarLeftContext.Provider>
357
+ </SidebarIsMobileContext.Provider>
162
358
  );
163
359
  }
164
360
  );
@@ -167,7 +363,7 @@ SidebarProvider.displayName = 'SidebarProvider';
167
363
  const Sidebar = React.forwardRef<
168
364
  HTMLDivElement,
169
365
  React.ComponentProps<'div'> & {
170
- side?: 'left'; // no right sidebar yet
366
+ side?: 'left' | 'right';
171
367
  collapsible?: 'offcanvas' | 'icon' | 'none';
172
368
  }
173
369
  >(
@@ -175,106 +371,116 @@ const Sidebar = React.forwardRef<
175
371
  { side = 'left', collapsible = 'offcanvas', className, children, ...props },
176
372
  ref
177
373
  ) => {
178
- const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
374
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar(side);
179
375
 
180
376
  if (isMobile) {
181
377
  return (
182
- <Drawer
183
- isOpen={openMobile}
184
- onDismiss={() => setOpenMobile(false)}
185
- placement={side}
186
- >
187
- <Box data-sidebar="sidebar" data-mobile="true" height="100">
188
- {children}
189
- </Box>
190
- </Drawer>
378
+ <SidebarSideContext.Provider value={side}>
379
+ <Drawer
380
+ isOpen={openMobile}
381
+ onDismiss={() => setOpenMobile(false)}
382
+ placement={side}
383
+ >
384
+ <Box data-sidebar="sidebar" data-mobile="true" height="100">
385
+ {children}
386
+ </Box>
387
+ </Drawer>
388
+ </SidebarSideContext.Provider>
191
389
  );
192
390
  }
193
391
 
194
392
  if (collapsible === 'none') {
195
393
  return (
196
- <div
197
- className={classNames(
198
- 'group display-flex h-100 font-size-xs flex-direction-column background-color-secondary font-color-base',
199
- className
200
- )}
201
- style={{
202
- width: 'var(--sidebar-width)',
203
- }}
204
- ref={ref}
205
- {...props}
206
- >
207
- {children}
208
- </div>
394
+ <SidebarSideContext.Provider value={side}>
395
+ <div
396
+ className={classNames(
397
+ 'group display-flex h-100 font-size-xs flex-direction-column background-color-secondary font-color-base',
398
+ className
399
+ )}
400
+ style={{
401
+ width: 'var(--sidebar-width)',
402
+ }}
403
+ ref={ref}
404
+ {...props}
405
+ >
406
+ {children}
407
+ </div>
408
+ </SidebarSideContext.Provider>
209
409
  );
210
410
  }
211
411
 
212
412
  return (
213
- <Box
214
- ref={ref}
215
- background="primary"
216
- display={{ base: 'none', desktop: 'block' }}
217
- color="base"
218
- fontSize="sm"
219
- position="relative"
220
- data-state={state}
221
- data-collapsible={collapsible}
222
- data-side={side}
223
- className="group"
224
- >
225
- <div
226
- style={{
227
- animationTimingFunction: 'var(--sidebar-transition-timing, linear)',
228
- transitionTimingFunction:
229
- 'var(--sidebar-transition-timing, linear)',
230
- transitionDuration: 'var(--sidebar-transition-duration, 200ms)',
231
- animationDuration: 'var(--sidebar-transition-duration, 200ms)',
232
- transitionProperty: 'width',
233
- width:
234
- state === 'collapsed' && collapsible === 'icon'
235
- ? 'var(--sidebar-width-icon)'
236
- : state === 'collapsed'
237
- ? '0'
238
- : 'var(--sidebar-width)',
239
- height: '100svh',
240
- }}
241
- className={classNames('position-relative', className)}
242
- />
243
- <div
244
- className={classNames(
245
- 'position-absolute display-none display-flex-desktop ',
246
- className
247
- )}
248
- style={{
249
- left:
250
- state === 'expanded' || collapsible === 'icon'
251
- ? '0'
252
- : 'calc(var(--sidebar-width)*-1)',
253
- top: '0',
254
- bottom: '0',
255
- zIndex: 'var(--size-z-index-drawer)',
256
- animationTimingFunction: 'var(--sidebar-transition-timing, linear)',
257
- transitionTimingFunction:
258
- 'var(--sidebar-transition-timing, linear)',
259
- transitionDuration: 'var(--sidebar-transition-duration, 200ms)',
260
- animationDuration: 'var(--sidebar-transition-duration, 200ms)',
261
- transitionProperty: 'left, right, width',
262
- width:
263
- state === 'collapsed' && collapsible === 'icon'
264
- ? 'var(--sidebar-width-icon)'
265
- : 'var(--sidebar-width)',
266
- height: '100svh',
267
- }}
268
- {...props}
413
+ <SidebarSideContext.Provider value={side}>
414
+ <Box
415
+ ref={ref}
416
+ background="primary"
417
+ display={{ base: 'none', desktop: 'block' }}
418
+ color="base"
419
+ fontSize="sm"
420
+ position="relative"
421
+ style={
422
+ side === 'right' && collapsible === 'offcanvas'
423
+ ? { overflowX: 'hidden' }
424
+ : undefined
425
+ }
426
+ data-state={state}
427
+ data-collapsible={collapsible}
428
+ data-side={side}
429
+ className="group"
269
430
  >
270
431
  <div
271
- data-sidebar="sidebar"
272
- className="display-flex h-100 w-100 flex-direction-column background-color-secondary font-color-base"
432
+ style={{
433
+ animationTimingFunction:
434
+ 'var(--sidebar-transition-timing, linear)',
435
+ transitionTimingFunction:
436
+ 'var(--sidebar-transition-timing, linear)',
437
+ transitionDuration: 'var(--sidebar-transition-duration, 200ms)',
438
+ animationDuration: 'var(--sidebar-transition-duration, 200ms)',
439
+ transitionProperty: 'width',
440
+ width: getSidebarWidth(state, collapsible),
441
+ height: '100svh',
442
+ }}
443
+ className={classNames('position-relative', className)}
444
+ />
445
+ <div
446
+ className={classNames(
447
+ 'position-absolute display-none display-flex-desktop ',
448
+ className
449
+ )}
450
+ style={{
451
+ ...getSidebarOffsetStyles(side, state, collapsible),
452
+ top: '0',
453
+ bottom: '0',
454
+ zIndex: 'var(--size-z-index-drawer)',
455
+ animationTimingFunction:
456
+ 'var(--sidebar-transition-timing, linear)',
457
+ transitionTimingFunction:
458
+ 'var(--sidebar-transition-timing, linear)',
459
+ transitionDuration: 'var(--sidebar-transition-duration, 200ms)',
460
+ animationDuration: 'var(--sidebar-transition-duration, 200ms)',
461
+ transitionProperty: 'left, right, width',
462
+ width:
463
+ state === 'collapsed' && collapsible === 'icon'
464
+ ? 'var(--sidebar-width-icon)'
465
+ : 'var(--sidebar-width)',
466
+ height: '100svh',
467
+ }}
468
+ {...props}
273
469
  >
274
- {children}
470
+ <div
471
+ data-sidebar="sidebar"
472
+ className={classNames(
473
+ 'display-flex h-100 w-100 flex-direction-column background-color-secondary font-color-base',
474
+ {
475
+ 'p-right-lg-desktop': side === 'right',
476
+ }
477
+ )}
478
+ >
479
+ {children}
480
+ </div>
275
481
  </div>
276
- </div>
277
- </Box>
482
+ </Box>
483
+ </SidebarSideContext.Provider>
278
484
  );
279
485
  }
280
486
  );
@@ -282,9 +488,12 @@ Sidebar.displayName = 'Sidebar';
282
488
 
283
489
  const SidebarTrigger = React.forwardRef<
284
490
  React.ElementRef<typeof Button>,
285
- React.ComponentProps<typeof Button>
286
- >(({ className, onClick, ...props }, ref) => {
287
- const { toggleSidebar } = useSidebar();
491
+ React.ComponentProps<typeof Button> & {
492
+ side?: SidebarSide;
493
+ iconName?: IconName;
494
+ }
495
+ >(({ className, onClick, side, iconName = 'dock-left', ...props }, ref) => {
496
+ const { toggleSidebar, side: contextSide } = useSidebar(side);
288
497
 
289
498
  return (
290
499
  <Button
@@ -292,13 +501,19 @@ const SidebarTrigger = React.forwardRef<
292
501
  data-sidebar="trigger"
293
502
  variant="tertiary"
294
503
  size="sm"
295
- iconPrefix="dock-left"
296
- className={classNames('m-left-sm m-left-0-tablet', className)}
504
+ iconPrefix={iconName}
505
+ className={classNames(
506
+ {
507
+ 'm-left-sm m-left-0-tablet': contextSide === 'left',
508
+ 'm-right-sm m-right-0-tablet': contextSide === 'right',
509
+ },
510
+ className
511
+ )}
297
512
  onClick={(event) => {
298
513
  onClick?.(event);
299
514
  toggleSidebar();
300
515
  }}
301
- aria-label="toggle sidebar"
516
+ aria-label={`Toggle ${contextSide} sidebar`}
302
517
  {...props}
303
518
  />
304
519
  );
@@ -429,7 +644,7 @@ const SidebarMenuButton = React.forwardRef<
429
644
  ref
430
645
  ) => {
431
646
  const Comp = asChild ? Slot : 'button';
432
- const { isMobile, state } = useSidebar();
647
+ const { isMobile, state, side } = useSidebar();
433
648
 
434
649
  const button = (
435
650
  <Comp
@@ -464,7 +679,7 @@ const SidebarMenuButton = React.forwardRef<
464
679
  <Tooltip>
465
680
  <TooltipTrigger asChild>{button}</TooltipTrigger>
466
681
  <TooltipContent
467
- side="right"
682
+ side={side === 'right' ? 'left' : 'right'}
468
683
  align="center"
469
684
  hidden={state !== 'collapsed' || isMobile}
470
685
  {...tooltip}
@@ -594,9 +809,19 @@ const SidebarRail = React.forwardRef<
594
809
  HTMLButtonElement,
595
810
  React.ComponentProps<'button'>
596
811
  >(({ className, ...props }, ref) => {
597
- const { open, toggleSidebar } = useSidebar();
598
-
599
- const caretIcon = open ? 'caret-sm-left' : 'caret-sm-right';
812
+ const { open, toggleSidebar, side } = useSidebar();
813
+ const shortcutLabel =
814
+ side === 'left'
815
+ ? SIDEBAR_KEYBOARD_SHORTCUT_LEFT
816
+ : SIDEBAR_KEYBOARD_SHORTCUT_RIGHT;
817
+
818
+ const caretIcon = open
819
+ ? side === 'right'
820
+ ? 'caret-sm-right'
821
+ : 'caret-sm-left'
822
+ : side === 'right'
823
+ ? 'caret-sm-left'
824
+ : 'caret-sm-right';
600
825
 
601
826
  return (
602
827
  <button
@@ -605,20 +830,23 @@ const SidebarRail = React.forwardRef<
605
830
  aria-label="Toggle Sidebar"
606
831
  tabIndex={-1}
607
832
  onClick={toggleSidebar}
608
- title="Toggle Sidebar ["
833
+ title={`Toggle Sidebar ${shortcutLabel}`}
609
834
  className={classNames(
610
835
  styles.rail,
611
836
  'hover-show-child background-color-transparent display-flex p-top-5xl p-left-xl p-right-0 justify-content-center position-absolute',
612
837
  {
613
- 'cursor-w-resize': open,
614
- 'cursor-e-resize': !open,
838
+ 'cursor-w-resize':
839
+ (open && side === 'left') || (!open && side === 'right'),
840
+ 'cursor-e-resize':
841
+ (!open && side === 'left') || (open && side === 'right'),
615
842
  },
616
843
  className
617
844
  )}
618
845
  style={{
619
846
  top: '20px',
620
847
  bottom: '20px',
621
- right: '-14px',
848
+ right: side === 'left' ? '-14px' : undefined,
849
+ left: side === 'right' ? '-14px' : undefined,
622
850
  width: '10px',
623
851
  }}
624
852
  {...props}
@@ -638,8 +866,10 @@ const SidebarRail = React.forwardRef<
638
866
  className={classNames(
639
867
  'hover-child',
640
868
  {
641
- 'cursor-w-resize': open,
642
- 'cursor-e-resize': !open,
869
+ 'cursor-w-resize':
870
+ (open && side === 'left') || (!open && side === 'right'),
871
+ 'cursor-e-resize':
872
+ (!open && side === 'left') || (open && side === 'right'),
643
873
  },
644
874
  className
645
875
  )}