@xsolla/xui-select 0.175.0 → 0.176.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xsolla/xui-select",
3
- "version": "0.175.0",
3
+ "version": "0.176.1",
4
4
  "main": "./web/index.js",
5
5
  "module": "./web/index.mjs",
6
6
  "types": "./web/index.d.ts",
@@ -13,13 +13,14 @@
13
13
  "test:coverage": "vitest run --coverage"
14
14
  },
15
15
  "dependencies": {
16
- "@xsolla/xui-core": "0.175.0",
17
- "@xsolla/xui-hooks": "0.175.0",
18
- "@xsolla/xui-icons-base": "0.175.0",
19
- "@xsolla/xui-primitives-core": "0.175.0"
16
+ "@xsolla/xui-core": "0.176.1",
17
+ "@xsolla/xui-hooks": "0.176.1",
18
+ "@xsolla/xui-icons-base": "0.176.1",
19
+ "@xsolla/xui-primitives-core": "0.176.1"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "react": ">=16.8.0",
23
+ "react-dom": ">=16.8.0",
23
24
  "styled-components": ">=4"
24
25
  },
25
26
  "devDependencies": {
package/web/index.js CHANGED
@@ -35,7 +35,7 @@ __export(index_exports, {
35
35
  module.exports = __toCommonJS(index_exports);
36
36
 
37
37
  // src/Select.tsx
38
- var import_react3 = require("react");
38
+ var import_react4 = require("react");
39
39
 
40
40
  // ../../foundation/primitives-web/src/Box.tsx
41
41
  var import_react2 = __toESM(require("react"));
@@ -361,6 +361,18 @@ var isNative = false;
361
361
  var import_xui_core = require("@xsolla/xui-core");
362
362
  var import_xui_hooks = require("@xsolla/xui-hooks");
363
363
  var import_xui_icons_base = require("@xsolla/xui-icons-base");
364
+
365
+ // src/Portal.web.tsx
366
+ var import_react3 = require("react");
367
+ var import_react_dom = require("react-dom");
368
+ var Portal = ({ children }) => {
369
+ const [mountNode, setMountNode] = (0, import_react3.useState)(null);
370
+ (0, import_react3.useEffect)(() => setMountNode(document.body), []);
371
+ if (!mountNode) return null;
372
+ return (0, import_react_dom.createPortal)(children, mountNode);
373
+ };
374
+
375
+ // src/Select.tsx
364
376
  var import_jsx_runtime4 = require("react/jsx-runtime");
365
377
  var Select = ({
366
378
  value,
@@ -400,20 +412,35 @@ var Select = ({
400
412
  themeProductContext: overlayThemeProductContext ?? themeProductContext
401
413
  });
402
414
  const overlayTheme = rawOverlayTheme;
403
- const [isOpen, setIsOpen] = (0, import_react3.useState)(false);
404
- const [selectedValue, setSelectedValue] = (0, import_react3.useState)(value);
405
- const [searchValue, setSearchValue] = (0, import_react3.useState)("");
406
- const containerRef = (0, import_react3.useRef)(null);
407
- const dropdownRef = (0, import_react3.useRef)(null);
408
- const selectedItemRef = (0, import_react3.useRef)(null);
409
- const searchInputRef = (0, import_react3.useRef)(null);
415
+ const modalId = (0, import_xui_core.useModalId)();
416
+ const [isOpen, setIsOpen] = (0, import_react4.useState)(false);
417
+ const [selectedValue, setSelectedValue] = (0, import_react4.useState)(value);
418
+ const [searchValue, setSearchValue] = (0, import_react4.useState)("");
419
+ const [dropdownPosition, setDropdownPosition] = (0, import_react4.useState)(null);
420
+ const containerRef = (0, import_react4.useRef)(null);
421
+ const triggerRef = (0, import_react4.useRef)(null);
422
+ const dropdownRef = (0, import_react4.useRef)(null);
423
+ const selectedItemRef = (0, import_react4.useRef)(null);
424
+ const searchInputRef = (0, import_react4.useRef)(null);
410
425
  const isDisable = externalState === "disable" || disabled;
411
426
  const isError = externalState === "error" || !!errorMessage;
412
427
  const isFocus = externalState === "focus" || isOpen;
413
- (0, import_react3.useEffect)(() => {
428
+ const sizeStyles = theme.sizing.input(size);
429
+ const inputColors = theme.colors.control.input;
430
+ const overlayInputColors = overlayTheme.colors.control.input;
431
+ const updateDropdownPosition = (0, import_react4.useCallback)(() => {
432
+ if (!isWeb || !triggerRef.current) return;
433
+ const rect = triggerRef.current.getBoundingClientRect();
434
+ setDropdownPosition({
435
+ left: rect.left,
436
+ top: rect.bottom + sizeStyles.fieldGap,
437
+ width: rect.width
438
+ });
439
+ }, [sizeStyles.fieldGap]);
440
+ (0, import_react4.useEffect)(() => {
414
441
  if (value !== void 0) setSelectedValue(value);
415
442
  }, [value]);
416
- (0, import_react3.useEffect)(() => {
443
+ (0, import_react4.useEffect)(() => {
417
444
  if (isFocus && selectedItemRef.current && dropdownRef.current) {
418
445
  const timeoutId = setTimeout(() => {
419
446
  const selectedItem = selectedItemRef.current;
@@ -423,15 +450,33 @@ var Select = ({
423
450
  return () => clearTimeout(timeoutId);
424
451
  }
425
452
  }, [isFocus]);
426
- (0, import_react3.useEffect)(() => {
453
+ (0, import_react4.useEffect)(() => {
454
+ if (!isWeb || !isFocus) return;
455
+ updateDropdownPosition();
456
+ window.addEventListener("resize", updateDropdownPosition);
457
+ window.addEventListener("scroll", updateDropdownPosition, true);
458
+ return () => {
459
+ window.removeEventListener("resize", updateDropdownPosition);
460
+ window.removeEventListener("scroll", updateDropdownPosition, true);
461
+ };
462
+ }, [isFocus, updateDropdownPosition]);
463
+ (0, import_react4.useEffect)(() => {
427
464
  if (isFocus && searchable) searchInputRef.current?.focus();
428
465
  }, [isFocus, searchable]);
429
- (0, import_react3.useEffect)(() => {
466
+ (0, import_react4.useEffect)(() => {
430
467
  if (!isFocus) setSearchValue("");
431
468
  }, [isFocus]);
432
469
  (0, import_xui_hooks.useClickOutside)(
433
470
  containerRef,
434
- (0, import_react3.useCallback)(() => setIsOpen(false), [setIsOpen]),
471
+ (0, import_react4.useCallback)((event) => {
472
+ const dropdownEl = dropdownRef.current;
473
+ if (dropdownEl) {
474
+ const path = typeof event.composedPath === "function" ? event.composedPath() : [];
475
+ const insideDropdown = path.length ? path.includes(dropdownEl) : dropdownEl.contains(event.target);
476
+ if (insideDropdown) return;
477
+ }
478
+ setIsOpen(false);
479
+ }, []),
435
480
  isWeb && isOpen
436
481
  );
437
482
  const getOptionLabel = (option) => typeof option === "string" ? option : option.label;
@@ -440,9 +485,6 @@ var Select = ({
440
485
  const filteredOptions = searchable && searchValue ? options.filter(
441
486
  (option) => getOptionLabel(option).toLowerCase().includes(searchValue.toLowerCase())
442
487
  ) : options;
443
- const sizeStyles = theme.sizing.input(size);
444
- const inputColors = theme.colors.control.input;
445
- const overlayInputColors = overlayTheme.colors.control.input;
446
488
  const handlePress = () => {
447
489
  if (!isDisable) {
448
490
  if (onPress) onPress();
@@ -486,6 +528,142 @@ var Select = ({
486
528
  ) : placeholder;
487
529
  const textColor = isDisable ? inputColors.textDisable : selectedValue ? inputColors.text : inputColors.placeholder;
488
530
  const iconColor = isDisable ? inputColors.textDisable : inputColors.text;
531
+ const dropdownTop = sizeStyles.height + (label ? sizeStyles.fontSize + sizeStyles.fieldGap : 0) + sizeStyles.fieldGap;
532
+ const dropdown = isFocus && options.length > 0 && (isNative || dropdownPosition) && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
533
+ Box,
534
+ {
535
+ ref: dropdownRef,
536
+ "data-modal-id": modalId,
537
+ position: isNative ? "absolute" : "fixed",
538
+ top: isNative ? dropdownTop : dropdownPosition?.top,
539
+ left: isNative ? void 0 : dropdownPosition?.left,
540
+ width: isNative ? "100%" : dropdownPosition?.width,
541
+ backgroundColor: overlayTheme.colors.background.secondary,
542
+ borderColor: overlayTheme.colors.border.secondary,
543
+ borderWidth: 1,
544
+ borderRadius: overlayTheme.shape.contextMenu[size].borderRadius,
545
+ style: {
546
+ zIndex: 2e3,
547
+ ...isNative ? { elevation: 4 } : { boxShadow: "0 4px 12px rgba(0,0,0,0.1)" },
548
+ minWidth: iconOnly ? sizeStyles.height * 3 : void 0
549
+ },
550
+ children: [
551
+ searchable && !isNative && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
552
+ Box,
553
+ {
554
+ paddingHorizontal: sizeStyles.paddingHorizontal,
555
+ paddingVertical: sizeStyles.paddingVertical,
556
+ borderBottomWidth: 1,
557
+ borderColor: overlayTheme.colors.border.secondary,
558
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
559
+ Box,
560
+ {
561
+ flexDirection: "row",
562
+ alignItems: "center",
563
+ gap: sizeStyles.paddingHorizontal / 2,
564
+ paddingHorizontal: 4,
565
+ children: [
566
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
567
+ Icon,
568
+ {
569
+ size: sizeStyles.iconSize - 2,
570
+ color: overlayInputColors.placeholder,
571
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_xui_icons_base.Search, {})
572
+ }
573
+ ),
574
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
575
+ Box,
576
+ {
577
+ as: "input",
578
+ ref: searchInputRef,
579
+ flex: 1,
580
+ type: "text",
581
+ value: searchValue,
582
+ onChange: (e) => setSearchValue(e.target.value),
583
+ placeholder: searchPlaceholder,
584
+ style: {
585
+ border: "none",
586
+ outline: "none",
587
+ background: "transparent",
588
+ color: overlayInputColors.text,
589
+ fontSize: sizeStyles.fontSize,
590
+ width: "100%"
591
+ }
592
+ }
593
+ )
594
+ ]
595
+ }
596
+ )
597
+ }
598
+ ),
599
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
600
+ Box,
601
+ {
602
+ paddingVertical: 4,
603
+ overflow: "scroll",
604
+ style: {
605
+ maxHeight: searchable ? maxHeight - 60 : maxHeight,
606
+ ...isWeb ? { overflowY: "auto" } : {}
607
+ },
608
+ children: filteredOptions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
609
+ Box,
610
+ {
611
+ paddingVertical: sizeStyles.paddingVertical * 2,
612
+ paddingHorizontal: sizeStyles.paddingHorizontal,
613
+ alignItems: "center",
614
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
615
+ Text,
616
+ {
617
+ color: overlayTheme.colors.content.tertiary,
618
+ fontSize: sizeStyles.fontSize,
619
+ children: noOptionsMessage
620
+ }
621
+ )
622
+ }
623
+ ) : filteredOptions.map((option, index2) => {
624
+ const optionValue = getOptionValue(option);
625
+ const optionLabel = getOptionLabel(option);
626
+ const isOptionDisabled = getOptionDisabled(option);
627
+ const isSelected = optionValue === selectedValue;
628
+ const brandColors = overlayTheme.colors.control.brand.primary;
629
+ const contentColors = overlayTheme.colors.content;
630
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
631
+ Box,
632
+ {
633
+ testID,
634
+ ref: isSelected ? selectedItemRef : void 0,
635
+ paddingVertical: sizeStyles.paddingVertical,
636
+ paddingHorizontal: sizeStyles.paddingHorizontal,
637
+ onPress: isOptionDisabled ? void 0 : () => handleSelect(option),
638
+ flexDirection: "row",
639
+ alignItems: "center",
640
+ justifyContent: "space-between",
641
+ backgroundColor: isSelected ? brandColors.bg : "transparent",
642
+ style: {
643
+ ...isWeb ? {
644
+ cursor: isOptionDisabled ? "not-allowed" : "pointer"
645
+ } : {},
646
+ opacity: isOptionDisabled ? 0.5 : 1
647
+ },
648
+ hoverStyle: !isSelected && !isOptionDisabled ? { backgroundColor: overlayInputColors.bgHover } : void 0,
649
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
650
+ Text,
651
+ {
652
+ color: isSelected ? contentColors.on.brand : overlayTheme.colors.content.secondary,
653
+ fontSize: sizeStyles.fontSize,
654
+ fontWeight: "400",
655
+ children: optionLabel
656
+ }
657
+ )
658
+ },
659
+ index2
660
+ );
661
+ })
662
+ }
663
+ )
664
+ ]
665
+ }
666
+ );
489
667
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
490
668
  Box,
491
669
  {
@@ -508,6 +686,7 @@ var Select = ({
508
686
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
509
687
  Box,
510
688
  {
689
+ ref: triggerRef,
511
690
  onPress: handlePress,
512
691
  backgroundColor,
513
692
  borderColor,
@@ -592,139 +771,7 @@ var Select = ({
592
771
  ]
593
772
  }
594
773
  ),
595
- isFocus && options.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
596
- Box,
597
- {
598
- ref: dropdownRef,
599
- position: "absolute",
600
- top: sizeStyles.height + (label ? sizeStyles.fontSize + sizeStyles.fieldGap : 0) + sizeStyles.fieldGap,
601
- width: "100%",
602
- backgroundColor: overlayTheme.colors.background.secondary,
603
- borderColor: overlayTheme.colors.border.secondary,
604
- borderWidth: 1,
605
- borderRadius: overlayTheme.shape.contextMenu[size].borderRadius,
606
- style: {
607
- zIndex: 1e3,
608
- ...isNative ? { elevation: 4 } : { boxShadow: "0 4px 12px rgba(0,0,0,0.1)" },
609
- minWidth: iconOnly ? sizeStyles.height * 3 : void 0
610
- },
611
- children: [
612
- searchable && !isNative && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
613
- Box,
614
- {
615
- paddingHorizontal: sizeStyles.paddingHorizontal,
616
- paddingVertical: sizeStyles.paddingVertical,
617
- borderBottomWidth: 1,
618
- borderColor: overlayTheme.colors.border.secondary,
619
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
620
- Box,
621
- {
622
- flexDirection: "row",
623
- alignItems: "center",
624
- gap: sizeStyles.paddingHorizontal / 2,
625
- paddingHorizontal: 4,
626
- children: [
627
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
628
- Icon,
629
- {
630
- size: sizeStyles.iconSize - 2,
631
- color: overlayInputColors.placeholder,
632
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_xui_icons_base.Search, {})
633
- }
634
- ),
635
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
636
- Box,
637
- {
638
- as: "input",
639
- ref: searchInputRef,
640
- flex: 1,
641
- type: "text",
642
- value: searchValue,
643
- onChange: (e) => setSearchValue(e.target.value),
644
- placeholder: searchPlaceholder,
645
- style: {
646
- border: "none",
647
- outline: "none",
648
- background: "transparent",
649
- color: overlayInputColors.text,
650
- fontSize: sizeStyles.fontSize,
651
- width: "100%"
652
- }
653
- }
654
- )
655
- ]
656
- }
657
- )
658
- }
659
- ),
660
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
661
- Box,
662
- {
663
- paddingVertical: 4,
664
- overflow: "scroll",
665
- style: {
666
- maxHeight: searchable ? maxHeight - 60 : maxHeight,
667
- ...isWeb ? { overflowY: "auto" } : {}
668
- },
669
- children: filteredOptions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
670
- Box,
671
- {
672
- paddingVertical: sizeStyles.paddingVertical * 2,
673
- paddingHorizontal: sizeStyles.paddingHorizontal,
674
- alignItems: "center",
675
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
676
- Text,
677
- {
678
- color: overlayTheme.colors.content.tertiary,
679
- fontSize: sizeStyles.fontSize,
680
- children: noOptionsMessage
681
- }
682
- )
683
- }
684
- ) : filteredOptions.map((option, index2) => {
685
- const optionValue = getOptionValue(option);
686
- const optionLabel = getOptionLabel(option);
687
- const isOptionDisabled = getOptionDisabled(option);
688
- const isSelected = optionValue === selectedValue;
689
- const brandColors = overlayTheme.colors.control.brand.primary;
690
- const contentColors = overlayTheme.colors.content;
691
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
692
- Box,
693
- {
694
- testID,
695
- ref: isSelected ? selectedItemRef : void 0,
696
- paddingVertical: sizeStyles.paddingVertical,
697
- paddingHorizontal: sizeStyles.paddingHorizontal,
698
- onPress: isOptionDisabled ? void 0 : () => handleSelect(option),
699
- flexDirection: "row",
700
- alignItems: "center",
701
- justifyContent: "space-between",
702
- backgroundColor: isSelected ? brandColors.bg : "transparent",
703
- style: {
704
- ...isWeb ? {
705
- cursor: isOptionDisabled ? "not-allowed" : "pointer"
706
- } : {},
707
- opacity: isOptionDisabled ? 0.5 : 1
708
- },
709
- hoverStyle: !isSelected && !isOptionDisabled ? { backgroundColor: overlayInputColors.bgHover } : void 0,
710
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
711
- Text,
712
- {
713
- color: isSelected ? contentColors.on.brand : overlayTheme.colors.content.secondary,
714
- fontSize: sizeStyles.fontSize,
715
- fontWeight: "400",
716
- children: optionLabel
717
- }
718
- )
719
- },
720
- index2
721
- );
722
- })
723
- }
724
- )
725
- ]
726
- }
727
- ),
774
+ isNative ? dropdown : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Portal, { children: dropdown }),
728
775
  isError && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
729
776
  Text,
730
777
  {