@usefui/components 1.5.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +233 -0
  2. package/LICENSE +21 -0
  3. package/README.md +0 -0
  4. package/babel.config.js +12 -0
  5. package/dist/index.d.mts +1299 -0
  6. package/dist/index.d.ts +1299 -0
  7. package/dist/index.js +3701 -0
  8. package/dist/index.mjs +3586 -0
  9. package/package.json +44 -0
  10. package/src/__tests__/Accordion.test.tsx +106 -0
  11. package/src/__tests__/Avatar.test.tsx +89 -0
  12. package/src/__tests__/Badge.test.tsx +58 -0
  13. package/src/__tests__/Button.test.tsx +88 -0
  14. package/src/__tests__/Checkbox.test.tsx +106 -0
  15. package/src/__tests__/Collapsible.test.tsx +79 -0
  16. package/src/__tests__/Dialog.test.tsx +109 -0
  17. package/src/__tests__/Dropdown.test.tsx +159 -0
  18. package/src/__tests__/Field.test.tsx +100 -0
  19. package/src/__tests__/OTPField.test.tsx +199 -0
  20. package/src/__tests__/Overlay.test.tsx +70 -0
  21. package/src/__tests__/Page.test.tsx +98 -0
  22. package/src/__tests__/Portal.test.tsx +28 -0
  23. package/src/__tests__/Sheet.test.tsx +125 -0
  24. package/src/__tests__/Switch.test.tsx +90 -0
  25. package/src/__tests__/Tabs.test.tsx +129 -0
  26. package/src/__tests__/Toggle.test.tsx +67 -0
  27. package/src/__tests__/Toolbar.test.tsx +147 -0
  28. package/src/__tests__/Tooltip.test.tsx +88 -0
  29. package/src/accordion/Accordion.stories.tsx +89 -0
  30. package/src/accordion/hooks/index.tsx +39 -0
  31. package/src/accordion/index.tsx +170 -0
  32. package/src/avatar/Avatar.stories.tsx +62 -0
  33. package/src/avatar/index.tsx +90 -0
  34. package/src/avatar/styles/index.ts +79 -0
  35. package/src/badge/Badge.stories.tsx +60 -0
  36. package/src/badge/index.tsx +58 -0
  37. package/src/badge/styles/index.ts +109 -0
  38. package/src/button/Button.stories.tsx +47 -0
  39. package/src/button/index.tsx +79 -0
  40. package/src/button/styles/index.ts +180 -0
  41. package/src/checkbox/Checkbox.stories.tsx +100 -0
  42. package/src/checkbox/hooks/index.tsx +40 -0
  43. package/src/checkbox/index.tsx +147 -0
  44. package/src/checkbox/styles/index.ts +139 -0
  45. package/src/collapsible/Collapsible.stories.tsx +95 -0
  46. package/src/collapsible/hooks/index.tsx +50 -0
  47. package/src/collapsible/index.tsx +137 -0
  48. package/src/dialog/Dialog.stories.tsx +73 -0
  49. package/src/dialog/hooks/index.tsx +35 -0
  50. package/src/dialog/index.tsx +221 -0
  51. package/src/dialog/styles/index.ts +72 -0
  52. package/src/divider/index.ts +10 -0
  53. package/src/dropdown/Dropdown.stories.tsx +100 -0
  54. package/src/dropdown/hooks/index.tsx +64 -0
  55. package/src/dropdown/index.tsx +316 -0
  56. package/src/dropdown/styles/index.ts +90 -0
  57. package/src/field/Field.stories.tsx +146 -0
  58. package/src/field/hooks/index.tsx +28 -0
  59. package/src/field/index.tsx +183 -0
  60. package/src/field/styles/index.ts +166 -0
  61. package/src/index.ts +33 -0
  62. package/src/otp-field/OTPField.stories.tsx +50 -0
  63. package/src/otp-field/hooks/index.tsx +13 -0
  64. package/src/otp-field/index.tsx +234 -0
  65. package/src/otp-field/styles/index.ts +33 -0
  66. package/src/otp-field/types/index.ts +23 -0
  67. package/src/overlay/Overlay.stories.tsx +59 -0
  68. package/src/overlay/index.tsx +58 -0
  69. package/src/overlay/styles/index.ts +26 -0
  70. package/src/page/Page.stories.tsx +85 -0
  71. package/src/page/index.tsx +265 -0
  72. package/src/page/styles/index.ts +59 -0
  73. package/src/portal/Portal.stories.tsx +27 -0
  74. package/src/portal/index.tsx +36 -0
  75. package/src/scrollarea/Scrollarea.stories.tsx +99 -0
  76. package/src/scrollarea/index.tsx +27 -0
  77. package/src/scrollarea/styles/index.ts +71 -0
  78. package/src/sheet/Sheet.stories.tsx +86 -0
  79. package/src/sheet/hooks/index.tsx +47 -0
  80. package/src/sheet/index.tsx +190 -0
  81. package/src/sheet/styles/index.ts +69 -0
  82. package/src/switch/Switch.stories.tsx +96 -0
  83. package/src/switch/hooks/index.tsx +33 -0
  84. package/src/switch/index.tsx +122 -0
  85. package/src/switch/styles/index.ts +118 -0
  86. package/src/table/index.tsx +138 -0
  87. package/src/table/styles/index.ts +48 -0
  88. package/src/tabs/Tabs.stories.tsx +87 -0
  89. package/src/tabs/hooks/index.tsx +35 -0
  90. package/src/tabs/index.tsx +161 -0
  91. package/src/tabs/styles/index.ts +9 -0
  92. package/src/toggle/Toggle.stories.tsx +118 -0
  93. package/src/toggle/index.tsx +55 -0
  94. package/src/toggle/styles/index.ts +0 -0
  95. package/src/toolbar/Toolbar.stories.tsx +89 -0
  96. package/src/toolbar/hooks/index.tsx +35 -0
  97. package/src/toolbar/index.tsx +243 -0
  98. package/src/toolbar/styles/index.ts +129 -0
  99. package/src/tooltip/Tooltip.stories.tsx +60 -0
  100. package/src/tooltip/index.tsx +177 -0
  101. package/src/tooltip/styles/index.ts +38 -0
  102. package/src/utils/index.ts +2 -0
  103. package/tsconfig.json +18 -0
  104. package/vitest.config.ts +16 -0
@@ -0,0 +1,243 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { useKeyPress } from "@usefui/hooks";
5
+
6
+ import { ToolbarProvider, useToolbar } from "./hooks";
7
+ import { ToolbarWrapper, ToolbarTriggerWrapper } from "./styles";
8
+ import { Button, IButtonProperties } from "..";
9
+ import {
10
+ IComponentStyling,
11
+ ComponentSizeEnum,
12
+ ComponentHeightEnum,
13
+ TComponentHeight,
14
+ ComponentVariantEnum,
15
+ IComponentSize,
16
+ IReactChildren,
17
+ ComponentSideEnum,
18
+ TComponentSide,
19
+ IComponentControlProperties,
20
+ KeyBindingEnum,
21
+ } from "../../../../types";
22
+
23
+ export interface IToolbarBodyProperties
24
+ extends IComponentStyling,
25
+ IComponentSize,
26
+ IComponentControlProperties,
27
+ React.ComponentProps<"aside"> {
28
+ defaultOpen?: boolean;
29
+ side?: TComponentSide;
30
+ height?: TComponentHeight;
31
+ }
32
+ export interface IToolbarSectionProperties
33
+ extends React.ComponentProps<"section"> {
34
+ showoncollapse?: boolean;
35
+ }
36
+ export interface IToolbarItemProperties extends React.ComponentProps<"div"> {
37
+ showfirstchild?: boolean;
38
+ }
39
+
40
+ export interface IToolbarComposition {
41
+ Root: typeof ToolbarRoot;
42
+ Trigger: typeof ToolbarTrigger;
43
+ Item: typeof ToolbarItem;
44
+ Section: typeof ToolbarSection;
45
+ }
46
+
47
+ /**
48
+ * Toolbar are component that provides a set of tools or actions for the user.
49
+ *
50
+ * **Best practices:**
51
+ *
52
+ * - Use semantic HTML elements to structure the content of the toolbar.
53
+ * - Ensure that the toolbar is visible and accessible to all users, including those using assistive technologies.
54
+ * - Use keyboard shortcuts to provide an alternative way of interacting with the toolbar.
55
+ * - Ensure that the toolbar is responsive and adapts to different screen sizes and orientations.
56
+ *
57
+ * @param {IToolbarBodyProperties} props - The props for the Toolbar component.
58
+ * @param {boolean} props.raw - Define whether the component is styled or not.
59
+ * @param {string} props.shortcut - The key combination used as keyboard shortcuts to trigger the toolbar.
60
+ * @param {string} props.hotkey - The key to use in the key combination for the keyboard shortcuts.
61
+ * @param {KeyBindingEnum} props.bindkey - The modifier key to use in the key combination.
62
+ * @param {TComponentSide} props.sizing - The size of the toolbar.
63
+ * @param {ComponentHeightEnum} props.height - The height definition of the toolbar.
64
+ * @param {ComponentSideEnum} props.side - The side of the toolbar.
65
+ * @param {boolean} props.defaultOpen - Whether the toolbar should be open by default.
66
+ * @param {ReactNode} props.children - The content to be rendered inside the toolbar.
67
+ * @returns {ReactElement} The Toolbar component.
68
+ */
69
+ const Toolbar = (props: IToolbarBodyProperties) => {
70
+ const {
71
+ shortcut,
72
+ hotkey,
73
+ bindkey = KeyBindingEnum.Ctrl,
74
+ raw,
75
+ sizing = ComponentSizeEnum.Medium,
76
+ side = ComponentSideEnum.Left,
77
+ height = ComponentHeightEnum.Fullscreen,
78
+ defaultOpen,
79
+ children,
80
+ ...restProps
81
+ } = props;
82
+ const { id, methods, states } = useToolbar();
83
+ const { toggleToolbar } = methods;
84
+
85
+ const shortcutControls = useKeyPress(String(hotkey), true, bindkey);
86
+ const orientation =
87
+ side && ["left", "right"].includes(side) ? "vertical" : "horizontal";
88
+
89
+ React.useEffect(() => {
90
+ if (defaultOpen && toggleToolbar) return toggleToolbar(true);
91
+ }, [defaultOpen]);
92
+
93
+ React.useEffect(() => {
94
+ if (shortcut && shortcutControls && toggleToolbar) toggleToolbar();
95
+ }, [shortcutControls]);
96
+
97
+ return (
98
+ <ToolbarWrapper
99
+ id={id}
100
+ role="toolbar"
101
+ aria-label={props["aria-label"] ?? `${id}-toolbar`}
102
+ aria-controls={`${id}-trigger`}
103
+ aria-expanded={Boolean(states.expanded)}
104
+ aria-orientation={orientation}
105
+ data-raw={Boolean(raw)}
106
+ data-size={sizing}
107
+ data-height={height}
108
+ data-side={side}
109
+ {...restProps}
110
+ >
111
+ {children}
112
+ </ToolbarWrapper>
113
+ );
114
+ };
115
+ Toolbar.displayName = "Toolbar";
116
+
117
+ const ToolbarRoot = ({ children }: IReactChildren) => {
118
+ return <ToolbarProvider>{children}</ToolbarProvider>;
119
+ };
120
+ ToolbarRoot.displayName = "Toolbar.Root";
121
+
122
+ /**
123
+ * Toolbar.Trigger are used to triggers the expansion and collapse of the associated Toolbar component.
124
+ *
125
+ * **Best practices:**
126
+ *
127
+ * - Use a clear and descriptive title for the trigger that accurately conveys the content of the associated toolbar section.
128
+ * - Ensure that the trigger can be operated using only the keyboard.
129
+ * - Ensure that the focus is properly managed when the trigger is activated.
130
+ *
131
+ * @param {IButtonProperties} props - The props for the Toolbar.Trigger component.
132
+ * @param {ReactNode} props.children - The content to be rendered inside the Toolbar trigger.
133
+ * @returns {ReactElement} The Toolbar.Trigger component.
134
+ */
135
+ const ToolbarTrigger = (props: IButtonProperties) => {
136
+ const {
137
+ raw,
138
+ onClick,
139
+ variant = ComponentVariantEnum.Ghost,
140
+ sizing = ComponentSizeEnum.Small,
141
+ children,
142
+ ...restProps
143
+ } = props;
144
+ const { id, methods, states } = useToolbar();
145
+ const { toggleToolbar } = methods;
146
+
147
+ const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
148
+ if (onClick) onClick(event);
149
+ if (toggleToolbar) toggleToolbar(!states.expanded);
150
+ };
151
+
152
+ return (
153
+ <ToolbarTriggerWrapper data-raw={Boolean(raw)}>
154
+ <Button
155
+ id={`${id}-trigger`}
156
+ variant={variant}
157
+ sizing={sizing}
158
+ raw={Boolean(raw)}
159
+ onClick={handleClick}
160
+ {...restProps}
161
+ >
162
+ {children}
163
+ </Button>
164
+ </ToolbarTriggerWrapper>
165
+ );
166
+ };
167
+ ToolbarTrigger.displayName = "Toolbar.Trigger";
168
+
169
+ /**
170
+ * ToolbarSection are used to hold components within a toolbar.
171
+ *
172
+ * **Best practices:**
173
+ *
174
+ * - Use semantic HTML elements to structure the content of the toolbar section.
175
+ * - Ensure that the toolbar section is visible and accessible to all users, including those using assistive technologies.
176
+ *
177
+ * @param {IToolbarSectionProperties} props - The props for the Toolbar.Section component.
178
+ * @param {boolean} props.showoncollapse - Whether the section should be shown only when the toolbar is collapsed.
179
+ * @param {ReactNode} props.children - The content to be rendered inside the toolbar section.
180
+ * @returns {ReactElement} The Toolbar.Section component.
181
+ */
182
+ const ToolbarSection = (props: IToolbarSectionProperties) => {
183
+ const { showoncollapse, children, ...restProps } = props;
184
+ const { states } = useToolbar();
185
+ const { expanded } = states;
186
+
187
+ if (showoncollapse) return <section {...restProps}>{children}</section>;
188
+ return <section {...restProps}>{expanded && children}</section>;
189
+ };
190
+ ToolbarSection.displayName = "Toolbar.Section";
191
+
192
+ /**
193
+ * ToolbarItem is a component that represents an item within a toolbar.
194
+ *
195
+ * **Best practices:**
196
+ *
197
+ * - Use semantic HTML elements to structure the content of the toolbar item.
198
+ * - Ensure that the toolbar item is visible and accessible to all users, including those using assistive technologies.
199
+ * - Ensure that the toolbar item is focusable and can be interacted with using the keyboard.
200
+ *
201
+ * @param {IToolbarItemProperties} props - The props for the Toolbar.Item component.
202
+ * @param {boolean} props.showfirstchild - Whether to show only the first child of the toolbar item when the toolbar is collapsed.
203
+ * @param {ReactNode} props.children - The content to be rendered inside the toolbar item.
204
+ * @returns {ReactElement} The Toolbar.Item component.
205
+ */
206
+ const ToolbarItem = (props: IToolbarItemProperties) => {
207
+ const { showfirstchild, onClick, children, ...restProps } = props;
208
+ const childArray = React.Children.toArray(children);
209
+
210
+ const { id, states, methods } = useToolbar();
211
+ const { expanded } = states;
212
+ const { toggleToolbar } = methods;
213
+
214
+ const displayFirstChild =
215
+ showfirstchild && childArray.length > 1 && !expanded;
216
+
217
+ const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
218
+ if (onClick) onClick(event);
219
+ if (toggleToolbar && !expanded) toggleToolbar(true);
220
+ };
221
+
222
+ return (
223
+ <div
224
+ tabIndex={-1}
225
+ aria-hidden
226
+ aria-describedby={id}
227
+ data-expanded={expanded}
228
+ onClick={handleClick}
229
+ {...restProps}
230
+ >
231
+ {displayFirstChild && childArray[0]}
232
+ {expanded && children}
233
+ </div>
234
+ );
235
+ };
236
+ ToolbarItem.displayName = "Toolbar.Item";
237
+
238
+ Toolbar.Root = ToolbarRoot;
239
+ Toolbar.Trigger = ToolbarTrigger;
240
+ Toolbar.Item = ToolbarItem;
241
+ Toolbar.Section = ToolbarSection;
242
+
243
+ export { Toolbar, ToolbarRoot, ToolbarTrigger, ToolbarItem, ToolbarSection };
@@ -0,0 +1,129 @@
1
+ import styled, { css } from "styled-components";
2
+
3
+ export const ToolbarDefaultStyles = css`
4
+ margin: 0;
5
+ display: grid;
6
+ grid-template-rows: min-content;
7
+ background-color: var(--body-color);
8
+ border: var(--measurement-small-10) solid transparent;
9
+ padding: var(--measurement-medium-30);
10
+ min-width: var(--measurement-large-30);
11
+ min-height: fit-content;
12
+
13
+ &[aria-expanded="true"] {
14
+ width: 100%;
15
+
16
+ &[aria-orientation="horizontal"] {
17
+ height: 100%;
18
+ width: 100%;
19
+ }
20
+
21
+ menu {
22
+ display: flex;
23
+ }
24
+ }
25
+
26
+ &[aria-expanded="false"] {
27
+ width: fit-content;
28
+ justify-items: center;
29
+
30
+ &[aria-orientation="horizontal"] {
31
+ justify-items: start;
32
+ height: fit-content;
33
+ width: 100%;
34
+ }
35
+ }
36
+ `;
37
+ export const ToolbarSizeStyles = css`
38
+ &[data-size="small"] {
39
+ &[aria-orientation="vertical"] {
40
+ max-width: var(--measurement-large-70);
41
+ }
42
+ &[aria-orientation="horizontal"] {
43
+ max-height: var(--measurement-large-70);
44
+ }
45
+ }
46
+
47
+ &[data-size="medium"] {
48
+ &[aria-orientation="vertical"] {
49
+ max-width: var(--measurement-large-80);
50
+ }
51
+ &[aria-orientation="horizontal"] {
52
+ max-height: var(--measurement-large-80);
53
+ }
54
+ }
55
+
56
+ &[data-size="large"] {
57
+ &[aria-orientation="vertical"] {
58
+ max-width: var(--measurement-large-90);
59
+ }
60
+ &[aria-orientation="horizontal"] {
61
+ max-height: var(--measurement-large-90);
62
+ }
63
+ }
64
+
65
+ &[data-height="display"] {
66
+ &[aria-orientation="vertical"] {
67
+ max-width: calc(var(--measurement-large-90) * 1.618);
68
+ }
69
+ &[aria-orientation="horizontal"] {
70
+ max-height: calc(var(--measurement-large-90) * 1.618);
71
+ }
72
+ }
73
+
74
+ &[data-height="fullscreen"] {
75
+ &[aria-orientation="vertical"] {
76
+ max-width: 100dvw;
77
+ }
78
+ &[aria-orientation="horizontal"] {
79
+ max-height: 100dvh;
80
+ }
81
+ }
82
+ `;
83
+ export const ToolbarSideStyles = css`
84
+ &[data-side="top"] {
85
+ border-bottom-color: var(--font-color-alpha-10);
86
+ }
87
+ &[data-side="right"] {
88
+ border-left-color: var(--font-color-alpha-10);
89
+
90
+ &[aria-expanded="true"] {
91
+ menu {
92
+ justify-content: flex-end;
93
+ }
94
+ }
95
+ &[aria-expanded="false"] {
96
+ justify-items: end;
97
+ }
98
+ }
99
+ &[data-side="bottom"] {
100
+ border-top-color: var(--font-color-alpha-10);
101
+ }
102
+ &[data-side="left"] {
103
+ border-right-color: var(--font-color-alpha-10);
104
+
105
+ &[aria-expanded="true"] {
106
+ menu {
107
+ justify-content: flex-start;
108
+ }
109
+ }
110
+ &[aria-expanded="false"] {
111
+ justify-items: start;
112
+ }
113
+ }
114
+ `;
115
+
116
+ export const ToolbarWrapper = styled.menu<any>`
117
+ &[data-raw="false"] {
118
+ ${ToolbarDefaultStyles}
119
+ ${ToolbarSizeStyles}
120
+
121
+ ${ToolbarSideStyles}
122
+ }
123
+ `;
124
+ export const ToolbarTriggerWrapper = styled.menu<any>`
125
+ &[data-raw="false"] {
126
+ all: unset;
127
+ align-self: flex-end;
128
+ }
129
+ `;
@@ -0,0 +1,60 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+
4
+ import { Tooltip } from "..";
5
+
6
+ // Duplicated doc: The JSDoc content isn't rendering on Storybook.
7
+
8
+ /**
9
+ * Toolpire are used to convey additional information while hovering a component.
10
+ *
11
+ * **Best practices:**
12
+ *
13
+ * - Ensure that the content is short and readable.
14
+ *
15
+ */
16
+ const meta = {
17
+ title: "Components/Tooltip",
18
+ component: Tooltip,
19
+ tags: ["autodocs"],
20
+ decorators: [
21
+ (Story) => (
22
+ <div className="h-100 w-100 grid justify-center align-center">
23
+ <Story />
24
+ </div>
25
+ ),
26
+ ],
27
+ } satisfies Meta<typeof Tooltip>;
28
+ export default meta;
29
+
30
+ type Story = StoryObj<typeof meta>;
31
+ export const Default: Story = {
32
+ args: {
33
+ raw: false,
34
+ content: "",
35
+ delay: 200,
36
+ children: "",
37
+ },
38
+ argTypes: {},
39
+ render: () => (
40
+ <p className="fs-medium-20 p-large-30">
41
+ User analytics is applied in various&nbsp;
42
+ <Tooltip content="Tooltips">
43
+ <b>industries</b>
44
+ </Tooltip>
45
+ &nbsp;and settings to gain insights into user behavior and improve user
46
+ experience. By analyzing user behavior,&nbsp;
47
+ <Tooltip content="are">
48
+ <b>organizations</b>
49
+ </Tooltip>
50
+ &nbsp;can identify areas where users may be experiencing difficulties or
51
+ frustration, take steps to improve their product, and then, improve user
52
+ experience. User analytics allows you to personalize experiences and
53
+ better understand &nbsp;
54
+ <Tooltip content="Great">
55
+ <b>customer</b>
56
+ </Tooltip>
57
+ &nbsp; needs.
58
+ </p>
59
+ ),
60
+ };
@@ -0,0 +1,177 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+
5
+ import { ContentBox, ContentWrapper } from "./styles";
6
+
7
+ import { applyDataState } from "../utils";
8
+ import { ComponentSideEnum, IComponentStyling } from "../../../../types";
9
+
10
+ interface ITooltipProperties
11
+ extends IComponentStyling,
12
+ React.ComponentProps<"span"> {
13
+ delay?: number;
14
+ content: string;
15
+ children: React.ReactNode;
16
+ }
17
+
18
+ /**
19
+ * Tooltips are used to convey additional information while hovering a component.
20
+ *
21
+ * **Best practices:**
22
+ *
23
+ * - Ensure that the content is short and readable.
24
+ *
25
+ * @param {ITooltipProperties} props - The props for the Tooltip component.
26
+ * @param {boolean} props.raw - Define whether the component is styled or not.
27
+ * @param {number} props.delay - The delay in ms awaited until the Tooltip is displayed. Default to 200ms.
28
+ * @param {string} props.content - The additional content to be rendered inside the Tooltip.
29
+ * @param {ReactNode} props.children - The content to be rendered as Tooltip children.
30
+ * @returns {ReactElement} The Tooltip component.
31
+ */
32
+ const Tooltip = ({
33
+ delay = 200,
34
+ content,
35
+ children,
36
+ ...restProps
37
+ }: ITooltipProperties) => {
38
+ const [visible, setVisible] = React.useState(false);
39
+ const [triggerProps, setTriggerProps] = React.useState<{
40
+ top: number;
41
+ right: number;
42
+ bottom: number;
43
+ left: number;
44
+ width: number;
45
+ height: number;
46
+ } | null>(null);
47
+ const [contentProps, setContentProps] = React.useState<{
48
+ top: number;
49
+ right: number;
50
+ bottom: number;
51
+ left: number;
52
+ width: number;
53
+ height: number;
54
+ } | null>(null);
55
+
56
+ const mounted = React.useRef(false);
57
+ const containerRef = React.useRef<HTMLDivElement>(null);
58
+ const contentRef = React.useRef<HTMLDivElement>(null);
59
+ const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
60
+
61
+ const contentRect = () => contentRef?.current?.getBoundingClientRect();
62
+ const bodyRect = React.useCallback(() => {
63
+ if (typeof document !== "undefined") {
64
+ return document.body.getBoundingClientRect();
65
+ }
66
+ }, []);
67
+
68
+ const positions = {
69
+ btt: `calc((${triggerProps?.top}px - ${contentProps?.height}px) - (var(--measurement-medium-10)))`,
70
+ ttb: `calc((${triggerProps?.top}px + ${triggerProps?.height}px) + var(--measurement-medium-10))`,
71
+ ltr: `${triggerProps?.left}px`,
72
+ rtl: `calc(${triggerProps?.left}px - (${contentProps?.width}px - ${triggerProps?.width}px))`,
73
+ };
74
+ const dimensions = {
75
+ body_width: bodyRect()?.width,
76
+ body_height: bodyRect()?.height,
77
+ content_width: contentProps?.width,
78
+ content_height: contentProps?.height,
79
+ content_left: contentProps?.left,
80
+ content_bottom: contentProps?.bottom,
81
+ };
82
+
83
+ const hasEnoughHorizontalSpace =
84
+ Number(dimensions.body_width) - Number(dimensions.content_left) >
85
+ Number(dimensions.content_width) * 1.1;
86
+
87
+ const hasEnoughVerticalSpace =
88
+ Number(dimensions.body_height) - Number(dimensions.content_bottom) >
89
+ Number(dimensions.content_height) * 0.9;
90
+
91
+ const showTooltip = React.useCallback(() => {
92
+ timeoutRef.current = setTimeout(() => setVisible(true), delay);
93
+ }, [delay]);
94
+
95
+ const hideTooltip = React.useCallback(() => {
96
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
97
+ setVisible(false);
98
+ }, []);
99
+
100
+ const handleMouseEnter = React.useCallback(() => {
101
+ const rect = containerRef.current?.getBoundingClientRect();
102
+ if (rect) {
103
+ setTriggerProps({
104
+ top: rect.top,
105
+ right: rect.right,
106
+ bottom: rect.bottom,
107
+ left: rect.left,
108
+ width: rect.width,
109
+ height: rect.height,
110
+ });
111
+ showTooltip();
112
+ }
113
+ }, [showTooltip]);
114
+
115
+ const handleMouseLeave = React.useCallback(
116
+ () => hideTooltip(),
117
+ [hideTooltip]
118
+ );
119
+
120
+ React.useEffect(() => {
121
+ mounted.current = true;
122
+
123
+ setContentProps &&
124
+ setContentProps({
125
+ top: Number(contentRect()?.top),
126
+ right: Number(contentRect()?.right),
127
+ bottom: Number(contentRect()?.bottom),
128
+ left: Number(contentRect()?.left),
129
+ width: Number(contentRect()?.width),
130
+ height: Number(contentRect()?.height),
131
+ });
132
+
133
+ return () => {
134
+ mounted.current = false;
135
+ };
136
+ }, [visible]);
137
+
138
+ return (
139
+ <ContentBox
140
+ ref={containerRef}
141
+ style={{ display: "inline-block", position: "relative" }}
142
+ onMouseEnter={handleMouseEnter}
143
+ onMouseLeave={handleMouseLeave}
144
+ {...restProps}
145
+ >
146
+ {children}
147
+ {visible && (
148
+ <ContentWrapper
149
+ ref={contentRef}
150
+ style={{
151
+ top: hasEnoughVerticalSpace ? positions.ttb : positions.btt,
152
+ left: hasEnoughHorizontalSpace ? positions.ltr : positions.rtl,
153
+ position: "fixed",
154
+ }}
155
+ role="tooltip"
156
+ data-state={applyDataState(visible)}
157
+ data-raw={Boolean(restProps.raw)}
158
+ data-side={
159
+ hasEnoughHorizontalSpace
160
+ ? ComponentSideEnum.Left
161
+ : ComponentSideEnum.Right
162
+ }
163
+ data-align={
164
+ hasEnoughHorizontalSpace
165
+ ? ComponentSideEnum.Left
166
+ : ComponentSideEnum.Right
167
+ }
168
+ >
169
+ <div>{content}</div>
170
+ </ContentWrapper>
171
+ )}
172
+ </ContentBox>
173
+ );
174
+ };
175
+ Tooltip.displayName = "Tooltip";
176
+
177
+ export { Tooltip };
@@ -0,0 +1,38 @@
1
+ import styled, { css, keyframes } from "styled-components";
2
+
3
+ const FadeIn = keyframes`
4
+ 0% {
5
+ opacity: 0;
6
+ }
7
+ 100% {
8
+ opacity: 1;
9
+ }
10
+ `;
11
+
12
+ export const ContentBox = styled.div<any>`
13
+ display: inline-block;
14
+ position: relative;
15
+ `;
16
+ export const ContentWrapper = styled.span<any>`
17
+ &[data-raw="false"] {
18
+ width: fit-content;
19
+ max-width: var(--measurement-large-60);
20
+
21
+ * {
22
+ color: var(--body-color) !important;
23
+ white-space: nowrap;
24
+ text-overflow: ellipsis;
25
+ overflow: hidden;
26
+ }
27
+
28
+ background: var(--font-color);
29
+ padding: var(--measurement-medium-10) var(--measurement-medium-30);
30
+ border: var(--measurement-small-10) solid var(--font-color-alpha-10);
31
+ border-radius: var(--measurement-medium-20);
32
+ font-size: var(--fontsize-medium-10);
33
+ z-index: var(--depth-default-100);
34
+ animation-duration: 0.2s;
35
+ animation-name: ${FadeIn};
36
+ animation-fill-mode: backwards;
37
+ }
38
+ `;
@@ -0,0 +1,2 @@
1
+ export const applyDataState = (condition: boolean): string | void =>
2
+ condition ? "open" : "closed";
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": "./src",
4
+ "outDir": "./dist",
5
+ "jsx": "react",
6
+ "target": "es2022",
7
+ "module": "CommonJS",
8
+ "esModuleInterop": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "strict": true,
11
+ "skipLibCheck": true,
12
+ "noUncheckedIndexedAccess": true,
13
+ "resolveJsonModule": true,
14
+ "noEmit": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules"]
18
+ }
@@ -0,0 +1,16 @@
1
+ import { coverageConfigDefaults, defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "jsdom",
6
+ coverage: {
7
+ all: true,
8
+ enabled: true,
9
+ provider: "istanbul",
10
+ reporter: ["lcov", "html", "text", "json-summary", "json"],
11
+ reportOnFailure: true,
12
+ allowExternal: true,
13
+ exclude: [...coverageConfigDefaults.exclude, "**/*.stories.*"],
14
+ },
15
+ },
16
+ });