@xsolla/xui-tag 0.222.0 → 0.224.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.d.mts CHANGED
@@ -14,12 +14,57 @@ interface TagProps extends ThemeOverrideProps {
14
14
  iconLeft?: ReactNode;
15
15
  /** Icon displayed on the right side of the label */
16
16
  iconRight?: ReactNode;
17
- /** Callback when the remove (X) icon is clicked */
17
+ /** Callback when the remove (X) button is pressed */
18
18
  onRemove?: () => void;
19
+ /**
20
+ * Accessible name of the remove button. Defaults to "Remove {children}" when
21
+ * `children` is a string or number, otherwise "Remove".
22
+ */
23
+ removeLabel?: string;
19
24
  testID?: string;
20
25
  /** Extra styles merged onto the chip. Used for fills the tone tokens do not cover. */
21
26
  style?: React.CSSProperties;
22
27
  }
23
28
  declare const Tag: React.FC<TagProps>;
24
29
 
25
- export { Tag, type TagProps };
30
+ type TagInputSize = "xl" | "lg" | "md" | "sm" | "xs";
31
+ interface TagInputProps extends ThemeOverrideProps {
32
+ /** Current list of chip values (controlled). */
33
+ value: string[];
34
+ /** Called with the next list whenever a chip is added or removed. */
35
+ onChange: (value: string[]) => void;
36
+ /** Placeholder shown in the text field while no draft text is typed. */
37
+ placeholder?: string;
38
+ /** Size of the field. Chips are sized to fit inside it. */
39
+ size?: TagInputSize;
40
+ /** Disables typing and chip removal. */
41
+ disabled?: boolean;
42
+ /** Highlights the field as invalid. */
43
+ error?: boolean;
44
+ /** Error message rendered below the field. Also highlights the field as invalid. */
45
+ errorMessage?: string;
46
+ /** Visible label rendered above the field. */
47
+ label?: string;
48
+ /** Accessible label for the text field when no visible `label` is set. */
49
+ "aria-label"?: string;
50
+ /** Id of the text field. Generated when omitted. */
51
+ id?: string;
52
+ /** Name of the text field. */
53
+ name?: string;
54
+ /** Called when the text field receives focus. */
55
+ onFocus?: () => void;
56
+ /** Called when the text field loses focus (after any draft is committed). */
57
+ onBlur?: () => void;
58
+ testID?: string;
59
+ }
60
+ /**
61
+ * Text field that turns typed values into removable chips.
62
+ *
63
+ * - Enter adds the typed text as a chip and never submits the surrounding form.
64
+ * - Leaving the field adds the typed text too.
65
+ * - Backspace in an empty field removes the last chip.
66
+ * - Values are kept as typed: no trimming and no de-duplication.
67
+ */
68
+ declare const TagInput: React.ForwardRefExoticComponent<TagInputProps & React.RefAttributes<HTMLInputElement>>;
69
+
70
+ export { Tag, TagInput, type TagInputProps, type TagProps };
package/web/index.d.ts CHANGED
@@ -14,12 +14,57 @@ interface TagProps extends ThemeOverrideProps {
14
14
  iconLeft?: ReactNode;
15
15
  /** Icon displayed on the right side of the label */
16
16
  iconRight?: ReactNode;
17
- /** Callback when the remove (X) icon is clicked */
17
+ /** Callback when the remove (X) button is pressed */
18
18
  onRemove?: () => void;
19
+ /**
20
+ * Accessible name of the remove button. Defaults to "Remove {children}" when
21
+ * `children` is a string or number, otherwise "Remove".
22
+ */
23
+ removeLabel?: string;
19
24
  testID?: string;
20
25
  /** Extra styles merged onto the chip. Used for fills the tone tokens do not cover. */
21
26
  style?: React.CSSProperties;
22
27
  }
23
28
  declare const Tag: React.FC<TagProps>;
24
29
 
25
- export { Tag, type TagProps };
30
+ type TagInputSize = "xl" | "lg" | "md" | "sm" | "xs";
31
+ interface TagInputProps extends ThemeOverrideProps {
32
+ /** Current list of chip values (controlled). */
33
+ value: string[];
34
+ /** Called with the next list whenever a chip is added or removed. */
35
+ onChange: (value: string[]) => void;
36
+ /** Placeholder shown in the text field while no draft text is typed. */
37
+ placeholder?: string;
38
+ /** Size of the field. Chips are sized to fit inside it. */
39
+ size?: TagInputSize;
40
+ /** Disables typing and chip removal. */
41
+ disabled?: boolean;
42
+ /** Highlights the field as invalid. */
43
+ error?: boolean;
44
+ /** Error message rendered below the field. Also highlights the field as invalid. */
45
+ errorMessage?: string;
46
+ /** Visible label rendered above the field. */
47
+ label?: string;
48
+ /** Accessible label for the text field when no visible `label` is set. */
49
+ "aria-label"?: string;
50
+ /** Id of the text field. Generated when omitted. */
51
+ id?: string;
52
+ /** Name of the text field. */
53
+ name?: string;
54
+ /** Called when the text field receives focus. */
55
+ onFocus?: () => void;
56
+ /** Called when the text field loses focus (after any draft is committed). */
57
+ onBlur?: () => void;
58
+ testID?: string;
59
+ }
60
+ /**
61
+ * Text field that turns typed values into removable chips.
62
+ *
63
+ * - Enter adds the typed text as a chip and never submits the surrounding form.
64
+ * - Leaving the field adds the typed text too.
65
+ * - Backspace in an empty field removes the last chip.
66
+ * - Values are kept as typed: no trimming and no de-duplication.
67
+ */
68
+ declare const TagInput: React.ForwardRefExoticComponent<TagInputProps & React.RefAttributes<HTMLInputElement>>;
69
+
70
+ export { Tag, TagInput, type TagInputProps, type TagProps };
package/web/index.js CHANGED
@@ -30,7 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.tsx
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- Tag: () => Tag
33
+ Tag: () => Tag,
34
+ TagInput: () => TagInput
34
35
  });
35
36
  module.exports = __toCommonJS(index_exports);
36
37
 
@@ -399,10 +400,141 @@ var Icon = ({
399
400
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StyledIcon, { "data-testid": dataTestId || testID, ...props, children });
400
401
  };
401
402
 
403
+ // ../../foundation/primitives-web/src/Input.tsx
404
+ var import_react3 = require("react");
405
+ var import_styled_components4 = __toESM(require("styled-components"));
406
+ var import_jsx_runtime4 = require("react/jsx-runtime");
407
+ var FilteredInput = createFilteredElement("input");
408
+ var StyledInput = (0, import_styled_components4.default)(FilteredInput)`
409
+ background: transparent;
410
+ border: none;
411
+ outline: none;
412
+ width: 100%;
413
+ height: 100%;
414
+ padding: 0;
415
+ margin: 0;
416
+ color: ${(props) => props.color || "inherit"};
417
+ font-size: ${(props) => typeof props.fontSize === "number" ? `${props.fontSize}px` : props.fontSize || "inherit"};
418
+ font-family: ${(props) => props.fontFamily || '"Aktiv Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'};
419
+ text-align: inherit;
420
+
421
+ &::placeholder {
422
+ color: ${(props) => props.placeholderTextColor || "rgba(255, 255, 255, 0.5)"};
423
+ }
424
+
425
+ &:disabled {
426
+ cursor: not-allowed;
427
+ }
428
+
429
+ /*
430
+ * Hide the native number spin buttons. type="number" is rendered with the
431
+ * toolkit's own chevron stepper (see Input "Type number" in Figma), so the
432
+ * OS chrome must not show alongside it. Keyboard ArrowUp / ArrowDown are
433
+ * unaffected — this only removes the painted control.
434
+ */
435
+ &[type="number"] {
436
+ -moz-appearance: textfield;
437
+ appearance: textfield;
438
+ }
439
+
440
+ &[type="number"]::-webkit-outer-spin-button,
441
+ &[type="number"]::-webkit-inner-spin-button {
442
+ -webkit-appearance: none;
443
+ appearance: none;
444
+ margin: 0;
445
+ }
446
+
447
+ /* Override browser autofill background */
448
+ &:-webkit-autofill,
449
+ &:-webkit-autofill:hover,
450
+ &:-webkit-autofill:focus,
451
+ &:-webkit-autofill:active {
452
+ -webkit-box-shadow: 0 0 0 1000px transparent inset !important;
453
+ -webkit-background-clip: text !important;
454
+ -webkit-text-fill-color: ${(props) => props.color || "inherit"} !important;
455
+ }
456
+ `;
457
+ var InputPrimitive = (0, import_react3.forwardRef)(
458
+ ({
459
+ value,
460
+ placeholder,
461
+ onChange,
462
+ onChangeText,
463
+ onFocus,
464
+ onBlur,
465
+ onKeyDown,
466
+ disabled,
467
+ secureTextEntry,
468
+ style,
469
+ color,
470
+ fontSize,
471
+ fontFamily,
472
+ placeholderTextColor,
473
+ maxLength,
474
+ name,
475
+ type,
476
+ inputMode,
477
+ autoComplete,
478
+ id,
479
+ "aria-invalid": ariaInvalid,
480
+ "aria-describedby": ariaDescribedBy,
481
+ "aria-labelledby": ariaLabelledBy,
482
+ "aria-label": ariaLabel,
483
+ "aria-disabled": ariaDisabled,
484
+ "data-testid": dataTestId,
485
+ testID,
486
+ ...rest
487
+ }, ref) => {
488
+ const handleChange = (e) => {
489
+ if (onChange) {
490
+ onChange(e);
491
+ }
492
+ if (onChangeText) {
493
+ onChangeText(e.target.value);
494
+ }
495
+ };
496
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
497
+ StyledInput,
498
+ {
499
+ ref,
500
+ id,
501
+ value,
502
+ name,
503
+ placeholder,
504
+ onChange: handleChange,
505
+ onFocus,
506
+ onBlur,
507
+ onKeyDown,
508
+ disabled,
509
+ type: secureTextEntry ? "password" : type || "text",
510
+ inputMode,
511
+ autoComplete,
512
+ style,
513
+ color,
514
+ fontSize,
515
+ fontFamily,
516
+ placeholderTextColor,
517
+ maxLength,
518
+ "aria-invalid": ariaInvalid,
519
+ "aria-describedby": ariaDescribedBy,
520
+ "aria-labelledby": ariaLabelledBy,
521
+ "aria-label": ariaLabel,
522
+ "aria-disabled": ariaDisabled,
523
+ "data-testid": dataTestId || testID,
524
+ ...rest
525
+ }
526
+ );
527
+ }
528
+ );
529
+ InputPrimitive.displayName = "InputPrimitive";
530
+
531
+ // ../../foundation/primitives-web/src/index.tsx
532
+ var isWeb = true;
533
+
402
534
  // src/Tag.tsx
403
535
  var import_xui_core = require("@xsolla/xui-core");
404
536
  var import_xui_icons = require("@xsolla/xui-icons");
405
- var import_jsx_runtime4 = require("react/jsx-runtime");
537
+ var import_jsx_runtime5 = require("react/jsx-runtime");
406
538
  var Tag = ({
407
539
  size = "md",
408
540
  tone = "primary",
@@ -411,6 +543,7 @@ var Tag = ({
411
543
  iconLeft,
412
544
  iconRight,
413
545
  onRemove,
546
+ removeLabel,
414
547
  testID,
415
548
  themeMode,
416
549
  themeProductContext,
@@ -473,7 +606,8 @@ var Tag = ({
473
606
  };
474
607
  const { bg, text, border } = resolveColors();
475
608
  const isIconOnly = !children && (!!iconLeft || !!iconRight);
476
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
609
+ const resolvedRemoveLabel = removeLabel ?? (typeof children === "string" || typeof children === "number" ? `Remove ${children}` : "Remove");
610
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
477
611
  Box,
478
612
  {
479
613
  testID,
@@ -496,8 +630,8 @@ var Tag = ({
496
630
  ...style
497
631
  },
498
632
  children: [
499
- iconLeft && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Icon, { size: sizeStyles.iconSize, color: text, children: iconLeft }),
500
- children && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
633
+ iconLeft && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Icon, { size: sizeStyles.iconSize, color: text, children: iconLeft }),
634
+ children && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
501
635
  Text,
502
636
  {
503
637
  color: text,
@@ -512,14 +646,238 @@ var Tag = ({
512
646
  children
513
647
  }
514
648
  ),
515
- iconRight && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Icon, { size: sizeStyles.iconSize, color: text, children: iconRight }),
516
- onRemove && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box, { onPress: onRemove, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_xui_icons.X, { size: sizeStyles.iconSize, color: text }) })
649
+ iconRight && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Icon, { size: sizeStyles.iconSize, color: text, children: iconRight }),
650
+ onRemove && // A real <button>: focusable, activates on Enter / Space, and
651
+ // announced with its accessible name by screen readers.
652
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
653
+ Box,
654
+ {
655
+ as: "button",
656
+ type: "button",
657
+ onPress: onRemove,
658
+ "aria-label": resolvedRemoveLabel,
659
+ alignItems: "center",
660
+ justifyContent: "center",
661
+ paddingHorizontal: 0,
662
+ paddingVertical: 0,
663
+ backgroundColor: "transparent",
664
+ borderWidth: 0,
665
+ cursor: "pointer",
666
+ "data-testid": "tag__remove-button",
667
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_xui_icons.X, { size: sizeStyles.iconSize, color: text })
668
+ }
669
+ )
517
670
  ]
518
671
  }
519
672
  );
520
673
  };
674
+
675
+ // src/TagInput.tsx
676
+ var import_react4 = require("react");
677
+ var import_xui_core2 = require("@xsolla/xui-core");
678
+ var import_jsx_runtime6 = require("react/jsx-runtime");
679
+ var chipSizeConfig = {
680
+ xl: "lg",
681
+ lg: "md",
682
+ md: "sm",
683
+ sm: "xs",
684
+ xs: "xs"
685
+ };
686
+ var TagInput = (0, import_react4.forwardRef)(
687
+ ({
688
+ value,
689
+ onChange,
690
+ placeholder,
691
+ size = "md",
692
+ disabled = false,
693
+ error,
694
+ errorMessage,
695
+ label,
696
+ "aria-label": ariaLabel,
697
+ id: providedId,
698
+ name,
699
+ onFocus,
700
+ onBlur,
701
+ testID,
702
+ themeMode,
703
+ themeProductContext
704
+ }, ref) => {
705
+ const { theme } = (0, import_xui_core2.useResolvedTheme)({ themeMode, themeProductContext });
706
+ const [draft, setDraft] = (0, import_react4.useState)("");
707
+ const [isFocus, setIsFocus] = (0, import_react4.useState)(false);
708
+ const inputRef = (0, import_react4.useRef)(null);
709
+ const rawId = (0, import_xui_core2.useId)();
710
+ const safeId = rawId.replace(/:/g, "");
711
+ const inputId = providedId || `tag-input-${safeId}`;
712
+ const labelId = `${inputId}-label`;
713
+ const errorId = `${inputId}-error`;
714
+ (0, import_react4.useImperativeHandle)(ref, () => inputRef.current, []);
715
+ const isError = !!(errorMessage || error);
716
+ const sizeStyles = theme.sizing.input(size);
717
+ const inputColors = theme.colors.control.input;
718
+ const borderRadius = theme.shape.input[size].borderRadius;
719
+ const chipSize = chipSizeConfig[size];
720
+ const chipHeight = theme.sizing.tag(chipSize).height;
721
+ const paddingVertical = Math.max(
722
+ (sizeStyles.height - chipHeight) / 2 - 1,
723
+ 0
724
+ );
725
+ const commitDraft = () => {
726
+ if (draft.length === 0) return;
727
+ onChange([...value, draft]);
728
+ setDraft("");
729
+ };
730
+ const removeAt = (index2) => {
731
+ if (disabled) return;
732
+ onChange(value.filter((_, i) => i !== index2));
733
+ inputRef.current?.focus();
734
+ };
735
+ const handleKeyDown = (e) => {
736
+ const nativeEvent = e.nativeEvent;
737
+ const isComposing = nativeEvent?.isComposing === true || nativeEvent?.keyCode === 229;
738
+ if (e.key === "Enter") {
739
+ e.preventDefault();
740
+ if (!isComposing) commitDraft();
741
+ return;
742
+ }
743
+ if (e.key === "Backspace" && draft.length === 0 && value.length > 0) {
744
+ e.preventDefault();
745
+ onChange(value.slice(0, -1));
746
+ }
747
+ };
748
+ const handleChange = (e) => {
749
+ setDraft(e.target.value);
750
+ };
751
+ const handleFocus = () => {
752
+ if (!disabled) setIsFocus(true);
753
+ onFocus?.();
754
+ };
755
+ const handleBlur = () => {
756
+ setIsFocus(false);
757
+ commitDraft();
758
+ onBlur?.();
759
+ };
760
+ let backgroundColor = inputColors.bg;
761
+ let borderColor = inputColors.border;
762
+ let outlineColor;
763
+ if (disabled) {
764
+ backgroundColor = inputColors.bgDisable;
765
+ borderColor = inputColors.borderDisable;
766
+ } else if (isError) {
767
+ outlineColor = theme.colors.border.alert;
768
+ if (isFocus) backgroundColor = theme.colors.control.focus.bg;
769
+ } else if (isFocus) {
770
+ backgroundColor = theme.colors.control.focus.bg;
771
+ outlineColor = theme.colors.border.brand;
772
+ }
773
+ const textColor = disabled ? inputColors.textDisable : inputColors.text;
774
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
775
+ Box,
776
+ {
777
+ flexDirection: "column",
778
+ gap: sizeStyles.fieldGap,
779
+ width: "100%",
780
+ testID,
781
+ children: [
782
+ label && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Box, { as: "label", id: labelId, htmlFor: inputId, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
783
+ Text,
784
+ {
785
+ color: theme.colors.content.secondary,
786
+ fontSize: sizeStyles.fontSize - 2,
787
+ fontWeight: "500",
788
+ children: label
789
+ }
790
+ ) }),
791
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
792
+ Box,
793
+ {
794
+ backgroundColor,
795
+ borderColor,
796
+ borderWidth: 1,
797
+ minHeight: sizeStyles.height,
798
+ paddingVertical,
799
+ paddingHorizontal: sizeStyles.paddingHorizontal,
800
+ flexDirection: "row",
801
+ flexWrap: "wrap",
802
+ alignItems: "center",
803
+ gap: 4,
804
+ cursor: disabled ? "not-allowed" : "text",
805
+ onPress: () => {
806
+ if (!disabled) inputRef.current?.focus();
807
+ },
808
+ "data-testid": "tag-input__container",
809
+ style: {
810
+ borderRadius,
811
+ ...outlineColor && isWeb ? { outline: `1px solid ${outlineColor}`, outlineOffset: "-1px" } : outlineColor && !isWeb ? { borderColor: outlineColor } : {}
812
+ },
813
+ hoverStyle: !disabled && !isFocus && !isError ? {
814
+ backgroundColor: inputColors.bgHover,
815
+ borderColor: inputColors.borderHover
816
+ } : void 0,
817
+ children: [
818
+ value.map((item, index2) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
819
+ Tag,
820
+ {
821
+ size: chipSize,
822
+ type: "outlined",
823
+ onRemove: disabled ? void 0 : () => removeAt(index2),
824
+ removeLabel: `Remove ${item}`,
825
+ testID: "tag-input__chip",
826
+ themeMode,
827
+ themeProductContext,
828
+ children: item
829
+ },
830
+ `${index2}-${item}`
831
+ )),
832
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Box, { flex: 1, minWidth: 80, height: chipHeight, justifyContent: "center", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
833
+ InputPrimitive,
834
+ {
835
+ ref: inputRef,
836
+ id: inputId,
837
+ name,
838
+ value: draft,
839
+ placeholder: value.length === 0 ? placeholder : void 0,
840
+ onChange: handleChange,
841
+ onKeyDown: handleKeyDown,
842
+ onFocus: handleFocus,
843
+ onBlur: handleBlur,
844
+ disabled,
845
+ type: "text",
846
+ color: textColor,
847
+ fontSize: sizeStyles.fontSize,
848
+ fontFamily: theme.fonts.body,
849
+ placeholderTextColor: inputColors.placeholder,
850
+ "aria-invalid": isError || void 0,
851
+ "aria-describedby": errorMessage ? errorId : void 0,
852
+ "aria-labelledby": label ? labelId : void 0,
853
+ "aria-label": !label ? ariaLabel : void 0,
854
+ "aria-disabled": disabled || void 0,
855
+ "data-testid": "tag-input__field"
856
+ }
857
+ ) })
858
+ ]
859
+ }
860
+ ),
861
+ errorMessage && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
862
+ Text,
863
+ {
864
+ id: errorId,
865
+ role: "alert",
866
+ color: theme.colors.content.alert.primary,
867
+ fontSize: sizeStyles.fontSize - 2,
868
+ style: { lineHeight: sizeStyles.lineHeight + "px" },
869
+ children: errorMessage
870
+ }
871
+ )
872
+ ]
873
+ }
874
+ );
875
+ }
876
+ );
877
+ TagInput.displayName = "TagInput";
521
878
  // Annotate the CommonJS export names for ESM import in node:
522
879
  0 && (module.exports = {
523
- Tag
880
+ Tag,
881
+ TagInput
524
882
  });
525
883
  //# sourceMappingURL=index.js.map