@heliosgraphics/ui 2.0.0-alpha.95 → 2.0.0-alpha.97

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.
Files changed (110) hide show
  1. package/components/Alert/Alert.tsx +2 -0
  2. package/components/Breadcrumb/Breadcrumb.tsx +17 -1
  3. package/components/Browser/Browser.tsx +2 -0
  4. package/components/Button/Button.tsx +7 -3
  5. package/components/ButtonGroup/ButtonGroup.tsx +2 -0
  6. package/components/Checkbox/Checkbox.tsx +66 -55
  7. package/components/Clock/Clock.tsx +23 -19
  8. package/components/Column/Column.tsx +2 -0
  9. package/components/Confirm/Confirm.tsx +2 -0
  10. package/components/DatePicker/DatePicker.meta.ts +12 -5
  11. package/components/DatePicker/DatePicker.module.css +70 -1
  12. package/components/DatePicker/DatePicker.tsx +116 -4
  13. package/components/DatePicker/DatePicker.types.ts +6 -1
  14. package/components/DatePicker/DatePicker.utils.ts +53 -0
  15. package/components/Debug/Debug.tsx +2 -0
  16. package/components/Details/Details.tsx +2 -0
  17. package/components/Dialog/Dialog.module.css +4 -6
  18. package/components/Dialog/Dialog.tsx +33 -24
  19. package/components/Donut/Donut.tsx +2 -0
  20. package/components/Dot/Dot.tsx +2 -0
  21. package/components/Dropdown/Dropdown.module.css +5 -0
  22. package/components/Dropdown/Dropdown.tsx +21 -26
  23. package/components/Fieldset/Fieldset.tsx +2 -0
  24. package/components/Flex/Flex.meta.ts +1 -0
  25. package/components/Flex/Flex.tsx +22 -2
  26. package/components/Flex/Flex.types.ts +1 -0
  27. package/components/Flex/Flex.utils.spec.ts +4 -1
  28. package/components/Flex/Flex.utils.ts +4 -1
  29. package/components/Grid/Grid.tsx +2 -0
  30. package/components/Heading/Heading.meta.ts +5 -0
  31. package/components/Heading/Heading.tsx +15 -9
  32. package/components/Heading/Heading.types.ts +1 -0
  33. package/components/Heading/components/H0/H0.tsx +2 -0
  34. package/components/Heading/components/H1/H1.tsx +2 -0
  35. package/components/Heading/components/H2/H2.tsx +2 -0
  36. package/components/Heading/components/H3/H3.tsx +2 -0
  37. package/components/Heading/components/H4/H4.tsx +2 -0
  38. package/components/Heading/components/H5/H5.tsx +2 -0
  39. package/components/Heading/components/H6/H6.tsx +2 -0
  40. package/components/Icon/Icon.tsx +2 -0
  41. package/components/Input/Input.tsx +103 -95
  42. package/components/Layout/Layout.tsx +2 -0
  43. package/components/Layout/components/LayoutAside/LayoutAside.tsx +2 -0
  44. package/components/Layout/components/LayoutAside/components/LayoutAsideContent/LayoutAsideContent.tsx +2 -0
  45. package/components/Layout/components/LayoutAside/components/LayoutAsideFooter/LayoutAsideFooter.tsx +2 -0
  46. package/components/Layout/components/LayoutAside/components/LayoutAsideToggle/LayoutAsideToggle.tsx +2 -0
  47. package/components/Layout/components/LayoutMain/LayoutMain.tsx +2 -0
  48. package/components/Layout/components/LayoutMain/components/LayoutMainContent/LayoutMainContent.tsx +2 -0
  49. package/components/Layout/components/LayoutNavigation/LayoutNavigation.tsx +2 -0
  50. package/components/Layout/components/LayoutSubNavigation/LayoutSubNavigation.tsx +2 -0
  51. package/components/Loading/Loading.tsx +2 -0
  52. package/components/Markdown/Markdown.tsx +2 -0
  53. package/components/Masonry/Masonry.tsx +5 -1
  54. package/components/Menu/Menu.tsx +2 -0
  55. package/components/Menu/components/MenuCategory/MenuCategory.tsx +2 -0
  56. package/components/Menu/components/MenuFilter/MenuFilter.tsx +2 -0
  57. package/components/Menu/components/MenuItem/MenuItem.tsx +2 -0
  58. package/components/Overlay/Overlay.tsx +18 -2
  59. package/components/Pie/Pie.tsx +2 -0
  60. package/components/Pill/Pill.meta.ts +9 -1
  61. package/components/Pill/Pill.module.css +11 -0
  62. package/components/Pill/Pill.tsx +28 -3
  63. package/components/Pill/Pill.types.ts +2 -0
  64. package/components/Placeholder/Placeholder.tsx +2 -0
  65. package/components/Progress/Progress.tsx +2 -0
  66. package/components/Radio/Radio.tsx +2 -0
  67. package/components/Range/Range.tsx +2 -0
  68. package/components/Segments/Segments.context.ts +19 -0
  69. package/components/Segments/Segments.meta.ts +4 -0
  70. package/components/Segments/Segments.tsx +34 -42
  71. package/components/Segments/Segments.types.ts +1 -0
  72. package/components/Segments/components/SegmentButton/SegmentButton.meta.ts +0 -4
  73. package/components/Segments/components/SegmentButton/SegmentButton.tsx +28 -3
  74. package/components/Segments/components/SegmentButton/SegmentButton.types.ts +0 -2
  75. package/components/Select/Select.tsx +40 -43
  76. package/components/Separator/Separator.tsx +2 -0
  77. package/components/Separator/components/HRMarkup/HRMarkup.tsx +2 -0
  78. package/components/Separator/components/HorizontalSeparator/HorizontalSeparator.tsx +2 -0
  79. package/components/Separator/components/VerticalSeparator/VerticalSeparator.tsx +2 -0
  80. package/components/Setup/Setup.tsx +3 -0
  81. package/components/Shimmer/Shimmer.tsx +2 -0
  82. package/components/Slider/Slider.tsx +2 -0
  83. package/components/Spacer/Spacer.tsx +2 -0
  84. package/components/Table/Table.tsx +2 -0
  85. package/components/Tabs/Tabs.meta.ts +12 -12
  86. package/components/Tabs/Tabs.module.css +25 -9
  87. package/components/Tabs/Tabs.tsx +49 -53
  88. package/components/Tabs/Tabs.types.ts +10 -3
  89. package/components/Text/Text.tsx +2 -0
  90. package/components/Text/components/Div/Div.tsx +2 -0
  91. package/components/Text/components/Micro/Micro.tsx +2 -0
  92. package/components/Text/components/P/P.tsx +2 -0
  93. package/components/Text/components/Small/Small.tsx +2 -0
  94. package/components/Text/components/Tiny/Tiny.tsx +2 -0
  95. package/components/Textarea/Textarea.tsx +14 -13
  96. package/components/Tile/Tile.tsx +2 -0
  97. package/components/Timestamp/Timestamp.tsx +2 -0
  98. package/components/Toggle/Toggle.tsx +2 -0
  99. package/components/Tooltip/Tooltip.tsx +17 -9
  100. package/components/Tooltip/Tooltip.types.ts +0 -1
  101. package/components/Tooltip/components/TooltipContent/TooltipContent.tsx +2 -0
  102. package/components/Tooltip/components/TooltipTrigger/TooltipTrigger.tsx +4 -2
  103. package/components/shared/InputLabel/InputLabel.tsx +2 -0
  104. package/components/shared/ResultList/ResultList.tsx +6 -4
  105. package/contexts/LayoutContext/LayoutContext.tsx +15 -34
  106. package/contexts/LayoutContext/LayoutContext.types.ts +0 -1
  107. package/hooks/useLayoutContext.tsx +0 -1
  108. package/hooks/useResizeObserver.tsx +2 -2
  109. package/index.ts +5 -0
  110. package/package.json +1 -1
@@ -1,41 +1,22 @@
1
1
  "use client"
2
2
 
3
- import {
4
- type FC,
5
- useState,
6
- useRef,
7
- useEffect,
8
- useCallback,
9
- Children,
10
- isValidElement,
11
- cloneElement,
12
- type ReactElement,
13
- } from "react"
3
+ import { type FC, useState, useRef, useEffect, useCallback } from "react"
14
4
  import { Flex } from "../Flex"
15
5
  import styles from "./Segments.module.css"
16
6
  import { getClasses } from "@heliosgraphics/utils"
17
- import { SegmentButton } from "./components/SegmentButton"
7
+ import { SegmentsContext } from "./Segments.context"
18
8
  import type { SegmentsProps } from "./Segments.types"
19
- import type { SegmentButtonProps } from "./components/SegmentButton/SegmentButton.types"
20
9
 
21
- export const Segments: FC<SegmentsProps> = ({ children, isFullWidth, isSmall }) => {
22
- const validChildren: ReactElement<SegmentButtonProps>[] = Children.toArray(children).filter(
23
- (child): child is ReactElement<SegmentButtonProps> => isValidElement(child) && child.type === SegmentButton,
24
- )
25
-
26
- const firstValue: string = validChildren.length > 0 ? validChildren[0]?.props?.value || "" : ""
27
- const defaultValue: string = ""
28
-
29
- const [activeValue, setActiveValue] = useState<string>(defaultValue || firstValue)
30
- const buttonRefs = useRef<(HTMLButtonElement | null)[]>([])
10
+ export const Segments: FC<SegmentsProps> = ({ children, isFullWidth, isSmall, defaultValue }) => {
11
+ const [activeValue, setActiveValue] = useState<string>(defaultValue ?? "")
12
+ const buttonRefs = useRef<Map<string, HTMLButtonElement>>(new Map())
31
13
  const containerRef = useRef<HTMLDivElement>(null)
32
14
  const sliderRef = useRef<HTMLDivElement>(null)
33
15
 
34
16
  const updateSliderPosition = useCallback((): void => {
35
17
  if (!containerRef.current || !sliderRef.current || !activeValue) return
36
18
 
37
- const activeIndex: number = validChildren.findIndex((child) => child.props.value === activeValue)
38
- const activeButton: HTMLButtonElement | null | undefined = buttonRefs.current[activeIndex]
19
+ const activeButton: HTMLButtonElement | undefined = buttonRefs.current.get(activeValue)
39
20
 
40
21
  if (!activeButton) return
41
22
 
@@ -48,7 +29,7 @@ export const Segments: FC<SegmentsProps> = ({ children, isFullWidth, isSmall })
48
29
  sliderRef.current.style.transform = `translateX(${left}px)`
49
30
  sliderRef.current.style.width = `${width}px`
50
31
  sliderRef.current.style.opacity = "1"
51
- }, [activeValue, validChildren])
32
+ }, [activeValue])
52
33
 
53
34
  useEffect(() => {
54
35
  const timer: ReturnType<typeof setTimeout> = setTimeout(updateSliderPosition, 0)
@@ -61,27 +42,38 @@ export const Segments: FC<SegmentsProps> = ({ children, isFullWidth, isSmall })
61
42
  }
62
43
  }, [updateSliderPosition])
63
44
 
45
+ const onSelect = useCallback((value: string): void => {
46
+ setActiveValue(value)
47
+ }, [])
48
+
49
+ const registerRef = useCallback((value: string, el: HTMLButtonElement | null): void => {
50
+ if (el) {
51
+ buttonRefs.current.set(value, el)
52
+ } else {
53
+ buttonRefs.current.delete(value)
54
+ }
55
+ }, [])
56
+
64
57
  const segmentedControlClasses: string = getClasses(styles.segmentedControl, {
65
58
  [styles.segmentedControlSmall]: isSmall,
66
59
  [styles.segmentedControlFull]: isFullWidth,
67
60
  })
68
61
 
69
62
  return (
70
- <Flex className={segmentedControlClasses} isYCentered={true} gap={2} isInline={!isFullWidth} ref={containerRef}>
71
- {validChildren.map((child: ReactElement<SegmentButtonProps>, index: number) =>
72
- cloneElement<SegmentButtonProps>(child, {
73
- key: child.props.value,
74
- isActive: child.props.value === activeValue,
75
- onClick: () => {
76
- setActiveValue(child.props.value)
77
- child.props.onClick?.()
78
- },
79
- ref: (el: HTMLButtonElement | null) => {
80
- buttonRefs.current[index] = el
81
- },
82
- }),
83
- )}
84
- <div ref={sliderRef} className={styles.segments__slider} />
85
- </Flex>
63
+ <SegmentsContext value={{ activeValue, onSelect, registerRef }}>
64
+ <Flex
65
+ className={segmentedControlClasses}
66
+ isYCentered={true}
67
+ gap={2}
68
+ isInline={!isFullWidth}
69
+ ref={containerRef}
70
+ data-ui-component="Segments"
71
+ >
72
+ {children}
73
+ <div ref={sliderRef} className={styles.segments__slider} />
74
+ </Flex>
75
+ </SegmentsContext>
86
76
  )
87
77
  }
78
+
79
+ Segments.displayName = "Segments"
@@ -2,6 +2,7 @@ import type { SegmentButtonProps } from "./components/SegmentButton/SegmentButto
2
2
  import type { FC, PropsWithChildren, ReactElement } from "react"
3
3
 
4
4
  export interface SegmentsBaseProps {
5
+ defaultValue?: string
5
6
  isFullWidth?: boolean
6
7
  isSmall?: boolean
7
8
  }
@@ -26,10 +26,6 @@ export const meta: HeliosAttributeMeta<SegmentButtonProps> = {
26
26
  type: "() => void",
27
27
  isOptional: true,
28
28
  },
29
- ref: {
30
- type: "Ref<HTMLButtonElement>",
31
- isOptional: true,
32
- },
33
29
  value: {
34
30
  type: "string",
35
31
  },
@@ -1,22 +1,47 @@
1
1
  import styles from "./SegmentButton.module.css"
2
2
  import { getClasses } from "@heliosgraphics/utils"
3
3
  import { Icon } from "../../../Icon"
4
- import type { FC } from "react"
4
+ import { useSegments } from "../../Segments.context"
5
+ import { useCallback, type FC } from "react"
5
6
  import type { HeliosIconType } from "../../../../types/icons"
6
7
  import type { SegmentButtonProps } from "./SegmentButton.types"
7
8
 
8
- export const SegmentButton: FC<SegmentButtonProps> = ({ isActive, value, icon, iconLeft, iconRight, onClick, ref }) => {
9
+ export const SegmentButton: FC<SegmentButtonProps> = ({
10
+ value,
11
+ icon,
12
+ iconLeft,
13
+ iconRight,
14
+ isActive: isActiveProp,
15
+ onClick,
16
+ }) => {
17
+ const { activeValue, onSelect, registerRef } = useSegments()
18
+ const isActive: boolean = isActiveProp ?? value === activeValue
19
+
9
20
  const segmentButtonClasses: string = getClasses(styles.segmentButton, {
10
21
  [styles.segmentButtonActive]: isActive,
11
22
  })
12
23
 
13
24
  const iconL: HeliosIconType | undefined = iconLeft || icon
14
25
 
26
+ const handleClick = useCallback((): void => {
27
+ onSelect(value)
28
+ onClick?.()
29
+ }, [value, onSelect, onClick])
30
+
31
+ const handleRef = useCallback(
32
+ (el: HTMLButtonElement | null): void => {
33
+ registerRef(value, el)
34
+ },
35
+ [value, registerRef],
36
+ )
37
+
15
38
  return (
16
- <button ref={ref} className={segmentButtonClasses} onClick={onClick} data-ui-component="SegmentButton">
39
+ <button ref={handleRef} className={segmentButtonClasses} onClick={handleClick} data-ui-component="SegmentButton">
17
40
  {iconL && <Icon icon={iconL} size={16} />}
18
41
  {value}
19
42
  {iconRight && <Icon icon={iconRight} size={16} />}
20
43
  </button>
21
44
  )
22
45
  }
46
+
47
+ SegmentButton.displayName = "SegmentButton"
@@ -1,4 +1,3 @@
1
- import type { Ref } from "react"
2
1
  import type { HeliosIconType } from "../../../../types/icons"
3
2
 
4
3
  export interface SegmentButtonProps {
@@ -7,6 +6,5 @@ export interface SegmentButtonProps {
7
6
  iconRight?: HeliosIconType
8
7
  isActive?: boolean
9
8
  onClick?: () => void
10
- ref?: Ref<HTMLButtonElement>
11
9
  value: string
12
10
  }
@@ -3,54 +3,51 @@
3
3
  import { Flex } from "../Flex"
4
4
  import { Icon } from "../Icon"
5
5
  import { getClasses } from "@heliosgraphics/utils"
6
- import { useId, type FC } from "react"
6
+ import { useId, forwardRef } from "react"
7
7
  import { InputLabel } from "../shared/InputLabel"
8
8
  import styles from "./Select.module.css"
9
9
  import type { SelectProps } from "./Select.types"
10
10
 
11
- export const Select: FC<SelectProps> = ({
12
- selectedValue,
13
- onChange,
14
- isLabelHidden,
15
- isSmall,
16
- isDisabled,
17
- isRequired,
18
- items,
19
- label,
20
- }) => {
21
- const htmlFor: string = useId()
11
+ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
12
+ ({ selectedValue, onChange, isLabelHidden, isSmall, isDisabled, isRequired, items, label }, ref) => {
13
+ const htmlFor: string = useId()
22
14
 
23
- const selectClasses: string = getClasses(styles.select, {
24
- [styles.selectDisabled]: isDisabled,
25
- [styles.selectSmall]: isSmall,
26
- [styles.selectHiddenLabel]: !label || isLabelHidden,
27
- })
15
+ const selectClasses: string = getClasses(styles.select, {
16
+ [styles.selectDisabled]: isDisabled,
17
+ [styles.selectSmall]: isSmall,
18
+ [styles.selectHiddenLabel]: !label || isLabelHidden,
19
+ })
28
20
 
29
- return (
30
- <Flex isColumn={true} className={selectClasses} data-ui-component="Select">
31
- <InputLabel label={label} id={htmlFor} isHidden={!!isLabelHidden} isDisabled={!!isDisabled} />
32
- <Flex>
33
- <select
34
- className={styles.select__select}
35
- onChange={onChange}
36
- id={htmlFor}
37
- value={selectedValue}
38
- disabled={isDisabled}
39
- required={isRequired}
40
- aria-label={isLabelHidden ? label : undefined}
41
- >
42
- {items?.map((item) => {
43
- return (
44
- <option key={item.value} value={item.value} disabled={item.isDisabled}>
45
- {item.name}
46
- </option>
47
- )
48
- })}
49
- </select>
50
- <Flex className={styles.select__icon} isCentered={true}>
51
- <Icon icon="chevron-down" size={20} />
21
+ return (
22
+ <Flex isColumn={true} className={selectClasses} data-ui-component="Select">
23
+ <InputLabel label={label} id={htmlFor} isHidden={!!isLabelHidden} isDisabled={!!isDisabled} />
24
+ <Flex>
25
+ <select
26
+ ref={ref}
27
+ className={styles.select__select}
28
+ onChange={onChange}
29
+ id={htmlFor}
30
+ value={selectedValue}
31
+ disabled={isDisabled}
32
+ required={isRequired}
33
+ aria-label={isLabelHidden ? label : undefined}
34
+ aria-required={isRequired || undefined}
35
+ >
36
+ {items?.map((item) => {
37
+ return (
38
+ <option key={item.value} value={item.value} disabled={item.isDisabled}>
39
+ {item.name}
40
+ </option>
41
+ )
42
+ })}
43
+ </select>
44
+ <Flex className={styles.select__icon} isCentered={true}>
45
+ <Icon icon="chevron-down" size={20} />
46
+ </Flex>
52
47
  </Flex>
53
48
  </Flex>
54
- </Flex>
55
- )
56
- }
49
+ )
50
+ },
51
+ )
52
+
53
+ Select.displayName = "Select"
@@ -32,3 +32,5 @@ export const Separator: FC<SeparatorProps> = ({
32
32
 
33
33
  return <HorizontalSeparator {...separatorProps} />
34
34
  }
35
+
36
+ Separator.displayName = "Separator"
@@ -6,3 +6,5 @@ export const HRMarkup: FC<HRMarkupProps> = (props) => {
6
6
 
7
7
  return <hr {...goodProps} data-ui-component="Separator" />
8
8
  }
9
+
10
+ HRMarkup.displayName = "HRMarkup"
@@ -18,3 +18,5 @@ export const HorizontalSeparator: FC<HorizontalSeparatorProps> = ({ lineStyle, c
18
18
 
19
19
  return <HRMarkup className={hrClassNames} style={horizontalStyle} />
20
20
  }
21
+
22
+ HorizontalSeparator.displayName = "HorizontalSeparator"
@@ -18,3 +18,5 @@ export const VerticalSeparator: FC<VerticalSeparatorProps> = ({ height, classNam
18
18
 
19
19
  return <HRMarkup style={verticalStyle} className={hrClassNames} aria-orientation="vertical" />
20
20
  }
21
+
22
+ VerticalSeparator.displayName = "VerticalSeparator"
@@ -41,6 +41,7 @@ export const Setup: FC<SetupProps> = ({ theme = "system", fixedTheme }) => {
41
41
  <link key={`stylesheet-${linkProps.id}`} {...linkProps} />
42
42
  ))}
43
43
  <script
44
+ data-ui-component="Setup"
44
45
  dangerouslySetInnerHTML={{
45
46
  __html: `(${code.toString()})("${theme}", ${fixedThemeArg});`,
46
47
  }}
@@ -48,3 +49,5 @@ export const Setup: FC<SetupProps> = ({ theme = "system", fixedTheme }) => {
48
49
  </>
49
50
  )
50
51
  }
52
+
53
+ Setup.displayName = "Setup"
@@ -27,3 +27,5 @@ export const Shimmer: FC<ShimmerProps> = ({ isRounded, paddingTop, paddingBottom
27
27
  </div>
28
28
  )
29
29
  }
30
+
31
+ Shimmer.displayName = "Shimmer"
@@ -14,3 +14,5 @@ export const Slider: FC<SliderProps> = ({ "aria-label": ariaLabel, items }) => {
14
14
  </ul>
15
15
  )
16
16
  }
17
+
18
+ Slider.displayName = "Slider"
@@ -4,3 +4,5 @@ import type { SpacerProps } from "./Spacer.types"
4
4
  export const Spacer: FC<SpacerProps> = ({ gap }) => {
5
5
  return <div style={{ height: `${gap ?? 0}px` }} data-ui-component="Spacer" />
6
6
  }
7
+
8
+ Spacer.displayName = "Spacer"
@@ -17,3 +17,5 @@ export const Table: FC<TableProps> = ({ children, hasBorder, isMonoHeader }) =>
17
17
  </div>
18
18
  )
19
19
  }
20
+
21
+ Table.displayName = "Table"
@@ -2,23 +2,23 @@ import type { HeliosAttributeMeta } from "../../types/meta"
2
2
  import type { TabsProps } from "./Tabs.types"
3
3
 
4
4
  export const meta: HeliosAttributeMeta<TabsProps> = {
5
+ _extends: [],
5
6
  _patterns: [
6
7
  {
7
8
  id: "ui-tabs-default",
8
9
  description: "default",
9
- content: `<Tabs items={TAB_ITEMS} sections={TAB_SECTIONS}/>`,
10
+ content:
11
+ '<Tabs items={[{ name: "Tab 1", isActive: true, onClick: () => {} }, { name: "Tab 2", onClick: () => {} }]}>{CHILDREN}</Tabs>',
12
+ },
13
+ {
14
+ id: "ui-tabs-with-href",
15
+ description: "with links",
16
+ content:
17
+ '<Tabs items={[{ name: "Overview", href: "/overview", isActive: true }, { name: "Settings", href: "/settings" }]} />',
10
18
  },
11
19
  ],
12
20
  _status: "experimental",
13
- _category: "layout",
14
- active: {
15
- type: "number",
16
- isOptional: true,
17
- },
18
- items: {
19
- type: "Array<string>",
20
- },
21
- sections: {
22
- type: "Array<ReactNode>",
23
- },
21
+ _category: "core",
22
+ children: { type: "ReactNode", isOptional: true },
23
+ items: { type: "Array<TabItem>", description: "Tab items with name, onClick, href, isActive, and isDisabled" },
24
24
  }
@@ -1,27 +1,43 @@
1
- .tabs__ol {
1
+ .tabs__list {
2
2
  position: relative;
3
+ display: flex;
4
+ gap: 2px;
3
5
 
4
- height: 36px;
6
+ height: 40px;
5
7
  padding: 0 8px 0 0;
6
8
 
7
9
  box-shadow: inset 0 -1px 0 0 var(--ui-border-secondary);
8
-
9
- line-height: 36px;
10
+ line-height: 40px;
10
11
  }
11
12
 
12
- .tabs__ol__item {
13
+ .tabs__item {
14
+ display: flex;
15
+ align-items: center;
13
16
  padding: 0 12px;
14
17
 
15
18
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
16
- border: 1px solid var(--ui-border-secondary);
19
+ color: var(--ui-text-secondary);
20
+ text-decoration: none;
17
21
 
18
22
  cursor: pointer;
23
+ user-select: none;
24
+ }
25
+
26
+ .tabs__item:hover {
27
+ color: var(--ui-text-primary);
28
+ }
29
+
30
+ .tabs__itemActive {
31
+ color: var(--ui-text-primary);
32
+ box-shadow: inset 0 -2px 0 0 var(--ui-text-primary);
19
33
  }
20
34
 
21
- .tabs__ol__itemActive {
22
- border-bottom-color: var(--ui-bg-secondary);
35
+ .tabs__itemDisabled {
36
+ opacity: 0.4;
37
+ cursor: not-allowed;
38
+ pointer-events: none;
23
39
  }
24
40
 
25
- .tabs__section {
41
+ .tabs__content {
26
42
  padding: 24px 0;
27
43
  }
@@ -1,69 +1,65 @@
1
1
  "use client"
2
2
 
3
3
  import { getClasses } from "@heliosgraphics/utils"
4
- import { useId, useRef, useState, type FC, type KeyboardEvent } from "react"
5
- import type { TabsProps } from "./Tabs.types"
6
- import styles from "./Tabs.module.css"
7
4
  import { Text } from "../Text"
5
+ import styles from "./Tabs.module.css"
6
+ import type { FC, KeyboardEvent } from "react"
7
+ import type { TabItem, TabsProps } from "./Tabs.types"
8
8
 
9
- export const Tabs: FC<TabsProps> = ({ active: activeNumber, items, sections }) => {
10
- const [active, setActive] = useState(activeNumber || 0)
11
- const tabsId: string = useId()
12
- const tabRefs = useRef<(HTMLDivElement | null)[]>([])
13
-
14
- if (!items || !sections) return null
15
-
16
- const tabListClasses: string = getClasses(styles.tabs__ol, "flex gap-2")
17
-
18
- const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>, index: number): void => {
19
- let nextIndex: number | null = null
9
+ export const Tabs: FC<TabsProps> = ({ items, children }) => {
10
+ if (!items?.length) return null
20
11
 
21
- if (event.key === "ArrowRight") {
22
- event.preventDefault()
23
- nextIndex = (index + 1) % items.length
24
- } else if (event.key === "ArrowLeft") {
12
+ const onKeyDown = (event: KeyboardEvent<HTMLElement>, item: TabItem): void => {
13
+ if (event.key === " " || event.key === "Enter") {
25
14
  event.preventDefault()
26
- nextIndex = (index - 1 + items.length) % items.length
27
- }
28
-
29
- if (nextIndex !== null) {
30
- setActive(nextIndex)
31
- tabRefs.current[nextIndex]?.focus()
15
+ item.onClick?.()
32
16
  }
33
17
  }
34
18
 
35
19
  return (
36
20
  <div data-ui-component="Tabs">
37
- <div role="tablist" className={tabListClasses}>
38
- {items.map((tab, index) => (
39
- <div
40
- key={tab}
41
- ref={(el) => {
42
- tabRefs.current[index] = el
43
- }}
44
- role="tab"
45
- tabIndex={active === index ? 0 : -1}
46
- aria-selected={active === index}
47
- aria-controls={`${tabsId}-panel-${index}`}
48
- id={`${tabsId}-tab-${index}`}
49
- onClick={() => setActive(index)}
50
- onKeyDown={(event) => handleKeyDown(event, index)}
51
- className={getClasses(styles.tabs__ol__item, { [styles.tabs__ol__itemActive]: active === index })}
52
- >
53
- <Text type="small" fontWeight="medium">
54
- {tab}
55
- </Text>
56
- </div>
57
- ))}
21
+ <div role="tablist" className={styles.tabs__list}>
22
+ {items.map((item: TabItem, index: number) => {
23
+ const itemClasses: string = getClasses(styles.tabs__item, {
24
+ [styles.tabs__itemActive]: !!item.isActive,
25
+ [styles.tabs__itemDisabled]: !!item.isDisabled,
26
+ })
27
+
28
+ const commonProps = {
29
+ key: index,
30
+ role: "tab" as const,
31
+ tabIndex: item.isDisabled ? -1 : 0,
32
+ "aria-selected": !!item.isActive,
33
+ "aria-disabled": !!item.isDisabled,
34
+ className: itemClasses,
35
+ }
36
+
37
+ if (item.href) {
38
+ return (
39
+ <a {...commonProps} href={item.href}>
40
+ <Text type="small" fontWeight="medium">
41
+ {item.name}
42
+ </Text>
43
+ </a>
44
+ )
45
+ }
46
+
47
+ return (
48
+ <div
49
+ {...commonProps}
50
+ onClick={item.isDisabled ? undefined : item.onClick}
51
+ onKeyDown={(e) => onKeyDown(e, item)}
52
+ >
53
+ <Text type="small" fontWeight="medium">
54
+ {item.name}
55
+ </Text>
56
+ </div>
57
+ )
58
+ })}
58
59
  </div>
59
- <section
60
- role="tabpanel"
61
- id={`${tabsId}-panel-${active}`}
62
- aria-labelledby={`${tabsId}-tab-${active}`}
63
- className="tabs__section"
64
- >
65
- {sections[active]}
66
- </section>
60
+ {children && <section className={styles.tabs__content}>{children}</section>}
67
61
  </div>
68
62
  )
69
63
  }
64
+
65
+ Tabs.displayName = "Tabs"
@@ -1,7 +1,14 @@
1
1
  import type { ReactNode } from "react"
2
2
 
3
+ export interface TabItem {
4
+ href?: string
5
+ isActive?: boolean
6
+ isDisabled?: boolean
7
+ name: string
8
+ onClick?: () => void
9
+ }
10
+
3
11
  export interface TabsProps {
4
- active?: number
5
- items: Array<string>
6
- sections: Array<ReactNode>
12
+ children?: ReactNode
13
+ items: Array<TabItem>
7
14
  }
@@ -55,3 +55,5 @@ export const Text: FC<TextProps> = (props) => {
55
55
  return <Div {...baseTextProps} />
56
56
  }
57
57
  }
58
+
59
+ Text.displayName = "Text"
@@ -5,3 +5,5 @@ import type { FC } from "react"
5
5
  export const Div: FC<DivProps> = (props) => {
6
6
  return <div {...props} className={getClasses("p", props.className)} data-ui-component="Text.Div" />
7
7
  }
8
+
9
+ Div.displayName = "Div"
@@ -5,3 +5,5 @@ import type { MicroProps } from "./Micro.types"
5
5
  export const Micro: FC<MicroProps> = (props) => {
6
6
  return <small {...props} className={getClasses("micro", props.className)} data-ui-component="Text.Micro" />
7
7
  }
8
+
9
+ Micro.displayName = "Micro"
@@ -5,3 +5,5 @@ import type { PProps } from "./P.types"
5
5
  export const P: FC<PProps> = (props) => {
6
6
  return <p {...props} className={getClasses("p", props.className)} data-ui-component="Text.P" />
7
7
  }
8
+
9
+ P.displayName = "P"
@@ -5,3 +5,5 @@ import type { SmallProps } from "./Small.types"
5
5
  export const Small: FC<SmallProps> = (props) => {
6
6
  return <small {...props} className={getClasses("small", props.className)} data-ui-component="Text.Small" />
7
7
  }
8
+
9
+ Small.displayName = "Small"
@@ -5,3 +5,5 @@ import type { TinyProps } from "./Tiny.types"
5
5
  export const Tiny: FC<TinyProps> = (props) => {
6
6
  return <small {...props} className={getClasses("tiny", props.className)} data-ui-component="Text.Tiny" />
7
7
  }
8
+
9
+ Tiny.displayName = "Tiny"