@edux-design/buttons 0.1.0 → 0.1.2
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/CHANGELOG.md +12 -0
- package/dist/index.d.mts +71 -1
- package/dist/index.d.ts +71 -1
- package/dist/index.js +290 -8
- package/dist/index.mjs +303 -15
- package/package.json +1 -1
- package/src/demo/Button.stories.jsx +20 -1
- package/src/demo/ButtonCounter.stories.jsx +101 -0
- package/src/elements/Button.jsx +23 -7
- package/src/elements/Button.test.jsx +41 -0
- package/src/elements/ButtonCounter.jsx +341 -0
- package/src/elements/ButtonCounter.test.jsx +100 -0
- package/src/index.js +1 -0
package/dist/index.mjs
CHANGED
|
@@ -59,10 +59,12 @@ var IconButton = forwardRef(
|
|
|
59
59
|
// src/elements/Button.jsx
|
|
60
60
|
import React2, { forwardRef as forwardRef2 } from "react";
|
|
61
61
|
import { cx as cx2 } from "@edux-design/utils";
|
|
62
|
-
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
62
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
63
63
|
var Button = forwardRef2(
|
|
64
64
|
({
|
|
65
65
|
children,
|
|
66
|
+
startIcon,
|
|
67
|
+
endIcon,
|
|
66
68
|
size = "medium",
|
|
67
69
|
isSecondary,
|
|
68
70
|
isDanger,
|
|
@@ -73,19 +75,27 @@ var Button = forwardRef2(
|
|
|
73
75
|
...props
|
|
74
76
|
}, ref) => {
|
|
75
77
|
let variant = "primary";
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
if (isDanger
|
|
79
|
-
|
|
78
|
+
if (disabled) {
|
|
79
|
+
variant = "disabled";
|
|
80
|
+
} else if (isDanger && isBare) {
|
|
81
|
+
variant = "dangerBare";
|
|
82
|
+
} else if (isDanger) {
|
|
83
|
+
variant = "danger";
|
|
84
|
+
} else if (isSecondary) {
|
|
85
|
+
variant = "secondary";
|
|
86
|
+
} else if (isBare) {
|
|
87
|
+
variant = "bare";
|
|
88
|
+
}
|
|
80
89
|
if (process.env.NODE_ENV !== "production") {
|
|
81
90
|
const count = [isSecondary, isDanger, isBare].filter(Boolean).length;
|
|
82
|
-
|
|
91
|
+
const hasAllowedCombo = isDanger && isBare && count === 2;
|
|
92
|
+
if (count > 1 && !hasAllowedCombo) {
|
|
83
93
|
console.warn(
|
|
84
94
|
"[Button]: Multiple variant props passed. Using the highest priority only."
|
|
85
95
|
);
|
|
86
96
|
}
|
|
87
97
|
}
|
|
88
|
-
const defaults = "flex justify-center items-center focus:shadow-focus focus-visible:shadow-focus outline-none transition-all duration-300 ease-in-out cursor-pointer";
|
|
98
|
+
const defaults = "flex justify-center items-center gap-8 focus:shadow-focus focus-visible:shadow-focus outline-none transition-all duration-300 ease-in-out cursor-pointer";
|
|
89
99
|
const fonts = "text-bold tracking-normal hover:underline hover:underline-offset-(--underline-offset-btn) active:underline active:underline-offset-(--underline-offset-btn)";
|
|
90
100
|
const sizes = {
|
|
91
101
|
small: "h-[36px] py-8 px-16 rounded-lg text-sm",
|
|
@@ -97,11 +107,12 @@ var Button = forwardRef2(
|
|
|
97
107
|
primary: "border-2 border-border-primary-emphasis bg-bg-primary-base text-white hover:bg-border-focus hover:text-fg-base hover:border-border-emphasis hover:shadow-md active:bg-border-focus active:bg-bg-focus-overlay active:text-fg-base",
|
|
98
108
|
secondary: "border-2 border-border-primary-base bg-bg-base text-fg-primary-emphasis hover:shadow-md hover:bg-bg-primary-subtle active:bg-bg-primary-subtle-90",
|
|
99
109
|
bare: "bg-bg-base text-fg-primary-emphasis hover:bg-bg-minimal active:bg-bg-minimal-5",
|
|
110
|
+
dangerBare: "bg-bg-base text-fg-danger-emphasis hover:bg-bg-danger-subtle active:bg-bg-danger-subtle",
|
|
100
111
|
danger: "bg-bg-danger-base border-2 border-border-danger-emphasis text-white hover:bg-bg-danger-base-5 active:bg-bg-danger-base-10",
|
|
101
112
|
disabled: "border-2 border-border-subtle bg-bg-inactive text-fg-inactive hover:no-underline"
|
|
102
113
|
};
|
|
103
114
|
const STYLES = cx2(defaults, sizes[size], width, fonts, variants[variant]);
|
|
104
|
-
return /* @__PURE__ */
|
|
115
|
+
return /* @__PURE__ */ jsxs(
|
|
105
116
|
"button",
|
|
106
117
|
{
|
|
107
118
|
role: "button",
|
|
@@ -111,7 +122,11 @@ var Button = forwardRef2(
|
|
|
111
122
|
className: STYLES,
|
|
112
123
|
ref,
|
|
113
124
|
...props,
|
|
114
|
-
children
|
|
125
|
+
children: [
|
|
126
|
+
startIcon,
|
|
127
|
+
children ? /* @__PURE__ */ jsx2("span", { children }) : null,
|
|
128
|
+
endIcon
|
|
129
|
+
]
|
|
115
130
|
}
|
|
116
131
|
);
|
|
117
132
|
}
|
|
@@ -128,7 +143,7 @@ var MenuButtonContext = createContext();
|
|
|
128
143
|
import { cx as cx3 } from "@edux-design/utils";
|
|
129
144
|
|
|
130
145
|
// ../icons/dist/index.mjs
|
|
131
|
-
import { jsxs, jsx as jsx3 } from "react/jsx-runtime";
|
|
146
|
+
import { jsxs as jsxs2, jsx as jsx3 } from "react/jsx-runtime";
|
|
132
147
|
var SvgChevron = (props) => /* @__PURE__ */ jsx3(
|
|
133
148
|
"svg",
|
|
134
149
|
{
|
|
@@ -152,7 +167,7 @@ var SvgChevron = (props) => /* @__PURE__ */ jsx3(
|
|
|
152
167
|
var Chevron_default = SvgChevron;
|
|
153
168
|
|
|
154
169
|
// src/elements/MenuButton.jsx
|
|
155
|
-
import { jsx as jsx4, jsxs as
|
|
170
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
156
171
|
function MenuButton({ children, size }) {
|
|
157
172
|
const SPLIT_BUTTON_STYLES = cx3(
|
|
158
173
|
"w-[36px] border-l-[1px] border-l-black flex flex-row justify-center items-center",
|
|
@@ -161,7 +176,7 @@ function MenuButton({ children, size }) {
|
|
|
161
176
|
"h-[40px] py-2": size === "medium" || size === void 0
|
|
162
177
|
}
|
|
163
178
|
);
|
|
164
|
-
return /* @__PURE__ */
|
|
179
|
+
return /* @__PURE__ */ jsxs3(MenuButtonContext.Provider, { value: true, children: [
|
|
165
180
|
/* @__PURE__ */ jsx4("div", { className: SPLIT_BUTTON_STYLES, children }),
|
|
166
181
|
/* @__PURE__ */ jsx4(Chevron_default, {})
|
|
167
182
|
] });
|
|
@@ -178,7 +193,7 @@ import React5, {
|
|
|
178
193
|
useState
|
|
179
194
|
} from "react";
|
|
180
195
|
import { cx as cx4, filterChildrenByDisplayName } from "@edux-design/utils";
|
|
181
|
-
import { jsx as jsx5, jsxs as
|
|
196
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
182
197
|
var SIZE_STYLES = {
|
|
183
198
|
small: {
|
|
184
199
|
button: "text-xs px-12 py-6 rounded-md "
|
|
@@ -371,7 +386,7 @@ var ButtonGroup = forwardRef3(function ButtonGroup2({
|
|
|
371
386
|
ref: setContainerRef,
|
|
372
387
|
role: "radiogroup",
|
|
373
388
|
className: rootClasses,
|
|
374
|
-
children: /* @__PURE__ */
|
|
389
|
+
children: /* @__PURE__ */ jsxs4("div", { className: trackClasses, children: [
|
|
375
390
|
/* @__PURE__ */ jsx5(
|
|
376
391
|
"span",
|
|
377
392
|
{
|
|
@@ -434,7 +449,7 @@ var ButtonGroup = forwardRef3(function ButtonGroup2({
|
|
|
434
449
|
const hasStartAdornment = option.startAdornment !== void 0 && option.startAdornment !== null;
|
|
435
450
|
const endAdornment = option.endAdornment ?? option.endIcon;
|
|
436
451
|
const hasEndAdornment = endAdornment !== void 0 && endAdornment !== null;
|
|
437
|
-
return /* @__PURE__ */
|
|
452
|
+
return /* @__PURE__ */ jsxs4(
|
|
438
453
|
"button",
|
|
439
454
|
{
|
|
440
455
|
ref: registerButtonRef(option.value),
|
|
@@ -480,8 +495,281 @@ function ButtonGroupItem(_props) {
|
|
|
480
495
|
return null;
|
|
481
496
|
}
|
|
482
497
|
ButtonGroupItem.displayName = "ButtonGroupItem";
|
|
498
|
+
|
|
499
|
+
// src/elements/ButtonCounter.jsx
|
|
500
|
+
import React7, {
|
|
501
|
+
forwardRef as forwardRef4,
|
|
502
|
+
useCallback as useCallback2,
|
|
503
|
+
useEffect as useEffect2,
|
|
504
|
+
useMemo as useMemo2,
|
|
505
|
+
useRef as useRef2,
|
|
506
|
+
useState as useState2
|
|
507
|
+
} from "react";
|
|
508
|
+
import { cx as cx5 } from "@edux-design/utils";
|
|
509
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
510
|
+
var ButtonCounter = forwardRef4(function ButtonCounter2({
|
|
511
|
+
value,
|
|
512
|
+
defaultValue = 1,
|
|
513
|
+
onChange,
|
|
514
|
+
min = Number.NEGATIVE_INFINITY,
|
|
515
|
+
max = Number.POSITIVE_INFINITY,
|
|
516
|
+
step = 1,
|
|
517
|
+
children,
|
|
518
|
+
size = "medium",
|
|
519
|
+
isSecondary,
|
|
520
|
+
isDanger,
|
|
521
|
+
isBare,
|
|
522
|
+
isStretched,
|
|
523
|
+
disabled,
|
|
524
|
+
isEditable = false,
|
|
525
|
+
inputProps,
|
|
526
|
+
decrementIcon,
|
|
527
|
+
incrementIcon,
|
|
528
|
+
startIcon,
|
|
529
|
+
endIcon,
|
|
530
|
+
decrementButtonProps,
|
|
531
|
+
incrementButtonProps,
|
|
532
|
+
className,
|
|
533
|
+
...rest
|
|
534
|
+
}, ref) {
|
|
535
|
+
const isControlled = value !== void 0;
|
|
536
|
+
const [internalValue, setInternalValue] = useState2(defaultValue);
|
|
537
|
+
const currentValue = isControlled ? value : internalValue;
|
|
538
|
+
const clampedValue = useMemo2(() => {
|
|
539
|
+
const safe = Number.isFinite(currentValue) ? currentValue : defaultValue;
|
|
540
|
+
return Math.min(max, Math.max(min, safe));
|
|
541
|
+
}, [currentValue, defaultValue, max, min]);
|
|
542
|
+
const commitValue = useCallback2(
|
|
543
|
+
(next) => {
|
|
544
|
+
const clamped = Math.min(max, Math.max(min, next));
|
|
545
|
+
if (!isControlled) setInternalValue(clamped);
|
|
546
|
+
onChange == null ? void 0 : onChange(clamped);
|
|
547
|
+
},
|
|
548
|
+
[isControlled, max, min, onChange]
|
|
549
|
+
);
|
|
550
|
+
const decrementDisabled = !!disabled || !Number.isFinite(clampedValue) || clampedValue - step < min;
|
|
551
|
+
const incrementDisabled = !!disabled || !Number.isFinite(clampedValue) || clampedValue + step > max;
|
|
552
|
+
let variant = "primary";
|
|
553
|
+
if (disabled) {
|
|
554
|
+
variant = "disabled";
|
|
555
|
+
} else if (isDanger && isBare) {
|
|
556
|
+
variant = "dangerBare";
|
|
557
|
+
} else if (isDanger) {
|
|
558
|
+
variant = "danger";
|
|
559
|
+
} else if (isSecondary) {
|
|
560
|
+
variant = "secondary";
|
|
561
|
+
} else if (isBare) {
|
|
562
|
+
variant = "bare";
|
|
563
|
+
}
|
|
564
|
+
if (process.env.NODE_ENV !== "production") {
|
|
565
|
+
const count = [isSecondary, isDanger, isBare].filter(Boolean).length;
|
|
566
|
+
const hasAllowedCombo = isDanger && isBare && count === 2;
|
|
567
|
+
if (count > 1 && !hasAllowedCombo) {
|
|
568
|
+
console.warn(
|
|
569
|
+
"[ButtonCounter]: Multiple variant props passed. Using the highest priority only."
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
if (step <= 0) {
|
|
573
|
+
console.warn("[ButtonCounter]: step should be > 0.");
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const CONTROL_SIZES = {
|
|
577
|
+
small: {
|
|
578
|
+
button: "h-[36px] w-[36px] text-sm",
|
|
579
|
+
display: "h-[36px] min-w-[48px] px-12 text-sm"
|
|
580
|
+
},
|
|
581
|
+
medium: {
|
|
582
|
+
button: "h-[44px] w-[44px] text-base",
|
|
583
|
+
display: "h-[44px] min-w-[56px] px-12 text-base"
|
|
584
|
+
},
|
|
585
|
+
large: {
|
|
586
|
+
button: "h-[56px] w-[56px] text-lg",
|
|
587
|
+
display: "h-[56px] min-w-[72px] px-16 text-lg"
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
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";
|
|
591
|
+
const defaultsDisplay = "flex items-center justify-center text-bold tracking-normal";
|
|
592
|
+
const width = isStretched ? "w-full" : "w-fit-content";
|
|
593
|
+
const variants = {
|
|
594
|
+
primary: {
|
|
595
|
+
button: "border-2 border-border-primary-emphasis bg-bg-primary-base text-white hover:bg-border-focus hover:text-fg-base hover:border-border-emphasis hover:shadow-md active:bg-border-focus active:bg-bg-focus-overlay active:text-fg-base",
|
|
596
|
+
display: "border-y-2 border-x-2 border-border-primary-emphasis bg-bg-primary-base text-white"
|
|
597
|
+
},
|
|
598
|
+
secondary: {
|
|
599
|
+
button: "border-2 border-border-primary-base bg-bg-base text-fg-primary-emphasis hover:shadow-md hover:bg-bg-primary-subtle active:bg-bg-primary-subtle-90",
|
|
600
|
+
display: "border-y-2 border-x-2 border-border-primary-base bg-bg-base text-fg-primary-emphasis"
|
|
601
|
+
},
|
|
602
|
+
bare: {
|
|
603
|
+
button: "bg-bg-base text-fg-primary-emphasis hover:bg-bg-minimal active:bg-bg-minimal-5",
|
|
604
|
+
display: "bg-bg-base text-fg-primary-emphasis"
|
|
605
|
+
},
|
|
606
|
+
dangerBare: {
|
|
607
|
+
button: "bg-bg-base text-fg-danger-emphasis hover:bg-bg-danger-subtle active:bg-bg-danger-subtle",
|
|
608
|
+
display: "bg-bg-base text-fg-danger-emphasis"
|
|
609
|
+
},
|
|
610
|
+
danger: {
|
|
611
|
+
button: "bg-bg-danger-base border-2 border-border-danger-emphasis text-white hover:bg-bg-danger-base-5 active:bg-bg-danger-base-10",
|
|
612
|
+
display: "border-y-2 border-x-2 border-border-danger-emphasis bg-bg-danger-base text-white"
|
|
613
|
+
},
|
|
614
|
+
disabled: {
|
|
615
|
+
button: "border-2 border-border-subtle bg-bg-inactive text-fg-inactive hover:no-underline cursor-not-allowed",
|
|
616
|
+
display: "border-y-2 border-x-2 border-border-subtle bg-bg-inactive text-fg-inactive"
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
const sizeStyles = CONTROL_SIZES[size] ?? CONTROL_SIZES.medium;
|
|
620
|
+
const variantStyles = variants[variant] ?? variants.primary;
|
|
621
|
+
const leftButtonStyles = cx5(
|
|
622
|
+
defaultsButton,
|
|
623
|
+
sizeStyles.button,
|
|
624
|
+
"rounded-l-lg",
|
|
625
|
+
"relative z-10",
|
|
626
|
+
variantStyles.button,
|
|
627
|
+
variant !== "bare" && variant !== "dangerBare" ? "border-r-0" : null
|
|
628
|
+
);
|
|
629
|
+
const rightButtonStyles = cx5(
|
|
630
|
+
defaultsButton,
|
|
631
|
+
sizeStyles.button,
|
|
632
|
+
"rounded-r-lg",
|
|
633
|
+
variantStyles.button,
|
|
634
|
+
variant !== "bare" && variant !== "dangerBare" ? "border-l-0" : null
|
|
635
|
+
);
|
|
636
|
+
const displayStyles = cx5(
|
|
637
|
+
defaultsDisplay,
|
|
638
|
+
sizeStyles.display,
|
|
639
|
+
"relative z-0",
|
|
640
|
+
variantStyles.display,
|
|
641
|
+
isStretched ? "flex-1" : null
|
|
642
|
+
);
|
|
643
|
+
const effectiveDecrementIcon = decrementIcon ?? startIcon ?? "-";
|
|
644
|
+
const effectiveIncrementIcon = incrementIcon ?? endIcon ?? "+";
|
|
645
|
+
const [draftValue, setDraftValue] = useState2(String(clampedValue));
|
|
646
|
+
const [isEditing, setIsEditing] = useState2(false);
|
|
647
|
+
const inputRef = useRef2(null);
|
|
648
|
+
useEffect2(() => {
|
|
649
|
+
if (isEditing) return;
|
|
650
|
+
setDraftValue(String(clampedValue));
|
|
651
|
+
}, [clampedValue, isEditing]);
|
|
652
|
+
const commitDraft = useCallback2(() => {
|
|
653
|
+
const trimmed = String(draftValue).trim();
|
|
654
|
+
if (trimmed === "") {
|
|
655
|
+
setDraftValue(String(clampedValue));
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const next = Number(trimmed);
|
|
659
|
+
if (!Number.isFinite(next)) {
|
|
660
|
+
setDraftValue(String(clampedValue));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
commitValue(next);
|
|
664
|
+
}, [clampedValue, commitValue, draftValue]);
|
|
665
|
+
return /* @__PURE__ */ jsxs5(
|
|
666
|
+
"div",
|
|
667
|
+
{
|
|
668
|
+
ref,
|
|
669
|
+
className: cx5("inline-flex items-stretch", width, className),
|
|
670
|
+
...rest,
|
|
671
|
+
children: [
|
|
672
|
+
/* @__PURE__ */ jsx6(
|
|
673
|
+
"button",
|
|
674
|
+
{
|
|
675
|
+
...decrementButtonProps,
|
|
676
|
+
type: "button",
|
|
677
|
+
"aria-label": (decrementButtonProps == null ? void 0 : decrementButtonProps["aria-label"]) ?? "Decrease",
|
|
678
|
+
"aria-disabled": decrementDisabled,
|
|
679
|
+
disabled: decrementDisabled,
|
|
680
|
+
className: cx5(
|
|
681
|
+
leftButtonStyles,
|
|
682
|
+
decrementDisabled && variant !== "disabled" ? variants.disabled.button : null,
|
|
683
|
+
decrementButtonProps == null ? void 0 : decrementButtonProps.className
|
|
684
|
+
),
|
|
685
|
+
onClick: (event) => {
|
|
686
|
+
var _a;
|
|
687
|
+
(_a = decrementButtonProps == null ? void 0 : decrementButtonProps.onClick) == null ? void 0 : _a.call(decrementButtonProps, event);
|
|
688
|
+
if (event.defaultPrevented) return;
|
|
689
|
+
commitValue(clampedValue - step);
|
|
690
|
+
},
|
|
691
|
+
children: effectiveDecrementIcon
|
|
692
|
+
}
|
|
693
|
+
),
|
|
694
|
+
isEditable && children == null ? /* @__PURE__ */ jsx6(
|
|
695
|
+
"input",
|
|
696
|
+
{
|
|
697
|
+
...inputProps,
|
|
698
|
+
ref: inputRef,
|
|
699
|
+
type: "number",
|
|
700
|
+
inputMode: "numeric",
|
|
701
|
+
disabled: !!disabled,
|
|
702
|
+
"aria-label": (inputProps == null ? void 0 : inputProps["aria-label"]) ?? "Value",
|
|
703
|
+
className: cx5(
|
|
704
|
+
displayStyles,
|
|
705
|
+
"w-[44px] text-center outline-none bg-transparent text-fg-base",
|
|
706
|
+
"[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:m-0 [&::-webkit-inner-spin-button]:m-0",
|
|
707
|
+
"focus:shadow-focus focus-visible:shadow-focus focus:z-15 focus-within:z-15",
|
|
708
|
+
inputProps == null ? void 0 : inputProps.className
|
|
709
|
+
),
|
|
710
|
+
value: draftValue,
|
|
711
|
+
onChange: (event) => {
|
|
712
|
+
var _a;
|
|
713
|
+
(_a = inputProps == null ? void 0 : inputProps.onChange) == null ? void 0 : _a.call(inputProps, event);
|
|
714
|
+
if (event.defaultPrevented) return;
|
|
715
|
+
setDraftValue(event.target.value);
|
|
716
|
+
},
|
|
717
|
+
onFocus: (event) => {
|
|
718
|
+
var _a;
|
|
719
|
+
(_a = inputProps == null ? void 0 : inputProps.onFocus) == null ? void 0 : _a.call(inputProps, event);
|
|
720
|
+
if (event.defaultPrevented) return;
|
|
721
|
+
setIsEditing(true);
|
|
722
|
+
},
|
|
723
|
+
onBlur: (event) => {
|
|
724
|
+
var _a;
|
|
725
|
+
(_a = inputProps == null ? void 0 : inputProps.onBlur) == null ? void 0 : _a.call(inputProps, event);
|
|
726
|
+
if (event.defaultPrevented) return;
|
|
727
|
+
setIsEditing(false);
|
|
728
|
+
commitDraft();
|
|
729
|
+
},
|
|
730
|
+
onKeyDown: (event) => {
|
|
731
|
+
var _a;
|
|
732
|
+
(_a = inputProps == null ? void 0 : inputProps.onKeyDown) == null ? void 0 : _a.call(inputProps, event);
|
|
733
|
+
if (event.defaultPrevented) return;
|
|
734
|
+
if (event.key === "Enter") {
|
|
735
|
+
event.currentTarget.blur();
|
|
736
|
+
}
|
|
737
|
+
if (event.key === "Escape") {
|
|
738
|
+
setDraftValue(String(clampedValue));
|
|
739
|
+
event.currentTarget.blur();
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
) : /* @__PURE__ */ jsx6("div", { className: cx5(displayStyles, "select-none"), children: children ?? clampedValue }),
|
|
744
|
+
/* @__PURE__ */ jsx6(
|
|
745
|
+
"button",
|
|
746
|
+
{
|
|
747
|
+
...incrementButtonProps,
|
|
748
|
+
type: "button",
|
|
749
|
+
"aria-label": (incrementButtonProps == null ? void 0 : incrementButtonProps["aria-label"]) ?? "Increase",
|
|
750
|
+
"aria-disabled": incrementDisabled,
|
|
751
|
+
disabled: incrementDisabled,
|
|
752
|
+
className: cx5(
|
|
753
|
+
rightButtonStyles,
|
|
754
|
+
incrementDisabled && variant !== "disabled" ? variants.disabled.button : null,
|
|
755
|
+
incrementButtonProps == null ? void 0 : incrementButtonProps.className
|
|
756
|
+
),
|
|
757
|
+
onClick: (event) => {
|
|
758
|
+
var _a;
|
|
759
|
+
(_a = incrementButtonProps == null ? void 0 : incrementButtonProps.onClick) == null ? void 0 : _a.call(incrementButtonProps, event);
|
|
760
|
+
if (event.defaultPrevented) return;
|
|
761
|
+
commitValue(clampedValue + step);
|
|
762
|
+
},
|
|
763
|
+
children: effectiveIncrementIcon
|
|
764
|
+
}
|
|
765
|
+
)
|
|
766
|
+
]
|
|
767
|
+
}
|
|
768
|
+
);
|
|
769
|
+
});
|
|
483
770
|
export {
|
|
484
771
|
Button,
|
|
772
|
+
ButtonCounter,
|
|
485
773
|
ButtonGroup,
|
|
486
774
|
ButtonGroupItem,
|
|
487
775
|
IconButton,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Button, IconButton } from "./../index";
|
|
3
|
-
import { Close } from "@edux-design/icons";
|
|
3
|
+
import { Close, External, Warning } from "@edux-design/icons";
|
|
4
4
|
|
|
5
5
|
export default {
|
|
6
6
|
title: "Components/Button",
|
|
@@ -15,6 +15,8 @@ export default {
|
|
|
15
15
|
isBare: { control: "boolean" },
|
|
16
16
|
isStretched: { control: "boolean" },
|
|
17
17
|
disabled: { control: "boolean" },
|
|
18
|
+
startIcon: { control: false },
|
|
19
|
+
endIcon: { control: false },
|
|
18
20
|
children: { control: "text" },
|
|
19
21
|
},
|
|
20
22
|
};
|
|
@@ -48,6 +50,13 @@ Bare.args = {
|
|
|
48
50
|
isBare: true,
|
|
49
51
|
};
|
|
50
52
|
|
|
53
|
+
export const DangerBare = Template.bind({});
|
|
54
|
+
DangerBare.args = {
|
|
55
|
+
children: "Log out",
|
|
56
|
+
isBare: true,
|
|
57
|
+
isDanger: true,
|
|
58
|
+
};
|
|
59
|
+
|
|
51
60
|
export const Disabled = Template.bind({});
|
|
52
61
|
Disabled.args = {
|
|
53
62
|
children: "Disabled",
|
|
@@ -82,3 +91,13 @@ export const Default = () => (
|
|
|
82
91
|
<IconButton icon={<Close />} disabled aria-label="Disabled" />
|
|
83
92
|
</div>
|
|
84
93
|
);
|
|
94
|
+
|
|
95
|
+
export const WithIcons = () => (
|
|
96
|
+
<div className="flex gap-4 items-center">
|
|
97
|
+
<Button startIcon={<Warning />}>Start icon</Button>
|
|
98
|
+
<Button endIcon={<External />}>End icon</Button>
|
|
99
|
+
<Button isDanger isBare endIcon={<External />}>
|
|
100
|
+
Log out
|
|
101
|
+
</Button>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ButtonCounter } from "./../index";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: "Components/Button",
|
|
6
|
+
component: ButtonCounter,
|
|
7
|
+
argTypes: {
|
|
8
|
+
size: {
|
|
9
|
+
control: { type: "select" },
|
|
10
|
+
options: ["small", "medium", "large"],
|
|
11
|
+
},
|
|
12
|
+
isSecondary: { control: "boolean" },
|
|
13
|
+
isDanger: { control: "boolean" },
|
|
14
|
+
isBare: { control: "boolean" },
|
|
15
|
+
isStretched: { control: "boolean" },
|
|
16
|
+
disabled: { control: "boolean" },
|
|
17
|
+
isEditable: { control: "boolean" },
|
|
18
|
+
inputProps: { control: false },
|
|
19
|
+
defaultValue: { control: "number" },
|
|
20
|
+
min: { control: "number" },
|
|
21
|
+
max: { control: "number" },
|
|
22
|
+
step: { control: "number" },
|
|
23
|
+
value: { control: false },
|
|
24
|
+
onChange: { control: false },
|
|
25
|
+
decrementIcon: { control: false },
|
|
26
|
+
incrementIcon: { control: false },
|
|
27
|
+
decrementButtonProps: { control: false },
|
|
28
|
+
incrementButtonProps: { control: false },
|
|
29
|
+
children: { control: false },
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const Template = (args) => <ButtonCounter {...args} />;
|
|
34
|
+
|
|
35
|
+
export const PrimaryCounter = Template.bind({});
|
|
36
|
+
PrimaryCounter.args = {
|
|
37
|
+
defaultValue: 1,
|
|
38
|
+
size: "medium",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const EditableCounter = Template.bind({});
|
|
42
|
+
EditableCounter.args = {
|
|
43
|
+
defaultValue: 1,
|
|
44
|
+
isEditable: true,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const SecondaryCounter = Template.bind({});
|
|
48
|
+
SecondaryCounter.args = {
|
|
49
|
+
defaultValue: 1,
|
|
50
|
+
isSecondary: true,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const DangerCounter = Template.bind({});
|
|
54
|
+
DangerCounter.args = {
|
|
55
|
+
defaultValue: 1,
|
|
56
|
+
isDanger: true,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const BareCounter = Template.bind({});
|
|
60
|
+
BareCounter.args = {
|
|
61
|
+
defaultValue: 1,
|
|
62
|
+
isBare: true,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const DangerBareCounter = Template.bind({});
|
|
66
|
+
DangerBareCounter.args = {
|
|
67
|
+
defaultValue: 1,
|
|
68
|
+
isBare: true,
|
|
69
|
+
isDanger: true,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const DisabledCounter = Template.bind({});
|
|
73
|
+
DisabledCounter.args = {
|
|
74
|
+
defaultValue: 1,
|
|
75
|
+
disabled: true,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const SmallCounter = Template.bind({});
|
|
79
|
+
SmallCounter.args = {
|
|
80
|
+
defaultValue: 1,
|
|
81
|
+
size: "small",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const LargeCounter = Template.bind({});
|
|
85
|
+
LargeCounter.args = {
|
|
86
|
+
defaultValue: 1,
|
|
87
|
+
size: "large",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const StretchedCounter = Template.bind({});
|
|
91
|
+
StretchedCounter.args = {
|
|
92
|
+
defaultValue: 1,
|
|
93
|
+
isStretched: true,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const WithBoundsCounter = Template.bind({});
|
|
97
|
+
WithBoundsCounter.args = {
|
|
98
|
+
defaultValue: 2,
|
|
99
|
+
min: 0,
|
|
100
|
+
max: 3,
|
|
101
|
+
};
|
package/src/elements/Button.jsx
CHANGED
|
@@ -9,6 +9,8 @@ import { cx } from "@edux-design/utils";
|
|
|
9
9
|
*
|
|
10
10
|
* @typedef {React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
11
11
|
* children?: React.ReactNode,
|
|
12
|
+
* startIcon?: React.ReactNode,
|
|
13
|
+
* endIcon?: React.ReactNode,
|
|
12
14
|
* size?: "small"|"medium"|"large",
|
|
13
15
|
* isSecondary?: boolean,
|
|
14
16
|
* isDanger?: boolean,
|
|
@@ -39,6 +41,8 @@ export const Button = forwardRef(
|
|
|
39
41
|
(
|
|
40
42
|
{
|
|
41
43
|
children,
|
|
44
|
+
startIcon,
|
|
45
|
+
endIcon,
|
|
42
46
|
size = "medium",
|
|
43
47
|
isSecondary,
|
|
44
48
|
isDanger,
|
|
@@ -51,15 +55,23 @@ export const Button = forwardRef(
|
|
|
51
55
|
ref
|
|
52
56
|
) => {
|
|
53
57
|
let variant = "primary";
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
if (isDanger
|
|
57
|
-
|
|
58
|
+
if (disabled) {
|
|
59
|
+
variant = "disabled";
|
|
60
|
+
} else if (isDanger && isBare) {
|
|
61
|
+
variant = "dangerBare";
|
|
62
|
+
} else if (isDanger) {
|
|
63
|
+
variant = "danger";
|
|
64
|
+
} else if (isSecondary) {
|
|
65
|
+
variant = "secondary";
|
|
66
|
+
} else if (isBare) {
|
|
67
|
+
variant = "bare";
|
|
68
|
+
}
|
|
58
69
|
|
|
59
70
|
// handle conflicting props - the last bool wins
|
|
60
71
|
if (process.env.NODE_ENV !== "production") {
|
|
61
72
|
const count = [isSecondary, isDanger, isBare].filter(Boolean).length;
|
|
62
|
-
|
|
73
|
+
const hasAllowedCombo = isDanger && isBare && count === 2;
|
|
74
|
+
if (count > 1 && !hasAllowedCombo) {
|
|
63
75
|
console.warn(
|
|
64
76
|
"[Button]: Multiple variant props passed. Using the highest priority only."
|
|
65
77
|
);
|
|
@@ -67,7 +79,7 @@ export const Button = forwardRef(
|
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
const defaults =
|
|
70
|
-
"flex justify-center items-center focus:shadow-focus focus-visible:shadow-focus outline-none transition-all duration-300 ease-in-out cursor-pointer";
|
|
82
|
+
"flex justify-center items-center gap-8 focus:shadow-focus focus-visible:shadow-focus outline-none transition-all duration-300 ease-in-out cursor-pointer";
|
|
71
83
|
|
|
72
84
|
const fonts =
|
|
73
85
|
"text-bold tracking-normal hover:underline hover:underline-offset-(--underline-offset-btn) active:underline active:underline-offset-(--underline-offset-btn)";
|
|
@@ -86,6 +98,8 @@ export const Button = forwardRef(
|
|
|
86
98
|
secondary:
|
|
87
99
|
"border-2 border-border-primary-base bg-bg-base text-fg-primary-emphasis hover:shadow-md hover:bg-bg-primary-subtle active:bg-bg-primary-subtle-90",
|
|
88
100
|
bare: "bg-bg-base text-fg-primary-emphasis hover:bg-bg-minimal active:bg-bg-minimal-5",
|
|
101
|
+
dangerBare:
|
|
102
|
+
"bg-bg-base text-fg-danger-emphasis hover:bg-bg-danger-subtle active:bg-bg-danger-subtle",
|
|
89
103
|
danger:
|
|
90
104
|
"bg-bg-danger-base border-2 border-border-danger-emphasis text-white hover:bg-bg-danger-base-5 active:bg-bg-danger-base-10",
|
|
91
105
|
disabled:
|
|
@@ -104,7 +118,9 @@ export const Button = forwardRef(
|
|
|
104
118
|
ref={ref}
|
|
105
119
|
{...props}
|
|
106
120
|
>
|
|
107
|
-
{
|
|
121
|
+
{startIcon}
|
|
122
|
+
{children ? <span>{children}</span> : null}
|
|
123
|
+
{endIcon}
|
|
108
124
|
</button>
|
|
109
125
|
);
|
|
110
126
|
}
|
|
@@ -52,6 +52,18 @@ describe("Button", () => {
|
|
|
52
52
|
expect(screen.getByRole("button").className).toMatch(/bg-bg-danger-base/);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
it("applies danger bare variant when isDanger and isBare", () => {
|
|
56
|
+
render(
|
|
57
|
+
<Button isDanger isBare>
|
|
58
|
+
Danger Bare
|
|
59
|
+
</Button>
|
|
60
|
+
);
|
|
61
|
+
const btn = screen.getByRole("button");
|
|
62
|
+
expect(btn.className).toMatch(/text-fg-danger-emphasis/);
|
|
63
|
+
expect(btn.className).toMatch(/bg-bg-base/);
|
|
64
|
+
expect(btn.className).not.toMatch(/bg-bg-danger-base/);
|
|
65
|
+
});
|
|
66
|
+
|
|
55
67
|
it("applies disabled variant when disabled=true", () => {
|
|
56
68
|
render(<Button disabled>Disabled</Button>);
|
|
57
69
|
const btn = screen.getByRole("button");
|
|
@@ -76,4 +88,33 @@ describe("Button", () => {
|
|
|
76
88
|
|
|
77
89
|
warnSpy.mockRestore();
|
|
78
90
|
});
|
|
91
|
+
|
|
92
|
+
it("renders start and end icons when provided", () => {
|
|
93
|
+
render(
|
|
94
|
+
<Button
|
|
95
|
+
startIcon={<span data-testid="start-icon">S</span>}
|
|
96
|
+
endIcon={<span data-testid="end-icon">E</span>}
|
|
97
|
+
>
|
|
98
|
+
With icons
|
|
99
|
+
</Button>
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(screen.getByTestId("start-icon")).toBeInTheDocument();
|
|
103
|
+
expect(screen.getByTestId("end-icon")).toBeInTheDocument();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("does not warn for the danger bare combination", () => {
|
|
107
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
108
|
+
process.env.NODE_ENV = "development";
|
|
109
|
+
|
|
110
|
+
render(
|
|
111
|
+
<Button isDanger isBare>
|
|
112
|
+
Allowed
|
|
113
|
+
</Button>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
117
|
+
|
|
118
|
+
warnSpy.mockRestore();
|
|
119
|
+
});
|
|
79
120
|
});
|