@hyphen/hyphen-components 9.0.0 → 9.1.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.
@@ -5,6 +5,7 @@ import React, {
5
5
  useEffect,
6
6
  forwardRef,
7
7
  } from 'react';
8
+ import { useId } from '@radix-ui/react-id';
8
9
  import { Slot } from '@radix-ui/react-slot';
9
10
  import classNames from 'classnames';
10
11
  import { Button } from '../Button/Button';
@@ -14,6 +15,11 @@ import { Box } from '../Box/Box';
14
15
  import { IconName } from 'src/types';
15
16
  import { Icon } from '../Icon/Icon';
16
17
  import styles from './Sidebar.module.scss';
18
+ import {
19
+ SidebarResizeContext,
20
+ SidebarResizeProps,
21
+ useSidebarResize,
22
+ } from './useSidebarResize';
17
23
  import {
18
24
  Tooltip,
19
25
  TooltipContent,
@@ -326,6 +332,14 @@ const SidebarProvider = forwardRef<
326
332
  ) {
327
333
  return;
328
334
  }
335
+ // Leave Cmd/Ctrl combinations (e.g. Cmd+[ for back) to the browser.
336
+ // AltGraph also sets ctrlKey on Windows and types [ or ] on some layouts.
337
+ if (
338
+ event.metaKey ||
339
+ (event.ctrlKey && !event.getModifierState('AltGraph'))
340
+ ) {
341
+ return;
342
+ }
329
343
  const shortcutSide =
330
344
  event.key === SIDEBAR_KEYBOARD_SHORTCUT_LEFT
331
345
  ? 'left'
@@ -369,6 +383,7 @@ const SidebarProvider = forwardRef<
369
383
  className
370
384
  )}
371
385
  ref={ref}
386
+ data-sidebar-provider=""
372
387
  {...props}
373
388
  >
374
389
  {children}
@@ -384,38 +399,76 @@ SidebarProvider.displayName = 'SidebarProvider';
384
399
 
385
400
  const Sidebar = React.forwardRef<
386
401
  HTMLDivElement,
387
- React.ComponentProps<'div'> & {
388
- /**
389
- * Which edge of the viewport the sidebar is attached to.
390
- */
391
- side?: 'left' | 'right';
392
- /**
393
- * Collapse behavior when the sidebar is toggled closed: slide fully out
394
- * of view (`offcanvas`), collapse to a narrow icon strip (`icon`), or
395
- * stay at full width (`none`).
396
- */
397
- collapsible?: 'offcanvas' | 'icon' | 'none';
398
- }
402
+ React.ComponentProps<'div'> &
403
+ SidebarResizeProps & {
404
+ /**
405
+ * Which edge of the viewport the sidebar is attached to.
406
+ */
407
+ side?: 'left' | 'right';
408
+ /**
409
+ * Collapse behavior when the sidebar is toggled closed: slide fully out
410
+ * of view (`offcanvas`), collapse to a narrow icon strip (`icon`), or
411
+ * stay at full width (`none`).
412
+ */
413
+ collapsible?: 'offcanvas' | 'icon' | 'none';
414
+ }
399
415
  >(
400
416
  (
401
- { side = 'left', collapsible = 'offcanvas', className, children, ...props },
417
+ {
418
+ side = 'left',
419
+ collapsible = 'offcanvas',
420
+ className,
421
+ children,
422
+ resizable,
423
+ defaultWidth,
424
+ minWidth,
425
+ maxWidth,
426
+ widthStorageKey,
427
+ ...props
428
+ },
402
429
  ref
403
430
  ) => {
404
431
  const { isMobile, state, openMobile, setOpenMobile } = useSidebar(side);
405
- const sidebarWidth = side === 'right' ? SIDEBAR_RIGHT_WIDTH : SIDEBAR_WIDTH;
432
+ const rootRef = React.useRef<HTMLDivElement | null>(null);
433
+ const setRootRef = React.useCallback(
434
+ (node: HTMLDivElement | null) => {
435
+ rootRef.current = node;
436
+ if (typeof ref === 'function') ref(node);
437
+ else if (ref) ref.current = node;
438
+ },
439
+ [ref]
440
+ );
441
+ const resize = useSidebarResize({
442
+ resizable,
443
+ defaultWidth,
444
+ minWidth,
445
+ maxWidth,
446
+ widthStorageKey,
447
+ side,
448
+ isMobile,
449
+ expanded: state === 'expanded' || collapsible === 'none',
450
+ rootRef,
451
+ });
452
+ const sidebarWidth = resizable
453
+ ? `${resize.width}px`
454
+ : side === 'right'
455
+ ? SIDEBAR_RIGHT_WIDTH
456
+ : SIDEBAR_WIDTH;
406
457
 
407
458
  if (isMobile) {
408
459
  return (
409
460
  <SidebarSideContext.Provider value={side}>
410
- <Drawer
411
- isOpen={openMobile}
412
- onDismiss={() => setOpenMobile(false)}
413
- placement={side}
414
- >
415
- <Box data-sidebar="sidebar" data-mobile="true" height="100">
416
- {children}
417
- </Box>
418
- </Drawer>
461
+ <SidebarResizeContext.Provider value={resize}>
462
+ <Drawer
463
+ isOpen={openMobile}
464
+ onDismiss={() => setOpenMobile(false)}
465
+ placement={side}
466
+ >
467
+ <Box data-sidebar="sidebar" data-mobile="true" height="100">
468
+ {children}
469
+ </Box>
470
+ </Drawer>
471
+ </SidebarResizeContext.Provider>
419
472
  </SidebarSideContext.Provider>
420
473
  );
421
474
  }
@@ -423,97 +476,106 @@ const Sidebar = React.forwardRef<
423
476
  if (collapsible === 'none') {
424
477
  return (
425
478
  <SidebarSideContext.Provider value={side}>
426
- <div
427
- className={classNames(
428
- 'group display-flex h-100 font-size-xs flex-direction-column background-color-secondary font-color-base',
429
- className
430
- )}
431
- style={
432
- {
433
- '--sidebar-width': sidebarWidth,
434
- width: 'var(--sidebar-width)',
435
- } as React.CSSProperties
436
- }
437
- ref={ref}
438
- {...props}
439
- >
440
- {children}
441
- </div>
479
+ <SidebarResizeContext.Provider value={resize}>
480
+ <div
481
+ className={classNames(
482
+ 'group display-flex h-100 font-size-xs flex-direction-column background-color-secondary font-color-base',
483
+ className
484
+ )}
485
+ style={
486
+ {
487
+ '--sidebar-width': sidebarWidth,
488
+ width: 'var(--sidebar-width)',
489
+ ...(resizable ? { position: 'relative', flexShrink: 0 } : {}),
490
+ } as React.CSSProperties
491
+ }
492
+ ref={setRootRef}
493
+ {...props}
494
+ >
495
+ {children}
496
+ </div>
497
+ </SidebarResizeContext.Provider>
442
498
  </SidebarSideContext.Provider>
443
499
  );
444
500
  }
445
501
 
446
502
  return (
447
503
  <SidebarSideContext.Provider value={side}>
448
- <Box
449
- ref={ref}
450
- background="primary"
451
- display={{ base: 'none', desktop: 'block' }}
452
- color="base"
453
- fontSize="sm"
454
- position="relative"
455
- style={
456
- {
457
- '--sidebar-width': sidebarWidth,
458
- } as React.CSSProperties
459
- }
460
- data-state={state}
461
- data-collapsible={collapsible}
462
- data-side={side}
463
- className="group"
464
- >
465
- <div
466
- style={{
467
- animationTimingFunction:
468
- 'var(--sidebar-transition-timing, linear)',
469
- transitionTimingFunction:
470
- 'var(--sidebar-transition-timing, linear)',
471
- transitionDuration: 'var(--sidebar-transition-duration, 200ms)',
472
- animationDuration: 'var(--sidebar-transition-duration, 200ms)',
473
- transitionProperty: 'width',
474
- width: getSidebarWidth(state, collapsible),
475
- height: '100svh',
476
- }}
477
- className={classNames('position-relative', className)}
478
- />
479
- <div
480
- className={classNames(
481
- 'position-absolute display-none display-flex-desktop ',
482
- className
483
- )}
484
- style={{
485
- ...getSidebarOffsetStyles(side, state, collapsible),
486
- top: '0',
487
- bottom: '0',
488
- zIndex: 'var(--size-z-index-drawer)',
489
- animationTimingFunction:
490
- 'var(--sidebar-transition-timing, linear)',
491
- transitionTimingFunction:
492
- 'var(--sidebar-transition-timing, linear)',
493
- transitionDuration: 'var(--sidebar-transition-duration, 200ms)',
494
- animationDuration: 'var(--sidebar-transition-duration, 200ms)',
495
- transitionProperty: 'left, right, width',
496
- width:
497
- state === 'collapsed' && collapsible === 'icon'
498
- ? 'var(--sidebar-width-icon)'
499
- : 'var(--sidebar-width)',
500
- height: '100svh',
501
- }}
502
- {...props}
504
+ <SidebarResizeContext.Provider value={resize}>
505
+ <Box
506
+ ref={setRootRef}
507
+ background="primary"
508
+ display={{ base: 'none', desktop: 'block' }}
509
+ color="base"
510
+ fontSize="sm"
511
+ position="relative"
512
+ style={
513
+ {
514
+ '--sidebar-width': sidebarWidth,
515
+ } as React.CSSProperties
516
+ }
517
+ data-state={state}
518
+ data-collapsible={collapsible}
519
+ data-side={side}
520
+ className="group"
503
521
  >
504
522
  <div
505
- data-sidebar="sidebar"
523
+ style={{
524
+ animationTimingFunction:
525
+ 'var(--sidebar-transition-timing, linear)',
526
+ transitionTimingFunction:
527
+ 'var(--sidebar-transition-timing, linear)',
528
+ transitionDuration: resize.animate
529
+ ? 'var(--sidebar-transition-duration, 200ms)'
530
+ : '0ms',
531
+ animationDuration: 'var(--sidebar-transition-duration, 200ms)',
532
+ transitionProperty: 'width',
533
+ width: getSidebarWidth(state, collapsible),
534
+ height: '100svh',
535
+ }}
536
+ className={classNames('position-relative', className)}
537
+ />
538
+ <div
506
539
  className={classNames(
507
- 'display-flex h-100 w-100 flex-direction-column background-color-secondary font-color-base',
508
- {
509
- 'p-right-lg-desktop': side === 'right',
510
- }
540
+ 'position-absolute display-none display-flex-desktop ',
541
+ className
511
542
  )}
543
+ style={{
544
+ ...getSidebarOffsetStyles(side, state, collapsible),
545
+ top: '0',
546
+ bottom: '0',
547
+ zIndex: 'var(--size-z-index-drawer)',
548
+ animationTimingFunction:
549
+ 'var(--sidebar-transition-timing, linear)',
550
+ transitionTimingFunction:
551
+ 'var(--sidebar-transition-timing, linear)',
552
+ transitionDuration: resize.animate
553
+ ? 'var(--sidebar-transition-duration, 200ms)'
554
+ : '0ms',
555
+ animationDuration: 'var(--sidebar-transition-duration, 200ms)',
556
+ transitionProperty: 'left, right, width',
557
+ width:
558
+ state === 'collapsed' && collapsible === 'icon'
559
+ ? 'var(--sidebar-width-icon)'
560
+ : 'var(--sidebar-width)',
561
+ height: '100svh',
562
+ }}
563
+ {...props}
512
564
  >
513
- {children}
565
+ <div
566
+ data-sidebar="sidebar"
567
+ className={classNames(
568
+ 'display-flex h-100 w-100 flex-direction-column background-color-secondary font-color-base',
569
+ {
570
+ 'p-right-lg-desktop': side === 'right',
571
+ }
572
+ )}
573
+ >
574
+ {children}
575
+ </div>
514
576
  </div>
515
- </div>
516
- </Box>
577
+ </Box>
578
+ </SidebarResizeContext.Provider>
517
579
  </SidebarSideContext.Provider>
518
580
  );
519
581
  }
@@ -860,6 +922,9 @@ const SidebarRail = React.forwardRef<
860
922
  React.ComponentProps<'button'>
861
923
  >(({ className, ...props }, ref) => {
862
924
  const { open, toggleSidebar, side } = useSidebar();
925
+ const resize = React.useContext(SidebarResizeContext);
926
+ // React.useId requires React 18; Radix's useId also supports 16.8 and 17.
927
+ const descriptionId = useId();
863
928
  const shortcutLabel =
864
929
  side === 'left'
865
930
  ? SIDEBAR_KEYBOARD_SHORTCUT_LEFT
@@ -870,64 +935,91 @@ const SidebarRail = React.forwardRef<
870
935
  ? 'caret-sm-right'
871
936
  : 'caret-sm-left'
872
937
  : side === 'right'
873
- ? 'caret-sm-left'
874
- : 'caret-sm-right';
938
+ ? 'caret-sm-left'
939
+ : 'caret-sm-right';
875
940
 
876
941
  return (
877
- <button
878
- ref={ref}
879
- data-sidebar="rail"
880
- aria-label="Toggle Sidebar"
881
- tabIndex={-1}
882
- onClick={toggleSidebar}
883
- title={`Toggle Sidebar ${shortcutLabel}`}
884
- className={classNames(
885
- styles.rail,
886
- 'hover-show-child background-color-transparent display-flex p-top-5xl p-left-xl p-right-0 justify-content-center position-absolute',
887
- {
888
- 'cursor-w-resize':
889
- (open && side === 'left') || (!open && side === 'right'),
890
- 'cursor-e-resize':
891
- (!open && side === 'left') || (open && side === 'right'),
892
- },
893
- className
894
- )}
895
- style={{
896
- top: '20px',
897
- bottom: '20px',
898
- right: side === 'left' ? '-14px' : undefined,
899
- left: side === 'right' ? '-18px' : undefined,
900
- width: '10px',
901
- }}
902
- type="button"
903
- {...props}
904
- >
905
- <Box
906
- radius="xl"
907
- background="primary"
908
- color="secondary"
909
- borderWidth="sm"
910
- padding="xs"
911
- margin="0"
912
- shadow="xs"
913
- width="3xl"
914
- height="3xl"
915
- alignItems="center"
916
- justifyContent="center"
942
+ <>
943
+ <button
944
+ ref={ref}
945
+ data-sidebar="rail"
946
+ data-resizable={resize?.enabled || undefined}
947
+ aria-label={
948
+ resize?.enabled
949
+ ? `Resize or toggle ${side} sidebar`
950
+ : 'Toggle Sidebar'
951
+ }
952
+ aria-describedby={resize?.enabled ? descriptionId : undefined}
953
+ tabIndex={resize?.enabled ? 0 : -1}
954
+ {...(resize?.enabled ? resize.railProps : {})}
955
+ onClick={toggleSidebar}
956
+ title={
957
+ resize?.enabled
958
+ ? `Drag to resize; click to toggle (${shortcutLabel})`
959
+ : `Toggle Sidebar ${shortcutLabel}`
960
+ }
917
961
  className={classNames(
918
- 'hover-child',
962
+ styles.rail,
963
+ 'hover-show-child background-color-transparent display-flex p-top-5xl p-left-xl p-right-0 justify-content-center position-absolute',
919
964
  {
920
965
  'cursor-w-resize':
921
- (open && side === 'left') || (!open && side === 'right'),
966
+ !resize?.enabled &&
967
+ ((open && side === 'left') || (!open && side === 'right')),
922
968
  'cursor-e-resize':
923
- (!open && side === 'left') || (open && side === 'right'),
969
+ !resize?.enabled &&
970
+ ((!open && side === 'left') || (open && side === 'right')),
924
971
  },
925
972
  className
926
973
  )}
974
+ style={{
975
+ top: '20px',
976
+ bottom: '20px',
977
+ right: side === 'left' ? '-14px' : undefined,
978
+ left: side === 'right' ? '-18px' : undefined,
979
+ width: '10px',
980
+ ...(resize?.enabled
981
+ ? { cursor: 'col-resize', touchAction: 'none' }
982
+ : {}),
983
+ }}
984
+ type="button"
985
+ {...props}
927
986
  >
928
- <Icon name={caretIcon} />
929
- </Box>
930
- </button>
987
+ <Box
988
+ radius="xl"
989
+ background="primary"
990
+ color="secondary"
991
+ borderWidth="sm"
992
+ padding="xs"
993
+ margin="0"
994
+ shadow="xs"
995
+ width="3xl"
996
+ height="3xl"
997
+ alignItems="center"
998
+ justifyContent="center"
999
+ className={classNames(
1000
+ 'hover-child',
1001
+ {
1002
+ 'cursor-w-resize':
1003
+ !resize?.enabled &&
1004
+ ((open && side === 'left') || (!open && side === 'right')),
1005
+ 'cursor-e-resize':
1006
+ !resize?.enabled &&
1007
+ ((!open && side === 'left') || (open && side === 'right')),
1008
+ },
1009
+ className
1010
+ )}
1011
+ >
1012
+ <Icon name={caretIcon} />
1013
+ </Box>
1014
+ </button>
1015
+ {resize?.enabled && (
1016
+ <span id={descriptionId} className={styles.resizeDescription}>
1017
+ Width {Math.round(resize.width)} pixels. Use left and right arrows to
1018
+ resize, Home for minimum, End for maximum, and Enter or Space to
1019
+ toggle.
1020
+ </span>
1021
+ )}
1022
+ </>
931
1023
  );
932
1024
  });
933
1025
  SidebarRail.displayName = 'SidebarRail';