@simplysm/solid 13.0.70 → 13.0.71

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.
Files changed (98) hide show
  1. package/README.md +1 -1
  2. package/dist/components/disclosure/Dropdown.d.ts +6 -4
  3. package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
  4. package/dist/components/disclosure/Dropdown.js +24 -8
  5. package/dist/components/disclosure/Dropdown.js.map +2 -2
  6. package/dist/components/disclosure/dialogZIndex.d.ts +2 -0
  7. package/dist/components/disclosure/dialogZIndex.d.ts.map +1 -1
  8. package/dist/components/disclosure/dialogZIndex.js +4 -0
  9. package/dist/components/disclosure/dialogZIndex.js.map +1 -1
  10. package/dist/components/features/crud-detail/CrudDetail.d.ts.map +1 -1
  11. package/dist/components/features/crud-detail/CrudDetail.js +16 -7
  12. package/dist/components/features/crud-detail/CrudDetail.js.map +2 -2
  13. package/dist/components/features/crud-sheet/CrudSheet.d.ts.map +1 -1
  14. package/dist/components/features/crud-sheet/CrudSheet.js +14 -5
  15. package/dist/components/features/crud-sheet/CrudSheet.js.map +2 -2
  16. package/dist/components/features/crudRegistry.d.ts +16 -0
  17. package/dist/components/features/crudRegistry.d.ts.map +1 -0
  18. package/dist/components/features/crudRegistry.js +37 -0
  19. package/dist/components/features/crudRegistry.js.map +6 -0
  20. package/dist/components/features/permission-table/PermissionTable.d.ts.map +1 -1
  21. package/dist/components/features/permission-table/PermissionTable.js +71 -86
  22. package/dist/components/features/permission-table/PermissionTable.js.map +2 -2
  23. package/dist/components/features/shared-data/SharedDataSelect.js +2 -4
  24. package/dist/components/features/shared-data/SharedDataSelect.js.map +2 -2
  25. package/dist/components/features/shared-data/SharedDataSelectList.d.ts +2 -4
  26. package/dist/components/features/shared-data/SharedDataSelectList.d.ts.map +1 -1
  27. package/dist/components/features/shared-data/SharedDataSelectList.js +11 -46
  28. package/dist/components/features/shared-data/SharedDataSelectList.js.map +2 -2
  29. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  30. package/dist/components/form-control/select/Select.js +1 -1
  31. package/dist/components/form-control/select/Select.js.map +1 -1
  32. package/dist/helpers/createAppStructure.d.ts.map +1 -1
  33. package/dist/helpers/createAppStructure.js +3 -2
  34. package/dist/helpers/createAppStructure.js.map +1 -1
  35. package/dist/helpers/createHmrSafeContext.d.ts +3 -0
  36. package/dist/helpers/createHmrSafeContext.d.ts.map +1 -0
  37. package/dist/helpers/createHmrSafeContext.js +10 -0
  38. package/dist/helpers/createHmrSafeContext.js.map +6 -0
  39. package/dist/hooks/createSelectionGroup.d.ts.map +1 -1
  40. package/dist/hooks/createSelectionGroup.js +3 -2
  41. package/dist/hooks/createSelectionGroup.js.map +2 -2
  42. package/package.json +6 -5
  43. package/src/components/disclosure/Dropdown.tsx +31 -17
  44. package/src/components/disclosure/dialogZIndex.ts +5 -0
  45. package/src/components/features/crud-detail/CrudDetail.tsx +16 -5
  46. package/src/components/features/crud-sheet/CrudSheet.tsx +13 -3
  47. package/src/components/features/crudRegistry.ts +60 -0
  48. package/src/components/features/permission-table/PermissionTable.tsx +49 -46
  49. package/src/components/features/shared-data/SharedDataSelect.tsx +2 -2
  50. package/src/components/features/shared-data/SharedDataSelectList.tsx +11 -36
  51. package/src/components/form-control/select/Select.tsx +1 -5
  52. package/src/helpers/createAppStructure.ts +3 -2
  53. package/src/helpers/createHmrSafeContext.ts +8 -0
  54. package/src/hooks/createSelectionGroup.tsx +4 -2
  55. package/tests/components/data/List.spec.tsx +52 -52
  56. package/tests/components/data/Pagination.spec.tsx +43 -43
  57. package/tests/components/data/Table.spec.tsx +4 -4
  58. package/tests/components/data/kanban/Kanban.selection.spec.tsx +21 -21
  59. package/tests/components/data/sheet/DataSheet.spec.tsx +50 -50
  60. package/tests/components/disclosure/Collapse.spec.tsx +24 -24
  61. package/tests/components/disclosure/Dialog.spec.tsx +33 -33
  62. package/tests/components/disclosure/DialogProvider.spec.tsx +9 -9
  63. package/tests/components/disclosure/Dropdown.spec.tsx +134 -14
  64. package/tests/components/disclosure/Tabs.spec.tsx +21 -21
  65. package/tests/components/disclosure/dialogZIndex.spec.ts +45 -0
  66. package/tests/components/display/Alert.spec.tsx +4 -4
  67. package/tests/components/display/Barcode.spec.tsx +7 -7
  68. package/tests/components/display/Card.spec.tsx +3 -3
  69. package/tests/components/display/Link.spec.tsx +5 -5
  70. package/tests/components/display/Tag.spec.tsx +4 -4
  71. package/tests/components/features/address/AddressSearch.spec.tsx +3 -3
  72. package/tests/components/features/crudRegistry.spec.ts +119 -0
  73. package/tests/components/features/data-select-button/DataSelectButton.spec.tsx +8 -8
  74. package/tests/components/features/permission-table/PermissionTable.spec.tsx +43 -43
  75. package/tests/components/features/shared-data/SharedDataSelectList.spec.tsx +2 -17
  76. package/tests/components/feedback/busy/BusyContainer.spec.tsx +7 -7
  77. package/tests/components/feedback/notification/NotificationBell.spec.tsx +9 -9
  78. package/tests/components/feedback/print/Print.spec.tsx +4 -4
  79. package/tests/components/form-control/Button.spec.tsx +18 -18
  80. package/tests/components/form-control/checkbox/Checkbox.spec.tsx +20 -20
  81. package/tests/components/form-control/checkbox/CheckboxGroup.spec.tsx +12 -12
  82. package/tests/components/form-control/checkbox/Radio.spec.tsx +21 -21
  83. package/tests/components/form-control/checkbox/RadioGroup.spec.tsx +12 -12
  84. package/tests/components/form-control/color-picker/ColorPicker.spec.tsx +10 -10
  85. package/tests/components/form-control/combobox/Combobox.spec.tsx +16 -16
  86. package/tests/components/form-control/combobox/ComboboxItem.spec.tsx +7 -7
  87. package/tests/components/form-control/date-range-picker/DateRangePicker.spec.tsx +24 -24
  88. package/tests/components/form-control/field/DatePicker.spec.tsx +50 -50
  89. package/tests/components/form-control/field/DateTimePicker.spec.tsx +47 -47
  90. package/tests/components/form-control/field/NumberInput.spec.tsx +54 -54
  91. package/tests/components/form-control/field/TextInput.spec.tsx +49 -49
  92. package/tests/components/form-control/field/Textarea.spec.tsx +33 -33
  93. package/tests/components/form-control/field/TimePicker.spec.tsx +42 -42
  94. package/tests/components/form-control/numpad/Numpad.spec.tsx +40 -40
  95. package/tests/components/form-control/select/Select.spec.tsx +9 -9
  96. package/tests/components/form-control/select/SelectItem.spec.tsx +10 -10
  97. package/tests/helpers/createAppStructure.spec.tsx +57 -57
  98. package/tests/helpers/mergeStyles.spec.ts +31 -31
@@ -4,9 +4,9 @@ import { Dialog } from "../../../src/components/disclosure/Dialog";
4
4
  import { I18nProvider } from "../../../src/providers/i18n/I18nContext";
5
5
  import { ConfigProvider } from "../../../src/providers/ConfigContext";
6
6
 
7
- describe("Dialog 컴포넌트", () => {
7
+ describe("Dialog", () => {
8
8
  describe("basic rendering", () => {
9
- it("open=true일 때 다이얼로그가 렌더링된다", async () => {
9
+ it("renders when open=true", async () => {
10
10
  render(() => (
11
11
  <ConfigProvider clientName="test"><I18nProvider>
12
12
  <Dialog open={true}>
@@ -21,7 +21,7 @@ describe("Dialog 컴포넌트", () => {
21
21
  });
22
22
  });
23
23
 
24
- it("open=false일 다이얼로그가 DOM 없다", () => {
24
+ it("is absent from DOM when open=false", () => {
25
25
  render(() => (
26
26
  <ConfigProvider clientName="test"><I18nProvider>
27
27
  <Dialog open={false}>
@@ -34,7 +34,7 @@ describe("Dialog 컴포넌트", () => {
34
34
  expect(content).toBeNull();
35
35
  });
36
36
 
37
- it("data-modal 속성이 설정된다", async () => {
37
+ it("sets data-modal attribute", async () => {
38
38
  render(() => (
39
39
  <ConfigProvider clientName="test"><I18nProvider>
40
40
  <Dialog open={true}>
@@ -49,7 +49,7 @@ describe("Dialog 컴포넌트", () => {
49
49
  });
50
50
  });
51
51
 
52
- it("Dialog.Header 슬롯이 헤더에 렌더링된다", async () => {
52
+ it("renders Dialog.Header slot in header", async () => {
53
53
  render(() => (
54
54
  <ConfigProvider clientName="test"><I18nProvider>
55
55
  <Dialog open={true}>
@@ -66,8 +66,8 @@ describe("Dialog 컴포넌트", () => {
66
66
  });
67
67
  });
68
68
 
69
- describe("헤더 옵션", () => {
70
- it("Dialog.Header 미제공 시 헤더가 렌더링되지 않는다", async () => {
69
+ describe("header options", () => {
70
+ it("does not render header when Dialog.Header is absent", async () => {
71
71
  render(() => (
72
72
  <ConfigProvider clientName="test"><I18nProvider>
73
73
  <Dialog open={true}>
@@ -84,7 +84,7 @@ describe("Dialog 컴포넌트", () => {
84
84
  expect(header).toBeNull();
85
85
  });
86
86
 
87
- it("Dialog.Action 슬롯이 닫기 버튼 옆에 렌더링된다", async () => {
87
+ it("renders Dialog.Action slot beside the close button", async () => {
88
88
  render(() => (
89
89
  <ConfigProvider clientName="test"><I18nProvider>
90
90
  <Dialog open={true}>
@@ -102,7 +102,7 @@ describe("Dialog 컴포넌트", () => {
102
102
  });
103
103
  });
104
104
 
105
- it("closable={false}일 때 닫기 버튼이 없다", async () => {
105
+ it("hides close button when closable={false}", async () => {
106
106
  render(() => (
107
107
  <ConfigProvider clientName="test"><I18nProvider>
108
108
  <Dialog open={true} closable={false}>
@@ -119,8 +119,8 @@ describe("Dialog 컴포넌트", () => {
119
119
  });
120
120
  });
121
121
 
122
- describe("닫힘 동작", () => {
123
- it("닫기 버튼 클릭 시 onOpenChange(false) 호출된다", async () => {
122
+ describe("close behavior", () => {
123
+ it("calls onOpenChange(false) when close button is clicked", async () => {
124
124
  const handleOpenChange = vi.fn();
125
125
  render(() => (
126
126
  <ConfigProvider clientName="test"><I18nProvider>
@@ -137,7 +137,7 @@ describe("Dialog 컴포넌트", () => {
137
137
  expect(handleOpenChange).toHaveBeenCalledWith(false);
138
138
  });
139
139
 
140
- it("closeOnBackdrop=true일 백드롭 클릭으로 닫힌다", async () => {
140
+ it("closes on backdrop click when closeOnBackdrop=true", async () => {
141
141
  const handleOpenChange = vi.fn();
142
142
  render(() => (
143
143
  <ConfigProvider clientName="test"><I18nProvider>
@@ -154,7 +154,7 @@ describe("Dialog 컴포넌트", () => {
154
154
  expect(handleOpenChange).toHaveBeenCalledWith(false);
155
155
  });
156
156
 
157
- it("closeOnBackdrop 미설정 백드롭 클릭으로 닫히지 않는다", async () => {
157
+ it("does not close on backdrop click when closeOnBackdrop is not set", async () => {
158
158
  const handleOpenChange = vi.fn();
159
159
  render(() => (
160
160
  <ConfigProvider clientName="test"><I18nProvider>
@@ -171,7 +171,7 @@ describe("Dialog 컴포넌트", () => {
171
171
  expect(handleOpenChange).not.toHaveBeenCalled();
172
172
  });
173
173
 
174
- it("기본적으로 Escape 키로 닫힌다", async () => {
174
+ it("closes on Escape key by default", async () => {
175
175
  const handleOpenChange = vi.fn();
176
176
  render(() => (
177
177
  <ConfigProvider clientName="test"><I18nProvider>
@@ -188,7 +188,7 @@ describe("Dialog 컴포넌트", () => {
188
188
  expect(handleOpenChange).toHaveBeenCalledWith(false);
189
189
  });
190
190
 
191
- it("closeOnEscape=true일 Escape 닫힌다", async () => {
191
+ it("closes on Escape when closeOnEscape=true", async () => {
192
192
  const handleOpenChange = vi.fn();
193
193
  render(() => (
194
194
  <ConfigProvider clientName="test"><I18nProvider>
@@ -205,7 +205,7 @@ describe("Dialog 컴포넌트", () => {
205
205
  expect(handleOpenChange).toHaveBeenCalledWith(false);
206
206
  });
207
207
 
208
- it("closeOnEscape=false일 Escape 닫히지 않는다", async () => {
208
+ it("does not close on Escape when closeOnEscape=false", async () => {
209
209
  const handleOpenChange = vi.fn();
210
210
  render(() => (
211
211
  <ConfigProvider clientName="test"><I18nProvider>
@@ -222,7 +222,7 @@ describe("Dialog 컴포넌트", () => {
222
222
  expect(handleOpenChange).not.toHaveBeenCalled();
223
223
  });
224
224
 
225
- it("canDeactivate가 false를 반환하면 닫히지 않는다", async () => {
225
+ it("does not close when canDeactivate returns false", async () => {
226
226
  const handleOpenChange = vi.fn();
227
227
  render(() => (
228
228
  <ConfigProvider clientName="test"><I18nProvider>
@@ -241,7 +241,7 @@ describe("Dialog 컴포넌트", () => {
241
241
  });
242
242
 
243
243
  describe("accessibility", () => {
244
- it("role=dialog aria-modal 속성이 설정된다", async () => {
244
+ it("sets role=dialog and aria-modal attributes", async () => {
245
245
  render(() => (
246
246
  <ConfigProvider clientName="test"><I18nProvider>
247
247
  <Dialog open={true}>
@@ -258,7 +258,7 @@ describe("Dialog 컴포넌트", () => {
258
258
  });
259
259
  });
260
260
 
261
- it("aria-labelledby Dialog.Header 요소를 참조한다", async () => {
261
+ it("aria-labelledby references the Dialog.Header element", async () => {
262
262
  render(() => (
263
263
  <ConfigProvider clientName="test"><I18nProvider>
264
264
  <Dialog open={true}>
@@ -277,7 +277,7 @@ describe("Dialog 컴포넌트", () => {
277
277
  });
278
278
  });
279
279
 
280
- it("Dialog.Header 미제공 시 aria-labelledby가 없다", async () => {
280
+ it("omits aria-labelledby when Dialog.Header is absent", async () => {
281
281
  render(() => (
282
282
  <ConfigProvider clientName="test"><I18nProvider>
283
283
  <Dialog open={true}>
@@ -293,7 +293,7 @@ describe("Dialog 컴포넌트", () => {
293
293
  });
294
294
  });
295
295
 
296
- it("float 모드에서는 aria-modal 설정되지 않는다", async () => {
296
+ it("does not set aria-modal in float mode", async () => {
297
297
  render(() => (
298
298
  <ConfigProvider clientName="test"><I18nProvider>
299
299
  <Dialog open={true} float>
@@ -311,8 +311,8 @@ describe("Dialog 컴포넌트", () => {
311
311
  });
312
312
  });
313
313
 
314
- describe("float 모드", () => {
315
- it("float=true일 때 백드롭이 없다", async () => {
314
+ describe("float mode", () => {
315
+ it("has no backdrop when float=true", async () => {
316
316
  render(() => (
317
317
  <ConfigProvider clientName="test"><I18nProvider>
318
318
  <Dialog open={true} float>
@@ -329,8 +329,8 @@ describe("Dialog 컴포넌트", () => {
329
329
  });
330
330
  });
331
331
 
332
- describe("fill 모드", () => {
333
- it("fill=true일 다이얼로그에 fill 스타일이 적용된다", async () => {
332
+ describe("fill mode", () => {
333
+ it("applies fill style to dialog when fill=true", async () => {
334
334
  render(() => (
335
335
  <ConfigProvider clientName="test"><I18nProvider>
336
336
  <Dialog open={true} fill>
@@ -348,8 +348,8 @@ describe("Dialog 컴포넌트", () => {
348
348
  });
349
349
  });
350
350
 
351
- describe("크기 제어", () => {
352
- it("width, height가 적용된다", async () => {
351
+ describe("size control", () => {
352
+ it("applies width and height", async () => {
353
353
  render(() => (
354
354
  <ConfigProvider clientName="test"><I18nProvider>
355
355
  <Dialog open={true} width={400} height={300}>
@@ -366,7 +366,7 @@ describe("Dialog 컴포넌트", () => {
366
366
  });
367
367
  });
368
368
 
369
- it("minWidth, minHeight가 적용된다", async () => {
369
+ it("applies minWidth and minHeight", async () => {
370
370
  render(() => (
371
371
  <ConfigProvider clientName="test"><I18nProvider>
372
372
  <Dialog open={true} minWidth={300} minHeight={200}>
@@ -384,8 +384,8 @@ describe("Dialog 컴포넌트", () => {
384
384
  });
385
385
  });
386
386
 
387
- describe("리사이즈", () => {
388
- it("resizable=true일 리사이즈 바가 렌더링된다", async () => {
387
+ describe("resize", () => {
388
+ it("renders resize bars when resizable=true", async () => {
389
389
  render(() => (
390
390
  <ConfigProvider clientName="test"><I18nProvider>
391
391
  <Dialog open={true} resizable>
@@ -400,7 +400,7 @@ describe("Dialog 컴포넌트", () => {
400
400
  });
401
401
  });
402
402
 
403
- it("resizable=false(기본)일 때 리사이즈 바가 없다", async () => {
403
+ it("has no resize bars when resizable=false (default)", async () => {
404
404
  render(() => (
405
405
  <ConfigProvider clientName="test"><I18nProvider>
406
406
  <Dialog open={true}>
@@ -417,8 +417,8 @@ describe("Dialog 컴포넌트", () => {
417
417
  });
418
418
  });
419
419
 
420
- describe("애니메이션", () => {
421
- it("열림 transition 클래스가 적용된다", async () => {
420
+ describe("animation", () => {
421
+ it("applies transition classes on open", async () => {
422
422
  render(() => (
423
423
  <ConfigProvider clientName="test"><I18nProvider>
424
424
  <Dialog open={true}>
@@ -4,7 +4,7 @@ import { DialogProvider } from "../../../src/components/disclosure/DialogProvide
4
4
  import { useDialog } from "../../../src/components/disclosure/DialogContext";
5
5
  import { useDialogInstance } from "../../../src/components/disclosure/DialogInstanceContext";
6
6
 
7
- // 테스트용 다이얼로그 콘텐츠 컴포넌트
7
+ // dialog content component for testing
8
8
  function TestContent() {
9
9
  const dialog = useDialogInstance<string>();
10
10
  return (
@@ -20,7 +20,7 @@ function TestContent() {
20
20
  );
21
21
  }
22
22
 
23
- // useDialog를 호출하는 테스트용 컴포넌트
23
+ // test component that calls useDialog
24
24
  function TestApp() {
25
25
  const dialog = useDialog();
26
26
 
@@ -36,7 +36,7 @@ function TestApp() {
36
36
  );
37
37
  }
38
38
 
39
- // header 없이 열리는 테스트용 컴포넌트
39
+ // test component that opens without a header
40
40
  function TestAppNoHeader() {
41
41
  const dialog = useDialog();
42
42
 
@@ -53,7 +53,7 @@ function TestAppNoHeader() {
53
53
  }
54
54
 
55
55
  describe("DialogProvider", () => {
56
- it("show()로 다이얼로그가 표시된다", async () => {
56
+ it("displays dialog via show()", async () => {
57
57
  render(() => (
58
58
  <DialogProvider>
59
59
  <TestApp />
@@ -67,7 +67,7 @@ describe("DialogProvider", () => {
67
67
  });
68
68
  });
69
69
 
70
- it("useDialogInstance로 close 호출하면 다이얼로그가 닫힌다", async () => {
70
+ it("closes dialog when close is called via useDialogInstance", async () => {
71
71
  render(() => (
72
72
  <DialogProvider>
73
73
  <TestApp />
@@ -82,13 +82,13 @@ describe("DialogProvider", () => {
82
82
 
83
83
  fireEvent.click(document.querySelector('[data-testid="close-btn"]')!);
84
84
 
85
- // 닫힘 애니메이션 fallback timer(200ms) 후 다이얼로그 콘텐츠가 제거됨
85
+ // dialog content is removed after close animation fallback timer (200ms)
86
86
  await waitFor(() => {
87
87
  expect(document.querySelector('[data-testid="modal-content"]')).toBeNull();
88
88
  });
89
89
  });
90
90
 
91
- it("close() 호출 시 다이얼로그가 닫힌다", async () => {
91
+ it("closes dialog when close() is called", async () => {
92
92
  render(() => (
93
93
  <DialogProvider>
94
94
  <TestApp />
@@ -108,7 +108,7 @@ describe("DialogProvider", () => {
108
108
  });
109
109
  });
110
110
 
111
- it("다이얼로그 header가 표시된다", async () => {
111
+ it("displays dialog header", async () => {
112
112
  render(() => (
113
113
  <DialogProvider>
114
114
  <TestApp />
@@ -124,7 +124,7 @@ describe("DialogProvider", () => {
124
124
  });
125
125
  });
126
126
 
127
- it("header 미제공 헤더가 렌더링되지 않는다", async () => {
127
+ it("does not render header when header is not provided", async () => {
128
128
  render(() => (
129
129
  <DialogProvider>
130
130
  <TestAppNoHeader />
@@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
3
3
  import { createSignal } from "solid-js";
4
4
  import { Dropdown } from "../../../src/components/disclosure/Dropdown";
5
5
 
6
- describe("Dropdown 컴포넌트", () => {
6
+ describe("Dropdown", () => {
7
7
  beforeEach(() => {
8
8
  vi.stubGlobal("innerWidth", 1024);
9
9
  vi.stubGlobal("innerHeight", 768);
@@ -15,8 +15,8 @@ describe("Dropdown 컴포넌트", () => {
15
15
  vi.unstubAllGlobals();
16
16
  });
17
17
 
18
- describe("Trigger/Content 구조", () => {
19
- it("Trigger 클릭 Content가 렌더링된다", async () => {
18
+ describe("Trigger/Content structure", () => {
19
+ it("renders Content on Trigger click", async () => {
20
20
  render(() => (
21
21
  <Dropdown>
22
22
  <Dropdown.Trigger>
@@ -38,7 +38,7 @@ describe("Dropdown 컴포넌트", () => {
38
38
  });
39
39
  });
40
40
 
41
- it("Trigger 재클릭 닫힌다", () => {
41
+ it("closes on second Trigger click", () => {
42
42
  const [open, setOpen] = createSignal(false);
43
43
  const handleOpenChange = vi.fn((v: boolean) => setOpen(v));
44
44
 
@@ -61,7 +61,7 @@ describe("Dropdown 컴포넌트", () => {
61
61
  expect(handleOpenChange).toHaveBeenCalledWith(false);
62
62
  });
63
63
 
64
- it("disabled Trigger 클릭이 무시된다", () => {
64
+ it("ignores Trigger click when disabled", () => {
65
65
  const [open, setOpen] = createSignal(false);
66
66
  const handleOpenChange = vi.fn((v: boolean) => setOpen(v));
67
67
 
@@ -81,8 +81,8 @@ describe("Dropdown 컴포넌트", () => {
81
81
  });
82
82
  });
83
83
 
84
- describe("Controlled 모드", () => {
85
- it("open prop으로 제어된다", async () => {
84
+ describe("controlled mode", () => {
85
+ it("is controlled by open prop", async () => {
86
86
  const [open, setOpen] = createSignal(false);
87
87
 
88
88
  render(() => (
@@ -106,8 +106,8 @@ describe("Dropdown 컴포넌트", () => {
106
106
  });
107
107
  });
108
108
 
109
- describe("Context menu (Trigger 없음)", () => {
110
- it("position prop으로 위치가 설정된다", async () => {
109
+ describe("Context menu (no Trigger)", () => {
110
+ it("sets position via position prop", async () => {
111
111
  render(() => (
112
112
  <Dropdown position={{ x: 300, y: 200 }} open={true}>
113
113
  <Dropdown.Content>
@@ -124,8 +124,8 @@ describe("Dropdown 컴포넌트", () => {
124
124
  });
125
125
  });
126
126
 
127
- describe("닫힘 감지", () => {
128
- it("외부 pointerdown 닫힌다", async () => {
127
+ describe("close detection", () => {
128
+ it("closes on outside pointerdown", async () => {
129
129
  const handleOpenChange = vi.fn();
130
130
 
131
131
  render(() => (
@@ -150,7 +150,7 @@ describe("Dropdown 컴포넌트", () => {
150
150
  expect(handleOpenChange).toHaveBeenCalledWith(false);
151
151
  });
152
152
 
153
- it("Escape 키로 닫힌다", async () => {
153
+ it("closes on Escape key", async () => {
154
154
  const handleOpenChange = vi.fn();
155
155
 
156
156
  render(() => (
@@ -174,7 +174,7 @@ describe("Dropdown 컴포넌트", () => {
174
174
  });
175
175
 
176
176
  describe("maxHeight", () => {
177
- it("기본값 300px", async () => {
177
+ it("defaults to 300px", async () => {
178
178
  render(() => (
179
179
  <Dropdown open={true}>
180
180
  <Dropdown.Trigger>
@@ -192,7 +192,7 @@ describe("Dropdown 컴포넌트", () => {
192
192
  });
193
193
  });
194
194
 
195
- it("커스텀 적용", async () => {
195
+ it("applies custom value", async () => {
196
196
  render(() => (
197
197
  <Dropdown open={true} maxHeight={500}>
198
198
  <Dropdown.Trigger>
@@ -210,4 +210,124 @@ describe("Dropdown 컴포넌트", () => {
210
210
  });
211
211
  });
212
212
  });
213
+
214
+ describe("keyboardNav tabbable navigation", () => {
215
+ it("ArrowDown from trigger focuses first tabbable in popup (input, not list item)", async () => {
216
+ const [open, setOpen] = createSignal(true);
217
+
218
+ render(() => (
219
+ <Dropdown open={open()} onOpenChange={setOpen} keyboardNav>
220
+ <Dropdown.Trigger>
221
+ <button data-testid="trigger">trigger</button>
222
+ </Dropdown.Trigger>
223
+ <Dropdown.Content>
224
+ <input data-testid="search" />
225
+ <button data-testid="action-btn" type="button">action</button>
226
+ <button data-list-item data-testid="item1" type="button">item1</button>
227
+ </Dropdown.Content>
228
+ </Dropdown>
229
+ ));
230
+
231
+ await waitFor(() => {
232
+ expect(document.querySelector("[data-dropdown]")).not.toBeNull();
233
+ });
234
+
235
+ const triggerWrapper = document.querySelector("[data-dropdown-trigger]") as HTMLElement;
236
+ triggerWrapper.focus();
237
+
238
+ fireEvent.keyDown(triggerWrapper, { key: "ArrowDown" });
239
+
240
+ const search = document.querySelector('[data-testid="search"]') as HTMLElement;
241
+ expect(document.activeElement).toBe(search);
242
+ });
243
+
244
+ it("ArrowDown from input focuses next tabbable (button)", async () => {
245
+ const [open, setOpen] = createSignal(true);
246
+
247
+ render(() => (
248
+ <Dropdown open={open()} onOpenChange={setOpen} keyboardNav>
249
+ <Dropdown.Trigger>
250
+ <button data-testid="trigger">trigger</button>
251
+ </Dropdown.Trigger>
252
+ <Dropdown.Content>
253
+ <input data-testid="search" />
254
+ <button data-testid="action-btn" type="button">action</button>
255
+ <button data-list-item data-testid="item1" type="button">item1</button>
256
+ </Dropdown.Content>
257
+ </Dropdown>
258
+ ));
259
+
260
+ await waitFor(() => {
261
+ expect(document.querySelector("[data-dropdown]")).not.toBeNull();
262
+ });
263
+
264
+ const search = document.querySelector('[data-testid="search"]') as HTMLElement;
265
+ search.focus();
266
+
267
+ const popup = document.querySelector("[data-dropdown]") as HTMLElement;
268
+ fireEvent.keyDown(popup, { key: "ArrowDown" });
269
+
270
+ const actionBtn = document.querySelector('[data-testid="action-btn"]') as HTMLElement;
271
+ expect(document.activeElement).toBe(actionBtn);
272
+ });
273
+
274
+ it("ArrowUp from input focuses trigger (no prev tabbable, dir=down)", async () => {
275
+ const [open, setOpen] = createSignal(true);
276
+
277
+ render(() => (
278
+ <Dropdown open={open()} onOpenChange={setOpen} keyboardNav>
279
+ <Dropdown.Trigger>
280
+ <button data-testid="trigger">trigger</button>
281
+ </Dropdown.Trigger>
282
+ <Dropdown.Content>
283
+ <input data-testid="search" />
284
+ <button data-list-item data-testid="item1" type="button">item1</button>
285
+ </Dropdown.Content>
286
+ </Dropdown>
287
+ ));
288
+
289
+ await waitFor(() => {
290
+ expect(document.querySelector("[data-dropdown]")).not.toBeNull();
291
+ });
292
+
293
+ const search = document.querySelector('[data-testid="search"]') as HTMLElement;
294
+ search.focus();
295
+
296
+ const popup = document.querySelector("[data-dropdown]") as HTMLElement;
297
+ fireEvent.keyDown(popup, { key: "ArrowUp" });
298
+
299
+ const triggerWrapper = document.querySelector("[data-dropdown-trigger]") as HTMLElement;
300
+ expect(document.activeElement).toBe(triggerWrapper);
301
+ });
302
+
303
+ it("ArrowUp from action button focuses prev tabbable (input)", async () => {
304
+ const [open, setOpen] = createSignal(true);
305
+
306
+ render(() => (
307
+ <Dropdown open={open()} onOpenChange={setOpen} keyboardNav>
308
+ <Dropdown.Trigger>
309
+ <button data-testid="trigger">trigger</button>
310
+ </Dropdown.Trigger>
311
+ <Dropdown.Content>
312
+ <input data-testid="search" />
313
+ <button data-testid="action-btn" type="button">action</button>
314
+ <button data-list-item data-testid="item1" type="button">item1</button>
315
+ </Dropdown.Content>
316
+ </Dropdown>
317
+ ));
318
+
319
+ await waitFor(() => {
320
+ expect(document.querySelector("[data-dropdown]")).not.toBeNull();
321
+ });
322
+
323
+ const actionBtn = document.querySelector('[data-testid="action-btn"]') as HTMLElement;
324
+ actionBtn.focus();
325
+
326
+ const popup = document.querySelector("[data-dropdown]") as HTMLElement;
327
+ fireEvent.keyDown(popup, { key: "ArrowUp" });
328
+
329
+ const search = document.querySelector('[data-testid="search"]') as HTMLElement;
330
+ expect(document.activeElement).toBe(search);
331
+ });
332
+ });
213
333
  });