@xsolla/xui-multi-select 0.149.1 → 0.150.0

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
@@ -1926,6 +1926,7 @@ var MultiSelectControl = (0, import_react8.forwardRef)(
1926
1926
  iconRight,
1927
1927
  disabled = false,
1928
1928
  extraClear = false,
1929
+ width,
1929
1930
  themeMode,
1930
1931
  themeProductContext
1931
1932
  }, ref) => {
@@ -1974,6 +1975,7 @@ var MultiSelectControl = (0, import_react8.forwardRef)(
1974
1975
  Box,
1975
1976
  {
1976
1977
  ref,
1978
+ width,
1977
1979
  backgroundColor,
1978
1980
  borderColor,
1979
1981
  borderWidth: borderColor !== "transparent" ? 1 : 0,
@@ -1985,7 +1987,7 @@ var MultiSelectControl = (0, import_react8.forwardRef)(
1985
1987
  alignItems: "center",
1986
1988
  gap: 8,
1987
1989
  style: {
1988
- cursor: isDisable ? "not-allowed" : "pointer",
1990
+ cursor: isDisable ? "not-allowed" : onClick ? "pointer" : "default",
1989
1991
  boxSizing: "border-box",
1990
1992
  height: flexible ? "auto" : sizeStyles.height,
1991
1993
  position: "relative",
@@ -1993,7 +1995,7 @@ var MultiSelectControl = (0, import_react8.forwardRef)(
1993
1995
  // Above backdrop when open
1994
1996
  },
1995
1997
  onPress: isDisable ? void 0 : onClick,
1996
- hoverStyle: !isDisable && !isFocus && !isOpen && !isError ? {
1998
+ hoverStyle: !isDisable && onClick && !isFocus && !isOpen && !isError ? {
1997
1999
  backgroundColor: inputColors.bgHover,
1998
2000
  borderColor: inputColors.borderHover
1999
2001
  } : void 0,
@@ -2149,6 +2151,7 @@ var useMultiSelect = ({
2149
2151
 
2150
2152
  // src/MultiSelect.tsx
2151
2153
  var import_jsx_runtime733 = require("react/jsx-runtime");
2154
+ var EXTERNAL_MENU_MIN_WIDTH_DEFAULT = 540;
2152
2155
  var MultiSelect = (0, import_react10.forwardRef)(
2153
2156
  ({
2154
2157
  options,
@@ -2167,6 +2170,10 @@ var MultiSelect = (0, import_react10.forwardRef)(
2167
2170
  iconLeft,
2168
2171
  iconRight,
2169
2172
  maxHeight = 300,
2173
+ dropdownMenu = true,
2174
+ onTriggerPress,
2175
+ menuOpen = false,
2176
+ menuMinWidth,
2170
2177
  themeMode,
2171
2178
  themeProductContext
2172
2179
  }, ref) => {
@@ -2194,10 +2201,10 @@ var MultiSelect = (0, import_react10.forwardRef)(
2194
2201
  onChange
2195
2202
  });
2196
2203
  (0, import_react10.useEffect)(() => {
2197
- if (isDisable) {
2204
+ if (isDisable || !dropdownMenu) {
2198
2205
  onClose();
2199
2206
  }
2200
- }, [isDisable, onClose]);
2207
+ }, [isDisable, dropdownMenu, onClose]);
2201
2208
  const menuItems = options.map((opt) => {
2202
2209
  const id = String(opt.value);
2203
2210
  const checked = values.map(String).includes(id);
@@ -2214,138 +2221,155 @@ var MultiSelect = (0, import_react10.forwardRef)(
2214
2221
  const newValues = checked ? [...values, value2] : values.filter((v) => v !== value2);
2215
2222
  onChoose(newValues.map(String));
2216
2223
  };
2217
- return /* @__PURE__ */ (0, import_jsx_runtime733.jsxs)(Box, { flexDirection: "column", gap: sizeStyles.fieldGap, children: [
2218
- label && /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2219
- Text,
2220
- {
2221
- color: theme.colors.content.secondary,
2222
- fontSize: sizeStyles.fontSize - 2,
2223
- fontWeight: "500",
2224
- children: label
2225
- }
2226
- ),
2227
- /* @__PURE__ */ (0, import_jsx_runtime733.jsxs)(
2228
- Box,
2229
- {
2230
- ref,
2231
- style: {
2232
- position: "relative"
2233
- },
2234
- children: [
2235
- /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2236
- MultiSelectControl,
2237
- {
2238
- ref: controlRef,
2239
- isOpen,
2240
- isFocus,
2241
- isError,
2242
- size,
2243
- state,
2244
- disabled: isDisable,
2245
- onClick: onSelectClick,
2246
- removeValue: onRemove,
2247
- removeAllValues: onRemoveAll,
2248
- stateList,
2249
- selectedItems,
2250
- variant,
2251
- flexible,
2252
- placeholder,
2253
- removeTagsButtons,
2254
- iconLeft,
2255
- iconRight,
2256
- extraClear
2257
- }
2258
- ),
2259
- isOpen && !isDisable && /* @__PURE__ */ (0, import_jsx_runtime733.jsxs)(import_jsx_runtime733.Fragment, { children: [
2260
- /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2261
- Box,
2262
- {
2263
- style: {
2264
- position: "fixed",
2265
- top: 0,
2266
- left: 0,
2267
- right: 0,
2268
- bottom: 0,
2269
- zIndex: 999,
2270
- cursor: "default"
2271
- },
2272
- onPress: onClose
2273
- }
2274
- ),
2275
- /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2276
- Box,
2277
- {
2278
- ref: menuRef,
2279
- backgroundColor: theme.colors.background.secondary,
2280
- borderColor: theme.colors.border.secondary,
2281
- borderWidth: 1,
2282
- borderRadius: theme.radius.button,
2283
- paddingVertical: 4,
2284
- style: {
2285
- position: "absolute",
2286
- top: "100%",
2287
- left: 0,
2288
- right: 0,
2289
- marginTop: 4,
2290
- zIndex: 1001,
2291
- // Above control (1000) and backdrop (999)
2292
- boxShadow: theme.shadow.popover,
2293
- maxHeight,
2294
- overflowY: "auto"
2295
- },
2296
- children: menuItems.map((item, _index) => {
2297
- const brandColors = theme.colors.control.brand.primary;
2298
- const contentColors = theme.colors.content;
2299
- return /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2300
- Box,
2301
- {
2302
- paddingHorizontal: sizeStyles.paddingHorizontal,
2303
- paddingVertical: 8,
2304
- onPress: () => {
2305
- if (!item.disabled) {
2306
- handleItemToggle(item.id, !item.checked);
2307
- }
2308
- },
2309
- flexDirection: "row",
2310
- alignItems: "center",
2311
- justifyContent: "space-between",
2312
- backgroundColor: item.checked ? brandColors.bg : "transparent",
2313
- hoverStyle: !item.disabled && !item.checked ? {
2314
- backgroundColor: theme.colors.control.input.bgHover
2315
- } : void 0,
2316
- style: {
2317
- cursor: item.disabled ? "not-allowed" : "pointer",
2318
- opacity: item.disabled ? 0.5 : 1
2319
- },
2320
- children: /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2321
- Text,
2322
- {
2323
- color: item.checked ? contentColors.on.brand : theme.colors.content.secondary,
2324
- fontSize: sizeStyles.fontSize,
2325
- fontWeight: "400",
2326
- children: item.children
2327
- }
2328
- )
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"
2329
2293
  },
2330
- item.id
2331
- );
2332
- })
2333
- }
2334
- )
2335
- ] })
2336
- ]
2337
- }
2338
- ),
2339
- errorMessage && /* @__PURE__ */ (0, import_jsx_runtime733.jsx)(
2340
- Text,
2341
- {
2342
- color: theme.colors.content.alert.primary,
2343
- fontSize: sizeStyles.fontSize - 2,
2344
- style: { lineHeight: sizeStyles.lineHeight + "px" },
2345
- children: errorMessage
2346
- }
2347
- )
2348
- ] });
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
+ );
2349
2373
  }
2350
2374
  );
2351
2375
  MultiSelect.displayName = "MultiSelect";