@edux-design/buttons 0.2.0 → 0.3.1
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 +16 -0
- package/README.md +4 -0
- package/dist/index.d.mts +136 -5
- package/dist/index.d.ts +136 -5
- package/dist/index.js +296 -34
- package/dist/index.mjs +295 -33
- package/docs/button-group.md +25 -0
- package/docs/toolbar.md +146 -0
- package/package.json +8 -17
- package/dist/chevron-EVM4GL6D.svg +0 -4
- package/dist/chunk-M47FNL4P.mjs +0 -10
- package/dist/elements/Button.js +0 -39
- package/dist/elements/Button.mjs +0 -6
- package/src/demo/Button.stories.jsx +0 -103
- package/src/demo/ButtonCompatibility.stories.jsx +0 -115
- package/src/demo/ButtonCounter.stories.jsx +0 -101
- package/src/demo/ButtonGroup.stories.jsx +0 -67
- package/src/elements/Button.a11y.test.jsx +0 -60
- package/src/elements/Button.jsx +0 -127
- package/src/elements/Button.test.jsx +0 -120
- package/src/elements/ButtonCounter.jsx +0 -356
- package/src/elements/ButtonCounter.test.jsx +0 -121
- package/src/elements/ButtonGroup.jsx +0 -404
- package/src/elements/ButtonGroup.test.jsx +0 -130
- package/src/elements/ButtonGroupItem.jsx +0 -8
- package/src/elements/IconButton.jsx +0 -115
- package/src/elements/IconButton.test.jsx +0 -62
- package/src/elements/MenuButton.jsx +0 -68
- package/src/elements/MenuButton.test.jsx +0 -40
- package/src/elements/testA11y.js +0 -14
- package/src/index.js +0 -6
- package/src/util/MenuButtonContext.jsx +0 -3
- package/tsconfig.json +0 -12
package/dist/index.mjs
CHANGED
|
@@ -35,19 +35,19 @@ var IconButton = forwardRef(
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
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";
|
|
38
|
-
const
|
|
38
|
+
const sizes2 = {
|
|
39
39
|
small: "size-control-sm text-sm",
|
|
40
40
|
medium: "size-control-md text-base",
|
|
41
41
|
large: "size-control-lg text-lg"
|
|
42
42
|
};
|
|
43
43
|
const variants = {
|
|
44
|
-
primary: "border-2 border-border-primary-emphasis bg-bg-primary-base text-
|
|
44
|
+
primary: "border-2 border-border-primary-emphasis bg-bg-primary-base text-fg-invert 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",
|
|
45
45
|
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",
|
|
46
46
|
bare: "bg-bg-base text-fg-primary-emphasis hover:bg-bg-minimal active:bg-bg-minimal-5",
|
|
47
47
|
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",
|
|
48
48
|
disabled: "border-2 border-border-subtle bg-bg-inactive text-fg-inactive hover:no-underline cursor-not-allowed"
|
|
49
49
|
};
|
|
50
|
-
const STYLES = cx(defaults,
|
|
50
|
+
const STYLES = cx(defaults, sizes2[size], variants[variant], className);
|
|
51
51
|
return /* @__PURE__ */ jsx(
|
|
52
52
|
"button",
|
|
53
53
|
{
|
|
@@ -105,21 +105,21 @@ var Button = forwardRef2(
|
|
|
105
105
|
}
|
|
106
106
|
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";
|
|
107
107
|
const fonts = "text-bold tracking-normal hover:underline hover:underline-offset-(--underline-offset-btn) active:underline active:underline-offset-(--underline-offset-btn)";
|
|
108
|
-
const
|
|
108
|
+
const sizes2 = {
|
|
109
109
|
small: "h-control-sm py-8 px-16 rounded-lg text-sm",
|
|
110
110
|
medium: "h-control-md py-8 px-16 rounded-lg text-base",
|
|
111
111
|
large: "h-control-lg py-8 px-16 rounded-lg text-lg"
|
|
112
112
|
};
|
|
113
113
|
const width = isStretched ? "w-full" : "w-fit-content";
|
|
114
114
|
const variants = {
|
|
115
|
-
primary: "border-2 border-border-primary-emphasis bg-bg-primary-base text-
|
|
115
|
+
primary: "border-2 border-border-primary-emphasis bg-bg-primary-base text-fg-invert 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",
|
|
116
116
|
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",
|
|
117
117
|
bare: "bg-bg-base text-fg-primary-emphasis hover:bg-bg-minimal active:bg-bg-minimal-5",
|
|
118
118
|
dangerBare: "bg-bg-base text-fg-danger-emphasis hover:bg-bg-danger-subtle active:bg-bg-danger-subtle",
|
|
119
119
|
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",
|
|
120
120
|
disabled: "border-2 border-border-subtle bg-bg-inactive text-fg-inactive hover:no-underline"
|
|
121
121
|
};
|
|
122
|
-
const STYLES = cx2(defaults,
|
|
122
|
+
const STYLES = cx2(defaults, sizes2[size], width, fonts, variants[variant]);
|
|
123
123
|
return /* @__PURE__ */ jsxs(
|
|
124
124
|
"button",
|
|
125
125
|
{
|
|
@@ -575,7 +575,7 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
|
|
|
575
575
|
(next) => {
|
|
576
576
|
const clamped = Math.min(max, Math.max(min, next));
|
|
577
577
|
if (!isControlled) setInternalValue(clamped);
|
|
578
|
-
onChange
|
|
578
|
+
onChange?.(clamped);
|
|
579
579
|
},
|
|
580
580
|
[isControlled, max, min, onChange]
|
|
581
581
|
);
|
|
@@ -608,15 +608,15 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
|
|
|
608
608
|
const CONTROL_SIZES = {
|
|
609
609
|
small: {
|
|
610
610
|
button: "size-control-sm text-sm",
|
|
611
|
-
display: "h-control-sm min-w-
|
|
611
|
+
display: "h-control-sm min-w-48 px-12 text-sm"
|
|
612
612
|
},
|
|
613
613
|
medium: {
|
|
614
614
|
button: "size-control-md text-base",
|
|
615
|
-
display: "h-control-md min-w-
|
|
615
|
+
display: "h-control-md min-w-56 px-12 text-base"
|
|
616
616
|
},
|
|
617
617
|
large: {
|
|
618
618
|
button: "size-control-lg text-lg",
|
|
619
|
-
display: "h-control-lg min-w-
|
|
619
|
+
display: "h-control-lg min-w-72 px-16 text-lg"
|
|
620
620
|
}
|
|
621
621
|
};
|
|
622
622
|
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";
|
|
@@ -624,8 +624,8 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
|
|
|
624
624
|
const width = isStretched ? "w-full" : "w-fit-content";
|
|
625
625
|
const variants = {
|
|
626
626
|
primary: {
|
|
627
|
-
button: "border-2 border-border-primary-emphasis bg-bg-primary-base text-
|
|
628
|
-
display: "border-y-2 border-x-2 border-border-primary-emphasis bg-bg-primary-base text-
|
|
627
|
+
button: "border-2 border-border-primary-emphasis bg-bg-primary-base text-fg-invert 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",
|
|
628
|
+
display: "border-y-2 border-x-2 border-border-primary-emphasis bg-bg-primary-base text-fg-invert"
|
|
629
629
|
},
|
|
630
630
|
secondary: {
|
|
631
631
|
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",
|
|
@@ -675,8 +675,8 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
|
|
|
675
675
|
const effectiveDecrementIcon = decrementIcon ?? startIcon ?? "-";
|
|
676
676
|
const effectiveIncrementIcon = incrementIcon ?? endIcon ?? "+";
|
|
677
677
|
const generatedValueId = useId();
|
|
678
|
-
const valueElementId =
|
|
679
|
-
const valueAriaLabel =
|
|
678
|
+
const valueElementId = inputProps?.id ?? generatedValueId;
|
|
679
|
+
const valueAriaLabel = inputProps?.["aria-label"] ?? ariaLabel ?? "Value";
|
|
680
680
|
const [draftValue, setDraftValue] = useState2(String(clampedValue));
|
|
681
681
|
const [isEditing, setIsEditing] = useState2(false);
|
|
682
682
|
const inputRef = useRef2(null);
|
|
@@ -709,18 +709,17 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
|
|
|
709
709
|
{
|
|
710
710
|
...decrementButtonProps,
|
|
711
711
|
type: "button",
|
|
712
|
-
"aria-label":
|
|
712
|
+
"aria-label": decrementButtonProps?.["aria-label"] ?? "Decrease",
|
|
713
713
|
"aria-controls": valueElementId,
|
|
714
714
|
"aria-disabled": decrementDisabled,
|
|
715
715
|
disabled: decrementDisabled,
|
|
716
716
|
className: cx5(
|
|
717
717
|
leftButtonStyles,
|
|
718
718
|
decrementDisabled && variant !== "disabled" ? variants.disabled.button : null,
|
|
719
|
-
decrementButtonProps
|
|
719
|
+
decrementButtonProps?.className
|
|
720
720
|
),
|
|
721
721
|
onClick: (event) => {
|
|
722
|
-
|
|
723
|
-
(_a = decrementButtonProps == null ? void 0 : decrementButtonProps.onClick) == null ? void 0 : _a.call(decrementButtonProps, event);
|
|
722
|
+
decrementButtonProps?.onClick?.(event);
|
|
724
723
|
if (event.defaultPrevented) return;
|
|
725
724
|
commitValue(clampedValue - step);
|
|
726
725
|
},
|
|
@@ -739,34 +738,30 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
|
|
|
739
738
|
"aria-label": valueAriaLabel,
|
|
740
739
|
className: cx5(
|
|
741
740
|
displayStyles,
|
|
742
|
-
"w-
|
|
741
|
+
"w-control-md text-center outline-none bg-transparent text-fg-base",
|
|
743
742
|
"[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",
|
|
744
743
|
"focus:shadow-focus focus-visible:shadow-focus focus:z-15 focus-within:z-15",
|
|
745
|
-
inputProps
|
|
744
|
+
inputProps?.className
|
|
746
745
|
),
|
|
747
746
|
value: draftValue,
|
|
748
747
|
onChange: (event) => {
|
|
749
|
-
|
|
750
|
-
(_a = inputProps == null ? void 0 : inputProps.onChange) == null ? void 0 : _a.call(inputProps, event);
|
|
748
|
+
inputProps?.onChange?.(event);
|
|
751
749
|
if (event.defaultPrevented) return;
|
|
752
750
|
setDraftValue(event.target.value);
|
|
753
751
|
},
|
|
754
752
|
onFocus: (event) => {
|
|
755
|
-
|
|
756
|
-
(_a = inputProps == null ? void 0 : inputProps.onFocus) == null ? void 0 : _a.call(inputProps, event);
|
|
753
|
+
inputProps?.onFocus?.(event);
|
|
757
754
|
if (event.defaultPrevented) return;
|
|
758
755
|
setIsEditing(true);
|
|
759
756
|
},
|
|
760
757
|
onBlur: (event) => {
|
|
761
|
-
|
|
762
|
-
(_a = inputProps == null ? void 0 : inputProps.onBlur) == null ? void 0 : _a.call(inputProps, event);
|
|
758
|
+
inputProps?.onBlur?.(event);
|
|
763
759
|
if (event.defaultPrevented) return;
|
|
764
760
|
setIsEditing(false);
|
|
765
761
|
commitDraft();
|
|
766
762
|
},
|
|
767
763
|
onKeyDown: (event) => {
|
|
768
|
-
|
|
769
|
-
(_a = inputProps == null ? void 0 : inputProps.onKeyDown) == null ? void 0 : _a.call(inputProps, event);
|
|
764
|
+
inputProps?.onKeyDown?.(event);
|
|
770
765
|
if (event.defaultPrevented) return;
|
|
771
766
|
if (event.key === "Enter") {
|
|
772
767
|
event.currentTarget.blur();
|
|
@@ -793,18 +788,17 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
|
|
|
793
788
|
{
|
|
794
789
|
...incrementButtonProps,
|
|
795
790
|
type: "button",
|
|
796
|
-
"aria-label":
|
|
791
|
+
"aria-label": incrementButtonProps?.["aria-label"] ?? "Increase",
|
|
797
792
|
"aria-controls": valueElementId,
|
|
798
793
|
"aria-disabled": incrementDisabled,
|
|
799
794
|
disabled: incrementDisabled,
|
|
800
795
|
className: cx5(
|
|
801
796
|
rightButtonStyles,
|
|
802
797
|
incrementDisabled && variant !== "disabled" ? variants.disabled.button : null,
|
|
803
|
-
incrementButtonProps
|
|
798
|
+
incrementButtonProps?.className
|
|
804
799
|
),
|
|
805
800
|
onClick: (event) => {
|
|
806
|
-
|
|
807
|
-
(_a = incrementButtonProps == null ? void 0 : incrementButtonProps.onClick) == null ? void 0 : _a.call(incrementButtonProps, event);
|
|
801
|
+
incrementButtonProps?.onClick?.(event);
|
|
808
802
|
if (event.defaultPrevented) return;
|
|
809
803
|
commitValue(clampedValue + step);
|
|
810
804
|
},
|
|
@@ -815,11 +809,279 @@ var ButtonCounter = forwardRef5(function ButtonCounter2({
|
|
|
815
809
|
}
|
|
816
810
|
);
|
|
817
811
|
});
|
|
812
|
+
|
|
813
|
+
// src/elements/Toolbar.tsx
|
|
814
|
+
import {
|
|
815
|
+
forwardRef as forwardRef6,
|
|
816
|
+
useEffect as useEffect3,
|
|
817
|
+
useImperativeHandle,
|
|
818
|
+
useRef as useRef3
|
|
819
|
+
} from "react";
|
|
820
|
+
import { cx as cx6 } from "@edux-design/utils";
|
|
821
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
822
|
+
function toolbarButtons(root) {
|
|
823
|
+
return Array.from(
|
|
824
|
+
root.querySelectorAll("[data-edux-toolbar-button]")
|
|
825
|
+
).filter((button) => {
|
|
826
|
+
if (button.closest("[data-edux-toolbar]") !== root) return false;
|
|
827
|
+
if (button.disabled || button.getAttribute("aria-disabled") === "true")
|
|
828
|
+
return false;
|
|
829
|
+
for (let node = button; node; node = node.parentElement) {
|
|
830
|
+
if (node.hidden || node.hasAttribute("inert") || node.getAttribute("aria-hidden") === "true")
|
|
831
|
+
return false;
|
|
832
|
+
const styles = node.ownerDocument.defaultView?.getComputedStyle(node);
|
|
833
|
+
if (styles?.display === "none" || styles?.visibility === "hidden")
|
|
834
|
+
return false;
|
|
835
|
+
if (node === root) break;
|
|
836
|
+
}
|
|
837
|
+
return true;
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
var Toolbar = forwardRef6(
|
|
841
|
+
function Toolbar2({
|
|
842
|
+
keyboardNavigation = "native",
|
|
843
|
+
orientation = "horizontal",
|
|
844
|
+
loop = true,
|
|
845
|
+
className,
|
|
846
|
+
children,
|
|
847
|
+
onKeyDown,
|
|
848
|
+
onFocusCapture,
|
|
849
|
+
...props
|
|
850
|
+
}, forwardedRef) {
|
|
851
|
+
const rootRef = useRef3(null);
|
|
852
|
+
useImperativeHandle(forwardedRef, () => rootRef.current);
|
|
853
|
+
const lastFocused = useRef3(null);
|
|
854
|
+
const originalTabIndexes = useRef3(
|
|
855
|
+
/* @__PURE__ */ new Map()
|
|
856
|
+
);
|
|
857
|
+
function syncTabStops() {
|
|
858
|
+
const root = rootRef.current;
|
|
859
|
+
if (!root || keyboardNavigation !== "roving") return;
|
|
860
|
+
const buttons = toolbarButtons(root);
|
|
861
|
+
const active = buttons.includes(lastFocused.current) ? lastFocused.current : buttons[0];
|
|
862
|
+
root.querySelectorAll("[data-edux-toolbar-button]").forEach((button) => {
|
|
863
|
+
const nextTabIndex = button === active ? 0 : -1;
|
|
864
|
+
if (button.closest("[data-edux-toolbar]") !== root) return;
|
|
865
|
+
if (!originalTabIndexes.current.has(button))
|
|
866
|
+
originalTabIndexes.current.set(
|
|
867
|
+
button,
|
|
868
|
+
button.getAttribute("tabindex")
|
|
869
|
+
);
|
|
870
|
+
if (button.tabIndex !== nextTabIndex) button.tabIndex = nextTabIndex;
|
|
871
|
+
});
|
|
872
|
+
lastFocused.current = active ?? null;
|
|
873
|
+
}
|
|
874
|
+
useEffect3(() => {
|
|
875
|
+
const root = rootRef.current;
|
|
876
|
+
if (!root || keyboardNavigation !== "roving") return;
|
|
877
|
+
syncTabStops();
|
|
878
|
+
const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver(syncTabStops);
|
|
879
|
+
observer?.observe(root, {
|
|
880
|
+
subtree: true,
|
|
881
|
+
childList: true,
|
|
882
|
+
attributes: true,
|
|
883
|
+
attributeFilter: [
|
|
884
|
+
"disabled",
|
|
885
|
+
"aria-disabled",
|
|
886
|
+
"hidden",
|
|
887
|
+
"inert",
|
|
888
|
+
"aria-hidden",
|
|
889
|
+
"class",
|
|
890
|
+
"style",
|
|
891
|
+
"tabindex"
|
|
892
|
+
]
|
|
893
|
+
});
|
|
894
|
+
return () => {
|
|
895
|
+
observer?.disconnect();
|
|
896
|
+
originalTabIndexes.current.forEach((value, button) => {
|
|
897
|
+
if (value === null) button.removeAttribute("tabindex");
|
|
898
|
+
else button.setAttribute("tabindex", value);
|
|
899
|
+
});
|
|
900
|
+
originalTabIndexes.current.clear();
|
|
901
|
+
};
|
|
902
|
+
}, [keyboardNavigation]);
|
|
903
|
+
return /* @__PURE__ */ jsx7(
|
|
904
|
+
"div",
|
|
905
|
+
{
|
|
906
|
+
...props,
|
|
907
|
+
ref: rootRef,
|
|
908
|
+
role: "toolbar",
|
|
909
|
+
"aria-orientation": orientation,
|
|
910
|
+
"data-edux-toolbar": "",
|
|
911
|
+
className: cx6(
|
|
912
|
+
"flex min-w-0 flex-wrap gap-8",
|
|
913
|
+
orientation === "vertical" ? "flex-col items-start" : "items-center",
|
|
914
|
+
className
|
|
915
|
+
),
|
|
916
|
+
onFocusCapture: (event) => {
|
|
917
|
+
onFocusCapture?.(event);
|
|
918
|
+
const target = event.target;
|
|
919
|
+
if (target instanceof HTMLButtonElement && target.closest("[data-edux-toolbar]") === event.currentTarget) {
|
|
920
|
+
if (toolbarButtons(event.currentTarget).includes(target))
|
|
921
|
+
lastFocused.current = target;
|
|
922
|
+
syncTabStops();
|
|
923
|
+
}
|
|
924
|
+
},
|
|
925
|
+
onKeyDown: (event) => {
|
|
926
|
+
onKeyDown?.(event);
|
|
927
|
+
if (keyboardNavigation !== "roving" || event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
|
|
928
|
+
return;
|
|
929
|
+
const root = event.currentTarget;
|
|
930
|
+
const target = event.target;
|
|
931
|
+
if (!(target instanceof HTMLButtonElement) || target.closest("[data-edux-toolbar]") !== root)
|
|
932
|
+
return;
|
|
933
|
+
const buttons = toolbarButtons(root);
|
|
934
|
+
const index = buttons.indexOf(target);
|
|
935
|
+
if (index === -1) return;
|
|
936
|
+
let next;
|
|
937
|
+
if (event.key === "Home") next = 0;
|
|
938
|
+
else if (event.key === "End") next = buttons.length - 1;
|
|
939
|
+
else {
|
|
940
|
+
const directionValue = root.ownerDocument.defaultView?.getComputedStyle(root).direction || root.closest("[dir]")?.getAttribute("dir");
|
|
941
|
+
const rtl = directionValue === "rtl";
|
|
942
|
+
const forward = orientation === "vertical" ? "ArrowDown" : rtl ? "ArrowLeft" : "ArrowRight";
|
|
943
|
+
const backward = orientation === "vertical" ? "ArrowUp" : rtl ? "ArrowRight" : "ArrowLeft";
|
|
944
|
+
if (event.key !== forward && event.key !== backward) return;
|
|
945
|
+
const direction = event.key === forward ? 1 : -1;
|
|
946
|
+
next = loop ? (index + direction + buttons.length) % buttons.length : Math.max(0, Math.min(buttons.length - 1, index + direction));
|
|
947
|
+
}
|
|
948
|
+
event.preventDefault();
|
|
949
|
+
buttons[next]?.focus();
|
|
950
|
+
},
|
|
951
|
+
children
|
|
952
|
+
}
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
);
|
|
956
|
+
var ToolbarGroup = forwardRef6(
|
|
957
|
+
function ToolbarGroup2({ orientation = "horizontal", className, ...props }, ref) {
|
|
958
|
+
return /* @__PURE__ */ jsx7(
|
|
959
|
+
"div",
|
|
960
|
+
{
|
|
961
|
+
...props,
|
|
962
|
+
ref,
|
|
963
|
+
role: "group",
|
|
964
|
+
className: cx6(
|
|
965
|
+
"inline-flex gap-4",
|
|
966
|
+
orientation === "vertical" ? "flex-col" : "flex-wrap items-center",
|
|
967
|
+
className
|
|
968
|
+
)
|
|
969
|
+
}
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
);
|
|
973
|
+
var ToolbarSeparator = forwardRef6(function ToolbarSeparator2({ orientation = "vertical", className, ...props }, ref) {
|
|
974
|
+
return /* @__PURE__ */ jsx7(
|
|
975
|
+
"div",
|
|
976
|
+
{
|
|
977
|
+
...props,
|
|
978
|
+
ref,
|
|
979
|
+
role: "separator",
|
|
980
|
+
"aria-orientation": orientation,
|
|
981
|
+
className: cx6(
|
|
982
|
+
"shrink-0 border-border-subtle",
|
|
983
|
+
orientation === "vertical" ? "h-control-sm border-l" : "w-full border-t",
|
|
984
|
+
className
|
|
985
|
+
)
|
|
986
|
+
}
|
|
987
|
+
);
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
// src/elements/ToolbarButton.tsx
|
|
991
|
+
import { forwardRef as forwardRef7, useState as useState3 } from "react";
|
|
992
|
+
import { cx as cx7 } from "@edux-design/utils";
|
|
993
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
994
|
+
var sizes = {
|
|
995
|
+
small: "h-control-sm min-w-control-sm text-sm",
|
|
996
|
+
medium: "h-control-md min-w-control-md text-base",
|
|
997
|
+
large: "h-control-lg min-w-control-lg text-lg"
|
|
998
|
+
};
|
|
999
|
+
var ToolbarButton = forwardRef7(
|
|
1000
|
+
function ToolbarButton2({
|
|
1001
|
+
active = false,
|
|
1002
|
+
size = "small",
|
|
1003
|
+
variant = "neutral",
|
|
1004
|
+
icon,
|
|
1005
|
+
iconOnly = false,
|
|
1006
|
+
children,
|
|
1007
|
+
className,
|
|
1008
|
+
type = "button",
|
|
1009
|
+
onClick,
|
|
1010
|
+
...props
|
|
1011
|
+
}, ref) {
|
|
1012
|
+
const pressed = active || props["aria-pressed"] === true || props["aria-pressed"] === "true" || props["aria-pressed"] === "mixed";
|
|
1013
|
+
const disabled = props.disabled || props["aria-disabled"] === true || props["aria-disabled"] === "true";
|
|
1014
|
+
const colours = {
|
|
1015
|
+
neutral: pressed ? "border-border-primary-base bg-bg-primary-subtle text-fg-primary-emphasis" : "border-transparent bg-transparent text-fg-base hover:bg-bg-minimal active:bg-bg-inactive",
|
|
1016
|
+
primary: pressed ? "border-border-primary-emphasis bg-bg-primary-subtle text-fg-primary-emphasis" : "border-transparent bg-transparent text-fg-primary-emphasis hover:bg-bg-primary-subtle active:bg-bg-primary-subtle-90",
|
|
1017
|
+
danger: pressed ? "border-border-danger-emphasis bg-bg-danger-subtle text-fg-danger-emphasis" : "border-transparent bg-transparent text-fg-danger-emphasis hover:bg-bg-danger-subtle active:bg-bg-danger-subtle"
|
|
1018
|
+
};
|
|
1019
|
+
return /* @__PURE__ */ jsxs6(
|
|
1020
|
+
"button",
|
|
1021
|
+
{
|
|
1022
|
+
"aria-pressed": active ? true : void 0,
|
|
1023
|
+
...props,
|
|
1024
|
+
ref,
|
|
1025
|
+
type,
|
|
1026
|
+
"data-edux-toolbar-button": "",
|
|
1027
|
+
"data-state": pressed ? "on" : "off",
|
|
1028
|
+
className: cx7(
|
|
1029
|
+
"inline-flex shrink-0 items-center justify-center gap-8 whitespace-nowrap rounded-md border-2 font-semibold cursor-pointer outline-none transition-colors duration-150 motion-reduce:transition-none focus-visible:shadow-focus disabled:cursor-not-allowed disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50",
|
|
1030
|
+
sizes[size],
|
|
1031
|
+
iconOnly ? "aspect-square p-0" : "px-8",
|
|
1032
|
+
disabled ? "border-border-subtle bg-bg-inactive text-fg-inactive" : colours[variant],
|
|
1033
|
+
className
|
|
1034
|
+
),
|
|
1035
|
+
onClick: (event) => {
|
|
1036
|
+
if (disabled) {
|
|
1037
|
+
event.preventDefault();
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
onClick?.(event);
|
|
1041
|
+
},
|
|
1042
|
+
children: [
|
|
1043
|
+
icon && /* @__PURE__ */ jsx8(
|
|
1044
|
+
"span",
|
|
1045
|
+
{
|
|
1046
|
+
"aria-hidden": "true",
|
|
1047
|
+
className: "inline-flex shrink-0 items-center",
|
|
1048
|
+
children: icon
|
|
1049
|
+
}
|
|
1050
|
+
),
|
|
1051
|
+
children
|
|
1052
|
+
]
|
|
1053
|
+
}
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
);
|
|
1057
|
+
var ToolbarToggleButton = forwardRef7(function ToolbarToggleButton2({ pressed, defaultPressed = false, onPressedChange, onClick, ...props }, ref) {
|
|
1058
|
+
const [internalPressed, setInternalPressed] = useState3(defaultPressed);
|
|
1059
|
+
const isPressed = pressed ?? internalPressed;
|
|
1060
|
+
return /* @__PURE__ */ jsx8(
|
|
1061
|
+
ToolbarButton,
|
|
1062
|
+
{
|
|
1063
|
+
...props,
|
|
1064
|
+
ref,
|
|
1065
|
+
"aria-pressed": isPressed,
|
|
1066
|
+
onClick: (event) => {
|
|
1067
|
+
onClick?.(event);
|
|
1068
|
+
if (event.defaultPrevented) return;
|
|
1069
|
+
if (pressed === void 0) setInternalPressed(!isPressed);
|
|
1070
|
+
onPressedChange?.(!isPressed, event);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
);
|
|
1074
|
+
});
|
|
818
1075
|
export {
|
|
819
1076
|
Button,
|
|
820
1077
|
ButtonCounter,
|
|
821
1078
|
ButtonGroup,
|
|
822
1079
|
ButtonGroupItem,
|
|
823
1080
|
IconButton,
|
|
824
|
-
MenuButton
|
|
1081
|
+
MenuButton,
|
|
1082
|
+
Toolbar,
|
|
1083
|
+
ToolbarButton,
|
|
1084
|
+
ToolbarGroup,
|
|
1085
|
+
ToolbarSeparator,
|
|
1086
|
+
ToolbarToggleButton
|
|
825
1087
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# ButtonGroup option types
|
|
2
|
+
|
|
3
|
+
`ButtonGroupItem` is an option marker read by `ButtonGroup`. It accepts `value`
|
|
4
|
+
(string or number), `disabled` (boolean), `label`, `children`, `startAdornment`,
|
|
5
|
+
`endAdornment`, and the legacy `endIcon` alias. Omitted values use the option's
|
|
6
|
+
index. The label falls back to children, then an automatically numbered label.
|
|
7
|
+
|
|
8
|
+
The marker renders no DOM element. It does not forward a ref, native button
|
|
9
|
+
attributes, or event handlers. Put selection handling on `ButtonGroup.onChange`,
|
|
10
|
+
which receives the selected value and option. The group forwards a div ref and
|
|
11
|
+
native div attributes. Use `Button` for a native button ref, attributes, and events.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<ButtonGroup defaultValue="draft" onChange={(value) => setFilter(value)}>
|
|
15
|
+
<ButtonGroupItem value="draft">Draft</ButtonGroupItem>
|
|
16
|
+
<ButtonGroupItem value="published" endAdornment={<span>3</span>}>
|
|
17
|
+
Published
|
|
18
|
+
</ButtonGroupItem>
|
|
19
|
+
<ButtonGroupItem value="archived" disabled>Archived</ButtonGroupItem>
|
|
20
|
+
</ButtonGroup>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The package-local `test/button-group-api.tsx` fixture checks the public entry point
|
|
24
|
+
under strict TypeScript, including no-`any` assertions, native button refs/events,
|
|
25
|
+
selection values, disabled options, and rejection of unsupported marker props.
|
package/docs/toolbar.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Toolbar controls
|
|
2
|
+
|
|
3
|
+
`ToolbarButton`, `ToolbarToggleButton`, `Toolbar`, `ToolbarGroup` and
|
|
4
|
+
`ToolbarSeparator` extend the Buttons package without changing existing Button,
|
|
5
|
+
IconButton or ButtonGroup APIs. Use them for compact editor, preview and page
|
|
6
|
+
actions. Commands, editor selection, persistence and menu state belong to the host.
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import {
|
|
10
|
+
Toolbar,
|
|
11
|
+
ToolbarGroup,
|
|
12
|
+
ToolbarSeparator,
|
|
13
|
+
ToolbarButton,
|
|
14
|
+
ToolbarToggleButton,
|
|
15
|
+
} from "@edux-design/buttons";
|
|
16
|
+
|
|
17
|
+
<Toolbar aria-label="Document formatting">
|
|
18
|
+
<ToolbarGroup aria-label="Text style">
|
|
19
|
+
<ToolbarToggleButton pressed={bold} onPressedChange={setBold}>
|
|
20
|
+
Bold
|
|
21
|
+
</ToolbarToggleButton>
|
|
22
|
+
<ToolbarToggleButton defaultPressed>Italic</ToolbarToggleButton>
|
|
23
|
+
</ToolbarGroup>
|
|
24
|
+
<ToolbarSeparator />
|
|
25
|
+
<ToolbarButton onClick={save}>Save draft</ToolbarButton>
|
|
26
|
+
</Toolbar>;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Buttons and state
|
|
30
|
+
|
|
31
|
+
All buttons render a single native `button`, forward an `HTMLButtonElement` ref,
|
|
32
|
+
and accept native attributes, events, ARIA, data attributes, `className`, `style`
|
|
33
|
+
and arbitrary children. Their default `type` is `button`; `type="submit"` is
|
|
34
|
+
supported. Handlers receive the native React event with the actual button as
|
|
35
|
+
`event.currentTarget`, suitable for menu anchors and measurement. No wrapper is
|
|
36
|
+
inserted around the button. An optional `icon` slot is decorative (`aria-hidden`).
|
|
37
|
+
|
|
38
|
+
| Prop | Default | Contract |
|
|
39
|
+
| --------------- | --------- | ----------------------------------------------------------- |
|
|
40
|
+
| `size` | `small` | `small`, `medium`, `large`: 36, 44, 56px semantic controls |
|
|
41
|
+
| `variant` | `neutral` | `neutral`, `primary`, `danger`; existing semantic tokens |
|
|
42
|
+
| `iconOnly` | `false` | Square geometry; caller supplies an accessible name |
|
|
43
|
+
| `icon` | — | Decorative icon before children |
|
|
44
|
+
| `active` | `false` | ToolbarButton compatibility visual state, no internal state |
|
|
45
|
+
| `disabled` | `false` | Native disabled behaviour; blocks activation |
|
|
46
|
+
| `aria-disabled` | — | Also blocks activation; roving focus skips this action |
|
|
47
|
+
|
|
48
|
+
An ordinary action has no `aria-pressed` attribute by default. Native
|
|
49
|
+
`aria-pressed` true/false/mixed is forwarded and controls selected styling.
|
|
50
|
+
`active` is a compatibility flag: true selects the visual and defaults
|
|
51
|
+
`aria-pressed` to true. Explicit native `aria-pressed`, including false or
|
|
52
|
+
undefined, takes precedence for semantics. A question selector can use
|
|
53
|
+
`active={current}` and `aria-pressed={undefined}` with the host's appropriate
|
|
54
|
+
`aria-current` semantics. It need not become a toggle merely to show selection.
|
|
55
|
+
|
|
56
|
+
`ToolbarToggleButton` is an independent toggle with boolean `pressed`,
|
|
57
|
+
`defaultPressed` (false by default), and
|
|
58
|
+
`onPressedChange(nextBoolean, clickEvent)`. It always exposes true or false
|
|
59
|
+
`aria-pressed`. Controlled state changes only when its host updates `pressed`.
|
|
60
|
+
The native `onClick` runs first; `event.preventDefault()` cancels state changes
|
|
61
|
+
and the change callback. Disabled actions cannot change state. Enter and Space
|
|
62
|
+
use native button activation. Keep a toggle's accessible label stable between
|
|
63
|
+
states. `active` is reserved for ToolbarButton, not the dedicated toggle.
|
|
64
|
+
|
|
65
|
+
The selected border reserves its space in every state, keeping dimensions stable.
|
|
66
|
+
Transitions honour reduced motion. Disabled controls keep their disabled colours
|
|
67
|
+
on hover. There are no new palette or semantic tokens.
|
|
68
|
+
|
|
69
|
+
## Grouping and keyboard policy
|
|
70
|
+
|
|
71
|
+
`Toolbar` renders a `div` with `role="toolbar"`; supply `aria-label` or
|
|
72
|
+
`aria-labelledby`. `ToolbarGroup` is a native `div` with `role="group"`, not a
|
|
73
|
+
radiogroup. Give meaningful groups accessible names. Root and group support
|
|
74
|
+
`orientation="horizontal"` (default) or `vertical`, refs and native div props.
|
|
75
|
+
`ToolbarSeparator` renders a non-focusable separator; its `orientation` refers
|
|
76
|
+
to the line, and defaults to `vertical` for a horizontal toolbar.
|
|
77
|
+
|
|
78
|
+
**Native Tab order is the default.** Grouping does not change button tab stops,
|
|
79
|
+
activate toggles, intercept arrows or manage menu state. Buttons and groups also
|
|
80
|
+
work independently without a Toolbar. This preserves existing footer/editor
|
|
81
|
+
keyboard behaviour when replacing presentation.
|
|
82
|
+
|
|
83
|
+
Opt into `keyboardNavigation="roving"` deliberately to use a single tab stop
|
|
84
|
+
across ToolbarButton and ToolbarToggleButton descendants. After hydration:
|
|
85
|
+
|
|
86
|
+
- Horizontal Left/Right arrows move focus in the layout direction, including RTL.
|
|
87
|
+
- Vertical Up/Down arrows move focus; the perpendicular axis remains available.
|
|
88
|
+
- Home/End focus the first/last enabled visible action.
|
|
89
|
+
- `loop` defaults to true; false stops at either end.
|
|
90
|
+
- Tab/Shift+Tab leave normally; returning remembers the last enabled action.
|
|
91
|
+
- Arrow navigation moves focus without activating an action or toggle.
|
|
92
|
+
- Native disabled, aria-disabled, hidden/inert and CSS-hidden actions are skipped.
|
|
93
|
+
- Nested toolbars keep independent focus. Inputs and other widgets retain their
|
|
94
|
+
own keys and tab order; place complex widgets in separate labelled groups when
|
|
95
|
+
a strict one-stop toolbar is required.
|
|
96
|
+
- Modified keys and caller-prevented key events are left alone, including table
|
|
97
|
+
Alt+Arrow shortcuts. The host's root `onKeyDown` runs before navigation.
|
|
98
|
+
|
|
99
|
+
Roving mode owns action tab stops while enabled, updates them when actions change,
|
|
100
|
+
and restores the original native values when disabled/unmounted. It does not
|
|
101
|
+
capture focus, auto-focus on mount, intercept Escape or handle editor commands.
|
|
102
|
+
|
|
103
|
+
## Payload compact and selection adapters
|
|
104
|
+
|
|
105
|
+
Existing Payload toolbars use 32px controls. The package's standard small control
|
|
106
|
+
is 36px. Preserve 32px explicitly during local acceptance rather than silently
|
|
107
|
+
resizing the client. This is component/host geometry, not a new global token:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
const compact = { height: 32, minWidth: 32 };
|
|
111
|
+
|
|
112
|
+
<ToolbarButton
|
|
113
|
+
style={compact}
|
|
114
|
+
onMouseDown={(event) => {
|
|
115
|
+
event.preventDefault(); // Host preserves editor selection.
|
|
116
|
+
onMouseDown?.(event); // Existing handler observes defaultPrevented.
|
|
117
|
+
}}
|
|
118
|
+
onClick={runCommand}
|
|
119
|
+
>
|
|
120
|
+
Insert
|
|
121
|
+
</ToolbarButton>;
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Mouse prevention is optional host behaviour. The package does not prevent pointer
|
|
125
|
+
focus or focus the editor. Keyboard navigation and activation remain available.
|
|
126
|
+
Table handles can retain their class/style/data hooks and specialised geometry,
|
|
127
|
+
pointer handlers, currentTarget anchors and Alt+Arrow reordering in local adapters.
|
|
128
|
+
|
|
129
|
+
## Tooltip, menu and theme composition
|
|
130
|
+
|
|
131
|
+
Use TooltipTrigger's `asChild` API around the same button, or pass that button
|
|
132
|
+
as the existing Menu's `button` prop. Provide TooltipProvider as usual.
|
|
133
|
+
Tooltips describe an action; icon-only actions
|
|
134
|
+
still need `aria-label`/`aria-labelledby`. Injected refs/handlers/ARIA are forwarded.
|
|
135
|
+
Menus own their `aria-expanded`, popup keyboard behaviour and focus restoration;
|
|
136
|
+
use ToolbarButton, not an independent toggle, for a menu trigger.
|
|
137
|
+
|
|
138
|
+
Import `@edux-design/tokens/theme` once, and include installed packages in Tailwind
|
|
139
|
+
source detection. Use existing `theme--light`/`theme--dark` and `brand--blue`/
|
|
140
|
+
`brand--teal` classes on the same scope. The controls use existing semantic
|
|
141
|
+
foreground, background, border and focus tokens. They do not own a theme provider.
|
|
142
|
+
|
|
143
|
+
Storybook **Components/Toolbar** includes native actions, explicit roving focus,
|
|
144
|
+
all sizes/states, light/dark × blue/teal, narrow wrapping, vertical/RTL, tooltip/menu
|
|
145
|
+
composition, a measured Payload32px recipe and the editor selection adapter. The package includes interaction,
|
|
146
|
+
structural accessibility, public consumer type and CJS/SSR fixtures.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edux-design/buttons",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css"
|
|
@@ -13,21 +13,6 @@
|
|
|
13
13
|
"require": "./dist/index.js"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
-
"scripts": {
|
|
17
|
-
"lint": "eslint . --max-warnings 0",
|
|
18
|
-
"build": "tsup src/index.js --format esm,cjs --dts",
|
|
19
|
-
"dev": "tsup src/index.js --watch --format esm,cjs --dts",
|
|
20
|
-
"generate:component": "turbo gen react-component",
|
|
21
|
-
"check-types": "tsc --noEmit"
|
|
22
|
-
},
|
|
23
|
-
"devDependencies": {
|
|
24
|
-
"typescript": "^5.9.2",
|
|
25
|
-
"tailwindcss": "^4.1.12",
|
|
26
|
-
"postcss": "^8.5.6",
|
|
27
|
-
"tsup": "^8.5.0",
|
|
28
|
-
"@tailwindcss/cli": "^4.1.12",
|
|
29
|
-
"@tailwindcss/postcss": "^4.1.12"
|
|
30
|
-
},
|
|
31
16
|
"dependencies": {
|
|
32
17
|
"@edux-design/utils": "*"
|
|
33
18
|
},
|
|
@@ -35,5 +20,11 @@
|
|
|
35
20
|
"@edux-design/tokens": ">=0.2.0 <0.3.0",
|
|
36
21
|
"react": "^19.1.0",
|
|
37
22
|
"react-dom": "^19.1.0"
|
|
38
|
-
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"docs",
|
|
27
|
+
"README.md",
|
|
28
|
+
"CHANGELOG.md"
|
|
29
|
+
]
|
|
39
30
|
}
|