@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.
- package/CHANGELOG.md +233 -0
- package/LICENSE +21 -0
- package/README.md +0 -0
- package/babel.config.js +12 -0
- package/dist/index.d.mts +1299 -0
- package/dist/index.d.ts +1299 -0
- package/dist/index.js +3701 -0
- package/dist/index.mjs +3586 -0
- package/package.json +44 -0
- package/src/__tests__/Accordion.test.tsx +106 -0
- package/src/__tests__/Avatar.test.tsx +89 -0
- package/src/__tests__/Badge.test.tsx +58 -0
- package/src/__tests__/Button.test.tsx +88 -0
- package/src/__tests__/Checkbox.test.tsx +106 -0
- package/src/__tests__/Collapsible.test.tsx +79 -0
- package/src/__tests__/Dialog.test.tsx +109 -0
- package/src/__tests__/Dropdown.test.tsx +159 -0
- package/src/__tests__/Field.test.tsx +100 -0
- package/src/__tests__/OTPField.test.tsx +199 -0
- package/src/__tests__/Overlay.test.tsx +70 -0
- package/src/__tests__/Page.test.tsx +98 -0
- package/src/__tests__/Portal.test.tsx +28 -0
- package/src/__tests__/Sheet.test.tsx +125 -0
- package/src/__tests__/Switch.test.tsx +90 -0
- package/src/__tests__/Tabs.test.tsx +129 -0
- package/src/__tests__/Toggle.test.tsx +67 -0
- package/src/__tests__/Toolbar.test.tsx +147 -0
- package/src/__tests__/Tooltip.test.tsx +88 -0
- package/src/accordion/Accordion.stories.tsx +89 -0
- package/src/accordion/hooks/index.tsx +39 -0
- package/src/accordion/index.tsx +170 -0
- package/src/avatar/Avatar.stories.tsx +62 -0
- package/src/avatar/index.tsx +90 -0
- package/src/avatar/styles/index.ts +79 -0
- package/src/badge/Badge.stories.tsx +60 -0
- package/src/badge/index.tsx +58 -0
- package/src/badge/styles/index.ts +109 -0
- package/src/button/Button.stories.tsx +47 -0
- package/src/button/index.tsx +79 -0
- package/src/button/styles/index.ts +180 -0
- package/src/checkbox/Checkbox.stories.tsx +100 -0
- package/src/checkbox/hooks/index.tsx +40 -0
- package/src/checkbox/index.tsx +147 -0
- package/src/checkbox/styles/index.ts +139 -0
- package/src/collapsible/Collapsible.stories.tsx +95 -0
- package/src/collapsible/hooks/index.tsx +50 -0
- package/src/collapsible/index.tsx +137 -0
- package/src/dialog/Dialog.stories.tsx +73 -0
- package/src/dialog/hooks/index.tsx +35 -0
- package/src/dialog/index.tsx +221 -0
- package/src/dialog/styles/index.ts +72 -0
- package/src/divider/index.ts +10 -0
- package/src/dropdown/Dropdown.stories.tsx +100 -0
- package/src/dropdown/hooks/index.tsx +64 -0
- package/src/dropdown/index.tsx +316 -0
- package/src/dropdown/styles/index.ts +90 -0
- package/src/field/Field.stories.tsx +146 -0
- package/src/field/hooks/index.tsx +28 -0
- package/src/field/index.tsx +183 -0
- package/src/field/styles/index.ts +166 -0
- package/src/index.ts +33 -0
- package/src/otp-field/OTPField.stories.tsx +50 -0
- package/src/otp-field/hooks/index.tsx +13 -0
- package/src/otp-field/index.tsx +234 -0
- package/src/otp-field/styles/index.ts +33 -0
- package/src/otp-field/types/index.ts +23 -0
- package/src/overlay/Overlay.stories.tsx +59 -0
- package/src/overlay/index.tsx +58 -0
- package/src/overlay/styles/index.ts +26 -0
- package/src/page/Page.stories.tsx +85 -0
- package/src/page/index.tsx +265 -0
- package/src/page/styles/index.ts +59 -0
- package/src/portal/Portal.stories.tsx +27 -0
- package/src/portal/index.tsx +36 -0
- package/src/scrollarea/Scrollarea.stories.tsx +99 -0
- package/src/scrollarea/index.tsx +27 -0
- package/src/scrollarea/styles/index.ts +71 -0
- package/src/sheet/Sheet.stories.tsx +86 -0
- package/src/sheet/hooks/index.tsx +47 -0
- package/src/sheet/index.tsx +190 -0
- package/src/sheet/styles/index.ts +69 -0
- package/src/switch/Switch.stories.tsx +96 -0
- package/src/switch/hooks/index.tsx +33 -0
- package/src/switch/index.tsx +122 -0
- package/src/switch/styles/index.ts +118 -0
- package/src/table/index.tsx +138 -0
- package/src/table/styles/index.ts +48 -0
- package/src/tabs/Tabs.stories.tsx +87 -0
- package/src/tabs/hooks/index.tsx +35 -0
- package/src/tabs/index.tsx +161 -0
- package/src/tabs/styles/index.ts +9 -0
- package/src/toggle/Toggle.stories.tsx +118 -0
- package/src/toggle/index.tsx +55 -0
- package/src/toggle/styles/index.ts +0 -0
- package/src/toolbar/Toolbar.stories.tsx +89 -0
- package/src/toolbar/hooks/index.tsx +35 -0
- package/src/toolbar/index.tsx +243 -0
- package/src/toolbar/styles/index.ts +129 -0
- package/src/tooltip/Tooltip.stories.tsx +60 -0
- package/src/tooltip/index.tsx +177 -0
- package/src/tooltip/styles/index.ts +38 -0
- package/src/utils/index.ts +2 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { useClickOutside, useDisabledScroll } from "@usefui/hooks";
|
|
5
|
+
import { DropdownMenuProvider, useDropdownMenu } from "./hooks";
|
|
6
|
+
import { RootWrapper, ContentWrapper, ItemWrapper } from "./styles";
|
|
7
|
+
import { Button, IButtonProperties } from "../button";
|
|
8
|
+
import { applyDataState } from "../utils";
|
|
9
|
+
import {
|
|
10
|
+
IReactChildren,
|
|
11
|
+
IComponentStyling,
|
|
12
|
+
IComponentSize,
|
|
13
|
+
ComponentSideEnum,
|
|
14
|
+
} from "../../../../types";
|
|
15
|
+
|
|
16
|
+
export interface IDropdownContentProperties
|
|
17
|
+
extends IComponentStyling,
|
|
18
|
+
IComponentSize,
|
|
19
|
+
React.ComponentPropsWithRef<"ul"> {
|
|
20
|
+
defaultOpen?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface IDropdownItemProperties
|
|
23
|
+
extends IComponentStyling,
|
|
24
|
+
React.ComponentProps<"li"> {
|
|
25
|
+
radio?: boolean;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
onClick?: (
|
|
28
|
+
event: React.MouseEvent<HTMLLIElement> | React.KeyboardEvent<HTMLLIElement>
|
|
29
|
+
) => void;
|
|
30
|
+
onKeyDown?: (event: React.KeyboardEvent<HTMLLIElement>) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface IDropdownComposition {
|
|
34
|
+
Root: typeof DropdownMenuRoot;
|
|
35
|
+
Trigger: typeof DropdownMenuTrigger;
|
|
36
|
+
Content: typeof DropdownMenuContent;
|
|
37
|
+
Item: typeof DropdownMenuItem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Dropdown are used to expand and collapse list of actions.
|
|
42
|
+
*
|
|
43
|
+
* **Best practices:**
|
|
44
|
+
*
|
|
45
|
+
* - Use semantic HTML elements to structure the dropdown menu content.
|
|
46
|
+
* - Ensure that the dropdown menu can be opened and closed using the keyboard.
|
|
47
|
+
* - Ensure that the dropdown menu is visibly focused when opened using the keyboard.
|
|
48
|
+
* - Ensure that the dropdown menu is dismissed when the user clicks outside of it or presses the Esc key.
|
|
49
|
+
*
|
|
50
|
+
* @param {React.ComponentProps<"div">} props - The props for the DropdownMenu component.
|
|
51
|
+
* @param {ReactNode} props.children - The content to be rendered inside the dropdown menu.
|
|
52
|
+
* @returns {ReactElement} The DropdownMenu component.
|
|
53
|
+
*/
|
|
54
|
+
const DropdownMenu = ({ children }: React.ComponentProps<"div">) => {
|
|
55
|
+
const DropdownContentRef = React.useRef(null);
|
|
56
|
+
const { states, methods } = useDropdownMenu();
|
|
57
|
+
const { toggleOpen } = methods;
|
|
58
|
+
|
|
59
|
+
const handleClickOutside = () => {
|
|
60
|
+
if (states.open && toggleOpen) toggleOpen();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
useClickOutside(DropdownContentRef, handleClickOutside);
|
|
64
|
+
useDisabledScroll(Boolean(states.open));
|
|
65
|
+
return <RootWrapper ref={DropdownContentRef}>{children}</RootWrapper>;
|
|
66
|
+
};
|
|
67
|
+
DropdownMenu.displayName = "DropdownMenu";
|
|
68
|
+
|
|
69
|
+
const DropdownMenuRoot = ({ children }: IReactChildren) => {
|
|
70
|
+
return <DropdownMenuProvider>{children}</DropdownMenuProvider>;
|
|
71
|
+
};
|
|
72
|
+
DropdownMenuRoot.displayName = "DropdownMenu.Root";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* DropdownMenu.Trigger is used to triggers the expansion and collapse of the associated DropdownMenu.Content component.
|
|
76
|
+
*
|
|
77
|
+
* **Best practices:**
|
|
78
|
+
*
|
|
79
|
+
* - Use a clear and descriptive title for the trigger that accurately conveys the content of the associated dropdown section.
|
|
80
|
+
* - Ensure that the trigger can be operated using only the keyboard.
|
|
81
|
+
* - Ensure that the focus is properly managed when the trigger is activated.
|
|
82
|
+
*
|
|
83
|
+
* @param {IButtonProperties} props - The props for the DropdownMenu.Trigger component.components.
|
|
84
|
+
* @param {ReactNode} props.children - The content to be rendered inside the dropdown trigger.
|
|
85
|
+
* @returns {ReactElement} The DropdownMenu.Trigger component.
|
|
86
|
+
*/
|
|
87
|
+
const DropdownMenuTrigger = (props: IButtonProperties) => {
|
|
88
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
|
89
|
+
const triggerRect = () => triggerRef.current?.getBoundingClientRect();
|
|
90
|
+
|
|
91
|
+
const { variant = "ghost", onClick, children, ...restProps } = props;
|
|
92
|
+
const { id, states, methods } = useDropdownMenu();
|
|
93
|
+
const { toggleOpen, setTriggerProps } = methods;
|
|
94
|
+
|
|
95
|
+
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
96
|
+
if (onClick) onClick(event);
|
|
97
|
+
if (toggleOpen) toggleOpen();
|
|
98
|
+
if (setTriggerProps)
|
|
99
|
+
setTriggerProps({
|
|
100
|
+
top: Number(triggerRect()?.top),
|
|
101
|
+
right: Number(triggerRect()?.right),
|
|
102
|
+
bottom: Number(triggerRect()?.bottom),
|
|
103
|
+
left: Number(triggerRect()?.left),
|
|
104
|
+
width: Number(triggerRect()?.width),
|
|
105
|
+
height: Number(triggerRect()?.height),
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Button
|
|
111
|
+
ref={triggerRef}
|
|
112
|
+
id={id.split("|").at(0)}
|
|
113
|
+
onClick={handleClick}
|
|
114
|
+
aria-haspopup="menu"
|
|
115
|
+
data-state={applyDataState(Boolean(states.open))}
|
|
116
|
+
variant={variant}
|
|
117
|
+
{...restProps}
|
|
118
|
+
>
|
|
119
|
+
{children}
|
|
120
|
+
</Button>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
DropdownMenuTrigger.displayName = "DropdownMenu.Trigger";
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* DropdownMenu.Content is used to contains the content of the associated DropdownMenu.Trigger component.
|
|
127
|
+
*
|
|
128
|
+
* **Best practices:**
|
|
129
|
+
*
|
|
130
|
+
* - Ensure that the content is hidden when the associated dropdown menu is collapsed.
|
|
131
|
+
* - Ensure that the content is properly focused when the associated dropdown menu is expanded.
|
|
132
|
+
*
|
|
133
|
+
* @param {IDropdownContentProperties} props - The props for the DropdownMenu.Content component.
|
|
134
|
+
* @param {boolean} props.raw - Define whether the component is styled or not.
|
|
135
|
+
* @param {ComponentSizeEnum} props.sizing - The size of the component.
|
|
136
|
+
* @param {boolean} props.defaultOpen - The initial open state of the dropdown menu. Defaults to false.
|
|
137
|
+
* @param {ReactNode} props.children - The content to be rendered inside the dropdown menu.
|
|
138
|
+
* @returns {ReactElement} The DropdownMenu.Content component.
|
|
139
|
+
*/
|
|
140
|
+
const DropdownMenuContent = React.forwardRef<
|
|
141
|
+
HTMLUListElement,
|
|
142
|
+
IDropdownContentProperties
|
|
143
|
+
>((props, _) => {
|
|
144
|
+
const { raw, sizing = "medium", defaultOpen, children, ...restProps } = props;
|
|
145
|
+
const { id, states, methods } = useDropdownMenu();
|
|
146
|
+
const { toggleOpen, setContentProps } = methods;
|
|
147
|
+
|
|
148
|
+
const mounted = React.useRef(false);
|
|
149
|
+
const contentRef = React.useRef<HTMLDivElement>(null);
|
|
150
|
+
|
|
151
|
+
const contentRect = () => contentRef?.current?.getBoundingClientRect();
|
|
152
|
+
const bodyRect = (): DOMRect | undefined => {
|
|
153
|
+
if (typeof document !== "undefined") {
|
|
154
|
+
return document?.body?.getBoundingClientRect();
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const positions = {
|
|
160
|
+
btt: `calc((${states?.triggerProps?.top}px - ${states?.contentProps?.height}px) - (var(--measurement-medium-10) * 2))`,
|
|
161
|
+
ttb: `calc((${states?.triggerProps?.top}px + ${states?.triggerProps?.height}px) + var(--measurement-medium-10))`,
|
|
162
|
+
ltr: `${states?.triggerProps?.left}px`,
|
|
163
|
+
rtl: `calc(${states?.triggerProps?.left}px - (${states?.contentProps?.width}px - ${states?.triggerProps?.width}px))`,
|
|
164
|
+
};
|
|
165
|
+
const dimensions = {
|
|
166
|
+
body_width: bodyRect()?.width ?? 0,
|
|
167
|
+
body_height: bodyRect()?.height ?? 0,
|
|
168
|
+
content_width: states.contentProps.width,
|
|
169
|
+
content_height: states.contentProps.height,
|
|
170
|
+
content_left: states.contentProps.left,
|
|
171
|
+
content_bottom: states.contentProps.bottom,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const hasEnoughHorizontalSpace =
|
|
175
|
+
dimensions.body_width - dimensions.content_left >
|
|
176
|
+
dimensions.content_width * 1.1;
|
|
177
|
+
|
|
178
|
+
const hasEnoughVerticalSpace =
|
|
179
|
+
dimensions.body_height - dimensions.content_bottom >
|
|
180
|
+
dimensions.content_height - dimensions.content_height * 0.9;
|
|
181
|
+
|
|
182
|
+
React.useEffect(() => {
|
|
183
|
+
if (defaultOpen && toggleOpen) toggleOpen();
|
|
184
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
187
|
+
React.useEffect(() => {
|
|
188
|
+
mounted.current = true;
|
|
189
|
+
|
|
190
|
+
setContentProps &&
|
|
191
|
+
setContentProps({
|
|
192
|
+
top: Number(contentRect()?.top),
|
|
193
|
+
right: Number(contentRect()?.right),
|
|
194
|
+
bottom: Number(contentRect()?.bottom),
|
|
195
|
+
left: Number(contentRect()?.left),
|
|
196
|
+
width: Number(contentRect()?.width),
|
|
197
|
+
height: Number(contentRect()?.height),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return () => {
|
|
201
|
+
mounted.current = false;
|
|
202
|
+
};
|
|
203
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
204
|
+
}, [states.open]);
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<>
|
|
208
|
+
{states.open && (
|
|
209
|
+
<ContentWrapper
|
|
210
|
+
ref={contentRef}
|
|
211
|
+
id={id.split("|").at(-1)}
|
|
212
|
+
role="menu"
|
|
213
|
+
tabIndex={-1}
|
|
214
|
+
aria-labelledby={id.split("|").at(0)}
|
|
215
|
+
data-state={applyDataState(Boolean(states.open))}
|
|
216
|
+
data-sizing={sizing}
|
|
217
|
+
data-side={
|
|
218
|
+
hasEnoughHorizontalSpace
|
|
219
|
+
? ComponentSideEnum.Left
|
|
220
|
+
: ComponentSideEnum.Right
|
|
221
|
+
}
|
|
222
|
+
data-align={
|
|
223
|
+
hasEnoughHorizontalSpace
|
|
224
|
+
? ComponentSideEnum.Left
|
|
225
|
+
: ComponentSideEnum.Right
|
|
226
|
+
}
|
|
227
|
+
data-raw={Boolean(raw)}
|
|
228
|
+
style={{
|
|
229
|
+
top: hasEnoughVerticalSpace ? positions.ttb : positions.btt,
|
|
230
|
+
left: hasEnoughHorizontalSpace ? positions.ltr : positions.rtl,
|
|
231
|
+
}}
|
|
232
|
+
{...restProps}
|
|
233
|
+
>
|
|
234
|
+
{children}
|
|
235
|
+
</ContentWrapper>
|
|
236
|
+
)}
|
|
237
|
+
</>
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
DropdownMenuContent.displayName = "DropdownMenu.Content";
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* DropdownMenu.Item is used to handle action inside DropdownMenu.Content.
|
|
244
|
+
*
|
|
245
|
+
* @param {IDropdownItemProperties} props - The props for the DropdownMenu.Item component.
|
|
246
|
+
* @param {boolean} props.raw - Define whether the component is styled or not.
|
|
247
|
+
* @param {boolean} props.radio - Define whether the component is collapses the dropdown menu on click or not.
|
|
248
|
+
* @returns {ReactElement} The DropdownMenu.Content component.
|
|
249
|
+
*/
|
|
250
|
+
const DropdownMenuItem = (props: IDropdownItemProperties) => {
|
|
251
|
+
const {
|
|
252
|
+
raw,
|
|
253
|
+
onClick,
|
|
254
|
+
radio = false,
|
|
255
|
+
disabled,
|
|
256
|
+
children,
|
|
257
|
+
...restProps
|
|
258
|
+
} = props;
|
|
259
|
+
const { methods } = useDropdownMenu();
|
|
260
|
+
const { toggleOpen } = methods;
|
|
261
|
+
|
|
262
|
+
const EventsHandler = {
|
|
263
|
+
toggle: () => {
|
|
264
|
+
if (!radio && toggleOpen) toggleOpen();
|
|
265
|
+
},
|
|
266
|
+
click: (
|
|
267
|
+
event:
|
|
268
|
+
| React.MouseEvent<HTMLLIElement>
|
|
269
|
+
| React.KeyboardEvent<HTMLLIElement>
|
|
270
|
+
) => {
|
|
271
|
+
if (onClick) onClick(event);
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const handleClick = (event: React.MouseEvent<HTMLLIElement>) => {
|
|
276
|
+
if (!disabled) {
|
|
277
|
+
EventsHandler.click(event);
|
|
278
|
+
EventsHandler.toggle();
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
const handleKeydown = (event: React.KeyboardEvent<HTMLLIElement>) => {
|
|
282
|
+
if (["Space", "Enter"].includes(event.code || event.key) && !disabled) {
|
|
283
|
+
EventsHandler.click(event);
|
|
284
|
+
EventsHandler.toggle();
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<ItemWrapper
|
|
290
|
+
role="menuitem"
|
|
291
|
+
tabIndex={0}
|
|
292
|
+
onClick={handleClick}
|
|
293
|
+
onKeyDown={handleKeydown}
|
|
294
|
+
aria-disabled={disabled}
|
|
295
|
+
data-orientation="vertical"
|
|
296
|
+
data-raw={Boolean(raw)}
|
|
297
|
+
{...restProps}
|
|
298
|
+
>
|
|
299
|
+
{children}
|
|
300
|
+
</ItemWrapper>
|
|
301
|
+
);
|
|
302
|
+
};
|
|
303
|
+
DropdownMenuItem.displayName = "DropdownMenu.Item";
|
|
304
|
+
|
|
305
|
+
DropdownMenu.Root = DropdownMenuRoot;
|
|
306
|
+
DropdownMenu.Trigger = DropdownMenuTrigger;
|
|
307
|
+
DropdownMenu.Content = DropdownMenuContent;
|
|
308
|
+
DropdownMenu.Item = DropdownMenuItem;
|
|
309
|
+
|
|
310
|
+
export {
|
|
311
|
+
DropdownMenuRoot,
|
|
312
|
+
DropdownMenu,
|
|
313
|
+
DropdownMenuTrigger,
|
|
314
|
+
DropdownMenuContent,
|
|
315
|
+
DropdownMenuItem,
|
|
316
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
const ContentWrapperSizes = css`
|
|
12
|
+
--small: var(--measurement-large-60);
|
|
13
|
+
--medium: var(--measurement-large-80);
|
|
14
|
+
--large: var(--measurement-large-90);
|
|
15
|
+
|
|
16
|
+
max-height: var(--measurement-large-90);
|
|
17
|
+
|
|
18
|
+
&[data-sizing="small"] {
|
|
19
|
+
width: var(--small);
|
|
20
|
+
max-width: var(--small);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&[data-sizing="medium"] {
|
|
24
|
+
width: var(--medium);
|
|
25
|
+
max-width: var(--medium);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&[data-sizing="large"] {
|
|
29
|
+
width: var(--large);
|
|
30
|
+
max-width: var(--large);
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
export const RootWrapper = styled.div`
|
|
35
|
+
position: relative;
|
|
36
|
+
`;
|
|
37
|
+
export const ContentWrapper = styled.ul<any>`
|
|
38
|
+
--small: var(--measurement-large-60);
|
|
39
|
+
--medium: var(--measurement-large-80);
|
|
40
|
+
--large: var(--measurement-large-90);
|
|
41
|
+
|
|
42
|
+
&[data-raw="false"] {
|
|
43
|
+
position: fixed;
|
|
44
|
+
margin: 0;
|
|
45
|
+
|
|
46
|
+
padding: var(--measurement-medium-30);
|
|
47
|
+
margin: var(--measurement-medium-10) 0;
|
|
48
|
+
|
|
49
|
+
background-color: var(--body-color);
|
|
50
|
+
border: var(--measurement-small-10) solid var(--font-color-alpha-10);
|
|
51
|
+
border-radius: var(--measurement-medium-30);
|
|
52
|
+
|
|
53
|
+
z-index: var(--depth-default-100);
|
|
54
|
+
animation-duration: 0.2s;
|
|
55
|
+
animation-name: ${FadeIn};
|
|
56
|
+
animation-fill-mode: backwards;
|
|
57
|
+
|
|
58
|
+
${ContentWrapperSizes}
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
export const ItemWrapper = styled.li<any>`
|
|
63
|
+
list-style: none;
|
|
64
|
+
padding: 0;
|
|
65
|
+
margin: 0;
|
|
66
|
+
user-select: none;
|
|
67
|
+
|
|
68
|
+
&[data-raw="false"] {
|
|
69
|
+
font-size: var(--fontsize-small-80);
|
|
70
|
+
padding: var(--measurement-medium-30);
|
|
71
|
+
border-radius: var(--measurement-medium-20);
|
|
72
|
+
text-align: left;
|
|
73
|
+
color: var(--font-color-alpha-60);
|
|
74
|
+
outline: none;
|
|
75
|
+
transition: all ease-in-out 0.2s;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
|
|
78
|
+
&:hover,
|
|
79
|
+
&:focus,
|
|
80
|
+
&:active {
|
|
81
|
+
color: var(--font-color);
|
|
82
|
+
background-color: var(--font-color-alpha-10);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
&[aria-disabled="true"] {
|
|
87
|
+
cursor: not-allowed;
|
|
88
|
+
opacity: 0.6;
|
|
89
|
+
}
|
|
90
|
+
`;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
|
|
4
|
+
import { Field } from "..";
|
|
5
|
+
import { ComponentVariantEnum, ComponentSizeEnum } from "../../../../types";
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "Components/Field",
|
|
9
|
+
component: Field,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
decorators: [
|
|
12
|
+
(Story) => (
|
|
13
|
+
<div className="m-medium-30">
|
|
14
|
+
<Story />
|
|
15
|
+
</div>
|
|
16
|
+
),
|
|
17
|
+
],
|
|
18
|
+
} satisfies Meta<typeof Field>;
|
|
19
|
+
export default meta;
|
|
20
|
+
|
|
21
|
+
type Story = StoryObj<typeof meta>;
|
|
22
|
+
export const Default: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
raw: false,
|
|
25
|
+
optional: false,
|
|
26
|
+
hint: "",
|
|
27
|
+
error: "",
|
|
28
|
+
},
|
|
29
|
+
argTypes: {
|
|
30
|
+
variant: {
|
|
31
|
+
options: [
|
|
32
|
+
ComponentVariantEnum.Primary,
|
|
33
|
+
ComponentVariantEnum.Secondary,
|
|
34
|
+
ComponentVariantEnum.Ghost,
|
|
35
|
+
],
|
|
36
|
+
control: { type: "radio" },
|
|
37
|
+
},
|
|
38
|
+
sizing: {
|
|
39
|
+
options: [
|
|
40
|
+
ComponentSizeEnum.Small,
|
|
41
|
+
ComponentSizeEnum.Medium,
|
|
42
|
+
ComponentSizeEnum.Large,
|
|
43
|
+
],
|
|
44
|
+
control: { type: "radio" },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
render: ({ ...args }) => {
|
|
48
|
+
return (
|
|
49
|
+
<Field.Root>
|
|
50
|
+
<Field />
|
|
51
|
+
</Field.Root>
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
export const Label = {
|
|
56
|
+
render: ({ ...args }) => {
|
|
57
|
+
return (
|
|
58
|
+
<Field.Root>
|
|
59
|
+
<Field.Wrapper>
|
|
60
|
+
<Field.Label>Label</Field.Label>
|
|
61
|
+
<Field />
|
|
62
|
+
</Field.Wrapper>
|
|
63
|
+
</Field.Root>
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
export const Description = {
|
|
68
|
+
render: ({ ...args }) => {
|
|
69
|
+
return (
|
|
70
|
+
<Field.Root>
|
|
71
|
+
<Field />
|
|
72
|
+
<Field.Meta>Meta</Field.Meta>
|
|
73
|
+
</Field.Root>
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
export const Hint = {
|
|
78
|
+
render: ({ ...args }) => {
|
|
79
|
+
return (
|
|
80
|
+
<Field.Root>
|
|
81
|
+
<Field hint="hint" />
|
|
82
|
+
</Field.Root>
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
export const Error = {
|
|
87
|
+
render: ({ ...args }) => {
|
|
88
|
+
return (
|
|
89
|
+
<Field.Root>
|
|
90
|
+
<Field error="error" />
|
|
91
|
+
</Field.Root>
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
export const Composed = {
|
|
96
|
+
render: ({ ...args }) => {
|
|
97
|
+
return (
|
|
98
|
+
<Field.Root>
|
|
99
|
+
<Field.Wrapper>
|
|
100
|
+
<Field.Label>Label</Field.Label>
|
|
101
|
+
<Field hint="hint" />
|
|
102
|
+
</Field.Wrapper>
|
|
103
|
+
<Field.Meta>Meta</Field.Meta>
|
|
104
|
+
</Field.Root>
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
export const ComposedError = {
|
|
109
|
+
render: ({ ...args }) => {
|
|
110
|
+
return (
|
|
111
|
+
<Field.Root>
|
|
112
|
+
<Field.Wrapper>
|
|
113
|
+
<Field.Label>Label</Field.Label>
|
|
114
|
+
<Field error="error" hint="hint" />
|
|
115
|
+
</Field.Wrapper>
|
|
116
|
+
<Field.Meta>Meta</Field.Meta>
|
|
117
|
+
</Field.Root>
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
export const Sizes = {
|
|
122
|
+
render: ({ ...args }) => {
|
|
123
|
+
return (
|
|
124
|
+
<div className="flex g-medium-30">
|
|
125
|
+
{["large", "medium", "small"].map((item) => (
|
|
126
|
+
<Field.Root key={item}>
|
|
127
|
+
<Field placeholder={item} sizing={item} />
|
|
128
|
+
</Field.Root>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
export const Variants = {
|
|
135
|
+
render: ({ ...args }) => {
|
|
136
|
+
return (
|
|
137
|
+
<div className="flex g-medium-30">
|
|
138
|
+
{["primary", "secondary", "ghost"].map((item) => (
|
|
139
|
+
<Field.Root key={item}>
|
|
140
|
+
<Field placeholder={item} variant={item} />
|
|
141
|
+
</Field.Root>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { createContext, useContext } from "react";
|
|
2
|
+
import { IReactChildren, IComponentAPI } from "../../../../../types";
|
|
3
|
+
|
|
4
|
+
const defaultComponentAPI = {
|
|
5
|
+
id: "",
|
|
6
|
+
states: {},
|
|
7
|
+
methods: {},
|
|
8
|
+
};
|
|
9
|
+
const FieldContext = createContext<IComponentAPI>(defaultComponentAPI);
|
|
10
|
+
export const useField = () => useContext(FieldContext);
|
|
11
|
+
|
|
12
|
+
export const FieldProvider = ({ children }: IReactChildren): JSX.Element => {
|
|
13
|
+
const context = useFieldProvider();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<FieldContext.Provider value={context}>{children}</FieldContext.Provider>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function useFieldProvider(): IComponentAPI {
|
|
21
|
+
const fieldId = React.useId();
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
id: fieldId,
|
|
25
|
+
states: {},
|
|
26
|
+
methods: {},
|
|
27
|
+
};
|
|
28
|
+
}
|