@team-monolith/cds 1.79.5 → 1.79.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.
@@ -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
+ onSelectionRangeChange: (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, onSelectionRangeChange } = props, inputProps = __rest(props, ["value", "selectionRange", "onSelectionRangeChange"]);
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
+ onSelectionRangeChange({
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, useEffect, 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,31 +49,25 @@ export function SegmentedInput(props) {
39
49
  }
40
50
  }, "");
41
51
  };
42
- // focusedIndex가 변경되지 않아도 setSelectionRange 실행하기 위한 state입니다.
43
- const [focusCounter, setFocusCounter] = useState(0);
44
- // focusedIndex 또는 focusCounter가 변경되면 실제 포커스를 이동시킵니다.
45
- // 현재 커서의 실제위치(hiddenRef.current?.selectionStart)는 포커스된 SquareInput 글자의 왼쪽 또는 오른쪽에 위치합니다.
46
- // 사용자는 hiddenInput을 볼 수 없고, 포커싱된 SquareInput만 볼 수 있으므로 실제커서 위치는 예상할 수 없습니다.
47
- // 그래서 현재 포커싱된 위치를 focusedIndex로 관리합니다. (포커스된 SquareInput 글자 왼쪽에 커서가 위치하게 됩니다.)
48
- useEffect(() => {
49
- var _a;
50
- if (focusedIndex !== null) {
51
- (_a = hiddenRef.current) === null || _a === void 0 ? void 0 : _a.setSelectionRange(focusedIndex, focusedIndex);
52
- }
53
- }, [focusedIndex, focusCounter]);
54
- 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, onSelectionRangeChange: setSelectionRange, onKeyDown: (e) => {
55
53
  var _a;
56
54
  if (e.key === "ArrowLeft") {
57
55
  e.preventDefault();
58
- if (focusedIndex !== null && focusedIndex > 0) {
59
- setFocusedIndex(focusedIndex - 1);
56
+ if (selectionRange !== null && selectionRange.start > 0) {
57
+ setSelectionRange({
58
+ start: selectionRange.start - 1,
59
+ end: selectionRange.start - 1,
60
+ });
60
61
  }
61
62
  }
62
63
  else if (e.key === "ArrowRight") {
63
64
  e.preventDefault();
64
- if (focusedIndex !== null &&
65
- focusedIndex < despacedFormat.length - 1) {
66
- setFocusedIndex(focusedIndex + 1);
65
+ if (selectionRange !== null &&
66
+ selectionRange.end < despacedFormat.length - 1) {
67
+ setSelectionRange({
68
+ start: selectionRange.end + 1,
69
+ end: selectionRange.end + 1,
70
+ });
67
71
  }
68
72
  }
69
73
  else if (e.key === "Backspace") {
@@ -81,33 +85,25 @@ export function SegmentedInput(props) {
81
85
  : value.slice(0, focusedIndex) + value.slice(focusedIndex + 1);
82
86
  onChange(getFormattedValue(newValue));
83
87
  // 이전 글자로 포커싱을 이동합니다.
84
- // 이 때, focusedIndex가 변하지 않더라도 focusCounter를 증가시켜서 setSelectionRange가 실행되도록 합니다.
85
88
  const newFocusedIndex = Math.max(focusedIndex - 1, 0);
86
- setFocusedIndex(newFocusedIndex);
87
- setFocusCounter((prev) => prev + 1);
89
+ setSelectionRange({
90
+ start: newFocusedIndex,
91
+ end: newFocusedIndex,
92
+ });
88
93
  }
89
94
  }, onChange: (e) => {
90
- var _a;
95
+ var _a, _b;
91
96
  onChange(getFormattedValue(e.target.value).slice(0, answerFormat.length));
92
- const selectionStart = (_a = hiddenRef.current) === null || _a === void 0 ? void 0 : _a.selectionStart;
93
- if (isComposing) {
94
- setFocusedIndex(selectionStart ? selectionStart - 1 : 0);
95
- }
96
- else {
97
- setFocusedIndex(selectionStart || 0);
98
- }
99
- }, onFocus: (e) => {
100
- // tab을 통해 focus가 이동되었을 때, focusedIndex를 설정합니다.
101
- if (focusedIndex === null) {
102
- const valueLength = e.target.value.length;
103
- setFocusedIndex(valueLength);
104
- }
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
+ });
105
101
  }, onBlur: (e) => {
106
102
  onChange(getFormattedValue(e.target.value).slice(0, answerFormat.length));
107
- setFocusedIndex(null);
103
+ setSelectionRange(null);
108
104
  }, css: css `
109
- opacity: 0;
110
- height: 0;
105
+ opacity: 1;
106
+ height: 10;
111
107
  ` }), _jsxs(InputWrapper, { children: [_jsx("div", Object.assign({ css: css `
112
108
  display: flex;
113
109
  align-items: center;
@@ -127,14 +123,14 @@ export function SegmentedInput(props) {
127
123
  readOnly,
128
124
  tabIndex: -1,
129
125
  onFocus: () => {
126
+ var _a;
130
127
  if (readOnly)
131
128
  return;
132
- setFocusedIndex(i);
133
- // setFocusedIndex이후에 실제 포커스를 이동시키기 위해 setTimeout을 사용합니다.
134
- setTimeout(() => {
135
- var _a;
136
- (_a = hiddenRef.current) === null || _a === void 0 ? void 0 : _a.focus();
137
- }, 0);
129
+ setSelectionRange({
130
+ start: i,
131
+ end: i,
132
+ });
133
+ (_a = hiddenRef.current) === null || _a === void 0 ? void 0 : _a.focus();
138
134
  },
139
135
  }, value: splitedValues[i] || "", onChange: () => { } }, i))) })), _jsx(InputMarker, {})] }), _jsx(Text, { children: placeholder })] }));
140
136
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-monolith/cds",
3
- "version": "1.79.5",
3
+ "version": "1.79.7",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "sideEffects": false,