@xsolla/xui-multi-select 0.148.1 → 0.149.0-pr270.1777888548

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -139,6 +139,51 @@ export default function DisabledMultiSelect() {
139
139
  }
140
140
  ```
141
141
 
142
+ ### External panel (B2B grouped select)
143
+
144
+ When the option list is rendered elsewhere (for example [`@xsolla/xui-b2b-group-select`](./b2b-group-select.md)), set **`dropdownMenu={false}`** so the control does not open the built-in list. Wire the same **`value`** / **`onChange`** to both components; use **`onTriggerPress`** to toggle your panel, **`menuOpen`** for chevron/open styling, and **`menuMinWidth`** (default **540**, aligned with `GROUP_SELECT_MIN_PANEL_WIDTH`) so the field matches the panel width.
145
+
146
+ ```tsx
147
+ import * as React from 'react';
148
+ import { MultiSelect } from '@xsolla/xui-multi-select';
149
+ import {
150
+ GroupSelect,
151
+ GROUP_SELECT_MIN_PANEL_WIDTH,
152
+ type GroupSelectGroup,
153
+ } from '@xsolla/xui-b2b-group-select';
154
+
155
+ const groups: GroupSelectGroup[] = [/* ... */];
156
+ const flatOptions = groups.flatMap((g) =>
157
+ g.items.map((it) => ({ value: it.id, label: it.label }))
158
+ );
159
+
160
+ export default function GroupedFieldShell() {
161
+ const [value, setValue] = React.useState<string[]>([]);
162
+ const [open, setOpen] = React.useState(false);
163
+
164
+ return (
165
+ <>
166
+ <MultiSelect
167
+ options={flatOptions}
168
+ value={value}
169
+ onChange={(v) => setValue(v.map(String))}
170
+ placeholder="Select regions"
171
+ size="sm"
172
+ dropdownMenu={false}
173
+ menuOpen={open}
174
+ menuMinWidth={GROUP_SELECT_MIN_PANEL_WIDTH}
175
+ onTriggerPress={() => setOpen((o) => !o)}
176
+ />
177
+ {open && (
178
+ <GroupSelect groups={groups} value={value} onChange={setValue} />
179
+ )}
180
+ </>
181
+ );
182
+ }
183
+ ```
184
+
185
+ Add backdrop, click-outside, and Escape handling in your layout as needed (see Storybook).
186
+
142
187
  ## API Reference
143
188
 
144
189
  ### MultiSelect
@@ -154,6 +199,10 @@ export default function DisabledMultiSelect() {
154
199
  | size | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` | `"md"` | Component size. |
155
200
  | label | `string` | - | Label above select. |
156
201
  | disabled | `boolean` | `false` | Disabled state. |
202
+ | dropdownMenu | `boolean` | `true` | When `false`, hides the built-in list; use with an external picker (e.g. B2B Group select). |
203
+ | onTriggerPress | `() => void` | - | When `dropdownMenu` is false: called when the user activates the field (toggle external panel). |
204
+ | menuOpen | `boolean` | `false` | When `dropdownMenu` is false: drives open/chevron state for the control. |
205
+ | menuMinWidth | `number` | `540` | When `dropdownMenu` is false: field `min-width` in px (aligned with `GroupSelect`). |
157
206
 
158
207
  ## Display Behavior
159
208
 
@@ -83,6 +83,29 @@ interface MultiSelectProps extends ThemeOverrideProps {
83
83
  * @default 300
84
84
  */
85
85
  maxHeight?: number;
86
+ /**
87
+ * When false, the built-in options list and backdrop are not shown and the control
88
+ * does not open on click. Use with an external picker (e.g. grouped select) wired
89
+ * to the same `value` / `onChange`.
90
+ * @default true
91
+ */
92
+ dropdownMenu?: boolean;
93
+ /**
94
+ * When `dropdownMenu` is false: fired when the user activates the field (same gesture
95
+ * that would open the built-in list). Typically toggle an external panel.
96
+ */
97
+ onTriggerPress?: () => void;
98
+ /**
99
+ * When `dropdownMenu` is false: whether an external menu/panel is open — drives
100
+ * chevron direction and control layering like the built-in open state.
101
+ */
102
+ menuOpen?: boolean;
103
+ /**
104
+ * When `dropdownMenu` is false: `min-width` of the field in px so it aligns with
105
+ * a typical grouped panel (default **540**, same as `GroupSelect`). Use `0` for
106
+ * no minimum. Ignored when the built-in dropdown is enabled.
107
+ */
108
+ menuMinWidth?: number;
86
109
  }
87
110
 
88
111
  declare const MultiSelect: react.ForwardRefExoticComponent<MultiSelectProps & react.RefAttributes<HTMLDivElement>>;
package/native/index.d.ts CHANGED
@@ -83,6 +83,29 @@ interface MultiSelectProps extends ThemeOverrideProps {
83
83
  * @default 300
84
84
  */
85
85
  maxHeight?: number;
86
+ /**
87
+ * When false, the built-in options list and backdrop are not shown and the control
88
+ * does not open on click. Use with an external picker (e.g. grouped select) wired
89
+ * to the same `value` / `onChange`.
90
+ * @default true
91
+ */
92
+ dropdownMenu?: boolean;
93
+ /**
94
+ * When `dropdownMenu` is false: fired when the user activates the field (same gesture
95
+ * that would open the built-in list). Typically toggle an external panel.
96
+ */
97
+ onTriggerPress?: () => void;
98
+ /**
99
+ * When `dropdownMenu` is false: whether an external menu/panel is open — drives
100
+ * chevron direction and control layering like the built-in open state.
101
+ */
102
+ menuOpen?: boolean;
103
+ /**
104
+ * When `dropdownMenu` is false: `min-width` of the field in px so it aligns with
105
+ * a typical grouped panel (default **540**, same as `GroupSelect`). Use `0` for
106
+ * no minimum. Ignored when the built-in dropdown is enabled.
107
+ */
108
+ menuMinWidth?: number;
86
109
  }
87
110
 
88
111
  declare const MultiSelect: react.ForwardRefExoticComponent<MultiSelectProps & react.RefAttributes<HTMLDivElement>>;
package/native/index.js CHANGED
@@ -1434,6 +1434,8 @@ var Box2 = import_react5.default.forwardRef(
1434
1434
  as,
1435
1435
  src,
1436
1436
  alt,
1437
+ onError,
1438
+ onLoad,
1437
1439
  type,
1438
1440
  disabled,
1439
1441
  id,
@@ -1447,6 +1449,8 @@ var Box2 = import_react5.default.forwardRef(
1447
1449
  {
1448
1450
  src,
1449
1451
  alt: alt || "",
1452
+ onError,
1453
+ onLoad,
1450
1454
  style: {
1451
1455
  display: "block",
1452
1456
  objectFit: "cover",
@@ -1922,6 +1926,7 @@ var MultiSelectControl = (0, import_react8.forwardRef)(
1922
1926
  iconRight,
1923
1927
  disabled = false,
1924
1928
  extraClear = false,
1929
+ width,
1925
1930
  themeMode,
1926
1931
  themeProductContext
1927
1932
  }, ref) => {
@@ -1970,6 +1975,7 @@ var MultiSelectControl = (0, import_react8.forwardRef)(
1970
1975
  Box,
1971
1976
  {
1972
1977
  ref,
1978
+ width,
1973
1979
  backgroundColor,
1974
1980
  borderColor,
1975
1981
  borderWidth: borderColor !== "transparent" ? 1 : 0,
@@ -1981,7 +1987,7 @@ var MultiSelectControl = (0, import_react8.forwardRef)(
1981
1987
  alignItems: "center",
1982
1988
  gap: 8,
1983
1989
  style: {
1984
- cursor: isDisable ? "not-allowed" : "pointer",
1990
+ cursor: isDisable ? "not-allowed" : onClick ? "pointer" : "default",
1985
1991
  boxSizing: "border-box",
1986
1992
  height: flexible ? "auto" : sizeStyles.height,
1987
1993
  position: "relative",
@@ -1989,7 +1995,7 @@ var MultiSelectControl = (0, import_react8.forwardRef)(
1989
1995
  // Above backdrop when open
1990
1996
  },
1991
1997
  onPress: isDisable ? void 0 : onClick,
1992
- hoverStyle: !isDisable && !isFocus && !isOpen && !isError ? {
1998
+ hoverStyle: !isDisable && onClick && !isFocus && !isOpen && !isError ? {
1993
1999
  backgroundColor: inputColors.bgHover,
1994
2000
  borderColor: inputColors.borderHover
1995
2001
  } : void 0,
@@ -2145,6 +2151,7 @@ var useMultiSelect = ({
2145
2151
 
2146
2152
  // src/MultiSelect.tsx
2147
2153
  var import_jsx_runtime733 = require("react/jsx-runtime");
2154
+ var EXTERNAL_MENU_MIN_WIDTH_DEFAULT = 540;
2148
2155
  var MultiSelect = (0, import_react10.forwardRef)(
2149
2156
  ({
2150
2157
  options,
@@ -2163,6 +2170,10 @@ var MultiSelect = (0, import_react10.forwardRef)(
2163
2170
  iconLeft,
2164
2171
  iconRight,
2165
2172
  maxHeight = 300,
2173
+ dropdownMenu = true,
2174
+ onTriggerPress,
2175
+ menuOpen = false,
2176
+ menuMinWidth,
2166
2177
  themeMode,
2167
2178
  themeProductContext
2168
2179
  }, ref) => {
@@ -2190,10 +2201,10 @@ var MultiSelect = (0, import_react10.forwardRef)(
2190
2201
  onChange
2191
2202
  });
2192
2203
  (0, import_react10.useEffect)(() => {
2193
- if (isDisable) {
2204
+ if (isDisable || !dropdownMenu) {
2194
2205
  onClose();
2195
2206
  }
2196
- }, [isDisable, onClose]);
2207
+ }, [isDisable, dropdownMenu, onClose]);
2197
2208
  const menuItems = options.map((opt) => {
2198
2209
  const id = String(opt.value);
2199
2210
  const checked = values.map(String).includes(id);
@@ -2210,138 +2221,155 @@ var MultiSelect = (0, import_react10.forwardRef)(
2210
2221
  const newValues = checked ? [...values, value2] : values.filter((v) => v !== value2);
2211
2222
  onChoose(newValues.map(String));
2212
2223
  };
2213
- return /* @__PURE__ */ (0, import_jsx_runtime733.jsxs)(Box, { flexDirection: "column", gap: sizeStyles.fieldGap, children: [
2214
- label && /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2215
- Text,
2216
- {
2217
- color: theme.colors.content.secondary,
2218
- fontSize: sizeStyles.fontSize - 2,
2219
- fontWeight: "500",
2220
- children: label
2221
- }
2222
- ),
2223
- /* @__PURE__ */ (0, import_jsx_runtime733.jsxs)(
2224
- Box,
2225
- {
2226
- ref,
2227
- style: {
2228
- position: "relative"
2229
- },
2230
- children: [
2231
- /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2232
- MultiSelectControl,
2233
- {
2234
- ref: controlRef,
2235
- isOpen,
2236
- isFocus,
2237
- isError,
2238
- size,
2239
- state,
2240
- disabled: isDisable,
2241
- onClick: onSelectClick,
2242
- removeValue: onRemove,
2243
- removeAllValues: onRemoveAll,
2244
- stateList,
2245
- selectedItems,
2246
- variant,
2247
- flexible,
2248
- placeholder,
2249
- removeTagsButtons,
2250
- iconLeft,
2251
- iconRight,
2252
- extraClear
2253
- }
2254
- ),
2255
- isOpen && !isDisable && /* @__PURE__ */ (0, import_jsx_runtime733.jsxs)(import_jsx_runtime733.Fragment, { children: [
2256
- /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2257
- Box,
2258
- {
2259
- style: {
2260
- position: "fixed",
2261
- top: 0,
2262
- left: 0,
2263
- right: 0,
2264
- bottom: 0,
2265
- zIndex: 999,
2266
- cursor: "default"
2267
- },
2268
- onPress: onClose
2269
- }
2270
- ),
2271
- /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2272
- Box,
2273
- {
2274
- ref: menuRef,
2275
- backgroundColor: theme.colors.background.secondary,
2276
- borderColor: theme.colors.border.secondary,
2277
- borderWidth: 1,
2278
- borderRadius: theme.radius.button,
2279
- paddingVertical: 4,
2280
- style: {
2281
- position: "absolute",
2282
- top: "100%",
2283
- left: 0,
2284
- right: 0,
2285
- marginTop: 4,
2286
- zIndex: 1001,
2287
- // Above control (1000) and backdrop (999)
2288
- boxShadow: theme.shadow.popover,
2289
- maxHeight,
2290
- overflowY: "auto"
2291
- },
2292
- children: menuItems.map((item, _index) => {
2293
- const brandColors = theme.colors.control.brand.primary;
2294
- const contentColors = theme.colors.content;
2295
- return /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2296
- Box,
2297
- {
2298
- paddingHorizontal: sizeStyles.paddingHorizontal,
2299
- paddingVertical: 8,
2300
- onPress: () => {
2301
- if (!item.disabled) {
2302
- handleItemToggle(item.id, !item.checked);
2303
- }
2304
- },
2305
- flexDirection: "row",
2306
- alignItems: "center",
2307
- justifyContent: "space-between",
2308
- backgroundColor: item.checked ? brandColors.bg : "transparent",
2309
- hoverStyle: !item.disabled && !item.checked ? {
2310
- backgroundColor: theme.colors.control.input.bgHover
2311
- } : void 0,
2312
- style: {
2313
- cursor: item.disabled ? "not-allowed" : "pointer",
2314
- opacity: item.disabled ? 0.5 : 1
2315
- },
2316
- children: /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2317
- Text,
2318
- {
2319
- color: item.checked ? contentColors.on.brand : theme.colors.content.secondary,
2320
- fontSize: sizeStyles.fontSize,
2321
- fontWeight: "400",
2322
- children: item.children
2323
- }
2324
- )
2224
+ const controlMenuOpen = dropdownMenu ? isOpen : Boolean(menuOpen);
2225
+ const controlOnClick = dropdownMenu ? onSelectClick : onTriggerPress;
2226
+ const externalFieldLayout = !dropdownMenu ? {
2227
+ width: "100%",
2228
+ minWidth: menuMinWidth ?? EXTERNAL_MENU_MIN_WIDTH_DEFAULT,
2229
+ boxSizing: "border-box"
2230
+ } : void 0;
2231
+ return /* @__PURE__ */ (0, import_jsx_runtime733.jsxs)(
2232
+ Box,
2233
+ {
2234
+ flexDirection: "column",
2235
+ gap: sizeStyles.fieldGap,
2236
+ style: externalFieldLayout,
2237
+ children: [
2238
+ label && /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2239
+ Text,
2240
+ {
2241
+ color: theme.colors.content.secondary,
2242
+ fontSize: sizeStyles.fontSize - 2,
2243
+ fontWeight: "500",
2244
+ children: label
2245
+ }
2246
+ ),
2247
+ /* @__PURE__ */ (0, import_jsx_runtime733.jsxs)(
2248
+ Box,
2249
+ {
2250
+ ref,
2251
+ style: {
2252
+ position: "relative",
2253
+ ...externalFieldLayout ? { width: "100%" } : {}
2254
+ },
2255
+ children: [
2256
+ /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2257
+ MultiSelectControl,
2258
+ {
2259
+ ref: controlRef,
2260
+ isOpen: controlMenuOpen,
2261
+ isFocus,
2262
+ isError,
2263
+ size,
2264
+ state,
2265
+ disabled: isDisable,
2266
+ onClick: controlOnClick,
2267
+ width: dropdownMenu ? void 0 : "100%",
2268
+ removeValue: onRemove,
2269
+ removeAllValues: onRemoveAll,
2270
+ stateList,
2271
+ selectedItems,
2272
+ variant,
2273
+ flexible,
2274
+ placeholder,
2275
+ removeTagsButtons,
2276
+ iconLeft,
2277
+ iconRight,
2278
+ extraClear
2279
+ }
2280
+ ),
2281
+ dropdownMenu && isOpen && !isDisable && /* @__PURE__ */ (0, import_jsx_runtime733.jsxs)(import_jsx_runtime733.Fragment, { children: [
2282
+ /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2283
+ Box,
2284
+ {
2285
+ style: {
2286
+ position: "fixed",
2287
+ top: 0,
2288
+ left: 0,
2289
+ right: 0,
2290
+ bottom: 0,
2291
+ zIndex: 999,
2292
+ cursor: "default"
2325
2293
  },
2326
- item.id
2327
- );
2328
- })
2329
- }
2330
- )
2331
- ] })
2332
- ]
2333
- }
2334
- ),
2335
- errorMessage && /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2336
- Text,
2337
- {
2338
- color: theme.colors.content.alert.primary,
2339
- fontSize: sizeStyles.fontSize - 2,
2340
- style: { lineHeight: sizeStyles.lineHeight + "px" },
2341
- children: errorMessage
2342
- }
2343
- )
2344
- ] });
2294
+ onPress: onClose
2295
+ }
2296
+ ),
2297
+ /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2298
+ Box,
2299
+ {
2300
+ ref: menuRef,
2301
+ backgroundColor: theme.colors.background.secondary,
2302
+ borderColor: theme.colors.border.secondary,
2303
+ borderWidth: 1,
2304
+ borderRadius: theme.radius.button,
2305
+ paddingVertical: 4,
2306
+ style: {
2307
+ position: "absolute",
2308
+ top: "100%",
2309
+ left: 0,
2310
+ right: 0,
2311
+ marginTop: 4,
2312
+ zIndex: 1001,
2313
+ // Above control (1000) and backdrop (999)
2314
+ boxShadow: theme.shadow.popover,
2315
+ maxHeight,
2316
+ overflowY: "auto"
2317
+ },
2318
+ children: menuItems.map((item, _index) => {
2319
+ const brandColors = theme.colors.control.brand.primary;
2320
+ const contentColors = theme.colors.content;
2321
+ return /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2322
+ Box,
2323
+ {
2324
+ paddingHorizontal: sizeStyles.paddingHorizontal,
2325
+ paddingVertical: 8,
2326
+ onPress: () => {
2327
+ if (!item.disabled) {
2328
+ handleItemToggle(item.id, !item.checked);
2329
+ }
2330
+ },
2331
+ flexDirection: "row",
2332
+ alignItems: "center",
2333
+ justifyContent: "space-between",
2334
+ backgroundColor: item.checked ? brandColors.bg : "transparent",
2335
+ hoverStyle: !item.disabled && !item.checked ? {
2336
+ backgroundColor: theme.colors.control.input.bgHover
2337
+ } : void 0,
2338
+ style: {
2339
+ cursor: item.disabled ? "not-allowed" : "pointer",
2340
+ opacity: item.disabled ? 0.5 : 1
2341
+ },
2342
+ children: /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2343
+ Text,
2344
+ {
2345
+ color: item.checked ? contentColors.on.brand : theme.colors.content.secondary,
2346
+ fontSize: sizeStyles.fontSize,
2347
+ fontWeight: "400",
2348
+ children: item.children
2349
+ }
2350
+ )
2351
+ },
2352
+ item.id
2353
+ );
2354
+ })
2355
+ }
2356
+ )
2357
+ ] })
2358
+ ]
2359
+ }
2360
+ ),
2361
+ errorMessage && /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2362
+ Text,
2363
+ {
2364
+ color: theme.colors.content.alert.primary,
2365
+ fontSize: sizeStyles.fontSize - 2,
2366
+ style: { lineHeight: sizeStyles.lineHeight + "px" },
2367
+ children: errorMessage
2368
+ }
2369
+ )
2370
+ ]
2371
+ }
2372
+ );
2345
2373
  }
2346
2374
  );
2347
2375
  MultiSelect.displayName = "MultiSelect";