@ncds/ui-admin 1.8.21-alpha.0 → 1.8.21-alpha.2
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.
- package/dist/cjs/src/components/forms-and-input/input-base/InputBase.js +42 -30
- package/dist/cjs/src/components/forms-and-input/input-base/__tests__/InputBase.test.js +228 -0
- package/dist/cjs/src/components/forms-and-input/password-input/PasswordInput.js +3 -2
- package/dist/cjs/src/components/forms-and-input/password-input/__tests__/PasswordInput.test.js +59 -0
- package/dist/cjs/src/components/index.js +11 -0
- package/dist/cjs/src/components/layout/ncua-scope/NcuaScope.js +73 -0
- package/dist/cjs/src/components/layout/ncua-scope/index.js +16 -0
- package/dist/cjs/src/generated/scoped-css.js +9 -0
- package/dist/esm/src/components/forms-and-input/input-base/InputBase.js +42 -30
- package/dist/esm/src/components/forms-and-input/input-base/__tests__/InputBase.test.js +228 -0
- package/dist/esm/src/components/forms-and-input/password-input/PasswordInput.js +3 -2
- package/dist/esm/src/components/forms-and-input/password-input/__tests__/PasswordInput.test.js +56 -0
- package/dist/esm/src/components/index.js +1 -0
- package/dist/esm/src/components/layout/ncua-scope/NcuaScope.js +67 -0
- package/dist/esm/src/components/layout/ncua-scope/index.js +1 -0
- package/dist/esm/src/generated/scoped-css.js +3 -0
- package/dist/temp/src/components/forms-and-input/input-base/InputBase.d.ts +1 -1
- package/dist/temp/src/components/forms-and-input/input-base/InputBase.js +33 -24
- package/dist/temp/src/components/forms-and-input/input-base/__tests__/InputBase.test.js +174 -0
- package/dist/temp/src/components/forms-and-input/password-input/PasswordInput.js +2 -1
- package/dist/temp/src/components/forms-and-input/password-input/__tests__/PasswordInput.test.d.ts +1 -0
- package/dist/temp/src/components/forms-and-input/password-input/__tests__/PasswordInput.test.js +48 -0
- package/dist/temp/src/components/index.d.ts +1 -0
- package/dist/temp/src/components/index.js +1 -0
- package/dist/temp/src/components/layout/ncua-scope/NcuaScope.d.ts +32 -0
- package/dist/temp/src/components/layout/ncua-scope/NcuaScope.js +47 -0
- package/dist/temp/src/components/layout/ncua-scope/index.d.ts +1 -0
- package/dist/temp/src/components/layout/ncua-scope/index.js +1 -0
- package/dist/temp/src/generated/scoped-css.d.ts +1 -0
- package/dist/temp/src/generated/scoped-css.js +3 -0
- package/dist/types/src/components/forms-and-input/input-base/InputBase.d.ts +1 -1
- package/dist/types/src/components/forms-and-input/password-input/__tests__/PasswordInput.test.d.ts +1 -0
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/layout/ncua-scope/NcuaScope.d.ts +32 -0
- package/dist/types/src/components/layout/ncua-scope/index.d.ts +1 -0
- package/dist/types/src/generated/scoped-css.d.ts +1 -0
- package/dist/ui-admin/assets/styles/style.css +51 -23
- package/dist/ui-admin/assets/styles/style.scoped.css +8383 -0
- package/package.json +13 -6
|
@@ -12,11 +12,9 @@ var _color = require("../../../../constant/color");
|
|
|
12
12
|
var _HintText = require("../../shared/hintText/HintText");
|
|
13
13
|
var _Label = require("../../shared/label/Label");
|
|
14
14
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
};
|
|
19
|
-
const generalSvgSize = {
|
|
15
|
+
// 슬롯 아이콘(leadingElement/trailingElement)과 상태 아이콘(clear·validation·destructive)은 같은 크기를 쓴다.
|
|
16
|
+
// slot.size 를 명시하면 그 값이 우선한다.
|
|
17
|
+
const iconSvgSize = {
|
|
20
18
|
xs: 14,
|
|
21
19
|
sm: 20
|
|
22
20
|
};
|
|
@@ -42,19 +40,26 @@ const InputBase = exports.InputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref,
|
|
|
42
40
|
} = _ref;
|
|
43
41
|
const inputRef = (0, _react.useRef)(null);
|
|
44
42
|
const [textCount, setTextCount] = (0, _react.useState)(0);
|
|
43
|
+
const [hasValue, setHasValue] = (0, _react.useState)(false);
|
|
44
|
+
// 값 유무는 DOM에서 읽는다 — props.value로 판정하면 uncontrolled Input에서 항상 빈 값으로 보인다.
|
|
45
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: controlled 값이 프로그래밍적으로 바뀔 때는 input 이벤트가 나지 않아 props.value로 재동기화해야 한다
|
|
45
46
|
(0, _react.useEffect)(() => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
const input = inputRef.current;
|
|
48
|
+
if (!input) return;
|
|
49
|
+
const syncFromInput = () => {
|
|
50
|
+
const {
|
|
51
|
+
length
|
|
52
|
+
} = input.value;
|
|
53
|
+
setHasValue(length > 0);
|
|
54
|
+
setTextCount(length);
|
|
55
|
+
};
|
|
56
|
+
syncFromInput();
|
|
57
|
+
input.addEventListener('input', syncFromInput);
|
|
58
|
+
return () => {
|
|
59
|
+
input.removeEventListener('input', syncFromInput);
|
|
60
|
+
};
|
|
61
|
+
}, [props.value]);
|
|
62
|
+
const renderOutsideSlot = (slot, position) => {
|
|
58
63
|
if (slot.type === 'custom' && slot.placement !== 'outside') {
|
|
59
64
|
return null;
|
|
60
65
|
}
|
|
@@ -68,7 +73,7 @@ const InputBase = exports.InputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref,
|
|
|
68
73
|
});
|
|
69
74
|
case 'text':
|
|
70
75
|
return (0, _jsxRuntime.jsx)("span", {
|
|
71
|
-
className: (0, _classnames.default)(
|
|
76
|
+
className: (0, _classnames.default)(`ncua-input__${position}-text`, `ncua-input__${position}-text--${size}`),
|
|
72
77
|
children: slot.text
|
|
73
78
|
});
|
|
74
79
|
case 'custom':
|
|
@@ -85,10 +90,15 @@ const InputBase = exports.InputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref,
|
|
|
85
90
|
return (0, _jsxRuntime.jsx)("div", {
|
|
86
91
|
className: "ncua-input__icon-wrap",
|
|
87
92
|
children: (0, _jsxRuntime.jsx)(SlotIcon, {
|
|
88
|
-
className: (0, _classnames.default)(`ncua-input__${position}-icon`,
|
|
93
|
+
className: (0, _classnames.default)(`ncua-input__${position}-icon`,
|
|
94
|
+
// color 를 지정하지 않았을 때만 상태별 색(Placeholder/Filled/Focused/Disabled)을 CSS 에 맡긴다.
|
|
95
|
+
// 지정한 경우에는 이 클래스를 붙이지 않아 지정 색이 그대로 유지된다.
|
|
96
|
+
{
|
|
97
|
+
'ncua-input__slot-icon': !slot.color
|
|
98
|
+
}, slot.className),
|
|
89
99
|
color: slot.color ? _color.COLOR[slot.color] : undefined,
|
|
90
|
-
width: slot.size ??
|
|
91
|
-
height: slot.size ??
|
|
100
|
+
width: slot.size ?? iconSvgSize[size],
|
|
101
|
+
height: slot.size ?? iconSvgSize[size]
|
|
92
102
|
})
|
|
93
103
|
});
|
|
94
104
|
}
|
|
@@ -109,8 +119,8 @@ const InputBase = exports.InputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref,
|
|
|
109
119
|
onClick: onClearText,
|
|
110
120
|
children: (0, _jsxRuntime.jsx)(_uiAdminIcon.X, {
|
|
111
121
|
className: "ncua-input__clear-icon",
|
|
112
|
-
width:
|
|
113
|
-
height:
|
|
122
|
+
width: iconSvgSize[size],
|
|
123
|
+
height: iconSvgSize[size]
|
|
114
124
|
})
|
|
115
125
|
});
|
|
116
126
|
};
|
|
@@ -120,8 +130,8 @@ const InputBase = exports.InputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref,
|
|
|
120
130
|
className: "ncua-input__icon-wrap ncua-input__right-icon",
|
|
121
131
|
children: (0, _jsxRuntime.jsx)(_uiAdminIcon.InfoCircle, {
|
|
122
132
|
className: "ncua-input__destructive-icon",
|
|
123
|
-
width:
|
|
124
|
-
height:
|
|
133
|
+
width: iconSvgSize[size],
|
|
134
|
+
height: iconSvgSize[size]
|
|
125
135
|
})
|
|
126
136
|
});
|
|
127
137
|
}
|
|
@@ -130,8 +140,8 @@ const InputBase = exports.InputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref,
|
|
|
130
140
|
className: "ncua-input__icon-wrap ncua-input__right-icon",
|
|
131
141
|
children: (0, _jsxRuntime.jsx)(_uiAdminIcon.CheckCircle, {
|
|
132
142
|
className: "ncua-input__validation-icon",
|
|
133
|
-
width:
|
|
134
|
-
height:
|
|
143
|
+
width: iconSvgSize[size],
|
|
144
|
+
height: iconSvgSize[size]
|
|
135
145
|
})
|
|
136
146
|
});
|
|
137
147
|
}
|
|
@@ -156,13 +166,15 @@ const InputBase = exports.InputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref,
|
|
|
156
166
|
'is-disabled': disabled,
|
|
157
167
|
'full-width': fullWidth,
|
|
158
168
|
'ncua-input__trailing-button': trailingElement?.type === 'button',
|
|
159
|
-
'ncua-input__leading-text-wrap': leadingElement?.type === 'text'
|
|
169
|
+
'ncua-input__leading-text-wrap': leadingElement?.type === 'text',
|
|
170
|
+
'ncua-input__trailing-text-wrap': trailingElement?.type === 'text',
|
|
171
|
+
'has-value': hasValue
|
|
160
172
|
}, className),
|
|
161
173
|
children: [renderLabel(), (0, _jsxRuntime.jsxs)("div", {
|
|
162
174
|
className: "ncua-input__content-wrap",
|
|
163
175
|
children: [(0, _jsxRuntime.jsxs)("div", {
|
|
164
176
|
className: "ncua-input__content",
|
|
165
|
-
children: [leadingElement && renderOutsideSlot(leadingElement), (0, _jsxRuntime.jsxs)("div", {
|
|
177
|
+
children: [leadingElement && renderOutsideSlot(leadingElement, 'leading'), (0, _jsxRuntime.jsxs)("div", {
|
|
166
178
|
className: (0, _classnames.default)('ncua-input__field', `ncua-input__field--${size}`),
|
|
167
179
|
children: [leadingElement && renderInsideSlot(leadingElement, 'left'), (0, _jsxRuntime.jsx)("input", {
|
|
168
180
|
ref: node => {
|
|
@@ -178,7 +190,7 @@ const InputBase = exports.InputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref,
|
|
|
178
190
|
maxLength: maxLength,
|
|
179
191
|
...props
|
|
180
192
|
}), renderClearButton(), renderStatusIcon(), trailingElement && renderInsideSlot(trailingElement, 'right')]
|
|
181
|
-
}), trailingElement && renderOutsideSlot(trailingElement)]
|
|
193
|
+
}), trailingElement && renderOutsideSlot(trailingElement, 'trailing')]
|
|
182
194
|
}), showTextCount && maxLength && (0, _jsxRuntime.jsxs)("div", {
|
|
183
195
|
className: "ncua-input__field-text-count",
|
|
184
196
|
children: [(0, _jsxRuntime.jsx)("output", {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
var _uiAdminIcon = require("@ncds/ui-admin-icon");
|
|
3
4
|
var _react = require("react");
|
|
4
5
|
var _client = require("react-dom/client");
|
|
5
6
|
var _testUtils = require("react-dom/test-utils");
|
|
6
7
|
var _vitest = require("vitest");
|
|
8
|
+
var _color = require("../../../../../constant/color");
|
|
7
9
|
var _InputBase = require("../InputBase");
|
|
8
10
|
// @vitest-environment jsdom
|
|
9
11
|
|
|
@@ -69,4 +71,230 @@ function mountInputBase() {
|
|
|
69
71
|
(0, _vitest.expect)(view.container.querySelector('.ncua-input__clear')).toBeNull();
|
|
70
72
|
view.unmount();
|
|
71
73
|
});
|
|
74
|
+
});
|
|
75
|
+
(0, _vitest.describe)('InputBase — 아이콘 사이즈', () => {
|
|
76
|
+
const iconSizeOf = (container, selector) => {
|
|
77
|
+
const svg = container.querySelector(selector);
|
|
78
|
+
return {
|
|
79
|
+
width: svg?.getAttribute('width'),
|
|
80
|
+
height: svg?.getAttribute('height')
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
// 슬롯 아이콘과 상태 아이콘은 같은 크기를 쓴다 — 과거 상태 아이콘만 sm=16으로 어긋나 있었다
|
|
84
|
+
_vitest.it.each([['xs', '14'], ['sm', '20']])('size=%s 일 때 슬롯 아이콘과 상태 아이콘이 모두 %spx로 렌더된다', (size, expected) => {
|
|
85
|
+
// Given/When: 슬롯 아이콘과 clear 버튼, destructive 상태를 함께 렌더하면
|
|
86
|
+
const view = mountInputBase({
|
|
87
|
+
size,
|
|
88
|
+
destructive: true,
|
|
89
|
+
clearText: true,
|
|
90
|
+
onClearText: _vitest.vi.fn(),
|
|
91
|
+
leadingElement: {
|
|
92
|
+
type: 'icon',
|
|
93
|
+
icon: _uiAdminIcon.SearchLg
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// Then: 슬롯·clear·destructive 아이콘의 크기가 서로 어긋나지 않는다
|
|
97
|
+
const expectedSize = {
|
|
98
|
+
width: expected,
|
|
99
|
+
height: expected
|
|
100
|
+
};
|
|
101
|
+
(0, _vitest.expect)(iconSizeOf(view.container, '.ncua-input__left-icon')).toEqual(expectedSize);
|
|
102
|
+
(0, _vitest.expect)(iconSizeOf(view.container, '.ncua-input__clear-icon')).toEqual(expectedSize);
|
|
103
|
+
(0, _vitest.expect)(iconSizeOf(view.container, '.ncua-input__destructive-icon')).toEqual(expectedSize);
|
|
104
|
+
view.unmount();
|
|
105
|
+
});
|
|
106
|
+
_vitest.it.each([['xs', '14'], ['sm', '20']])('size=%s 일 때 validation 아이콘도 %spx로 렌더된다', (size, expected) => {
|
|
107
|
+
// Given/When: validation 상태로 렌더하면
|
|
108
|
+
const view = mountInputBase({
|
|
109
|
+
size,
|
|
110
|
+
validation: true
|
|
111
|
+
});
|
|
112
|
+
// Then: 다른 아이콘과 같은 크기다
|
|
113
|
+
(0, _vitest.expect)(iconSizeOf(view.container, '.ncua-input__validation-icon')).toEqual({
|
|
114
|
+
width: expected,
|
|
115
|
+
height: expected
|
|
116
|
+
});
|
|
117
|
+
view.unmount();
|
|
118
|
+
});
|
|
119
|
+
(0, _vitest.it)('slot.size를 직접 주면 기본 사이즈보다 우선한다', () => {
|
|
120
|
+
// Given/When: 슬롯에 size를 명시해 렌더하면
|
|
121
|
+
const view = mountInputBase({
|
|
122
|
+
size: 'xs',
|
|
123
|
+
leadingElement: {
|
|
124
|
+
type: 'icon',
|
|
125
|
+
icon: _uiAdminIcon.SearchLg,
|
|
126
|
+
size: 24
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
// Then: 기본값(14)이 아니라 명시한 값이 적용된다
|
|
130
|
+
(0, _vitest.expect)(iconSizeOf(view.container, '.ncua-input__left-icon')).toEqual({
|
|
131
|
+
width: '24',
|
|
132
|
+
height: '24'
|
|
133
|
+
});
|
|
134
|
+
view.unmount();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
(0, _vitest.describe)('InputBase — 슬롯 아이콘 상태 색상', () => {
|
|
138
|
+
// 상태별 색은 CSS 가 결정한다. jsdom 에서는 스타일시트를 로드하지 않으므로
|
|
139
|
+
// "상태 규칙을 타는 클래스가 붙었는지" 와 "지정 색이 인라인으로 남았는지" 로 검증한다.
|
|
140
|
+
const leftIconOf = container => container.querySelector('.ncua-input__left-icon');
|
|
141
|
+
(0, _vitest.it)('color 를 지정하지 않으면 상태 색상 클래스가 붙는다', () => {
|
|
142
|
+
// Given/When: color 없이 슬롯 아이콘을 렌더하면
|
|
143
|
+
const view = mountInputBase({
|
|
144
|
+
leadingElement: {
|
|
145
|
+
type: 'icon',
|
|
146
|
+
icon: _uiAdminIcon.SearchLg
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
// Then: 상태 규칙을 타는 클래스가 붙고, 인라인 색은 지정되지 않는다
|
|
150
|
+
const icon = leftIconOf(view.container);
|
|
151
|
+
(0, _vitest.expect)(icon?.classList.contains('ncua-input__slot-icon')).toBe(true);
|
|
152
|
+
(0, _vitest.expect)(icon?.hasAttribute('color')).toBe(false);
|
|
153
|
+
view.unmount();
|
|
154
|
+
});
|
|
155
|
+
(0, _vitest.it)('color 를 지정하면 상태 색상 클래스가 붙지 않고 지정 색이 유지된다', () => {
|
|
156
|
+
// Given/When: color 를 지정해 렌더하면
|
|
157
|
+
const view = mountInputBase({
|
|
158
|
+
leadingElement: {
|
|
159
|
+
type: 'icon',
|
|
160
|
+
icon: _uiAdminIcon.SearchLg,
|
|
161
|
+
color: 'blue500'
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
// Then: 상태 규칙에서 제외되고 지정한 색이 그대로 남는다
|
|
165
|
+
const icon = leftIconOf(view.container);
|
|
166
|
+
(0, _vitest.expect)(icon?.classList.contains('ncua-input__slot-icon')).toBe(false);
|
|
167
|
+
(0, _vitest.expect)(icon?.getAttribute('color')).toBe(_color.COLOR.blue500);
|
|
168
|
+
view.unmount();
|
|
169
|
+
});
|
|
170
|
+
(0, _vitest.it)('slot.className 을 함께 주어도 상태 색상 클래스가 유지된다', () => {
|
|
171
|
+
// Given/When: className 을 추가로 넘기면
|
|
172
|
+
const view = mountInputBase({
|
|
173
|
+
leadingElement: {
|
|
174
|
+
type: 'icon',
|
|
175
|
+
icon: _uiAdminIcon.SearchLg,
|
|
176
|
+
className: 'my-icon'
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
// Then: 두 클래스가 모두 붙는다
|
|
180
|
+
const icon = leftIconOf(view.container);
|
|
181
|
+
(0, _vitest.expect)(icon?.classList.contains('ncua-input__slot-icon')).toBe(true);
|
|
182
|
+
(0, _vitest.expect)(icon?.classList.contains('my-icon')).toBe(true);
|
|
183
|
+
view.unmount();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
(0, _vitest.describe)('InputBase — has-value 상태 클래스', () => {
|
|
187
|
+
const typeInto = (container, text) => {
|
|
188
|
+
const input = container.querySelector('input');
|
|
189
|
+
if (!input) throw new Error('input이 렌더되지 않았습니다');
|
|
190
|
+
(0, _testUtils.act)(() => {
|
|
191
|
+
input.value = text;
|
|
192
|
+
input.dispatchEvent(new Event('input', {
|
|
193
|
+
bubbles: true
|
|
194
|
+
}));
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
(0, _vitest.it)('값이 비어 있으면 has-value가 붙지 않는다', () => {
|
|
198
|
+
// Given/When: 빈 상태로 렌더하면
|
|
199
|
+
const view = mountInputBase();
|
|
200
|
+
// Then: 루트에 has-value가 없다
|
|
201
|
+
(0, _vitest.expect)(view.container.querySelector('.ncua-input')?.classList.contains('has-value')).toBe(false);
|
|
202
|
+
view.unmount();
|
|
203
|
+
});
|
|
204
|
+
// props.value 로 판정하면 uncontrolled 에서 영원히 빈 값으로 보인다 — 자물쇠 아이콘 색상이 반영되지 않던 원인
|
|
205
|
+
(0, _vitest.it)('uncontrolled Input에 입력하면 has-value가 붙는다', () => {
|
|
206
|
+
// Given: value를 넘기지 않은 uncontrolled 상태에서
|
|
207
|
+
const view = mountInputBase();
|
|
208
|
+
// When: 실제로 입력하면
|
|
209
|
+
typeInto(view.container, 'hello');
|
|
210
|
+
// Then: has-value가 붙는다
|
|
211
|
+
(0, _vitest.expect)(view.container.querySelector('.ncua-input')?.classList.contains('has-value')).toBe(true);
|
|
212
|
+
view.unmount();
|
|
213
|
+
});
|
|
214
|
+
(0, _vitest.it)('입력값을 지우면 has-value가 떨어진다', () => {
|
|
215
|
+
// Given: 값이 있는 상태에서
|
|
216
|
+
const view = mountInputBase();
|
|
217
|
+
typeInto(view.container, 'hello');
|
|
218
|
+
// When: 값을 비우면
|
|
219
|
+
typeInto(view.container, '');
|
|
220
|
+
// Then: has-value가 사라진다
|
|
221
|
+
(0, _vitest.expect)(view.container.querySelector('.ncua-input')?.classList.contains('has-value')).toBe(false);
|
|
222
|
+
view.unmount();
|
|
223
|
+
});
|
|
224
|
+
(0, _vitest.it)('controlled Input은 value prop만으로도 has-value가 붙는다', () => {
|
|
225
|
+
// Given/When: value를 직접 넘겨 렌더하면 (input 이벤트 없이)
|
|
226
|
+
const view = mountInputBase({
|
|
227
|
+
value: 'preset',
|
|
228
|
+
onChange: _vitest.vi.fn()
|
|
229
|
+
});
|
|
230
|
+
// Then: has-value가 붙는다
|
|
231
|
+
(0, _vitest.expect)(view.container.querySelector('.ncua-input')?.classList.contains('has-value')).toBe(true);
|
|
232
|
+
view.unmount();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
(0, _vitest.describe)('InputBase — 텍스트 슬롯', () => {
|
|
236
|
+
(0, _vitest.it)('leadingElement 텍스트는 leading 클래스와 wrap 클래스가 붙는다', () => {
|
|
237
|
+
const {
|
|
238
|
+
container,
|
|
239
|
+
unmount
|
|
240
|
+
} = mountInputBase({
|
|
241
|
+
leadingElement: {
|
|
242
|
+
type: 'text',
|
|
243
|
+
text: 'https://'
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
const text = container.querySelector('.ncua-input__leading-text');
|
|
247
|
+
(0, _vitest.expect)(text?.textContent).toBe('https://');
|
|
248
|
+
(0, _vitest.expect)(container.querySelector('.ncua-input__leading-text-wrap')).not.toBeNull();
|
|
249
|
+
unmount();
|
|
250
|
+
});
|
|
251
|
+
(0, _vitest.it)('trailingElement 텍스트는 trailing 클래스와 wrap 클래스가 붙는다', () => {
|
|
252
|
+
const {
|
|
253
|
+
container,
|
|
254
|
+
unmount
|
|
255
|
+
} = mountInputBase({
|
|
256
|
+
trailingElement: {
|
|
257
|
+
type: 'text',
|
|
258
|
+
text: 'px'
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
const text = container.querySelector('.ncua-input__trailing-text');
|
|
262
|
+
(0, _vitest.expect)(text?.textContent).toBe('px');
|
|
263
|
+
(0, _vitest.expect)(container.querySelector('.ncua-input__trailing-text-wrap')).not.toBeNull();
|
|
264
|
+
// leading 클래스가 섞이면 모서리·테두리 방향이 반대가 된다
|
|
265
|
+
(0, _vitest.expect)(container.querySelector('.ncua-input__leading-text')).toBeNull();
|
|
266
|
+
unmount();
|
|
267
|
+
});
|
|
268
|
+
(0, _vitest.it)('trailingElement 텍스트는 입력 필드 뒤에 온다', () => {
|
|
269
|
+
const {
|
|
270
|
+
container,
|
|
271
|
+
unmount
|
|
272
|
+
} = mountInputBase({
|
|
273
|
+
trailingElement: {
|
|
274
|
+
type: 'text',
|
|
275
|
+
text: 'px'
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
const content = container.querySelector('.ncua-input__content');
|
|
279
|
+
const children = Array.from(content?.children ?? []);
|
|
280
|
+
const fieldIndex = children.findIndex(el => el.classList.contains('ncua-input__field'));
|
|
281
|
+
const textIndex = children.findIndex(el => el.classList.contains('ncua-input__trailing-text'));
|
|
282
|
+
(0, _vitest.expect)(fieldIndex).toBeGreaterThanOrEqual(0);
|
|
283
|
+
(0, _vitest.expect)(textIndex).toBeGreaterThan(fieldIndex);
|
|
284
|
+
unmount();
|
|
285
|
+
});
|
|
286
|
+
(0, _vitest.it)('사이즈 modifier 가 함께 붙는다', () => {
|
|
287
|
+
const {
|
|
288
|
+
container,
|
|
289
|
+
unmount
|
|
290
|
+
} = mountInputBase({
|
|
291
|
+
size: 'sm',
|
|
292
|
+
trailingElement: {
|
|
293
|
+
type: 'text',
|
|
294
|
+
text: 'px'
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
(0, _vitest.expect)(container.querySelector('.ncua-input__trailing-text--sm')).not.toBeNull();
|
|
298
|
+
unmount();
|
|
299
|
+
});
|
|
72
300
|
});
|
|
@@ -33,8 +33,9 @@ const PasswordInput = exports.PasswordInput = /*#__PURE__*/(0, _react.forwardRef
|
|
|
33
33
|
type: isVisible ? 'text' : 'password',
|
|
34
34
|
leadingElement: {
|
|
35
35
|
type: 'icon',
|
|
36
|
-
icon: _uiAdminIcon.Lock01
|
|
37
|
-
color
|
|
36
|
+
icon: _uiAdminIcon.Lock01
|
|
37
|
+
// color 를 넘기지 않아 슬롯 아이콘 공통 규칙(상태별 색)을 그대로 따른다.
|
|
38
|
+
// 여기서 props.value 로 판정하면 uncontrolled Input 에서 영원히 빈 값 취급된다.
|
|
38
39
|
},
|
|
39
40
|
trailingElement: {
|
|
40
41
|
type: 'custom',
|
package/dist/cjs/src/components/forms-and-input/password-input/__tests__/PasswordInput.test.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _react = require("react");
|
|
4
|
+
var _client = require("react-dom/client");
|
|
5
|
+
var _testUtils = require("react-dom/test-utils");
|
|
6
|
+
var _vitest = require("vitest");
|
|
7
|
+
var _PasswordInput = require("../PasswordInput");
|
|
8
|
+
// @vitest-environment jsdom
|
|
9
|
+
|
|
10
|
+
// React 18에서 act() 사용 시 필요한 플래그 (미설정 시 console.error 경고 발생)
|
|
11
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
12
|
+
function mountPasswordInput() {
|
|
13
|
+
let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
14
|
+
const container = document.createElement('div');
|
|
15
|
+
document.body.appendChild(container);
|
|
16
|
+
const root = (0, _client.createRoot)(container);
|
|
17
|
+
(0, _testUtils.act)(() => {
|
|
18
|
+
root.render(/*#__PURE__*/(0, _react.createElement)(_PasswordInput.PasswordInput, props));
|
|
19
|
+
});
|
|
20
|
+
const unmount = () => {
|
|
21
|
+
(0, _testUtils.act)(() => {
|
|
22
|
+
root.unmount();
|
|
23
|
+
});
|
|
24
|
+
container.remove();
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
container,
|
|
28
|
+
unmount
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
(0, _vitest.describe)('PasswordInput — 자물쇠 아이콘 색상', () => {
|
|
32
|
+
// 자물쇠 색은 CSS 가 결정한다. 인라인 color 를 넘기면 상태 규칙이 무시되므로,
|
|
33
|
+
// "인라인 색이 없고 슬롯 아이콘 공통 클래스를 탄다" 는 것이 색상 반영의 전제다.
|
|
34
|
+
(0, _vitest.it)('자물쇠에 인라인 색을 넘기지 않고 슬롯 아이콘 공통 클래스를 탄다', () => {
|
|
35
|
+
// Given/When: 기본 상태로 렌더하면
|
|
36
|
+
const view = mountPasswordInput();
|
|
37
|
+
// Then: 상태 규칙을 타는 클래스가 붙고 인라인 색은 없다
|
|
38
|
+
const lockIcon = view.container.querySelector('.ncua-input__left-icon');
|
|
39
|
+
(0, _vitest.expect)(lockIcon).not.toBeNull();
|
|
40
|
+
(0, _vitest.expect)(lockIcon?.classList.contains('ncua-input__slot-icon')).toBe(true);
|
|
41
|
+
(0, _vitest.expect)(lockIcon?.hasAttribute('color')).toBe(false);
|
|
42
|
+
view.unmount();
|
|
43
|
+
});
|
|
44
|
+
// 값이 있는 채로 비활성화되면 has-value 가 살아 있어 자물쇠가 진하게 남던 문제
|
|
45
|
+
(0, _vitest.it)('값이 있는 채로 disabled 여도 자물쇠가 상태 규칙 대상으로 남는다', () => {
|
|
46
|
+
// Given/When: 값이 있는 비활성화 상태로 렌더하면
|
|
47
|
+
const view = mountPasswordInput({
|
|
48
|
+
value: 'secret',
|
|
49
|
+
disabled: true,
|
|
50
|
+
onChange: _vitest.vi.fn()
|
|
51
|
+
});
|
|
52
|
+
// Then: 루트에 is-disabled 와 has-value 가 함께 붙고, 자물쇠는 공통 클래스를 유지한다
|
|
53
|
+
const root = view.container.querySelector('.ncua-input');
|
|
54
|
+
(0, _vitest.expect)(root?.classList.contains('is-disabled')).toBe(true);
|
|
55
|
+
(0, _vitest.expect)(root?.classList.contains('has-value')).toBe(true);
|
|
56
|
+
(0, _vitest.expect)(view.container.querySelector('.ncua-input__left-icon')?.classList.contains('ncua-input__slot-icon')).toBe(true);
|
|
57
|
+
view.unmount();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -388,6 +388,17 @@ Object.keys(_divider).forEach(function (key) {
|
|
|
388
388
|
}
|
|
389
389
|
});
|
|
390
390
|
});
|
|
391
|
+
var _ncuaScope = require("./layout/ncua-scope");
|
|
392
|
+
Object.keys(_ncuaScope).forEach(function (key) {
|
|
393
|
+
if (key === "default" || key === "__esModule") return;
|
|
394
|
+
if (key in exports && exports[key] === _ncuaScope[key]) return;
|
|
395
|
+
Object.defineProperty(exports, key, {
|
|
396
|
+
enumerable: true,
|
|
397
|
+
get: function () {
|
|
398
|
+
return _ncuaScope[key];
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
});
|
|
391
402
|
var _pageTitle = require("./layout/page-title");
|
|
392
403
|
Object.keys(_pageTitle).forEach(function (key) {
|
|
393
404
|
if (key === "default" || key === "__esModule") return;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.NcuaScope = void 0;
|
|
8
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
9
|
+
var _classnames = _interopRequireDefault(require("classnames"));
|
|
10
|
+
var _react = require("react");
|
|
11
|
+
var _scopedCss = require("../../../generated/scoped-css");
|
|
12
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
+
// 조상에 NcuaScope 가 이미 있으면 스타일을 다시 심지 않는다 (중첩 대응).
|
|
14
|
+
const NcuaStyleMountedContext = /*#__PURE__*/(0, _react.createContext)(false);
|
|
15
|
+
// React 19 는 <style> 에 href·precedence 가 함께 있으면 <head> 로 올리고 href 로 중복을
|
|
16
|
+
// 제거한다. 18 에는 그 기능도, 두 prop 의 타입 정의도 없어 Context 로 중첩만 막고
|
|
17
|
+
// 타입은 우회한다.
|
|
18
|
+
const STYLE_HOISTING_MIN_REACT_MAJOR = 19;
|
|
19
|
+
const DECIMAL_RADIX = 10;
|
|
20
|
+
const SUPPORTS_STYLE_HOISTING = Number.parseInt(_react.version, DECIMAL_RADIX) >= STYLE_HOISTING_MIN_REACT_MAJOR;
|
|
21
|
+
const STYLE_KEY = 'ncua-scoped';
|
|
22
|
+
const STYLE_HOISTING_PROPS = {
|
|
23
|
+
href: STYLE_KEY,
|
|
24
|
+
precedence: 'default'
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* NCUA 스타일을 이 엘리먼트 하위로 한정하는 경계.
|
|
28
|
+
*
|
|
29
|
+
* 다른 디자인 시스템을 전역 적용한 사용처에서 NCUA 컴포넌트를 이걸로 감싼다.
|
|
30
|
+
* 스타일을 직접 들고 있으므로 **CSS 파일을 따로 import 하지 않는다.**
|
|
31
|
+
*
|
|
32
|
+
* ```tsx
|
|
33
|
+
* import { NcuaScope, Button } from '@ncds/ui-admin';
|
|
34
|
+
*
|
|
35
|
+
* <NcuaScope>
|
|
36
|
+
* <Button label="신청하기" hierarchy="primary" size="md" />
|
|
37
|
+
* </NcuaScope>
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* `document.body` 로 portal 되는 컴포넌트(Modal · Tooltip · Dropdown · SelectBox ·
|
|
41
|
+
* Notification)는 이 경계 밖에 놓이지만, 스코프 스타일시트가 그 루트 클래스들을 함께
|
|
42
|
+
* 지정하므로 토큰을 받는다 (`base/_scope.scss` 의 `$roots`). portal 목적지는 바뀌지
|
|
43
|
+
* 않으므로 어드민에서의 기존 동작과 같다.
|
|
44
|
+
*
|
|
45
|
+
* 어드민처럼 `style.css` 를 전역 적용하는 사용처는 이 컴포넌트가 필요 없다.
|
|
46
|
+
*
|
|
47
|
+
* 중첩해서 써도 스타일은 한 번만 들어간다. React 19 에서는 형제로 여러 개를 써도
|
|
48
|
+
* href 기준으로 중복이 제거된다. React 18 에서는 형제마다 `<style>` 이 하나씩 생기므로,
|
|
49
|
+
* 스코프가 여러 곳에 필요하면 공통 조상 한 곳에서 감싸는 편이 낫다.
|
|
50
|
+
*/
|
|
51
|
+
const NcuaScope = exports.NcuaScope = /*#__PURE__*/(0, _react.forwardRef)((_ref, ref) => {
|
|
52
|
+
let {
|
|
53
|
+
className,
|
|
54
|
+
children
|
|
55
|
+
} = _ref;
|
|
56
|
+
const styleAlreadyMounted = (0, _react.useContext)(NcuaStyleMountedContext);
|
|
57
|
+
const shouldRenderStyle = SUPPORTS_STYLE_HOISTING || !styleAlreadyMounted;
|
|
58
|
+
return (0, _jsxRuntime.jsxs)(NcuaStyleMountedContext.Provider, {
|
|
59
|
+
value: true,
|
|
60
|
+
children: [shouldRenderStyle && (SUPPORTS_STYLE_HOISTING ? (0, _jsxRuntime.jsx)("style", {
|
|
61
|
+
...STYLE_HOISTING_PROPS,
|
|
62
|
+
children: _scopedCss.NCUA_SCOPED_CSS
|
|
63
|
+
}) : (0, _jsxRuntime.jsx)("style", {
|
|
64
|
+
"data-ncua-scope": STYLE_KEY,
|
|
65
|
+
children: _scopedCss.NCUA_SCOPED_CSS
|
|
66
|
+
})), (0, _jsxRuntime.jsx)("div", {
|
|
67
|
+
ref: ref,
|
|
68
|
+
className: (0, _classnames.default)('ncua-scope', className),
|
|
69
|
+
children: children
|
|
70
|
+
})]
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
NcuaScope.displayName = 'NcuaScope';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
var _NcuaScope = require("./NcuaScope");
|
|
7
|
+
Object.keys(_NcuaScope).forEach(function (key) {
|
|
8
|
+
if (key === "default" || key === "__esModule") return;
|
|
9
|
+
if (key in exports && exports[key] === _NcuaScope[key]) return;
|
|
10
|
+
Object.defineProperty(exports, key, {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
get: function () {
|
|
13
|
+
return _NcuaScope[key];
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
});
|