jy-headless 0.3.6 → 0.3.7
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/Input/index.d.ts +2 -0
- package/Popover/Popover.d.ts +2 -2
- package/Popover/Popover.js +15 -62
- package/Popover/Popover.type.d.ts +6 -3
- package/Select/Select.js +176 -0
- package/Select/index.d.ts +1 -0
- package/cjs/Input/index.d.ts +2 -0
- package/cjs/Popover/Popover.d.ts +2 -2
- package/cjs/Popover/Popover.js +13 -60
- package/cjs/Popover/Popover.type.d.ts +6 -3
- package/cjs/Select/Select.js +178 -0
- package/cjs/Select/index.d.ts +1 -0
- package/cjs/index.d.ts +2 -2
- package/cjs/index.js +2 -0
- package/index.d.ts +2 -2
- package/index.js +1 -0
- package/package.json +16 -11
- package/version.txt +1 -1
package/Input/index.d.ts
ADDED
package/Popover/Popover.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare const Popover: ({
|
|
1
|
+
import { PopoverProps } from './Popover.type';
|
|
2
|
+
export declare const Popover: ({ direction, popover, children, key, gap, autoFlip }: PopoverProps) => import("react/jsx-runtime").JSX.Element;
|
package/Popover/Popover.js
CHANGED
|
@@ -1,75 +1,28 @@
|
|
|
1
|
-
import { jsxs
|
|
2
|
-
import { useState, useRef
|
|
3
|
-
import
|
|
1
|
+
import { jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useRef } from 'react';
|
|
3
|
+
import usePortal from '../hooks/usePortal.js';
|
|
4
4
|
|
|
5
|
-
const Popover = ({
|
|
5
|
+
const Popover = ({ direction = 'top', popover, children, key, gap = 0, autoFlip = true }) => {
|
|
6
6
|
const [visible, setVisible] = useState(false);
|
|
7
|
-
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
8
|
-
const rootDom = domNode ?? document.getElementById(rootId ?? 'root');
|
|
9
7
|
const targetRef = useRef(null);
|
|
10
8
|
const popoverRef = useRef(null);
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
top = targetRect.top - popoverRect.height;
|
|
22
|
-
left = targetRect.left + (targetRect.width - popoverRect.width) / 2;
|
|
23
|
-
break;
|
|
24
|
-
case 'top-right':
|
|
25
|
-
top = targetRect.top - popoverRect.height;
|
|
26
|
-
left = targetRect.right - popoverRect.width;
|
|
27
|
-
break;
|
|
28
|
-
case 'left':
|
|
29
|
-
top = targetRect.top + (targetRect.height - popoverRect.height) / 2;
|
|
30
|
-
left = targetRect.left - popoverRect.width;
|
|
31
|
-
break;
|
|
32
|
-
case 'right':
|
|
33
|
-
top = targetRect.top + (targetRect.height - popoverRect.height) / 2;
|
|
34
|
-
left = targetRect.right;
|
|
35
|
-
break;
|
|
36
|
-
case 'bottom-left':
|
|
37
|
-
top = targetRect.bottom;
|
|
38
|
-
left = targetRect.left;
|
|
39
|
-
break;
|
|
40
|
-
case 'bottom-center':
|
|
41
|
-
case 'bottom':
|
|
42
|
-
top = targetRect.bottom;
|
|
43
|
-
left = targetRect.left + (targetRect.width - popoverRect.width) / 2;
|
|
44
|
-
break;
|
|
45
|
-
case 'bottom-right':
|
|
46
|
-
top = targetRect.bottom;
|
|
47
|
-
left = targetRect.right - popoverRect.width;
|
|
48
|
-
break;
|
|
49
|
-
}
|
|
50
|
-
return { top, left };
|
|
51
|
-
}, [direction]);
|
|
9
|
+
const { portal, rootDom } = usePortal({
|
|
10
|
+
content: popover,
|
|
11
|
+
key,
|
|
12
|
+
visible,
|
|
13
|
+
targetRef,
|
|
14
|
+
popoverRef,
|
|
15
|
+
direction,
|
|
16
|
+
gap,
|
|
17
|
+
autoFlip,
|
|
18
|
+
});
|
|
52
19
|
const handleMouseEnter = () => {
|
|
53
20
|
setVisible(true);
|
|
54
21
|
};
|
|
55
22
|
const handleMouseLeave = () => {
|
|
56
23
|
setVisible(false);
|
|
57
24
|
};
|
|
58
|
-
|
|
59
|
-
if (visible && targetRef.current && popoverRef.current) {
|
|
60
|
-
const targetRect = targetRef.current.getBoundingClientRect();
|
|
61
|
-
const popoverRect = popoverRef.current.getBoundingClientRect();
|
|
62
|
-
const { top, left } = getPopoverPosition(targetRect, popoverRect);
|
|
63
|
-
setPosition({ top: top + window.scrollY, left: left + window.scrollX });
|
|
64
|
-
}
|
|
65
|
-
}, [visible, popover, getPopoverPosition]);
|
|
66
|
-
return (jsxs("span", { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ref: targetRef, children: [children, rootDom &&
|
|
67
|
-
visible &&
|
|
68
|
-
createPortal(jsx("span", { ref: popoverRef, style: {
|
|
69
|
-
position: 'absolute',
|
|
70
|
-
top: `${position.top}px`,
|
|
71
|
-
left: `${position.left}px`,
|
|
72
|
-
}, children: popover }), rootDom, key)] }));
|
|
25
|
+
return (jsxs("span", { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ref: targetRef, children: [children, rootDom && visible && portal] }));
|
|
73
26
|
};
|
|
74
27
|
|
|
75
28
|
export { Popover };
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
|
|
2
|
+
import { Direction } from '../hooks';
|
|
3
|
+
export interface PopoverProps {
|
|
3
4
|
children: ReactNode;
|
|
4
5
|
popover: ReactNode;
|
|
5
|
-
direction:
|
|
6
|
-
|
|
6
|
+
direction: Direction;
|
|
7
|
+
targetId?: string;
|
|
7
8
|
domNode?: Element;
|
|
8
9
|
key?: string;
|
|
10
|
+
gap?: number;
|
|
11
|
+
autoFlip?: boolean;
|
|
9
12
|
}
|
package/Select/Select.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { createContext, useContext, useState, useRef, useEffect, useMemo } from 'react';
|
|
3
|
+
import usePortal from '../hooks/usePortal.js';
|
|
4
|
+
|
|
5
|
+
const SelectContext = createContext(null);
|
|
6
|
+
/**
|
|
7
|
+
* Select Context 접근 훅
|
|
8
|
+
*
|
|
9
|
+
* @throws Select 외부에서 사용할 경우 에러
|
|
10
|
+
*/
|
|
11
|
+
const useSelectContext = () => {
|
|
12
|
+
const ctx = useContext(SelectContext);
|
|
13
|
+
if (!ctx)
|
|
14
|
+
throw new Error('Select components must be used within <Select>');
|
|
15
|
+
return ctx;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Select 루트 컨테이너
|
|
19
|
+
*
|
|
20
|
+
* - 상태 관리 담당
|
|
21
|
+
* - Context Provider 역할
|
|
22
|
+
*/
|
|
23
|
+
const SelectContainer = ({ value, onChange, multiple = false, children }) => {
|
|
24
|
+
const [open, setOpen] = useState(false);
|
|
25
|
+
const triggerRef = useRef(null);
|
|
26
|
+
const optionRefs = useRef([]);
|
|
27
|
+
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
28
|
+
const toggleValue = (v) => {
|
|
29
|
+
if (!multiple) {
|
|
30
|
+
onChange([v]);
|
|
31
|
+
setOpen(false);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
onChange(value.includes(v)
|
|
35
|
+
? value.filter(i => i !== v)
|
|
36
|
+
: [...value, v]);
|
|
37
|
+
};
|
|
38
|
+
return (jsx(SelectContext.Provider, { value: {
|
|
39
|
+
open,
|
|
40
|
+
setOpen,
|
|
41
|
+
value,
|
|
42
|
+
toggleValue,
|
|
43
|
+
multiple,
|
|
44
|
+
triggerRef,
|
|
45
|
+
optionRefs,
|
|
46
|
+
focusedIndex,
|
|
47
|
+
setFocusedIndex,
|
|
48
|
+
}, children: children }));
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Select 트리거 버튼
|
|
52
|
+
*
|
|
53
|
+
* - 클릭 시 Options 열림
|
|
54
|
+
* - 최초 포커스를 첫 옵션으로 이동
|
|
55
|
+
*/
|
|
56
|
+
const Trigger = (props) => {
|
|
57
|
+
const { open, setOpen, triggerRef, setFocusedIndex } = useSelectContext();
|
|
58
|
+
return (jsx("div", { ref: triggerRef, role: 'button', "aria-expanded": open, onClick: (e) => {
|
|
59
|
+
setOpen(!open);
|
|
60
|
+
setFocusedIndex(0);
|
|
61
|
+
props.onClick?.(e);
|
|
62
|
+
}, ...props }));
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Options 드롭다운 영역
|
|
66
|
+
*
|
|
67
|
+
* 기능:
|
|
68
|
+
* - 외부 클릭 시 닫힘
|
|
69
|
+
* - ESC 키 닫기
|
|
70
|
+
* - ↑ ↓ 키 포커스 이동
|
|
71
|
+
* - portal 렌더링
|
|
72
|
+
*/
|
|
73
|
+
const Options = ({ children, ...props }) => {
|
|
74
|
+
const { open, triggerRef, setOpen, setFocusedIndex, optionRefs } = useSelectContext();
|
|
75
|
+
const popoverRef = useRef(null);
|
|
76
|
+
const triggerWidth = triggerRef.current?.getBoundingClientRect().width;
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!open)
|
|
79
|
+
return;
|
|
80
|
+
const handleOutsideClick = (e) => {
|
|
81
|
+
const target = e.target;
|
|
82
|
+
if (triggerRef.current?.contains(target) ||
|
|
83
|
+
popoverRef.current?.contains(target)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
setOpen(false);
|
|
87
|
+
};
|
|
88
|
+
document.addEventListener('mousedown', handleOutsideClick);
|
|
89
|
+
return () => {
|
|
90
|
+
document.removeEventListener('mousedown', handleOutsideClick);
|
|
91
|
+
};
|
|
92
|
+
}, [open, setOpen, triggerRef]);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!open)
|
|
95
|
+
return;
|
|
96
|
+
const handleKeyDown = (e) => {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
if (e.key === 'Escape') {
|
|
99
|
+
setOpen(false);
|
|
100
|
+
setFocusedIndex(-1);
|
|
101
|
+
}
|
|
102
|
+
if (e.key === 'ArrowUp') {
|
|
103
|
+
setFocusedIndex(prev => prev - 1 < 0 ? optionRefs.current.length - 1 : prev - 1);
|
|
104
|
+
}
|
|
105
|
+
if (e.key === 'ArrowDown') {
|
|
106
|
+
setFocusedIndex(prev => prev + 1 >= optionRefs.current.length ? 0 : prev + 1);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
110
|
+
return () => {
|
|
111
|
+
optionRefs.current = [];
|
|
112
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
113
|
+
};
|
|
114
|
+
}, [open]);
|
|
115
|
+
const { portal } = usePortal({
|
|
116
|
+
visible: open,
|
|
117
|
+
targetRef: triggerRef,
|
|
118
|
+
popoverRef,
|
|
119
|
+
direction: 'bottom',
|
|
120
|
+
gap: 4,
|
|
121
|
+
content: (jsx("div", { ref: popoverRef, style: { width: triggerWidth }, ...props, children: children })),
|
|
122
|
+
});
|
|
123
|
+
return open ? portal : null;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* 개별 선택 옵션
|
|
127
|
+
*
|
|
128
|
+
* 기능:
|
|
129
|
+
* - 선택 상태 표시
|
|
130
|
+
* - 포커스 관리
|
|
131
|
+
* - disabled 지원
|
|
132
|
+
*/
|
|
133
|
+
const Option = ({ value, disabled, children, ...props }) => {
|
|
134
|
+
const { value: selected, toggleValue, optionRefs, focusedIndex } = useSelectContext();
|
|
135
|
+
const isSelected = selected.includes(value);
|
|
136
|
+
const [index, setIndex] = useState(null);
|
|
137
|
+
const isFocused = useMemo(() => focusedIndex === index, [focusedIndex, index]);
|
|
138
|
+
const ref = useRef(null);
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (ref.current && index === null) {
|
|
141
|
+
setIndex(optionRefs.current.length);
|
|
142
|
+
optionRefs.current[optionRefs.current.length] = ref.current;
|
|
143
|
+
}
|
|
144
|
+
}, []);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
ref.current?.setAttribute('data-focused', String(focusedIndex === index));
|
|
147
|
+
if (focusedIndex === index) {
|
|
148
|
+
ref.current?.focus();
|
|
149
|
+
}
|
|
150
|
+
}, [focusedIndex, index]);
|
|
151
|
+
return (jsx("div", { ref: ref, role: 'option', "aria-selected": isSelected, "aria-disabled": disabled, "data-focused": isFocused, onClick: () => {
|
|
152
|
+
if (!disabled)
|
|
153
|
+
toggleValue(value);
|
|
154
|
+
}, ...props, children: children }));
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Compound Select 컴포넌트
|
|
158
|
+
*
|
|
159
|
+
* 사용 예시:
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* <Select value={value} onChange={setValue}>
|
|
163
|
+
* <Select.Trigger>열기</Select.Trigger>
|
|
164
|
+
* <Select.Options>
|
|
165
|
+
* <Select.Option value="a">A</Select.Option>
|
|
166
|
+
* <Select.Option value="b">B</Select.Option>
|
|
167
|
+
* </Select.Options>
|
|
168
|
+
* </Select>
|
|
169
|
+
*/
|
|
170
|
+
Object.assign(SelectContainer, {
|
|
171
|
+
Trigger,
|
|
172
|
+
Options,
|
|
173
|
+
Option,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
export { useSelectContext };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Select';
|
package/cjs/Popover/Popover.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare const Popover: ({
|
|
1
|
+
import { PopoverProps } from './Popover.type';
|
|
2
|
+
export declare const Popover: ({ direction, popover, children, key, gap, autoFlip }: PopoverProps) => import("react/jsx-runtime").JSX.Element;
|
package/cjs/Popover/Popover.js
CHANGED
|
@@ -2,76 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
var jsxRuntime = require('react/jsx-runtime');
|
|
4
4
|
var react = require('react');
|
|
5
|
-
var
|
|
5
|
+
var usePortal = require('../hooks/usePortal.js');
|
|
6
6
|
|
|
7
|
-
const Popover = ({
|
|
7
|
+
const Popover = ({ direction = 'top', popover, children, key, gap = 0, autoFlip = true }) => {
|
|
8
8
|
const [visible, setVisible] = react.useState(false);
|
|
9
|
-
const [position, setPosition] = react.useState({ top: 0, left: 0 });
|
|
10
|
-
const rootDom = domNode ?? document.getElementById(rootId ?? 'root');
|
|
11
9
|
const targetRef = react.useRef(null);
|
|
12
10
|
const popoverRef = react.useRef(null);
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
top = targetRect.top - popoverRect.height;
|
|
24
|
-
left = targetRect.left + (targetRect.width - popoverRect.width) / 2;
|
|
25
|
-
break;
|
|
26
|
-
case 'top-right':
|
|
27
|
-
top = targetRect.top - popoverRect.height;
|
|
28
|
-
left = targetRect.right - popoverRect.width;
|
|
29
|
-
break;
|
|
30
|
-
case 'left':
|
|
31
|
-
top = targetRect.top + (targetRect.height - popoverRect.height) / 2;
|
|
32
|
-
left = targetRect.left - popoverRect.width;
|
|
33
|
-
break;
|
|
34
|
-
case 'right':
|
|
35
|
-
top = targetRect.top + (targetRect.height - popoverRect.height) / 2;
|
|
36
|
-
left = targetRect.right;
|
|
37
|
-
break;
|
|
38
|
-
case 'bottom-left':
|
|
39
|
-
top = targetRect.bottom;
|
|
40
|
-
left = targetRect.left;
|
|
41
|
-
break;
|
|
42
|
-
case 'bottom-center':
|
|
43
|
-
case 'bottom':
|
|
44
|
-
top = targetRect.bottom;
|
|
45
|
-
left = targetRect.left + (targetRect.width - popoverRect.width) / 2;
|
|
46
|
-
break;
|
|
47
|
-
case 'bottom-right':
|
|
48
|
-
top = targetRect.bottom;
|
|
49
|
-
left = targetRect.right - popoverRect.width;
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
return { top, left };
|
|
53
|
-
}, [direction]);
|
|
11
|
+
const { portal, rootDom } = usePortal({
|
|
12
|
+
content: popover,
|
|
13
|
+
key,
|
|
14
|
+
visible,
|
|
15
|
+
targetRef,
|
|
16
|
+
popoverRef,
|
|
17
|
+
direction,
|
|
18
|
+
gap,
|
|
19
|
+
autoFlip,
|
|
20
|
+
});
|
|
54
21
|
const handleMouseEnter = () => {
|
|
55
22
|
setVisible(true);
|
|
56
23
|
};
|
|
57
24
|
const handleMouseLeave = () => {
|
|
58
25
|
setVisible(false);
|
|
59
26
|
};
|
|
60
|
-
|
|
61
|
-
if (visible && targetRef.current && popoverRef.current) {
|
|
62
|
-
const targetRect = targetRef.current.getBoundingClientRect();
|
|
63
|
-
const popoverRect = popoverRef.current.getBoundingClientRect();
|
|
64
|
-
const { top, left } = getPopoverPosition(targetRect, popoverRect);
|
|
65
|
-
setPosition({ top: top + window.scrollY, left: left + window.scrollX });
|
|
66
|
-
}
|
|
67
|
-
}, [visible, popover, getPopoverPosition]);
|
|
68
|
-
return (jsxRuntime.jsxs("span", { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ref: targetRef, children: [children, rootDom &&
|
|
69
|
-
visible &&
|
|
70
|
-
reactDom.createPortal(jsxRuntime.jsx("span", { ref: popoverRef, style: {
|
|
71
|
-
position: 'absolute',
|
|
72
|
-
top: `${position.top}px`,
|
|
73
|
-
left: `${position.left}px`,
|
|
74
|
-
}, children: popover }), rootDom, key)] }));
|
|
27
|
+
return (jsxRuntime.jsxs("span", { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ref: targetRef, children: [children, rootDom && visible && portal] }));
|
|
75
28
|
};
|
|
76
29
|
|
|
77
30
|
exports.Popover = Popover;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
|
|
2
|
+
import { Direction } from '../hooks';
|
|
3
|
+
export interface PopoverProps {
|
|
3
4
|
children: ReactNode;
|
|
4
5
|
popover: ReactNode;
|
|
5
|
-
direction:
|
|
6
|
-
|
|
6
|
+
direction: Direction;
|
|
7
|
+
targetId?: string;
|
|
7
8
|
domNode?: Element;
|
|
8
9
|
key?: string;
|
|
10
|
+
gap?: number;
|
|
11
|
+
autoFlip?: boolean;
|
|
9
12
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var usePortal = require('../hooks/usePortal.js');
|
|
6
|
+
|
|
7
|
+
const SelectContext = react.createContext(null);
|
|
8
|
+
/**
|
|
9
|
+
* Select Context 접근 훅
|
|
10
|
+
*
|
|
11
|
+
* @throws Select 외부에서 사용할 경우 에러
|
|
12
|
+
*/
|
|
13
|
+
const useSelectContext = () => {
|
|
14
|
+
const ctx = react.useContext(SelectContext);
|
|
15
|
+
if (!ctx)
|
|
16
|
+
throw new Error('Select components must be used within <Select>');
|
|
17
|
+
return ctx;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Select 루트 컨테이너
|
|
21
|
+
*
|
|
22
|
+
* - 상태 관리 담당
|
|
23
|
+
* - Context Provider 역할
|
|
24
|
+
*/
|
|
25
|
+
const SelectContainer = ({ value, onChange, multiple = false, children }) => {
|
|
26
|
+
const [open, setOpen] = react.useState(false);
|
|
27
|
+
const triggerRef = react.useRef(null);
|
|
28
|
+
const optionRefs = react.useRef([]);
|
|
29
|
+
const [focusedIndex, setFocusedIndex] = react.useState(-1);
|
|
30
|
+
const toggleValue = (v) => {
|
|
31
|
+
if (!multiple) {
|
|
32
|
+
onChange([v]);
|
|
33
|
+
setOpen(false);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
onChange(value.includes(v)
|
|
37
|
+
? value.filter(i => i !== v)
|
|
38
|
+
: [...value, v]);
|
|
39
|
+
};
|
|
40
|
+
return (jsxRuntime.jsx(SelectContext.Provider, { value: {
|
|
41
|
+
open,
|
|
42
|
+
setOpen,
|
|
43
|
+
value,
|
|
44
|
+
toggleValue,
|
|
45
|
+
multiple,
|
|
46
|
+
triggerRef,
|
|
47
|
+
optionRefs,
|
|
48
|
+
focusedIndex,
|
|
49
|
+
setFocusedIndex,
|
|
50
|
+
}, children: children }));
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Select 트리거 버튼
|
|
54
|
+
*
|
|
55
|
+
* - 클릭 시 Options 열림
|
|
56
|
+
* - 최초 포커스를 첫 옵션으로 이동
|
|
57
|
+
*/
|
|
58
|
+
const Trigger = (props) => {
|
|
59
|
+
const { open, setOpen, triggerRef, setFocusedIndex } = useSelectContext();
|
|
60
|
+
return (jsxRuntime.jsx("div", { ref: triggerRef, role: 'button', "aria-expanded": open, onClick: (e) => {
|
|
61
|
+
setOpen(!open);
|
|
62
|
+
setFocusedIndex(0);
|
|
63
|
+
props.onClick?.(e);
|
|
64
|
+
}, ...props }));
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Options 드롭다운 영역
|
|
68
|
+
*
|
|
69
|
+
* 기능:
|
|
70
|
+
* - 외부 클릭 시 닫힘
|
|
71
|
+
* - ESC 키 닫기
|
|
72
|
+
* - ↑ ↓ 키 포커스 이동
|
|
73
|
+
* - portal 렌더링
|
|
74
|
+
*/
|
|
75
|
+
const Options = ({ children, ...props }) => {
|
|
76
|
+
const { open, triggerRef, setOpen, setFocusedIndex, optionRefs } = useSelectContext();
|
|
77
|
+
const popoverRef = react.useRef(null);
|
|
78
|
+
const triggerWidth = triggerRef.current?.getBoundingClientRect().width;
|
|
79
|
+
react.useEffect(() => {
|
|
80
|
+
if (!open)
|
|
81
|
+
return;
|
|
82
|
+
const handleOutsideClick = (e) => {
|
|
83
|
+
const target = e.target;
|
|
84
|
+
if (triggerRef.current?.contains(target) ||
|
|
85
|
+
popoverRef.current?.contains(target)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
setOpen(false);
|
|
89
|
+
};
|
|
90
|
+
document.addEventListener('mousedown', handleOutsideClick);
|
|
91
|
+
return () => {
|
|
92
|
+
document.removeEventListener('mousedown', handleOutsideClick);
|
|
93
|
+
};
|
|
94
|
+
}, [open, setOpen, triggerRef]);
|
|
95
|
+
react.useEffect(() => {
|
|
96
|
+
if (!open)
|
|
97
|
+
return;
|
|
98
|
+
const handleKeyDown = (e) => {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
if (e.key === 'Escape') {
|
|
101
|
+
setOpen(false);
|
|
102
|
+
setFocusedIndex(-1);
|
|
103
|
+
}
|
|
104
|
+
if (e.key === 'ArrowUp') {
|
|
105
|
+
setFocusedIndex(prev => prev - 1 < 0 ? optionRefs.current.length - 1 : prev - 1);
|
|
106
|
+
}
|
|
107
|
+
if (e.key === 'ArrowDown') {
|
|
108
|
+
setFocusedIndex(prev => prev + 1 >= optionRefs.current.length ? 0 : prev + 1);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
112
|
+
return () => {
|
|
113
|
+
optionRefs.current = [];
|
|
114
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
115
|
+
};
|
|
116
|
+
}, [open]);
|
|
117
|
+
const { portal } = usePortal({
|
|
118
|
+
visible: open,
|
|
119
|
+
targetRef: triggerRef,
|
|
120
|
+
popoverRef,
|
|
121
|
+
direction: 'bottom',
|
|
122
|
+
gap: 4,
|
|
123
|
+
content: (jsxRuntime.jsx("div", { ref: popoverRef, style: { width: triggerWidth }, ...props, children: children })),
|
|
124
|
+
});
|
|
125
|
+
return open ? portal : null;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* 개별 선택 옵션
|
|
129
|
+
*
|
|
130
|
+
* 기능:
|
|
131
|
+
* - 선택 상태 표시
|
|
132
|
+
* - 포커스 관리
|
|
133
|
+
* - disabled 지원
|
|
134
|
+
*/
|
|
135
|
+
const Option = ({ value, disabled, children, ...props }) => {
|
|
136
|
+
const { value: selected, toggleValue, optionRefs, focusedIndex } = useSelectContext();
|
|
137
|
+
const isSelected = selected.includes(value);
|
|
138
|
+
const [index, setIndex] = react.useState(null);
|
|
139
|
+
const isFocused = react.useMemo(() => focusedIndex === index, [focusedIndex, index]);
|
|
140
|
+
const ref = react.useRef(null);
|
|
141
|
+
react.useEffect(() => {
|
|
142
|
+
if (ref.current && index === null) {
|
|
143
|
+
setIndex(optionRefs.current.length);
|
|
144
|
+
optionRefs.current[optionRefs.current.length] = ref.current;
|
|
145
|
+
}
|
|
146
|
+
}, []);
|
|
147
|
+
react.useEffect(() => {
|
|
148
|
+
ref.current?.setAttribute('data-focused', String(focusedIndex === index));
|
|
149
|
+
if (focusedIndex === index) {
|
|
150
|
+
ref.current?.focus();
|
|
151
|
+
}
|
|
152
|
+
}, [focusedIndex, index]);
|
|
153
|
+
return (jsxRuntime.jsx("div", { ref: ref, role: 'option', "aria-selected": isSelected, "aria-disabled": disabled, "data-focused": isFocused, onClick: () => {
|
|
154
|
+
if (!disabled)
|
|
155
|
+
toggleValue(value);
|
|
156
|
+
}, ...props, children: children }));
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Compound Select 컴포넌트
|
|
160
|
+
*
|
|
161
|
+
* 사용 예시:
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* <Select value={value} onChange={setValue}>
|
|
165
|
+
* <Select.Trigger>열기</Select.Trigger>
|
|
166
|
+
* <Select.Options>
|
|
167
|
+
* <Select.Option value="a">A</Select.Option>
|
|
168
|
+
* <Select.Option value="b">B</Select.Option>
|
|
169
|
+
* </Select.Options>
|
|
170
|
+
* </Select>
|
|
171
|
+
*/
|
|
172
|
+
Object.assign(SelectContainer, {
|
|
173
|
+
Trigger,
|
|
174
|
+
Options,
|
|
175
|
+
Option,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
exports.useSelectContext = useSelectContext;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Select';
|
package/cjs/index.d.ts
CHANGED
package/cjs/index.js
CHANGED
|
@@ -6,9 +6,11 @@ require('react-dom');
|
|
|
6
6
|
var useDebounce = require('./hooks/useDebounce.js');
|
|
7
7
|
var useThrottle = require('./hooks/useThrottle.js');
|
|
8
8
|
var Tooltip = require('./Tooltip/Tooltip.js');
|
|
9
|
+
var Select = require('./Select/Select.js');
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
exports.useDebounce = useDebounce.useDebounce;
|
|
13
14
|
exports.useThrottle = useThrottle.useThrottle;
|
|
14
15
|
exports.Tooltip = Tooltip.Tooltip;
|
|
16
|
+
exports.useSelectContext = Select.useSelectContext;
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jy-headless",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "A lightweight and customizable headless UI library for React components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "https://github.com/yCZwIqY/jy-headless",
|
|
@@ -13,6 +13,21 @@
|
|
|
13
13
|
"require": "./cjs/index.js",
|
|
14
14
|
"types": "./index.d.ts"
|
|
15
15
|
},
|
|
16
|
+
"./Popover": {
|
|
17
|
+
"import": "./cjs/Popover/Popover.js",
|
|
18
|
+
"require": "./cjs/cjs/Popover/Popover.js",
|
|
19
|
+
"types": "./cjs/Popover/Popover.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./Select": {
|
|
22
|
+
"import": "./cjs/Select/Select.js",
|
|
23
|
+
"require": "./cjs/cjs/Select/Select.js",
|
|
24
|
+
"types": "./cjs/Select/Select.d.ts"
|
|
25
|
+
},
|
|
26
|
+
"./Tooltip": {
|
|
27
|
+
"import": "./cjs/Tooltip/Tooltip.js",
|
|
28
|
+
"require": "./cjs/cjs/Tooltip/Tooltip.js",
|
|
29
|
+
"types": "./cjs/Tooltip/Tooltip.d.ts"
|
|
30
|
+
},
|
|
16
31
|
"./cjs": {
|
|
17
32
|
"import": "./cjs/index.js",
|
|
18
33
|
"require": "./cjs/cjs/index.js",
|
|
@@ -37,16 +52,6 @@
|
|
|
37
52
|
"import": "./index.js",
|
|
38
53
|
"require": "./cjs/index.js",
|
|
39
54
|
"types": "./index.d.ts"
|
|
40
|
-
},
|
|
41
|
-
"./Popover": {
|
|
42
|
-
"import": "./Popover/Popover.js",
|
|
43
|
-
"require": "./cjs/Popover/Popover.js",
|
|
44
|
-
"types": "./Popover/Popover.d.ts"
|
|
45
|
-
},
|
|
46
|
-
"./Tooltip": {
|
|
47
|
-
"import": "./Tooltip/Tooltip.js",
|
|
48
|
-
"require": "./cjs/Tooltip/Tooltip.js",
|
|
49
|
-
"types": "./Tooltip/Tooltip.d.ts"
|
|
50
55
|
}
|
|
51
56
|
},
|
|
52
57
|
"keywords": [
|
package/version.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.7
|