@rovula/ui 0.0.61 → 0.0.63

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 (43) hide show
  1. package/dist/cjs/bundle.js +3 -3
  2. package/dist/cjs/bundle.js.map +1 -1
  3. package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +5 -1
  4. package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
  5. package/dist/cjs/types/components/Search/Search.stories.d.ts +2 -0
  6. package/dist/cjs/types/components/Tabs/Tabs.d.ts +1 -0
  7. package/dist/cjs/types/components/Tabs/Tabs.stories.d.ts +2 -0
  8. package/dist/cjs/types/components/Tree/Tree.stories.d.ts +1 -0
  9. package/dist/cjs/types/components/Tree/type.d.ts +1 -0
  10. package/dist/cjs/types/hooks/index.d.ts +1 -0
  11. package/dist/cjs/types/hooks/usePrevious.d.ts +2 -0
  12. package/dist/cjs/types/index.d.ts +1 -0
  13. package/dist/components/Dropdown/Dropdown.js +42 -3
  14. package/dist/components/Input/Input.js +2 -1
  15. package/dist/components/Tabs/Tabs.js +48 -44
  16. package/dist/components/Tree/Tree.js +26 -1
  17. package/dist/components/Tree/Tree.stories.js +12 -0
  18. package/dist/esm/bundle.js +3 -3
  19. package/dist/esm/bundle.js.map +1 -1
  20. package/dist/esm/types/components/Dropdown/Dropdown.d.ts +5 -1
  21. package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
  22. package/dist/esm/types/components/Search/Search.stories.d.ts +2 -0
  23. package/dist/esm/types/components/Tabs/Tabs.d.ts +1 -0
  24. package/dist/esm/types/components/Tabs/Tabs.stories.d.ts +2 -0
  25. package/dist/esm/types/components/Tree/Tree.stories.d.ts +1 -0
  26. package/dist/esm/types/components/Tree/type.d.ts +1 -0
  27. package/dist/esm/types/hooks/index.d.ts +1 -0
  28. package/dist/esm/types/hooks/usePrevious.d.ts +2 -0
  29. package/dist/esm/types/index.d.ts +1 -0
  30. package/dist/hooks/index.js +1 -0
  31. package/dist/hooks/usePrevious.js +9 -0
  32. package/dist/index.d.ts +10 -2
  33. package/dist/index.js +2 -0
  34. package/package.json +1 -1
  35. package/src/components/Dropdown/Dropdown.tsx +54 -3
  36. package/src/components/Input/Input.tsx +2 -0
  37. package/src/components/Tabs/Tabs.tsx +57 -42
  38. package/src/components/Tree/Tree.stories.tsx +35 -0
  39. package/src/components/Tree/Tree.tsx +33 -0
  40. package/src/components/Tree/type.ts +1 -0
  41. package/src/hooks/index.ts +1 -0
  42. package/src/hooks/usePrevious.ts +13 -0
  43. package/src/index.ts +3 -0
@@ -1,4 +1,4 @@
1
- import React, { ReactNode } from "react";
1
+ import React, { CSSProperties, ReactNode } from "react";
2
2
  import { InputProps } from "../TextInput/TextInput";
3
3
  type RenderLabelCallbackArg = {
4
4
  value: string;
@@ -36,6 +36,8 @@ export type DropdownProps = {
36
36
  optionsFiltered: Options[];
37
37
  selectedOption: Options | null | undefined;
38
38
  onClick: (option: Options) => void;
39
+ style?: CSSProperties;
40
+ dropdownRef?: React.RefObject<HTMLUListElement>;
39
41
  }) => ReactNode;
40
42
  } & Omit<InputProps, "value" | "onSelect">;
41
43
  declare const Dropdown: React.ForwardRefExoticComponent<{
@@ -63,6 +65,8 @@ declare const Dropdown: React.ForwardRefExoticComponent<{
63
65
  optionsFiltered: Options[];
64
66
  selectedOption: Options | null | undefined;
65
67
  onClick: (option: Options) => void;
68
+ style?: CSSProperties;
69
+ dropdownRef?: React.RefObject<HTMLUListElement>;
66
70
  }) => ReactNode) | undefined;
67
71
  } & Omit<InputProps, "onSelect" | "value"> & React.RefAttributes<HTMLInputElement>>;
68
72
  export default Dropdown;
@@ -27,6 +27,8 @@ declare const meta: {
27
27
  optionsFiltered: Options[];
28
28
  selectedOption: Options | null | undefined;
29
29
  onClick: (option: Options) => void;
30
+ style?: React.CSSProperties | undefined;
31
+ dropdownRef?: React.RefObject<HTMLUListElement> | undefined;
30
32
  }) => React.ReactNode) | undefined;
31
33
  } & Omit<import("../..").InputProps, "onSelect" | "value"> & React.RefAttributes<HTMLInputElement>>;
32
34
  tags: string[];
@@ -58,6 +60,8 @@ declare const meta: {
58
60
  optionsFiltered: Options[];
59
61
  selectedOption: Options | null | undefined;
60
62
  onClick: (option: Options) => void;
63
+ style?: React.CSSProperties | undefined;
64
+ dropdownRef?: React.RefObject<HTMLUListElement> | undefined;
61
65
  }) => React.ReactNode) | undefined;
62
66
  suppressHydrationWarning?: boolean | undefined;
63
67
  color?: string | undefined;
@@ -322,6 +322,8 @@ declare const meta: {
322
322
  optionsFiltered: Options[];
323
323
  selectedOption: Options | null | undefined;
324
324
  onClick: (option: Options) => void;
325
+ style?: React.CSSProperties | undefined;
326
+ dropdownRef?: React.RefObject<HTMLUListElement> | undefined;
325
327
  }) => React.ReactNode) | undefined;
326
328
  optionContainerClassName?: string | undefined;
327
329
  optionItemClassName?: string | undefined;
@@ -30,6 +30,7 @@ type TabsProps = {
30
30
  leftAction?: React.ReactNode;
31
31
  rightAction?: React.ReactNode;
32
32
  disabled?: boolean;
33
+ keepMounted?: boolean;
33
34
  onAddTab?: () => void;
34
35
  onTabChange?: (tabIndex: number) => void;
35
36
  };
@@ -30,6 +30,7 @@ declare const meta: {
30
30
  leftAction?: React.ReactNode;
31
31
  rightAction?: React.ReactNode;
32
32
  disabled?: boolean | undefined;
33
+ keepMounted?: boolean | undefined;
33
34
  onAddTab?: (() => void) | undefined;
34
35
  onTabChange?: ((tabIndex: number) => void) | undefined;
35
36
  }>;
@@ -66,6 +67,7 @@ declare const meta: {
66
67
  leftAction?: React.ReactNode;
67
68
  rightAction?: React.ReactNode;
68
69
  disabled?: boolean | undefined;
70
+ keepMounted?: boolean | undefined;
69
71
  onAddTab?: (() => void) | undefined;
70
72
  onTabChange?: ((tabIndex: number) => void) | undefined;
71
73
  }>) => import("react/jsx-runtime").JSX.Element)[];
@@ -15,3 +15,4 @@ export declare const MaximumLevel: StoryObj<typeof Tree>;
15
15
  export declare const Leaf: StoryObj<typeof Tree>;
16
16
  export declare const HideCheckboxMode: StoryObj<typeof Tree>;
17
17
  export declare const RadioMode: StoryObj<typeof Tree>;
18
+ export declare const CheckAll: StoryObj<typeof Tree>;
@@ -94,4 +94,5 @@ export interface TreeProps extends Pick<TreeItemProps, "renderIcon" | "renderRig
94
94
  maxLevel?: number;
95
95
  mode?: "checkbox" | "radio";
96
96
  autoDisabled?: boolean;
97
+ checkedAll?: boolean;
97
98
  }
@@ -0,0 +1 @@
1
+ export { default as usePrevious } from "./usePrevious";
@@ -0,0 +1,2 @@
1
+ declare function usePrevious<T>(value: T): T | undefined;
2
+ export default usePrevious;
@@ -40,4 +40,5 @@ export type { NavbarProps } from "./components/Navbar/Navbar";
40
40
  export type { AvatarProps } from "./components/Avatar/Avatar";
41
41
  export type { AvatarGroupProps } from "./components/Avatar/AvatarGroup";
42
42
  export { resloveTimestamp, getStartDateOfDay, getEndDateOfDay, getStartEndTimestampOfDay, getTimestampUTC, } from "./utils/datetime";
43
+ export * from "./hooks";
43
44
  export { cn } from "./utils/cn";
@@ -0,0 +1 @@
1
+ export { default as usePrevious } from "./usePrevious";
@@ -0,0 +1,9 @@
1
+ import { useEffect, useRef } from "react";
2
+ function usePrevious(value) {
3
+ const ref = useRef();
4
+ useEffect(() => {
5
+ ref.current = value;
6
+ });
7
+ return ref.current;
8
+ }
9
+ export default usePrevious;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import React__default, { ReactElement, ReactNode, FC, ComponentPropsWithoutRef, CSSProperties } from 'react';
2
+ import React__default, { ReactElement, ReactNode, CSSProperties, FC, ComponentPropsWithoutRef } from 'react';
3
3
  import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
4
4
  import * as class_variance_authority_dist_types from 'class-variance-authority/dist/types';
5
5
  import * as LabelPrimitive from '@radix-ui/react-label';
@@ -135,6 +135,7 @@ type TabsProps = {
135
135
  leftAction?: React__default.ReactNode;
136
136
  rightAction?: React__default.ReactNode;
137
137
  disabled?: boolean;
138
+ keepMounted?: boolean;
138
139
  onAddTab?: () => void;
139
140
  onTabChange?: (tabIndex: number) => void;
140
141
  };
@@ -176,6 +177,8 @@ type DropdownProps = {
176
177
  optionsFiltered: Options$1[];
177
178
  selectedOption: Options$1 | null | undefined;
178
179
  onClick: (option: Options$1) => void;
180
+ style?: CSSProperties;
181
+ dropdownRef?: React__default.RefObject<HTMLUListElement>;
179
182
  }) => ReactNode;
180
183
  } & Omit<InputProps, "value" | "onSelect">;
181
184
  declare const Dropdown: React__default.ForwardRefExoticComponent<{
@@ -203,6 +206,8 @@ declare const Dropdown: React__default.ForwardRefExoticComponent<{
203
206
  optionsFiltered: Options$1[];
204
207
  selectedOption: Options$1 | null | undefined;
205
208
  onClick: (option: Options$1) => void;
209
+ style?: CSSProperties;
210
+ dropdownRef?: React__default.RefObject<HTMLUListElement>;
206
211
  }) => ReactNode) | undefined;
207
212
  } & Omit<InputProps, "onSelect" | "value"> & React__default.RefAttributes<HTMLInputElement>>;
208
213
 
@@ -710,6 +715,7 @@ interface TreeProps extends Pick<TreeItemProps, "renderIcon" | "renderRightSecti
710
715
  maxLevel?: number;
711
716
  mode?: "checkbox" | "radio";
712
717
  autoDisabled?: boolean;
718
+ checkedAll?: boolean;
713
719
  }
714
720
 
715
721
  declare const Tree: FC<TreeProps>;
@@ -725,6 +731,8 @@ declare const getStartEndTimestampOfDay: () => {
725
731
  };
726
732
  declare const getTimestampUTC: (date: Date) => number;
727
733
 
734
+ declare function usePrevious<T>(value: T): T | undefined;
735
+
728
736
  declare function cn(...inputs: ClassValue[]): string;
729
737
 
730
- export { ActionButton, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, Avatar, AvatarGroup, type AvatarGroupProps, type AvatarProps, Button, type ButtonProps, Calendar, Checkbox, Collapsible, DataTable, type DataTableProps, DatePicker, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Dropdown, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, type DropdownProps, Icon, Input, InputFilter, type InputFilterProps, type InputProps, Label, Loading, Navbar, type NavbarProps, type Options$1 as Options, Popover, PopoverContent, PopoverTrigger, ProgressBar, Search, type SearchProps, Slider, type SliderProps, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, Text, TextInput, Toast$1 as Toast, ToastAction, type ToastActionElement, ToastClose, ToastDescription, type ToastProps, ToastProvider, ToastTitle, ToastViewport, Toaster, Tooltip, TooltipArrow, TooltipContent, TooltipProvider, TooltipSimple, TooltipTrigger, Tree, type TreeData, TreeItem, type TreeItemProps, type TreeProps, cn, getEndDateOfDay, getStartDateOfDay, getStartEndTimestampOfDay, getTimestampUTC, reducer, resloveTimestamp, toast, useToast };
738
+ export { ActionButton, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, Avatar, AvatarGroup, type AvatarGroupProps, type AvatarProps, Button, type ButtonProps, Calendar, Checkbox, Collapsible, DataTable, type DataTableProps, DatePicker, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Dropdown, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, type DropdownProps, Icon, Input, InputFilter, type InputFilterProps, type InputProps, Label, Loading, Navbar, type NavbarProps, type Options$1 as Options, Popover, PopoverContent, PopoverTrigger, ProgressBar, Search, type SearchProps, Slider, type SliderProps, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, Text, TextInput, Toast$1 as Toast, ToastAction, type ToastActionElement, ToastClose, ToastDescription, type ToastProps, ToastProvider, ToastTitle, ToastViewport, Toaster, Tooltip, TooltipArrow, TooltipContent, TooltipProvider, TooltipSimple, TooltipTrigger, Tree, type TreeData, TreeItem, type TreeItemProps, type TreeProps, cn, getEndDateOfDay, getStartDateOfDay, getStartEndTimestampOfDay, getTimestampUTC, reducer, resloveTimestamp, toast, usePrevious, useToast };
package/dist/index.js CHANGED
@@ -37,6 +37,8 @@ export * from "./components/Toast/useToast";
37
37
  export * from "./components/Tree";
38
38
  // UTILS
39
39
  export { resloveTimestamp, getStartDateOfDay, getEndDateOfDay, getStartEndTimestampOfDay, getTimestampUTC, } from "./utils/datetime";
40
+ // Hooks
41
+ export * from "./hooks";
40
42
  export { cn } from "./utils/cn";
41
43
  // const mainPreset = require("./theme/main-preset");
42
44
  // export { mainPreset };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rovula/ui",
3
- "version": "0.0.61",
3
+ "version": "0.0.63",
4
4
  "main": "dist/cjs/bundle.js",
5
5
  "module": "dist/esm/bundle.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,14 +1,16 @@
1
1
  import React, {
2
+ CSSProperties,
2
3
  Fragment,
3
4
  ReactNode,
4
5
  forwardRef,
5
6
  useCallback,
6
7
  useEffect,
8
+ useImperativeHandle,
7
9
  useMemo,
8
10
  useRef,
9
11
  useState,
10
12
  } from "react";
11
-
13
+ import * as Portal from "@radix-ui/react-portal";
12
14
  import TextInput, { InputProps } from "../TextInput/TextInput";
13
15
  import {
14
16
  customInputVariant,
@@ -57,6 +59,8 @@ export type DropdownProps = {
57
59
  optionsFiltered: Options[];
58
60
  selectedOption: Options | null | undefined;
59
61
  onClick: (option: Options) => void;
62
+ style?: CSSProperties;
63
+ dropdownRef?: React.RefObject<HTMLUListElement>;
60
64
  }) => ReactNode;
61
65
  } & Omit<InputProps, "value" | "onSelect">;
62
66
 
@@ -96,6 +100,12 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
96
100
  const [textValue, setTextValue] = useState("");
97
101
  const keyCode = useRef("");
98
102
 
103
+ const dropdownRef = useRef<HTMLUListElement>(null);
104
+ const inputRef = useRef<HTMLInputElement>(null);
105
+ const [dropdownStyles, setDropdownStyles] = useState({});
106
+
107
+ useImperativeHandle(ref, () => inputRef?.current as HTMLInputElement);
108
+
99
109
  useEffect(() => {
100
110
  setSelectedOption(value);
101
111
  setTextValue(value?.label ?? "");
@@ -130,12 +140,51 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
130
140
  );
131
141
  }, [options, filterMode, textValue]);
132
142
 
143
+ const updateDropdownPosition = useCallback(() => {
144
+ if (inputRef.current) {
145
+ const rect = inputRef.current.getBoundingClientRect();
146
+ const dropdownHeight = dropdownRef.current?.offsetHeight || 0;
147
+ const spaceBelow = window.innerHeight - rect.bottom;
148
+ const spaceAbove = rect.top;
149
+
150
+ const position =
151
+ spaceBelow >= dropdownHeight || spaceBelow > spaceAbove
152
+ ? {
153
+ top: `${rect.bottom + window.scrollY}px`,
154
+ left: `${rect.left + window.scrollX}px`,
155
+ width: `${rect.width}px`,
156
+ }
157
+ : {
158
+ top: `${rect.top - dropdownHeight + window.scrollY}px`,
159
+ left: `${rect.left + window.scrollX}px`,
160
+ width: `${rect.width}px`,
161
+ };
162
+
163
+ setDropdownStyles(position);
164
+ }
165
+ }, []);
166
+
167
+ useEffect(() => {
168
+ if (isFocused) {
169
+ updateDropdownPosition();
170
+ window.addEventListener("resize", updateDropdownPosition);
171
+ window.addEventListener("scroll", updateDropdownPosition, true);
172
+ }
173
+
174
+ return () => {
175
+ window.removeEventListener("resize", updateDropdownPosition);
176
+ window.removeEventListener("scroll", updateDropdownPosition, true);
177
+ };
178
+ }, [isFocused, updateDropdownPosition]);
179
+
133
180
  const renderOptions = () => {
134
181
  if (customRenderOptions) {
135
182
  return customRenderOptions({
136
183
  optionsFiltered,
137
184
  selectedOption,
138
185
  onClick: handleOptionClick,
186
+ style: dropdownStyles,
187
+ dropdownRef,
139
188
  });
140
189
  }
141
190
 
@@ -145,6 +194,8 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
145
194
  "absolute mt-1 w-full bg-base-popup border border-base-popup text-base-popup-foreground rounded-md shadow-md z-10 max-h-60 overflow-y-auto",
146
195
  optionContainerClassName
147
196
  )}
197
+ style={dropdownStyles}
198
+ ref={dropdownRef}
148
199
  >
149
200
  {optionsFiltered.map((option) => {
150
201
  if (option.renderLabel) {
@@ -265,7 +316,7 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
265
316
  </div>
266
317
  }
267
318
  {...props}
268
- ref={ref}
319
+ ref={inputRef}
269
320
  readOnly={!filterMode}
270
321
  value={textValue}
271
322
  onChange={handleOnChangeText}
@@ -288,7 +339,7 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
288
339
  onBlur={handleOnBlur}
289
340
  onKeyDown={handleOnKeyDown}
290
341
  />
291
- {isFocused && renderOptions()}
342
+ {isFocused && <Portal.Root>{renderOptions()}</Portal.Root>}
292
343
  </div>
293
344
  );
294
345
  }
@@ -21,6 +21,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
21
21
  type = "text",
22
22
  size = "md",
23
23
  variant = "outline",
24
+ rounded = "normal",
24
25
  fullwidth = false,
25
26
  disabled = false,
26
27
  error = false,
@@ -41,6 +42,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
41
42
  error,
42
43
  hiddenPlaceholder,
43
44
  disabled,
45
+ rounded,
44
46
  }),
45
47
  className
46
48
  )}
@@ -36,6 +36,7 @@ type TabsProps = {
36
36
  leftAction?: React.ReactNode;
37
37
  rightAction?: React.ReactNode;
38
38
  disabled?: boolean;
39
+ keepMounted?: boolean;
39
40
  onAddTab?: () => void;
40
41
  onTabChange?: (tabIndex: number) => void;
41
42
  };
@@ -49,6 +50,7 @@ const Tabs: React.FC<TabsProps> = ({
49
50
  enableAddTabButton = false,
50
51
  keepIconSpace = true,
51
52
  disabled = false,
53
+ keepMounted = false,
52
54
  tabMode = "start",
53
55
  className,
54
56
  tabBarClassName,
@@ -78,53 +80,54 @@ const Tabs: React.FC<TabsProps> = ({
78
80
  }
79
81
  }, [value]);
80
82
 
81
- // const updateSliderStyle = () => {
82
- // const activeTabElement = tabRefs.current[activeTab];
83
- // if (activeTabElement) {
84
- // setSliderStyle({
85
- // width: `${activeTabElement.offsetWidth}px`,
86
- // transform: `translateX(${activeTabElement.offsetLeft}px)`,
87
- // });
88
- // }
89
- // };
83
+ const updateSliderStyle = () => {
84
+ const activeTabElement = tabRefs.current[activeTab];
85
+ if (activeTabElement) {
86
+ setSliderStyle({
87
+ width: `${activeTabElement.offsetWidth}px`,
88
+ transform: `translateX(${activeTabElement.offsetLeft}px)`,
89
+ });
90
+ }
91
+ };
92
+
93
+ useEffect(() => {
94
+ let timeout: NodeJS.Timeout;
90
95
 
91
- // useEffect(() => {
92
- // let timer: NodeJS.Timeout;
96
+ if (isInitialMount.current) {
97
+ isInitialMount.current = false;
93
98
 
94
- // if (isInitialMount.current) {
95
- // isInitialMount.current = false;
99
+ // Set initial position without animation
100
+ const activeTabElement = tabRefs.current[activeTab];
101
+ if (activeTabElement) {
102
+ setSliderStyle({
103
+ width: "0px",
104
+ transform: `translateX(${
105
+ activeTabElement.offsetLeft + activeTabElement.offsetWidth / 2
106
+ }px)`,
107
+ });
96
108
 
97
- // // Set initial position without animation
98
- // const activeTabElement = tabRefs.current[activeTab];
99
- // if (activeTabElement) {
100
- // setSliderStyle({
101
- // width: "0px",
102
- // transform: `translateX(${
103
- // activeTabElement.offsetLeft + activeTabElement.offsetWidth / 2
104
- // }px)`,
105
- // });
109
+ // Trigger reflow
110
+ timeout = setTimeout(() => {
111
+ updateSliderStyle();
112
+ }, 50);
113
+ }
114
+ } else {
115
+ updateSliderStyle();
116
+ }
106
117
 
107
- // // Trigger reflow
108
- // timer = setTimeout(() => {
109
- // updateSliderStyle();
110
- // }, 50);
111
- // }
112
- // } else {
113
- // updateSliderStyle();
114
- // }
118
+ const handleResize = () => {
119
+ updateSliderStyle();
120
+ };
115
121
 
116
- // const handleResize = () => {
117
- // updateSliderStyle();
118
- // };
122
+ window.addEventListener("resize", handleResize);
123
+ return () => {
124
+ window.removeEventListener("resize", handleResize);
119
125
 
120
- // window.addEventListener("resize", handleResize);
121
- // return () => {
122
- // window.removeEventListener("resize", handleResize);
123
- // if (timer) {
124
- // clearTimeout(timer);
125
- // }
126
- // };
127
- // }, [activeTab, tabs, tabMode, keepIconSpace]);
126
+ if (timeout) {
127
+ clearTimeout(timeout);
128
+ }
129
+ };
130
+ }, [activeTab, tabs, tabMode, keepIconSpace]);
128
131
 
129
132
  return (
130
133
  <div className={cn("w-full", className)}>
@@ -240,7 +243,19 @@ const Tabs: React.FC<TabsProps> = ({
240
243
  id={`tab-content-${activeTab}`}
241
244
  aria-labelledby={`tab-${activeTab}`}
242
245
  >
243
- {tabs[activeTab]?.content}
246
+ {tabs.map((tab, idx) => {
247
+ if (!keepMounted && activeTab !== idx) {
248
+ return null;
249
+ }
250
+ return (
251
+ <div
252
+ key={tab.id ?? idx}
253
+ className={`transition ${activeTab === idx ? "block" : "hidden"}`}
254
+ >
255
+ {tab.content}
256
+ </div>
257
+ );
258
+ })}
244
259
  </div>
245
260
  </div>
246
261
  );
@@ -411,3 +411,38 @@ export const RadioMode: StoryObj<typeof Tree> = {
411
411
  );
412
412
  },
413
413
  };
414
+
415
+ export const CheckAll: StoryObj<typeof Tree> = {
416
+ args: {
417
+ data: exampleData,
418
+ },
419
+ render: (args) => {
420
+ const [isCheckedAll, setIsCheckedAll] = useState(false);
421
+ const [checkedId, onCheckedId] = useState<string[]>([]);
422
+
423
+ return (
424
+ <div className="flex flex-col gap-4 w-full">
425
+ <div className="flex gap-2">
426
+ <Button
427
+ variant="outline"
428
+ onClick={() => setIsCheckedAll(!isCheckedAll)}
429
+ >
430
+ {isCheckedAll ? "Unchecked" : "Checked"} All
431
+ </Button>
432
+ <Button variant="outline" onClick={() => onCheckedId([])}>
433
+ Clear
434
+ </Button>
435
+ </div>
436
+ <Tree
437
+ {...args}
438
+ hierarchicalCheck
439
+ checkedAll={isCheckedAll}
440
+ checkedId={checkedId}
441
+ onCheckedChange={(state) => {
442
+ onCheckedId(Object.keys(state).filter((key) => state?.[key]));
443
+ }}
444
+ />
445
+ </div>
446
+ );
447
+ },
448
+ };
@@ -2,6 +2,7 @@ import React, { FC, useCallback, useEffect, useState } from "react";
2
2
  import TreeItem from "./TreeItem";
3
3
  import { TreeData, TreeProps } from "./type";
4
4
  import { cn } from "@/utils/cn";
5
+ import { usePrevious } from "@/hooks";
5
6
 
6
7
  const Tree: FC<TreeProps> = ({
7
8
  classes,
@@ -32,6 +33,7 @@ const Tree: FC<TreeProps> = ({
32
33
  maxLevel,
33
34
  mode = "checkbox",
34
35
  autoDisabled = false,
36
+ checkedAll = false,
35
37
  }) => {
36
38
  const [checkedState, setCheckedState] = useState<Record<string, boolean>>({});
37
39
  const [expandedState, setExpandedState] = useState<Record<string, boolean>>(
@@ -93,6 +95,7 @@ const Tree: FC<TreeProps> = ({
93
95
  }
94
96
  }, [checkedId]);
95
97
 
98
+ // For usecase loaded data a children coming after.
96
99
  useEffect(() => {
97
100
  if (!hierarchicalCheck) {
98
101
  return;
@@ -124,6 +127,36 @@ const Tree: FC<TreeProps> = ({
124
127
  });
125
128
  }, [data, hierarchicalCheck]);
126
129
 
130
+ const prevCheckedAll = usePrevious(checkedAll);
131
+ useEffect(() => {
132
+ if (prevCheckedAll === undefined || prevCheckedAll === checkedAll) return;
133
+ if (checkedAll) {
134
+ const newState: Record<string, boolean> = {};
135
+
136
+ traverseTree(data, (node) => {
137
+ if (node.id) {
138
+ newState[node.id] = true;
139
+ }
140
+ });
141
+ setCheckedState(newState);
142
+ onCheckedChange?.(newState, undefined, null);
143
+ } else {
144
+ let newState: Record<string, boolean> = {};
145
+
146
+ setCheckedState((prev) => {
147
+ newState = Object.keys(prev).reduce(
148
+ (p, key) => ({
149
+ ...p,
150
+ [key]: false,
151
+ }),
152
+ {}
153
+ );
154
+ return newState;
155
+ });
156
+ onCheckedChange?.(newState, undefined, null);
157
+ }
158
+ }, [data, checkedAll, prevCheckedAll]);
159
+
127
160
  const handleExpandChange = useCallback(
128
161
  (id: string, expanded: boolean, itemData: any) => {
129
162
  onExpandChange?.(id, expanded, itemData);
@@ -117,4 +117,5 @@ export interface TreeProps
117
117
  mode?: "checkbox" | "radio";
118
118
  // Only radio mode
119
119
  autoDisabled?: boolean;
120
+ checkedAll?: boolean;
120
121
  }
@@ -0,0 +1 @@
1
+ export { default as usePrevious } from "./usePrevious";
@@ -0,0 +1,13 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ function usePrevious<T>(value: T): T | undefined {
4
+ const ref = useRef<T>();
5
+
6
+ useEffect(() => {
7
+ ref.current = value;
8
+ });
9
+
10
+ return ref.current;
11
+ }
12
+
13
+ export default usePrevious;
package/src/index.ts CHANGED
@@ -58,6 +58,9 @@ export {
58
58
  getTimestampUTC,
59
59
  } from "./utils/datetime";
60
60
 
61
+ // Hooks
62
+ export * from "./hooks";
63
+
61
64
  export { cn } from "./utils/cn";
62
65
 
63
66
  // const mainPreset = require("./theme/main-preset");