@refraktor/core 0.0.2 → 0.0.3

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 (217) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/build/components/breadcrumbs/breadcrumbs.d.ts +4 -0
  3. package/build/components/breadcrumbs/breadcrumbs.d.ts.map +1 -0
  4. package/build/components/breadcrumbs/breadcrumbs.js +64 -0
  5. package/build/components/breadcrumbs/breadcrumbs.styles.d.ts +12 -0
  6. package/build/components/breadcrumbs/breadcrumbs.styles.d.ts.map +1 -0
  7. package/build/components/breadcrumbs/breadcrumbs.styles.js +43 -0
  8. package/build/components/breadcrumbs/breadcrumbs.test.d.ts +2 -0
  9. package/build/components/breadcrumbs/breadcrumbs.test.d.ts.map +1 -0
  10. package/build/components/breadcrumbs/breadcrumbs.test.js +72 -0
  11. package/build/components/breadcrumbs/breadcrumbs.types.d.ts +56 -0
  12. package/build/components/breadcrumbs/breadcrumbs.types.d.ts.map +1 -0
  13. package/build/components/breadcrumbs/breadcrumbs.types.js +1 -0
  14. package/build/components/breadcrumbs/breadcrumbs.utils.d.ts +10 -0
  15. package/build/components/breadcrumbs/breadcrumbs.utils.d.ts.map +1 -0
  16. package/build/components/breadcrumbs/breadcrumbs.utils.js +36 -0
  17. package/build/components/breadcrumbs/index.d.ts +3 -0
  18. package/build/components/breadcrumbs/index.d.ts.map +1 -0
  19. package/build/components/breadcrumbs/index.js +1 -0
  20. package/build/components/drawer/drawer-close/drawer-close.d.ts +4 -0
  21. package/build/components/drawer/drawer-close/drawer-close.d.ts.map +1 -0
  22. package/build/components/drawer/drawer-close/drawer-close.js +19 -0
  23. package/build/components/drawer/drawer-close/index.d.ts +2 -0
  24. package/build/components/drawer/drawer-close/index.d.ts.map +1 -0
  25. package/build/components/drawer/drawer-close/index.js +1 -0
  26. package/build/components/drawer/drawer-content/drawer-content.d.ts +4 -0
  27. package/build/components/drawer/drawer-content/drawer-content.d.ts.map +1 -0
  28. package/build/components/drawer/drawer-content/drawer-content.js +41 -0
  29. package/build/components/drawer/drawer-content/index.d.ts +2 -0
  30. package/build/components/drawer/drawer-content/index.d.ts.map +1 -0
  31. package/build/components/drawer/drawer-content/index.js +1 -0
  32. package/build/components/drawer/drawer-header/drawer-header.d.ts +4 -0
  33. package/build/components/drawer/drawer-header/drawer-header.d.ts.map +1 -0
  34. package/build/components/drawer/drawer-header/drawer-header.js +13 -0
  35. package/build/components/drawer/drawer-header/index.d.ts +2 -0
  36. package/build/components/drawer/drawer-header/index.d.ts.map +1 -0
  37. package/build/components/drawer/drawer-header/index.js +1 -0
  38. package/build/components/drawer/drawer-overlay/drawer-overlay.d.ts +4 -0
  39. package/build/components/drawer/drawer-overlay/drawer-overlay.d.ts.map +1 -0
  40. package/build/components/drawer/drawer-overlay/drawer-overlay.js +31 -0
  41. package/build/components/drawer/drawer-overlay/index.d.ts +2 -0
  42. package/build/components/drawer/drawer-overlay/index.d.ts.map +1 -0
  43. package/build/components/drawer/drawer-overlay/index.js +1 -0
  44. package/build/components/drawer/drawer-root/drawer-root.d.ts +4 -0
  45. package/build/components/drawer/drawer-root/drawer-root.d.ts.map +1 -0
  46. package/build/components/drawer/drawer-root/drawer-root.js +50 -0
  47. package/build/components/drawer/drawer-root/index.d.ts +2 -0
  48. package/build/components/drawer/drawer-root/index.d.ts.map +1 -0
  49. package/build/components/drawer/drawer-root/index.js +1 -0
  50. package/build/components/drawer/drawer.context.d.ts +23 -0
  51. package/build/components/drawer/drawer.context.d.ts.map +1 -0
  52. package/build/components/drawer/drawer.context.js +2 -0
  53. package/build/components/drawer/drawer.d.ts +4 -0
  54. package/build/components/drawer/drawer.d.ts.map +1 -0
  55. package/build/components/drawer/drawer.js +19 -0
  56. package/build/components/drawer/drawer.styles.d.ts +6 -0
  57. package/build/components/drawer/drawer.styles.d.ts.map +1 -0
  58. package/build/components/drawer/drawer.styles.js +21 -0
  59. package/build/components/drawer/drawer.test.d.ts +2 -0
  60. package/build/components/drawer/drawer.test.d.ts.map +1 -0
  61. package/build/components/drawer/drawer.test.js +70 -0
  62. package/build/components/drawer/drawer.types.d.ts +116 -0
  63. package/build/components/drawer/drawer.types.d.ts.map +1 -0
  64. package/build/components/drawer/drawer.types.js +1 -0
  65. package/build/components/drawer/index.d.ts +8 -0
  66. package/build/components/drawer/index.d.ts.map +1 -0
  67. package/build/components/drawer/index.js +6 -0
  68. package/build/components/drawer/use-drawer.d.ts +17 -0
  69. package/build/components/drawer/use-drawer.d.ts.map +1 -0
  70. package/build/components/drawer/use-drawer.js +61 -0
  71. package/build/components/index.d.ts +4 -0
  72. package/build/components/index.d.ts.map +1 -1
  73. package/build/components/index.js +4 -0
  74. package/build/components/modal/modal-overlay/modal-overlay.d.ts.map +1 -1
  75. package/build/components/modal/modal-overlay/modal-overlay.js +10 -2
  76. package/build/components/modal/modal.test.js +16 -0
  77. package/build/components/modal/modal.types.d.ts +4 -0
  78. package/build/components/modal/modal.types.d.ts.map +1 -1
  79. package/build/components/segmented-control/index.d.ts +3 -0
  80. package/build/components/segmented-control/index.d.ts.map +1 -0
  81. package/build/components/segmented-control/index.js +1 -0
  82. package/build/components/segmented-control/segmented-control.d.ts +4 -0
  83. package/build/components/segmented-control/segmented-control.d.ts.map +1 -0
  84. package/build/components/segmented-control/segmented-control.js +113 -0
  85. package/build/components/segmented-control/segmented-control.styles.d.ts +9 -0
  86. package/build/components/segmented-control/segmented-control.styles.d.ts.map +1 -0
  87. package/build/components/segmented-control/segmented-control.styles.js +28 -0
  88. package/build/components/segmented-control/segmented-control.test.d.ts +2 -0
  89. package/build/components/segmented-control/segmented-control.test.d.ts.map +1 -0
  90. package/build/components/segmented-control/segmented-control.test.js +81 -0
  91. package/build/components/segmented-control/segmented-control.types.d.ts +49 -0
  92. package/build/components/segmented-control/segmented-control.types.d.ts.map +1 -0
  93. package/build/components/segmented-control/segmented-control.types.js +1 -0
  94. package/build/components/select/select-item/select-item.js +1 -1
  95. package/build/components/split-pane/index.d.ts +3 -0
  96. package/build/components/split-pane/index.d.ts.map +1 -0
  97. package/build/components/split-pane/index.js +1 -0
  98. package/build/components/split-pane/split-pane.d.ts +4 -0
  99. package/build/components/split-pane/split-pane.d.ts.map +1 -0
  100. package/build/components/split-pane/split-pane.js +201 -0
  101. package/build/components/split-pane/split-pane.styles.d.ts +3 -0
  102. package/build/components/split-pane/split-pane.styles.d.ts.map +1 -0
  103. package/build/components/split-pane/split-pane.styles.js +8 -0
  104. package/build/components/split-pane/split-pane.test.d.ts +2 -0
  105. package/build/components/split-pane/split-pane.test.d.ts.map +1 -0
  106. package/build/components/split-pane/split-pane.test.js +105 -0
  107. package/build/components/split-pane/split-pane.types.d.ts +51 -0
  108. package/build/components/split-pane/split-pane.types.d.ts.map +1 -0
  109. package/build/components/split-pane/split-pane.types.js +1 -0
  110. package/build/components/table/index.d.ts +9 -0
  111. package/build/components/table/index.d.ts.map +1 -0
  112. package/build/components/table/index.js +7 -0
  113. package/build/components/table/table-body/index.d.ts +2 -0
  114. package/build/components/table/table-body/index.d.ts.map +1 -0
  115. package/build/components/table/table-body/index.js +1 -0
  116. package/build/components/table/table-body/table-body.d.ts +4 -0
  117. package/build/components/table/table-body/table-body.d.ts.map +1 -0
  118. package/build/components/table/table-body/table-body.js +17 -0
  119. package/build/components/table/table-caption/index.d.ts +2 -0
  120. package/build/components/table/table-caption/index.d.ts.map +1 -0
  121. package/build/components/table/table-caption/index.js +1 -0
  122. package/build/components/table/table-caption/table-caption.d.ts +4 -0
  123. package/build/components/table/table-caption/table-caption.d.ts.map +1 -0
  124. package/build/components/table/table-caption/table-caption.js +13 -0
  125. package/build/components/table/table-cell/index.d.ts +2 -0
  126. package/build/components/table/table-cell/index.d.ts.map +1 -0
  127. package/build/components/table/table-cell/index.js +1 -0
  128. package/build/components/table/table-cell/table-cell.d.ts +4 -0
  129. package/build/components/table/table-cell/table-cell.d.ts.map +1 -0
  130. package/build/components/table/table-cell/table-cell.js +13 -0
  131. package/build/components/table/table-head/index.d.ts +2 -0
  132. package/build/components/table/table-head/index.d.ts.map +1 -0
  133. package/build/components/table/table-head/index.js +1 -0
  134. package/build/components/table/table-head/table-head.d.ts +4 -0
  135. package/build/components/table/table-head/table-head.d.ts.map +1 -0
  136. package/build/components/table/table-head/table-head.js +11 -0
  137. package/build/components/table/table-header-cell/index.d.ts +2 -0
  138. package/build/components/table/table-header-cell/index.d.ts.map +1 -0
  139. package/build/components/table/table-header-cell/index.js +1 -0
  140. package/build/components/table/table-header-cell/table-header-cell.d.ts +4 -0
  141. package/build/components/table/table-header-cell/table-header-cell.d.ts.map +1 -0
  142. package/build/components/table/table-header-cell/table-header-cell.js +13 -0
  143. package/build/components/table/table-row/index.d.ts +2 -0
  144. package/build/components/table/table-row/index.d.ts.map +1 -0
  145. package/build/components/table/table-row/index.js +1 -0
  146. package/build/components/table/table-row/table-row.d.ts +4 -0
  147. package/build/components/table/table-row/table-row.d.ts.map +1 -0
  148. package/build/components/table/table-row/table-row.js +11 -0
  149. package/build/components/table/table.context.d.ts +16 -0
  150. package/build/components/table/table.context.d.ts.map +1 -0
  151. package/build/components/table/table.context.js +2 -0
  152. package/build/components/table/table.d.ts +4 -0
  153. package/build/components/table/table.d.ts.map +1 -0
  154. package/build/components/table/table.js +46 -0
  155. package/build/components/table/table.styles.d.ts +16 -0
  156. package/build/components/table/table.styles.d.ts.map +1 -0
  157. package/build/components/table/table.styles.js +39 -0
  158. package/build/components/table/table.test.d.ts +2 -0
  159. package/build/components/table/table.test.d.ts.map +1 -0
  160. package/build/components/table/table.test.js +59 -0
  161. package/build/components/table/table.types.d.ts +113 -0
  162. package/build/components/table/table.types.d.ts.map +1 -0
  163. package/build/components/table/table.types.js +1 -0
  164. package/build/style.css +1 -1
  165. package/package.json +2 -2
  166. package/src/components/breadcrumbs/breadcrumbs.styles.ts +55 -0
  167. package/src/components/breadcrumbs/breadcrumbs.test.tsx +136 -0
  168. package/src/components/breadcrumbs/breadcrumbs.tsx +199 -0
  169. package/src/components/breadcrumbs/breadcrumbs.types.ts +78 -0
  170. package/src/components/breadcrumbs/breadcrumbs.utils.ts +70 -0
  171. package/src/components/breadcrumbs/index.ts +6 -0
  172. package/src/components/drawer/drawer-close/drawer-close.tsx +43 -0
  173. package/src/components/drawer/drawer-close/index.ts +1 -0
  174. package/src/components/drawer/drawer-content/drawer-content.tsx +98 -0
  175. package/src/components/drawer/drawer-content/index.ts +1 -0
  176. package/src/components/drawer/drawer-header/drawer-header.tsx +40 -0
  177. package/src/components/drawer/drawer-header/index.ts +1 -0
  178. package/src/components/drawer/drawer-overlay/drawer-overlay.tsx +86 -0
  179. package/src/components/drawer/drawer-overlay/index.ts +1 -0
  180. package/src/components/drawer/drawer-root/drawer-root.tsx +94 -0
  181. package/src/components/drawer/drawer-root/index.ts +1 -0
  182. package/src/components/drawer/drawer.context.ts +25 -0
  183. package/src/components/drawer/drawer.styles.ts +32 -0
  184. package/src/components/drawer/drawer.test.tsx +166 -0
  185. package/src/components/drawer/drawer.tsx +30 -0
  186. package/src/components/drawer/drawer.types.ts +158 -0
  187. package/src/components/drawer/index.ts +16 -0
  188. package/src/components/drawer/use-drawer.ts +101 -0
  189. package/src/components/index.ts +10 -6
  190. package/src/components/modal/modal-overlay/modal-overlay.tsx +43 -21
  191. package/src/components/modal/modal.test.tsx +47 -11
  192. package/src/components/modal/modal.types.ts +6 -0
  193. package/src/components/segmented-control/index.ts +6 -0
  194. package/src/components/segmented-control/segmented-control.styles.ts +37 -0
  195. package/src/components/segmented-control/segmented-control.test.tsx +152 -0
  196. package/src/components/segmented-control/segmented-control.tsx +245 -0
  197. package/src/components/segmented-control/segmented-control.types.ts +75 -0
  198. package/src/components/select/select-item/select-item.tsx +1 -1
  199. package/src/components/table/index.ts +24 -0
  200. package/src/components/table/table-body/index.ts +1 -0
  201. package/src/components/table/table-body/table-body.tsx +37 -0
  202. package/src/components/table/table-caption/index.ts +1 -0
  203. package/src/components/table/table-caption/table-caption.tsx +32 -0
  204. package/src/components/table/table-cell/index.ts +1 -0
  205. package/src/components/table/table-cell/table-cell.tsx +33 -0
  206. package/src/components/table/table-head/index.ts +1 -0
  207. package/src/components/table/table-head/table-head.tsx +29 -0
  208. package/src/components/table/table-header-cell/index.ts +1 -0
  209. package/src/components/table/table-header-cell/table-header-cell.tsx +33 -0
  210. package/src/components/table/table-row/index.ts +1 -0
  211. package/src/components/table/table-row/table-row.tsx +30 -0
  212. package/src/components/table/table.context.ts +18 -0
  213. package/src/components/table/table.styles.ts +62 -0
  214. package/src/components/table/table.test.tsx +145 -0
  215. package/src/components/table/table.tsx +91 -0
  216. package/src/components/table/table.types.ts +145 -0
  217. package/tsconfig.tsbuildinfo +1 -1
@@ -5,16 +5,33 @@ import { Transition } from "../../transition";
5
5
  import { useModalContext } from "../modal.context";
6
6
  import { ModalOverlayFactoryPayload } from "../modal.types";
7
7
 
8
- const ModalOverlay = factory<ModalOverlayFactoryPayload>(
9
- ({ closeOnClick = true, className, onMouseDown, ...props }, ref) => {
10
- const { cx } = useTheme();
11
- const {
12
- modal,
13
- closeOnClickOutside,
14
- withinPortal,
15
- transitionProps,
16
- getStyles
17
- } = useModalContext();
8
+ const ModalOverlay = factory<ModalOverlayFactoryPayload>(
9
+ (
10
+ {
11
+ closeOnClick = true,
12
+ backgroundOpacity = 0.5,
13
+ blur = 0,
14
+ className,
15
+ onMouseDown,
16
+ style,
17
+ ...props
18
+ },
19
+ ref
20
+ ) => {
21
+ const { cx } = useTheme();
22
+ const {
23
+ modal,
24
+ closeOnClickOutside,
25
+ withinPortal,
26
+ transitionProps,
27
+ getStyles
28
+ } = useModalContext();
29
+
30
+ const blurValue = typeof blur === "number" ? `${blur}px` : blur;
31
+ const backdropFilterValue =
32
+ blurValue !== "0" && blurValue !== "0px"
33
+ ? `blur(${blurValue})`
34
+ : undefined;
18
35
 
19
36
  const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
20
37
  onMouseDown?.(event);
@@ -37,17 +54,22 @@ const ModalOverlay = factory<ModalOverlayFactoryPayload>(
37
54
  mounted={modal.opened}
38
55
  {...transitionProps}
39
56
  >
40
- <div
41
- ref={ref}
42
- aria-hidden="true"
43
- className={cx(
44
- "fixed inset-0 z-40 bg-gradient-to-b from-black/60 via-black/45 to-black/65 backdrop-blur-[2px]",
45
- getStyles("overlay"),
46
- className
47
- )}
48
- onMouseDown={handleMouseDown}
49
- {...props}
50
- />
57
+ <div
58
+ ref={ref}
59
+ aria-hidden="true"
60
+ className={cx(
61
+ "fixed inset-0 z-40",
62
+ getStyles("overlay"),
63
+ className
64
+ )}
65
+ style={{
66
+ backgroundColor: `rgba(0, 0, 0, ${backgroundOpacity})`,
67
+ backdropFilter: backdropFilterValue,
68
+ ...style
69
+ }}
70
+ onMouseDown={handleMouseDown}
71
+ {...props}
72
+ />
51
73
  </Transition>
52
74
  );
53
75
 
@@ -74,19 +74,55 @@ describe("@refraktor/core/Modal", () => {
74
74
  });
75
75
  });
76
76
 
77
- it("supports standalone subcomponents with ModalRoot", async () => {
78
- await render(
79
- <ModalRoot defaultOpened transitionProps={transitionProps}>
80
- <ModalOverlay />
81
- <ModalContent>Standalone composition</ModalContent>
77
+ it("supports standalone subcomponents with ModalRoot", async () => {
78
+ await render(
79
+ <ModalRoot defaultOpened transitionProps={transitionProps}>
80
+ <ModalOverlay />
81
+ <ModalContent>Standalone composition</ModalContent>
82
82
  </ModalRoot>
83
83
  );
84
-
85
- expect(await screen.findByRole("dialog")).toBeInTheDocument();
86
- });
87
-
88
- it("locks and unlocks body scroll when enabled", async () => {
89
- const user = userEvent.setup();
84
+
85
+ expect(await screen.findByRole("dialog")).toBeInTheDocument();
86
+ });
87
+
88
+ it("applies custom overlay background opacity and blur", async () => {
89
+ await render(
90
+ <Modal defaultOpened transitionProps={transitionProps}>
91
+ <Modal.Overlay
92
+ data-testid="overlay"
93
+ backgroundOpacity={0.4}
94
+ blur={6}
95
+ />
96
+ <Modal.Content>Styled overlay</Modal.Content>
97
+ </Modal>
98
+ );
99
+
100
+ const overlay = await screen.findByTestId("overlay");
101
+
102
+ expect(overlay).toHaveStyle({
103
+ backgroundColor: "rgba(0, 0, 0, 0.4)",
104
+ backdropFilter: "blur(6px)"
105
+ });
106
+ });
107
+
108
+ it("does not set backdrop blur for zero blur", async () => {
109
+ await render(
110
+ <Modal defaultOpened transitionProps={transitionProps}>
111
+ <Modal.Overlay data-testid="overlay" blur={0} />
112
+ <Modal.Content>No blur</Modal.Content>
113
+ </Modal>
114
+ );
115
+
116
+ const overlay = await screen.findByTestId("overlay");
117
+
118
+ expect(overlay).toHaveStyle({
119
+ backgroundColor: "rgba(0, 0, 0, 0.5)"
120
+ });
121
+ expect(overlay.style.backdropFilter).toBe("");
122
+ });
123
+
124
+ it("locks and unlocks body scroll when enabled", async () => {
125
+ const user = userEvent.setup();
90
126
 
91
127
  await render(
92
128
  <Modal defaultOpened lockScroll transitionProps={transitionProps}>
@@ -64,6 +64,12 @@ export interface ModalOverlayProps extends ComponentPropsWithoutRef<"div"> {
64
64
  /** Whether clicking the overlay closes modal @default `true` */
65
65
  closeOnClick?: boolean;
66
66
 
67
+ /** Overlay background opacity @default `0.5` */
68
+ backgroundOpacity?: number;
69
+
70
+ /** Backdrop blur amount in px (or any CSS length) @default `0` */
71
+ blur?: number | string;
72
+
67
73
  /** Used for editing root class name */
68
74
  className?: string;
69
75
  }
@@ -0,0 +1,6 @@
1
+ export { default as SegmentedControl } from "./segmented-control";
2
+ export type {
3
+ SegmentedControlProps,
4
+ SegmentedControlClassNames,
5
+ SegmentedControlItem
6
+ } from "./segmented-control.types";
@@ -0,0 +1,37 @@
1
+ import { RefraktorSize } from "../../theme";
2
+
3
+ type SegmentedControlSizeStyles = {
4
+ root: string;
5
+ control: string;
6
+ label: string;
7
+ };
8
+
9
+ const sizes: Record<RefraktorSize, SegmentedControlSizeStyles> = {
10
+ xs: {
11
+ root: "p-0.5 gap-0.5",
12
+ control: "h-6 px-2 min-w-7",
13
+ label: "text-[10px]"
14
+ },
15
+ sm: {
16
+ root: "p-0.5 gap-0.5",
17
+ control: "h-7 px-2.5 min-w-8",
18
+ label: "text-xs"
19
+ },
20
+ md: {
21
+ root: "p-1 gap-1",
22
+ control: "h-8 px-3 min-w-9",
23
+ label: "text-sm"
24
+ },
25
+ lg: {
26
+ root: "p-1 gap-1",
27
+ control: "h-9 px-3.5 min-w-10",
28
+ label: "text-base"
29
+ },
30
+ xl: {
31
+ root: "p-1.5 gap-1.5",
32
+ control: "h-10 px-4 min-w-11",
33
+ label: "text-lg"
34
+ }
35
+ };
36
+
37
+ export const getSize = (size: RefraktorSize = "md") => sizes[size];
@@ -0,0 +1,152 @@
1
+ import { createRef, useState } from "react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { render, screen, userEvent } from "../../vitest";
4
+ import SegmentedControl from "./segmented-control";
5
+
6
+ const data = [
7
+ { value: "react", label: "React" },
8
+ { value: "vue", label: "Vue" },
9
+ { value: "svelte", label: "Svelte" }
10
+ ];
11
+
12
+ describe("@refraktor/core/SegmentedControl", () => {
13
+ it("renders options and selected value", async () => {
14
+ await render(<SegmentedControl data={data} defaultValue="react" />);
15
+
16
+ expect(screen.getByRole("radiogroup")).toBeInTheDocument();
17
+ expect(screen.getByRole("radio", { name: "React" })).toHaveAttribute(
18
+ "aria-checked",
19
+ "true"
20
+ );
21
+ expect(screen.getByRole("radio", { name: "Vue" })).toHaveAttribute(
22
+ "aria-checked",
23
+ "false"
24
+ );
25
+ });
26
+
27
+ it("forwards ref correctly", async () => {
28
+ const ref = createRef<HTMLDivElement>();
29
+
30
+ await render(<SegmentedControl ref={ref} data={data} />);
31
+
32
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
33
+ expect(ref.current?.tagName).toBe("DIV");
34
+ });
35
+
36
+ it("handles click change events", async () => {
37
+ const user = userEvent.setup();
38
+ const onChange = vi.fn();
39
+
40
+ await render(
41
+ <SegmentedControl
42
+ data={data}
43
+ defaultValue="react"
44
+ onChange={onChange}
45
+ />
46
+ );
47
+
48
+ await user.click(screen.getByRole("radio", { name: "Vue" }));
49
+
50
+ expect(onChange).toHaveBeenCalledWith("vue");
51
+ expect(screen.getByRole("radio", { name: "Vue" })).toHaveAttribute(
52
+ "aria-checked",
53
+ "true"
54
+ );
55
+ });
56
+
57
+ it("supports controlled mode", async () => {
58
+ const user = userEvent.setup();
59
+
60
+ function Demo() {
61
+ const [value, setValue] = useState("react");
62
+
63
+ return (
64
+ <SegmentedControl
65
+ data={data}
66
+ value={value}
67
+ onChange={setValue}
68
+ />
69
+ );
70
+ }
71
+
72
+ await render(<Demo />);
73
+
74
+ await user.click(screen.getByRole("radio", { name: "Svelte" }));
75
+
76
+ expect(screen.getByRole("radio", { name: "Svelte" })).toHaveAttribute(
77
+ "aria-checked",
78
+ "true"
79
+ );
80
+ });
81
+
82
+ it("does not select disabled item", async () => {
83
+ const user = userEvent.setup();
84
+ const onChange = vi.fn();
85
+
86
+ await render(
87
+ <SegmentedControl
88
+ defaultValue="react"
89
+ onChange={onChange}
90
+ data={[
91
+ { value: "react", label: "React" },
92
+ { value: "vue", label: "Vue", disabled: true }
93
+ ]}
94
+ />
95
+ );
96
+
97
+ const disabledControl = screen.getByRole("radio", { name: "Vue" });
98
+
99
+ expect(disabledControl).toBeDisabled();
100
+
101
+ await user.click(disabledControl);
102
+
103
+ expect(onChange).not.toHaveBeenCalled();
104
+ expect(disabledControl).toHaveAttribute("aria-checked", "false");
105
+ });
106
+
107
+ it("supports keyboard navigation and skips disabled items", async () => {
108
+ const user = userEvent.setup();
109
+
110
+ await render(
111
+ <SegmentedControl
112
+ defaultValue="react"
113
+ data={[
114
+ { value: "react", label: "React" },
115
+ { value: "vue", label: "Vue", disabled: true },
116
+ { value: "svelte", label: "Svelte" }
117
+ ]}
118
+ />
119
+ );
120
+
121
+ const reactControl = screen.getByRole("radio", { name: "React" });
122
+ reactControl.focus();
123
+
124
+ await user.keyboard("{ArrowRight}");
125
+
126
+ const svelteControl = screen.getByRole("radio", { name: "Svelte" });
127
+
128
+ expect(svelteControl).toHaveFocus();
129
+ expect(svelteControl).toHaveAttribute("aria-checked", "true");
130
+ });
131
+
132
+ it("supports root and slot class names", async () => {
133
+ const { container } = await render(
134
+ <SegmentedControl
135
+ data={data}
136
+ className="custom-root"
137
+ classNames={{
138
+ control: "custom-control",
139
+ label: "custom-label"
140
+ }}
141
+ />
142
+ );
143
+
144
+ const root = container.firstElementChild as HTMLDivElement;
145
+ const control = screen.getByRole("radio", { name: "React" });
146
+ const label = screen.getByText("React");
147
+
148
+ expect(root).toHaveClass("custom-root");
149
+ expect(control).toHaveClass("custom-control");
150
+ expect(label).toHaveClass("custom-label");
151
+ });
152
+ });
@@ -0,0 +1,245 @@
1
+ import { useId, useUncontrolled } from "@refraktor/utils";
2
+ import { KeyboardEvent, useMemo, useRef } from "react";
3
+ import { useTheme } from "../../theme";
4
+ import {
5
+ createClassNamesConfig,
6
+ createComponentConfig,
7
+ factory,
8
+ useClassNames,
9
+ useProps
10
+ } from "../../utils";
11
+ import { getSize } from "./segmented-control.styles";
12
+ import {
13
+ SegmentedControlClassNames,
14
+ SegmentedControlFactoryPayload,
15
+ SegmentedControlProps
16
+ } from "./segmented-control.types";
17
+
18
+ const defaultProps = {
19
+ size: "md",
20
+ radius: "default",
21
+ fullWidth: false,
22
+ disabled: false
23
+ } satisfies Partial<SegmentedControlProps>;
24
+
25
+ const SegmentedControl = factory<SegmentedControlFactoryPayload>(
26
+ (_props, ref) => {
27
+ const { cx, getRadius } = useTheme();
28
+ const {
29
+ id,
30
+ data,
31
+ value,
32
+ defaultValue,
33
+ onChange,
34
+ size,
35
+ radius,
36
+ fullWidth,
37
+ disabled,
38
+ name,
39
+ className,
40
+ classNames,
41
+ ...props
42
+ } = useProps("SegmentedControl", defaultProps, _props);
43
+ const classes = useClassNames("SegmentedControl", classNames);
44
+ const _id = useId(id);
45
+
46
+ const firstEnabledValue = useMemo(
47
+ () => data.find((item) => !item.disabled)?.value ?? "",
48
+ [data]
49
+ );
50
+
51
+ const [selectedValue, setSelectedValue] = useUncontrolled<string>({
52
+ value,
53
+ defaultValue,
54
+ finalValue: firstEnabledValue,
55
+ onChange
56
+ });
57
+
58
+ const sizeStyles = getSize(size);
59
+ const controlRefs = useRef<Array<HTMLButtonElement | null>>([]);
60
+
61
+ const enabledIndexes = useMemo(() => {
62
+ const indexes: number[] = [];
63
+
64
+ data.forEach((item, index) => {
65
+ if (!item.disabled) {
66
+ indexes.push(index);
67
+ }
68
+ });
69
+
70
+ return indexes;
71
+ }, [data]);
72
+
73
+ const activeIndex = useMemo(
74
+ () => data.findIndex((item) => item.value === selectedValue),
75
+ [data, selectedValue]
76
+ );
77
+
78
+ const tabStopIndex = useMemo(() => {
79
+ if (activeIndex !== -1 && !data[activeIndex]?.disabled) {
80
+ return activeIndex;
81
+ }
82
+
83
+ return enabledIndexes[0] ?? -1;
84
+ }, [activeIndex, data, enabledIndexes]);
85
+
86
+ const selectByIndex = (index: number) => {
87
+ const item = data[index];
88
+
89
+ if (
90
+ !item ||
91
+ disabled ||
92
+ item.disabled ||
93
+ item.value === selectedValue
94
+ ) {
95
+ return;
96
+ }
97
+
98
+ setSelectedValue(item.value);
99
+ };
100
+
101
+ const moveSelection = (currentIndex: number, direction: 1 | -1) => {
102
+ if (disabled || enabledIndexes.length === 0) {
103
+ return;
104
+ }
105
+
106
+ const currentEnabledPosition = enabledIndexes.indexOf(currentIndex);
107
+ const basePosition =
108
+ currentEnabledPosition === -1 ? 0 : currentEnabledPosition;
109
+ const nextPosition =
110
+ (basePosition + direction + enabledIndexes.length) %
111
+ enabledIndexes.length;
112
+ const nextIndex = enabledIndexes[nextPosition];
113
+
114
+ selectByIndex(nextIndex);
115
+ controlRefs.current[nextIndex]?.focus();
116
+ };
117
+
118
+ const handleKeyDown = (
119
+ event: KeyboardEvent<HTMLButtonElement>,
120
+ index: number,
121
+ isControlDisabled: boolean
122
+ ) => {
123
+ if (event.defaultPrevented || disabled || isControlDisabled) {
124
+ return;
125
+ }
126
+
127
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") {
128
+ event.preventDefault();
129
+ moveSelection(index, 1);
130
+ return;
131
+ }
132
+
133
+ if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
134
+ event.preventDefault();
135
+ moveSelection(index, -1);
136
+ return;
137
+ }
138
+
139
+ if (event.key === "Home") {
140
+ event.preventDefault();
141
+
142
+ const firstEnabledIndex = enabledIndexes[0];
143
+
144
+ if (firstEnabledIndex !== undefined) {
145
+ selectByIndex(firstEnabledIndex);
146
+ controlRefs.current[firstEnabledIndex]?.focus();
147
+ }
148
+
149
+ return;
150
+ }
151
+
152
+ if (event.key === "End") {
153
+ event.preventDefault();
154
+
155
+ const lastEnabledIndex =
156
+ enabledIndexes[enabledIndexes.length - 1];
157
+
158
+ if (lastEnabledIndex !== undefined) {
159
+ selectByIndex(lastEnabledIndex);
160
+ controlRefs.current[lastEnabledIndex]?.focus();
161
+ }
162
+ }
163
+ };
164
+
165
+ return (
166
+ <div
167
+ ref={ref}
168
+ id={_id}
169
+ role="radiogroup"
170
+ data-disabled={disabled}
171
+ aria-disabled={disabled}
172
+ className={cx(
173
+ "relative inline-flex items-stretch border border-[var(--refraktor-border)]",
174
+ "bg-[var(--refraktor-bg-subtle)]",
175
+ "data-[disabled=true]:opacity-50",
176
+ fullWidth && "w-full",
177
+ sizeStyles.root,
178
+ getRadius(radius),
179
+ classes.root,
180
+ className
181
+ )}
182
+ {...props}
183
+ >
184
+ {data.map((item, index) => {
185
+ const isActive = item.value === selectedValue;
186
+ const isControlDisabled = !!(disabled || item.disabled);
187
+
188
+ return (
189
+ <button
190
+ key={item.value}
191
+ ref={(node) => {
192
+ controlRefs.current[index] = node;
193
+ }}
194
+ type="button"
195
+ role="radio"
196
+ id={`${_id}-control-${index}`}
197
+ aria-checked={isActive}
198
+ aria-disabled={isControlDisabled}
199
+ data-active={isActive}
200
+ data-disabled={isControlDisabled}
201
+ disabled={isControlDisabled}
202
+ tabIndex={
203
+ isControlDisabled || tabStopIndex !== index
204
+ ? -1
205
+ : 0
206
+ }
207
+ className={cx(
208
+ "relative inline-flex items-center justify-center whitespace-nowrap select-none outline-none transition-colors",
209
+ "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--refraktor-primary)]",
210
+ "data-[disabled=true]:opacity-50 data-[disabled=true]:pointer-events-none data-[disabled=true]:cursor-not-allowed",
211
+ fullWidth && "flex-1",
212
+ sizeStyles.control,
213
+ sizeStyles.label,
214
+ getRadius(radius),
215
+ isActive
216
+ ? "bg-[var(--refraktor-primary)] text-[var(--refraktor-primary-text)]"
217
+ : "text-[var(--refraktor-text-secondary)] hover:bg-[var(--refraktor-bg-hover)] hover:text-[var(--refraktor-text)]",
218
+ classes.control
219
+ )}
220
+ onClick={() => selectByIndex(index)}
221
+ onKeyDown={(event) =>
222
+ handleKeyDown(event, index, isControlDisabled)
223
+ }
224
+ >
225
+ <span className={cx("leading-none", classes.label)}>
226
+ {item.label}
227
+ </span>
228
+ </button>
229
+ );
230
+ })}
231
+
232
+ {name && (
233
+ <input type="hidden" name={name} value={selectedValue} />
234
+ )}
235
+ </div>
236
+ );
237
+ }
238
+ );
239
+
240
+ SegmentedControl.displayName = "@refraktor/core/SegmentedControl";
241
+ SegmentedControl.configure = createComponentConfig<SegmentedControlProps>();
242
+ SegmentedControl.classNames =
243
+ createClassNamesConfig<SegmentedControlClassNames>();
244
+
245
+ export default SegmentedControl;
@@ -0,0 +1,75 @@
1
+ import { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import { RefraktorRadius, RefraktorSize } from "../../theme";
3
+ import {
4
+ createClassNamesConfig,
5
+ createComponentConfig,
6
+ FactoryPayload
7
+ } from "../../utils";
8
+
9
+ export interface SegmentedControlItem {
10
+ /** Item value */
11
+ value: string;
12
+
13
+ /** Item label */
14
+ label: ReactNode;
15
+
16
+ /** Whether item is disabled */
17
+ disabled?: boolean;
18
+ }
19
+
20
+ export type SegmentedControlClassNames = {
21
+ root?: string;
22
+ control?: string;
23
+ label?: string;
24
+ };
25
+
26
+ export interface SegmentedControlProps extends Omit<
27
+ ComponentPropsWithoutRef<"div">,
28
+ "onChange"
29
+ > {
30
+ /** Items to render */
31
+ data: SegmentedControlItem[];
32
+
33
+ /** Selected value (controlled) */
34
+ value?: string;
35
+
36
+ /** Initial selected value (uncontrolled) */
37
+ defaultValue?: string;
38
+
39
+ /** Callback called when selected value changes */
40
+ onChange?: (value: string) => void;
41
+
42
+ /** The size of segmented control @default `md` */
43
+ size?: RefraktorSize;
44
+
45
+ /** The radius of segmented control @default `default` */
46
+ radius?: RefraktorRadius;
47
+
48
+ /** Whether segmented control should take full width @default `false` */
49
+ fullWidth?: boolean;
50
+
51
+ /** Whether segmented control is disabled @default `false` */
52
+ disabled?: boolean;
53
+
54
+ /** Hidden input name for forms */
55
+ name?: string;
56
+
57
+ /** Used for editing root class name */
58
+ className?: string;
59
+
60
+ /** Used for styling different parts of the component */
61
+ classNames?: SegmentedControlClassNames;
62
+ }
63
+
64
+ export interface SegmentedControlFactoryPayload extends FactoryPayload {
65
+ props: SegmentedControlProps;
66
+ ref: HTMLDivElement;
67
+ compound: {
68
+ configure: ReturnType<
69
+ typeof createComponentConfig<SegmentedControlProps>
70
+ >;
71
+ classNames: ReturnType<
72
+ typeof createClassNamesConfig<SegmentedControlClassNames>
73
+ >;
74
+ };
75
+ }
@@ -92,7 +92,7 @@ const SelectItem = factory<SelectItemFactoryPayload>(
92
92
  aria-selected={isSelected}
93
93
  aria-disabled={disabled}
94
94
  className={cx(
95
- "w-full text-left p-1.5 text-sm rounded-none appearance-none border-none bg-transparent",
95
+ "w-full text-left p-1.5 text-xs rounded-none appearance-none border-none bg-transparent",
96
96
  "inline-flex items-center gap-2 outline-none transition-colors",
97
97
  "hover:bg-[var(--refraktor-bg-hover)] focus-visible:bg-[var(--refraktor-bg-hover)]",
98
98
  "data-[disabled=true]:opacity-50 data-[disabled=true]:pointer-events-none data-[disabled=true]:cursor-not-allowed",