@ncds/ui-admin 1.8.13 → 1.8.15
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/checkbox/CheckboxInput.js +3 -2
- package/dist/cjs/src/components/forms-and-input/date-picker/DatePicker.js +17 -20
- package/dist/cjs/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.js +53 -0
- package/dist/cjs/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.js +129 -0
- package/dist/cjs/src/components/forms-and-input/range-date-picker/RangeDatePicker.js +22 -0
- package/dist/cjs/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.js +95 -0
- package/dist/cjs/src/components/forms-and-input/range-date-picker-with-buttons/RangeDatePickerWithButtons.js +7 -1
- package/dist/cjs/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.js +57 -0
- package/dist/cjs/src/components/forms-and-input/select/Select.js +12 -1
- package/dist/cjs/src/components/forms-and-input/select-box/SelectBox.js +5 -2
- package/dist/cjs/src/components/forms-and-input/slider/Slider.js +7 -1
- package/dist/cjs/src/components/navigation/pagination/Pagination.js +12 -2
- package/dist/cjs/src/components/navigation/pagination/__tests__/Pagination.test.js +62 -0
- package/dist/cjs/src/components/overlays/tooltip/Tooltip.js +15 -2
- package/dist/cjs/src/components/overlays/tooltip/__tests__/utils.test.js +40 -0
- package/dist/cjs/src/components/overlays/tooltip/utils.js +20 -0
- package/dist/cjs/src/hooks/index.js +11 -0
- package/dist/cjs/src/hooks/useIsomorphicLayoutEffect.js +8 -0
- package/dist/cjs/src/hooks/useMediaQuery.js +3 -0
- package/dist/cjs/src/utils/__tests__/date-picker.test.js +29 -0
- package/dist/cjs/src/utils/date-picker.js +15 -1
- package/dist/esm/src/components/forms-and-input/checkbox/CheckboxInput.js +4 -3
- package/dist/esm/src/components/forms-and-input/date-picker/DatePicker.js +18 -21
- package/dist/esm/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.js +50 -0
- package/dist/esm/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.js +126 -0
- package/dist/esm/src/components/forms-and-input/range-date-picker/RangeDatePicker.js +22 -0
- package/dist/esm/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.js +92 -0
- package/dist/esm/src/components/forms-and-input/range-date-picker-with-buttons/RangeDatePickerWithButtons.js +7 -1
- package/dist/esm/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.js +54 -0
- package/dist/esm/src/components/forms-and-input/select/Select.js +12 -1
- package/dist/esm/src/components/forms-and-input/select-box/SelectBox.js +5 -2
- package/dist/esm/src/components/forms-and-input/slider/Slider.js +7 -1
- package/dist/esm/src/components/navigation/pagination/Pagination.js +12 -2
- package/dist/esm/src/components/navigation/pagination/__tests__/Pagination.test.js +59 -0
- package/dist/esm/src/components/overlays/tooltip/Tooltip.js +15 -2
- package/dist/esm/src/components/overlays/tooltip/__tests__/utils.test.js +38 -0
- package/dist/esm/src/components/overlays/tooltip/utils.js +13 -0
- package/dist/esm/src/hooks/index.js +1 -0
- package/dist/esm/src/hooks/useIsomorphicLayoutEffect.js +2 -0
- package/dist/esm/src/hooks/useMediaQuery.js +3 -0
- package/dist/esm/src/utils/__tests__/date-picker.test.js +26 -0
- package/dist/esm/src/utils/date-picker.js +13 -0
- package/dist/temp/src/components/forms-and-input/checkbox/CheckboxInput.js +4 -3
- package/dist/temp/src/components/forms-and-input/date-picker/DatePicker.js +18 -28
- package/dist/temp/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.d.ts +1 -0
- package/dist/temp/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.js +43 -0
- package/dist/temp/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.d.ts +1 -0
- package/dist/temp/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.js +109 -0
- package/dist/temp/src/components/forms-and-input/range-date-picker/RangeDatePicker.js +22 -0
- package/dist/temp/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.d.ts +1 -0
- package/dist/temp/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.js +60 -0
- package/dist/temp/src/components/forms-and-input/range-date-picker-with-buttons/RangeDatePickerWithButtons.js +9 -1
- package/dist/temp/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.d.ts +1 -0
- package/dist/temp/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.js +42 -0
- package/dist/temp/src/components/forms-and-input/select/Select.d.ts +1 -0
- package/dist/temp/src/components/forms-and-input/select/Select.js +10 -2
- package/dist/temp/src/components/forms-and-input/select-box/SelectBox.d.ts +1 -0
- package/dist/temp/src/components/forms-and-input/select-box/SelectBox.js +5 -4
- package/dist/temp/src/components/forms-and-input/slider/Slider.js +7 -1
- package/dist/temp/src/components/navigation/pagination/Pagination.js +12 -2
- package/dist/temp/src/components/navigation/pagination/__tests__/Pagination.test.d.ts +1 -0
- package/dist/temp/src/components/navigation/pagination/__tests__/Pagination.test.js +59 -0
- package/dist/temp/src/components/overlays/tooltip/Tooltip.js +21 -4
- package/dist/temp/src/components/overlays/tooltip/__tests__/utils.test.d.ts +1 -0
- package/dist/temp/src/components/overlays/tooltip/__tests__/utils.test.js +38 -0
- package/dist/temp/src/components/overlays/tooltip/utils.d.ts +9 -0
- package/dist/temp/src/components/overlays/tooltip/utils.js +13 -0
- package/dist/temp/src/hooks/index.d.ts +1 -0
- package/dist/temp/src/hooks/index.js +1 -0
- package/dist/temp/src/hooks/useIsomorphicLayoutEffect.d.ts +2 -0
- package/dist/temp/src/hooks/useIsomorphicLayoutEffect.js +2 -0
- package/dist/temp/src/hooks/useMediaQuery.js +3 -0
- package/dist/temp/src/utils/__tests__/date-picker.test.d.ts +1 -0
- package/dist/temp/src/utils/__tests__/date-picker.test.js +26 -0
- package/dist/temp/src/utils/date-picker.d.ts +2 -0
- package/dist/temp/src/utils/date-picker.js +13 -0
- package/dist/types/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.d.ts +1 -0
- package/dist/types/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.d.ts +1 -0
- package/dist/types/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.d.ts +1 -0
- package/dist/types/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.d.ts +1 -0
- package/dist/types/src/components/forms-and-input/select/Select.d.ts +1 -0
- package/dist/types/src/components/forms-and-input/select-box/SelectBox.d.ts +1 -0
- package/dist/types/src/components/navigation/pagination/__tests__/Pagination.test.d.ts +1 -0
- package/dist/types/src/components/overlays/tooltip/__tests__/utils.test.d.ts +1 -0
- package/dist/types/src/components/overlays/tooltip/utils.d.ts +9 -0
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/useIsomorphicLayoutEffect.d.ts +2 -0
- package/dist/types/src/utils/__tests__/date-picker.test.d.ts +1 -0
- package/dist/types/src/utils/date-picker.d.ts +2 -0
- package/dist/ui-admin/assets/styles/style.css +33 -21
- package/package.json +2 -1
|
@@ -4,7 +4,7 @@ import { Korean } from 'flatpickr/dist/l10n/ko';
|
|
|
4
4
|
import moment from 'moment';
|
|
5
5
|
import { forwardRef, useCallback, useEffect, useId, useMemo, useRef } from 'react';
|
|
6
6
|
import Flatpickr from 'react-flatpickr';
|
|
7
|
-
import { formatDateInput, formatHourInput, formatMinuteInput } from '../../../utils/date-picker';
|
|
7
|
+
import { convertToMomentFormat, formatDateInput, formatHourInput, formatMinuteInput } from '../../../utils/date-picker';
|
|
8
8
|
import { CustomInput } from './CustomInput';
|
|
9
9
|
// ══════════════════════════════════════════
|
|
10
10
|
// 유틸리티 함수
|
|
@@ -171,6 +171,12 @@ const setupTimeInputHandlers = (instance, input, isPortal, onHourInput, onMinute
|
|
|
171
171
|
};
|
|
172
172
|
/** 시간 입력 필드 이벤트 리스너 정리 */
|
|
173
173
|
const cleanupTimeInputHandlers = (instance, input, isPortal, onHourInput, onMinuteInput) => {
|
|
174
|
+
// document 전역 리스너는 캘린더 DOM 조회 성공 여부와 무관하게 항상 먼저 제거한다 (누수 방지)
|
|
175
|
+
const handleMouseDown = instance._handleMouseDown;
|
|
176
|
+
if (handleMouseDown) {
|
|
177
|
+
document.removeEventListener('mousedown', handleMouseDown, true);
|
|
178
|
+
instance._handleMouseDown = undefined;
|
|
179
|
+
}
|
|
174
180
|
const timeContainer = isPortal ? instance.calendarContainer : input.parentElement;
|
|
175
181
|
const timeInputWrapper = timeContainer?.querySelector('.flatpickr-time');
|
|
176
182
|
if (!timeInputWrapper)
|
|
@@ -183,10 +189,6 @@ const cleanupTimeInputHandlers = (instance, input, isPortal, onHourInput, onMinu
|
|
|
183
189
|
if (minuteInput) {
|
|
184
190
|
minuteInput.removeEventListener('input', onMinuteInput);
|
|
185
191
|
}
|
|
186
|
-
const handleMouseDown = instance._handleMouseDown;
|
|
187
|
-
if (handleMouseDown) {
|
|
188
|
-
document.removeEventListener('mousedown', handleMouseDown, true);
|
|
189
|
-
}
|
|
190
192
|
};
|
|
191
193
|
// ══════════════════════════════════════════
|
|
192
194
|
// DatePicker 컴포넌트
|
|
@@ -204,7 +206,7 @@ export const DatePicker = forwardRef(({ shouldFocus = true, currentDate, size =
|
|
|
204
206
|
const onValidationErrorRef = useRef(onValidationError);
|
|
205
207
|
onValidationErrorRef.current = onValidationError;
|
|
206
208
|
/** portal 컨테이너를 lazily 생성 (이미 있으면 재사용), className은 매번 갱신 */
|
|
207
|
-
const getPortalContainer = () => {
|
|
209
|
+
const getPortalContainer = useCallback(() => {
|
|
208
210
|
if (!portal || typeof document === 'undefined')
|
|
209
211
|
return undefined;
|
|
210
212
|
const portalClassName = classNames('ncua-date-picker', 'ncua-date-picker--portal', `ncua-date-picker--${size}`, {
|
|
@@ -221,7 +223,7 @@ export const DatePicker = forwardRef(({ shouldFocus = true, currentDate, size =
|
|
|
221
223
|
document.body.appendChild(el);
|
|
222
224
|
portalContainerRef.current = el;
|
|
223
225
|
return el;
|
|
224
|
-
};
|
|
226
|
+
}, [portal, size, hasTimeOption]);
|
|
225
227
|
/** 컴포넌트 언마운트 시 포탈 컨테이너 및 스크롤 리스너 정리 */
|
|
226
228
|
useEffect(() => {
|
|
227
229
|
return () => {
|
|
@@ -233,7 +235,7 @@ export const DatePicker = forwardRef(({ shouldFocus = true, currentDate, size =
|
|
|
233
235
|
// 날짜 유효성 검사
|
|
234
236
|
// ──────────────────────────────────────────────
|
|
235
237
|
/** minDate/maxDate 범위를 벗어났는지 확인 */
|
|
236
|
-
const checkDateViolations = (date, minDate, maxDate) => {
|
|
238
|
+
const checkDateViolations = useCallback((date, minDate, maxDate) => {
|
|
237
239
|
const violations = [];
|
|
238
240
|
const inputDate = moment(date);
|
|
239
241
|
if (!inputDate.isValid())
|
|
@@ -251,16 +253,16 @@ export const DatePicker = forwardRef(({ shouldFocus = true, currentDate, size =
|
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
255
|
return violations;
|
|
254
|
-
};
|
|
256
|
+
}, []);
|
|
255
257
|
// ──────────────────────────────────────────────
|
|
256
258
|
// 이벤트 핸들러
|
|
257
259
|
// ──────────────────────────────────────────────
|
|
258
260
|
/** 유효하지 않은 입력 시 이전 날짜로 복원 */
|
|
259
|
-
const restorePreviousDate = (target, instance) => {
|
|
261
|
+
const restorePreviousDate = useCallback((target, instance) => {
|
|
260
262
|
if (instance.selectedDates.length > 0) {
|
|
261
263
|
const prevDate = instance.selectedDates[0];
|
|
262
264
|
if (prevDate instanceof Date && !Number.isNaN(prevDate.getTime())) {
|
|
263
|
-
const momentFormat = dateFormatRef.current
|
|
265
|
+
const momentFormat = convertToMomentFormat(dateFormatRef.current);
|
|
264
266
|
target.value = moment(prevDate).format(momentFormat);
|
|
265
267
|
instance.setDate(prevDate, false);
|
|
266
268
|
return;
|
|
@@ -268,7 +270,7 @@ export const DatePicker = forwardRef(({ shouldFocus = true, currentDate, size =
|
|
|
268
270
|
}
|
|
269
271
|
target.value = '';
|
|
270
272
|
instance.setDate('', false);
|
|
271
|
-
};
|
|
273
|
+
}, []);
|
|
272
274
|
/** flatpickr에서 날짜가 변경되었을 때 호출 */
|
|
273
275
|
const onChangeDateHandler = useCallback((dateTimeStamp, dateStr, fpInstance) => {
|
|
274
276
|
const instance = fpInstance;
|
|
@@ -283,7 +285,9 @@ export const DatePicker = forwardRef(({ shouldFocus = true, currentDate, size =
|
|
|
283
285
|
const maxDate = instance.config.maxDate;
|
|
284
286
|
const violations = checkDateViolations(selectedDate, minDate, maxDate);
|
|
285
287
|
if (violations.length > 0 && onValidationErrorRef.current) {
|
|
286
|
-
|
|
288
|
+
// flatpickr는 onChange 발화 전에 selectedDates를 위반된 새 날짜로 갱신하므로,
|
|
289
|
+
// 직전 유효 날짜를 보관해 둔 _previousDateBeforeInput을 previousDate로 사용한다
|
|
290
|
+
const prevDate = instance._previousDateBeforeInput;
|
|
287
291
|
const validPrevDate = prevDate instanceof Date && !Number.isNaN(prevDate.getTime()) ? prevDate : undefined;
|
|
288
292
|
onValidationErrorRef.current({
|
|
289
293
|
date: selectedDate,
|
|
@@ -299,7 +303,6 @@ export const DatePicker = forwardRef(({ shouldFocus = true, currentDate, size =
|
|
|
299
303
|
isValidDate(formattedDate) ? onChangeDateRef.current(formattedDate) : onChangeDateRef.current(dateStr);
|
|
300
304
|
}, [checkDateViolations, restorePreviousDate]);
|
|
301
305
|
/** input에 직접 타이핑할 때 날짜 형식 자동 변환 및 유효성 검사 */
|
|
302
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
303
306
|
const onInputHandler = useCallback((e) => {
|
|
304
307
|
const target = e.target;
|
|
305
308
|
const input = target.value;
|
|
@@ -356,12 +359,6 @@ export const DatePicker = forwardRef(({ shouldFocus = true, currentDate, size =
|
|
|
356
359
|
// ──────────────────────────────────────────────
|
|
357
360
|
// flatpickr 옵션
|
|
358
361
|
// ──────────────────────────────────────────────
|
|
359
|
-
// datePickerOptions는 부모에서 매 렌더 새 객체일 수 있으므로 값 기반 비교
|
|
360
|
-
// 주의: JSON.stringify는 함수를 무시하고 Date를 문자열로 변환하므로,
|
|
361
|
-
// datePickerOptions에 함수나 Date 객체를 넘기면 변경 감지가 누락될 수 있음
|
|
362
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
363
|
-
const _datePickerOptionsKey = JSON.stringify(datePickerOptions);
|
|
364
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
365
362
|
const options = useMemo(() => ({
|
|
366
363
|
mode: 'single',
|
|
367
364
|
static: !portal,
|
|
@@ -382,14 +379,7 @@ export const DatePicker = forwardRef(({ shouldFocus = true, currentDate, size =
|
|
|
382
379
|
if (!momentDate.isValid()) {
|
|
383
380
|
return '';
|
|
384
381
|
}
|
|
385
|
-
const momentFormat = format
|
|
386
|
-
.replace(/Y/g, 'YYYY')
|
|
387
|
-
.replace(/y/g, 'YY')
|
|
388
|
-
.replace(/m/g, 'MM')
|
|
389
|
-
.replace(/d/g, 'DD')
|
|
390
|
-
.replace(/H/g, 'HH')
|
|
391
|
-
.replace(/i/g, 'mm')
|
|
392
|
-
.replace(/S/g, 'ss');
|
|
382
|
+
const momentFormat = convertToMomentFormat(format);
|
|
393
383
|
return momentDate.format(momentFormat);
|
|
394
384
|
}
|
|
395
385
|
catch (_error) {
|
package/dist/temp/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/temp/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { createElement, useState } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { act } from 'react-dom/test-utils';
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { DatePicker } from '../DatePicker';
|
|
7
|
+
const { captured } = vi.hoisted(() => ({ captured: [] }));
|
|
8
|
+
// react-flatpickr를 mock하여 DatePicker가 넘기는 options 참조를 렌더마다 수집한다.
|
|
9
|
+
// (vi.mock / vi.hoisted는 vitest가 import 위로 호이스팅하므로 DatePicker import보다 먼저 적용된다)
|
|
10
|
+
vi.mock('react-flatpickr', () => ({
|
|
11
|
+
default: (props) => {
|
|
12
|
+
captured.push(props.options);
|
|
13
|
+
return null;
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
describe('#1 options useMemo 안정성', () => {
|
|
17
|
+
it('부모가 같은 props로 강제 리렌더해도 options 참조가 동일하게 유지된다(재생성 안 됨)', () => {
|
|
18
|
+
let bump = () => undefined;
|
|
19
|
+
const Harness = () => {
|
|
20
|
+
const [, setN] = useState(0);
|
|
21
|
+
bump = () => setN((x) => x + 1);
|
|
22
|
+
return createElement(DatePicker, { currentDate: '2024-03-20', onChangeDate: () => undefined });
|
|
23
|
+
};
|
|
24
|
+
const container = document.createElement('div');
|
|
25
|
+
document.body.appendChild(container);
|
|
26
|
+
const root = createRoot(container);
|
|
27
|
+
act(() => {
|
|
28
|
+
root.render(createElement(Harness));
|
|
29
|
+
});
|
|
30
|
+
const countAfterMount = captured.length;
|
|
31
|
+
const firstOptions = captured[captured.length - 1];
|
|
32
|
+
act(() => {
|
|
33
|
+
bump();
|
|
34
|
+
});
|
|
35
|
+
const lastOptions = captured[captured.length - 1];
|
|
36
|
+
expect(captured.length).toBeGreaterThan(countAfterMount); // 리렌더가 실제로 일어났는지
|
|
37
|
+
expect(Object.is(firstOptions, lastOptions)).toBe(true); // 동일 참조 = options 재생성 안 됨
|
|
38
|
+
act(() => {
|
|
39
|
+
root.unmount();
|
|
40
|
+
});
|
|
41
|
+
container.remove();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { createElement } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { act } from 'react-dom/test-utils';
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { DatePicker } from '../DatePicker';
|
|
7
|
+
const mounted = [];
|
|
8
|
+
function mount(props) {
|
|
9
|
+
const container = document.createElement('div');
|
|
10
|
+
document.body.appendChild(container);
|
|
11
|
+
const root = createRoot(container);
|
|
12
|
+
act(() => {
|
|
13
|
+
root.render(createElement(DatePicker, props));
|
|
14
|
+
});
|
|
15
|
+
mounted.push({ root, container });
|
|
16
|
+
return container;
|
|
17
|
+
}
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
for (const { root, container } of mounted.splice(0)) {
|
|
20
|
+
act(() => {
|
|
21
|
+
root.unmount();
|
|
22
|
+
});
|
|
23
|
+
container.remove();
|
|
24
|
+
}
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
});
|
|
27
|
+
describe('DatePicker 렌더 (스모크)', () => {
|
|
28
|
+
it('기본 props로 input을 렌더하고 크래시하지 않는다', () => {
|
|
29
|
+
const container = mount({ currentDate: '2024-03-20', onChangeDate: () => undefined });
|
|
30
|
+
expect(container.querySelector('input')).not.toBeNull();
|
|
31
|
+
});
|
|
32
|
+
it('enableTime 옵션으로도 정상 렌더한다', () => {
|
|
33
|
+
const container = mount({
|
|
34
|
+
currentDate: '2024-03-20 14:30',
|
|
35
|
+
onChangeDate: () => undefined,
|
|
36
|
+
datePickerOptions: { enableTime: true, dateFormat: 'Y-m-d H:i' },
|
|
37
|
+
});
|
|
38
|
+
expect(container.querySelector('input')).not.toBeNull();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('#4 document mousedown 리스너 정리', () => {
|
|
42
|
+
it('enableTime 마운트 후 언마운트 시, 추가된 mousedown 리스너가 모두 제거된다(누수 없음)', () => {
|
|
43
|
+
const added = [];
|
|
44
|
+
const removed = [];
|
|
45
|
+
const origAdd = document.addEventListener.bind(document);
|
|
46
|
+
const origRemove = document.removeEventListener.bind(document);
|
|
47
|
+
vi.spyOn(document, 'addEventListener').mockImplementation((type, fn, opts) => {
|
|
48
|
+
if (type === 'mousedown' && fn)
|
|
49
|
+
added.push(fn);
|
|
50
|
+
return origAdd(type, fn, opts);
|
|
51
|
+
});
|
|
52
|
+
vi.spyOn(document, 'removeEventListener').mockImplementation((type, fn, opts) => {
|
|
53
|
+
if (type === 'mousedown' && fn)
|
|
54
|
+
removed.push(fn);
|
|
55
|
+
return origRemove(type, fn, opts);
|
|
56
|
+
});
|
|
57
|
+
const container = document.createElement('div');
|
|
58
|
+
document.body.appendChild(container);
|
|
59
|
+
const root = createRoot(container);
|
|
60
|
+
act(() => {
|
|
61
|
+
root.render(createElement(DatePicker, {
|
|
62
|
+
currentDate: '2024-03-20 14:30',
|
|
63
|
+
onChangeDate: () => undefined,
|
|
64
|
+
datePickerOptions: { enableTime: true, dateFormat: 'Y-m-d H:i' },
|
|
65
|
+
}));
|
|
66
|
+
});
|
|
67
|
+
expect(added.length).toBeGreaterThanOrEqual(1);
|
|
68
|
+
act(() => {
|
|
69
|
+
root.unmount();
|
|
70
|
+
});
|
|
71
|
+
container.remove();
|
|
72
|
+
for (const fn of added) {
|
|
73
|
+
expect(removed).toContain(fn);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('#3 onValidationError.previousDate', () => {
|
|
78
|
+
// 허용 범위(10~25일) 기준: 마지막 유효 선택일과 범위 밖 위반일
|
|
79
|
+
const LAST_VALID_DAY = 15;
|
|
80
|
+
const OUT_OF_RANGE_DAY = 30;
|
|
81
|
+
it('min/max 위반 시 previousDate는 위반 날짜가 아니라 직전 유효 날짜다', () => {
|
|
82
|
+
const onValidationError = vi.fn();
|
|
83
|
+
const container = mount({
|
|
84
|
+
currentDate: '2024-03-20',
|
|
85
|
+
onChangeDate: () => undefined,
|
|
86
|
+
datePickerOptions: { minDate: '2024-03-10', maxDate: '2024-03-25', allowInput: true },
|
|
87
|
+
onValidationError,
|
|
88
|
+
});
|
|
89
|
+
// flatpickr 인스턴스는 초기화된 input 엘리먼트의 _flatpickr에 부착된다
|
|
90
|
+
const input = container.querySelector('input');
|
|
91
|
+
const instance = input?._flatpickr;
|
|
92
|
+
expect(instance).toBeTruthy();
|
|
93
|
+
// 1) 유효 날짜를 먼저 선택 → 직전 유효값(_previousDateBeforeInput)이 확립됨
|
|
94
|
+
act(() => {
|
|
95
|
+
instance.setDate(`2024-03-${LAST_VALID_DAY}`, true);
|
|
96
|
+
});
|
|
97
|
+
expect(onValidationError).not.toHaveBeenCalled();
|
|
98
|
+
// 2) 허용 범위 밖 날짜 선택 → 위반 분기 진입
|
|
99
|
+
act(() => {
|
|
100
|
+
instance.setDate(`2024-03-${OUT_OF_RANGE_DAY}`, true);
|
|
101
|
+
});
|
|
102
|
+
expect(onValidationError).toHaveBeenCalledTimes(1);
|
|
103
|
+
const arg = onValidationError.mock.calls[0][0];
|
|
104
|
+
expect(arg.violations).toContain('maxDate');
|
|
105
|
+
expect(arg.date.getDate()).toBe(OUT_OF_RANGE_DAY); // 위반 날짜
|
|
106
|
+
expect(arg.previousDate?.getDate()).toBe(LAST_VALID_DAY); // 직전 유효 날짜 (≠ 위반 날짜)
|
|
107
|
+
expect(arg.previousDate?.getTime()).not.toBe(arg.date.getTime());
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -3,6 +3,10 @@ import moment from 'moment';
|
|
|
3
3
|
import { forwardRef, useCallback, useEffect } from 'react';
|
|
4
4
|
import { DatePicker } from '../date-picker/DatePicker';
|
|
5
5
|
import { getDateFormat } from '../date-picker/utils';
|
|
6
|
+
/** 시작일·종료일이 모두 채워져 있고 유효한 날짜일 때만 범위 검증이 의미가 있다.
|
|
7
|
+
* 한쪽이라도 비어 있으면(예: ENTIRE 초기화로 양쪽이 '') moment 비교가 전부 무효가 되어
|
|
8
|
+
* onDateValidation 에 "Invalid date" 같은 잘못된 newDate 가 전달되므로 검증을 건너뛴다. */
|
|
9
|
+
const areBothDatesValid = (start, end) => Boolean(start) && Boolean(end) && moment(start).isValid() && moment(end).isValid();
|
|
6
10
|
const RangeDatePicker = forwardRef(({ startDateOptions, endDateOptions, validationOption, onDateValidation, size = 'xs' }, ref) => {
|
|
7
11
|
const getOverlapResult = useCallback((type) => {
|
|
8
12
|
if (type === 'start' && moment(startDateOptions.currentDate).isAfter(endDateOptions.currentDate)) {
|
|
@@ -52,6 +56,9 @@ const RangeDatePicker = forwardRef(({ startDateOptions, endDateOptions, validati
|
|
|
52
56
|
}
|
|
53
57
|
}, [startDateOptions, endDateOptions, validationOption, onDateValidation, getOverlapResult, getSettingNewDate]);
|
|
54
58
|
useEffect(() => {
|
|
59
|
+
if (!areBothDatesValid(startDateOptions.currentDate, endDateOptions.currentDate)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
55
62
|
const isOverDate = moment(startDateOptions.currentDate).isAfter(endDateOptions.currentDate);
|
|
56
63
|
if (isOverDate) {
|
|
57
64
|
changeSettingDateAndAlert('start');
|
|
@@ -73,8 +80,18 @@ const RangeDatePicker = forwardRef(({ startDateOptions, endDateOptions, validati
|
|
|
73
80
|
else {
|
|
74
81
|
changeSettingDateAndAlert('start', true);
|
|
75
82
|
}
|
|
83
|
+
// 시작일이 변경될 때만 검증한다. 반대편 날짜·콜백·setting 을 deps 에 넣으면, 부모가 매 렌더
|
|
84
|
+
// 새 options 객체와 onDateValidation 을 내려보내는 경우(예: RangeDatePickerWithButtons)
|
|
85
|
+
// effect 가 매 렌더 재실행되어 검증 루프가 발생하므로 의도적으로 deps 를 좁게 유지한다.
|
|
86
|
+
// (과거 무한 루프 수정 47f09427 + deps 축소 1c5ac7ad 의 결과 — deps 를 되돌리면 루프 재발)
|
|
87
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
76
88
|
}, [startDateOptions.currentDate]);
|
|
77
89
|
useEffect(() => {
|
|
90
|
+
if (!areBothDatesValid(startDateOptions.currentDate, endDateOptions.currentDate)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// 종료일이 '오늘'이면 종료측 검증을 건너뛴다: shopby 는 종료일=오늘이 정상 기본값이라
|
|
94
|
+
// (고도몰=어제) 이 상태를 에러로 잡지 않기 위함. (도입 d1bd8751)
|
|
78
95
|
const isNotTodayEndDate = !moment(endDateOptions.currentDate).isSame(moment(), 'day');
|
|
79
96
|
if (!validationOption?.setting || !isNotTodayEndDate) {
|
|
80
97
|
return;
|
|
@@ -97,6 +114,11 @@ const RangeDatePicker = forwardRef(({ startDateOptions, endDateOptions, validati
|
|
|
97
114
|
else {
|
|
98
115
|
changeSettingDateAndAlert('end', true);
|
|
99
116
|
}
|
|
117
|
+
// 종료일이 변경될 때만 검증한다(위 시작일 effect 와 동일 — 47f09427/1c5ac7ad).
|
|
118
|
+
// 본문이 startDateOptions.currentDate 도 참조하지만 시작일 변경 시 종료측 검증을 다시 돌리지
|
|
119
|
+
// 않는 건 의도된 것이다: 시작일 변경은 위 effect 가 type:'start' 로 처리하며, 여기에 start 를
|
|
120
|
+
// deps 로 넣으면 1c5ac7ad 가 제거한 그 루프가 재발한다.
|
|
121
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
100
122
|
}, [endDateOptions.currentDate]);
|
|
101
123
|
return (_jsxs("div", { className: "ncua-range-date-picker", ref: ref || undefined, children: [_jsx(DatePicker, { ...startDateOptions, ref: undefined, size: size }), "~", _jsx(DatePicker, { isEndDate: true, ...endDateOptions, ref: undefined, size: size })] }));
|
|
102
124
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/temp/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { createElement } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { act } from 'react-dom/test-utils';
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { RangeDatePicker } from '../RangeDatePicker';
|
|
7
|
+
const mounted = [];
|
|
8
|
+
function mount(props) {
|
|
9
|
+
const container = document.createElement('div');
|
|
10
|
+
document.body.appendChild(container);
|
|
11
|
+
const root = createRoot(container);
|
|
12
|
+
act(() => {
|
|
13
|
+
root.render(createElement(RangeDatePicker, props));
|
|
14
|
+
});
|
|
15
|
+
mounted.push({ root, container });
|
|
16
|
+
return container;
|
|
17
|
+
}
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
for (const { root, container } of mounted.splice(0)) {
|
|
20
|
+
act(() => {
|
|
21
|
+
root.unmount();
|
|
22
|
+
});
|
|
23
|
+
container.remove();
|
|
24
|
+
}
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
});
|
|
27
|
+
describe('#1 빈/무효 날짜 검증 가드', () => {
|
|
28
|
+
const validationOption = { setting: { unit: 'days', period: 7 } };
|
|
29
|
+
it('양쪽 날짜가 비어 있으면 onDateValidation 을 호출하지 않는다', () => {
|
|
30
|
+
const onDateValidation = vi.fn();
|
|
31
|
+
mount({
|
|
32
|
+
startDateOptions: { currentDate: '', onChangeDate: () => undefined },
|
|
33
|
+
endDateOptions: { currentDate: '', onChangeDate: () => undefined },
|
|
34
|
+
validationOption,
|
|
35
|
+
onDateValidation,
|
|
36
|
+
});
|
|
37
|
+
expect(onDateValidation).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
it('한쪽(종료일)만 비어 있어도 "Invalid date" 를 newDate 로 전달하지 않는다', () => {
|
|
40
|
+
const onDateValidation = vi.fn();
|
|
41
|
+
mount({
|
|
42
|
+
startDateOptions: { currentDate: '2024-03-01', onChangeDate: () => undefined },
|
|
43
|
+
endDateOptions: { currentDate: '', onChangeDate: () => undefined },
|
|
44
|
+
validationOption,
|
|
45
|
+
onDateValidation,
|
|
46
|
+
});
|
|
47
|
+
const leakedInvalidDate = onDateValidation.mock.calls.some(([arg]) => arg?.newDate === 'Invalid date');
|
|
48
|
+
expect(leakedInvalidDate).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
it('양쪽 날짜가 유효하면 검증이 정상 동작한다(가드가 정상 입력을 막지 않음)', () => {
|
|
51
|
+
const onDateValidation = vi.fn();
|
|
52
|
+
mount({
|
|
53
|
+
startDateOptions: { currentDate: '2024-03-01', onChangeDate: () => undefined },
|
|
54
|
+
endDateOptions: { currentDate: '2024-03-05', onChangeDate: () => undefined },
|
|
55
|
+
validationOption,
|
|
56
|
+
onDateValidation,
|
|
57
|
+
});
|
|
58
|
+
expect(onDateValidation).toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -21,6 +21,9 @@ const RangeDatePickerWithButtons = ({ useYesterdayAsEndDate = false, size = 'xs'
|
|
|
21
21
|
unit: currentPeriodItem.unit,
|
|
22
22
|
}));
|
|
23
23
|
startDateOptions.onChangeDate(startDate);
|
|
24
|
+
// 종료일 기준이 플랫폼마다 다르다: 고도몰=어제, shopby=오늘.
|
|
25
|
+
// useYesterdayAsEndDate=true 면 종료일을 어제로(고도몰), false 면 오늘로(shopby) 둔다.
|
|
26
|
+
// 종료일만 옮길 뿐 시작일을 종료일 기준으로 재계산하지는 않는다(abc16b3d).
|
|
24
27
|
const endDate = useYesterdayAsEndDate
|
|
25
28
|
? getDateFormat(getSubtractDate({
|
|
26
29
|
period: 1,
|
|
@@ -37,7 +40,12 @@ const RangeDatePickerWithButtons = ({ useYesterdayAsEndDate = false, size = 'xs'
|
|
|
37
40
|
if (currentButtonId !== 'NONE') {
|
|
38
41
|
setCalculatedDate();
|
|
39
42
|
}
|
|
43
|
+
// 선택된 기간 버튼(currentButtonId)이 바뀔 때만 날짜를 재계산한다. setCalculatedDate 는
|
|
44
|
+
// 매 렌더 새로 생성되므로 deps 에 넣으면 effect 가 매 렌더 실행된다 — 의도적으로 좁게 유지.
|
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
40
46
|
}, [currentButtonId]);
|
|
41
|
-
return (_jsxs("div", { className: "ncua-range-date-picker-with-buttons", children: [_jsx(ButtonGroup, { size: size, children: periodKeys
|
|
47
|
+
return (_jsxs("div", { className: "ncua-range-date-picker-with-buttons", children: [_jsx(ButtonGroup, { size: size, children: periodKeys
|
|
48
|
+
.filter((key) => items[key])
|
|
49
|
+
.map((key) => (_jsx(ButtonGroup.Item, { isCurrent: currentButtonId === key, label: items[key].text, onClick: () => setCurrentButtonId(key) }, key))) }), _jsx(RangeDatePicker, { startDateOptions: { ...startDateOptions, onChangeDate: (date) => handleOnChangeDate(date, 'START') }, endDateOptions: { ...endDateOptions, onChangeDate: (date) => handleOnChangeDate(date, 'END') }, validationOption: validationOption, onDateValidation: onDateValidation, size: size })] }));
|
|
42
50
|
};
|
|
43
51
|
export { RangeDatePickerWithButtons };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { createElement } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { act } from 'react-dom/test-utils';
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { RangeDatePickerWithButtons } from '../RangeDatePickerWithButtons';
|
|
7
|
+
const mounted = [];
|
|
8
|
+
function mount(props) {
|
|
9
|
+
const container = document.createElement('div');
|
|
10
|
+
document.body.appendChild(container);
|
|
11
|
+
const root = createRoot(container);
|
|
12
|
+
act(() => {
|
|
13
|
+
root.render(createElement(RangeDatePickerWithButtons, props));
|
|
14
|
+
});
|
|
15
|
+
mounted.push({ root, container });
|
|
16
|
+
return container;
|
|
17
|
+
}
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
for (const { root, container } of mounted.splice(0)) {
|
|
20
|
+
act(() => {
|
|
21
|
+
root.unmount();
|
|
22
|
+
});
|
|
23
|
+
container.remove();
|
|
24
|
+
}
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
});
|
|
27
|
+
describe('#6 periodItems 에 없는 periodKey 방어', () => {
|
|
28
|
+
const baseProps = {
|
|
29
|
+
currentButtonId: 'NONE',
|
|
30
|
+
setCurrentButtonId: () => undefined,
|
|
31
|
+
startDateOptions: { currentDate: '2024-03-01', onChangeDate: () => undefined },
|
|
32
|
+
endDateOptions: { currentDate: '2024-03-05', onChangeDate: () => undefined },
|
|
33
|
+
};
|
|
34
|
+
it('periodKeys 에 periodItems 에 없는 키가 있어도 크래시하지 않고 유효한 버튼만 렌더한다', () => {
|
|
35
|
+
const container = mount({
|
|
36
|
+
...baseProps,
|
|
37
|
+
periodKeys: ['TODAY', 'UNKNOWN_KEY'],
|
|
38
|
+
});
|
|
39
|
+
// 유효한 키(TODAY='오늘')는 렌더되고, 정의에 없는 키는 건너뛴다(크래시 없음)
|
|
40
|
+
expect(container.textContent).toContain('오늘');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -7,6 +7,7 @@ export interface SelectProps extends Omit<ComponentPropsWithRef<'select'>, 'size
|
|
|
7
7
|
disabledPlaceholder?: boolean;
|
|
8
8
|
hintText?: string;
|
|
9
9
|
destructive?: boolean;
|
|
10
|
+
readOnly?: boolean;
|
|
10
11
|
size?: Extract<Size, 'xs' | 'sm' | 'md'>;
|
|
11
12
|
optionItems?: OptionType[];
|
|
12
13
|
register?: UseFormRegisterReturn;
|
|
@@ -2,9 +2,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import classNames from 'classnames';
|
|
3
3
|
import { forwardRef } from 'react';
|
|
4
4
|
import { HintText } from '../../shared/hintText/HintText';
|
|
5
|
-
export const Select = forwardRef(({ placeholder, disabledPlaceholder = false, id, className, hintText, children, size = 'xs', type = 'default', destructive = false, value, optionItems, register, disabled = false, ...props }, ref) => {
|
|
5
|
+
export const Select = forwardRef(({ placeholder, disabledPlaceholder = false, id, className, hintText, children, size = 'xs', type = 'default', destructive = false, value, optionItems, register, disabled = false, readOnly = false, ...props }, ref) => {
|
|
6
6
|
return (_jsxs("span", { className: classNames('ncua-select', {
|
|
7
7
|
destructive: destructive,
|
|
8
8
|
'ncua-select--simple': type === 'simple',
|
|
9
|
-
|
|
9
|
+
'ncua-select--readonly': readOnly,
|
|
10
|
+
}, className, `ncua-select--${size}`), children: [_jsx("span", { className: "ncua-select__content", children: _jsxs("select", { value: value, ref: ref, id: id, className: classNames('ncua-select__tag', className), disabled: disabled, "aria-readonly": readOnly, ...register, ...props, onKeyDown: (e) => {
|
|
11
|
+
props.onKeyDown?.(e);
|
|
12
|
+
// native select에는 readonly 속성이 없어 키보드로 값이 바뀐다.
|
|
13
|
+
// 값 변경 키는 막고, Tab/Shift+Tab(포커스 이동)은 허용한다.
|
|
14
|
+
if (readOnly && e.key !== 'Tab') {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
}
|
|
17
|
+
}, children: [placeholder && (_jsx("option", { value: "", disabled: disabledPlaceholder, children: placeholder })), optionItems?.map((item) => (_jsx("option", { value: item.id, children: item.label }, item.id))), children] }) }), hintText && (_jsx(HintText, { destructive: destructive, className: "ncua-hint-text", children: hintText }))] }));
|
|
10
18
|
});
|
|
@@ -16,6 +16,7 @@ type SelectBoxProps = Omit<ComponentPropsWithRef<'div'>, 'size' | 'onChange'> &
|
|
|
16
16
|
value?: OptionValue;
|
|
17
17
|
onChange?: OptionChangeHandler;
|
|
18
18
|
disabled?: boolean;
|
|
19
|
+
readOnly?: boolean;
|
|
19
20
|
register?: UseFormRegisterReturn;
|
|
20
21
|
maxHeight?: number;
|
|
21
22
|
multiple?: boolean;
|
|
@@ -40,7 +40,7 @@ function DisplayValue({ displayValue }) {
|
|
|
40
40
|
}
|
|
41
41
|
return (_jsxs("div", { className: "ncua-selectbox__value-container", children: [displayValue.icon && (_jsx("span", { className: "ncua-selectbox__value-icon", children: _jsx(displayValue.icon, { width: 16, height: 16 }) })), _jsx("span", { className: "ncua-selectbox__value-text", children: displayValue.label })] }));
|
|
42
42
|
}
|
|
43
|
-
const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceholder = false, hintText, size = 'xs', type = 'default', autoWidth = true, destructive = false, value, optionItems = [], disabled = false, maxHeight = DEFAULT_MAX_HEIGHT, multiple = false, maxSelection, align = 'left', usePortal, id, className, children, register, onChange, onEdit, onComplete, ...props }, ref
|
|
43
|
+
const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceholder = false, hintText, size = 'xs', type = 'default', autoWidth = true, destructive = false, value, optionItems = [], disabled = false, readOnly = false, maxHeight = DEFAULT_MAX_HEIGHT, multiple = false, maxSelection, align = 'left', usePortal, id, className, children, register, onChange, onEdit, onComplete, ...props }, ref
|
|
44
44
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: 옵션/멀티/태그/포탈 등 필수 분기 통합
|
|
45
45
|
) => {
|
|
46
46
|
const internalRef = useRef(null);
|
|
@@ -59,7 +59,7 @@ const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceho
|
|
|
59
59
|
return selectedOption ? selectedOption : placeholder;
|
|
60
60
|
}, [multiple, selectedOption, placeholder]);
|
|
61
61
|
const handleOptionSelect = (option) => {
|
|
62
|
-
if (disabled)
|
|
62
|
+
if (disabled || readOnly)
|
|
63
63
|
return;
|
|
64
64
|
if (multiple) {
|
|
65
65
|
const newValue = tryToggle(option.id, Array.isArray(value) ? value : []);
|
|
@@ -79,7 +79,7 @@ const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceho
|
|
|
79
79
|
maxHeight,
|
|
80
80
|
itemCount: optionItems.length,
|
|
81
81
|
optionItems,
|
|
82
|
-
disabled,
|
|
82
|
+
disabled: disabled || readOnly,
|
|
83
83
|
multiple,
|
|
84
84
|
onSelect: handleOptionSelect,
|
|
85
85
|
});
|
|
@@ -147,10 +147,11 @@ const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceho
|
|
|
147
147
|
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: internalRef, className: classNames('ncua-selectbox', `ncua-selectbox--${size}`, {
|
|
148
148
|
'ncua-selectbox--open': isOpen,
|
|
149
149
|
'ncua-selectbox--disabled': disabled,
|
|
150
|
+
'ncua-selectbox--readonly': readOnly,
|
|
150
151
|
'ncua-selectbox--simple': type === 'simple',
|
|
151
152
|
'ncua-selectbox--multiple': multiple,
|
|
152
153
|
destructive: destructive,
|
|
153
|
-
}, className), ...props, children: [_jsxs("div", { className: classNames('ncua-selectbox__content'), onClick: toggleDropdown, onKeyDown: handleKeyDown, tabIndex: disabled ? -1 : 0, role: "combobox", "aria-expanded": isOpen, "aria-haspopup": "listbox", "aria-controls": `selectbox-options-${id || 'default'}`, "aria-disabled": disabled, "aria-label": selectedOption ? selectedOption.label : placeholder, "aria-activedescendant": activeDescendantId, children: [_jsxs("div", { className: "ncua-selectbox__content-inner", children: [_jsx("div", { className: "ncua-selectbox__value", children: _jsx(DisplayValue, { displayValue: displayValue }) }), _jsx(ChevronDown, { width: size === 'xs' ? ICON_SIZE_XS : ICON_SIZE_SM, height: size === 'xs' ? ICON_SIZE_XS : ICON_SIZE_SM, color: disabled ? COLOR.gray300 : COLOR.gray500, className: classNames('ncua-selectbox__arrow', {
|
|
154
|
+
}, className), ...props, children: [_jsxs("div", { className: classNames('ncua-selectbox__content'), onClick: toggleDropdown, onKeyDown: handleKeyDown, tabIndex: disabled ? -1 : 0, role: "combobox", "aria-expanded": isOpen, "aria-haspopup": "listbox", "aria-controls": `selectbox-options-${id || 'default'}`, "aria-disabled": disabled, "aria-readonly": readOnly, "aria-label": selectedOption ? selectedOption.label : placeholder, "aria-activedescendant": activeDescendantId, children: [_jsxs("div", { className: "ncua-selectbox__content-inner", children: [_jsx("div", { className: "ncua-selectbox__value", children: _jsx(DisplayValue, { displayValue: displayValue }) }), _jsx(ChevronDown, { width: size === 'xs' ? ICON_SIZE_XS : ICON_SIZE_SM, height: size === 'xs' ? ICON_SIZE_XS : ICON_SIZE_SM, color: disabled ? COLOR.gray300 : COLOR.gray500, className: classNames('ncua-selectbox__arrow', {
|
|
154
155
|
'ncua-selectbox__arrow--up': isOpen,
|
|
155
156
|
}) })] }), !shouldPortal && selectDropdownNode] }), hintText && (_jsx(HintText, { destructive: destructive, className: "ncua-hint-text", children: hintText })), register && (_jsx("input", { type: "hidden", ...register, value: multiple ? JSON.stringify(value || []) : String(value || '') }))] }), selectedTags.length > 0 && (_jsx("div", { className: "ncua-selectbox__tags", children: selectedTags.map((tag) => (_jsx(Tag, { text: tag.label, size: "sm", close: true, onButtonClick: () => handleRemoveTag(tag.id) }, tag.id))) })), portaledDropdown] }));
|
|
156
157
|
});
|
|
@@ -4,6 +4,9 @@ import { Slider as NcuaSlider } from '../../../../assets/scripts/slider';
|
|
|
4
4
|
export const Slider = ({ min = 0, max = 100, step = 1, value = 0, onChange, labelPosition, disabled = false, className, }) => {
|
|
5
5
|
const containerRef = useRef(null);
|
|
6
6
|
const sliderInstanceRef = useRef(null);
|
|
7
|
+
// value 의 "타입"이 바뀔 때만(범위↔단일 등) 슬라이더를 재생성한다. valueType 으로 추출해
|
|
8
|
+
// 정적으로 드러낸다 — 96cf7b3a 에서 deps 를 [] → [typeof value, ...] 로 좁힌 의도.
|
|
9
|
+
const valueType = typeof value;
|
|
7
10
|
useEffect(() => {
|
|
8
11
|
if (!containerRef.current)
|
|
9
12
|
return;
|
|
@@ -22,7 +25,10 @@ export const Slider = ({ min = 0, max = 100, step = 1, value = 0, onChange, labe
|
|
|
22
25
|
sliderInstanceRef.current = null;
|
|
23
26
|
}
|
|
24
27
|
};
|
|
25
|
-
|
|
28
|
+
// value 변경은 아래 setValue effect, disabled 변경은 enable/disable effect 가 처리하므로
|
|
29
|
+
// 여기 deps 에 넣지 않는다(넣으면 매 렌더 인스턴스 재생성 → 상태 손실).
|
|
30
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
31
|
+
}, [valueType, onChange, min, max, step, labelPosition]);
|
|
26
32
|
useEffect(() => {
|
|
27
33
|
if (sliderInstanceRef.current) {
|
|
28
34
|
sliderInstanceRef.current.setValue(value);
|
|
@@ -16,6 +16,13 @@ export const Pagination = ({ className = '', totalCount, itemCountPerPage = 10,
|
|
|
16
16
|
}
|
|
17
17
|
};
|
|
18
18
|
useEffect(() => {
|
|
19
|
+
const lastGroupStart = Math.max(1, Math.floor((totalPage - 1) / pageCount) * pageCount + 1);
|
|
20
|
+
// totalPage/pageCount 가 줄어 start 가 마지막 그룹을 넘으면 마지막 그룹으로 보정한다.
|
|
21
|
+
// (예: 100→50 페이지로 축소 시 start 가 91 에 머물러 범위 밖 버튼을 표시하던 문제)
|
|
22
|
+
if (start > lastGroupStart) {
|
|
23
|
+
setStart(lastGroupStart);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
19
26
|
if (currentPage === start + pageCount) {
|
|
20
27
|
setStart(start + pageCount);
|
|
21
28
|
}
|
|
@@ -30,10 +37,13 @@ export const Pagination = ({ className = '', totalCount, itemCountPerPage = 10,
|
|
|
30
37
|
setStart(1);
|
|
31
38
|
}
|
|
32
39
|
if (currentPage === totalPage) {
|
|
33
|
-
const lastGroupStart = Math.floor((totalPage - 1) / pageCount) * pageCount + 1;
|
|
34
40
|
setStart(lastGroupStart);
|
|
35
41
|
}
|
|
36
|
-
|
|
42
|
+
// start 는 이 effect 가 setStart 로 갱신하는 대상이라 deps 에서 제외한다(넣으면 자기 갱신으로
|
|
43
|
+
// 재실행). currentPage 외에 totalPage/pageCount 도 추적해, 이들이 줄어 start 가 범위를 벗어나면
|
|
44
|
+
// 위에서 보정한다.
|
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
+
}, [currentPage, totalPage, pageCount]);
|
|
37
47
|
return (_jsxs("div", { className: classNames('ncua-pagination', `ncua-pagination--${breakPoint}`, className), children: [showJumpPageButton && (_jsxs(_Fragment, { children: [NavButton({
|
|
38
48
|
type: 'first',
|
|
39
49
|
breakPoint,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|