@team-monolith/cds 1.79.4 → 1.79.6

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.
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ export interface SelectionRange {
3
+ start: number;
4
+ end: number;
5
+ }
6
+ /** value, selectionRange에 대한 controlled input 컴포넌트입니다.
7
+ * 입력값과, 선택 범위에 대한 state, setState를 props로 받아 사용합니다.
8
+ */
9
+ export declare const InputControlledSelectionRange: React.ForwardRefExoticComponent<{
10
+ value: string;
11
+ selectionRange: SelectionRange | null;
12
+ onSelectionChange: (selectionRange: SelectionRange | null) => void;
13
+ } & Omit<React.InputHTMLAttributes<HTMLInputElement>, "value"> & React.RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,47 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx } from "react/jsx-runtime";
13
+ import React from "react";
14
+ import { useEffect } from "react";
15
+ /** value, selectionRange에 대한 controlled input 컴포넌트입니다.
16
+ * 입력값과, 선택 범위에 대한 state, setState를 props로 받아 사용합니다.
17
+ */
18
+ export const InputControlledSelectionRange = React.forwardRef(function InputControlledSelectionRange(props, ref) {
19
+ const { value, selectionRange, onSelectionChange } = props, inputProps = __rest(props, ["value", "selectionRange", "onSelectionChange"]);
20
+ // value 또는 selectionRange가 바뀔 때마다,
21
+ // 실제 DOM input의 selection range를 동기화
22
+ useEffect(() => {
23
+ if (typeof ref === "function" || !(ref === null || ref === void 0 ? void 0 : ref.current) || !selectionRange)
24
+ return;
25
+ const { start, end } = selectionRange;
26
+ try {
27
+ ref.current.setSelectionRange(start, end);
28
+ }
29
+ catch (err) {
30
+ // 범위가 잘못되거나 input이 포커스가 없을 때 에러가 발생할 수 있으므로 주의
31
+ console.warn("setSelectionRange error:", err);
32
+ }
33
+ }, [selectionRange, value]);
34
+ // selectionRange 변경 감지 핸들러 (React에선 onSelect 사용)
35
+ const handleSelect = (e) => {
36
+ const { selectionStart, selectionEnd } = e.currentTarget;
37
+ if (selectionStart != null && selectionEnd != null) {
38
+ onSelectionChange({
39
+ start: selectionStart,
40
+ end: selectionEnd,
41
+ });
42
+ }
43
+ };
44
+ return (_jsx("input", Object.assign({ ref: ref,
45
+ // Controlled input
46
+ value: value, onSelect: handleSelect }, inputProps)));
47
+ });
@@ -3,8 +3,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
3
3
  import { css, useTheme } from "@emotion/react";
4
4
  import styled from "@emotion/styled";
5
5
  import { InputBase } from "../../../../components/InputBase";
6
- import { useCallback, useRef, useState } from "react";
6
+ import { useRef, useState } from "react";
7
7
  import { useEventListener } from "usehooks-ts";
8
+ import { InputControlledSelectionRange, } from "./InputControlledSelectionRange";
8
9
  /**
9
10
  * SOT는 value이고, 그 value는 숨겨진 input에 작성됩니다.
10
11
  * 그리고 그 value는 split되어 segmented input에 표시됩니다.
@@ -19,14 +20,23 @@ export function SegmentedInput(props) {
19
20
  const despacedFormat = splitedFormat.filter((v) => v !== " ");
20
21
  // "안녕 하세요" => ["안","녕","하","세","요"]
21
22
  const splitedValues = value.split("").filter((v) => v !== " ");
22
- const [focusedIndex, setFocusedIndex] = useState(null);
23
+ // 현재 선택범위를 state로 관리합니다. 현재 커서 위치의 Source of Truth입니다.
24
+ const [selectionRange, setSelectionRange] = useState(null);
23
25
  // 글자를 조합중인지 여부 (ex: "안"을 입력중 "아"일 때 해당합니다.)
24
26
  const [isComposing, setIsComposing] = useState(false);
25
- const setComposingTrue = useCallback(() => setIsComposing(true), []);
26
- const setComposingFalse = useCallback(() => setIsComposing(false), []);
27
+ // 현재 포커스된 SquareInput의 index
28
+ const focusedIndex = !selectionRange
29
+ ? null
30
+ : isComposing
31
+ ? selectionRange.start - 1
32
+ : selectionRange.start;
27
33
  const hiddenRef = useRef(null);
28
- useEventListener("compositionstart", setComposingTrue, hiddenRef);
29
- useEventListener("compositionend", setComposingFalse, hiddenRef);
34
+ useEventListener("compositionstart", () => {
35
+ setIsComposing(true);
36
+ }, hiddenRef);
37
+ useEventListener("compositionend", () => {
38
+ setIsComposing(false);
39
+ }, hiddenRef);
30
40
  /** value의 공백을 제거하고, format에 맞춰 공백을 추가한 값을 리턴합니다. */
31
41
  const getFormattedValue = (value) => {
32
42
  const despacedValue = value.replace(/ /g, "");
@@ -39,23 +49,28 @@ export function SegmentedInput(props) {
39
49
  }
40
50
  }, "");
41
51
  };
42
- return (_jsxs(Container, { children: [_jsx("input", { type: "input", ref: hiddenRef, value: splitedValues.join(""), readOnly: readOnly, onKeyDown: (e) => {
52
+ return (_jsxs(Container, { children: [_jsx(InputControlledSelectionRange, { type: "input", ref: hiddenRef, value: splitedValues.join(""), readOnly: readOnly, selectionRange: selectionRange, onSelectionChange: setSelectionRange, onKeyDown: (e) => {
43
53
  var _a;
44
54
  if (e.key === "ArrowLeft") {
45
- if (focusedIndex !== null && focusedIndex > 0) {
46
- setFocusedIndex(focusedIndex - 1);
55
+ e.preventDefault();
56
+ if (selectionRange !== null && selectionRange.start > 0) {
57
+ setSelectionRange({
58
+ start: selectionRange.start - 1,
59
+ end: selectionRange.start - 1,
60
+ });
47
61
  }
48
62
  }
49
63
  else if (e.key === "ArrowRight") {
50
- if (focusedIndex !== null &&
51
- focusedIndex < despacedFormat.length - 1) {
52
- setFocusedIndex(focusedIndex + 1);
64
+ e.preventDefault();
65
+ if (selectionRange !== null &&
66
+ selectionRange.end < despacedFormat.length - 1) {
67
+ setSelectionRange({
68
+ start: selectionRange.end + 1,
69
+ end: selectionRange.end + 1,
70
+ });
53
71
  }
54
72
  }
55
73
  else if (e.key === "Backspace") {
56
- // 현재 커서의 실제위치(hiddenRef.current?.selectionStart)는 포커스된 SquareInput 글자의 왼쪽 또는 오른쪽에 위치합니다.
57
- // 사용자는 hiddenInput을 볼 수 없고, 포커싱된 SquareInput만 볼 수 있으므로 실제커서 위치는 예상할 수 없습니다.
58
- // 그래서 현재 포커싱된 위치를 focusedIndex로 관리합니다.
59
74
  // 만약 조합중이라면 원래 동작대로 현재 글자를 지우고,
60
75
  // 조합중이 아니라면 focusedIndex의 글자를 지워야 합니다.(delete와 같은 동작)
61
76
  if (isComposing)
@@ -71,32 +86,24 @@ export function SegmentedInput(props) {
71
86
  onChange(getFormattedValue(newValue));
72
87
  // 이전 글자로 포커싱을 이동합니다.
73
88
  const newFocusedIndex = Math.max(focusedIndex - 1, 0);
74
- setFocusedIndex(newFocusedIndex);
75
- // onChange이후에 실제 포커스를 이동시키기 위해 setTimeout을 사용합니다.
76
- setTimeout(() => {
77
- var _a;
78
- (_a = hiddenRef.current) === null || _a === void 0 ? void 0 : _a.setSelectionRange(newFocusedIndex, newFocusedIndex);
79
- }, 0);
89
+ setSelectionRange({
90
+ start: newFocusedIndex,
91
+ end: newFocusedIndex,
92
+ });
80
93
  }
81
94
  }, onChange: (e) => {
82
- var _a;
95
+ var _a, _b;
83
96
  onChange(getFormattedValue(e.target.value).slice(0, answerFormat.length));
84
- const selectionStart = (_a = hiddenRef.current) === null || _a === void 0 ? void 0 : _a.selectionStart;
85
- setFocusedIndex(selectionStart ? selectionStart - 1 : 0);
86
- }, onFocus: (e) => {
87
- var _a;
88
- // tab을 통해 focus가 이동되었을 때, focusedIndex를 설정합니다.
89
- if (focusedIndex === null) {
90
- const valueLength = e.target.value.length;
91
- setFocusedIndex(valueLength);
92
- (_a = hiddenRef.current) === null || _a === void 0 ? void 0 : _a.setSelectionRange(valueLength, valueLength);
93
- }
97
+ setSelectionRange({
98
+ start: (_a = e.target.selectionStart) !== null && _a !== void 0 ? _a : 0,
99
+ end: (_b = e.target.selectionEnd) !== null && _b !== void 0 ? _b : 0,
100
+ });
94
101
  }, onBlur: (e) => {
95
102
  onChange(getFormattedValue(e.target.value).slice(0, answerFormat.length));
96
- setFocusedIndex(null);
103
+ setSelectionRange(null);
97
104
  }, css: css `
98
- opacity: 0;
99
- height: 0;
105
+ opacity: 1;
106
+ height: 10;
100
107
  ` }), _jsxs(InputWrapper, { children: [_jsx("div", Object.assign({ css: css `
101
108
  display: flex;
102
109
  align-items: center;
@@ -116,15 +123,14 @@ export function SegmentedInput(props) {
116
123
  readOnly,
117
124
  tabIndex: -1,
118
125
  onFocus: () => {
126
+ var _a;
119
127
  if (readOnly)
120
128
  return;
121
- setFocusedIndex(i);
122
- // setFocusedIndex이후에 실제 포커스를 이동시키기 위해 setTimeout을 사용합니다.
123
- setTimeout(() => {
124
- var _a, _b;
125
- (_a = hiddenRef.current) === null || _a === void 0 ? void 0 : _a.focus();
126
- (_b = hiddenRef.current) === null || _b === void 0 ? void 0 : _b.setSelectionRange(i, i);
127
- }, 0);
129
+ setSelectionRange({
130
+ start: i,
131
+ end: i,
132
+ });
133
+ (_a = hiddenRef.current) === null || _a === void 0 ? void 0 : _a.focus();
128
134
  },
129
135
  }, value: splitedValues[i] || "", onChange: () => { } }, i))) })), _jsx(InputMarker, {})] }), _jsx(Text, { children: placeholder })] }));
130
136
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-monolith/cds",
3
- "version": "1.79.4",
3
+ "version": "1.79.6",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "sideEffects": false,