@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.
package/web/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/web/index.js CHANGED
@@ -871,9 +871,27 @@ var IconButton = ({
871
871
  IconButton.displayName = "IconButton";
872
872
 
873
873
  // src/FlexButton.tsx
874
- var import_react5 = require("react");
874
+ var import_react5 = __toESM(require("react"));
875
875
  var import_xui_core3 = require("@xsolla/xui-core");
876
876
  var import_jsx_runtime7 = require("react/jsx-runtime");
877
+ var FLEX_BUTTON_PADDING = 4;
878
+ var hasVisibleLabel = (node) => {
879
+ if (node == null || node === false || node === true) return false;
880
+ if (typeof node === "string") return node.trim().length > 0;
881
+ if (typeof node === "number") return true;
882
+ if (Array.isArray(node)) return node.some(hasVisibleLabel);
883
+ if (import_react5.default.isValidElement(node)) {
884
+ const { children: elementChildren } = node.props ?? {};
885
+ return hasVisibleLabel(elementChildren);
886
+ }
887
+ return true;
888
+ };
889
+ var hasRenderableContent = (node) => {
890
+ if (node == null || node === false || node === true) return false;
891
+ if (typeof node === "string") return node.trim().length > 0;
892
+ if (Array.isArray(node)) return node.some(hasRenderableContent);
893
+ return true;
894
+ };
877
895
  var ICON_SIZES = {
878
896
  xs: 12,
879
897
  sm: 14,
@@ -1073,6 +1091,10 @@ var FlexButton = ({
1073
1091
  const fontSize = FONT_SIZES[size];
1074
1092
  const borderRadius = BORDER_RADIUS[size];
1075
1093
  const lineHeight = LINE_HEIGHTS[size];
1094
+ const showTextLabel = hasVisibleLabel(children);
1095
+ const hasIconChildren = !showTextLabel && hasRenderableContent(children);
1096
+ const isIconOnly = !loading && !showTextLabel && Boolean(iconLeft || iconRight || hasIconChildren);
1097
+ const iconOnlyHitArea = theme.sizing.flexButton(size).height;
1076
1098
  const handleMouseEnter = () => {
1077
1099
  if (!isDisabled) {
1078
1100
  isMouseOverRef.current = true;
@@ -1139,8 +1161,9 @@ var FlexButton = ({
1139
1161
  display: "inline-flex",
1140
1162
  alignItems: "center",
1141
1163
  justifyContent: "center",
1142
- gap: "2px",
1143
- padding: noPadding ? "0px" : "4px",
1164
+ gap: isIconOnly ? 0 : "2px",
1165
+ boxSizing: "border-box",
1166
+ padding: noPadding ? "0px" : `${FLEX_BUTTON_PADDING}px`,
1144
1167
  backgroundColor: loading ? "transparent" : colors.bg,
1145
1168
  color: colors.text,
1146
1169
  border: "none",
@@ -1149,18 +1172,30 @@ var FlexButton = ({
1149
1172
  cursor: isDisabled ? "not-allowed" : "pointer",
1150
1173
  fontSize: `${fontSize}px`,
1151
1174
  fontWeight: 500,
1152
- lineHeight,
1175
+ lineHeight: isIconOnly ? 0 : lineHeight,
1153
1176
  fontFamily: "inherit",
1154
1177
  transition: "background-color 100ms ease-in-out, color 100ms ease-in-out, box-shadow 100ms ease-in-out",
1155
1178
  outline: "none",
1156
1179
  boxShadow: combinedBoxShadow,
1157
- opacity: isDisabled && !loading ? 0.6 : 1
1180
+ opacity: isDisabled && !loading ? 0.6 : 1,
1181
+ ...isIconOnly && {
1182
+ width: iconOnlyHitArea,
1183
+ height: iconOnlyHitArea,
1184
+ minWidth: iconOnlyHitArea,
1185
+ minHeight: iconOnlyHitArea,
1186
+ flexShrink: 0
1187
+ }
1158
1188
  };
1159
1189
  const contentStyle = {
1160
1190
  display: "flex",
1161
1191
  alignItems: "center",
1162
1192
  justifyContent: "center",
1163
- gap: "2px"
1193
+ gap: isIconOnly ? 0 : "2px",
1194
+ ...isIconOnly && {
1195
+ width: "100%",
1196
+ height: "100%",
1197
+ lineHeight: 0
1198
+ }
1164
1199
  };
1165
1200
  const spinnerStyle = {
1166
1201
  display: "flex",
@@ -1199,7 +1234,8 @@ var FlexButton = ({
1199
1234
  "data-testid": testID || "flex-button",
1200
1235
  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: [
1201
1236
  iconLeft && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Icon, { size: iconSize, color: colors.text, children: iconLeft }),
1202
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children }),
1237
+ showTextLabel && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children }),
1238
+ hasIconChildren && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Icon, { size: iconSize, color: colors.text, children }),
1203
1239
  iconRight && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Icon, { size: iconSize, color: colors.text, children: iconRight })
1204
1240
  ] }) })
1205
1241
  }