@xsolla/xui-select 0.176.0 → 0.177.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/web/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/Select.tsx
2
- import { useState, useRef, useEffect, useCallback } from "react";
2
+ import { useState as useState2, useRef, useEffect as useEffect2, useCallback } from "react";
3
3
 
4
4
  // ../../foundation/primitives-web/src/Box.tsx
5
5
  import React2 from "react";
@@ -323,6 +323,7 @@ var isNative = false;
323
323
 
324
324
  // src/Select.tsx
325
325
  import {
326
+ useModalId,
326
327
  useResolvedTheme
327
328
  } from "@xsolla/xui-core";
328
329
  import { useClickOutside } from "@xsolla/xui-hooks";
@@ -333,6 +334,18 @@ import {
333
334
  Remove,
334
335
  Search as SearchIcon
335
336
  } from "@xsolla/xui-icons-base";
337
+
338
+ // src/Portal.web.tsx
339
+ import { useEffect, useState } from "react";
340
+ import { createPortal } from "react-dom";
341
+ var Portal = ({ children }) => {
342
+ const [mountNode, setMountNode] = useState(null);
343
+ useEffect(() => setMountNode(document.body), []);
344
+ if (!mountNode) return null;
345
+ return createPortal(children, mountNode);
346
+ };
347
+
348
+ // src/Select.tsx
336
349
  import { jsx as jsx4, jsxs } from "react/jsx-runtime";
337
350
  var Select = ({
338
351
  value,
@@ -372,20 +385,35 @@ var Select = ({
372
385
  themeProductContext: overlayThemeProductContext ?? themeProductContext
373
386
  });
374
387
  const overlayTheme = rawOverlayTheme;
375
- const [isOpen, setIsOpen] = useState(false);
376
- const [selectedValue, setSelectedValue] = useState(value);
377
- const [searchValue, setSearchValue] = useState("");
388
+ const modalId = useModalId();
389
+ const [isOpen, setIsOpen] = useState2(false);
390
+ const [selectedValue, setSelectedValue] = useState2(value);
391
+ const [searchValue, setSearchValue] = useState2("");
392
+ const [dropdownPosition, setDropdownPosition] = useState2(null);
378
393
  const containerRef = useRef(null);
394
+ const triggerRef = useRef(null);
379
395
  const dropdownRef = useRef(null);
380
396
  const selectedItemRef = useRef(null);
381
397
  const searchInputRef = useRef(null);
382
398
  const isDisable = externalState === "disable" || disabled;
383
399
  const isError = externalState === "error" || !!errorMessage;
384
400
  const isFocus = externalState === "focus" || isOpen;
385
- useEffect(() => {
401
+ const sizeStyles = theme.sizing.input(size);
402
+ const inputColors = theme.colors.control.input;
403
+ const overlayInputColors = overlayTheme.colors.control.input;
404
+ const updateDropdownPosition = useCallback(() => {
405
+ if (!isWeb || !triggerRef.current) return;
406
+ const rect = triggerRef.current.getBoundingClientRect();
407
+ setDropdownPosition({
408
+ left: rect.left,
409
+ top: rect.bottom + sizeStyles.fieldGap,
410
+ width: rect.width
411
+ });
412
+ }, [sizeStyles.fieldGap]);
413
+ useEffect2(() => {
386
414
  if (value !== void 0) setSelectedValue(value);
387
415
  }, [value]);
388
- useEffect(() => {
416
+ useEffect2(() => {
389
417
  if (isFocus && selectedItemRef.current && dropdownRef.current) {
390
418
  const timeoutId = setTimeout(() => {
391
419
  const selectedItem = selectedItemRef.current;
@@ -395,15 +423,33 @@ var Select = ({
395
423
  return () => clearTimeout(timeoutId);
396
424
  }
397
425
  }, [isFocus]);
398
- useEffect(() => {
426
+ useEffect2(() => {
427
+ if (!isWeb || !isFocus) return;
428
+ updateDropdownPosition();
429
+ window.addEventListener("resize", updateDropdownPosition);
430
+ window.addEventListener("scroll", updateDropdownPosition, true);
431
+ return () => {
432
+ window.removeEventListener("resize", updateDropdownPosition);
433
+ window.removeEventListener("scroll", updateDropdownPosition, true);
434
+ };
435
+ }, [isFocus, updateDropdownPosition]);
436
+ useEffect2(() => {
399
437
  if (isFocus && searchable) searchInputRef.current?.focus();
400
438
  }, [isFocus, searchable]);
401
- useEffect(() => {
439
+ useEffect2(() => {
402
440
  if (!isFocus) setSearchValue("");
403
441
  }, [isFocus]);
404
442
  useClickOutside(
405
443
  containerRef,
406
- useCallback(() => setIsOpen(false), [setIsOpen]),
444
+ useCallback((event) => {
445
+ const dropdownEl = dropdownRef.current;
446
+ if (dropdownEl) {
447
+ const path = typeof event.composedPath === "function" ? event.composedPath() : [];
448
+ const insideDropdown = path.length ? path.includes(dropdownEl) : dropdownEl.contains(event.target);
449
+ if (insideDropdown) return;
450
+ }
451
+ setIsOpen(false);
452
+ }, []),
407
453
  isWeb && isOpen
408
454
  );
409
455
  const getOptionLabel = (option) => typeof option === "string" ? option : option.label;
@@ -412,9 +458,6 @@ var Select = ({
412
458
  const filteredOptions = searchable && searchValue ? options.filter(
413
459
  (option) => getOptionLabel(option).toLowerCase().includes(searchValue.toLowerCase())
414
460
  ) : options;
415
- const sizeStyles = theme.sizing.input(size);
416
- const inputColors = theme.colors.control.input;
417
- const overlayInputColors = overlayTheme.colors.control.input;
418
461
  const handlePress = () => {
419
462
  if (!isDisable) {
420
463
  if (onPress) onPress();
@@ -458,6 +501,142 @@ var Select = ({
458
501
  ) : placeholder;
459
502
  const textColor = isDisable ? inputColors.textDisable : selectedValue ? inputColors.text : inputColors.placeholder;
460
503
  const iconColor = isDisable ? inputColors.textDisable : inputColors.text;
504
+ const dropdownTop = sizeStyles.height + (label ? sizeStyles.fontSize + sizeStyles.fieldGap : 0) + sizeStyles.fieldGap;
505
+ const dropdown = isFocus && options.length > 0 && (isNative || dropdownPosition) && /* @__PURE__ */ jsxs(
506
+ Box,
507
+ {
508
+ ref: dropdownRef,
509
+ "data-modal-id": modalId,
510
+ position: isNative ? "absolute" : "fixed",
511
+ top: isNative ? dropdownTop : dropdownPosition?.top,
512
+ left: isNative ? void 0 : dropdownPosition?.left,
513
+ width: isNative ? "100%" : dropdownPosition?.width,
514
+ backgroundColor: overlayTheme.colors.background.secondary,
515
+ borderColor: overlayTheme.colors.border.secondary,
516
+ borderWidth: 1,
517
+ borderRadius: overlayTheme.shape.contextMenu[size].borderRadius,
518
+ style: {
519
+ zIndex: 2e3,
520
+ ...isNative ? { elevation: 4 } : { boxShadow: "0 4px 12px rgba(0,0,0,0.1)" },
521
+ minWidth: iconOnly ? sizeStyles.height * 3 : void 0
522
+ },
523
+ children: [
524
+ searchable && !isNative && /* @__PURE__ */ jsx4(
525
+ Box,
526
+ {
527
+ paddingHorizontal: sizeStyles.paddingHorizontal,
528
+ paddingVertical: sizeStyles.paddingVertical,
529
+ borderBottomWidth: 1,
530
+ borderColor: overlayTheme.colors.border.secondary,
531
+ children: /* @__PURE__ */ jsxs(
532
+ Box,
533
+ {
534
+ flexDirection: "row",
535
+ alignItems: "center",
536
+ gap: sizeStyles.paddingHorizontal / 2,
537
+ paddingHorizontal: 4,
538
+ children: [
539
+ /* @__PURE__ */ jsx4(
540
+ Icon,
541
+ {
542
+ size: sizeStyles.iconSize - 2,
543
+ color: overlayInputColors.placeholder,
544
+ children: /* @__PURE__ */ jsx4(SearchIcon, {})
545
+ }
546
+ ),
547
+ /* @__PURE__ */ jsx4(
548
+ Box,
549
+ {
550
+ as: "input",
551
+ ref: searchInputRef,
552
+ flex: 1,
553
+ type: "text",
554
+ value: searchValue,
555
+ onChange: (e) => setSearchValue(e.target.value),
556
+ placeholder: searchPlaceholder,
557
+ style: {
558
+ border: "none",
559
+ outline: "none",
560
+ background: "transparent",
561
+ color: overlayInputColors.text,
562
+ fontSize: sizeStyles.fontSize,
563
+ width: "100%"
564
+ }
565
+ }
566
+ )
567
+ ]
568
+ }
569
+ )
570
+ }
571
+ ),
572
+ /* @__PURE__ */ jsx4(
573
+ Box,
574
+ {
575
+ paddingVertical: 4,
576
+ overflow: "scroll",
577
+ style: {
578
+ maxHeight: searchable ? maxHeight - 60 : maxHeight,
579
+ ...isWeb ? { overflowY: "auto" } : {}
580
+ },
581
+ children: filteredOptions.length === 0 ? /* @__PURE__ */ jsx4(
582
+ Box,
583
+ {
584
+ paddingVertical: sizeStyles.paddingVertical * 2,
585
+ paddingHorizontal: sizeStyles.paddingHorizontal,
586
+ alignItems: "center",
587
+ children: /* @__PURE__ */ jsx4(
588
+ Text,
589
+ {
590
+ color: overlayTheme.colors.content.tertiary,
591
+ fontSize: sizeStyles.fontSize,
592
+ children: noOptionsMessage
593
+ }
594
+ )
595
+ }
596
+ ) : filteredOptions.map((option, index2) => {
597
+ const optionValue = getOptionValue(option);
598
+ const optionLabel = getOptionLabel(option);
599
+ const isOptionDisabled = getOptionDisabled(option);
600
+ const isSelected = optionValue === selectedValue;
601
+ const brandColors = overlayTheme.colors.control.brand.primary;
602
+ const contentColors = overlayTheme.colors.content;
603
+ return /* @__PURE__ */ jsx4(
604
+ Box,
605
+ {
606
+ testID,
607
+ ref: isSelected ? selectedItemRef : void 0,
608
+ paddingVertical: sizeStyles.paddingVertical,
609
+ paddingHorizontal: sizeStyles.paddingHorizontal,
610
+ onPress: isOptionDisabled ? void 0 : () => handleSelect(option),
611
+ flexDirection: "row",
612
+ alignItems: "center",
613
+ justifyContent: "space-between",
614
+ backgroundColor: isSelected ? brandColors.bg : "transparent",
615
+ style: {
616
+ ...isWeb ? {
617
+ cursor: isOptionDisabled ? "not-allowed" : "pointer"
618
+ } : {},
619
+ opacity: isOptionDisabled ? 0.5 : 1
620
+ },
621
+ hoverStyle: !isSelected && !isOptionDisabled ? { backgroundColor: overlayInputColors.bgHover } : void 0,
622
+ children: /* @__PURE__ */ jsx4(
623
+ Text,
624
+ {
625
+ color: isSelected ? contentColors.on.brand : overlayTheme.colors.content.secondary,
626
+ fontSize: sizeStyles.fontSize,
627
+ fontWeight: "400",
628
+ children: optionLabel
629
+ }
630
+ )
631
+ },
632
+ index2
633
+ );
634
+ })
635
+ }
636
+ )
637
+ ]
638
+ }
639
+ );
461
640
  return /* @__PURE__ */ jsxs(
462
641
  Box,
463
642
  {
@@ -480,6 +659,7 @@ var Select = ({
480
659
  /* @__PURE__ */ jsxs(
481
660
  Box,
482
661
  {
662
+ ref: triggerRef,
483
663
  onPress: handlePress,
484
664
  backgroundColor,
485
665
  borderColor,
@@ -564,139 +744,7 @@ var Select = ({
564
744
  ]
565
745
  }
566
746
  ),
567
- isFocus && options.length > 0 && /* @__PURE__ */ jsxs(
568
- Box,
569
- {
570
- ref: dropdownRef,
571
- position: "absolute",
572
- top: sizeStyles.height + (label ? sizeStyles.fontSize + sizeStyles.fieldGap : 0) + sizeStyles.fieldGap,
573
- width: "100%",
574
- backgroundColor: overlayTheme.colors.background.secondary,
575
- borderColor: overlayTheme.colors.border.secondary,
576
- borderWidth: 1,
577
- borderRadius: overlayTheme.shape.contextMenu[size].borderRadius,
578
- style: {
579
- zIndex: 1e3,
580
- ...isNative ? { elevation: 4 } : { boxShadow: "0 4px 12px rgba(0,0,0,0.1)" },
581
- minWidth: iconOnly ? sizeStyles.height * 3 : void 0
582
- },
583
- children: [
584
- searchable && !isNative && /* @__PURE__ */ jsx4(
585
- Box,
586
- {
587
- paddingHorizontal: sizeStyles.paddingHorizontal,
588
- paddingVertical: sizeStyles.paddingVertical,
589
- borderBottomWidth: 1,
590
- borderColor: overlayTheme.colors.border.secondary,
591
- children: /* @__PURE__ */ jsxs(
592
- Box,
593
- {
594
- flexDirection: "row",
595
- alignItems: "center",
596
- gap: sizeStyles.paddingHorizontal / 2,
597
- paddingHorizontal: 4,
598
- children: [
599
- /* @__PURE__ */ jsx4(
600
- Icon,
601
- {
602
- size: sizeStyles.iconSize - 2,
603
- color: overlayInputColors.placeholder,
604
- children: /* @__PURE__ */ jsx4(SearchIcon, {})
605
- }
606
- ),
607
- /* @__PURE__ */ jsx4(
608
- Box,
609
- {
610
- as: "input",
611
- ref: searchInputRef,
612
- flex: 1,
613
- type: "text",
614
- value: searchValue,
615
- onChange: (e) => setSearchValue(e.target.value),
616
- placeholder: searchPlaceholder,
617
- style: {
618
- border: "none",
619
- outline: "none",
620
- background: "transparent",
621
- color: overlayInputColors.text,
622
- fontSize: sizeStyles.fontSize,
623
- width: "100%"
624
- }
625
- }
626
- )
627
- ]
628
- }
629
- )
630
- }
631
- ),
632
- /* @__PURE__ */ jsx4(
633
- Box,
634
- {
635
- paddingVertical: 4,
636
- overflow: "scroll",
637
- style: {
638
- maxHeight: searchable ? maxHeight - 60 : maxHeight,
639
- ...isWeb ? { overflowY: "auto" } : {}
640
- },
641
- children: filteredOptions.length === 0 ? /* @__PURE__ */ jsx4(
642
- Box,
643
- {
644
- paddingVertical: sizeStyles.paddingVertical * 2,
645
- paddingHorizontal: sizeStyles.paddingHorizontal,
646
- alignItems: "center",
647
- children: /* @__PURE__ */ jsx4(
648
- Text,
649
- {
650
- color: overlayTheme.colors.content.tertiary,
651
- fontSize: sizeStyles.fontSize,
652
- children: noOptionsMessage
653
- }
654
- )
655
- }
656
- ) : filteredOptions.map((option, index2) => {
657
- const optionValue = getOptionValue(option);
658
- const optionLabel = getOptionLabel(option);
659
- const isOptionDisabled = getOptionDisabled(option);
660
- const isSelected = optionValue === selectedValue;
661
- const brandColors = overlayTheme.colors.control.brand.primary;
662
- const contentColors = overlayTheme.colors.content;
663
- return /* @__PURE__ */ jsx4(
664
- Box,
665
- {
666
- testID,
667
- ref: isSelected ? selectedItemRef : void 0,
668
- paddingVertical: sizeStyles.paddingVertical,
669
- paddingHorizontal: sizeStyles.paddingHorizontal,
670
- onPress: isOptionDisabled ? void 0 : () => handleSelect(option),
671
- flexDirection: "row",
672
- alignItems: "center",
673
- justifyContent: "space-between",
674
- backgroundColor: isSelected ? brandColors.bg : "transparent",
675
- style: {
676
- ...isWeb ? {
677
- cursor: isOptionDisabled ? "not-allowed" : "pointer"
678
- } : {},
679
- opacity: isOptionDisabled ? 0.5 : 1
680
- },
681
- hoverStyle: !isSelected && !isOptionDisabled ? { backgroundColor: overlayInputColors.bgHover } : void 0,
682
- children: /* @__PURE__ */ jsx4(
683
- Text,
684
- {
685
- color: isSelected ? contentColors.on.brand : overlayTheme.colors.content.secondary,
686
- fontSize: sizeStyles.fontSize,
687
- fontWeight: "400",
688
- children: optionLabel
689
- }
690
- )
691
- },
692
- index2
693
- );
694
- })
695
- }
696
- )
697
- ]
698
- }
699
- ),
747
+ isNative ? dropdown : /* @__PURE__ */ jsx4(Portal, { children: dropdown }),
700
748
  isError && errorMessage && /* @__PURE__ */ jsx4(
701
749
  Text,
702
750
  {