jy-headless 0.3.7 → 0.3.11

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 (63) hide show
  1. package/README.md +62 -0
  2. package/dist/Autocomplete/Autocomplete.d.ts +8 -0
  3. package/dist/Autocomplete/Autocomplete.js +309 -0
  4. package/dist/Autocomplete/Autocomplete.type.d.ts +36 -0
  5. package/dist/Autocomplete/index.d.ts +1 -0
  6. package/{cjs → dist}/Input/TextInput.type.d.ts +2 -2
  7. package/dist/cjs/Autocomplete/Autocomplete.d.ts +8 -0
  8. package/dist/cjs/Autocomplete/Autocomplete.js +311 -0
  9. package/dist/cjs/Autocomplete/Autocomplete.type.d.ts +36 -0
  10. package/dist/cjs/Autocomplete/index.d.ts +1 -0
  11. package/{Input → dist/cjs/Input}/TextInput.type.d.ts +2 -2
  12. package/{cjs → dist/cjs}/index.d.ts +1 -0
  13. package/{cjs → dist/cjs}/index.js +1 -0
  14. package/{index.d.ts → dist/index.d.ts} +1 -0
  15. package/{index.js → dist/index.js} +1 -0
  16. package/package.json +66 -50
  17. package/Popover/Popover.d.ts +0 -2
  18. package/Popover/Popover.js +0 -28
  19. package/Popover/Popover.type.d.ts +0 -12
  20. package/Popover/index.d.ts +0 -2
  21. package/cjs/Popover/Popover.d.ts +0 -2
  22. package/cjs/Popover/Popover.js +0 -30
  23. package/cjs/Popover/Popover.type.d.ts +0 -12
  24. package/cjs/Popover/index.d.ts +0 -2
  25. package/version.txt +0 -1
  26. /package/{Input → dist/Input}/NumberInput.d.ts +0 -0
  27. /package/{Input → dist/Input}/NumberInput.type.d.ts +0 -0
  28. /package/{Input → dist/Input}/TextInput.d.ts +0 -0
  29. /package/{Input → dist/Input}/index.d.ts +0 -0
  30. /package/{Select → dist/Select}/Select.d.ts +0 -0
  31. /package/{Select → dist/Select}/Select.js +0 -0
  32. /package/{Select → dist/Select}/Select.type.d.ts +0 -0
  33. /package/{Select → dist/Select}/index.d.ts +0 -0
  34. /package/{Tooltip → dist/Tooltip}/Tooltip.d.ts +0 -0
  35. /package/{Tooltip → dist/Tooltip}/Tooltip.js +0 -0
  36. /package/{Tooltip → dist/Tooltip}/Tooltip.type.d.ts +0 -0
  37. /package/{Tooltip → dist/Tooltip}/index.d.ts +0 -0
  38. /package/{cjs → dist/cjs}/Input/NumberInput.d.ts +0 -0
  39. /package/{cjs → dist/cjs}/Input/NumberInput.type.d.ts +0 -0
  40. /package/{cjs → dist/cjs}/Input/TextInput.d.ts +0 -0
  41. /package/{cjs → dist/cjs}/Input/index.d.ts +0 -0
  42. /package/{cjs → dist/cjs}/Select/Select.d.ts +0 -0
  43. /package/{cjs → dist/cjs}/Select/Select.js +0 -0
  44. /package/{cjs → dist/cjs}/Select/Select.type.d.ts +0 -0
  45. /package/{cjs → dist/cjs}/Select/index.d.ts +0 -0
  46. /package/{cjs → dist/cjs}/Tooltip/Tooltip.d.ts +0 -0
  47. /package/{cjs → dist/cjs}/Tooltip/Tooltip.js +0 -0
  48. /package/{cjs → dist/cjs}/Tooltip/Tooltip.type.d.ts +0 -0
  49. /package/{cjs → dist/cjs}/Tooltip/index.d.ts +0 -0
  50. /package/{cjs → dist/cjs}/hooks/index.d.ts +0 -0
  51. /package/{cjs → dist/cjs}/hooks/useDebounce.d.ts +0 -0
  52. /package/{cjs → dist/cjs}/hooks/useDebounce.js +0 -0
  53. /package/{cjs → dist/cjs}/hooks/usePortal.d.ts +0 -0
  54. /package/{cjs → dist/cjs}/hooks/usePortal.js +0 -0
  55. /package/{cjs → dist/cjs}/hooks/useThrottle.d.ts +0 -0
  56. /package/{cjs → dist/cjs}/hooks/useThrottle.js +0 -0
  57. /package/{hooks → dist/hooks}/index.d.ts +0 -0
  58. /package/{hooks → dist/hooks}/useDebounce.d.ts +0 -0
  59. /package/{hooks → dist/hooks}/useDebounce.js +0 -0
  60. /package/{hooks → dist/hooks}/usePortal.d.ts +0 -0
  61. /package/{hooks → dist/hooks}/usePortal.js +0 -0
  62. /package/{hooks → dist/hooks}/useThrottle.d.ts +0 -0
  63. /package/{hooks → dist/hooks}/useThrottle.js +0 -0
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # jy-headless
2
+
3
+ React용 Headless UI 라이브러리
4
+
5
+ ---
6
+
7
+ ## 특징
8
+
9
+ * React 기반 Headless UI
10
+ * 스타일 비강제 (디자인 시스템 자유 적용)
11
+ * Portal 기반 레이어 UI 지원
12
+ * 고급 입력 처리 지원
13
+ * debounce / throttle
14
+ * validate / format / parse
15
+ * IME 대응
16
+ * 접근성 고려한 인터랙션 로직
17
+ * 재사용 가능한 훅 제공
18
+
19
+ ---
20
+
21
+ ## 제공 요소
22
+
23
+ ### Components
24
+
25
+ * Select (Compound Component, Portal 기반)
26
+ * Autocomplete (Compound Component, Portal 기반)
27
+ * TextInput
28
+ * NumberInput
29
+ * Tooltip (Portal 기반)
30
+
31
+ ---
32
+
33
+ ### Hooks
34
+
35
+ * usePortal
36
+ * useDebounce
37
+ * useThrottle
38
+ * (추가 예정) useOutsideClick 등
39
+
40
+ ---
41
+
42
+ ## 사용 목적
43
+
44
+ * 디자인 시스템 구축
45
+ * 커스텀 UI 라이브러리 제작
46
+ * 접근성 + 인터랙션 로직 재사용
47
+ * 입력 처리 로직 표준화
48
+
49
+ ---
50
+
51
+ ## 스타일링
52
+
53
+ * Tailwind
54
+ * CSS-in-JS
55
+ * CSS Modules
56
+ * 어떤 방식이든 자유
57
+
58
+ ---
59
+
60
+ ## 라이선스
61
+
62
+ MIT
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import type { AutocompleteInputProps, AutocompleteOptionProps, AutocompleteOptionsProps, AutocompleteProps } from './Autocomplete.type';
3
+ declare const Autocomplete: (({ value, onChange, inputValue, onInputChange, disabled, filterFn, children, }: AutocompleteProps) => import("react/jsx-runtime").JSX.Element) & {
4
+ Input: ({ onKeyDown, onFocus, onChange, ...props }: AutocompleteInputProps) => import("react/jsx-runtime").JSX.Element;
5
+ Options: ({ items, renderItem, itemHeight, maxVisibleItems, overscan, children, ...props }: AutocompleteOptionsProps) => React.ReactPortal | null;
6
+ Option: ({ value, label, disabled, children, ...props }: AutocompleteOptionProps) => import("react/jsx-runtime").JSX.Element | null;
7
+ };
8
+ export default Autocomplete;
@@ -0,0 +1,309 @@
1
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
+ import { createContext, useState, useRef, useId, useMemo, useEffect, useContext } from 'react';
3
+ import usePortal from '../hooks/usePortal.js';
4
+
5
+ const AutocompleteContext = createContext(null);
6
+ const useAutocomplete = () => {
7
+ const ctx = useContext(AutocompleteContext);
8
+ if (!ctx)
9
+ throw new Error('Autocomplete components must be used within <Autocomplete>');
10
+ return ctx;
11
+ };
12
+ const defaultFilter = (item, query) => item.label.toLowerCase().includes(query.trim().toLowerCase());
13
+ /**
14
+ * Root
15
+ */
16
+ const AutocompleteContainer = ({ value, onChange, inputValue, onInputChange, disabled, filterFn = defaultFilter, children, }) => {
17
+ const [open, setOpen] = useState(false);
18
+ const [activeIndex, setActiveIndex] = useState(-1);
19
+ const inputRef = useRef(null);
20
+ // controlled/uncontrolled query
21
+ const [internalQuery, setInternalQuery] = useState('');
22
+ const query = inputValue ?? internalQuery;
23
+ const setQuery = (v) => {
24
+ onInputChange?.(v);
25
+ if (inputValue === undefined)
26
+ setInternalQuery(v);
27
+ };
28
+ const listboxId = useId();
29
+ // NOTE:
30
+ // - virtualization을 제대로 하려면 options data(items)가 Root에 필요하지만,
31
+ // compound API 유지를 위해 Options에서 items를 주입받아 Root에서 filtered를 만들기 어렵다.
32
+ // 그래서 Root에서는 "filtered"를 Options에서 제공할 수 있게 설계하면 복잡해짐.
33
+ // ✅ 해결: Options에 items를 주면 Root가 접근할 수 있도록 "itemsRef"를 둔다.
34
+ const itemsRef = useRef([]);
35
+ const setItems = (items) => {
36
+ itemsRef.current = items;
37
+ };
38
+ const filtered = useMemo(() => {
39
+ const src = itemsRef.current ?? [];
40
+ if (!query.trim())
41
+ return src;
42
+ return src.filter((it) => filterFn(it, query));
43
+ }, [query, filterFn]);
44
+ const close = () => {
45
+ setOpen(false);
46
+ setActiveIndex(-1);
47
+ };
48
+ const commitByIndex = (index) => {
49
+ const item = filtered[index];
50
+ if (!item || item.disabled)
51
+ return;
52
+ onChange(item.value);
53
+ setQuery(item.label);
54
+ close();
55
+ };
56
+ // 외부 value 변경 시 input label 동기화
57
+ useEffect(() => {
58
+ if (!value)
59
+ return;
60
+ const found = itemsRef.current.find((i) => i.value === value);
61
+ if (found)
62
+ setQuery(found.label);
63
+ // eslint-disable-next-line react-hooks/exhaustive-deps
64
+ }, [value]);
65
+ const statusText = useMemo(() => {
66
+ if (!open)
67
+ return '';
68
+ const n = filtered.length;
69
+ if (n === 0)
70
+ return 'No results.';
71
+ if (n === 1)
72
+ return '1 result available.';
73
+ return `${n} results available.`;
74
+ }, [open, filtered.length]);
75
+ return (jsxs(AutocompleteContext.Provider, { value: {
76
+ open,
77
+ setOpen,
78
+ selectedValue: value,
79
+ setSelectedValue: onChange,
80
+ query,
81
+ setQuery,
82
+ disabled,
83
+ inputRef,
84
+ listboxId,
85
+ activeIndex,
86
+ setActiveIndex,
87
+ filtered,
88
+ statusText,
89
+ commitByIndex,
90
+ close,
91
+ }, children: [jsx(ItemsBridge, { onItems: setItems }), children] }));
92
+ };
93
+ /**
94
+ * Options에서 items를 주입해주기 위한 브릿지(보이지 않는 컴포넌트)
95
+ * - Options가 items prop을 받으면, 내부에서 window.__ 같은 걸 쓰지 않고 Root ref에 주입
96
+ */
97
+ const ItemsBridgeContext = createContext(null);
98
+ const ItemsBridge = ({ onItems }) => {
99
+ return jsx(ItemsBridgeContext.Provider, { value: onItems });
100
+ };
101
+ const useItemsBridge = () => useContext(ItemsBridgeContext);
102
+ /**
103
+ * Input (Combobox Trigger)
104
+ * - 포커스는 input 유지
105
+ * - aria-activedescendant로 active option을 알려줌
106
+ */
107
+ const Input = ({ onKeyDown, onFocus, onChange, ...props }) => {
108
+ const { open, setOpen, query, setQuery, disabled, listboxId, activeIndex, setActiveIndex, filtered, commitByIndex, close, inputRef, } = useAutocomplete();
109
+ const activeId = activeIndex >= 0 ? `${listboxId}-opt-${activeIndex}` : undefined;
110
+ const move = (delta) => {
111
+ if (!filtered.length)
112
+ return;
113
+ setOpen(true);
114
+ setActiveIndex(activeIndex < 0 ? 0 : (activeIndex + delta + filtered.length) % filtered.length);
115
+ };
116
+ const pageMove = (deltaPages) => {
117
+ if (!filtered.length)
118
+ return;
119
+ setOpen(true);
120
+ // 10개 단위 이동(관례). 필요하면 props로 빼도 됨
121
+ const jump = 10 * deltaPages;
122
+ setActiveIndex(Math.max(0, Math.min(filtered.length - 1, (activeIndex < 0 ? 0 : activeIndex) + jump)));
123
+ };
124
+ return (jsxs(Fragment, { children: [jsx("input", { ref: inputRef, role: 'combobox', "aria-autocomplete": 'list', "aria-expanded": open, "aria-controls": listboxId, "aria-activedescendant": activeId, disabled: disabled, value: query, onFocus: (e) => {
125
+ if (!disabled) {
126
+ setOpen(true);
127
+ setActiveIndex(filtered.length ? 0 : -1);
128
+ }
129
+ onFocus?.(e);
130
+ }, onChange: (e) => {
131
+ if (disabled)
132
+ return;
133
+ setQuery(e.target.value);
134
+ setOpen(true);
135
+ setActiveIndex(0);
136
+ onChange?.(e);
137
+ }, onKeyDown: (e) => {
138
+ if (disabled)
139
+ return;
140
+ switch (e.key) {
141
+ case 'ArrowDown':
142
+ e.preventDefault();
143
+ move(1);
144
+ break;
145
+ case 'ArrowUp':
146
+ e.preventDefault();
147
+ move(-1);
148
+ break;
149
+ case 'Home':
150
+ e.preventDefault();
151
+ setOpen(true);
152
+ setActiveIndex(filtered.length ? 0 : -1);
153
+ break;
154
+ case 'End':
155
+ e.preventDefault();
156
+ setOpen(true);
157
+ setActiveIndex(filtered.length ? filtered.length - 1 : -1);
158
+ break;
159
+ case 'PageDown':
160
+ e.preventDefault();
161
+ pageMove(1);
162
+ break;
163
+ case 'PageUp':
164
+ e.preventDefault();
165
+ pageMove(-1);
166
+ break;
167
+ case 'Enter':
168
+ if (open && activeIndex >= 0) {
169
+ e.preventDefault();
170
+ commitByIndex(activeIndex);
171
+ }
172
+ break;
173
+ case 'Escape':
174
+ e.preventDefault();
175
+ close();
176
+ break;
177
+ case 'Tab':
178
+ // 관례: 탭 이동 시 팝오버 닫기
179
+ close();
180
+ break;
181
+ }
182
+ onKeyDown?.(e);
183
+ }, ...props }), jsx("span", { "aria-live": 'polite', style: {
184
+ position: 'absolute',
185
+ width: 1,
186
+ height: 1,
187
+ overflow: 'hidden',
188
+ clip: 'rect(0 0 0 0)',
189
+ whiteSpace: 'nowrap',
190
+ }, children: open ? `${filtered.length} results.` : '' })] }));
191
+ };
192
+ /**
193
+ * Options
194
+ * - portal
195
+ * - outside click close
196
+ * - virtualization (items + renderItem provided)
197
+ */
198
+ const Options = ({ items, renderItem, itemHeight = 36, maxVisibleItems = 8, overscan = 3, children, ...props }) => {
199
+ const bridge = useItemsBridge();
200
+ const { open, setOpen, close, inputRef, listboxId, filtered, activeIndex, setActiveIndex, commitByIndex, } = useAutocomplete();
201
+ // items 주입(가상화 모드)
202
+ useEffect(() => {
203
+ if (items && bridge)
204
+ bridge(items);
205
+ }, [items, bridge]);
206
+ const popoverRef = useRef(null);
207
+ const triggerWidth = inputRef.current?.getBoundingClientRect().width;
208
+ // outside click
209
+ useEffect(() => {
210
+ if (!open)
211
+ return;
212
+ const onDown = (e) => {
213
+ const t = e.target;
214
+ if (inputRef.current?.contains(t) || popoverRef.current?.contains(t))
215
+ return;
216
+ close();
217
+ };
218
+ document.addEventListener('mousedown', onDown);
219
+ return () => document.removeEventListener('mousedown', onDown);
220
+ }, [open, close, inputRef]);
221
+ // scroll container ref for virtualization
222
+ const scrollRef = useRef(null);
223
+ // active option이 항상 보이게 스크롤 보정
224
+ useEffect(() => {
225
+ if (!open)
226
+ return;
227
+ if (!scrollRef.current)
228
+ return;
229
+ if (activeIndex < 0)
230
+ return;
231
+ const top = activeIndex * itemHeight;
232
+ const bottom = top + itemHeight;
233
+ const viewTop = scrollRef.current.scrollTop;
234
+ const viewBottom = viewTop + scrollRef.current.clientHeight;
235
+ if (top < viewTop)
236
+ scrollRef.current.scrollTop = top;
237
+ else if (bottom > viewBottom)
238
+ scrollRef.current.scrollTop = bottom - scrollRef.current.clientHeight;
239
+ }, [open, activeIndex, itemHeight]);
240
+ // virtualization range
241
+ const total = filtered.length;
242
+ const viewportCount = Math.min(maxVisibleItems, Math.max(1, total));
243
+ const viewportHeight = viewportCount * itemHeight;
244
+ const [scrollTop, setScrollTop] = useState(0);
245
+ const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
246
+ const endIndex = Math.min(total, Math.ceil((scrollTop + viewportHeight) / itemHeight) + overscan);
247
+ const visible = filtered.slice(startIndex, endIndex);
248
+ const { portal } = usePortal({
249
+ visible: open,
250
+ targetRef: inputRef,
251
+ popoverRef,
252
+ direction: 'bottom',
253
+ gap: 4,
254
+ content: (jsx("div", { ref: popoverRef, style: { width: triggerWidth }, ...props, children: items && renderItem ? (jsx("div", { id: listboxId, role: 'listbox', ref: scrollRef, style: { maxHeight: viewportHeight, overflow: 'auto', position: 'relative' }, onScroll: (e) => setScrollTop(e.target.scrollTop), children: jsx("div", { style: { height: total * itemHeight, position: 'relative' }, children: visible.map((item, i) => {
255
+ const index = startIndex + i; // filtered index
256
+ const isActive = index === activeIndex;
257
+ const isSelected = item.value === undefined; // selection 표시를 더 강하게 원하면 ctx.selectedValue 비교해서 쓰면 됨
258
+ return (jsx("div", { id: `${listboxId}-opt-${index}`, role: 'option', "aria-selected": isSelected, "aria-disabled": item.disabled, "data-active": isActive, tabIndex: -1, onMouseEnter: () => setActiveIndex(index), onMouseDown: (e) => {
259
+ // 클릭 시 input blur 방지(중요)
260
+ e.preventDefault();
261
+ }, onClick: () => commitByIndex(index), style: {
262
+ position: 'absolute',
263
+ top: index * itemHeight,
264
+ left: 0,
265
+ right: 0,
266
+ height: itemHeight,
267
+ display: 'flex',
268
+ alignItems: 'center',
269
+ }, children: renderItem(item) }, item.value));
270
+ }) }) })) : (
271
+ /* ✅ children 모드(비가상화) */
272
+ jsx("div", { id: listboxId, role: 'listbox', children: children })) })),
273
+ });
274
+ return open ? portal : null;
275
+ };
276
+ /**
277
+ * Option (children 모드 전용 / small list)
278
+ * - a11y option role/ids만 최소 보장
279
+ * - 가상화는 여기엔 적용하지 않음
280
+ */
281
+ const Option = ({ value, label, disabled, children, ...props }) => {
282
+ const { listboxId, filtered, query, open, setOpen, setQuery, setSelectedValue, activeIndex, setActiveIndex, } = useAutocomplete();
283
+ // children 모드에서 필터링은 “간단 버전”
284
+ const visible = useMemo(() => {
285
+ if (!query.trim())
286
+ return true;
287
+ return label.toLowerCase().includes(query.trim().toLowerCase());
288
+ }, [label, query]);
289
+ useMemo(() => {
290
+ // filtered는 items 모드에서만 의미있음.
291
+ // children 모드는 간단히 -1 처리(aria-activedescendant는 items 모드가 권장)
292
+ return -1;
293
+ }, []);
294
+ if (!visible)
295
+ return null;
296
+ return (jsx("div", { role: "option", "aria-disabled": disabled, "aria-selected": false, tabIndex: -1, onMouseDown: (e) => e.preventDefault(), onClick: () => {
297
+ if (disabled)
298
+ return;
299
+ setSelectedValue(value);
300
+ setQuery(label);
301
+ setOpen(false);
302
+ setActiveIndex(-1);
303
+ }, ...props, children: children ?? label }));
304
+ };
305
+ Object.assign(AutocompleteContainer, {
306
+ Input,
307
+ Options,
308
+ Option,
309
+ });
@@ -0,0 +1,36 @@
1
+ import { HTMLAttributes, InputHTMLAttributes, ReactNode } from 'react';
2
+ export type AutocompleteItem = {
3
+ value: string;
4
+ label: string;
5
+ disabled?: boolean;
6
+ };
7
+ export interface AutocompleteProps {
8
+ value: string | null;
9
+ onChange: (v: string | null) => void;
10
+ inputValue?: string;
11
+ onInputChange?: (v: string) => void;
12
+ disabled?: boolean;
13
+ /** 옵션 필터 커스터마이즈 */
14
+ filterFn?: (item: AutocompleteItem, query: string) => boolean;
15
+ children: ReactNode;
16
+ }
17
+ export interface AutocompleteInputProps extends InputHTMLAttributes<HTMLInputElement> {
18
+ /** label 연결용 (없으면 aria-label 필수) */
19
+ 'aria-label'?: string;
20
+ }
21
+ export interface AutocompleteOptionsProps extends HTMLAttributes<HTMLDivElement> {
22
+ /** ✅ 가상화 모드: 데이터 기반 */
23
+ items?: AutocompleteItem[];
24
+ /** 가상화 모드에서 항목 렌더 */
25
+ renderItem?: (item: AutocompleteItem) => ReactNode;
26
+ /** virtualization 옵션 */
27
+ itemHeight?: number;
28
+ maxVisibleItems?: number;
29
+ overscan?: number;
30
+ }
31
+ export interface AutocompleteOptionProps extends HTMLAttributes<HTMLDivElement> {
32
+ /** children 모드(비가상화) */
33
+ value: string;
34
+ label: string;
35
+ disabled?: boolean;
36
+ }
@@ -0,0 +1 @@
1
+ export * from './Autocomplete';
@@ -1,4 +1,4 @@
1
- import { ChangeEvent, CompositionEvent, HTMLProps } from 'react';
1
+ import { ChangeEvent, CompositionEvent, InputHTMLAttributes } from 'react';
2
2
  /**
3
3
  * 고급 기능을 제공하는 TextInput 컴포넌트 props
4
4
  *
@@ -9,7 +9,7 @@ import { ChangeEvent, CompositionEvent, HTMLProps } from 'react';
9
9
  * - IME(한글 입력) 대응
10
10
  * - 공백 제어
11
11
  */
12
- export interface TextInputProps extends HTMLProps<HTMLInputElement> {
12
+ export interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> {
13
13
  /**
14
14
  * 입력값 검증 함수
15
15
  *
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import type { AutocompleteInputProps, AutocompleteOptionProps, AutocompleteOptionsProps, AutocompleteProps } from './Autocomplete.type';
3
+ declare const Autocomplete: (({ value, onChange, inputValue, onInputChange, disabled, filterFn, children, }: AutocompleteProps) => import("react/jsx-runtime").JSX.Element) & {
4
+ Input: ({ onKeyDown, onFocus, onChange, ...props }: AutocompleteInputProps) => import("react/jsx-runtime").JSX.Element;
5
+ Options: ({ items, renderItem, itemHeight, maxVisibleItems, overscan, children, ...props }: AutocompleteOptionsProps) => React.ReactPortal | null;
6
+ Option: ({ value, label, disabled, children, ...props }: AutocompleteOptionProps) => import("react/jsx-runtime").JSX.Element | null;
7
+ };
8
+ export default Autocomplete;
@@ -0,0 +1,311 @@
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 AutocompleteContext = react.createContext(null);
8
+ const useAutocomplete = () => {
9
+ const ctx = react.useContext(AutocompleteContext);
10
+ if (!ctx)
11
+ throw new Error('Autocomplete components must be used within <Autocomplete>');
12
+ return ctx;
13
+ };
14
+ const defaultFilter = (item, query) => item.label.toLowerCase().includes(query.trim().toLowerCase());
15
+ /**
16
+ * Root
17
+ */
18
+ const AutocompleteContainer = ({ value, onChange, inputValue, onInputChange, disabled, filterFn = defaultFilter, children, }) => {
19
+ const [open, setOpen] = react.useState(false);
20
+ const [activeIndex, setActiveIndex] = react.useState(-1);
21
+ const inputRef = react.useRef(null);
22
+ // controlled/uncontrolled query
23
+ const [internalQuery, setInternalQuery] = react.useState('');
24
+ const query = inputValue ?? internalQuery;
25
+ const setQuery = (v) => {
26
+ onInputChange?.(v);
27
+ if (inputValue === undefined)
28
+ setInternalQuery(v);
29
+ };
30
+ const listboxId = react.useId();
31
+ // NOTE:
32
+ // - virtualization을 제대로 하려면 options data(items)가 Root에 필요하지만,
33
+ // compound API 유지를 위해 Options에서 items를 주입받아 Root에서 filtered를 만들기 어렵다.
34
+ // 그래서 Root에서는 "filtered"를 Options에서 제공할 수 있게 설계하면 복잡해짐.
35
+ // ✅ 해결: Options에 items를 주면 Root가 접근할 수 있도록 "itemsRef"를 둔다.
36
+ const itemsRef = react.useRef([]);
37
+ const setItems = (items) => {
38
+ itemsRef.current = items;
39
+ };
40
+ const filtered = react.useMemo(() => {
41
+ const src = itemsRef.current ?? [];
42
+ if (!query.trim())
43
+ return src;
44
+ return src.filter((it) => filterFn(it, query));
45
+ }, [query, filterFn]);
46
+ const close = () => {
47
+ setOpen(false);
48
+ setActiveIndex(-1);
49
+ };
50
+ const commitByIndex = (index) => {
51
+ const item = filtered[index];
52
+ if (!item || item.disabled)
53
+ return;
54
+ onChange(item.value);
55
+ setQuery(item.label);
56
+ close();
57
+ };
58
+ // 외부 value 변경 시 input label 동기화
59
+ react.useEffect(() => {
60
+ if (!value)
61
+ return;
62
+ const found = itemsRef.current.find((i) => i.value === value);
63
+ if (found)
64
+ setQuery(found.label);
65
+ // eslint-disable-next-line react-hooks/exhaustive-deps
66
+ }, [value]);
67
+ const statusText = react.useMemo(() => {
68
+ if (!open)
69
+ return '';
70
+ const n = filtered.length;
71
+ if (n === 0)
72
+ return 'No results.';
73
+ if (n === 1)
74
+ return '1 result available.';
75
+ return `${n} results available.`;
76
+ }, [open, filtered.length]);
77
+ return (jsxRuntime.jsxs(AutocompleteContext.Provider, { value: {
78
+ open,
79
+ setOpen,
80
+ selectedValue: value,
81
+ setSelectedValue: onChange,
82
+ query,
83
+ setQuery,
84
+ disabled,
85
+ inputRef,
86
+ listboxId,
87
+ activeIndex,
88
+ setActiveIndex,
89
+ filtered,
90
+ statusText,
91
+ commitByIndex,
92
+ close,
93
+ }, children: [jsxRuntime.jsx(ItemsBridge, { onItems: setItems }), children] }));
94
+ };
95
+ /**
96
+ * Options에서 items를 주입해주기 위한 브릿지(보이지 않는 컴포넌트)
97
+ * - Options가 items prop을 받으면, 내부에서 window.__ 같은 걸 쓰지 않고 Root ref에 주입
98
+ */
99
+ const ItemsBridgeContext = react.createContext(null);
100
+ const ItemsBridge = ({ onItems }) => {
101
+ return jsxRuntime.jsx(ItemsBridgeContext.Provider, { value: onItems });
102
+ };
103
+ const useItemsBridge = () => react.useContext(ItemsBridgeContext);
104
+ /**
105
+ * Input (Combobox Trigger)
106
+ * - 포커스는 input 유지
107
+ * - aria-activedescendant로 active option을 알려줌
108
+ */
109
+ const Input = ({ onKeyDown, onFocus, onChange, ...props }) => {
110
+ const { open, setOpen, query, setQuery, disabled, listboxId, activeIndex, setActiveIndex, filtered, commitByIndex, close, inputRef, } = useAutocomplete();
111
+ const activeId = activeIndex >= 0 ? `${listboxId}-opt-${activeIndex}` : undefined;
112
+ const move = (delta) => {
113
+ if (!filtered.length)
114
+ return;
115
+ setOpen(true);
116
+ setActiveIndex(activeIndex < 0 ? 0 : (activeIndex + delta + filtered.length) % filtered.length);
117
+ };
118
+ const pageMove = (deltaPages) => {
119
+ if (!filtered.length)
120
+ return;
121
+ setOpen(true);
122
+ // 10개 단위 이동(관례). 필요하면 props로 빼도 됨
123
+ const jump = 10 * deltaPages;
124
+ setActiveIndex(Math.max(0, Math.min(filtered.length - 1, (activeIndex < 0 ? 0 : activeIndex) + jump)));
125
+ };
126
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("input", { ref: inputRef, role: 'combobox', "aria-autocomplete": 'list', "aria-expanded": open, "aria-controls": listboxId, "aria-activedescendant": activeId, disabled: disabled, value: query, onFocus: (e) => {
127
+ if (!disabled) {
128
+ setOpen(true);
129
+ setActiveIndex(filtered.length ? 0 : -1);
130
+ }
131
+ onFocus?.(e);
132
+ }, onChange: (e) => {
133
+ if (disabled)
134
+ return;
135
+ setQuery(e.target.value);
136
+ setOpen(true);
137
+ setActiveIndex(0);
138
+ onChange?.(e);
139
+ }, onKeyDown: (e) => {
140
+ if (disabled)
141
+ return;
142
+ switch (e.key) {
143
+ case 'ArrowDown':
144
+ e.preventDefault();
145
+ move(1);
146
+ break;
147
+ case 'ArrowUp':
148
+ e.preventDefault();
149
+ move(-1);
150
+ break;
151
+ case 'Home':
152
+ e.preventDefault();
153
+ setOpen(true);
154
+ setActiveIndex(filtered.length ? 0 : -1);
155
+ break;
156
+ case 'End':
157
+ e.preventDefault();
158
+ setOpen(true);
159
+ setActiveIndex(filtered.length ? filtered.length - 1 : -1);
160
+ break;
161
+ case 'PageDown':
162
+ e.preventDefault();
163
+ pageMove(1);
164
+ break;
165
+ case 'PageUp':
166
+ e.preventDefault();
167
+ pageMove(-1);
168
+ break;
169
+ case 'Enter':
170
+ if (open && activeIndex >= 0) {
171
+ e.preventDefault();
172
+ commitByIndex(activeIndex);
173
+ }
174
+ break;
175
+ case 'Escape':
176
+ e.preventDefault();
177
+ close();
178
+ break;
179
+ case 'Tab':
180
+ // 관례: 탭 이동 시 팝오버 닫기
181
+ close();
182
+ break;
183
+ }
184
+ onKeyDown?.(e);
185
+ }, ...props }), jsxRuntime.jsx("span", { "aria-live": 'polite', style: {
186
+ position: 'absolute',
187
+ width: 1,
188
+ height: 1,
189
+ overflow: 'hidden',
190
+ clip: 'rect(0 0 0 0)',
191
+ whiteSpace: 'nowrap',
192
+ }, children: open ? `${filtered.length} results.` : '' })] }));
193
+ };
194
+ /**
195
+ * Options
196
+ * - portal
197
+ * - outside click close
198
+ * - virtualization (items + renderItem provided)
199
+ */
200
+ const Options = ({ items, renderItem, itemHeight = 36, maxVisibleItems = 8, overscan = 3, children, ...props }) => {
201
+ const bridge = useItemsBridge();
202
+ const { open, setOpen, close, inputRef, listboxId, filtered, activeIndex, setActiveIndex, commitByIndex, } = useAutocomplete();
203
+ // items 주입(가상화 모드)
204
+ react.useEffect(() => {
205
+ if (items && bridge)
206
+ bridge(items);
207
+ }, [items, bridge]);
208
+ const popoverRef = react.useRef(null);
209
+ const triggerWidth = inputRef.current?.getBoundingClientRect().width;
210
+ // outside click
211
+ react.useEffect(() => {
212
+ if (!open)
213
+ return;
214
+ const onDown = (e) => {
215
+ const t = e.target;
216
+ if (inputRef.current?.contains(t) || popoverRef.current?.contains(t))
217
+ return;
218
+ close();
219
+ };
220
+ document.addEventListener('mousedown', onDown);
221
+ return () => document.removeEventListener('mousedown', onDown);
222
+ }, [open, close, inputRef]);
223
+ // scroll container ref for virtualization
224
+ const scrollRef = react.useRef(null);
225
+ // active option이 항상 보이게 스크롤 보정
226
+ react.useEffect(() => {
227
+ if (!open)
228
+ return;
229
+ if (!scrollRef.current)
230
+ return;
231
+ if (activeIndex < 0)
232
+ return;
233
+ const top = activeIndex * itemHeight;
234
+ const bottom = top + itemHeight;
235
+ const viewTop = scrollRef.current.scrollTop;
236
+ const viewBottom = viewTop + scrollRef.current.clientHeight;
237
+ if (top < viewTop)
238
+ scrollRef.current.scrollTop = top;
239
+ else if (bottom > viewBottom)
240
+ scrollRef.current.scrollTop = bottom - scrollRef.current.clientHeight;
241
+ }, [open, activeIndex, itemHeight]);
242
+ // virtualization range
243
+ const total = filtered.length;
244
+ const viewportCount = Math.min(maxVisibleItems, Math.max(1, total));
245
+ const viewportHeight = viewportCount * itemHeight;
246
+ const [scrollTop, setScrollTop] = react.useState(0);
247
+ const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
248
+ const endIndex = Math.min(total, Math.ceil((scrollTop + viewportHeight) / itemHeight) + overscan);
249
+ const visible = filtered.slice(startIndex, endIndex);
250
+ const { portal } = usePortal({
251
+ visible: open,
252
+ targetRef: inputRef,
253
+ popoverRef,
254
+ direction: 'bottom',
255
+ gap: 4,
256
+ content: (jsxRuntime.jsx("div", { ref: popoverRef, style: { width: triggerWidth }, ...props, children: items && renderItem ? (jsxRuntime.jsx("div", { id: listboxId, role: 'listbox', ref: scrollRef, style: { maxHeight: viewportHeight, overflow: 'auto', position: 'relative' }, onScroll: (e) => setScrollTop(e.target.scrollTop), children: jsxRuntime.jsx("div", { style: { height: total * itemHeight, position: 'relative' }, children: visible.map((item, i) => {
257
+ const index = startIndex + i; // filtered index
258
+ const isActive = index === activeIndex;
259
+ const isSelected = item.value === undefined; // selection 표시를 더 강하게 원하면 ctx.selectedValue 비교해서 쓰면 됨
260
+ return (jsxRuntime.jsx("div", { id: `${listboxId}-opt-${index}`, role: 'option', "aria-selected": isSelected, "aria-disabled": item.disabled, "data-active": isActive, tabIndex: -1, onMouseEnter: () => setActiveIndex(index), onMouseDown: (e) => {
261
+ // 클릭 시 input blur 방지(중요)
262
+ e.preventDefault();
263
+ }, onClick: () => commitByIndex(index), style: {
264
+ position: 'absolute',
265
+ top: index * itemHeight,
266
+ left: 0,
267
+ right: 0,
268
+ height: itemHeight,
269
+ display: 'flex',
270
+ alignItems: 'center',
271
+ }, children: renderItem(item) }, item.value));
272
+ }) }) })) : (
273
+ /* ✅ children 모드(비가상화) */
274
+ jsxRuntime.jsx("div", { id: listboxId, role: 'listbox', children: children })) })),
275
+ });
276
+ return open ? portal : null;
277
+ };
278
+ /**
279
+ * Option (children 모드 전용 / small list)
280
+ * - a11y option role/ids만 최소 보장
281
+ * - 가상화는 여기엔 적용하지 않음
282
+ */
283
+ const Option = ({ value, label, disabled, children, ...props }) => {
284
+ const { listboxId, filtered, query, open, setOpen, setQuery, setSelectedValue, activeIndex, setActiveIndex, } = useAutocomplete();
285
+ // children 모드에서 필터링은 “간단 버전”
286
+ const visible = react.useMemo(() => {
287
+ if (!query.trim())
288
+ return true;
289
+ return label.toLowerCase().includes(query.trim().toLowerCase());
290
+ }, [label, query]);
291
+ react.useMemo(() => {
292
+ // filtered는 items 모드에서만 의미있음.
293
+ // children 모드는 간단히 -1 처리(aria-activedescendant는 items 모드가 권장)
294
+ return -1;
295
+ }, []);
296
+ if (!visible)
297
+ return null;
298
+ return (jsxRuntime.jsx("div", { role: "option", "aria-disabled": disabled, "aria-selected": false, tabIndex: -1, onMouseDown: (e) => e.preventDefault(), onClick: () => {
299
+ if (disabled)
300
+ return;
301
+ setSelectedValue(value);
302
+ setQuery(label);
303
+ setOpen(false);
304
+ setActiveIndex(-1);
305
+ }, ...props, children: children ?? label }));
306
+ };
307
+ Object.assign(AutocompleteContainer, {
308
+ Input,
309
+ Options,
310
+ Option,
311
+ });
@@ -0,0 +1,36 @@
1
+ import { HTMLAttributes, InputHTMLAttributes, ReactNode } from 'react';
2
+ export type AutocompleteItem = {
3
+ value: string;
4
+ label: string;
5
+ disabled?: boolean;
6
+ };
7
+ export interface AutocompleteProps {
8
+ value: string | null;
9
+ onChange: (v: string | null) => void;
10
+ inputValue?: string;
11
+ onInputChange?: (v: string) => void;
12
+ disabled?: boolean;
13
+ /** 옵션 필터 커스터마이즈 */
14
+ filterFn?: (item: AutocompleteItem, query: string) => boolean;
15
+ children: ReactNode;
16
+ }
17
+ export interface AutocompleteInputProps extends InputHTMLAttributes<HTMLInputElement> {
18
+ /** label 연결용 (없으면 aria-label 필수) */
19
+ 'aria-label'?: string;
20
+ }
21
+ export interface AutocompleteOptionsProps extends HTMLAttributes<HTMLDivElement> {
22
+ /** ✅ 가상화 모드: 데이터 기반 */
23
+ items?: AutocompleteItem[];
24
+ /** 가상화 모드에서 항목 렌더 */
25
+ renderItem?: (item: AutocompleteItem) => ReactNode;
26
+ /** virtualization 옵션 */
27
+ itemHeight?: number;
28
+ maxVisibleItems?: number;
29
+ overscan?: number;
30
+ }
31
+ export interface AutocompleteOptionProps extends HTMLAttributes<HTMLDivElement> {
32
+ /** children 모드(비가상화) */
33
+ value: string;
34
+ label: string;
35
+ disabled?: boolean;
36
+ }
@@ -0,0 +1 @@
1
+ export * from './Autocomplete';
@@ -1,4 +1,4 @@
1
- import { ChangeEvent, CompositionEvent, HTMLProps } from 'react';
1
+ import { ChangeEvent, CompositionEvent, InputHTMLAttributes } from 'react';
2
2
  /**
3
3
  * 고급 기능을 제공하는 TextInput 컴포넌트 props
4
4
  *
@@ -9,7 +9,7 @@ import { ChangeEvent, CompositionEvent, HTMLProps } from 'react';
9
9
  * - IME(한글 입력) 대응
10
10
  * - 공백 제어
11
11
  */
12
- export interface TextInputProps extends HTMLProps<HTMLInputElement> {
12
+ export interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> {
13
13
  /**
14
14
  * 입력값 검증 함수
15
15
  *
@@ -2,3 +2,4 @@ export * from './hooks';
2
2
  export * from './Tooltip';
3
3
  export * from './Input';
4
4
  export * from './Select';
5
+ export * from './Autocomplete';
@@ -7,6 +7,7 @@ var useDebounce = require('./hooks/useDebounce.js');
7
7
  var useThrottle = require('./hooks/useThrottle.js');
8
8
  var Tooltip = require('./Tooltip/Tooltip.js');
9
9
  var Select = require('./Select/Select.js');
10
+ require('./Autocomplete/Autocomplete.js');
10
11
 
11
12
 
12
13
 
@@ -2,3 +2,4 @@ export * from './hooks';
2
2
  export * from './Tooltip';
3
3
  export * from './Input';
4
4
  export * from './Select';
5
+ export * from './Autocomplete';
@@ -5,3 +5,4 @@ export { useDebounce } from './hooks/useDebounce.js';
5
5
  export { useThrottle } from './hooks/useThrottle.js';
6
6
  export { Tooltip } from './Tooltip/Tooltip.js';
7
7
  export { useSelectContext } from './Select/Select.js';
8
+ import './Autocomplete/Autocomplete.js';
package/package.json CHANGED
@@ -1,57 +1,25 @@
1
1
  {
2
2
  "name": "jy-headless",
3
- "version": "0.3.7",
3
+ "version": "0.3.11",
4
4
  "description": "A lightweight and customizable headless UI library for React components",
5
- "license": "MIT",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
6
8
  "repository": "https://github.com/yCZwIqY/jy-headless",
7
- "main": "./cjs/index.js",
8
- "module": "./index.js",
9
- "types": "./index.d.ts",
9
+ "scripts": {
10
+ "build": " rollup -c",
11
+ "build-publish": "pnpm run build && node setupPackage.mjs && cd dist && npm publish",
12
+ "storybook": "storybook dev -p 6006",
13
+ "build-storybook": "storybook build"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
10
18
  "exports": {
11
19
  ".": {
12
- "import": "./index.js",
13
- "require": "./cjs/index.js",
14
- "types": "./index.d.ts"
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
- },
31
- "./cjs": {
32
- "import": "./cjs/index.js",
33
- "require": "./cjs/cjs/index.js",
34
- "types": "./cjs/index.d.ts"
35
- },
36
- "./useDebounce": {
37
- "import": "./hooks/useDebounce.js",
38
- "require": "./cjs/hooks/useDebounce.js",
39
- "types": "./hooks/useDebounce.d.ts"
40
- },
41
- "./usePortal": {
42
- "import": "./hooks/usePortal.js",
43
- "require": "./cjs/hooks/usePortal.js",
44
- "types": "./hooks/usePortal.d.ts"
45
- },
46
- "./useThrottle": {
47
- "import": "./hooks/useThrottle.js",
48
- "require": "./cjs/hooks/useThrottle.js",
49
- "types": "./hooks/useThrottle.d.ts"
50
- },
51
- "./index": {
52
- "import": "./index.js",
53
- "require": "./cjs/index.js",
54
- "types": "./index.d.ts"
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/cjs/index.js",
22
+ "types": "./dist/index.d.ts"
55
23
  }
56
24
  },
57
25
  "keywords": [
@@ -61,5 +29,53 @@
61
29
  "ui-library",
62
30
  "tailwind",
63
31
  "storybook"
64
- ]
65
- }
32
+ ],
33
+ "author": "",
34
+ "license": "MIT",
35
+ "type": "module",
36
+ "devDependencies": {
37
+ "@babel/core": "^7.26.10",
38
+ "@babel/preset-env": "^7.26.7",
39
+ "@babel/preset-react": "^7.26.3",
40
+ "@chromatic-com/storybook": "3.2.4",
41
+ "@rollup/plugin-babel": "^6.0.4",
42
+ "@rollup/plugin-commonjs": "^28.0.2",
43
+ "@rollup/plugin-node-resolve": "^16.0.0",
44
+ "@storybook/addon-essentials": "^8.5.1",
45
+ "@storybook/addon-interactions": "^8.5.1",
46
+ "@storybook/addon-onboarding": "^8.5.1",
47
+ "@storybook/addon-postcss": "^2.0.0",
48
+ "@storybook/blocks": "^8.5.1",
49
+ "@storybook/react": "^8.5.1",
50
+ "@storybook/react-vite": "^8.5.1",
51
+ "@storybook/test": "^8.5.1",
52
+ "@testing-library/jest-dom": "^6.6.3",
53
+ "@testing-library/react": "^16.2.0",
54
+ "@types/jest": "^29.5.14",
55
+ "@types/react": "^19.0.7",
56
+ "@types/react-dom": "^19.0.3",
57
+ "babel-jest": "^29.7.0",
58
+ "chromatic": "^11.25.1",
59
+ "jest": "^29.7.0",
60
+ "jest-environment-jsdom": "^29.7.0",
61
+ "postcss": "^8.5.1",
62
+ "prettier": "^3.4.2",
63
+ "react": "^19.0.0",
64
+ "react-dom": "^19.0.0",
65
+ "rollup": "^4.31.0",
66
+ "rollup-plugin-postcss": "^4.0.2",
67
+ "rollup-plugin-typescript2": "^0.36.0",
68
+ "storybook": "^8.5.1",
69
+ "ts-jest": "^29.2.5",
70
+ "typescript": "^5.7.3"
71
+ },
72
+ "dependencies": {
73
+ "@testing-library/user-event": "^14.6.1",
74
+ "rollup-plugin-peer-deps-external": "^2.2.4",
75
+ "tslib": "^2.8.1"
76
+ },
77
+ "peerDependencies": {
78
+ "react": "^18.0.0 || ^19.0.0",
79
+ "react-dom": "^18.0.0 || ^19.0.0"
80
+ }
81
+ }
@@ -1,2 +0,0 @@
1
- import { PopoverProps } from './Popover.type';
2
- export declare const Popover: ({ direction, popover, children, key, gap, autoFlip }: PopoverProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,28 +0,0 @@
1
- import { jsxs } from 'react/jsx-runtime';
2
- import { useState, useRef } from 'react';
3
- import usePortal from '../hooks/usePortal.js';
4
-
5
- const Popover = ({ direction = 'top', popover, children, key, gap = 0, autoFlip = true }) => {
6
- const [visible, setVisible] = useState(false);
7
- const targetRef = useRef(null);
8
- const popoverRef = useRef(null);
9
- const { portal, rootDom } = usePortal({
10
- content: popover,
11
- key,
12
- visible,
13
- targetRef,
14
- popoverRef,
15
- direction,
16
- gap,
17
- autoFlip,
18
- });
19
- const handleMouseEnter = () => {
20
- setVisible(true);
21
- };
22
- const handleMouseLeave = () => {
23
- setVisible(false);
24
- };
25
- return (jsxs("span", { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ref: targetRef, children: [children, rootDom && visible && portal] }));
26
- };
27
-
28
- export { Popover };
@@ -1,12 +0,0 @@
1
- import { ReactNode } from 'react';
2
- import { Direction } from '../hooks';
3
- export interface PopoverProps {
4
- children: ReactNode;
5
- popover: ReactNode;
6
- direction: Direction;
7
- targetId?: string;
8
- domNode?: Element;
9
- key?: string;
10
- gap?: number;
11
- autoFlip?: boolean;
12
- }
@@ -1,2 +0,0 @@
1
- export * from './Popover';
2
- export * from './Popover.type';
@@ -1,2 +0,0 @@
1
- import { PopoverProps } from './Popover.type';
2
- export declare const Popover: ({ direction, popover, children, key, gap, autoFlip }: PopoverProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,30 +0,0 @@
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 Popover = ({ direction = 'top', popover, children, key, gap = 0, autoFlip = true }) => {
8
- const [visible, setVisible] = react.useState(false);
9
- const targetRef = react.useRef(null);
10
- const popoverRef = react.useRef(null);
11
- const { portal, rootDom } = usePortal({
12
- content: popover,
13
- key,
14
- visible,
15
- targetRef,
16
- popoverRef,
17
- direction,
18
- gap,
19
- autoFlip,
20
- });
21
- const handleMouseEnter = () => {
22
- setVisible(true);
23
- };
24
- const handleMouseLeave = () => {
25
- setVisible(false);
26
- };
27
- return (jsxRuntime.jsxs("span", { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ref: targetRef, children: [children, rootDom && visible && portal] }));
28
- };
29
-
30
- exports.Popover = Popover;
@@ -1,12 +0,0 @@
1
- import { ReactNode } from 'react';
2
- import { Direction } from '../hooks';
3
- export interface PopoverProps {
4
- children: ReactNode;
5
- popover: ReactNode;
6
- direction: Direction;
7
- targetId?: string;
8
- domNode?: Element;
9
- key?: string;
10
- gap?: number;
11
- autoFlip?: boolean;
12
- }
@@ -1,2 +0,0 @@
1
- export * from './Popover';
2
- export * from './Popover.type';
package/version.txt DELETED
@@ -1 +0,0 @@
1
- 0.3.7
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes