gbs-add-block 1.2.15 → 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 (129) hide show
  1. package/README.md +2 -2
  2. package/index.cjs +214 -21
  3. package/package.json +1 -1
  4. package/source/beta-components/breadcrumb/react/props.ts +2 -2
  5. package/source/beta-components/breadcrumb/styles.css +8 -8
  6. package/source/beta-components/button/react/props.ts +2 -2
  7. package/source/beta-components/button/styles.css +15 -15
  8. package/source/beta-components/checkbox/react/Checkbox.tsx +147 -146
  9. package/source/beta-components/checkbox/react/CheckboxGroup.tsx +8 -8
  10. package/source/beta-components/checkbox/react/props.ts +2 -2
  11. package/source/beta-components/checkbox/styles.css +11 -11
  12. package/source/beta-components/combobox/README.md +3 -3
  13. package/source/beta-components/combobox/react/MultiSelect.tsx +265 -263
  14. package/source/beta-components/combobox/react/Popover.tsx +38 -70
  15. package/source/beta-components/combobox/react/Select.tsx +200 -198
  16. package/source/beta-components/combobox/react/icons.tsx +6 -29
  17. package/source/beta-components/combobox/react/props.ts +4 -2
  18. package/source/beta-components/combobox/styles.css +18 -18
  19. package/source/beta-components/data-grid/react/Popover.tsx +35 -0
  20. package/source/beta-components/{datagrid → data-grid}/react/context.ts +2 -2
  21. package/source/beta-components/{datagrid → data-grid}/react/icons.tsx +8 -35
  22. package/source/beta-components/{datagrid → data-grid}/styles.css +24 -24
  23. package/source/beta-components/{datepicker → date-picker}/README.md +3 -3
  24. package/source/beta-components/{datepicker → date-picker}/react/DatePicker.tsx +249 -249
  25. package/source/beta-components/{datepicker → date-picker}/react/DateRangePicker.tsx +291 -291
  26. package/source/beta-components/date-picker/react/Popover.tsx +45 -0
  27. package/source/beta-components/date-picker/react/icons.tsx +15 -0
  28. package/source/beta-components/{datepicker → date-picker}/react/props.ts +2 -2
  29. package/source/beta-components/{datepicker → date-picker}/styles.css +18 -18
  30. package/source/beta-components/dialog/react/icons.tsx +16 -47
  31. package/source/beta-components/dialog/react/props.ts +2 -2
  32. package/source/beta-components/dialog/styles.css +21 -21
  33. package/source/beta-components/{fileuploader → file-uploader}/react/FileUploader.tsx +511 -515
  34. package/source/beta-components/{fileuploader → file-uploader}/react/icons.tsx +7 -27
  35. package/source/beta-components/{fileuploader → file-uploader}/react/props.ts +2 -2
  36. package/source/beta-components/{fileuploader → file-uploader}/styles.css +17 -17
  37. package/source/beta-components/input/core/count.ts +3 -12
  38. package/source/beta-components/input/react/Input.tsx +219 -219
  39. package/source/beta-components/input/react/OtpInput.tsx +256 -256
  40. package/source/beta-components/input/react/icons.tsx +3 -21
  41. package/source/beta-components/input/react/props.ts +2 -2
  42. package/source/beta-components/input/styles.css +15 -15
  43. package/source/beta-components/modal/react/Modal.tsx +242 -238
  44. package/source/beta-components/modal/react/icons.tsx +1 -19
  45. package/source/beta-components/modal/react/props.ts +2 -2
  46. package/source/beta-components/modal/styles.css +17 -13
  47. package/source/beta-components/shared/README.md +54 -0
  48. package/source/beta-components/shared/__tests__/boundaries.test.ts +95 -0
  49. package/source/beta-components/shared/__tests__/core.test.ts +55 -0
  50. package/source/beta-components/shared/core/cx.ts +9 -0
  51. package/source/beta-components/shared/core/field.ts +35 -0
  52. package/source/beta-components/shared/core/text.ts +14 -0
  53. package/source/beta-components/shared/index.ts +7 -0
  54. package/source/beta-components/shared/react/Popover.tsx +147 -0
  55. package/source/beta-components/shared/react/icons.tsx +74 -0
  56. package/source/beta-components/shared/react/useControllableState.ts +42 -0
  57. package/source/beta-components/shared/version.json +4 -0
  58. package/source/beta-components/shared/version.ts +6 -0
  59. package/source/beta-components/spinner/react/props.ts +2 -2
  60. package/source/beta-components/spinner/styles.css +5 -5
  61. package/source/beta-components/tabs/react/Tabs.tsx +258 -260
  62. package/source/beta-components/tabs/styles.css +15 -15
  63. package/source/beta-components/textarea/core/count.ts +3 -12
  64. package/source/beta-components/textarea/react/Textarea.tsx +208 -209
  65. package/source/beta-components/textarea/react/props.ts +2 -2
  66. package/source/beta-components/textarea/styles.css +12 -12
  67. package/source/beta-components/toaster/react/icons.tsx +19 -52
  68. package/source/beta-components/toaster/react/props.ts +2 -2
  69. package/source/beta-components/toaster/styles.css +18 -18
  70. package/source/beta-components/datagrid/react/Popover.tsx +0 -79
  71. package/source/beta-components/datepicker/react/Popover.tsx +0 -87
  72. package/source/beta-components/datepicker/react/icons.tsx +0 -32
  73. /package/source/beta-components/{datagrid → data-grid}/README.md +0 -0
  74. /package/source/beta-components/{datagrid → data-grid}/__tests__/core.test.ts +0 -0
  75. /package/source/beta-components/{datagrid → data-grid}/__tests__/export.test.ts +0 -0
  76. /package/source/beta-components/{datagrid → data-grid}/core/columnHelper.ts +0 -0
  77. /package/source/beta-components/{datagrid → data-grid}/core/columns.ts +0 -0
  78. /package/source/beta-components/{datagrid → data-grid}/core/filtering.ts +0 -0
  79. /package/source/beta-components/{datagrid → data-grid}/core/grid.ts +0 -0
  80. /package/source/beta-components/{datagrid → data-grid}/core/index.ts +0 -0
  81. /package/source/beta-components/{datagrid → data-grid}/core/rows.ts +0 -0
  82. /package/source/beta-components/{datagrid → data-grid}/core/sorting.ts +0 -0
  83. /package/source/beta-components/{datagrid → data-grid}/core/state.ts +0 -0
  84. /package/source/beta-components/{datagrid → data-grid}/core/store.ts +0 -0
  85. /package/source/beta-components/{datagrid → data-grid}/core/types.ts +0 -0
  86. /package/source/beta-components/{datagrid → data-grid}/core/values.ts +0 -0
  87. /package/source/beta-components/{datagrid → data-grid}/core/virtual.ts +0 -0
  88. /package/source/beta-components/{datagrid → data-grid}/export/csv.ts +0 -0
  89. /package/source/beta-components/{datagrid → data-grid}/export/download.ts +0 -0
  90. /package/source/beta-components/{datagrid → data-grid}/export/pdf.ts +0 -0
  91. /package/source/beta-components/{datagrid → data-grid}/export/table.ts +0 -0
  92. /package/source/beta-components/{datagrid → data-grid}/export/xlsx.ts +0 -0
  93. /package/source/beta-components/{datagrid → data-grid}/export/zip.ts +0 -0
  94. /package/source/beta-components/{datagrid → data-grid}/index.ts +0 -0
  95. /package/source/beta-components/{datagrid → data-grid}/react/Cell.tsx +0 -0
  96. /package/source/beta-components/{datagrid → data-grid}/react/ColumnMenu.tsx +0 -0
  97. /package/source/beta-components/{datagrid → data-grid}/react/DataGrid.tsx +0 -0
  98. /package/source/beta-components/{datagrid → data-grid}/react/FilterForm.tsx +0 -0
  99. /package/source/beta-components/{datagrid → data-grid}/react/HeaderRow.tsx +0 -0
  100. /package/source/beta-components/{datagrid → data-grid}/react/Pagination.tsx +0 -0
  101. /package/source/beta-components/{datagrid → data-grid}/react/Row.tsx +0 -0
  102. /package/source/beta-components/{datagrid → data-grid}/react/Toolbar.tsx +0 -0
  103. /package/source/beta-components/{datagrid → data-grid}/react/Viewport.tsx +0 -0
  104. /package/source/beta-components/{datagrid → data-grid}/react/hooks.ts +0 -0
  105. /package/source/beta-components/{datagrid → data-grid}/react/keyboard.ts +0 -0
  106. /package/source/beta-components/{datagrid → data-grid}/react/locale.ts +0 -0
  107. /package/source/beta-components/{datepicker → date-picker}/__tests__/core.test.ts +0 -0
  108. /package/source/beta-components/{datepicker → date-picker}/core/calendar.ts +0 -0
  109. /package/source/beta-components/{datepicker → date-picker}/core/format.ts +0 -0
  110. /package/source/beta-components/{datepicker → date-picker}/core/index.ts +0 -0
  111. /package/source/beta-components/{datepicker → date-picker}/core/types.ts +0 -0
  112. /package/source/beta-components/{datepicker → date-picker}/index.ts +0 -0
  113. /package/source/beta-components/{datepicker → date-picker}/react/Calendar.tsx +0 -0
  114. /package/source/beta-components/{datepicker → date-picker}/react/CalendarPanel.tsx +0 -0
  115. /package/source/beta-components/{datepicker → date-picker}/react/MonthYearPanels.tsx +0 -0
  116. /package/source/beta-components/{datepicker → date-picker}/react/locale.ts +0 -0
  117. /package/source/beta-components/{datepicker → date-picker}/react/useDatePicker.ts +0 -0
  118. /package/source/beta-components/{fileuploader → file-uploader}/README.md +0 -0
  119. /package/source/beta-components/{fileuploader → file-uploader}/__tests__/core.test.ts +0 -0
  120. /package/source/beta-components/{fileuploader → file-uploader}/core/format.ts +0 -0
  121. /package/source/beta-components/{fileuploader → file-uploader}/core/index.ts +0 -0
  122. /package/source/beta-components/{fileuploader → file-uploader}/core/store.ts +0 -0
  123. /package/source/beta-components/{fileuploader → file-uploader}/core/transport.ts +0 -0
  124. /package/source/beta-components/{fileuploader → file-uploader}/core/types.ts +0 -0
  125. /package/source/beta-components/{fileuploader → file-uploader}/core/validate.ts +0 -0
  126. /package/source/beta-components/{fileuploader → file-uploader}/index.ts +0 -0
  127. /package/source/beta-components/{fileuploader → file-uploader}/react/FileRow.tsx +0 -0
  128. /package/source/beta-components/{fileuploader → file-uploader}/react/locale.ts +0 -0
  129. /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
+ }