@uniai-fe/uds-primitives 0.9.3 → 0.10.1
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/package.json +9 -9
- package/src/components/calendar/markup/Core.tsx +22 -12
- package/src/components/carousel/hooks/useContext.ts +3 -12
- package/src/components/carousel/markup/Track.tsx +7 -9
- package/src/components/drawer/markup/Drawer.tsx +59 -22
- package/src/components/dropdown/markup/Template.tsx +20 -20
- package/src/components/input/hooks/useDigitField.ts +7 -10
- package/src/components/input/hooks/useInputFileContext.ts +22 -5
- package/src/components/input/markup/date/Trigger.tsx +2 -2
- package/src/components/radio/markup/RadioCardGroup.tsx +2 -2
- package/src/components/segmented-control/markup/List.tsx +1 -1
- package/src/components/segmented-control/types/index.ts +2 -2
- package/src/components/select/hooks/interaction.ts +3 -10
- package/src/components/select/markup/Default.tsx +109 -33
- package/src/components/select/markup/multiple/Multiple.tsx +25 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniai-fe/uds-primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "UNIAI Design System; Primitives Components Package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"@radix-ui/react-checkbox": "^1.3.3",
|
|
67
67
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
68
68
|
"@radix-ui/react-popover": "^1.1.15",
|
|
69
|
-
"@radix-ui/react-radio-group": "^1.
|
|
69
|
+
"@radix-ui/react-radio-group": "^1.4.1",
|
|
70
70
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
71
71
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
72
72
|
"@radix-ui/react-visually-hidden": "^1.2.4",
|
|
@@ -79,19 +79,19 @@
|
|
|
79
79
|
"@mantine/dates": "^8.3.18",
|
|
80
80
|
"@mantine/hooks": "^8.3.18",
|
|
81
81
|
"@svgr/webpack": "^8.1.0",
|
|
82
|
-
"@types/node": "^24.
|
|
82
|
+
"@types/node": "^24.13.2",
|
|
83
83
|
"@types/react": "^19.2.17",
|
|
84
84
|
"@types/react-dom": "^19.2.3",
|
|
85
|
-
"eslint": "^9.39.
|
|
86
|
-
"prettier": "^3.8.
|
|
87
|
-
"react-hook-form": "^7.
|
|
85
|
+
"eslint": "^9.39.4",
|
|
86
|
+
"prettier": "^3.8.5",
|
|
87
|
+
"react-hook-form": "^7.81.0",
|
|
88
88
|
"sass": "^1.101.0",
|
|
89
89
|
"typescript": "6.0.3",
|
|
90
|
-
"@uniai-fe/eslint-config": "0.
|
|
91
|
-
"@uniai-fe/next-devkit": "0.3.0",
|
|
90
|
+
"@uniai-fe/eslint-config": "0.4.1",
|
|
92
91
|
"@uniai-fe/uds-foundation": "0.5.0",
|
|
92
|
+
"@uniai-fe/util-functions": "0.4.3",
|
|
93
93
|
"@uniai-fe/tsconfig": "0.2.0",
|
|
94
|
-
"@uniai-fe/
|
|
94
|
+
"@uniai-fe/next-devkit": "0.4.0"
|
|
95
95
|
},
|
|
96
96
|
"scripts": {
|
|
97
97
|
"check:pre-commit": "pnpm --dir ../../.. run check:pre-commit",
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
DatesProvider,
|
|
6
6
|
type DateStringValue,
|
|
7
7
|
} from "@mantine/dates";
|
|
8
|
-
import {
|
|
8
|
+
import { useState } from "react";
|
|
9
9
|
import { dayjs } from "../../../init/dayjs";
|
|
10
10
|
import { CalendarIcon } from "./Icon";
|
|
11
11
|
import type { CalendarDatePickerProps, CalendarGridProps } from "../types";
|
|
@@ -43,16 +43,23 @@ export default function CalendarCore({
|
|
|
43
43
|
void valueFormat;
|
|
44
44
|
|
|
45
45
|
const isDisplayedDateControlled = date !== undefined;
|
|
46
|
-
const [
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
const [displayedDateState, setDisplayedDateState] = useState(() => ({
|
|
47
|
+
defaultDate,
|
|
48
|
+
displayedDate: date ?? mapValueToPicker(value) ?? defaultDate ?? new Date(),
|
|
49
|
+
value,
|
|
50
|
+
}));
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
if (
|
|
53
|
+
!isDisplayedDateControlled &&
|
|
54
|
+
(displayedDateState.value !== value ||
|
|
55
|
+
displayedDateState.defaultDate !== defaultDate)
|
|
56
|
+
) {
|
|
57
|
+
setDisplayedDateState({
|
|
58
|
+
defaultDate,
|
|
59
|
+
displayedDate: mapValueToPicker(value) ?? defaultDate ?? new Date(),
|
|
60
|
+
value,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
56
63
|
|
|
57
64
|
// 기본 DatePicker 옵션/스타일 책임을 Calendar.Core에 고정한다.
|
|
58
65
|
const resolvedDatePickerProps = {
|
|
@@ -101,7 +108,10 @@ export default function CalendarCore({
|
|
|
101
108
|
|
|
102
109
|
const handleDateChange = (nextDate: DateStringValue) => {
|
|
103
110
|
if (!isDisplayedDateControlled) {
|
|
104
|
-
|
|
111
|
+
setDisplayedDateState(previousState => ({
|
|
112
|
+
...previousState,
|
|
113
|
+
displayedDate: nextDate,
|
|
114
|
+
}));
|
|
105
115
|
}
|
|
106
116
|
onDateChange?.(nextDate);
|
|
107
117
|
};
|
|
@@ -124,7 +134,7 @@ export default function CalendarCore({
|
|
|
124
134
|
value={mapValueToPicker(value) as never}
|
|
125
135
|
onChange={handleChange as never}
|
|
126
136
|
{...resolvedDatePickerProps}
|
|
127
|
-
date={date ?? displayedDate}
|
|
137
|
+
date={date ?? displayedDateState.displayedDate}
|
|
128
138
|
onDateChange={handleDateChange}
|
|
129
139
|
/>
|
|
130
140
|
</DatesProvider>
|
|
@@ -183,6 +183,9 @@ export const useCarouselProviderController = ({
|
|
|
183
183
|
setCurrentIndex(previousIndex =>
|
|
184
184
|
Math.min(previousIndex, Math.max(normalizedCount - visibleCount, 0)),
|
|
185
185
|
);
|
|
186
|
+
setFocusIndexState(previousFocusIndex =>
|
|
187
|
+
Math.min(previousFocusIndex, Math.max(normalizedCount - 1, 0)),
|
|
188
|
+
);
|
|
186
189
|
},
|
|
187
190
|
[visibleCount],
|
|
188
191
|
);
|
|
@@ -234,11 +237,6 @@ export const useCarouselProviderController = ({
|
|
|
234
237
|
);
|
|
235
238
|
}, [fixedVisibleCount, itemCount]);
|
|
236
239
|
|
|
237
|
-
useEffect(() => {
|
|
238
|
-
// item 수/폭 조건 변화 시 visibleCount를 재계산한다.
|
|
239
|
-
updateVisibleCount();
|
|
240
|
-
}, [updateVisibleCount, itemCount]);
|
|
241
|
-
|
|
242
240
|
useEffect(() => {
|
|
243
241
|
/**
|
|
244
242
|
* ResizeObserver effect
|
|
@@ -502,13 +500,6 @@ export const useCarouselProviderController = ({
|
|
|
502
500
|
}
|
|
503
501
|
}, [currentIndex, onIndexChange]);
|
|
504
502
|
|
|
505
|
-
useEffect(() => {
|
|
506
|
-
// 아이템 수 감소 시 focusIndex 범위를 보정한다.
|
|
507
|
-
setFocusIndexState(previousFocusIndex =>
|
|
508
|
-
Math.min(previousFocusIndex, Math.max(itemCount - 1, 0)),
|
|
509
|
-
);
|
|
510
|
-
}, [itemCount]);
|
|
511
|
-
|
|
512
503
|
useEffect(() => {
|
|
513
504
|
// 상태 변화마다 edge를 재평가해 버튼 disabled 오차를 줄인다.
|
|
514
505
|
syncScrollEdgeState();
|
|
@@ -32,17 +32,18 @@ const CarouselTrack = ({
|
|
|
32
32
|
itemClassName,
|
|
33
33
|
...restProps
|
|
34
34
|
}: CarouselTrackProps) => {
|
|
35
|
-
const
|
|
35
|
+
const { focusIndex, registerItemCount, trackRef, viewportRef } =
|
|
36
|
+
useCarousel();
|
|
36
37
|
const childArray = Children.toArray(children);
|
|
37
38
|
const itemCount = childArray.length;
|
|
38
39
|
|
|
39
40
|
useEffect(() => {
|
|
40
41
|
// track에 렌더된 panel 개수를 Provider에 등록한다.
|
|
41
|
-
|
|
42
|
-
}, [
|
|
42
|
+
registerItemCount(itemCount);
|
|
43
|
+
}, [itemCount, registerItemCount]);
|
|
43
44
|
|
|
44
45
|
const enhancedChildren = childArray.map((child, index) => {
|
|
45
|
-
const isFocused = index ===
|
|
46
|
+
const isFocused = index === focusIndex;
|
|
46
47
|
const itemProps = {
|
|
47
48
|
className: clsx("carousel-item", itemClassName),
|
|
48
49
|
"data-carousel-index": index,
|
|
@@ -68,13 +69,10 @@ const CarouselTrack = ({
|
|
|
68
69
|
return (
|
|
69
70
|
<div
|
|
70
71
|
className={clsx("carousel-viewport", className)}
|
|
71
|
-
ref={
|
|
72
|
+
ref={viewportRef}
|
|
72
73
|
{...restProps}
|
|
73
74
|
>
|
|
74
|
-
<ul
|
|
75
|
-
className={clsx("carousel-track", trackClassName)}
|
|
76
|
-
ref={carousel.trackRef}
|
|
77
|
-
>
|
|
75
|
+
<ul className={clsx("carousel-track", trackClassName)} ref={trackRef}>
|
|
78
76
|
{enhancedChildren}
|
|
79
77
|
</ul>
|
|
80
78
|
</div>
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
|
2
2
|
import clsx from "clsx";
|
|
3
3
|
import {
|
|
4
|
+
Component,
|
|
4
5
|
forwardRef,
|
|
5
6
|
useCallback,
|
|
6
7
|
useEffect,
|
|
7
8
|
useMemo,
|
|
8
9
|
useRef,
|
|
9
10
|
useState,
|
|
11
|
+
useSyncExternalStore,
|
|
10
12
|
} from "react";
|
|
11
13
|
import { createPortal } from "react-dom";
|
|
12
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
ComponentPropsWithoutRef,
|
|
16
|
+
CSSProperties,
|
|
17
|
+
ForwardedRef,
|
|
18
|
+
ReactNode,
|
|
19
|
+
} from "react";
|
|
13
20
|
import CloseIcon from "../img/close.svg";
|
|
14
21
|
import type {
|
|
15
22
|
DrawerContextValue,
|
|
@@ -34,6 +41,23 @@ import {
|
|
|
34
41
|
} from "../utils";
|
|
35
42
|
|
|
36
43
|
const ANIMATION_DURATION = 320;
|
|
44
|
+
const subscribeToClient = () => () => undefined;
|
|
45
|
+
const getClientSnapshot = () => true;
|
|
46
|
+
const getServerSnapshot = () => false;
|
|
47
|
+
|
|
48
|
+
interface DrawerButtonLikeProps {
|
|
49
|
+
asChild?: boolean;
|
|
50
|
+
buttonProps: ComponentPropsWithoutRef<"button">;
|
|
51
|
+
buttonRef: ForwardedRef<HTMLButtonElement>;
|
|
52
|
+
children: ReactNode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class DrawerButtonLike extends Component<DrawerButtonLikeProps> {
|
|
56
|
+
override render() {
|
|
57
|
+
const { asChild, buttonProps, buttonRef, children } = this.props;
|
|
58
|
+
return renderButtonLike(asChild, children, buttonProps, buttonRef);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
37
61
|
|
|
38
62
|
/**
|
|
39
63
|
* DrawerRoot — open 상태/phase를 관리하는 컨텍스트 루트.
|
|
@@ -55,7 +79,17 @@ const DrawerRoot = ({
|
|
|
55
79
|
defaultOpen ?? false,
|
|
56
80
|
);
|
|
57
81
|
const open = isControlled ? (openProp as boolean) : uncontrolledOpen;
|
|
58
|
-
const [
|
|
82
|
+
const [phaseState, setPhaseState] = useState(() => ({
|
|
83
|
+
open,
|
|
84
|
+
phase: (open ? "entered" : "exited") as DrawerPhase,
|
|
85
|
+
}));
|
|
86
|
+
if (phaseState.open !== open) {
|
|
87
|
+
setPhaseState({
|
|
88
|
+
open,
|
|
89
|
+
phase: open ? "entering" : "exiting",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const phase = phaseState.phase;
|
|
59
93
|
const closeTimerRef = useRef<number | null>(null);
|
|
60
94
|
const enterRafRef = useRef<number | null>(null);
|
|
61
95
|
|
|
@@ -75,9 +109,8 @@ const DrawerRoot = ({
|
|
|
75
109
|
window.clearTimeout(closeTimerRef.current);
|
|
76
110
|
closeTimerRef.current = null;
|
|
77
111
|
}
|
|
78
|
-
setPhase("entering");
|
|
79
112
|
enterRafRef.current = window.requestAnimationFrame(() => {
|
|
80
|
-
|
|
113
|
+
setPhaseState({ open: true, phase: "entered" });
|
|
81
114
|
enterRafRef.current = null;
|
|
82
115
|
});
|
|
83
116
|
return () => {
|
|
@@ -91,9 +124,8 @@ const DrawerRoot = ({
|
|
|
91
124
|
window.cancelAnimationFrame(enterRafRef.current);
|
|
92
125
|
enterRafRef.current = null;
|
|
93
126
|
}
|
|
94
|
-
setPhase("exiting");
|
|
95
127
|
closeTimerRef.current = window.setTimeout(() => {
|
|
96
|
-
|
|
128
|
+
setPhaseState({ open: false, phase: "exited" });
|
|
97
129
|
closeTimerRef.current = null;
|
|
98
130
|
}, ANIMATION_DURATION);
|
|
99
131
|
return () => {
|
|
@@ -142,11 +174,14 @@ const DrawerTrigger = forwardRef<HTMLButtonElement, DrawerTriggerProps>(
|
|
|
142
174
|
({ asChild, children, onClick, ...props }, forwardedRef) => {
|
|
143
175
|
const { setOpen } = useDrawerContext();
|
|
144
176
|
const handleClick = composeEventHandlers(onClick, () => setOpen(true));
|
|
145
|
-
return
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
177
|
+
return (
|
|
178
|
+
<DrawerButtonLike
|
|
179
|
+
asChild={asChild}
|
|
180
|
+
buttonProps={{ ...props, onClick: handleClick }}
|
|
181
|
+
buttonRef={forwardedRef}
|
|
182
|
+
>
|
|
183
|
+
{children}
|
|
184
|
+
</DrawerButtonLike>
|
|
150
185
|
);
|
|
151
186
|
},
|
|
152
187
|
);
|
|
@@ -160,12 +195,11 @@ DrawerTrigger.displayName = "DrawerTrigger";
|
|
|
160
195
|
*/
|
|
161
196
|
const DrawerPortal = ({ children }: DrawerPortalProps) => {
|
|
162
197
|
useDrawerContext();
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}, []);
|
|
198
|
+
const mounted = useSyncExternalStore(
|
|
199
|
+
subscribeToClient,
|
|
200
|
+
getClientSnapshot,
|
|
201
|
+
getServerSnapshot,
|
|
202
|
+
);
|
|
169
203
|
|
|
170
204
|
if (!mounted) {
|
|
171
205
|
return null;
|
|
@@ -453,11 +487,14 @@ const DrawerClose = forwardRef<HTMLButtonElement, DrawerCloseProps>(
|
|
|
453
487
|
({ asChild, children, onClick, ...props }, forwardedRef) => {
|
|
454
488
|
const { setOpen } = useDrawerContext();
|
|
455
489
|
const handleClick = composeEventHandlers(onClick, () => setOpen(false));
|
|
456
|
-
return
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
490
|
+
return (
|
|
491
|
+
<DrawerButtonLike
|
|
492
|
+
asChild={asChild}
|
|
493
|
+
buttonProps={{ ...props, onClick: handleClick }}
|
|
494
|
+
buttonRef={forwardedRef}
|
|
495
|
+
>
|
|
496
|
+
{children}
|
|
497
|
+
</DrawerButtonLike>
|
|
461
498
|
);
|
|
462
499
|
},
|
|
463
500
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
6
|
DropdownTemplateItem,
|
|
@@ -82,30 +82,27 @@ const DropdownTemplate = ({
|
|
|
82
82
|
* - Template은 items[].selected를 초기값으로 받아 내부 상태를 관리한다.
|
|
83
83
|
* - 초기값은 selectedValuesFromItems를 그대로 사용한다.
|
|
84
84
|
*/
|
|
85
|
-
const [
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
const [selectionState, setSelectionState] = useState(() => ({
|
|
86
|
+
selectedValues: selectedValuesFromItems,
|
|
87
|
+
selectedValuesFromItems,
|
|
88
|
+
}));
|
|
88
89
|
|
|
89
90
|
/**
|
|
90
91
|
* 4) options(items) 변경 동기화
|
|
91
92
|
* - source of truth를 items[].selected로 고정해 외부 선택 상태를 즉시 반영한다.
|
|
92
93
|
* - single/multiple 정책은 normalizeSelectedValuesByMode로 통일 적용한다.
|
|
93
94
|
*/
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return previousSelectedValues;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return nextSelectedValues;
|
|
95
|
+
if (
|
|
96
|
+
!isSameSelectedValueList(
|
|
97
|
+
selectionState.selectedValuesFromItems,
|
|
98
|
+
selectedValuesFromItems,
|
|
99
|
+
)
|
|
100
|
+
) {
|
|
101
|
+
setSelectionState({
|
|
102
|
+
selectedValues: selectedValuesFromItems,
|
|
103
|
+
selectedValuesFromItems,
|
|
107
104
|
});
|
|
108
|
-
}
|
|
105
|
+
}
|
|
109
106
|
|
|
110
107
|
/**
|
|
111
108
|
* 5) 최종 선택 value 계산
|
|
@@ -113,7 +110,7 @@ const DropdownTemplate = ({
|
|
|
113
110
|
* - 렌더/이벤트/selected 스타일 계산은 이 값만 참조한다.
|
|
114
111
|
*/
|
|
115
112
|
const resolvedSelectedValues = normalizeSelectedValuesByMode(
|
|
116
|
-
|
|
113
|
+
selectionState.selectedValues,
|
|
117
114
|
isMultiple,
|
|
118
115
|
);
|
|
119
116
|
const selectedValueKeySet = useMemo(
|
|
@@ -173,7 +170,10 @@ const DropdownTemplate = ({
|
|
|
173
170
|
* 7-3) 내부 state 업데이트
|
|
174
171
|
* - Template 내부 선택 상태를 즉시 반영한다.
|
|
175
172
|
*/
|
|
176
|
-
|
|
173
|
+
setSelectionState(previousState => ({
|
|
174
|
+
...previousState,
|
|
175
|
+
selectedValues: nextSelectedValues,
|
|
176
|
+
}));
|
|
177
177
|
|
|
178
178
|
/**
|
|
179
179
|
* 7-4) 선택 결과 이벤트(onChange)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { ChangeEvent, useCallback,
|
|
3
|
+
import { ChangeEvent, useCallback, useMemo, useState } from "react";
|
|
4
4
|
import type { UseDigitFieldOptions, UseDigitFieldResult } from "../types";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -31,18 +31,15 @@ export const useDigitField = ({
|
|
|
31
31
|
clampDigits(defaultValue ?? ""),
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
setInnerDigits(prev => clampDigits(prev));
|
|
40
|
-
}, [clampDigits, isControlled]);
|
|
34
|
+
const clampedInnerDigits = clampDigits(innerDigits);
|
|
35
|
+
if (!isControlled && innerDigits !== clampedInnerDigits) {
|
|
36
|
+
setInnerDigits(clampedInnerDigits);
|
|
37
|
+
}
|
|
41
38
|
|
|
42
39
|
const resolvedDigits = useMemo(() => {
|
|
43
|
-
const target = isControlled ? (value ?? "") :
|
|
40
|
+
const target = isControlled ? (value ?? "") : clampedInnerDigits;
|
|
44
41
|
return clampDigits(target);
|
|
45
|
-
}, [clampDigits,
|
|
42
|
+
}, [clampDigits, clampedInnerDigits, isControlled, value]);
|
|
46
43
|
|
|
47
44
|
const handleDigitsChange = useCallback(
|
|
48
45
|
(event: ChangeEvent<HTMLInputElement>) => {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useId,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
4
11
|
import type {
|
|
5
12
|
InputFileEntry,
|
|
6
13
|
InputFileEvent,
|
|
@@ -47,11 +54,12 @@ export const useInputFileContext = ({
|
|
|
47
54
|
initialFiles,
|
|
48
55
|
mergeMode = "append",
|
|
49
56
|
}: UseInputFileContextOptions = {}): UseInputFileContextResult => {
|
|
57
|
+
const initialEntryId = useId();
|
|
50
58
|
/**
|
|
51
59
|
* Input Hook; File Upload 엔트리 고유 id 시퀀스 ref
|
|
52
60
|
*/
|
|
53
61
|
// 파일이 추가될 때마다 증가하는 시퀀스를 유지한다.
|
|
54
|
-
const entryIdRef = useRef(0);
|
|
62
|
+
const entryIdRef = useRef(initialFiles?.length ?? 0);
|
|
55
63
|
|
|
56
64
|
/**
|
|
57
65
|
* Input Hook; File Upload 엔트리 id 생성
|
|
@@ -83,13 +91,22 @@ export const useInputFileContext = ({
|
|
|
83
91
|
* Input Hook; File Upload 엔트리 상태
|
|
84
92
|
*/
|
|
85
93
|
const [entries, setEntries] = useState<InputFileEntry[]>(() =>
|
|
86
|
-
// 초기
|
|
87
|
-
|
|
94
|
+
// 초기 렌더에서는 ref 기반 id generator를 읽지 않고 useId 기반 key를 만든다.
|
|
95
|
+
(initialFiles ?? []).map((file, index) => ({
|
|
96
|
+
id: `${initialEntryId}-${index + 1}`,
|
|
97
|
+
file,
|
|
98
|
+
})),
|
|
88
99
|
);
|
|
89
100
|
/**
|
|
90
101
|
* Input Hook; File Upload initialFiles 시그니처 비교 ref
|
|
91
102
|
*/
|
|
92
|
-
const initialFilesSignatureRef = useRef(
|
|
103
|
+
const initialFilesSignatureRef = useRef(
|
|
104
|
+
(initialFiles ?? [])
|
|
105
|
+
.map(
|
|
106
|
+
file => `${file.name}-${file.size}-${file.lastModified}-${file.type}`,
|
|
107
|
+
)
|
|
108
|
+
.join("|"),
|
|
109
|
+
);
|
|
93
110
|
|
|
94
111
|
/**
|
|
95
112
|
* Input Hook; File Upload 파일 전체 교체 핸들러
|
|
@@ -4,7 +4,7 @@ import clsx from "clsx";
|
|
|
4
4
|
import type {
|
|
5
5
|
ClipboardEvent,
|
|
6
6
|
DragEvent,
|
|
7
|
-
|
|
7
|
+
InputEvent,
|
|
8
8
|
KeyboardEvent,
|
|
9
9
|
MouseEvent,
|
|
10
10
|
} from "react";
|
|
@@ -122,7 +122,7 @@ const InputDateTrigger = forwardRef<
|
|
|
122
122
|
event.preventDefault();
|
|
123
123
|
};
|
|
124
124
|
|
|
125
|
-
const handleBeforeInput = (event:
|
|
125
|
+
const handleBeforeInput = (event: InputEvent<HTMLInputElement>) => {
|
|
126
126
|
if (shouldBlockTyping) {
|
|
127
127
|
event.preventDefault();
|
|
128
128
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import clsx from "clsx";
|
|
2
2
|
import { forwardRef, useState } from "react";
|
|
3
3
|
import * as RadixRadioGroup from "@radix-ui/react-radio-group";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ComponentRef } from "react";
|
|
5
5
|
import { RadioCard } from "./RadioCard";
|
|
6
6
|
import type { RadioCardGroupProps } from "../types";
|
|
7
7
|
|
|
@@ -23,7 +23,7 @@ const RADIO_CARD_GROUP_CLASSNAME = "radio-card-group";
|
|
|
23
23
|
* <RadioCardGroup options={[{ id: "farm-1", title: "농장" }]} value="farm-1" />
|
|
24
24
|
*/
|
|
25
25
|
const RadioCardGroup = forwardRef<
|
|
26
|
-
|
|
26
|
+
ComponentRef<typeof RadixRadioGroup.Root>,
|
|
27
27
|
RadioCardGroupProps
|
|
28
28
|
>(function RadioCardGroup(
|
|
29
29
|
{
|
|
@@ -14,7 +14,7 @@ import SegmentedControlLabel from "./Label";
|
|
|
14
14
|
* @param {SegmentedControlValue | undefined} props.selectedValue 현재 선택된 값.
|
|
15
15
|
* @param {number} props.focusableIndex 포커스를 받을 기본 index.
|
|
16
16
|
* @param {number} props.fallbackIndex 첫 번째 활성화 index.
|
|
17
|
-
* @param {
|
|
17
|
+
* @param {RefObject<Array<HTMLButtonElement | null>>} props.itemRefs 버튼 ref 목록.
|
|
18
18
|
* @param {(value: SegmentedControlValue | undefined) => void} props.onSelect 값 변경 콜백.
|
|
19
19
|
* @param {(index: number) => void} props.onFocusItemAt index 포커스 함수.
|
|
20
20
|
* @param {(event: KeyboardEvent<HTMLButtonElement>, currentIndex: number) => void} props.onArrowNavigate 화살표 키 처리 콜백.
|
|
@@ -3,8 +3,8 @@ import type {
|
|
|
3
3
|
FocusEvent,
|
|
4
4
|
KeyboardEvent,
|
|
5
5
|
MouseEvent,
|
|
6
|
-
MutableRefObject,
|
|
7
6
|
PointerEvent,
|
|
7
|
+
RefObject,
|
|
8
8
|
} from "react";
|
|
9
9
|
import type { SlotTextProps } from "../../slot";
|
|
10
10
|
|
|
@@ -67,7 +67,7 @@ export interface SegmentedControlListProps {
|
|
|
67
67
|
selectedValue?: SegmentedControlValue;
|
|
68
68
|
focusableIndex: number;
|
|
69
69
|
fallbackIndex: number;
|
|
70
|
-
itemRefs:
|
|
70
|
+
itemRefs: RefObject<Array<HTMLButtonElement | null>>;
|
|
71
71
|
onSelect: (value: SegmentedControlValue | undefined) => void;
|
|
72
72
|
onFocusItemAt: (index: number) => void;
|
|
73
73
|
onArrowNavigate: (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback,
|
|
3
|
+
import { useCallback, useMemo, useState } from "react";
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
6
|
UseSelectDropdownOpenStateParams,
|
|
@@ -38,17 +38,10 @@ export const useSelectDropdownOpenState = ({
|
|
|
38
38
|
defaultOpen ?? false,
|
|
39
39
|
);
|
|
40
40
|
|
|
41
|
-
// 3)
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
if (isControlled) return;
|
|
44
|
-
|
|
45
|
-
setUncontrolledOpen(defaultOpen ?? false);
|
|
46
|
-
}, [defaultOpen, isControlled]);
|
|
47
|
-
|
|
48
|
-
// 4) 최종 open state는 제어형 우선, 아니면 내부 state를 사용한다.
|
|
41
|
+
// 3) 최종 open state는 제어형 우선, 아니면 최초 defaultOpen으로 만든 내부 state를 사용한다.
|
|
49
42
|
const resolvedOpen = isControlled ? (open as boolean) : uncontrolledOpen;
|
|
50
43
|
|
|
51
|
-
//
|
|
44
|
+
// 4) setOpen은 내부 state 갱신 + 외부 콜백 브릿지를 동시에 담당한다.
|
|
52
45
|
const setOpen = useCallback(
|
|
53
46
|
(nextOpen: boolean) => {
|
|
54
47
|
if (!isControlled) setUncontrolledOpen(nextOpen);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import clsx from "clsx";
|
|
4
|
-
import {
|
|
4
|
+
import { useMemo, useRef, useState } from "react";
|
|
5
5
|
import type { ReactNode } from "react";
|
|
6
6
|
|
|
7
7
|
import { Dropdown } from "../../dropdown/markup";
|
|
8
|
+
import type { DropdownContainerProps } from "../../dropdown/types";
|
|
8
9
|
import { SelectTriggerBase, SelectTriggerSelected } from "./foundation";
|
|
9
10
|
import Container from "./foundation/Container";
|
|
10
11
|
import { useSelectDropdownOpenState } from "../hooks";
|
|
@@ -20,9 +21,21 @@ import {
|
|
|
20
21
|
const SELECT_CUSTOM_OPTION_BASE_ID = "__select_custom_input__";
|
|
21
22
|
const SELECT_CUSTOM_OPTION_VALUE = "CUSTOM";
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Radix DropdownMenu.Content가 runtime에서 처리하는 open auto-focus 확장 props
|
|
26
|
+
* @property {(event: Event) => void} [onOpenAutoFocus] open 직후 focus 이동을 제어하는 consumer handler
|
|
27
|
+
*/
|
|
28
|
+
type DropdownContainerRuntimeProps = DropdownContainerProps & {
|
|
29
|
+
/**
|
|
30
|
+
* open 직후 focus 이동을 제어하는 consumer handler
|
|
31
|
+
*/
|
|
32
|
+
onOpenAutoFocus?: (event: Event) => void;
|
|
33
|
+
};
|
|
34
|
+
|
|
23
35
|
/**
|
|
24
36
|
* Select default trigger; 단일 선택 드롭다운을 렌더링한다.
|
|
25
37
|
* @component
|
|
38
|
+
* @desc enabled selected option이 있으면 open 직후 해당 item을 focus하고 dropdown panel viewport만 nearest 기준으로 정렬한다. consumer open auto-focus handler가 기본 동작을 막으면 consumer 결정을 우선한다.
|
|
26
39
|
* @param {SelectDefaultComponentProps} props default trigger props
|
|
27
40
|
* @param {string} [props.className] container className
|
|
28
41
|
* @param {ReactNode} [props.displayLabel] 강제 표시 라벨
|
|
@@ -86,10 +99,7 @@ export function SelectDefault<OptionData = unknown>({
|
|
|
86
99
|
const resolvedSize =
|
|
87
100
|
priority !== "primary" && size === "xsmall" ? "small" : size;
|
|
88
101
|
|
|
89
|
-
// 2) custom
|
|
90
|
-
const [customLabelValue, setCustomLabelValue] = useState("");
|
|
91
|
-
|
|
92
|
-
// 3) custom option id가 기존 item id와 충돌하지 않도록 안전한 id를 만든다.
|
|
102
|
+
// 2) custom option id가 기존 item id와 충돌하지 않도록 안전한 id를 만든다.
|
|
93
103
|
const customOptionId = useMemo(() => {
|
|
94
104
|
const existingIdSet = new Set(items.map(option => option.id));
|
|
95
105
|
if (!existingIdSet.has(SELECT_CUSTOM_OPTION_BASE_ID)) {
|
|
@@ -132,38 +142,42 @@ export function SelectDefault<OptionData = unknown>({
|
|
|
132
142
|
}, [items]);
|
|
133
143
|
|
|
134
144
|
// 6) 내부 선택 state를 선언한다.
|
|
135
|
-
const [
|
|
136
|
-
|
|
137
|
-
|
|
145
|
+
const [selectionState, setSelectionState] = useState(() => ({
|
|
146
|
+
hasCustomOptions: Boolean(customOptions),
|
|
147
|
+
selectedValue: selectedValueFromItems,
|
|
148
|
+
selectedValueFromItems,
|
|
149
|
+
}));
|
|
138
150
|
|
|
139
151
|
// 7) 외부 items 변경 시 value 규칙으로 내부 선택 state를 재동기화한다.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
if (
|
|
153
|
+
selectionState.hasCustomOptions !== Boolean(customOptions) ||
|
|
154
|
+
!isSameSelectedValue(
|
|
155
|
+
selectionState.selectedValueFromItems,
|
|
156
|
+
selectedValueFromItems,
|
|
157
|
+
)
|
|
158
|
+
) {
|
|
159
|
+
setSelectionState({
|
|
160
|
+
hasCustomOptions: Boolean(customOptions),
|
|
161
|
+
selectedValue:
|
|
143
162
|
customOptions &&
|
|
144
163
|
isSameSelectedValue(
|
|
145
|
-
|
|
164
|
+
selectionState.selectedValue,
|
|
146
165
|
SELECT_CUSTOM_OPTION_VALUE,
|
|
147
166
|
) &&
|
|
148
167
|
selectedValueFromItems === undefined
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
if (isSameSelectedValue(previousSelectedValue, selectedValueFromItems)) {
|
|
154
|
-
return previousSelectedValue;
|
|
155
|
-
}
|
|
156
|
-
return selectedValueFromItems;
|
|
168
|
+
? selectionState.selectedValue
|
|
169
|
+
: selectedValueFromItems,
|
|
170
|
+
selectedValueFromItems,
|
|
157
171
|
});
|
|
158
|
-
}
|
|
172
|
+
}
|
|
159
173
|
|
|
160
174
|
// 8) value 기반으로 현재 선택 option을 계산한다.
|
|
161
175
|
const selectedOption = useMemo(
|
|
162
176
|
() =>
|
|
163
177
|
mergedOptions.find(option =>
|
|
164
|
-
isSameSelectedValue(option.value,
|
|
178
|
+
isSameSelectedValue(option.value, selectionState.selectedValue),
|
|
165
179
|
),
|
|
166
|
-
[mergedOptions,
|
|
180
|
+
[mergedOptions, selectionState.selectedValue],
|
|
167
181
|
);
|
|
168
182
|
|
|
169
183
|
// 9) customOptions가 있는 경우 inputProps.value가 있고 option 매칭이 없으면 custom mode로 간주한다.
|
|
@@ -190,12 +204,16 @@ export function SelectDefault<OptionData = unknown>({
|
|
|
190
204
|
toSelectedValueKey(SELECT_CUSTOM_OPTION_VALUE),
|
|
191
205
|
);
|
|
192
206
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
207
|
+
const [customInputState, setCustomInputState] = useState({
|
|
208
|
+
isActive: false,
|
|
209
|
+
value: "",
|
|
210
|
+
});
|
|
211
|
+
if (customInputState.isActive !== isCustomInputActive) {
|
|
212
|
+
setCustomInputState({
|
|
213
|
+
isActive: isCustomInputActive,
|
|
214
|
+
value: isCustomInputActive ? customInputState.value : "",
|
|
215
|
+
});
|
|
216
|
+
}
|
|
199
217
|
|
|
200
218
|
// 11) 외부 displayLabel이 있으면 우선 사용하고, 없으면 선택 option label을 사용한다.
|
|
201
219
|
const resolvedDisplayLabel =
|
|
@@ -212,6 +230,42 @@ export function SelectDefault<OptionData = unknown>({
|
|
|
212
230
|
// 13) disabled/readOnly 상태에서는 open/option 선택 인터랙션을 모두 차단한다.
|
|
213
231
|
const isInteractionBlocked = disabled || readOnly;
|
|
214
232
|
|
|
233
|
+
// 13-1) panel과 enabled selected item을 runtime focus/viewport 정렬 대상으로 추적한다.
|
|
234
|
+
const dropdownPanelRef = useRef<HTMLDivElement>(null);
|
|
235
|
+
const selectedOptionRef = useRef<HTMLDivElement>(null);
|
|
236
|
+
|
|
237
|
+
// 13-2) consumer handler → selected focus → panel-only nearest scroll 순서를 보존한다.
|
|
238
|
+
const onOpenAutoFocus = (event: Event) => {
|
|
239
|
+
// consumer가 명시적으로 기본 동작을 막으면 내부 focus/scroll을 적용하지 않는다.
|
|
240
|
+
(
|
|
241
|
+
dropdownOptions?.containerProps as
|
|
242
|
+
DropdownContainerRuntimeProps | undefined
|
|
243
|
+
)?.onOpenAutoFocus?.(event);
|
|
244
|
+
if (event.defaultPrevented) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const panel = dropdownPanelRef.current;
|
|
249
|
+
const selectedItem = selectedOptionRef.current;
|
|
250
|
+
// selected가 없거나 disabled이면 ref가 없으므로 Radix 기본 focus 동작을 유지한다.
|
|
251
|
+
if (!panel || !selectedItem) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Radix의 panel focus를 막고 outer container/document scroll 없이 selected item만 focus한다.
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
selectedItem.focus({ preventScroll: true });
|
|
258
|
+
|
|
259
|
+
// viewport 밖으로 벗어난 거리만 panel.scrollTop에 반영해 nearest 정렬을 수행한다.
|
|
260
|
+
const panelRect = panel.getBoundingClientRect();
|
|
261
|
+
const selectedItemRect = selectedItem.getBoundingClientRect();
|
|
262
|
+
if (selectedItemRect.top < panelRect.top) {
|
|
263
|
+
panel.scrollTop -= panelRect.top - selectedItemRect.top;
|
|
264
|
+
} else if (selectedItemRect.bottom > panelRect.bottom) {
|
|
265
|
+
panel.scrollTop += selectedItemRect.bottom - panelRect.bottom;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
215
269
|
// 14) open 상태 변경 처리: 차단 상태면 닫힘 고정, 아니면 nextOpen 반영.
|
|
216
270
|
const handleOpenChange = (nextOpen: boolean) => {
|
|
217
271
|
if (isInteractionBlocked) {
|
|
@@ -255,7 +309,10 @@ export function SelectDefault<OptionData = unknown>({
|
|
|
255
309
|
});
|
|
256
310
|
}
|
|
257
311
|
|
|
258
|
-
|
|
312
|
+
setSelectionState(previousState => ({
|
|
313
|
+
...previousState,
|
|
314
|
+
selectedValue: option.value,
|
|
315
|
+
}));
|
|
259
316
|
onSelectChange?.(option, previousOption, event);
|
|
260
317
|
}
|
|
261
318
|
|
|
@@ -266,8 +323,8 @@ export function SelectDefault<OptionData = unknown>({
|
|
|
266
323
|
const labelInputValue = isCustomInputActive
|
|
267
324
|
? typeof inputProps?.value === "string" ||
|
|
268
325
|
typeof inputProps?.value === "number"
|
|
269
|
-
? String(inputProps.value) ||
|
|
270
|
-
:
|
|
326
|
+
? String(inputProps.value) || customInputState.value
|
|
327
|
+
: customInputState.value
|
|
271
328
|
: toSelectInputText(resolvedDisplayLabel);
|
|
272
329
|
|
|
273
330
|
// 17) 렌더: Container → Dropdown.Root → Trigger → Menu.List 구조를 유지한다.
|
|
@@ -328,15 +385,24 @@ export function SelectDefault<OptionData = unknown>({
|
|
|
328
385
|
valueStateSource={isCustomInputActive ? "label" : "hidden"}
|
|
329
386
|
// 변경: custom mode 진입 시 label input에 focus를 연결한다.
|
|
330
387
|
shouldFocusInput={isCustomInputActive}
|
|
331
|
-
onLabelChange={
|
|
388
|
+
onLabelChange={value => {
|
|
389
|
+
setCustomInputState(previousState => ({
|
|
390
|
+
...previousState,
|
|
391
|
+
value,
|
|
392
|
+
}));
|
|
393
|
+
}}
|
|
332
394
|
/>
|
|
333
395
|
</SelectTriggerBase>
|
|
334
396
|
</Dropdown.Trigger>
|
|
335
397
|
<Dropdown.Container
|
|
336
398
|
{...dropdownOptions?.containerProps}
|
|
399
|
+
ref={dropdownPanelRef}
|
|
337
400
|
size={dropdownOptions?.size ?? resolvedSize}
|
|
338
401
|
width={dropdownOptions?.width ?? "match"}
|
|
339
402
|
minWidth={dropdownOptions?.minWidth}
|
|
403
|
+
{...({
|
|
404
|
+
onOpenAutoFocus,
|
|
405
|
+
} as DropdownContainerProps)}
|
|
340
406
|
>
|
|
341
407
|
<Dropdown.Menu.List {...dropdownOptions?.menuListProps}>
|
|
342
408
|
{mergedOptions.length > 0 ? (
|
|
@@ -344,6 +410,16 @@ export function SelectDefault<OptionData = unknown>({
|
|
|
344
410
|
{mergedOptions.map(option => (
|
|
345
411
|
<Dropdown.Menu.Item
|
|
346
412
|
key={option.id}
|
|
413
|
+
// disabled option은 초기 focus 대상에서 제외하고 현재 단일 selected item만 추적한다.
|
|
414
|
+
ref={
|
|
415
|
+
!option.disabled &&
|
|
416
|
+
isSameSelectedValue(
|
|
417
|
+
resolvedSelectedOption?.value,
|
|
418
|
+
option.value,
|
|
419
|
+
)
|
|
420
|
+
? selectedOptionRef
|
|
421
|
+
: undefined
|
|
422
|
+
}
|
|
347
423
|
label={option.label}
|
|
348
424
|
description={option.description}
|
|
349
425
|
disabled={option.disabled}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import clsx from "clsx";
|
|
4
|
-
import {
|
|
4
|
+
import { useMemo, useState } from "react";
|
|
5
5
|
|
|
6
6
|
import Container from "../foundation/Container";
|
|
7
7
|
import { Dropdown } from "../../../dropdown/markup";
|
|
@@ -146,26 +146,26 @@ export function SelectMultipleTrigger<OptionData = unknown>({
|
|
|
146
146
|
* - controlled 모드에서는 source로 사용되지 않는다.
|
|
147
147
|
* - uncontrolled 모드에서는 최종 선택 배열을 내부 state가 소유한다.
|
|
148
148
|
*/
|
|
149
|
-
const [
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
const [selectionState, setSelectionState] = useState(() => ({
|
|
150
|
+
selectedValues: selectedValuesFromOptions,
|
|
151
|
+
selectedValuesFromOptions,
|
|
152
|
+
}));
|
|
152
153
|
|
|
153
154
|
/**
|
|
154
155
|
* 6) items 변경 시 내부 state 정합성 보정
|
|
155
156
|
* - source of truth를 items[].selected(value)로 고정해 외부 선택 상태를 즉시 반영한다.
|
|
156
157
|
*/
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return nextSelectedValues;
|
|
158
|
+
if (
|
|
159
|
+
!isSameSelectedValueList(
|
|
160
|
+
selectionState.selectedValuesFromOptions,
|
|
161
|
+
selectedValuesFromOptions,
|
|
162
|
+
)
|
|
163
|
+
) {
|
|
164
|
+
setSelectionState({
|
|
165
|
+
selectedValues: selectedValuesFromOptions,
|
|
166
|
+
selectedValuesFromOptions,
|
|
167
167
|
});
|
|
168
|
-
}
|
|
168
|
+
}
|
|
169
169
|
|
|
170
170
|
/**
|
|
171
171
|
* 7) 최종 선택 value 계산
|
|
@@ -173,10 +173,10 @@ export function SelectMultipleTrigger<OptionData = unknown>({
|
|
|
173
173
|
*/
|
|
174
174
|
const resolvedSelectedValues = useMemo(
|
|
175
175
|
() =>
|
|
176
|
-
|
|
176
|
+
selectionState.selectedValues.filter(selectedValue =>
|
|
177
177
|
optionMap.has(toSelectedValueKey(selectedValue)),
|
|
178
178
|
),
|
|
179
|
-
[optionMap,
|
|
179
|
+
[optionMap, selectionState.selectedValues],
|
|
180
180
|
);
|
|
181
181
|
const selectedValueKeySet = useMemo(
|
|
182
182
|
() =>
|
|
@@ -340,7 +340,10 @@ export function SelectMultipleTrigger<OptionData = unknown>({
|
|
|
340
340
|
|
|
341
341
|
onSelectOption?.(selectAllOption, undefined, event);
|
|
342
342
|
if (didChange) {
|
|
343
|
-
|
|
343
|
+
setSelectionState(previousState => ({
|
|
344
|
+
...previousState,
|
|
345
|
+
selectedValues: nextSelectedValues,
|
|
346
|
+
}));
|
|
344
347
|
onSelectChange?.(selectAllOption, undefined, event);
|
|
345
348
|
}
|
|
346
349
|
return;
|
|
@@ -359,7 +362,10 @@ export function SelectMultipleTrigger<OptionData = unknown>({
|
|
|
359
362
|
: [...resolvedSelectedValues, option.value];
|
|
360
363
|
|
|
361
364
|
onSelectOption?.(option, undefined, event);
|
|
362
|
-
|
|
365
|
+
setSelectionState(previousState => ({
|
|
366
|
+
...previousState,
|
|
367
|
+
selectedValues: nextSelectedValues,
|
|
368
|
+
}));
|
|
363
369
|
onSelectChange?.(option, undefined, event);
|
|
364
370
|
};
|
|
365
371
|
|