@team-monolith/cds 1.81.0 → 1.82.0

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
+ /** selectionRange에 대한 controlled input 컴포넌트입니다.
7
+ * 일반적인 input처럼 동작하도록 작성되어, 비지니스 로직을 포함하지 않습니다.
8
+ * 선택 범위에 대한 state, setState를 props로 받아 사용합니다.
9
+ */
10
+ export declare const InputControlledSelectionRange: React.ForwardRefExoticComponent<{
11
+ selectionRange: SelectionRange;
12
+ onSelectionRangeChange: (selectionRange: SelectionRange) => void;
13
+ } & React.InputHTMLAttributes<HTMLInputElement> & React.RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,54 @@
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, { useRef } from "react";
14
+ import { useEffect } from "react";
15
+ /** selectionRange에 대한 controlled input 컴포넌트입니다.
16
+ * 일반적인 input처럼 동작하도록 작성되어, 비지니스 로직을 포함하지 않습니다.
17
+ * 선택 범위에 대한 state, setState를 props로 받아 사용합니다.
18
+ */
19
+ export const InputControlledSelectionRange = React.forwardRef(function InputControlledSelectionRange(props, ref) {
20
+ // eslint-disable-next-line react/prop-types
21
+ const { selectionRange, onSelectionRangeChange, onSelect } = props, inputProps = __rest(props, ["selectionRange", "onSelectionRangeChange", "onSelect"]);
22
+ const internalRef = useRef(null);
23
+ // selectionRange가 바뀔 때마다, 실제 DOM input의 selection range를 동기화
24
+ useEffect(() => {
25
+ const element = internalRef.current;
26
+ if (!element)
27
+ return;
28
+ const { start, end } = selectionRange;
29
+ try {
30
+ element.setSelectionRange(start, end);
31
+ }
32
+ catch (err) {
33
+ // 범위가 잘못되거나 input이 포커스가 없을 때 에러가 발생할 수 있으므로 주의
34
+ console.warn("setSelectionRange error:", err);
35
+ }
36
+ }, [selectionRange]);
37
+ return (_jsx("input", Object.assign({}, inputProps, { ref: (element) => {
38
+ internalRef.current = element;
39
+ if (typeof ref === "function") {
40
+ ref(element);
41
+ }
42
+ else if (ref !== null) {
43
+ ref.current =
44
+ element;
45
+ }
46
+ }, onSelect: (e) => {
47
+ var _a, _b;
48
+ onSelect === null || onSelect === void 0 ? void 0 : onSelect(e);
49
+ onSelectionRangeChange({
50
+ start: (_a = e.currentTarget.selectionStart) !== null && _a !== void 0 ? _a : 0,
51
+ end: (_b = e.currentTarget.selectionEnd) !== null && _b !== void 0 ? _b : 0,
52
+ });
53
+ } })));
54
+ });
@@ -4,6 +4,8 @@ import { css, useTheme } from "@emotion/react";
4
4
  import styled from "@emotion/styled";
5
5
  import { InputBase } from "../../../../components/InputBase";
6
6
  import { useRef, useState } from "react";
7
+ import { useEventListener } from "usehooks-ts";
8
+ import { InputControlledSelectionRange, } from "./InputControlledSelectionRange";
7
9
  /**
8
10
  * SOT는 value이고, 그 value는 숨겨진 input에 작성됩니다.
9
11
  * 그리고 그 value는 split되어 segmented input에 표시됩니다.
@@ -18,8 +20,35 @@ export function SegmentedInput(props) {
18
20
  const despacedFormat = splitedFormat.filter((v) => v !== " ");
19
21
  // "안녕 하세요" => ["안","녕","하","세","요"]
20
22
  const splitedValues = value.split("").filter((v) => v !== " ");
21
- const [focusedIndex, setFocusedIndex] = useState(null);
23
+ // 현재 선택범위를 state로 관리합니다. 현재 커서 위치의 Source of Truth입니다.
24
+ const [selectionRange, setSelectionRange] = useState({
25
+ start: 0,
26
+ end: 0,
27
+ });
28
+ // 글자를 조합중인지 여부 (ex: "안"을 입력중 "아"일 때 해당합니다.)
29
+ const [isComposing, setIsComposing] = useState(false);
30
+ // 현재 선택된 SquareInput의 index Range.([이상, 이하])
31
+ // isComposing일 때는 입력중인 글자만 포커싱되도록 합니다.
32
+ // isComposing이 아닐 때는 start이상 ~ end미만의 범위가 포커싱되도록 합니다.
33
+ const selectedSquareInputIndexRange = isComposing
34
+ ? [
35
+ Math.max(selectionRange.start - 1, 0),
36
+ Math.max(selectionRange.start - 1, 0),
37
+ ]
38
+ : [
39
+ selectionRange.start,
40
+ Math.max(selectionRange.end - 1, selectionRange.start),
41
+ ];
42
+ const isSelectedSquareInputIndex = (i) => i >= selectedSquareInputIndexRange[0] &&
43
+ i <= selectedSquareInputIndexRange[1];
22
44
  const hiddenRef = useRef(null);
45
+ const [isHiddenInputFocused, setIsHiddenInputFocused] = useState(false);
46
+ useEventListener("compositionstart", () => {
47
+ setIsComposing(true);
48
+ }, hiddenRef);
49
+ useEventListener("compositionend", () => {
50
+ setIsComposing(false);
51
+ }, hiddenRef);
23
52
  /** value의 공백을 제거하고, format에 맞춰 공백을 추가한 값을 리턴합니다. */
24
53
  const getFormattedValue = (value) => {
25
54
  const despacedValue = value.replace(/ /g, "");
@@ -32,26 +61,36 @@ export function SegmentedInput(props) {
32
61
  }
33
62
  }, "");
34
63
  };
35
- return (_jsxs(Container, { children: [_jsx("input", { type: "input", ref: hiddenRef, value: splitedValues.join(""), readOnly: readOnly, onKeyDown: (e) => {
36
- if (e.key === "ArrowLeft") {
37
- if (focusedIndex !== null && focusedIndex > 0) {
38
- setFocusedIndex(focusedIndex - 1);
39
- }
40
- }
41
- else if (e.key === "ArrowRight") {
42
- if (focusedIndex !== null &&
43
- focusedIndex < despacedFormat.length - 1) {
44
- setFocusedIndex(focusedIndex + 1);
45
- }
46
- }
47
- }, onChange: (event) => {
64
+ return (_jsxs(Container, { children: [_jsx(InputControlledSelectionRange, { type: "input", ref: hiddenRef, value: splitedValues.join(""), readOnly: readOnly, selectionRange: selectionRange, onSelectionRangeChange: setSelectionRange, onKeyDown: (e) => {
48
65
  var _a;
49
- onChange(getFormattedValue(event.target.value).slice(0, answerFormat.length));
50
- const selectionStart = (_a = hiddenRef.current) === null || _a === void 0 ? void 0 : _a.selectionStart;
51
- setFocusedIndex(selectionStart ? selectionStart - 1 : null);
66
+ if (e.key === "Backspace") {
67
+ // 만약 조합중이라면 원래 동작대로 현재 글자를 지우고,
68
+ // 조합중이 아니라면 selectedSquareInputIndex의 글자를 지워야 합니다.(delete와 같은 동작)
69
+ if (isComposing)
70
+ return;
71
+ e.preventDefault();
72
+ const value = ((_a = hiddenRef.current) === null || _a === void 0 ? void 0 : _a.value) || "";
73
+ const focusedIndex = selectedSquareInputIndexRange[0];
74
+ // 마지막 글자에 포커싱된 경우 마지막 글자를 지우고, 아닌 경우 포커싱된 글자를 지웁니다.
75
+ const newValue = focusedIndex >= value.length - 1
76
+ ? value.slice(0, -1)
77
+ : value.slice(0, focusedIndex) + value.slice(focusedIndex + 1);
78
+ onChange(getFormattedValue(newValue));
79
+ // 이전 글자로 포커싱을 이동합니다.
80
+ const newFocusedIndex = Math.max(focusedIndex - 1, 0);
81
+ setSelectionRange({
82
+ start: newFocusedIndex,
83
+ end: newFocusedIndex,
84
+ });
85
+ }
86
+ }, onChange: (e) => {
87
+ onChange(getFormattedValue(e.target.value).slice(0, answerFormat.length));
88
+ }, onFocus: () => {
89
+ setIsHiddenInputFocused(true);
52
90
  }, onBlur: (e) => {
91
+ setIsHiddenInputFocused(false);
53
92
  onChange(getFormattedValue(e.target.value).slice(0, answerFormat.length));
54
- setFocusedIndex(null);
93
+ setIsComposing(false);
55
94
  }, css: css `
56
95
  opacity: 0;
57
96
  height: 0;
@@ -63,7 +102,7 @@ export function SegmentedInput(props) {
63
102
  ? "activeSuccess"
64
103
  : isCorrect === false
65
104
  ? "activeDanger"
66
- : focusedIndex === i
105
+ : isHiddenInputFocused && isSelectedSquareInputIndex(i)
67
106
  ? "activePrimary"
68
107
  : "default", css: isCorrect === false &&
69
108
  css `
@@ -72,13 +111,16 @@ export function SegmentedInput(props) {
72
111
  }
73
112
  `, inputProps: {
74
113
  readOnly,
114
+ tabIndex: -1,
75
115
  onFocus: () => {
76
- var _a, _b;
116
+ var _a;
77
117
  if (readOnly)
78
118
  return;
79
- setFocusedIndex(i);
119
+ setSelectionRange({
120
+ start: i,
121
+ end: i,
122
+ });
80
123
  (_a = hiddenRef.current) === null || _a === void 0 ? void 0 : _a.focus();
81
- (_b = hiddenRef.current) === null || _b === void 0 ? void 0 : _b.setSelectionRange(i, i);
82
124
  },
83
125
  }, value: splitedValues[i] || "", onChange: () => { } }, i))) })), _jsx(InputMarker, {})] }), _jsx(Text, { children: placeholder })] }));
84
126
  }
@@ -23,16 +23,12 @@ export function ImagesPlugin({ captionsEnabled, }) {
23
23
  }
24
24
  return mergeRegister(editor.registerCommand(INSERT_IMAGE_COMMAND, (payload) => {
25
25
  const imageNode = $createImageNode(payload);
26
- $insertNodeToNearestRoot(imageNode);
27
- // lexical의 원본코드: 이미지 노드를 텍스트 노드 안에 삽입하고 있습니다.
26
+ // lexical의 원본코드에서는 이미지 노드를 텍스트 노드 안에 삽입하고 있었습니다.
28
27
  // 이 때문에 이미지 노드 아래에 불필요한 텍스트 라인이 생성됩니다.
29
- // 이를 막기 위해 다른 커스텀 노드처럼 노드를 직접 삽입합니다.
30
28
  // before: images/2025-01-06-15-16-05.png
29
+ // 이를 막기 위해 다른 커스텀 노드처럼 노드를 직접 삽입합니다.
31
30
  // after: images/2025-01-06-15-14-39.png
32
- // $insertNodes([imageNode]);
33
- // if ($isRootOrShadowRoot(imageNode.getParentOrThrow())) {
34
- // $wrapNodeInElement(imageNode, $createParagraphNode).selectEnd();
35
- // }
31
+ $insertNodeToNearestRoot(imageNode);
36
32
  return true;
37
33
  }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(DRAGSTART_COMMAND, (event) => {
38
34
  return onDragStart(event);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-monolith/cds",
3
- "version": "1.81.0",
3
+ "version": "1.82.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "sideEffects": false,