@ncds/ui-admin 1.8.14 → 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/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/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/esm/src/components/forms-and-input/checkbox/CheckboxInput.js +4 -3
- 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/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/temp/src/components/forms-and-input/checkbox/CheckboxInput.js +4 -3
- 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/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/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/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/ui-admin/assets/styles/style.css +15 -19
- package/package.json +1 -1
package/dist/esm/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
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(/*#__PURE__*/createElement(RangeDatePicker, props));
|
|
14
|
+
});
|
|
15
|
+
mounted.push({
|
|
16
|
+
root,
|
|
17
|
+
container
|
|
18
|
+
});
|
|
19
|
+
return container;
|
|
20
|
+
}
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
for (const {
|
|
23
|
+
root,
|
|
24
|
+
container
|
|
25
|
+
} of mounted.splice(0)) {
|
|
26
|
+
act(() => {
|
|
27
|
+
root.unmount();
|
|
28
|
+
});
|
|
29
|
+
container.remove();
|
|
30
|
+
}
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
});
|
|
33
|
+
describe('#1 빈/무효 날짜 검증 가드', () => {
|
|
34
|
+
const validationOption = {
|
|
35
|
+
setting: {
|
|
36
|
+
unit: 'days',
|
|
37
|
+
period: 7
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
it('양쪽 날짜가 비어 있으면 onDateValidation 을 호출하지 않는다', () => {
|
|
41
|
+
const onDateValidation = vi.fn();
|
|
42
|
+
mount({
|
|
43
|
+
startDateOptions: {
|
|
44
|
+
currentDate: '',
|
|
45
|
+
onChangeDate: () => undefined
|
|
46
|
+
},
|
|
47
|
+
endDateOptions: {
|
|
48
|
+
currentDate: '',
|
|
49
|
+
onChangeDate: () => undefined
|
|
50
|
+
},
|
|
51
|
+
validationOption,
|
|
52
|
+
onDateValidation
|
|
53
|
+
});
|
|
54
|
+
expect(onDateValidation).not.toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
it('한쪽(종료일)만 비어 있어도 "Invalid date" 를 newDate 로 전달하지 않는다', () => {
|
|
57
|
+
const onDateValidation = vi.fn();
|
|
58
|
+
mount({
|
|
59
|
+
startDateOptions: {
|
|
60
|
+
currentDate: '2024-03-01',
|
|
61
|
+
onChangeDate: () => undefined
|
|
62
|
+
},
|
|
63
|
+
endDateOptions: {
|
|
64
|
+
currentDate: '',
|
|
65
|
+
onChangeDate: () => undefined
|
|
66
|
+
},
|
|
67
|
+
validationOption,
|
|
68
|
+
onDateValidation
|
|
69
|
+
});
|
|
70
|
+
const leakedInvalidDate = onDateValidation.mock.calls.some(_ref => {
|
|
71
|
+
let [arg] = _ref;
|
|
72
|
+
return arg?.newDate === 'Invalid date';
|
|
73
|
+
});
|
|
74
|
+
expect(leakedInvalidDate).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
it('양쪽 날짜가 유효하면 검증이 정상 동작한다(가드가 정상 입력을 막지 않음)', () => {
|
|
77
|
+
const onDateValidation = vi.fn();
|
|
78
|
+
mount({
|
|
79
|
+
startDateOptions: {
|
|
80
|
+
currentDate: '2024-03-01',
|
|
81
|
+
onChangeDate: () => undefined
|
|
82
|
+
},
|
|
83
|
+
endDateOptions: {
|
|
84
|
+
currentDate: '2024-03-05',
|
|
85
|
+
onChangeDate: () => undefined
|
|
86
|
+
},
|
|
87
|
+
validationOption,
|
|
88
|
+
onDateValidation
|
|
89
|
+
});
|
|
90
|
+
expect(onDateValidation).toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -33,6 +33,9 @@ const RangeDatePickerWithButtons = _ref => {
|
|
|
33
33
|
unit: currentPeriodItem.unit
|
|
34
34
|
}));
|
|
35
35
|
startDateOptions.onChangeDate(startDate);
|
|
36
|
+
// 종료일 기준이 플랫폼마다 다르다: 고도몰=어제, shopby=오늘.
|
|
37
|
+
// useYesterdayAsEndDate=true 면 종료일을 어제로(고도몰), false 면 오늘로(shopby) 둔다.
|
|
38
|
+
// 종료일만 옮길 뿐 시작일을 종료일 기준으로 재계산하지는 않는다(abc16b3d).
|
|
36
39
|
const endDate = useYesterdayAsEndDate ? getDateFormat(getSubtractDate({
|
|
37
40
|
period: 1,
|
|
38
41
|
unit: 'days'
|
|
@@ -47,12 +50,15 @@ const RangeDatePickerWithButtons = _ref => {
|
|
|
47
50
|
if (currentButtonId !== 'NONE') {
|
|
48
51
|
setCalculatedDate();
|
|
49
52
|
}
|
|
53
|
+
// 선택된 기간 버튼(currentButtonId)이 바뀔 때만 날짜를 재계산한다. setCalculatedDate 는
|
|
54
|
+
// 매 렌더 새로 생성되므로 deps 에 넣으면 effect 가 매 렌더 실행된다 — 의도적으로 좁게 유지.
|
|
55
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
50
56
|
}, [currentButtonId]);
|
|
51
57
|
return _jsxs("div", {
|
|
52
58
|
className: "ncua-range-date-picker-with-buttons",
|
|
53
59
|
children: [_jsx(ButtonGroup, {
|
|
54
60
|
size: size,
|
|
55
|
-
children: periodKeys.map(key => _jsx(ButtonGroup.Item, {
|
|
61
|
+
children: periodKeys.filter(key => items[key]).map(key => _jsx(ButtonGroup.Item, {
|
|
56
62
|
isCurrent: currentButtonId === key,
|
|
57
63
|
label: items[key].text,
|
|
58
64
|
onClick: () => setCurrentButtonId(key)
|
|
@@ -0,0 +1,54 @@
|
|
|
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(/*#__PURE__*/createElement(RangeDatePickerWithButtons, props));
|
|
14
|
+
});
|
|
15
|
+
mounted.push({
|
|
16
|
+
root,
|
|
17
|
+
container
|
|
18
|
+
});
|
|
19
|
+
return container;
|
|
20
|
+
}
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
for (const {
|
|
23
|
+
root,
|
|
24
|
+
container
|
|
25
|
+
} of mounted.splice(0)) {
|
|
26
|
+
act(() => {
|
|
27
|
+
root.unmount();
|
|
28
|
+
});
|
|
29
|
+
container.remove();
|
|
30
|
+
}
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
});
|
|
33
|
+
describe('#6 periodItems 에 없는 periodKey 방어', () => {
|
|
34
|
+
const baseProps = {
|
|
35
|
+
currentButtonId: 'NONE',
|
|
36
|
+
setCurrentButtonId: () => undefined,
|
|
37
|
+
startDateOptions: {
|
|
38
|
+
currentDate: '2024-03-01',
|
|
39
|
+
onChangeDate: () => undefined
|
|
40
|
+
},
|
|
41
|
+
endDateOptions: {
|
|
42
|
+
currentDate: '2024-03-05',
|
|
43
|
+
onChangeDate: () => undefined
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
it('periodKeys 에 periodItems 에 없는 키가 있어도 크래시하지 않고 유효한 버튼만 렌더한다', () => {
|
|
47
|
+
const container = mount({
|
|
48
|
+
...baseProps,
|
|
49
|
+
periodKeys: ['TODAY', 'UNKNOWN_KEY']
|
|
50
|
+
});
|
|
51
|
+
// 유효한 키(TODAY='오늘')는 렌더되고, 정의에 없는 키는 건너뛴다(크래시 없음)
|
|
52
|
+
expect(container.textContent).toContain('오늘');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -14,6 +14,9 @@ export const Slider = _ref => {
|
|
|
14
14
|
} = _ref;
|
|
15
15
|
const containerRef = useRef(null);
|
|
16
16
|
const sliderInstanceRef = useRef(null);
|
|
17
|
+
// value 의 "타입"이 바뀔 때만(범위↔단일 등) 슬라이더를 재생성한다. valueType 으로 추출해
|
|
18
|
+
// 정적으로 드러낸다 — 96cf7b3a 에서 deps 를 [] → [typeof value, ...] 로 좁힌 의도.
|
|
19
|
+
const valueType = typeof value;
|
|
17
20
|
useEffect(() => {
|
|
18
21
|
if (!containerRef.current) return;
|
|
19
22
|
sliderInstanceRef.current = new NcuaSlider(containerRef.current, {
|
|
@@ -31,7 +34,10 @@ export const Slider = _ref => {
|
|
|
31
34
|
sliderInstanceRef.current = null;
|
|
32
35
|
}
|
|
33
36
|
};
|
|
34
|
-
|
|
37
|
+
// value 변경은 아래 setValue effect, disabled 변경은 enable/disable effect 가 처리하므로
|
|
38
|
+
// 여기 deps 에 넣지 않는다(넣으면 매 렌더 인스턴스 재생성 → 상태 손실).
|
|
39
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
40
|
+
}, [valueType, onChange, min, max, step, labelPosition]);
|
|
35
41
|
useEffect(() => {
|
|
36
42
|
if (sliderInstanceRef.current) {
|
|
37
43
|
sliderInstanceRef.current.setValue(value);
|
|
@@ -26,6 +26,13 @@ export const Pagination = _ref => {
|
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
28
|
useEffect(() => {
|
|
29
|
+
const lastGroupStart = Math.max(1, Math.floor((totalPage - 1) / pageCount) * pageCount + 1);
|
|
30
|
+
// totalPage/pageCount 가 줄어 start 가 마지막 그룹을 넘으면 마지막 그룹으로 보정한다.
|
|
31
|
+
// (예: 100→50 페이지로 축소 시 start 가 91 에 머물러 범위 밖 버튼을 표시하던 문제)
|
|
32
|
+
if (start > lastGroupStart) {
|
|
33
|
+
setStart(lastGroupStart);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
29
36
|
if (currentPage === start + pageCount) {
|
|
30
37
|
setStart(start + pageCount);
|
|
31
38
|
}
|
|
@@ -40,10 +47,13 @@ export const Pagination = _ref => {
|
|
|
40
47
|
setStart(1);
|
|
41
48
|
}
|
|
42
49
|
if (currentPage === totalPage) {
|
|
43
|
-
const lastGroupStart = Math.floor((totalPage - 1) / pageCount) * pageCount + 1;
|
|
44
50
|
setStart(lastGroupStart);
|
|
45
51
|
}
|
|
46
|
-
|
|
52
|
+
// start 는 이 effect 가 setStart 로 갱신하는 대상이라 deps 에서 제외한다(넣으면 자기 갱신으로
|
|
53
|
+
// 재실행). currentPage 외에 totalPage/pageCount 도 추적해, 이들이 줄어 start 가 범위를 벗어나면
|
|
54
|
+
// 위에서 보정한다.
|
|
55
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
56
|
+
}, [currentPage, totalPage, pageCount]);
|
|
47
57
|
return _jsxs("div", {
|
|
48
58
|
className: classNames('ncua-pagination', `ncua-pagination--${breakPoint}`, className),
|
|
49
59
|
children: [showJumpPageButton && _jsxs(_Fragment, {
|
|
@@ -0,0 +1,59 @@
|
|
|
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 } from 'vitest';
|
|
6
|
+
import { Pagination } from '../Pagination';
|
|
7
|
+
const ITEM_COUNT_PER_PAGE = 10;
|
|
8
|
+
const PAGE_GROUP_SIZE = 10;
|
|
9
|
+
const TOTAL_COUNT_100_PAGES = 1000;
|
|
10
|
+
const TOTAL_COUNT_50_PAGES = 500;
|
|
11
|
+
const CURRENT_PAGE = 91; // 100페이지 기준 마지막 그룹(91~100)을 보던 페이지
|
|
12
|
+
const GROUP_START_100_PAGES = 91; // currentPage=91 일 때의 그룹 시작
|
|
13
|
+
const GROUP_START_50_PAGES = 41; // totalPage=50 으로 줄었을 때의 마지막 그룹 시작
|
|
14
|
+
const LAST_PAGE_50 = 50;
|
|
15
|
+
let root = null;
|
|
16
|
+
let container = null;
|
|
17
|
+
function render(props) {
|
|
18
|
+
if (!container) {
|
|
19
|
+
container = document.createElement('div');
|
|
20
|
+
document.body.appendChild(container);
|
|
21
|
+
root = createRoot(container);
|
|
22
|
+
}
|
|
23
|
+
act(() => {
|
|
24
|
+
root?.render(/*#__PURE__*/createElement(Pagination, props));
|
|
25
|
+
});
|
|
26
|
+
return container;
|
|
27
|
+
}
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
if (root && container) {
|
|
30
|
+
act(() => root?.unmount());
|
|
31
|
+
container.remove();
|
|
32
|
+
}
|
|
33
|
+
root = null;
|
|
34
|
+
container = null;
|
|
35
|
+
});
|
|
36
|
+
const pageNums = el => Array.from(el.querySelectorAll('.ncua-pagination__page-num')).map(b => Number(b.textContent));
|
|
37
|
+
describe('#332 Pagination — totalPage 축소 시 start 보정', () => {
|
|
38
|
+
it('totalPage 가 줄면 start 를 마지막 그룹으로 보정하고 범위 밖 페이지 버튼을 표시하지 않는다', () => {
|
|
39
|
+
// 1) 100 페이지에서 91 페이지 보는 중 → 91~100 그룹 표시
|
|
40
|
+
const el = render({
|
|
41
|
+
totalCount: TOTAL_COUNT_100_PAGES,
|
|
42
|
+
itemCountPerPage: ITEM_COUNT_PER_PAGE,
|
|
43
|
+
pageCount: PAGE_GROUP_SIZE,
|
|
44
|
+
currentPage: CURRENT_PAGE
|
|
45
|
+
});
|
|
46
|
+
expect(pageNums(el)[0]).toBe(GROUP_START_100_PAGES);
|
|
47
|
+
// 2) 데이터가 50 페이지로 줄어듦 (currentPage 는 그대로 — 부모가 clamp 안 한 상황)
|
|
48
|
+
render({
|
|
49
|
+
totalCount: TOTAL_COUNT_50_PAGES,
|
|
50
|
+
itemCountPerPage: ITEM_COUNT_PER_PAGE,
|
|
51
|
+
pageCount: PAGE_GROUP_SIZE,
|
|
52
|
+
currentPage: CURRENT_PAGE
|
|
53
|
+
});
|
|
54
|
+
const nums = pageNums(el);
|
|
55
|
+
// 마지막 그룹(41~50)으로 보정되고, totalPage(50)를 넘는 버튼은 없어야 한다
|
|
56
|
+
expect(nums[0]).toBe(GROUP_START_50_PAGES);
|
|
57
|
+
expect(Math.max(...nums)).toBeLessThanOrEqual(LAST_PAGE_50);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -4,6 +4,7 @@ import classNames from 'classnames';
|
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
6
|
import { ButtonCloseX } from '../../action/button/ButtonCloseX';
|
|
7
|
+
import { calculateArrowOffsetX } from './utils';
|
|
7
8
|
const ICON_SIZE_SM = 14;
|
|
8
9
|
const ICON_SIZE_DEFAULT = 16;
|
|
9
10
|
// HTML 구현체(assets/scripts/tooltip)와 동일한 상수
|
|
@@ -64,9 +65,16 @@ const computePanelCoords = (prefer, anchor, panel) => {
|
|
|
64
65
|
}
|
|
65
66
|
left = Math.max(VIEWPORT_MARGIN, Math.min(left, window.innerWidth - panel.width - VIEWPORT_MARGIN));
|
|
66
67
|
top = Math.max(VIEWPORT_MARGIN, Math.min(top, window.innerHeight - panel.height - VIEWPORT_MARGIN));
|
|
68
|
+
// 경계 보정으로 패널이 앵커 중심에서 밀렸을 때도 화살표(::after)가 앵커를 가리키도록
|
|
69
|
+
// 패널 내 X 오프셋 계산 (top/bottom 계열만 — SCSS의 --top/--bottom 규칙이 소비)
|
|
70
|
+
let arrowX = null;
|
|
71
|
+
if (calculatedPosition === 'top' || calculatedPosition === 'bottom') {
|
|
72
|
+
arrowX = calculateArrowOffsetX(anchor.left + anchor.width / 2, left, panel.width);
|
|
73
|
+
}
|
|
67
74
|
return {
|
|
68
75
|
top: Math.round(top),
|
|
69
76
|
left: Math.round(left),
|
|
77
|
+
arrowX,
|
|
70
78
|
calculatedPosition
|
|
71
79
|
};
|
|
72
80
|
};
|
|
@@ -79,6 +87,9 @@ const buildPanelStyle = (disablePortal, coords, visible, zIndex) => {
|
|
|
79
87
|
top: `${coords.top}px`,
|
|
80
88
|
left: `${coords.left}px`,
|
|
81
89
|
opacity,
|
|
90
|
+
...(coords.arrowX !== null && {
|
|
91
|
+
'--ncua-tooltip-arrow-x': `${coords.arrowX}px`
|
|
92
|
+
}),
|
|
82
93
|
...(zIndex && {
|
|
83
94
|
zIndex
|
|
84
95
|
})
|
|
@@ -131,7 +142,8 @@ export const Tooltip = _ref => {
|
|
|
131
142
|
const [mounted, setMounted] = useState(false);
|
|
132
143
|
const [coords, setCoords] = useState({
|
|
133
144
|
top: OFFSCREEN,
|
|
134
|
-
left: OFFSCREEN
|
|
145
|
+
left: OFFSCREEN,
|
|
146
|
+
arrowX: null
|
|
135
147
|
});
|
|
136
148
|
const [calculatedPosition, setCalculatedPosition] = useState(position === 'auto' ? 'bottom' : position);
|
|
137
149
|
const [isVisible, setIsVisible] = useState(false);
|
|
@@ -153,7 +165,8 @@ export const Tooltip = _ref => {
|
|
|
153
165
|
const next = computePanelCoords(position, anchor, panel);
|
|
154
166
|
setCoords({
|
|
155
167
|
top: next.top,
|
|
156
|
-
left: next.left
|
|
168
|
+
left: next.left,
|
|
169
|
+
arrowX: next.arrowX
|
|
157
170
|
});
|
|
158
171
|
setCalculatedPosition(next.calculatedPosition);
|
|
159
172
|
}, [position, disablePortal]);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ARROW_EDGE_MARGIN, calculateArrowOffsetX } from '../utils';
|
|
3
|
+
// 공통 픽스처 — 너비 300px 패널이 viewport 왼쪽 여백(8px)에 클램핑된 상태를 가정
|
|
4
|
+
const PANEL_WIDTH = 300;
|
|
5
|
+
const CLAMPED_LEFT = 8;
|
|
6
|
+
describe('calculateArrowOffsetX — 화살표가 앵커 중심을 가리키도록 패널 내 X 오프셋 계산', () => {
|
|
7
|
+
it('패널이 클램핑되지 않으면(앵커 중심 = 패널 중심) 패널 너비의 절반을 반환한다', () => {
|
|
8
|
+
const anchorCenterX = 200;
|
|
9
|
+
const centeredPanelLeft = 50;
|
|
10
|
+
const panelHalfWidth = 150;
|
|
11
|
+
expect(calculateArrowOffsetX(anchorCenterX, centeredPanelLeft, PANEL_WIDTH)).toBe(panelHalfWidth);
|
|
12
|
+
});
|
|
13
|
+
it('패널이 오른쪽으로 밀린 경우(왼쪽 경계 클램핑) 화살표는 왼쪽으로 이동해 앵커를 가리킨다', () => {
|
|
14
|
+
const anchorCenterX = 60;
|
|
15
|
+
const expectedOffset = 52; // 60 - 8
|
|
16
|
+
expect(calculateArrowOffsetX(anchorCenterX, CLAMPED_LEFT, PANEL_WIDTH)).toBe(expectedOffset);
|
|
17
|
+
});
|
|
18
|
+
it('패널이 왼쪽으로 밀린 경우(오른쪽 경계 클램핑) 화살표는 오른쪽으로 이동해 앵커를 가리킨다', () => {
|
|
19
|
+
const anchorCenterX = 950;
|
|
20
|
+
const clampedRightPanelLeft = 692;
|
|
21
|
+
const expectedOffset = 258; // 950 - 692
|
|
22
|
+
expect(calculateArrowOffsetX(anchorCenterX, clampedRightPanelLeft, PANEL_WIDTH)).toBe(expectedOffset);
|
|
23
|
+
});
|
|
24
|
+
it('앵커가 패널 왼쪽 모서리보다 바깥이면 최소 여백(모서리 radius + 화살표)으로 클램핑한다', () => {
|
|
25
|
+
const outsideLeftAnchorX = 10;
|
|
26
|
+
expect(calculateArrowOffsetX(outsideLeftAnchorX, CLAMPED_LEFT, PANEL_WIDTH)).toBe(ARROW_EDGE_MARGIN);
|
|
27
|
+
});
|
|
28
|
+
it('앵커가 패널 오른쪽 모서리보다 바깥이면 최대 여백으로 클램핑한다', () => {
|
|
29
|
+
const outsideRightAnchorX = 999;
|
|
30
|
+
expect(calculateArrowOffsetX(outsideRightAnchorX, CLAMPED_LEFT, PANEL_WIDTH)).toBe(PANEL_WIDTH - ARROW_EDGE_MARGIN);
|
|
31
|
+
});
|
|
32
|
+
it('소수 좌표는 반올림한다', () => {
|
|
33
|
+
const fractionalAnchorX = 100.4;
|
|
34
|
+
const panelLeft = 50;
|
|
35
|
+
const expectedRounded = 50; // 100.4 - 50 = 50.4 → 50
|
|
36
|
+
expect(calculateArrowOffsetX(fractionalAnchorX, panelLeft, PANEL_WIDTH)).toBe(expectedRounded);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// 화살표가 패널 모서리(border-radius 6px + 화살표 반너비 6px) 안쪽으로 파고들지 않기 위한 최소 여백
|
|
2
|
+
export const ARROW_EDGE_MARGIN = 12;
|
|
3
|
+
/**
|
|
4
|
+
* top/bottom 툴팁 화살표(::after)의 패널 내 X 오프셋.
|
|
5
|
+
* 패널이 viewport 경계 보정으로 앵커 중심에서 밀려도 화살표는 앵커를 가리키도록,
|
|
6
|
+
* 클램핑된 패널 left 기준으로 앵커 중심까지의 거리를 계산한다.
|
|
7
|
+
* 앵커가 패널 밖에 있으면 패널 모서리 여백까지만 이동한다.
|
|
8
|
+
* React(Tooltip.tsx)와 HTML 구현체(assets/scripts/tooltip)가 공유한다.
|
|
9
|
+
*/
|
|
10
|
+
export function calculateArrowOffsetX(anchorCenterX, panelLeft, panelWidth) {
|
|
11
|
+
const raw = anchorCenterX - panelLeft;
|
|
12
|
+
return Math.round(Math.max(ARROW_EDGE_MARGIN, Math.min(raw, panelWidth - ARROW_EDGE_MARGIN)));
|
|
13
|
+
}
|
|
@@ -14,6 +14,9 @@ export const useMediaQuery = (query, options) => {
|
|
|
14
14
|
return () => {
|
|
15
15
|
mediaQuery.removeEventListener('change', handleChange);
|
|
16
16
|
};
|
|
17
|
+
// options 객체 전체가 아니라 실제 사용하는 onMatched 콜백에만 의존한다. options 를 통째로
|
|
18
|
+
// 넣으면 매 렌더 새 객체로 간주돼 불필요하게 재구독한다.
|
|
19
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
17
20
|
}, [query, options?.onMatched]);
|
|
18
21
|
return matches;
|
|
19
22
|
};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import classNames from 'classnames';
|
|
3
|
-
import { forwardRef,
|
|
3
|
+
import { forwardRef, useRef } from 'react';
|
|
4
|
+
import { useIsomorphicLayoutEffect } from '../../../hooks/useIsomorphicLayoutEffect';
|
|
4
5
|
export const CheckboxInput = forwardRef(({ size = 'xs', indeterminate = false, disabled, className, destructive = false, onChange, ...props }, ref) => {
|
|
5
6
|
const inputRef = useRef(null);
|
|
6
|
-
|
|
7
|
+
useIsomorphicLayoutEffect(() => {
|
|
7
8
|
const input = ref && typeof ref !== 'function' ? ref.current : inputRef.current;
|
|
8
9
|
if (input) {
|
|
9
|
-
input.indeterminate = indeterminate;
|
|
10
|
+
input.indeterminate = indeterminate;
|
|
10
11
|
}
|
|
11
12
|
}, [indeterminate, ref]);
|
|
12
13
|
return (_jsx("span", { className: classNames('ncua-checkbox-input', `ncua-checkbox-input--${size}`, { 'has-indeterminate': indeterminate }, { destructive }, className), children: _jsx("input", { type: "checkbox", ref: (node) => {
|
|
@@ -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
|
+
});
|
|
@@ -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);
|