@simplysm/solid 13.0.85 → 13.0.88

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 (116) hide show
  1. package/README.md +143 -28
  2. package/dist/components/data/list/ListItem.d.ts.map +1 -1
  3. package/dist/components/data/list/ListItem.js +11 -4
  4. package/dist/components/data/list/ListItem.js.map +2 -2
  5. package/dist/components/data/list/ListItem.styles.d.ts +2 -0
  6. package/dist/components/data/list/ListItem.styles.d.ts.map +1 -1
  7. package/dist/components/data/list/ListItem.styles.js +11 -1
  8. package/dist/components/data/list/ListItem.styles.js.map +1 -1
  9. package/dist/components/features/crud-sheet/CrudSheet.d.ts.map +1 -1
  10. package/dist/components/features/crud-sheet/CrudSheet.js +7 -0
  11. package/dist/components/features/crud-sheet/CrudSheet.js.map +2 -2
  12. package/dist/components/features/data-select-button/DataSelectButton.d.ts.map +1 -1
  13. package/dist/components/features/data-select-button/DataSelectButton.js +30 -26
  14. package/dist/components/features/data-select-button/DataSelectButton.js.map +2 -2
  15. package/dist/components/features/permission-table/PermissionTable.js +5 -1
  16. package/dist/components/features/permission-table/PermissionTable.js.map +2 -2
  17. package/dist/components/form-control/DropdownTrigger.styles.js +1 -1
  18. package/dist/components/form-control/combobox/Combobox.d.ts +19 -5
  19. package/dist/components/form-control/combobox/Combobox.d.ts.map +1 -1
  20. package/dist/components/form-control/combobox/Combobox.js +2 -4
  21. package/dist/components/form-control/combobox/Combobox.js.map +1 -1
  22. package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts +2 -2
  23. package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts.map +1 -1
  24. package/dist/components/form-control/date-range-picker/DateRangePicker.js +10 -1
  25. package/dist/components/form-control/date-range-picker/DateRangePicker.js.map +2 -2
  26. package/dist/components/form-control/editor/RichTextEditor.d.ts +2 -2
  27. package/dist/components/form-control/editor/RichTextEditor.d.ts.map +1 -1
  28. package/dist/components/form-control/editor/RichTextEditor.js +2 -2
  29. package/dist/components/form-control/editor/RichTextEditor.js.map +1 -1
  30. package/dist/components/form-control/field/DatePicker.d.ts +2 -2
  31. package/dist/components/form-control/field/DatePicker.d.ts.map +1 -1
  32. package/dist/components/form-control/field/DatePicker.js.map +1 -1
  33. package/dist/components/form-control/field/DateTimePicker.d.ts +2 -2
  34. package/dist/components/form-control/field/DateTimePicker.d.ts.map +1 -1
  35. package/dist/components/form-control/field/DateTimePicker.js.map +1 -1
  36. package/dist/components/form-control/field/Field.styles.d.ts +6 -7
  37. package/dist/components/form-control/field/Field.styles.d.ts.map +1 -1
  38. package/dist/components/form-control/field/Field.styles.js.map +1 -1
  39. package/dist/components/form-control/field/NumberInput.d.ts +2 -2
  40. package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
  41. package/dist/components/form-control/field/NumberInput.js.map +1 -1
  42. package/dist/components/form-control/field/TextInput.d.ts +2 -2
  43. package/dist/components/form-control/field/TextInput.d.ts.map +1 -1
  44. package/dist/components/form-control/field/TextInput.js.map +1 -1
  45. package/dist/components/form-control/field/Textarea.d.ts +2 -2
  46. package/dist/components/form-control/field/Textarea.d.ts.map +1 -1
  47. package/dist/components/form-control/field/Textarea.js.map +1 -1
  48. package/dist/components/form-control/field/TimePicker.d.ts +2 -2
  49. package/dist/components/form-control/field/TimePicker.d.ts.map +1 -1
  50. package/dist/components/form-control/field/TimePicker.js.map +1 -1
  51. package/dist/components/form-control/numpad/Numpad.d.ts.map +1 -1
  52. package/dist/components/form-control/numpad/Numpad.js +4 -17
  53. package/dist/components/form-control/numpad/Numpad.js.map +2 -2
  54. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  55. package/dist/components/form-control/select/Select.js +19 -6
  56. package/dist/components/form-control/select/Select.js.map +2 -2
  57. package/dist/components/form-control/state-preset/StatePreset.d.ts +1 -3
  58. package/dist/components/form-control/state-preset/StatePreset.d.ts.map +1 -1
  59. package/dist/components/form-control/state-preset/StatePreset.js +69 -91
  60. package/dist/components/form-control/state-preset/StatePreset.js.map +2 -2
  61. package/dist/components/layout/FormGroup.js +1 -1
  62. package/dist/components/layout/FormGroup.js.map +1 -1
  63. package/dist/components/layout/FormTable.js +3 -3
  64. package/dist/components/layout/FormTable.js.map +1 -1
  65. package/dist/components/layout/sidebar/Sidebar.d.ts.map +1 -1
  66. package/dist/components/layout/sidebar/Sidebar.js +3 -6
  67. package/dist/components/layout/sidebar/Sidebar.js.map +2 -2
  68. package/dist/providers/i18n/locales/en.d.ts +2 -3
  69. package/dist/providers/i18n/locales/en.d.ts.map +1 -1
  70. package/dist/providers/i18n/locales/en.js +3 -4
  71. package/dist/providers/i18n/locales/en.js.map +1 -1
  72. package/dist/providers/i18n/locales/ko.d.ts +2 -3
  73. package/dist/providers/i18n/locales/ko.d.ts.map +1 -1
  74. package/dist/providers/i18n/locales/ko.js +3 -4
  75. package/dist/providers/i18n/locales/ko.js.map +1 -1
  76. package/docs/display-feedback.md +279 -0
  77. package/docs/features.md +357 -213
  78. package/docs/form-controls.md +261 -403
  79. package/docs/layout-data.md +386 -0
  80. package/docs/providers-hooks.md +411 -0
  81. package/package.json +5 -5
  82. package/src/components/data/list/ListItem.styles.ts +14 -2
  83. package/src/components/data/list/ListItem.tsx +13 -4
  84. package/src/components/features/crud-sheet/CrudSheet.tsx +8 -0
  85. package/src/components/features/data-select-button/DataSelectButton.tsx +39 -32
  86. package/src/components/features/permission-table/PermissionTable.tsx +1 -1
  87. package/src/components/form-control/DropdownTrigger.styles.ts +1 -1
  88. package/src/components/form-control/combobox/Combobox.tsx +42 -16
  89. package/src/components/form-control/date-range-picker/DateRangePicker.tsx +6 -4
  90. package/src/components/form-control/editor/RichTextEditor.tsx +5 -6
  91. package/src/components/form-control/field/DatePicker.tsx +3 -2
  92. package/src/components/form-control/field/DateTimePicker.tsx +3 -2
  93. package/src/components/form-control/field/Field.styles.ts +6 -8
  94. package/src/components/form-control/field/NumberInput.tsx +3 -2
  95. package/src/components/form-control/field/TextInput.tsx +3 -2
  96. package/src/components/form-control/field/Textarea.tsx +3 -2
  97. package/src/components/form-control/field/TimePicker.tsx +3 -2
  98. package/src/components/form-control/numpad/Numpad.tsx +16 -18
  99. package/src/components/form-control/select/Select.tsx +19 -5
  100. package/src/components/form-control/state-preset/StatePreset.tsx +32 -57
  101. package/src/components/layout/FormGroup.tsx +1 -1
  102. package/src/components/layout/FormTable.tsx +3 -3
  103. package/src/components/layout/sidebar/Sidebar.tsx +2 -3
  104. package/src/providers/i18n/locales/en.ts +2 -3
  105. package/src/providers/i18n/locales/ko.ts +2 -3
  106. package/tests/components/features/data-select-button/DataSelectButton.spec.tsx +62 -7
  107. package/tests/components/form-control/combobox/Combobox.spec.tsx +3 -3
  108. package/tests/components/form-control/date-range-picker/DateRangePicker.spec.tsx +56 -0
  109. package/docs/data.md +0 -204
  110. package/docs/disclosure.md +0 -146
  111. package/docs/display.md +0 -125
  112. package/docs/feedback.md +0 -156
  113. package/docs/helpers.md +0 -173
  114. package/docs/hooks.md +0 -146
  115. package/docs/layout.md +0 -94
  116. package/docs/providers.md +0 -180
@@ -9,7 +9,6 @@ import { useNotification } from "../../feedback/notification/NotificationProvide
9
9
  import { Icon } from "../../display/Icon";
10
10
  import { bg, text } from "../../../styles/base.styles";
11
11
  import { type ComponentSize, gap, pad } from "../../../styles/control.styles";
12
- import { themeTokens } from "../../../styles/theme.styles";
13
12
  import { Button } from "../Button";
14
13
  import { useI18n } from "../../../providers/i18n/I18nProvider";
15
14
 
@@ -20,53 +19,33 @@ interface StatePresetItem<TValue> {
20
19
  state: TValue;
21
20
  }
22
21
 
23
- type StatePresetSize = ComponentSize;
24
-
25
22
  export interface StatePresetProps<TValue> {
26
23
  storageKey: string;
27
24
  value: TValue;
28
25
  onValueChange: (value: TValue) => void;
29
- size?: StatePresetSize;
26
+ size?: ComponentSize;
30
27
  class?: string;
31
28
  style?: JSX.CSSProperties;
32
29
  }
33
30
 
34
31
  // ── Style constants ──
35
32
 
36
- const chipSizeClasses: Record<StatePresetSize, string> = {
33
+ const sizeClasses: Record<ComponentSize, string> = {
37
34
  md: pad.md,
38
- xs: clsx(pad.xs, "text-sm"),
35
+ xs: pad.xs,
39
36
  sm: pad.sm,
40
37
  lg: pad.lg,
41
- xl: clsx(pad.xl, "text-lg"),
42
- };
43
-
44
- const iconBtnSizeClasses: Record<StatePresetSize, string> = {
45
- md: "p-0.5",
46
- xs: "p-0",
47
- sm: "p-0.5",
48
- lg: "p-1",
49
- xl: "p-1.5",
38
+ xl: pad.xl,
50
39
  };
51
40
 
52
- const starBtnSizeClasses: Record<StatePresetSize, string> = {
53
- md: "p-1",
54
- xs: "p-0",
55
- sm: "p-0.5",
56
- lg: "p-1.5",
57
- xl: "p-2",
41
+ const iconSizeMap: Record<ComponentSize, string> = {
42
+ xs: "0.875em",
43
+ sm: "1em",
44
+ md: "1.25em",
45
+ lg: "1.5em",
46
+ xl: "1.75em",
58
47
  };
59
48
 
60
- const inputSizeClasses: Record<StatePresetSize, string> = {
61
- md: clsx(pad.md, "w-24"),
62
- xs: clsx("w-16", pad.xs, "text-sm"),
63
- sm: clsx(pad.sm, "w-20"),
64
- lg: clsx(pad.lg, "w-32"),
65
- xl: clsx(pad.xl, "w-36 text-lg"),
66
- };
67
-
68
- const iconSize = "0.85em";
69
-
70
49
  // ── Component ──
71
50
 
72
51
  function StatePresetInner<TValue>(props: StatePresetProps<TValue>): JSX.Element {
@@ -191,31 +170,29 @@ function StatePresetInner<TValue>(props: StatePresetProps<TValue>): JSX.Element
191
170
 
192
171
  // ── Render ──
193
172
 
194
- const resolvedIconBtnClass = () =>
195
- twMerge("rounded-full", iconBtnSizeClasses[local.size ?? "md"]);
173
+ const resolvedSize = () => local.size ?? "md";
196
174
 
197
175
  return (
198
176
  <div class={twMerge(clsx("inline-flex items-center", gap.lg, "flex-wrap"), local.class)} style={local.style}>
199
177
  {/* Star button - add preset */}
200
- <button
201
- type="button"
202
- class={twMerge(
203
- clsx("inline-flex cursor-pointer items-center justify-center rounded-full text-warning-500 transition-colors focus:outline-none", themeTokens.warning.hoverBg),
204
- starBtnSizeClasses[local.size ?? "md"],
205
- )}
178
+ <Button
179
+ size={local.size}
180
+ variant="ghost"
181
+ class="rounded-full text-warning-500"
206
182
  onClick={handleStartAdd}
207
183
  title={i18n.t("statePreset.addPreset")}
208
184
  >
209
- <Icon icon={IconStar} size={iconSize} />
210
- </button>
185
+ {"\u200B"}
186
+ <Icon icon={IconStar} size={iconSizeMap[resolvedSize()]} />
187
+ </Button>
211
188
 
212
189
  {/* Preset chips */}
213
190
  <For each={presets()}>
214
191
  {(preset, index) => (
215
192
  <span
216
193
  class={twMerge(
217
- clsx("inline-flex items-center", gap.md, "rounded-full", bg.subtle, text.default, "cursor-default"),
218
- chipSizeClasses[local.size ?? "md"],
194
+ clsx("inline-flex items-center", gap.md, "rounded-full border border-transparent", bg.subtle, text.default, "cursor-default"),
195
+ sizeClasses[resolvedSize()],
219
196
  )}
220
197
  >
221
198
  <button
@@ -226,24 +203,22 @@ function StatePresetInner<TValue>(props: StatePresetProps<TValue>): JSX.Element
226
203
  >
227
204
  {preset.name}
228
205
  </button>
229
- <Button
230
- variant="ghost"
231
- size="xs"
232
- class={resolvedIconBtnClass()}
206
+ <button
207
+ type="button"
208
+ class="cursor-pointer rounded-full opacity-50 hover:opacity-100 focus:outline-none"
233
209
  onClick={() => handleOverwrite(index())}
234
210
  title={i18n.t("statePreset.overwrite")}
235
211
  >
236
- <Icon icon={IconDeviceFloppy} size={iconSize} />
237
- </Button>
238
- <Button
239
- variant="ghost"
240
- size="xs"
241
- class={resolvedIconBtnClass()}
212
+ <Icon icon={IconDeviceFloppy} size={iconSizeMap[resolvedSize()]} />
213
+ </button>
214
+ <button
215
+ type="button"
216
+ class="cursor-pointer rounded-full opacity-50 hover:opacity-100 focus:outline-none"
242
217
  onClick={() => handleDelete(index())}
243
218
  title={i18n.t("statePreset.deletePreset")}
244
219
  >
245
- <Icon icon={IconX} size={iconSize} />
246
- </Button>
220
+ <Icon icon={IconX} size={iconSizeMap[resolvedSize()]} />
221
+ </button>
247
222
  </span>
248
223
  )}
249
224
  </For>
@@ -257,8 +232,8 @@ function StatePresetInner<TValue>(props: StatePresetProps<TValue>): JSX.Element
257
232
  }}
258
233
  type="text"
259
234
  class={twMerge(
260
- clsx("rounded-full", bg.subtle, text.default, "border border-transparent focus:outline-none focus:ring-1 focus:ring-primary-400", text.placeholder),
261
- inputSizeClasses[local.size ?? "md"],
235
+ clsx("inline-flex items-center rounded-full leading-normal", bg.subtle, text.default, "border border-transparent focus:outline-none focus:ring-1 focus:ring-primary-400", text.placeholder),
236
+ sizeClasses[resolvedSize()],
262
237
  )}
263
238
  placeholder={i18n.t("statePreset.namePlaceholder")}
264
239
  autocomplete="one-time-code"
@@ -43,7 +43,7 @@ const FormGroupBase: ParentComponent<FormGroupProps> = (props) => {
43
43
  const [local, rest] = splitProps(props, ["children", "class", "inline"]);
44
44
 
45
45
  const getClassName = () => twMerge(
46
- local.inline ? "inline-flex flex-row flex-wrap items-center gap-2" : "inline-flex flex-col gap-2",
46
+ local.inline ? "inline-flex flex-row flex-wrap items-center gap-x-2 gap-y-1" : "inline-flex flex-col gap-2",
47
47
  local.class,
48
48
  );
49
49
 
@@ -11,7 +11,7 @@ export interface FormTableItemProps extends JSX.TdHTMLAttributes<HTMLTableCellEl
11
11
 
12
12
  const FormTableRow: ParentComponent<JSX.HTMLAttributes<HTMLTableRowElement>> = (props) => {
13
13
  const [local, rest] = splitProps(props, ["children", "class"]);
14
- return <tr class={twMerge("[&>*:last-child]:pr-0 last:[&>*]:pb-0", local.class)} {...rest}>{local.children}</tr>;
14
+ return <tr class={twMerge("[&>td:last-child]:pr-0 [&:last-child>*]:pb-0", local.class)} {...rest}>{local.children}</tr>;
15
15
  };
16
16
 
17
17
  const FormTableItem: ParentComponent<FormTableItemProps> = (props) => {
@@ -26,9 +26,9 @@ const FormTableItem: ParentComponent<FormTableItemProps> = (props) => {
26
26
  return (
27
27
  <>
28
28
  <Show when={local.label}>
29
- <th class="w-0 whitespace-nowrap pb-1 pl-1 pr-1.5 text-right align-middle">{local.label}</th>
29
+ <th class="w-0 whitespace-nowrap pb-1 pr-2 text-right align-middle">{local.label}</th>
30
30
  </Show>
31
- <td class={twMerge("pb-1 pr-1.5 align-middle", local.class)} colspan={effectiveColspan()} {...rest}>
31
+ <td class={twMerge("pb-1 pr-2 align-middle", local.class)} colspan={effectiveColspan()} {...rest}>
32
32
  {local.children}
33
33
  </td>
34
34
  </>
@@ -216,7 +216,6 @@ const MenuContext = createContext<MenuContextValue>();
216
216
  */
217
217
  const SidebarMenu: Component<SidebarMenuProps> = (props) => {
218
218
  const [local, rest] = splitProps(props, ["menus", "class"]);
219
- const i18n = useI18n();
220
219
 
221
220
  const location = useLocation();
222
221
 
@@ -252,9 +251,9 @@ const SidebarMenu: Component<SidebarMenuProps> = (props) => {
252
251
  return (
253
252
  <MenuContext.Provider value={{ initialOpenItems }}>
254
253
  <div {...rest} data-sidebar-menu class={getClassName()}>
255
- <div class={clsx("px-4 py-2 text-xs font-bold", text.muted, "uppercase tracking-wider")}>{i18n.t("sidebarMenu.menu")}</div>
254
+ <div class={clsx("px-3.5 py-1 text-sm font-bold", text.muted, "uppercase tracking-wider")}>MENU</div>
256
255
  <List inset>
257
- <For each={local.menus}>{(menu) => <MenuItem menu={menu} size="lg" />}</For>
256
+ <For each={local.menus}>{(menu) => <MenuItem menu={menu} />}</For>
258
257
  </List>
259
258
  </div>
260
259
  </MenuContext.Provider>
@@ -83,6 +83,8 @@ export default {
83
83
  },
84
84
  permissionTable: {
85
85
  permissionItem: "Permission Item",
86
+ use: "Use",
87
+ edit: "Edit",
86
88
  },
87
89
  crudSheet: {
88
90
  save: "Save",
@@ -180,9 +182,6 @@ export default {
180
182
  numpad: {
181
183
  enter: "ENT",
182
184
  },
183
- sidebarMenu: {
184
- menu: "MENU",
185
- },
186
185
  validation: {
187
186
  required: "This is a required field",
188
187
  requiredField: "Required field",
@@ -83,6 +83,8 @@ export default {
83
83
  },
84
84
  permissionTable: {
85
85
  permissionItem: "권한 항목",
86
+ use: "사용",
87
+ edit: "편집",
86
88
  },
87
89
  crudSheet: {
88
90
  save: "저장",
@@ -180,9 +182,6 @@ export default {
180
182
  numpad: {
181
183
  enter: "입력",
182
184
  },
183
- sidebarMenu: {
184
- menu: "메뉴",
185
- },
186
185
  validation: {
187
186
  required: "필수 입력 항목입니다",
188
187
  requiredField: "필수 항목",
@@ -395,9 +395,8 @@ describe("DataSelectButton", () => {
395
395
  />
396
396
  ));
397
397
 
398
- const trigger = container.querySelector("[role='combobox']") as HTMLElement;
399
- expect(trigger.getAttribute("aria-disabled")).toBe("true");
400
- expect(trigger.tabIndex).toBe(-1);
398
+ const trigger = container.querySelector("button[data-trigger]") as HTMLButtonElement;
399
+ expect(trigger.disabled).toBe(true);
401
400
  });
402
401
 
403
402
  it("sets required aria attribute", () => {
@@ -412,11 +411,67 @@ describe("DataSelectButton", () => {
412
411
  />
413
412
  ));
414
413
 
415
- const trigger = container.querySelector("[role='combobox']") as HTMLElement;
414
+ const trigger = container.querySelector("button[data-trigger]") as HTMLElement;
416
415
  expect(trigger.getAttribute("aria-required")).toBe("true");
417
416
  });
418
417
 
419
- it("opens dialog on Enter key press", async () => {
418
+ it("trigger is a button element with aria-haspopup='dialog'", () => {
419
+ const load = createTestLoad();
420
+ const { container } = renderWithDialog(() => (
421
+ <DataSelectButton
422
+ load={load}
423
+ dialog={TestDialogComponent}
424
+ dialogProps={{ confirmKeys: [] }}
425
+ renderItem={(item: TestItem) => <span>{item.name}</span>}
426
+ />
427
+ ));
428
+
429
+ const trigger = container.querySelector("button[data-trigger]") as HTMLButtonElement;
430
+ expect(trigger).not.toBeNull();
431
+ expect(trigger.tagName).toBe("BUTTON");
432
+ expect(trigger.getAttribute("aria-haspopup")).toBe("dialog");
433
+ expect(trigger.getAttribute("type")).toBe("button");
434
+ });
435
+
436
+ it("aria-expanded changes dynamically with dialog open state", async () => {
437
+ const load = createTestLoad();
438
+
439
+ const { container } = renderWithDialog(() => (
440
+ <DataSelectButton
441
+ load={load}
442
+ dialog={TestDialogComponent}
443
+ dialogProps={{ confirmKeys: [1] }}
444
+ renderItem={(item: TestItem) => <span>{item.name}</span>}
445
+ />
446
+ ));
447
+
448
+ const trigger = container.querySelector("button[data-trigger]") as HTMLButtonElement;
449
+
450
+ // Initially false
451
+ expect(trigger.getAttribute("aria-expanded")).toBe("false");
452
+
453
+ // Open dialog via search button
454
+ const searchBtn = container.querySelector("[data-search-button]") as HTMLButtonElement;
455
+ searchBtn.click();
456
+
457
+ // While dialog is open, aria-expanded should be true
458
+ await vi.waitFor(() => {
459
+ expect(trigger.getAttribute("aria-expanded")).toBe("true");
460
+ });
461
+
462
+ // Confirm dialog to close it
463
+ const confirmBtn = document.querySelector(
464
+ "[data-testid='dialog-confirm']",
465
+ ) as HTMLButtonElement;
466
+ confirmBtn.click();
467
+
468
+ // After dialog closes, aria-expanded should be false again
469
+ await vi.waitFor(() => {
470
+ expect(trigger.getAttribute("aria-expanded")).toBe("false");
471
+ });
472
+ });
473
+
474
+ it("opens dialog on trigger click", async () => {
420
475
  const load = createTestLoad();
421
476
  const onValueChange = vi.fn();
422
477
 
@@ -430,8 +485,8 @@ describe("DataSelectButton", () => {
430
485
  />
431
486
  ));
432
487
 
433
- const trigger = container.querySelector("[role='combobox']") as HTMLElement;
434
- trigger.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
488
+ const trigger = container.querySelector("button[data-trigger]") as HTMLElement;
489
+ trigger.click();
435
490
 
436
491
  await vi.waitFor(() => {
437
492
  const confirmBtn = document.querySelector(
@@ -143,7 +143,7 @@ describe("Combobox component", () => {
143
143
  onValueChange={handleChange}
144
144
  allowsCustomValue
145
145
  parseCustomValue={(text) => text}
146
- renderValue={(v) => <>{v}</>}
146
+ renderValue={(v: string) => <>{v}</>}
147
147
  />
148
148
  </I18nProvider></ConfigProvider>
149
149
  ));
@@ -178,7 +178,7 @@ describe("Combobox component", () => {
178
178
  expect(handleChange).toHaveBeenCalledWith({ name: "테스트", custom: true });
179
179
  });
180
180
 
181
- it("sets undefined when allowsCustomValue is true without parseCustomValue", async () => {
181
+ it("uses query text as value when allowsCustomValue is true without parseCustomValue", async () => {
182
182
  const onValueChange = vi.fn();
183
183
  const { container, getByRole } = render(() => (
184
184
  <ConfigProvider clientName="test"><I18nProvider>
@@ -196,7 +196,7 @@ describe("Combobox component", () => {
196
196
  fireEvent.keyDown(getByRole("combobox"), { key: "Enter" });
197
197
 
198
198
  await waitFor(() => {
199
- expect(onValueChange).toHaveBeenCalledWith(undefined);
199
+ expect(onValueChange).toHaveBeenCalledWith("custom text");
200
200
  });
201
201
  });
202
202
  });
@@ -115,6 +115,62 @@ describe("DateRangePicker component", () => {
115
115
  });
116
116
  });
117
117
 
118
+ describe("required prop propagation", () => {
119
+ it("propagates required to child DatePickers in range mode", () => {
120
+ const { container } = render(() => (
121
+ <ConfigProvider clientName="test"><I18nProvider>
122
+ <DateRangePicker periodType="range" required />
123
+ </I18nProvider></ConfigProvider>
124
+ ));
125
+ const wrapper = container.querySelector("[data-date-range-picker]");
126
+ const dateFields = wrapper?.querySelectorAll("[data-date-field]");
127
+
128
+ // range mode has 2 DatePickers (from + to)
129
+ expect(dateFields?.length).toBe(2);
130
+
131
+ // Each DatePicker's hidden input should have the required validation message
132
+ dateFields?.forEach((field) => {
133
+ const hiddenInput = field.querySelector("input[aria-hidden='true']") as HTMLInputElement;
134
+ expect(hiddenInput).toBeTruthy();
135
+ expect(hiddenInput.validationMessage).toBe("This is a required field");
136
+ });
137
+ });
138
+
139
+ it("propagates required to child DatePicker in day mode", () => {
140
+ const { container } = render(() => (
141
+ <ConfigProvider clientName="test"><I18nProvider>
142
+ <DateRangePicker periodType="day" required />
143
+ </I18nProvider></ConfigProvider>
144
+ ));
145
+ const wrapper = container.querySelector("[data-date-range-picker]");
146
+ const dateFields = wrapper?.querySelectorAll("[data-date-field]");
147
+
148
+ // day mode has 1 DatePicker
149
+ expect(dateFields?.length).toBe(1);
150
+
151
+ const hiddenInput = dateFields?.[0].querySelector("input[aria-hidden='true']") as HTMLInputElement;
152
+ expect(hiddenInput).toBeTruthy();
153
+ expect(hiddenInput.validationMessage).toBe("This is a required field");
154
+ });
155
+
156
+ it("propagates required to child DatePicker in month mode", () => {
157
+ const { container } = render(() => (
158
+ <ConfigProvider clientName="test"><I18nProvider>
159
+ <DateRangePicker periodType="month" required />
160
+ </I18nProvider></ConfigProvider>
161
+ ));
162
+ const wrapper = container.querySelector("[data-date-range-picker]");
163
+ const dateFields = wrapper?.querySelectorAll("[data-date-field]");
164
+
165
+ // month mode has 1 DatePicker
166
+ expect(dateFields?.length).toBe(1);
167
+
168
+ const hiddenInput = dateFields?.[0].querySelector("input[aria-hidden='true']") as HTMLInputElement;
169
+ expect(hiddenInput).toBeTruthy();
170
+ expect(hiddenInput.validationMessage).toBe("This is a required field");
171
+ });
172
+ });
173
+
118
174
  describe("from change - 'range' mode", () => {
119
175
  it("calls onToChange(from) when from > to", () => {
120
176
  const onFromChange = vi.fn();
package/docs/data.md DELETED
@@ -1,204 +0,0 @@
1
- # Data
2
-
3
- ## Table
4
-
5
- ```typescript
6
- interface TableProps extends JSX.HTMLAttributes<HTMLTableElement> {
7
- inset?: boolean;
8
- }
9
- ```
10
-
11
- Basic HTML table with consistent styling.
12
-
13
- **Sub-components:** `Table.Row`, `Table.HeaderCell`, `Table.Cell`
14
-
15
- ---
16
-
17
- ## List
18
-
19
- ```typescript
20
- interface ListProps extends JSX.HTMLAttributes<HTMLDivElement> {
21
- inset?: boolean;
22
- }
23
- ```
24
-
25
- Vertical list with keyboard navigation (ArrowUp/Down, Home/End) and tree-view support (ArrowRight/Left to expand/collapse).
26
-
27
- **Sub-component:** `List.Item`
28
-
29
- ---
30
-
31
- ## Pagination
32
-
33
- ```typescript
34
- interface PaginationProps extends JSX.HTMLAttributes<HTMLElement> {
35
- page: number;
36
- onPageChange?: (page: number) => void;
37
- totalPageCount: number;
38
- displayPageCount?: number;
39
- size?: "xs" | "sm" | "md" | "lg" | "xl";
40
- }
41
- ```
42
-
43
- Page navigation control. `page` is 1-based. `displayPageCount` controls how many page numbers are visible at once.
44
-
45
- ---
46
-
47
- ## DataSheet
48
-
49
- ```typescript
50
- interface DataSheetProps<TItem> {
51
- items?: TItem[];
52
- storageKey?: string;
53
- hideConfigBar?: boolean;
54
- inset?: boolean;
55
- contentStyle?: JSX.CSSProperties | string;
56
-
57
- // Sorting
58
- sorts?: SortingDef[];
59
- onSortsChange?: (sorts: SortingDef[]) => void;
60
- autoSort?: boolean;
61
-
62
- // Pagination
63
- page?: number;
64
- onPageChange?: (page: number) => void;
65
- totalPageCount?: number;
66
- pageSize?: number;
67
- displayPageCount?: number;
68
-
69
- // Selection
70
- selectionMode?: "single" | "multiple";
71
- selection?: TItem[];
72
- onSelectionChange?: (items: TItem[]) => void;
73
- autoSelect?: boolean;
74
- isItemSelectable?: (item: TItem) => boolean | string;
75
-
76
- // Tree expansion
77
- expandedItems?: TItem[];
78
- onExpandedItemsChange?: (items: TItem[]) => void;
79
- itemChildren?: (item: TItem, index: number) => TItem[] | undefined;
80
-
81
- // Cell styling
82
- cellClass?: (item: TItem, colKey: string) => string | undefined;
83
- cellStyle?: (item: TItem, colKey: string) => string | undefined;
84
-
85
- // Reordering
86
- onItemsReorder?: (event: DataSheetReorderEvent<TItem>) => void;
87
-
88
- class?: string;
89
- children: JSX.Element;
90
- }
91
- ```
92
-
93
- Advanced data grid with sorting, pagination, row selection, tree expansion, column reordering, and column configuration persistence.
94
-
95
- - `storageKey` — persists column configuration (width, visibility, order) to sync storage
96
- - `autoSort` — sorts items client-side without `onSortsChange`
97
- - `autoSelect` — manages selection state internally
98
- - `itemChildren` — enables tree-structured rows
99
-
100
- **Sub-component:** `DataSheet.Column`
101
-
102
- ```typescript
103
- interface DataSheetColumnProps<TItem> {
104
- key: string;
105
- header?: string | string[];
106
- headerContent?: () => JSX.Element;
107
- headerStyle?: string;
108
- summary?: () => JSX.Element;
109
- tooltip?: string;
110
- fixed?: boolean;
111
- hidden?: boolean;
112
- collapse?: boolean;
113
- width?: string;
114
- class?: string;
115
- sortable?: boolean;
116
- resizable?: boolean;
117
- children: (ctx: DataSheetCellContext<TItem>) => JSX.Element;
118
- }
119
-
120
- interface DataSheetCellContext<TItem> {
121
- item: TItem;
122
- index: number;
123
- row: number;
124
- depth: number;
125
- }
126
-
127
- interface SortingDef {
128
- key: string;
129
- desc: boolean;
130
- }
131
-
132
- interface DataSheetReorderEvent<TItem> {
133
- item: TItem;
134
- targetItem: TItem;
135
- position: "before" | "after" | "inside";
136
- }
137
- ```
138
-
139
- ---
140
-
141
- ## Calendar
142
-
143
- ```typescript
144
- interface CalendarProps<TValue> extends Omit<JSX.HTMLAttributes<HTMLTableElement>, "children"> {
145
- items: TValue[];
146
- getItemDate: (item: TValue, index: number) => DateOnly;
147
- renderItem: (item: TValue, index: number) => JSX.Element;
148
- yearMonth?: DateOnly;
149
- onYearMonthChange?: (value: DateOnly) => void;
150
- weekStartDay?: number;
151
- minDaysInFirstWeek?: number;
152
- }
153
- ```
154
-
155
- Monthly calendar view that renders items on their corresponding dates. `getItemDate` maps items to dates. `renderItem` renders each item cell.
156
-
157
- ---
158
-
159
- ## Kanban
160
-
161
- ```typescript
162
- interface KanbanProps<TCardValue, TLaneValue> extends Omit<JSX.HTMLAttributes<HTMLDivElement>, "children" | "onDrop"> {
163
- onDrop?: (info: KanbanDropInfo<TLaneValue, TCardValue>) => void;
164
- selectedValues?: TCardValue[];
165
- onSelectedValuesChange?: (values: TCardValue[]) => void;
166
- children?: JSX.Element;
167
- }
168
- ```
169
-
170
- Kanban board with drag-and-drop card management.
171
-
172
- **Sub-components:**
173
- - `Kanban.Lane` — `{ value?: TLaneValue; busy?: boolean; collapsible?: boolean; collapsed?: boolean; onCollapsedChange?: (collapsed: boolean) => void }`
174
- - `Kanban.Card` — `{ value?: TCardValue; draggable?: boolean; selectable?: boolean; contentClass?: string }`
175
- - `Kanban.LaneTitle` — lane title slot
176
- - `Kanban.LaneTools` — lane toolbar slot
177
-
178
- ---
179
-
180
- ## Usage Examples
181
-
182
- ```typescript
183
- import { DataSheet, Pagination } from "@simplysm/solid";
184
-
185
- <DataSheet
186
- items={data()}
187
- sorts={sorts()}
188
- onSortsChange={setSorts}
189
- page={page()}
190
- onPageChange={setPage}
191
- totalPageCount={totalPages()}
192
- selectionMode="multiple"
193
- selection={selected()}
194
- onSelectionChange={setSelected}
195
- storageKey="my-table"
196
- >
197
- <DataSheet.Column key="name" header="Name" sortable>
198
- {(ctx) => ctx.item.name}
199
- </DataSheet.Column>
200
- <DataSheet.Column key="age" header="Age" sortable width="80px">
201
- {(ctx) => ctx.item.age}
202
- </DataSheet.Column>
203
- </DataSheet>
204
- ```