@uniai-fe/uds-primitives 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-primitives",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "UNIAI Design System; Primitives Components Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -63,13 +63,13 @@
63
63
  "dayjs": "^1.11.20"
64
64
  },
65
65
  "dependencies": {
66
- "@radix-ui/react-checkbox": "^1.3.3",
67
- "@radix-ui/react-dropdown-menu": "^2.1.16",
68
- "@radix-ui/react-popover": "^1.1.15",
69
- "@radix-ui/react-radio-group": "^1.3.8",
70
- "@radix-ui/react-tabs": "^1.1.13",
71
- "@radix-ui/react-tooltip": "^1.2.8",
72
- "@radix-ui/react-visually-hidden": "^1.2.4",
66
+ "@radix-ui/react-checkbox": "^1.3.8",
67
+ "@radix-ui/react-dropdown-menu": "^2.1.21",
68
+ "@radix-ui/react-popover": "^1.1.20",
69
+ "@radix-ui/react-radio-group": "^1.4.4",
70
+ "@radix-ui/react-tabs": "^1.1.18",
71
+ "@radix-ui/react-tooltip": "^1.2.13",
72
+ "@radix-ui/react-visually-hidden": "^1.2.8",
73
73
  "clsx": "^2.1.1",
74
74
  "dayjs": "^1.11.21",
75
75
  "react-daum-postcode": "^4.0.0"
@@ -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.12.3",
82
+ "@types/node": "^24.13.3",
83
83
  "@types/react": "^19.2.17",
84
84
  "@types/react-dom": "^19.2.3",
85
- "eslint": "^9.39.2",
86
- "prettier": "^3.8.4",
87
- "react-hook-form": "^7.80.0",
88
- "sass": "^1.101.0",
85
+ "eslint": "^9.39.5",
86
+ "prettier": "^3.9.6",
87
+ "react-hook-form": "^7.82.0",
88
+ "sass": "^1.101.3",
89
89
  "typescript": "6.0.3",
90
- "@uniai-fe/eslint-config": "0.4.0",
91
- "@uniai-fe/util-functions": "0.4.1",
92
- "@uniai-fe/next-devkit": "0.4.0",
90
+ "@uniai-fe/uds-foundation": "0.5.0",
91
+ "@uniai-fe/eslint-config": "0.4.2",
93
92
  "@uniai-fe/tsconfig": "0.2.0",
94
- "@uniai-fe/uds-foundation": "0.5.0"
93
+ "@uniai-fe/util-functions": "0.4.3",
94
+ "@uniai-fe/next-devkit": "0.4.0"
95
95
  },
96
96
  "scripts": {
97
97
  "check:pre-commit": "pnpm --dir ../../.. run check:pre-commit",
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
 
3
3
  import clsx from "clsx";
4
- import { useMemo, useState } from "react";
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] 강제 표시 라벨
@@ -217,6 +230,42 @@ export function SelectDefault<OptionData = unknown>({
217
230
  // 13) disabled/readOnly 상태에서는 open/option 선택 인터랙션을 모두 차단한다.
218
231
  const isInteractionBlocked = disabled || readOnly;
219
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
+
220
269
  // 14) open 상태 변경 처리: 차단 상태면 닫힘 고정, 아니면 nextOpen 반영.
221
270
  const handleOpenChange = (nextOpen: boolean) => {
222
271
  if (isInteractionBlocked) {
@@ -347,9 +396,13 @@ export function SelectDefault<OptionData = unknown>({
347
396
  </Dropdown.Trigger>
348
397
  <Dropdown.Container
349
398
  {...dropdownOptions?.containerProps}
399
+ ref={dropdownPanelRef}
350
400
  size={dropdownOptions?.size ?? resolvedSize}
351
401
  width={dropdownOptions?.width ?? "match"}
352
402
  minWidth={dropdownOptions?.minWidth}
403
+ {...({
404
+ onOpenAutoFocus,
405
+ } as DropdownContainerProps)}
353
406
  >
354
407
  <Dropdown.Menu.List {...dropdownOptions?.menuListProps}>
355
408
  {mergedOptions.length > 0 ? (
@@ -357,6 +410,16 @@ export function SelectDefault<OptionData = unknown>({
357
410
  {mergedOptions.map(option => (
358
411
  <Dropdown.Menu.Item
359
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
+ }
360
423
  label={option.label}
361
424
  description={option.description}
362
425
  disabled={option.disabled}