@edux-design/buttons 0.1.4 → 0.2.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/dist/index.js CHANGED
@@ -65,12 +65,20 @@ var IconButton = (0, import_react.forwardRef)(
65
65
  "[IconButton]: Multiple variant props passed. Using the highest priority only."
66
66
  );
67
67
  }
68
+ const hasAccessibleName = Boolean(
69
+ props["aria-label"] || props["aria-labelledby"]
70
+ );
71
+ if (!hasAccessibleName) {
72
+ console.warn(
73
+ "[IconButton]: Provide an accessible name with aria-label or aria-labelledby."
74
+ );
75
+ }
68
76
  }
69
77
  const defaults = "flex justify-center items-center focus:shadow-focus focus-visible:shadow-focus outline-none transition-all duration-300 ease-in-out rounded-full cursor-pointer";
70
78
  const sizes = {
71
- small: "h-[36px] w-[36px] text-sm",
72
- medium: "h-[44px] w-[44px] text-base",
73
- large: "h-[56px] w-[56px] text-lg"
79
+ small: "size-control-sm text-sm",
80
+ medium: "size-control-md text-base",
81
+ large: "size-control-lg text-lg"
74
82
  };
75
83
  const variants = {
76
84
  primary: "border-2 border-border-primary-emphasis bg-bg-primary-base text-white hover:bg-border-focus hover:text-fg-focus hover:border-border-emphasis hover:shadow-md active:bg-border-focus active:bg-bg-focus-overlay active:text-fg-focus",
@@ -173,13 +181,7 @@ var Button = (0, import_react2.forwardRef)(
173
181
  );
174
182
 
175
183
  // src/elements/MenuButton.jsx
176
- var import_react4 = __toESM(require("react"));
177
-
178
- // src/util/MenuButtonContext.jsx
179
184
  var import_react3 = __toESM(require("react"));
180
- var MenuButtonContext = (0, import_react3.createContext)();
181
-
182
- // src/elements/MenuButton.jsx
183
185
  var import_utils3 = require("@edux-design/utils");
184
186
 
185
187
  // ../icons/dist/index.mjs
@@ -208,22 +210,41 @@ var Chevron_default = SvgChevron;
208
210
 
209
211
  // src/elements/MenuButton.jsx
210
212
  var import_jsx_runtime4 = require("react/jsx-runtime");
211
- function MenuButton({ children, size }) {
212
- const SPLIT_BUTTON_STYLES = (0, import_utils3.cx)(
213
- "w-[36px] border-l-[1px] border-l-black flex flex-row justify-center items-center",
213
+ var MenuButton = (0, import_react3.forwardRef)(function MenuButton2({
214
+ children,
215
+ caret,
216
+ size = "medium",
217
+ className,
218
+ type = "button",
219
+ "aria-haspopup": ariaHasPopup = "menu",
220
+ ...props
221
+ }, ref) {
222
+ const styles = (0, import_utils3.cx)(
223
+ "inline-flex items-center justify-center gap-8 rounded-lg border-2 border-border-primary-base bg-bg-base px-16 text-fg-primary-emphasis transition-all duration-300 ease-in-out outline-none focus:shadow-focus focus-visible:shadow-focus disabled:cursor-not-allowed disabled:border-border-subtle disabled:bg-bg-inactive disabled:text-fg-inactive",
224
+ {
225
+ "min-h-control-sm text-sm": size === "small",
226
+ "min-h-control-md text-base": size === "medium"
227
+ },
228
+ className
229
+ );
230
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
231
+ "button",
214
232
  {
215
- "h-[32px]": size === "small",
216
- "h-[40px] py-2": size === "medium" || size === void 0
233
+ ...props,
234
+ ref,
235
+ type,
236
+ "aria-haspopup": ariaHasPopup,
237
+ className: styles,
238
+ children: [
239
+ children ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children }) : null,
240
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { "aria-hidden": "true", className: "flex items-center text-current", children: caret ?? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Chevron_default, {}) })
241
+ ]
217
242
  }
218
243
  );
219
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(MenuButtonContext.Provider, { value: true, children: [
220
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: SPLIT_BUTTON_STYLES, children }),
221
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Chevron_default, {})
222
- ] });
223
- }
244
+ });
224
245
 
225
246
  // src/elements/ButtonGroup.jsx
226
- var import_react5 = __toESM(require("react"));
247
+ var import_react4 = __toESM(require("react"));
227
248
  var import_utils4 = require("@edux-design/utils");
228
249
  var import_jsx_runtime5 = require("react/jsx-runtime");
229
250
  var SIZE_STYLES = {
@@ -238,8 +259,8 @@ var SIZE_STYLES = {
238
259
  }
239
260
  };
240
261
  var isBrowser = typeof window !== "undefined";
241
- var useIsomorphicLayoutEffect = isBrowser ? import_react5.useLayoutEffect : import_react5.useEffect;
242
- var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
262
+ var useIsomorphicLayoutEffect = isBrowser ? import_react4.useLayoutEffect : import_react4.useEffect;
263
+ var ButtonGroup = (0, import_react4.forwardRef)(function ButtonGroup2({
243
264
  value,
244
265
  defaultValue,
245
266
  onChange,
@@ -249,7 +270,7 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
249
270
  children,
250
271
  ...rest
251
272
  }, forwardedRef) {
252
- const normalizedOptions = (0, import_react5.useMemo)(() => {
273
+ const normalizedOptions = (0, import_react4.useMemo)(() => {
253
274
  const childItems = (0, import_utils4.filterChildrenByDisplayName)(children, "ButtonGroupItem");
254
275
  return childItems.map((child, index) => {
255
276
  const props = child.props || {};
@@ -264,30 +285,30 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
264
285
  }, [children]);
265
286
  const sizeStyles = SIZE_STYLES[size] ?? SIZE_STYLES.medium;
266
287
  const initialValue = value ?? defaultValue ?? (normalizedOptions.length ? normalizedOptions[0].value : void 0);
267
- const [internalValue, setInternalValue] = (0, import_react5.useState)(initialValue);
288
+ const [internalValue, setInternalValue] = (0, import_react4.useState)(initialValue);
268
289
  const isControlled = value !== void 0;
269
290
  const selectedValue = isControlled ? value : internalValue;
270
- (0, import_react5.useEffect)(() => {
291
+ (0, import_react4.useEffect)(() => {
271
292
  if (!isControlled && defaultValue !== void 0) {
272
293
  setInternalValue(defaultValue);
273
294
  }
274
295
  }, [defaultValue, isControlled]);
275
- (0, import_react5.useEffect)(() => {
296
+ (0, import_react4.useEffect)(() => {
276
297
  if (isControlled) return;
277
298
  if (normalizedOptions.length && !normalizedOptions.some((option) => option.value === internalValue)) {
278
299
  setInternalValue(normalizedOptions[0].value);
279
300
  }
280
301
  }, [internalValue, isControlled, normalizedOptions]);
281
- const buttonRefs = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
282
- const containerRef = (0, import_react5.useRef)(null);
283
- const indicatorRef = (0, import_react5.useRef)({ width: 0, height: 0, x: 0, y: 0 });
284
- const [indicator, setIndicator] = (0, import_react5.useState)({
302
+ const buttonRefs = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
303
+ const containerRef = (0, import_react4.useRef)(null);
304
+ const indicatorRef = (0, import_react4.useRef)({ width: 0, height: 0, x: 0, y: 0 });
305
+ const [indicator, setIndicator] = (0, import_react4.useState)({
285
306
  width: 0,
286
307
  height: 0,
287
308
  x: 0,
288
309
  y: 0
289
310
  });
290
- const setContainerRef = (0, import_react5.useCallback)(
311
+ const setContainerRef = (0, import_react4.useCallback)(
291
312
  (node) => {
292
313
  containerRef.current = node;
293
314
  if (typeof forwardedRef === "function") {
@@ -298,7 +319,7 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
298
319
  },
299
320
  [forwardedRef]
300
321
  );
301
- const registerButtonRef = (0, import_react5.useCallback)((optionValue) => {
322
+ const registerButtonRef = (0, import_react4.useCallback)((optionValue) => {
302
323
  return (node) => {
303
324
  if (!buttonRefs.current) return;
304
325
  if (node) {
@@ -308,7 +329,7 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
308
329
  }
309
330
  };
310
331
  }, []);
311
- const updateIndicator = (0, import_react5.useCallback)(() => {
332
+ const updateIndicator = (0, import_react4.useCallback)(() => {
312
333
  const container = containerRef.current;
313
334
  const target = buttonRefs.current.get(selectedValue);
314
335
  if (!container || !target) {
@@ -335,13 +356,13 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
335
356
  useIsomorphicLayoutEffect(() => {
336
357
  updateIndicator();
337
358
  }, [updateIndicator, normalizedOptions.length, size]);
338
- (0, import_react5.useEffect)(() => {
359
+ (0, import_react4.useEffect)(() => {
339
360
  if (!isBrowser) return;
340
361
  const handleResize = () => updateIndicator();
341
362
  window.addEventListener("resize", handleResize);
342
363
  return () => window.removeEventListener("resize", handleResize);
343
364
  }, [updateIndicator]);
344
- (0, import_react5.useEffect)(() => {
365
+ (0, import_react4.useEffect)(() => {
345
366
  if (!isBrowser) return;
346
367
  const node = containerRef.current;
347
368
  if (!node || typeof ResizeObserver === "undefined") return;
@@ -351,13 +372,13 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
351
372
  observer.observe(node);
352
373
  return () => observer.disconnect();
353
374
  }, [updateIndicator]);
354
- const focusOption = (0, import_react5.useCallback)((option) => {
375
+ const focusOption = (0, import_react4.useCallback)((option) => {
355
376
  const node = buttonRefs.current.get(option.value);
356
377
  if (node) {
357
378
  node.focus();
358
379
  }
359
380
  }, []);
360
- const handleSelect = (0, import_react5.useCallback)(
381
+ const handleSelect = (0, import_react4.useCallback)(
361
382
  (option) => {
362
383
  if (option.disabled || option.value === selectedValue) {
363
384
  return;
@@ -371,7 +392,7 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
371
392
  },
372
393
  [isControlled, onChange, selectedValue]
373
394
  );
374
- const getNextEnabledIndex = (0, import_react5.useCallback)(
395
+ const getNextEnabledIndex = (0, import_react4.useCallback)(
375
396
  (startIndex, direction) => {
376
397
  if (!normalizedOptions.length) return -1;
377
398
  let nextIndex = startIndex;
@@ -385,10 +406,10 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
385
406
  },
386
407
  [normalizedOptions]
387
408
  );
388
- const findFirstEnabledIndex = (0, import_react5.useCallback)(() => {
409
+ const findFirstEnabledIndex = (0, import_react4.useCallback)(() => {
389
410
  return normalizedOptions.findIndex((option) => !option.disabled);
390
411
  }, [normalizedOptions]);
391
- const findLastEnabledIndex = (0, import_react5.useCallback)(() => {
412
+ const findLastEnabledIndex = (0, import_react4.useCallback)(() => {
392
413
  for (let i = normalizedOptions.length - 1; i >= 0; i -= 1) {
393
414
  if (!normalizedOptions[i].disabled) {
394
415
  return i;
@@ -397,8 +418,15 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
397
418
  return -1;
398
419
  }, [normalizedOptions]);
399
420
  const indicatorVisible = indicator.width > 0 && indicator.height > 0;
421
+ const selectedIndex = normalizedOptions.findIndex(
422
+ (option) => option.value === selectedValue && !option.disabled
423
+ );
424
+ const firstEnabledIndex = normalizedOptions.findIndex(
425
+ (option) => !option.disabled
426
+ );
427
+ const activeTabStopIndex = selectedIndex >= 0 ? selectedIndex : firstEnabledIndex;
400
428
  const rootClasses = (0, import_utils4.cx)(
401
- "inline-flex h-[44px] rounded-lg border-2 border-border-subtle bg-bg-base/90 p-2 shadow-inner",
429
+ "inline-flex h-control-md rounded-lg border-2 border-border-subtle bg-bg-base/90 p-2 shadow-inner",
402
430
  isStretched ? "w-full" : "w-auto",
403
431
  className
404
432
  );
@@ -407,7 +435,7 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
407
435
  !isStretched && "w-auto"
408
436
  );
409
437
  const buttonBaseClasses = (0, import_utils4.cx)(
410
- "relative z-10 h-[36px] inline-flex items-center justify-center gap-2 whitespace-nowrap font-semibold tracking-tight transition-colors duration-200 ease-out focus-visible:outline-none focus-visible:shadow-focus motion-reduce:duration-0",
438
+ "relative z-10 h-control-sm inline-flex items-center justify-center gap-2 whitespace-nowrap font-semibold tracking-tight transition-colors duration-200 ease-out focus-visible:outline-none focus-visible:shadow-focus motion-reduce:duration-0",
411
439
  sizeStyles.button,
412
440
  isStretched ? "flex-1" : "w-auto"
413
441
  );
@@ -417,6 +445,7 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
417
445
  ...rest,
418
446
  ref: setContainerRef,
419
447
  role: "radiogroup",
448
+ "aria-orientation": "horizontal",
420
449
  className: rootClasses,
421
450
  children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: trackClasses, children: [
422
451
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
@@ -424,13 +453,13 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
424
453
  {
425
454
  "aria-hidden": "true",
426
455
  className: (0, import_utils4.cx)(
427
- "pointer-events-none h-[44px] absolute rounded-md border-2 border-border-primary-emphasis bg-bg-base shadow-[0_8px_20px_rgb(22_92_131/0.18)] transition-[transform,width,height] duration-300 ease-out motion-reduce:duration-0",
456
+ "pointer-events-none h-control-md absolute rounded-md border-2 border-border-primary-emphasis bg-bg-base shadow-[0_8px_20px_rgb(22_92_131/0.18)] transition-[transform,width,height] duration-300 ease-out motion-reduce:duration-0",
428
457
  !indicatorVisible && "opacity-0"
429
458
  ),
430
459
  style: {
431
460
  width: indicator.width || void 0,
432
461
  height: indicator.height || void 0,
433
- transform: `translate(${indicator.x - 4}px`
462
+ transform: `translate(${indicator.x - 4}px, ${indicator.y - 4}px)`
434
463
  }
435
464
  }
436
465
  ),
@@ -488,6 +517,7 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
488
517
  type: "button",
489
518
  role: "radio",
490
519
  "aria-checked": isActive,
520
+ tabIndex: option.disabled ? -1 : index === activeTabStopIndex ? 0 : -1,
491
521
  disabled: option.disabled,
492
522
  className: buttonClasses,
493
523
  onClick: () => handleSelect(option),
@@ -522,17 +552,17 @@ var ButtonGroup = (0, import_react5.forwardRef)(function ButtonGroup2({
522
552
  });
523
553
 
524
554
  // src/elements/ButtonGroupItem.jsx
525
- var import_react6 = __toESM(require("react"));
555
+ var import_react5 = __toESM(require("react"));
526
556
  function ButtonGroupItem(_props) {
527
557
  return null;
528
558
  }
529
559
  ButtonGroupItem.displayName = "ButtonGroupItem";
530
560
 
531
561
  // src/elements/ButtonCounter.jsx
532
- var import_react7 = __toESM(require("react"));
562
+ var import_react6 = __toESM(require("react"));
533
563
  var import_utils5 = require("@edux-design/utils");
534
564
  var import_jsx_runtime6 = require("react/jsx-runtime");
535
- var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
565
+ var ButtonCounter = (0, import_react6.forwardRef)(function ButtonCounter2({
536
566
  value,
537
567
  defaultValue = 1,
538
568
  onChange,
@@ -555,16 +585,17 @@ var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
555
585
  decrementButtonProps,
556
586
  incrementButtonProps,
557
587
  className,
588
+ "aria-label": ariaLabel,
558
589
  ...rest
559
590
  }, ref) {
560
591
  const isControlled = value !== void 0;
561
- const [internalValue, setInternalValue] = (0, import_react7.useState)(defaultValue);
592
+ const [internalValue, setInternalValue] = (0, import_react6.useState)(defaultValue);
562
593
  const currentValue = isControlled ? value : internalValue;
563
- const clampedValue = (0, import_react7.useMemo)(() => {
594
+ const clampedValue = (0, import_react6.useMemo)(() => {
564
595
  const safe = Number.isFinite(currentValue) ? currentValue : defaultValue;
565
596
  return Math.min(max, Math.max(min, safe));
566
597
  }, [currentValue, defaultValue, max, min]);
567
- const commitValue = (0, import_react7.useCallback)(
598
+ const commitValue = (0, import_react6.useCallback)(
568
599
  (next) => {
569
600
  const clamped = Math.min(max, Math.max(min, next));
570
601
  if (!isControlled) setInternalValue(clamped);
@@ -600,16 +631,16 @@ var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
600
631
  }
601
632
  const CONTROL_SIZES = {
602
633
  small: {
603
- button: "h-[36px] w-[36px] text-sm",
604
- display: "h-[36px] min-w-[48px] px-12 text-sm"
634
+ button: "size-control-sm text-sm",
635
+ display: "h-control-sm min-w-[48px] px-12 text-sm"
605
636
  },
606
637
  medium: {
607
- button: "h-[44px] w-[44px] text-base",
608
- display: "h-[44px] min-w-[56px] px-12 text-base"
638
+ button: "size-control-md text-base",
639
+ display: "h-control-md min-w-[56px] px-12 text-base"
609
640
  },
610
641
  large: {
611
- button: "h-[56px] w-[56px] text-lg",
612
- display: "h-[56px] min-w-[72px] px-16 text-lg"
642
+ button: "size-control-lg text-lg",
643
+ display: "h-control-lg min-w-[72px] px-16 text-lg"
613
644
  }
614
645
  };
615
646
  const defaultsButton = "flex justify-center items-center focus:shadow-focus focus-visible:shadow-focus outline-none transition-all duration-300 ease-in-out cursor-pointer";
@@ -667,14 +698,17 @@ var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
667
698
  );
668
699
  const effectiveDecrementIcon = decrementIcon ?? startIcon ?? "-";
669
700
  const effectiveIncrementIcon = incrementIcon ?? endIcon ?? "+";
670
- const [draftValue, setDraftValue] = (0, import_react7.useState)(String(clampedValue));
671
- const [isEditing, setIsEditing] = (0, import_react7.useState)(false);
672
- const inputRef = (0, import_react7.useRef)(null);
673
- (0, import_react7.useEffect)(() => {
701
+ const generatedValueId = (0, import_react6.useId)();
702
+ const valueElementId = (inputProps == null ? void 0 : inputProps.id) ?? generatedValueId;
703
+ const valueAriaLabel = (inputProps == null ? void 0 : inputProps["aria-label"]) ?? ariaLabel ?? "Value";
704
+ const [draftValue, setDraftValue] = (0, import_react6.useState)(String(clampedValue));
705
+ const [isEditing, setIsEditing] = (0, import_react6.useState)(false);
706
+ const inputRef = (0, import_react6.useRef)(null);
707
+ (0, import_react6.useEffect)(() => {
674
708
  if (isEditing) return;
675
709
  setDraftValue(String(clampedValue));
676
710
  }, [clampedValue, isEditing]);
677
- const commitDraft = (0, import_react7.useCallback)(() => {
711
+ const commitDraft = (0, import_react6.useCallback)(() => {
678
712
  const trimmed = String(draftValue).trim();
679
713
  if (trimmed === "") {
680
714
  setDraftValue(String(clampedValue));
@@ -700,6 +734,7 @@ var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
700
734
  ...decrementButtonProps,
701
735
  type: "button",
702
736
  "aria-label": (decrementButtonProps == null ? void 0 : decrementButtonProps["aria-label"]) ?? "Decrease",
737
+ "aria-controls": valueElementId,
703
738
  "aria-disabled": decrementDisabled,
704
739
  disabled: decrementDisabled,
705
740
  className: (0, import_utils5.cx)(
@@ -723,8 +758,9 @@ var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
723
758
  ref: inputRef,
724
759
  type: "number",
725
760
  inputMode: "numeric",
761
+ id: valueElementId,
726
762
  disabled: !!disabled,
727
- "aria-label": (inputProps == null ? void 0 : inputProps["aria-label"]) ?? "Value",
763
+ "aria-label": valueAriaLabel,
728
764
  className: (0, import_utils5.cx)(
729
765
  displayStyles,
730
766
  "w-[44px] text-center outline-none bg-transparent text-fg-base",
@@ -765,13 +801,24 @@ var ButtonCounter = (0, import_react7.forwardRef)(function ButtonCounter2({
765
801
  }
766
802
  }
767
803
  }
768
- ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: (0, import_utils5.cx)(displayStyles, "select-none"), children: children ?? clampedValue }),
804
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
805
+ "output",
806
+ {
807
+ id: valueElementId,
808
+ "aria-label": valueAriaLabel,
809
+ "aria-live": "polite",
810
+ "aria-atomic": "true",
811
+ className: (0, import_utils5.cx)(displayStyles, "select-none"),
812
+ children: children ?? clampedValue
813
+ }
814
+ ),
769
815
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
770
816
  "button",
771
817
  {
772
818
  ...incrementButtonProps,
773
819
  type: "button",
774
820
  "aria-label": (incrementButtonProps == null ? void 0 : incrementButtonProps["aria-label"]) ?? "Increase",
821
+ "aria-controls": valueElementId,
775
822
  "aria-disabled": incrementDisabled,
776
823
  disabled: incrementDisabled,
777
824
  className: (0, import_utils5.cx)(