jy-headless 0.3.11 → 0.3.13

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.
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import type { AutocompleteInputProps, AutocompleteOptionProps, AutocompleteOptionsProps, AutocompleteProps } from './Autocomplete.type';
3
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;
4
+ Input: ({ onKeyDown, onFocus, onChange, onCompositionStart, onCompositionEnd, ...props }: AutocompleteInputProps) => import("react/jsx-runtime").JSX.Element;
5
5
  Options: ({ items, renderItem, itemHeight, maxVisibleItems, overscan, children, ...props }: AutocompleteOptionsProps) => React.ReactPortal | null;
6
6
  Option: ({ value, label, disabled, children, ...props }: AutocompleteOptionProps) => import("react/jsx-runtime").JSX.Element | null;
7
7
  };
@@ -1,6 +1,7 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import { createContext, useState, useRef, useId, useMemo, useEffect, useContext } from 'react';
3
3
  import usePortal from '../hooks/usePortal.js';
4
+ import TextInput from '../Input/TextInput.js';
4
5
 
5
6
  const AutocompleteContext = createContext(null);
6
7
  const useAutocomplete = () => {
@@ -104,8 +105,11 @@ const useItemsBridge = () => useContext(ItemsBridgeContext);
104
105
  * - 포커스는 input 유지
105
106
  * - aria-activedescendant로 active option을 알려줌
106
107
  */
107
- const Input = ({ onKeyDown, onFocus, onChange, ...props }) => {
108
+ // ...생략
109
+ const Input = ({ onKeyDown, onFocus, onChange, onCompositionStart, onCompositionEnd, ...props }) => {
108
110
  const { open, setOpen, query, setQuery, disabled, listboxId, activeIndex, setActiveIndex, filtered, commitByIndex, close, inputRef, } = useAutocomplete();
111
+ // ✅ IME 조합 중에는 방향키/엔터로 옵션 선택하지 않게 막기
112
+ const composingRef = useRef(false);
109
113
  const activeId = activeIndex >= 0 ? `${listboxId}-opt-${activeIndex}` : undefined;
110
114
  const move = (delta) => {
111
115
  if (!filtered.length)
@@ -113,15 +117,7 @@ const Input = ({ onKeyDown, onFocus, onChange, ...props }) => {
113
117
  setOpen(true);
114
118
  setActiveIndex(activeIndex < 0 ? 0 : (activeIndex + delta + filtered.length) % filtered.length);
115
119
  };
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) => {
120
+ return (jsxs(Fragment, { children: [jsx(TextInput, { ref: inputRef, role: 'combobox', "aria-autocomplete": 'list', "aria-expanded": open, "aria-controls": listboxId, "aria-activedescendant": activeId, disabled: disabled, value: query, onFocus: (e) => {
125
121
  if (!disabled) {
126
122
  setOpen(true);
127
123
  setActiveIndex(filtered.length ? 0 : -1);
@@ -134,9 +130,20 @@ const Input = ({ onKeyDown, onFocus, onChange, ...props }) => {
134
130
  setOpen(true);
135
131
  setActiveIndex(0);
136
132
  onChange?.(e);
133
+ }, onCompositionStart: (e) => {
134
+ composingRef.current = true;
135
+ onCompositionStart?.(e);
136
+ }, onCompositionEnd: (e) => {
137
+ composingRef.current = false;
138
+ onCompositionEnd?.(e);
137
139
  }, onKeyDown: (e) => {
138
140
  if (disabled)
139
141
  return;
142
+ // ✅ 조합중이면 Autocomplete 키처리 하지 않음
143
+ if (composingRef.current) {
144
+ onKeyDown?.(e);
145
+ return;
146
+ }
140
147
  switch (e.key) {
141
148
  case 'ArrowDown':
142
149
  e.preventDefault();
@@ -146,24 +153,6 @@ const Input = ({ onKeyDown, onFocus, onChange, ...props }) => {
146
153
  e.preventDefault();
147
154
  move(-1);
148
155
  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
156
  case 'Enter':
168
157
  if (open && activeIndex >= 0) {
169
158
  e.preventDefault();
@@ -175,7 +164,6 @@ const Input = ({ onKeyDown, onFocus, onChange, ...props }) => {
175
164
  close();
176
165
  break;
177
166
  case 'Tab':
178
- // 관례: 탭 이동 시 팝오버 닫기
179
167
  close();
180
168
  break;
181
169
  }
@@ -1,4 +1,5 @@
1
- import { HTMLAttributes, InputHTMLAttributes, ReactNode } from 'react';
1
+ import { HTMLAttributes, ReactNode } from 'react';
2
+ import { TextInputProps } from '../Input/TextInput.type';
2
3
  export type AutocompleteItem = {
3
4
  value: string;
4
5
  label: string;
@@ -14,7 +15,7 @@ export interface AutocompleteProps {
14
15
  filterFn?: (item: AutocompleteItem, query: string) => boolean;
15
16
  children: ReactNode;
16
17
  }
17
- export interface AutocompleteInputProps extends InputHTMLAttributes<HTMLInputElement> {
18
+ export interface AutocompleteInputProps extends TextInputProps {
18
19
  /** label 연결용 (없으면 aria-label 필수) */
19
20
  'aria-label'?: string;
20
21
  }
@@ -1 +1,2 @@
1
1
  export * from './Autocomplete';
2
+ export * from './Autocomplete.type';
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import { TextInputProps } from './TextInput.type';
2
- declare const TextInput: ({ maxLength, onChange, pattern, onValidate, validator, onCompositionStart, onCompositionEnd, disallowPattern, trimWhitespace, debounceMs, throttleMs, onDebouncedChange, onThrottledChange, onBlur, ...props }: TextInputProps) => import("react/jsx-runtime").JSX.Element;
3
+ declare const TextInput: React.ForwardRefExoticComponent<TextInputProps & React.RefAttributes<HTMLInputElement>>;
3
4
  export default TextInput;
@@ -0,0 +1,77 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { forwardRef, useState, useRef, useMemo } from 'react';
3
+ import 'react-dom';
4
+ import { useDebounce } from '../hooks/useDebounce.js';
5
+ import { useThrottle } from '../hooks/useThrottle.js';
6
+
7
+ function mergeRefs(...refs) {
8
+ return (value) => {
9
+ refs.forEach((ref) => {
10
+ if (!ref)
11
+ return;
12
+ if (typeof ref === 'function')
13
+ ref(value);
14
+ else
15
+ ref.current = value;
16
+ });
17
+ };
18
+ }
19
+ const TextInput = forwardRef(({ maxLength, onChange, pattern, onValidate, validator, onCompositionStart, onCompositionEnd, disallowPattern, trimWhitespace, debounceMs, throttleMs, onDebouncedChange, onThrottledChange, onBlur, ...props }, ref) => {
20
+ const [isComposing, setIsComposing] = useState(false);
21
+ const innerRef = useRef(null);
22
+ const combinedRef = useMemo(() => mergeRefs(innerRef, ref), [ref]);
23
+ const debouncedChange = useDebounce((value) => onDebouncedChange?.(value), debounceMs || 0);
24
+ const throttledChange = useThrottle((value) => onThrottledChange?.(value), throttleMs || 0);
25
+ const handleCompositionStart = (e) => {
26
+ setIsComposing(true);
27
+ onCompositionStart?.(e);
28
+ };
29
+ const handleCompositionEnd = (e) => {
30
+ setIsComposing(false);
31
+ onCompositionEnd?.(e);
32
+ };
33
+ const handleChange = (e) => {
34
+ if (maxLength && !isComposing && e.target.value.length > maxLength)
35
+ return;
36
+ if (pattern) {
37
+ const regex = new RegExp(pattern);
38
+ if (!regex.test(e.target.value))
39
+ return;
40
+ }
41
+ if (disallowPattern && !disallowPattern.test(e.target.value))
42
+ return;
43
+ if (debounceMs && onDebouncedChange)
44
+ debouncedChange(e.target.value);
45
+ if (throttleMs && onThrottledChange)
46
+ throttledChange(e.target.value);
47
+ if (validator) {
48
+ const result = validator(e.target.value);
49
+ const isValid = typeof result === 'boolean' ? result : true;
50
+ const error = typeof result === 'string' ? result : undefined;
51
+ onValidate?.(isValid, error);
52
+ if (!isValid)
53
+ return;
54
+ }
55
+ onChange?.(e);
56
+ };
57
+ const handleBlur = (e) => {
58
+ if (trimWhitespace && innerRef.current) {
59
+ const trimmedValue = e.target.value.trim();
60
+ if (trimmedValue !== e.target.value) {
61
+ innerRef.current.value = trimmedValue;
62
+ const syntheticEvent = {
63
+ ...e,
64
+ target: innerRef.current,
65
+ currentTarget: innerRef.current,
66
+ type: 'change',
67
+ };
68
+ onChange?.(syntheticEvent);
69
+ }
70
+ }
71
+ onBlur?.(e);
72
+ };
73
+ return (jsx("input", { ref: combinedRef, ...props, onBlur: handleBlur, onChange: handleChange, onCompositionStart: handleCompositionStart, onCompositionEnd: handleCompositionEnd }));
74
+ });
75
+ TextInput.displayName = 'TextInput';
76
+
77
+ export { TextInput as default };
@@ -1,2 +1,4 @@
1
1
  export * from './NumberInput';
2
2
  export * from './TextInput';
3
+ export * from './TextInput.type';
4
+ export * from './NumberInput.type';
@@ -1 +1,2 @@
1
1
  export * from './Select';
2
+ export * from './Select.type';
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import type { AutocompleteInputProps, AutocompleteOptionProps, AutocompleteOptionsProps, AutocompleteProps } from './Autocomplete.type';
3
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;
4
+ Input: ({ onKeyDown, onFocus, onChange, onCompositionStart, onCompositionEnd, ...props }: AutocompleteInputProps) => import("react/jsx-runtime").JSX.Element;
5
5
  Options: ({ items, renderItem, itemHeight, maxVisibleItems, overscan, children, ...props }: AutocompleteOptionsProps) => React.ReactPortal | null;
6
6
  Option: ({ value, label, disabled, children, ...props }: AutocompleteOptionProps) => import("react/jsx-runtime").JSX.Element | null;
7
7
  };
@@ -3,6 +3,7 @@
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var react = require('react');
5
5
  var usePortal = require('../hooks/usePortal.js');
6
+ var TextInput = require('../Input/TextInput.js');
6
7
 
7
8
  const AutocompleteContext = react.createContext(null);
8
9
  const useAutocomplete = () => {
@@ -106,8 +107,11 @@ const useItemsBridge = () => react.useContext(ItemsBridgeContext);
106
107
  * - 포커스는 input 유지
107
108
  * - aria-activedescendant로 active option을 알려줌
108
109
  */
109
- const Input = ({ onKeyDown, onFocus, onChange, ...props }) => {
110
+ // ...생략
111
+ const Input = ({ onKeyDown, onFocus, onChange, onCompositionStart, onCompositionEnd, ...props }) => {
110
112
  const { open, setOpen, query, setQuery, disabled, listboxId, activeIndex, setActiveIndex, filtered, commitByIndex, close, inputRef, } = useAutocomplete();
113
+ // ✅ IME 조합 중에는 방향키/엔터로 옵션 선택하지 않게 막기
114
+ const composingRef = react.useRef(false);
111
115
  const activeId = activeIndex >= 0 ? `${listboxId}-opt-${activeIndex}` : undefined;
112
116
  const move = (delta) => {
113
117
  if (!filtered.length)
@@ -115,15 +119,7 @@ const Input = ({ onKeyDown, onFocus, onChange, ...props }) => {
115
119
  setOpen(true);
116
120
  setActiveIndex(activeIndex < 0 ? 0 : (activeIndex + delta + filtered.length) % filtered.length);
117
121
  };
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) => {
122
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(TextInput, { ref: inputRef, role: 'combobox', "aria-autocomplete": 'list', "aria-expanded": open, "aria-controls": listboxId, "aria-activedescendant": activeId, disabled: disabled, value: query, onFocus: (e) => {
127
123
  if (!disabled) {
128
124
  setOpen(true);
129
125
  setActiveIndex(filtered.length ? 0 : -1);
@@ -136,9 +132,20 @@ const Input = ({ onKeyDown, onFocus, onChange, ...props }) => {
136
132
  setOpen(true);
137
133
  setActiveIndex(0);
138
134
  onChange?.(e);
135
+ }, onCompositionStart: (e) => {
136
+ composingRef.current = true;
137
+ onCompositionStart?.(e);
138
+ }, onCompositionEnd: (e) => {
139
+ composingRef.current = false;
140
+ onCompositionEnd?.(e);
139
141
  }, onKeyDown: (e) => {
140
142
  if (disabled)
141
143
  return;
144
+ // ✅ 조합중이면 Autocomplete 키처리 하지 않음
145
+ if (composingRef.current) {
146
+ onKeyDown?.(e);
147
+ return;
148
+ }
142
149
  switch (e.key) {
143
150
  case 'ArrowDown':
144
151
  e.preventDefault();
@@ -148,24 +155,6 @@ const Input = ({ onKeyDown, onFocus, onChange, ...props }) => {
148
155
  e.preventDefault();
149
156
  move(-1);
150
157
  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
158
  case 'Enter':
170
159
  if (open && activeIndex >= 0) {
171
160
  e.preventDefault();
@@ -177,7 +166,6 @@ const Input = ({ onKeyDown, onFocus, onChange, ...props }) => {
177
166
  close();
178
167
  break;
179
168
  case 'Tab':
180
- // 관례: 탭 이동 시 팝오버 닫기
181
169
  close();
182
170
  break;
183
171
  }
@@ -1,4 +1,5 @@
1
- import { HTMLAttributes, InputHTMLAttributes, ReactNode } from 'react';
1
+ import { HTMLAttributes, ReactNode } from 'react';
2
+ import { TextInputProps } from '../Input/TextInput.type';
2
3
  export type AutocompleteItem = {
3
4
  value: string;
4
5
  label: string;
@@ -14,7 +15,7 @@ export interface AutocompleteProps {
14
15
  filterFn?: (item: AutocompleteItem, query: string) => boolean;
15
16
  children: ReactNode;
16
17
  }
17
- export interface AutocompleteInputProps extends InputHTMLAttributes<HTMLInputElement> {
18
+ export interface AutocompleteInputProps extends TextInputProps {
18
19
  /** label 연결용 (없으면 aria-label 필수) */
19
20
  'aria-label'?: string;
20
21
  }
@@ -1 +1,2 @@
1
1
  export * from './Autocomplete';
2
+ export * from './Autocomplete.type';
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import { TextInputProps } from './TextInput.type';
2
- declare const TextInput: ({ maxLength, onChange, pattern, onValidate, validator, onCompositionStart, onCompositionEnd, disallowPattern, trimWhitespace, debounceMs, throttleMs, onDebouncedChange, onThrottledChange, onBlur, ...props }: TextInputProps) => import("react/jsx-runtime").JSX.Element;
3
+ declare const TextInput: React.ForwardRefExoticComponent<TextInputProps & React.RefAttributes<HTMLInputElement>>;
3
4
  export default TextInput;
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var react = require('react');
5
+ require('react-dom');
6
+ var useDebounce = require('../hooks/useDebounce.js');
7
+ var useThrottle = require('../hooks/useThrottle.js');
8
+
9
+ function mergeRefs(...refs) {
10
+ return (value) => {
11
+ refs.forEach((ref) => {
12
+ if (!ref)
13
+ return;
14
+ if (typeof ref === 'function')
15
+ ref(value);
16
+ else
17
+ ref.current = value;
18
+ });
19
+ };
20
+ }
21
+ const TextInput = react.forwardRef(({ maxLength, onChange, pattern, onValidate, validator, onCompositionStart, onCompositionEnd, disallowPattern, trimWhitespace, debounceMs, throttleMs, onDebouncedChange, onThrottledChange, onBlur, ...props }, ref) => {
22
+ const [isComposing, setIsComposing] = react.useState(false);
23
+ const innerRef = react.useRef(null);
24
+ const combinedRef = react.useMemo(() => mergeRefs(innerRef, ref), [ref]);
25
+ const debouncedChange = useDebounce.useDebounce((value) => onDebouncedChange?.(value), debounceMs || 0);
26
+ const throttledChange = useThrottle.useThrottle((value) => onThrottledChange?.(value), throttleMs || 0);
27
+ const handleCompositionStart = (e) => {
28
+ setIsComposing(true);
29
+ onCompositionStart?.(e);
30
+ };
31
+ const handleCompositionEnd = (e) => {
32
+ setIsComposing(false);
33
+ onCompositionEnd?.(e);
34
+ };
35
+ const handleChange = (e) => {
36
+ if (maxLength && !isComposing && e.target.value.length > maxLength)
37
+ return;
38
+ if (pattern) {
39
+ const regex = new RegExp(pattern);
40
+ if (!regex.test(e.target.value))
41
+ return;
42
+ }
43
+ if (disallowPattern && !disallowPattern.test(e.target.value))
44
+ return;
45
+ if (debounceMs && onDebouncedChange)
46
+ debouncedChange(e.target.value);
47
+ if (throttleMs && onThrottledChange)
48
+ throttledChange(e.target.value);
49
+ if (validator) {
50
+ const result = validator(e.target.value);
51
+ const isValid = typeof result === 'boolean' ? result : true;
52
+ const error = typeof result === 'string' ? result : undefined;
53
+ onValidate?.(isValid, error);
54
+ if (!isValid)
55
+ return;
56
+ }
57
+ onChange?.(e);
58
+ };
59
+ const handleBlur = (e) => {
60
+ if (trimWhitespace && innerRef.current) {
61
+ const trimmedValue = e.target.value.trim();
62
+ if (trimmedValue !== e.target.value) {
63
+ innerRef.current.value = trimmedValue;
64
+ const syntheticEvent = {
65
+ ...e,
66
+ target: innerRef.current,
67
+ currentTarget: innerRef.current,
68
+ type: 'change',
69
+ };
70
+ onChange?.(syntheticEvent);
71
+ }
72
+ }
73
+ onBlur?.(e);
74
+ };
75
+ return (jsxRuntime.jsx("input", { ref: combinedRef, ...props, onBlur: handleBlur, onChange: handleChange, onCompositionStart: handleCompositionStart, onCompositionEnd: handleCompositionEnd }));
76
+ });
77
+ TextInput.displayName = 'TextInput';
78
+
79
+ module.exports = TextInput;
@@ -1,2 +1,4 @@
1
1
  export * from './NumberInput';
2
2
  export * from './TextInput';
3
+ export * from './TextInput.type';
4
+ export * from './NumberInput.type';
@@ -1 +1,2 @@
1
1
  export * from './Select';
2
+ export * from './Select.type';
package/dist/cjs/index.js CHANGED
@@ -6,6 +6,7 @@ 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
+ require('./Input/TextInput.js');
9
10
  var Select = require('./Select/Select.js');
10
11
  require('./Autocomplete/Autocomplete.js');
11
12
 
package/dist/index.js CHANGED
@@ -4,5 +4,6 @@ import 'react-dom';
4
4
  export { useDebounce } from './hooks/useDebounce.js';
5
5
  export { useThrottle } from './hooks/useThrottle.js';
6
6
  export { Tooltip } from './Tooltip/Tooltip.js';
7
+ import './Input/TextInput.js';
7
8
  export { useSelectContext } from './Select/Select.js';
8
9
  import './Autocomplete/Autocomplete.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jy-headless",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "description": "A lightweight and customizable headless UI library for React components",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",