@xsolla/xui-button 0.190.0 → 0.192.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.
@@ -170,8 +170,15 @@ interface IconButtonProps extends ThemeOverrideProps {
170
170
  declare const IconButton: React.FC<IconButtonProps>;
171
171
 
172
172
  interface FlexButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "type"> {
173
- /** Button content */
174
- children: ReactNode;
173
+ /**
174
+ * Button label. Omit for icon-only buttons (provide `aria-label`).
175
+ *
176
+ * An icon may also be passed directly as `children` instead of via
177
+ * `iconLeft`/`iconRight`. When `children` carries no text, the button is
178
+ * treated as icon-only and renders as a square sized from the design-system
179
+ * height token for `size`.
180
+ */
181
+ children?: ReactNode;
175
182
  /** Visual variant of the button */
176
183
  variant?: "brand" | "primary" | "secondary" | "tertiary" | "brandExtra" | "inverse";
177
184
  /** Size of the button */
@@ -190,6 +197,9 @@ interface FlexButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElem
190
197
  * The component applies padding via an inline style, which overrides any CSS
191
198
  * class or `style` prop a consumer passes, so this prop is the supported way
192
199
  * to render a zero-padding FlexButton (e.g. an inline text action).
200
+ *
201
+ * Icon-only buttons keep their square dimensions when `noPadding` is set —
202
+ * only the inner padding is removed.
193
203
  * @default false
194
204
  */
195
205
  noPadding?: boolean;
@@ -224,6 +234,19 @@ interface FlexButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElem
224
234
  *
225
235
  * Renders as a semantic `<button>` element with full ARIA support.
226
236
  *
237
+ * ## Icon-only buttons
238
+ *
239
+ * A FlexButton is icon-only when it has no text label and an icon is supplied
240
+ * either as `children` or via `iconLeft`/`iconRight`. Icon-only buttons render
241
+ * as a square whose side equals `theme.sizing.flexButton(size).height`, so they
242
+ * line up with the Modal header icon slots (36x36 at `size="xl"`).
243
+ *
244
+ * ```tsx
245
+ * <FlexButton variant="secondary" size="xl" aria-label="Go back">
246
+ * <BackwardAlt variant="line" />
247
+ * </FlexButton>
248
+ * ```
249
+ *
227
250
  * ## Accessibility Features
228
251
  *
229
252
  * - **Semantic HTML**: Renders as a native `<button>` element
package/native/index.d.ts CHANGED
@@ -170,8 +170,15 @@ interface IconButtonProps extends ThemeOverrideProps {
170
170
  declare const IconButton: React.FC<IconButtonProps>;
171
171
 
172
172
  interface FlexButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "type"> {
173
- /** Button content */
174
- children: ReactNode;
173
+ /**
174
+ * Button label. Omit for icon-only buttons (provide `aria-label`).
175
+ *
176
+ * An icon may also be passed directly as `children` instead of via
177
+ * `iconLeft`/`iconRight`. When `children` carries no text, the button is
178
+ * treated as icon-only and renders as a square sized from the design-system
179
+ * height token for `size`.
180
+ */
181
+ children?: ReactNode;
175
182
  /** Visual variant of the button */
176
183
  variant?: "brand" | "primary" | "secondary" | "tertiary" | "brandExtra" | "inverse";
177
184
  /** Size of the button */
@@ -190,6 +197,9 @@ interface FlexButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElem
190
197
  * The component applies padding via an inline style, which overrides any CSS
191
198
  * class or `style` prop a consumer passes, so this prop is the supported way
192
199
  * to render a zero-padding FlexButton (e.g. an inline text action).
200
+ *
201
+ * Icon-only buttons keep their square dimensions when `noPadding` is set —
202
+ * only the inner padding is removed.
193
203
  * @default false
194
204
  */
195
205
  noPadding?: boolean;
@@ -224,6 +234,19 @@ interface FlexButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElem
224
234
  *
225
235
  * Renders as a semantic `<button>` element with full ARIA support.
226
236
  *
237
+ * ## Icon-only buttons
238
+ *
239
+ * A FlexButton is icon-only when it has no text label and an icon is supplied
240
+ * either as `children` or via `iconLeft`/`iconRight`. Icon-only buttons render
241
+ * as a square whose side equals `theme.sizing.flexButton(size).height`, so they
242
+ * line up with the Modal header icon slots (36x36 at `size="xl"`).
243
+ *
244
+ * ```tsx
245
+ * <FlexButton variant="secondary" size="xl" aria-label="Go back">
246
+ * <BackwardAlt variant="line" />
247
+ * </FlexButton>
248
+ * ```
249
+ *
227
250
  * ## Accessibility Features
228
251
  *
229
252
  * - **Semantic HTML**: Renders as a native `<button>` element
package/native/index.js CHANGED
@@ -808,9 +808,27 @@ var IconButton = ({
808
808
  IconButton.displayName = "IconButton";
809
809
 
810
810
  // src/FlexButton.tsx
811
- var import_react4 = require("react");
811
+ var import_react4 = __toESM(require("react"));
812
812
  var import_xui_core3 = require("@xsolla/xui-core");
813
813
  var import_jsx_runtime7 = require("react/jsx-runtime");
814
+ var FLEX_BUTTON_PADDING = 4;
815
+ var hasVisibleLabel = (node) => {
816
+ if (node == null || node === false || node === true) return false;
817
+ if (typeof node === "string") return node.trim().length > 0;
818
+ if (typeof node === "number") return true;
819
+ if (Array.isArray(node)) return node.some(hasVisibleLabel);
820
+ if (import_react4.default.isValidElement(node)) {
821
+ const { children: elementChildren } = node.props ?? {};
822
+ return hasVisibleLabel(elementChildren);
823
+ }
824
+ return true;
825
+ };
826
+ var hasRenderableContent = (node) => {
827
+ if (node == null || node === false || node === true) return false;
828
+ if (typeof node === "string") return node.trim().length > 0;
829
+ if (Array.isArray(node)) return node.some(hasRenderableContent);
830
+ return true;
831
+ };
814
832
  var ICON_SIZES = {
815
833
  xs: 12,
816
834
  sm: 14,
@@ -1010,6 +1028,10 @@ var FlexButton = ({
1010
1028
  const fontSize = FONT_SIZES[size];
1011
1029
  const borderRadius = BORDER_RADIUS[size];
1012
1030
  const lineHeight = LINE_HEIGHTS[size];
1031
+ const showTextLabel = hasVisibleLabel(children);
1032
+ const hasIconChildren = !showTextLabel && hasRenderableContent(children);
1033
+ const isIconOnly = !loading && !showTextLabel && Boolean(iconLeft || iconRight || hasIconChildren);
1034
+ const iconOnlyHitArea = theme.sizing.flexButton(size).height;
1013
1035
  const handleMouseEnter = () => {
1014
1036
  if (!isDisabled) {
1015
1037
  isMouseOverRef.current = true;
@@ -1076,8 +1098,9 @@ var FlexButton = ({
1076
1098
  display: "inline-flex",
1077
1099
  alignItems: "center",
1078
1100
  justifyContent: "center",
1079
- gap: "2px",
1080
- padding: noPadding ? "0px" : "4px",
1101
+ gap: isIconOnly ? 0 : "2px",
1102
+ boxSizing: "border-box",
1103
+ padding: noPadding ? "0px" : `${FLEX_BUTTON_PADDING}px`,
1081
1104
  backgroundColor: loading ? "transparent" : colors.bg,
1082
1105
  color: colors.text,
1083
1106
  border: "none",
@@ -1086,18 +1109,30 @@ var FlexButton = ({
1086
1109
  cursor: isDisabled ? "not-allowed" : "pointer",
1087
1110
  fontSize: `${fontSize}px`,
1088
1111
  fontWeight: 500,
1089
- lineHeight,
1112
+ lineHeight: isIconOnly ? 0 : lineHeight,
1090
1113
  fontFamily: "inherit",
1091
1114
  transition: "background-color 100ms ease-in-out, color 100ms ease-in-out, box-shadow 100ms ease-in-out",
1092
1115
  outline: "none",
1093
1116
  boxShadow: combinedBoxShadow,
1094
- opacity: isDisabled && !loading ? 0.6 : 1
1117
+ opacity: isDisabled && !loading ? 0.6 : 1,
1118
+ ...isIconOnly && {
1119
+ width: iconOnlyHitArea,
1120
+ height: iconOnlyHitArea,
1121
+ minWidth: iconOnlyHitArea,
1122
+ minHeight: iconOnlyHitArea,
1123
+ flexShrink: 0
1124
+ }
1095
1125
  };
1096
1126
  const contentStyle = {
1097
1127
  display: "flex",
1098
1128
  alignItems: "center",
1099
1129
  justifyContent: "center",
1100
- gap: "2px"
1130
+ gap: isIconOnly ? 0 : "2px",
1131
+ ...isIconOnly && {
1132
+ width: "100%",
1133
+ height: "100%",
1134
+ lineHeight: 0
1135
+ }
1101
1136
  };
1102
1137
  const spinnerStyle = {
1103
1138
  display: "flex",
@@ -1136,7 +1171,8 @@ var FlexButton = ({
1136
1171
  "data-testid": testID || "flex-button",
1137
1172
  children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: contentStyle, children: loading ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: spinnerStyle, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Spinner, { size: spinnerSize, color: spinnerColor }) }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1138
1173
  iconLeft && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Icon, { size: iconSize, color: colors.text, children: iconLeft }),
1139
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children }),
1174
+ showTextLabel && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children }),
1175
+ hasIconChildren && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Icon, { size: iconSize, color: colors.text, children }),
1140
1176
  iconRight && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Icon, { size: iconSize, color: colors.text, children: iconRight })
1141
1177
  ] }) })
1142
1178
  }