gbs-add-block 1.2.14 → 1.2.16

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 (169) hide show
  1. package/README.md +2 -2
  2. package/index.cjs +219 -21
  3. package/package.json +1 -1
  4. package/source/beta-components/breadcrumb/README.md +34 -0
  5. package/source/beta-components/breadcrumb/__tests__/core.test.ts +58 -0
  6. package/source/beta-components/breadcrumb/core/index.ts +4 -0
  7. package/source/beta-components/breadcrumb/core/trail.ts +42 -0
  8. package/source/beta-components/breadcrumb/core/types.ts +20 -0
  9. package/source/beta-components/breadcrumb/index.ts +5 -0
  10. package/source/beta-components/breadcrumb/react/Breadcrumb.tsx +177 -0
  11. package/source/beta-components/breadcrumb/react/locale.ts +6 -0
  12. package/source/beta-components/breadcrumb/react/props.ts +14 -0
  13. package/source/beta-components/breadcrumb/styles.css +139 -0
  14. package/source/beta-components/button/README.md +32 -0
  15. package/source/beta-components/button/__tests__/core.test.ts +31 -0
  16. package/source/beta-components/button/core/index.ts +3 -0
  17. package/source/beta-components/button/core/state.ts +16 -0
  18. package/source/beta-components/button/core/types.ts +28 -0
  19. package/source/beta-components/button/index.ts +5 -0
  20. package/source/beta-components/button/react/Button.tsx +173 -0
  21. package/source/beta-components/button/react/locale.ts +5 -0
  22. package/source/beta-components/button/react/props.ts +23 -0
  23. package/source/beta-components/button/styles.css +233 -0
  24. package/source/beta-components/checkbox/README.md +36 -0
  25. package/source/beta-components/checkbox/__tests__/core.test.ts +40 -0
  26. package/source/beta-components/checkbox/core/group.ts +30 -0
  27. package/source/beta-components/checkbox/core/index.ts +3 -0
  28. package/source/beta-components/checkbox/core/types.ts +17 -0
  29. package/source/beta-components/checkbox/index.ts +9 -0
  30. package/source/beta-components/checkbox/react/Checkbox.tsx +147 -0
  31. package/source/beta-components/checkbox/react/CheckboxGroup.tsx +149 -0
  32. package/source/beta-components/checkbox/react/context.ts +16 -0
  33. package/source/beta-components/checkbox/react/locale.ts +5 -0
  34. package/source/beta-components/checkbox/react/props.ts +6 -0
  35. package/source/beta-components/checkbox/styles.css +212 -0
  36. package/source/beta-components/combobox/README.md +3 -3
  37. package/source/beta-components/combobox/react/MultiSelect.tsx +265 -263
  38. package/source/beta-components/combobox/react/Popover.tsx +38 -70
  39. package/source/beta-components/combobox/react/Select.tsx +200 -198
  40. package/source/beta-components/combobox/react/icons.tsx +6 -29
  41. package/source/beta-components/combobox/react/props.ts +4 -2
  42. package/source/beta-components/combobox/styles.css +18 -18
  43. package/source/beta-components/data-grid/react/Popover.tsx +35 -0
  44. package/source/beta-components/{datagrid → data-grid}/react/context.ts +2 -2
  45. package/source/beta-components/{datagrid → data-grid}/react/icons.tsx +8 -35
  46. package/source/beta-components/{datagrid → data-grid}/styles.css +24 -24
  47. package/source/beta-components/{datepicker → date-picker}/README.md +3 -3
  48. package/source/beta-components/{datepicker → date-picker}/react/DatePicker.tsx +249 -249
  49. package/source/beta-components/{datepicker → date-picker}/react/DateRangePicker.tsx +291 -291
  50. package/source/beta-components/date-picker/react/Popover.tsx +45 -0
  51. package/source/beta-components/date-picker/react/icons.tsx +15 -0
  52. package/source/beta-components/{datepicker → date-picker}/react/props.ts +2 -2
  53. package/source/beta-components/{datepicker → date-picker}/styles.css +18 -18
  54. package/source/beta-components/dialog/react/icons.tsx +16 -47
  55. package/source/beta-components/dialog/react/props.ts +2 -2
  56. package/source/beta-components/dialog/styles.css +21 -21
  57. package/source/beta-components/{fileuploader → file-uploader}/react/FileUploader.tsx +511 -515
  58. package/source/beta-components/{fileuploader → file-uploader}/react/icons.tsx +7 -27
  59. package/source/beta-components/{fileuploader → file-uploader}/react/props.ts +2 -2
  60. package/source/beta-components/{fileuploader → file-uploader}/styles.css +17 -17
  61. package/source/beta-components/input/core/count.ts +3 -12
  62. package/source/beta-components/input/react/Input.tsx +219 -219
  63. package/source/beta-components/input/react/OtpInput.tsx +256 -256
  64. package/source/beta-components/input/react/icons.tsx +3 -21
  65. package/source/beta-components/input/react/props.ts +2 -2
  66. package/source/beta-components/input/styles.css +15 -15
  67. package/source/beta-components/modal/react/Modal.tsx +242 -238
  68. package/source/beta-components/modal/react/icons.tsx +1 -19
  69. package/source/beta-components/modal/react/props.ts +2 -2
  70. package/source/beta-components/modal/styles.css +17 -13
  71. package/source/beta-components/shared/README.md +54 -0
  72. package/source/beta-components/shared/__tests__/boundaries.test.ts +95 -0
  73. package/source/beta-components/shared/__tests__/core.test.ts +55 -0
  74. package/source/beta-components/shared/core/cx.ts +9 -0
  75. package/source/beta-components/shared/core/field.ts +35 -0
  76. package/source/beta-components/shared/core/text.ts +14 -0
  77. package/source/beta-components/shared/index.ts +7 -0
  78. package/source/beta-components/shared/react/Popover.tsx +147 -0
  79. package/source/beta-components/shared/react/icons.tsx +74 -0
  80. package/source/beta-components/shared/react/useControllableState.ts +42 -0
  81. package/source/beta-components/shared/version.json +4 -0
  82. package/source/beta-components/shared/version.ts +6 -0
  83. package/source/beta-components/spinner/README.md +29 -0
  84. package/source/beta-components/spinner/__tests__/core.test.ts +48 -0
  85. package/source/beta-components/spinner/core/index.ts +3 -0
  86. package/source/beta-components/spinner/core/types.ts +28 -0
  87. package/source/beta-components/spinner/core/visibility.ts +34 -0
  88. package/source/beta-components/spinner/index.ts +6 -0
  89. package/source/beta-components/spinner/react/Spinner.tsx +96 -0
  90. package/source/beta-components/spinner/react/locale.ts +5 -0
  91. package/source/beta-components/spinner/react/props.ts +4 -0
  92. package/source/beta-components/spinner/react/useDelayedLoading.ts +39 -0
  93. package/source/beta-components/spinner/styles.css +167 -0
  94. package/source/beta-components/tabs/README.md +32 -0
  95. package/source/beta-components/tabs/__tests__/core.test.ts +51 -0
  96. package/source/beta-components/tabs/core/index.ts +3 -0
  97. package/source/beta-components/tabs/core/navigation.ts +54 -0
  98. package/source/beta-components/tabs/core/types.ts +16 -0
  99. package/source/beta-components/tabs/index.ts +5 -0
  100. package/source/beta-components/tabs/react/Tabs.tsx +258 -0
  101. package/source/beta-components/tabs/react/context.ts +21 -0
  102. package/source/beta-components/tabs/styles.css +257 -0
  103. package/source/beta-components/textarea/core/count.ts +3 -12
  104. package/source/beta-components/textarea/react/Textarea.tsx +208 -209
  105. package/source/beta-components/textarea/react/props.ts +2 -2
  106. package/source/beta-components/textarea/styles.css +12 -12
  107. package/source/beta-components/toaster/react/icons.tsx +19 -52
  108. package/source/beta-components/toaster/react/props.ts +2 -2
  109. package/source/beta-components/toaster/styles.css +18 -18
  110. package/source/beta-components/datagrid/react/Popover.tsx +0 -79
  111. package/source/beta-components/datepicker/react/Popover.tsx +0 -87
  112. package/source/beta-components/datepicker/react/icons.tsx +0 -32
  113. /package/source/beta-components/{datagrid → data-grid}/README.md +0 -0
  114. /package/source/beta-components/{datagrid → data-grid}/__tests__/core.test.ts +0 -0
  115. /package/source/beta-components/{datagrid → data-grid}/__tests__/export.test.ts +0 -0
  116. /package/source/beta-components/{datagrid → data-grid}/core/columnHelper.ts +0 -0
  117. /package/source/beta-components/{datagrid → data-grid}/core/columns.ts +0 -0
  118. /package/source/beta-components/{datagrid → data-grid}/core/filtering.ts +0 -0
  119. /package/source/beta-components/{datagrid → data-grid}/core/grid.ts +0 -0
  120. /package/source/beta-components/{datagrid → data-grid}/core/index.ts +0 -0
  121. /package/source/beta-components/{datagrid → data-grid}/core/rows.ts +0 -0
  122. /package/source/beta-components/{datagrid → data-grid}/core/sorting.ts +0 -0
  123. /package/source/beta-components/{datagrid → data-grid}/core/state.ts +0 -0
  124. /package/source/beta-components/{datagrid → data-grid}/core/store.ts +0 -0
  125. /package/source/beta-components/{datagrid → data-grid}/core/types.ts +0 -0
  126. /package/source/beta-components/{datagrid → data-grid}/core/values.ts +0 -0
  127. /package/source/beta-components/{datagrid → data-grid}/core/virtual.ts +0 -0
  128. /package/source/beta-components/{datagrid → data-grid}/export/csv.ts +0 -0
  129. /package/source/beta-components/{datagrid → data-grid}/export/download.ts +0 -0
  130. /package/source/beta-components/{datagrid → data-grid}/export/pdf.ts +0 -0
  131. /package/source/beta-components/{datagrid → data-grid}/export/table.ts +0 -0
  132. /package/source/beta-components/{datagrid → data-grid}/export/xlsx.ts +0 -0
  133. /package/source/beta-components/{datagrid → data-grid}/export/zip.ts +0 -0
  134. /package/source/beta-components/{datagrid → data-grid}/index.ts +0 -0
  135. /package/source/beta-components/{datagrid → data-grid}/react/Cell.tsx +0 -0
  136. /package/source/beta-components/{datagrid → data-grid}/react/ColumnMenu.tsx +0 -0
  137. /package/source/beta-components/{datagrid → data-grid}/react/DataGrid.tsx +0 -0
  138. /package/source/beta-components/{datagrid → data-grid}/react/FilterForm.tsx +0 -0
  139. /package/source/beta-components/{datagrid → data-grid}/react/HeaderRow.tsx +0 -0
  140. /package/source/beta-components/{datagrid → data-grid}/react/Pagination.tsx +0 -0
  141. /package/source/beta-components/{datagrid → data-grid}/react/Row.tsx +0 -0
  142. /package/source/beta-components/{datagrid → data-grid}/react/Toolbar.tsx +0 -0
  143. /package/source/beta-components/{datagrid → data-grid}/react/Viewport.tsx +0 -0
  144. /package/source/beta-components/{datagrid → data-grid}/react/hooks.ts +0 -0
  145. /package/source/beta-components/{datagrid → data-grid}/react/keyboard.ts +0 -0
  146. /package/source/beta-components/{datagrid → data-grid}/react/locale.ts +0 -0
  147. /package/source/beta-components/{datepicker → date-picker}/__tests__/core.test.ts +0 -0
  148. /package/source/beta-components/{datepicker → date-picker}/core/calendar.ts +0 -0
  149. /package/source/beta-components/{datepicker → date-picker}/core/format.ts +0 -0
  150. /package/source/beta-components/{datepicker → date-picker}/core/index.ts +0 -0
  151. /package/source/beta-components/{datepicker → date-picker}/core/types.ts +0 -0
  152. /package/source/beta-components/{datepicker → date-picker}/index.ts +0 -0
  153. /package/source/beta-components/{datepicker → date-picker}/react/Calendar.tsx +0 -0
  154. /package/source/beta-components/{datepicker → date-picker}/react/CalendarPanel.tsx +0 -0
  155. /package/source/beta-components/{datepicker → date-picker}/react/MonthYearPanels.tsx +0 -0
  156. /package/source/beta-components/{datepicker → date-picker}/react/locale.ts +0 -0
  157. /package/source/beta-components/{datepicker → date-picker}/react/useDatePicker.ts +0 -0
  158. /package/source/beta-components/{fileuploader → file-uploader}/README.md +0 -0
  159. /package/source/beta-components/{fileuploader → file-uploader}/__tests__/core.test.ts +0 -0
  160. /package/source/beta-components/{fileuploader → file-uploader}/core/format.ts +0 -0
  161. /package/source/beta-components/{fileuploader → file-uploader}/core/index.ts +0 -0
  162. /package/source/beta-components/{fileuploader → file-uploader}/core/store.ts +0 -0
  163. /package/source/beta-components/{fileuploader → file-uploader}/core/transport.ts +0 -0
  164. /package/source/beta-components/{fileuploader → file-uploader}/core/types.ts +0 -0
  165. /package/source/beta-components/{fileuploader → file-uploader}/core/validate.ts +0 -0
  166. /package/source/beta-components/{fileuploader → file-uploader}/index.ts +0 -0
  167. /package/source/beta-components/{fileuploader → file-uploader}/react/FileRow.tsx +0 -0
  168. /package/source/beta-components/{fileuploader → file-uploader}/react/locale.ts +0 -0
  169. /package/source/beta-components/{fileuploader → file-uploader}/react/useFileUploader.ts +0 -0
@@ -1,263 +1,265 @@
1
- "use client";
2
-
3
- import { useId, useImperativeHandle, useMemo, useRef, type Ref } from "react";
4
- import type { ComboboxOption, OptionValue } from "../core/types";
5
- import { ChevronIcon, XIcon } from "./icons";
6
- import { Listbox } from "./Listbox";
7
- import { defaultComboboxText } from "./locale";
8
- import { ComboboxPopover } from "./Popover";
9
- import { cx, type ComboboxHandle, type ComboboxSharedProps } from "./props";
10
- import { useCombobox } from "./useCombobox";
11
-
12
- export interface MultiSelectProps<V extends OptionValue = string> extends ComboboxSharedProps<V> {
13
- value?: V[];
14
- defaultValue?: V[];
15
- onChange?(values: NoInfer<V>[], options: ComboboxOption<NoInfer<V>>[]): void;
16
- /** Maximum number of selections. */
17
- max?: number;
18
- /** Tags shown before collapsing into "+N more". Default 3. */
19
- maxVisibleTags?: number;
20
- /** Show "Select all" / "Clear all" under the list. Default true. */
21
- showSelectAll?: boolean;
22
- /** Close the list after each choice. Default false. */
23
- closeOnSelect?: boolean;
24
- ref?: Ref<ComboboxHandle<V>>;
25
- }
26
-
27
- /** Multi-select combobox: tags, option search and select-all. */
28
- export function MultiSelect<V extends OptionValue = string>(props: MultiSelectProps<V>) {
29
- const {
30
- ref,
31
- value,
32
- defaultValue,
33
- onChange,
34
- max,
35
- maxVisibleTags = 3,
36
- showSelectAll = true,
37
- closeOnSelect = false,
38
- options,
39
- label,
40
- placeholder = "Select…",
41
- description,
42
- error,
43
- required = false,
44
- disabled = false,
45
- clearable = true,
46
- size = "md",
47
- name,
48
- id: idProp,
49
- className,
50
- classNames,
51
- style,
52
- localeText,
53
- maxHeight = 280,
54
- virtualize = true,
55
- emptyMessage,
56
- searchable = true,
57
- loading = false,
58
- hasMore,
59
- onLoadMore,
60
- renderOption,
61
- } = props;
62
-
63
- const reactId = useId();
64
- const id = idProp ?? reactId;
65
- const text = useMemo(() => ({ ...defaultComboboxText, ...localeText }), [localeText]);
66
-
67
- const controlRef = useRef<HTMLDivElement>(null);
68
- const searchRef = useRef<HTMLInputElement>(null);
69
- const listRef = useRef<HTMLDivElement>(null);
70
-
71
- const combobox = useCombobox<V>({
72
- controlRef,
73
- searchRef,
74
- listRef,
75
- ...props,
76
- multiple: true,
77
- closeOnSelect,
78
- max,
79
- value,
80
- defaultValue: defaultValue ?? [],
81
- onValuesChange: (values, selectedOptions) => onChange?.(values, selectedOptions),
82
- text,
83
- id,
84
- });
85
-
86
- const { selectedOptions } = combobox;
87
- const hidden = Math.max(0, selectedOptions.length - maxVisibleTags);
88
- const visibleTags = hidden > 0 ? selectedOptions.slice(0, maxVisibleTags) : selectedOptions;
89
-
90
- useImperativeHandle(ref, () => combobox.handle, [combobox.handle]);
91
-
92
- const selectableVisible = combobox.entries.filter(
93
- (entry) => !entry.option.disabled && !combobox.isSelected(entry.option.value),
94
- );
95
- const canSelectAll =
96
- showSelectAll &&
97
- selectableVisible.length > 0 &&
98
- (max === undefined || selectedOptions.length + selectableVisible.length <= max);
99
-
100
- return (
101
- <div
102
- className={cx("cb-root", classNames?.root, className)}
103
- style={style}
104
- data-size={size}
105
- data-state={combobox.open ? "open" : "closed"}
106
- data-invalid={error ? "" : undefined}
107
- >
108
- {label && (
109
- <span id={`${id}-label`} className={cx("cb-label", classNames?.label)} data-required={required || undefined}>
110
- {label}
111
- </span>
112
- )}
113
-
114
- <div
115
- ref={controlRef}
116
- id={id}
117
- role="combobox"
118
- tabIndex={disabled ? -1 : 0}
119
- aria-expanded={combobox.open}
120
- aria-haspopup="listbox"
121
- aria-controls={combobox.open ? combobox.listboxId : undefined}
122
- aria-labelledby={label ? `${id}-label` : undefined}
123
- aria-describedby={cx(description ? `${id}-description` : "", error ? `${id}-error` : "") || undefined}
124
- aria-required={required || undefined}
125
- aria-invalid={error ? true : undefined}
126
- aria-disabled={disabled || undefined}
127
- className={cx("cb-control", classNames?.control)}
128
- data-disabled={disabled || undefined}
129
- data-invalid={error ? "" : undefined}
130
- onPointerDown={combobox.onControlPointerDown}
131
- onClick={combobox.onControlClick}
132
- onKeyDown={combobox.onKeyDown}
133
- >
134
- {selectedOptions.length === 0 ? (
135
- <span className={cx("cb-value", classNames?.value)}>
136
- <span className="cb-placeholder">{placeholder}</span>
137
- </span>
138
- ) : (
139
- <span className="cb-tags">
140
- {visibleTags.map((option) => (
141
- <span key={String(option.value)} className={cx("cb-tag", classNames?.tag)}>
142
- <span className="cb-tag-label">{option.label}</span>
143
- {!disabled && (
144
- <button
145
- type="button"
146
- tabIndex={-1}
147
- className="cb-tag-remove"
148
- aria-label={text.removeOption(option.label)}
149
- onClick={(event) => {
150
- event.stopPropagation();
151
- combobox.removeValue(option.value);
152
- }}
153
- >
154
- <XIcon width={12} height={12} />
155
- </button>
156
- )}
157
- </span>
158
- ))}
159
- {hidden > 0 && <span className="cb-tag-overflow">{text.moreCount(String(hidden))}</span>}
160
- </span>
161
- )}
162
-
163
- <span className="cb-actions">
164
- {clearable && selectedOptions.length > 0 && !disabled && (
165
- <button
166
- type="button"
167
- tabIndex={-1}
168
- className="cb-icon-button"
169
- aria-label={text.clearAll}
170
- onClick={(event) => {
171
- event.stopPropagation();
172
- combobox.clear();
173
- }}
174
- >
175
- <XIcon width={14} height={14} />
176
- </button>
177
- )}
178
- <ChevronIcon className="cb-chevron" />
179
- </span>
180
- </div>
181
-
182
- {name &&
183
- selectedOptions.map((option) => (
184
- <input key={String(option.value)} type="hidden" name={name} value={option.value} />
185
- ))}
186
- {description && (
187
- <span id={`${id}-description`} className="cb-description">
188
- {description}
189
- </span>
190
- )}
191
- {error && (
192
- <span id={`${id}-error`} className="cb-error" role="alert">
193
- {error}
194
- </span>
195
- )}
196
-
197
- {combobox.anchor && (
198
- <ComboboxPopover
199
- anchor={combobox.anchor}
200
- onClose={combobox.onPopoverClose}
201
- className={classNames?.popover}
202
- >
203
- <Listbox<V>
204
- listboxId={combobox.listboxId}
205
- labelledBy={label ? `${id}-label` : id}
206
- items={combobox.items}
207
- optionCount={combobox.entries.length}
208
- activeIndex={combobox.activeIndex}
209
- multiple
210
- size={size}
211
- maxHeight={maxHeight}
212
- virtualize={virtualize}
213
- loading={loading}
214
- hasMore={hasMore}
215
- searchable={searchable}
216
- search={combobox.search}
217
- searchRef={searchRef}
218
- listRef={listRef}
219
- text={text}
220
- classNames={classNames}
221
- emptyMessage={emptyMessage}
222
- createLabel={combobox.createLabel}
223
- getOptionId={combobox.getOptionId}
224
- isSelected={combobox.isSelected}
225
- onSelect={combobox.selectOption}
226
- onActiveIndexChange={combobox.setActiveIndex}
227
- onSearchChange={combobox.setSearch}
228
- onKeyDown={combobox.onKeyDown}
229
- onLoadMore={onLoadMore}
230
- onCreate={combobox.commitCreate}
231
- renderOption={renderOption}
232
- footer={
233
- <>
234
- <span className="cb-footer-count">
235
- {max !== undefined && selectedOptions.length >= max
236
- ? text.maxReached(String(max))
237
- : text.selectedCount(String(selectedOptions.length))}
238
- </span>
239
- <span>
240
- {canSelectAll && (
241
- <button
242
- type="button"
243
- className="cb-link-button"
244
- onClick={() => combobox.selectAllVisible()}
245
- >
246
- {text.selectAll}
247
- </button>
248
- )}
249
- {selectedOptions.length > 0 && (
250
- <button type="button" className="cb-link-button" onClick={() => combobox.clear()}>
251
- {text.clearAll}
252
- </button>
253
- )}
254
- </span>
255
- </>
256
- }
257
- />
258
- </ComboboxPopover>
259
- )}
260
- {options.length === 0 && loading && <span className="cb-sr-only">{text.loading}</span>}
261
- </div>
262
- );
263
- }
1
+ "use client";
2
+
3
+ import { useId, useImperativeHandle, useMemo, useRef, type Ref } from "react";
4
+ import { describeField } from "../../shared/core/field";
5
+ import type { ComboboxOption, OptionValue } from "../core/types";
6
+ import { ChevronIcon, XIcon } from "./icons";
7
+ import { Listbox } from "./Listbox";
8
+ import { defaultComboboxText } from "./locale";
9
+ import { ComboboxPopover } from "./Popover";
10
+ import { cx, type ComboboxHandle, type ComboboxSharedProps } from "./props";
11
+ import { useCombobox } from "./useCombobox";
12
+
13
+ export interface MultiSelectProps<V extends OptionValue = string> extends ComboboxSharedProps<V> {
14
+ value?: V[];
15
+ defaultValue?: V[];
16
+ onChange?(values: NoInfer<V>[], options: ComboboxOption<NoInfer<V>>[]): void;
17
+ /** Maximum number of selections. */
18
+ max?: number;
19
+ /** Tags shown before collapsing into "+N more". Default 3. */
20
+ maxVisibleTags?: number;
21
+ /** Show "Select all" / "Clear all" under the list. Default true. */
22
+ showSelectAll?: boolean;
23
+ /** Close the list after each choice. Default false. */
24
+ closeOnSelect?: boolean;
25
+ ref?: Ref<ComboboxHandle<V>>;
26
+ }
27
+
28
+ /** Multi-select combobox: tags, option search and select-all. */
29
+ export function MultiSelect<V extends OptionValue = string>(props: MultiSelectProps<V>) {
30
+ const {
31
+ ref,
32
+ value,
33
+ defaultValue,
34
+ onChange,
35
+ max,
36
+ maxVisibleTags = 3,
37
+ showSelectAll = true,
38
+ closeOnSelect = false,
39
+ options,
40
+ label,
41
+ placeholder = "Select…",
42
+ description,
43
+ error,
44
+ required = false,
45
+ disabled = false,
46
+ clearable = true,
47
+ size = "md",
48
+ name,
49
+ id: idProp,
50
+ className,
51
+ classNames,
52
+ style,
53
+ localeText,
54
+ maxHeight = 280,
55
+ virtualize = true,
56
+ emptyMessage,
57
+ searchable = true,
58
+ loading = false,
59
+ hasMore,
60
+ onLoadMore,
61
+ renderOption,
62
+ } = props;
63
+
64
+ const reactId = useId();
65
+ const id = idProp ?? reactId;
66
+ const text = useMemo(() => ({ ...defaultComboboxText, ...localeText }), [localeText]);
67
+
68
+ const controlRef = useRef<HTMLDivElement>(null);
69
+ const searchRef = useRef<HTMLInputElement>(null);
70
+ const listRef = useRef<HTMLDivElement>(null);
71
+
72
+ const combobox = useCombobox<V>({
73
+ controlRef,
74
+ searchRef,
75
+ listRef,
76
+ ...props,
77
+ multiple: true,
78
+ closeOnSelect,
79
+ max,
80
+ value,
81
+ defaultValue: defaultValue ?? [],
82
+ onValuesChange: (values, selectedOptions) => onChange?.(values, selectedOptions),
83
+ text,
84
+ id,
85
+ });
86
+
87
+ const { selectedOptions } = combobox;
88
+ const hidden = Math.max(0, selectedOptions.length - maxVisibleTags);
89
+ const visibleTags = hidden > 0 ? selectedOptions.slice(0, maxVisibleTags) : selectedOptions;
90
+
91
+ useImperativeHandle(ref, () => combobox.handle, [combobox.handle]);
92
+
93
+ const selectableVisible = combobox.entries.filter(
94
+ (entry) => !entry.option.disabled && !combobox.isSelected(entry.option.value),
95
+ );
96
+ const canSelectAll =
97
+ showSelectAll &&
98
+ selectableVisible.length > 0 &&
99
+ (max === undefined || selectedOptions.length + selectableVisible.length <= max);
100
+
101
+ return (
102
+ <div
103
+ className={cx("cb-root", classNames?.root, className)}
104
+ style={style}
105
+ data-size={size}
106
+ data-state={combobox.open ? "open" : "closed"}
107
+ data-invalid={error ? "" : undefined}
108
+ >
109
+ {label && (
110
+ <span id={`${id}-label`} className={cx("cb-label", classNames?.label)} data-required={required || undefined}>
111
+ {label}
112
+ </span>
113
+ )}
114
+
115
+ <div
116
+ ref={controlRef}
117
+ id={id}
118
+ role="combobox"
119
+ tabIndex={disabled ? -1 : 0}
120
+ aria-expanded={combobox.open}
121
+ aria-haspopup="listbox"
122
+ aria-controls={combobox.open ? combobox.listboxId : undefined}
123
+ aria-labelledby={label ? `${id}-label` : undefined}
124
+ aria-label={label ? undefined : props["aria-label"]}
125
+ aria-describedby={describeField(id, { description, error })}
126
+ aria-required={required || undefined}
127
+ aria-invalid={error ? true : undefined}
128
+ aria-disabled={disabled || undefined}
129
+ className={cx("cb-control", classNames?.control)}
130
+ data-disabled={disabled || undefined}
131
+ data-invalid={error ? "" : undefined}
132
+ onPointerDown={combobox.onControlPointerDown}
133
+ onClick={combobox.onControlClick}
134
+ onKeyDown={combobox.onKeyDown}
135
+ >
136
+ {selectedOptions.length === 0 ? (
137
+ <span className={cx("cb-value", classNames?.value)}>
138
+ <span className="cb-placeholder">{placeholder}</span>
139
+ </span>
140
+ ) : (
141
+ <span className="cb-tags">
142
+ {visibleTags.map((option) => (
143
+ <span key={String(option.value)} className={cx("cb-tag", classNames?.tag)}>
144
+ <span className="cb-tag-label">{option.label}</span>
145
+ {!disabled && (
146
+ <button
147
+ type="button"
148
+ tabIndex={-1}
149
+ className="cb-tag-remove"
150
+ aria-label={text.removeOption(option.label)}
151
+ onClick={(event) => {
152
+ event.stopPropagation();
153
+ combobox.removeValue(option.value);
154
+ }}
155
+ >
156
+ <XIcon width={12} height={12} />
157
+ </button>
158
+ )}
159
+ </span>
160
+ ))}
161
+ {hidden > 0 && <span className="cb-tag-overflow">{text.moreCount(String(hidden))}</span>}
162
+ </span>
163
+ )}
164
+
165
+ <span className="cb-actions">
166
+ {clearable && selectedOptions.length > 0 && !disabled && (
167
+ <button
168
+ type="button"
169
+ tabIndex={-1}
170
+ className="cb-icon-button"
171
+ aria-label={text.clearAll}
172
+ onClick={(event) => {
173
+ event.stopPropagation();
174
+ combobox.clear();
175
+ }}
176
+ >
177
+ <XIcon width={14} height={14} />
178
+ </button>
179
+ )}
180
+ <ChevronIcon className="cb-chevron" />
181
+ </span>
182
+ </div>
183
+
184
+ {name &&
185
+ selectedOptions.map((option) => (
186
+ <input key={String(option.value)} type="hidden" name={name} value={option.value} />
187
+ ))}
188
+ {description && (
189
+ <span id={`${id}-description`} className="cb-description">
190
+ {description}
191
+ </span>
192
+ )}
193
+ {error && (
194
+ <span id={`${id}-error`} className="cb-error" role="alert">
195
+ {error}
196
+ </span>
197
+ )}
198
+
199
+ {combobox.anchor && (
200
+ <ComboboxPopover
201
+ anchor={combobox.anchor}
202
+ onClose={combobox.onPopoverClose}
203
+ className={classNames?.popover}
204
+ >
205
+ <Listbox<V>
206
+ listboxId={combobox.listboxId}
207
+ labelledBy={label ? `${id}-label` : id}
208
+ items={combobox.items}
209
+ optionCount={combobox.entries.length}
210
+ activeIndex={combobox.activeIndex}
211
+ multiple
212
+ size={size}
213
+ maxHeight={maxHeight}
214
+ virtualize={virtualize}
215
+ loading={loading}
216
+ hasMore={hasMore}
217
+ searchable={searchable}
218
+ search={combobox.search}
219
+ searchRef={searchRef}
220
+ listRef={listRef}
221
+ text={text}
222
+ classNames={classNames}
223
+ emptyMessage={emptyMessage}
224
+ createLabel={combobox.createLabel}
225
+ getOptionId={combobox.getOptionId}
226
+ isSelected={combobox.isSelected}
227
+ onSelect={combobox.selectOption}
228
+ onActiveIndexChange={combobox.setActiveIndex}
229
+ onSearchChange={combobox.setSearch}
230
+ onKeyDown={combobox.onKeyDown}
231
+ onLoadMore={onLoadMore}
232
+ onCreate={combobox.commitCreate}
233
+ renderOption={renderOption}
234
+ footer={
235
+ <>
236
+ <span className="cb-footer-count">
237
+ {max !== undefined && selectedOptions.length >= max
238
+ ? text.maxReached(String(max))
239
+ : text.selectedCount(String(selectedOptions.length))}
240
+ </span>
241
+ <span>
242
+ {canSelectAll && (
243
+ <button
244
+ type="button"
245
+ className="cb-link-button"
246
+ onClick={() => combobox.selectAllVisible()}
247
+ >
248
+ {text.selectAll}
249
+ </button>
250
+ )}
251
+ {selectedOptions.length > 0 && (
252
+ <button type="button" className="cb-link-button" onClick={() => combobox.clear()}>
253
+ {text.clearAll}
254
+ </button>
255
+ )}
256
+ </span>
257
+ </>
258
+ }
259
+ />
260
+ </ComboboxPopover>
261
+ )}
262
+ {options.length === 0 && loading && <span className="cb-sr-only">{text.loading}</span>}
263
+ </div>
264
+ );
265
+ }
@@ -1,70 +1,38 @@
1
- "use client";
2
-
3
- import { useLayoutEffect, useRef, type ReactNode, type ToggleEvent } from "react";
4
- import { cx } from "./props";
5
-
6
- interface PopoverProps {
7
- anchor: HTMLElement;
8
- onClose(): void;
9
- children: ReactNode;
10
- className?: string;
11
- /** Distance between the anchor and the popover. */
12
- gap?: number;
13
- }
14
-
15
- /**
16
- * Anchored popover on the native Popover API: top layer (never clipped by a
17
- * scrolling parent or overflow), with light dismiss and Escape built in.
18
- * It matches the anchor's width and flips above when space is tight.
19
- */
20
- export function ComboboxPopover({ anchor, onClose, children, className, gap = 4 }: PopoverProps) {
21
- const ref = useRef<HTMLDivElement>(null);
22
-
23
- useLayoutEffect(() => {
24
- const el = ref.current;
25
- if (!el) return;
26
-
27
- const position = () => {
28
- const rect = anchor.getBoundingClientRect();
29
- el.style.minWidth = `${rect.width}px`;
30
- const height = el.offsetHeight;
31
- const below = window.innerHeight - rect.bottom - gap - 8;
32
- const flip = height > below && rect.top - gap - 8 > below;
33
- const left = Math.min(Math.max(8, rect.left), window.innerWidth - el.offsetWidth - 8);
34
- el.style.left = `${left}px`;
35
- el.style.top = flip ? `${Math.max(8, rect.top - gap - height)}px` : `${rect.bottom + gap}px`;
36
- el.style.maxHeight = `${Math.max(140, flip ? rect.top - gap - 8 : below)}px`;
37
- };
38
-
39
- el.showPopover();
40
- position();
41
- // Focus once the popover is actually shown; a popover is display:none until then.
42
- el.querySelector<HTMLElement>("[data-autofocus], input, [tabindex]")?.focus({ preventScroll: true });
43
-
44
- // Follow both sizes: the list as options filter, and the control as tags wrap.
45
- const observer = new ResizeObserver(position);
46
- observer.observe(el);
47
- observer.observe(anchor);
48
- window.addEventListener("resize", position);
49
- window.addEventListener("scroll", position, true);
50
- return () => {
51
- observer.disconnect();
52
- window.removeEventListener("resize", position);
53
- window.removeEventListener("scroll", position, true);
54
- if (el.matches(":popover-open")) el.hidePopover();
55
- };
56
- }, [anchor, gap]);
57
-
58
- return (
59
- <div
60
- ref={ref}
61
- popover="auto"
62
- className={cx("cb-popover", className)}
63
- onToggle={(event: ToggleEvent<HTMLDivElement>) => {
64
- if (event.newState === "closed") onClose();
65
- }}
66
- >
67
- {children}
68
- </div>
69
- );
70
- }
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { AnchoredPopover } from "../../shared/react/Popover";
5
+ import { cx } from "./props";
6
+
7
+ interface PopoverProps {
8
+ anchor: HTMLElement;
9
+ onClose(): void;
10
+ children: ReactNode;
11
+ className?: string;
12
+ /** Distance between the anchor and the popover. */
13
+ gap?: number;
14
+ }
15
+
16
+ /**
17
+ * The listbox popover: as wide as the control, capped to the space available so
18
+ * the list scrolls rather than running off screen, and following both elements
19
+ * as the list filters and the control's tags wrap.
20
+ *
21
+ * No `label`, because the listbox inside carries its own role and name.
22
+ */
23
+ export function ComboboxPopover({ anchor, onClose, children, className, gap = 4 }: PopoverProps) {
24
+ return (
25
+ <AnchoredPopover
26
+ anchor={anchor}
27
+ onClose={onClose}
28
+ className={cx("cb-popover", className)}
29
+ gap={gap}
30
+ matchAnchorWidth
31
+ fitHeight
32
+ autoFocus="[data-autofocus], input, [tabindex]"
33
+ observeResize="both"
34
+ >
35
+ {children}
36
+ </AnchoredPopover>
37
+ );
38
+ }