@edux-design/buttons 0.1.3 → 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.
@@ -1,44 +1,68 @@
1
- import React from "react";
2
- import { MenuButtonContext } from "../util/MenuButtonContext";
1
+ import React, { forwardRef } from "react";
3
2
  import { cx } from "@edux-design/utils";
4
3
  import { Chevron } from "@edux-design/icons";
5
4
 
6
5
  /**
7
6
  * Props for the MenuButton component.
8
7
  *
9
- * @typedef {Object} MenuButtonProps
10
- * @property {React.ReactNode} [children] - Content to display inside the button (usually label or icon).
11
- * @property {"small"|"medium"} [size="medium"] - Size variant controlling button height.
8
+ * @typedef {React.ButtonHTMLAttributes<HTMLButtonElement> & {
9
+ * children?: React.ReactNode,
10
+ * caret?: React.ReactNode,
11
+ * size?: "small"|"medium",
12
+ * className?: string,
13
+ * }} MenuButtonProps
12
14
  */
13
15
 
14
16
  /**
15
17
  * MenuButton component.
16
18
  *
17
- * Provides a styled menu trigger button with optional sizing.
18
- * It uses a `MenuButtonContext` provider to share internal state
19
- * with child menu items.
19
+ * Provides a styled button intended for menu triggers.
20
+ * Consumers should pass `aria-expanded`, `aria-controls`, and any
21
+ * custom labeling they need. `aria-haspopup="menu"` is applied by default.
20
22
  *
21
23
  * @component
22
- * @type {React.FC<MenuButtonProps>}
24
+ * @type {React.ForwardRefExoticComponent<MenuButtonProps & React.RefAttributes<HTMLButtonElement>>}
23
25
  *
24
26
  * @example
25
27
  * ```jsx
26
- * <MenuButton size="small">Options</MenuButton>
28
+ * <MenuButton aria-expanded={open} onClick={() => setOpen((prev) => !prev)}>
29
+ * Options
30
+ * </MenuButton>
27
31
  * ```
28
32
  */
29
- export function MenuButton({ children, size }) {
30
- const SPLIT_BUTTON_STYLES = cx(
31
- "w-[36px] border-l-[1px] border-l-black flex flex-row justify-center items-center",
33
+ export const MenuButton = forwardRef(function MenuButton(
34
+ {
35
+ children,
36
+ caret,
37
+ size = "medium",
38
+ className,
39
+ type = "button",
40
+ "aria-haspopup": ariaHasPopup = "menu",
41
+ ...props
42
+ },
43
+ ref,
44
+ ) {
45
+ const styles = cx(
46
+ "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",
32
47
  {
33
- "h-[32px]": size === "small",
34
- "h-[40px] py-2": size === "medium" || size === undefined,
35
- }
48
+ "min-h-control-sm text-sm": size === "small",
49
+ "min-h-control-md text-base": size === "medium",
50
+ },
51
+ className,
36
52
  );
37
53
 
38
54
  return (
39
- <MenuButtonContext.Provider value>
40
- <div className={SPLIT_BUTTON_STYLES}>{children}</div>
41
- <Chevron />
42
- </MenuButtonContext.Provider>
55
+ <button
56
+ {...props}
57
+ ref={ref}
58
+ type={type}
59
+ aria-haspopup={ariaHasPopup}
60
+ className={styles}
61
+ >
62
+ {children ? <span>{children}</span> : null}
63
+ <span aria-hidden="true" className="flex items-center text-current">
64
+ {caret ?? <Chevron />}
65
+ </span>
66
+ </button>
43
67
  );
44
- }
68
+ });
@@ -0,0 +1,40 @@
1
+ import React from "react";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+ import { MenuButton } from "./MenuButton";
4
+
5
+ describe("MenuButton", () => {
6
+ it("renders a semantic button trigger", () => {
7
+ render(<MenuButton>Options</MenuButton>);
8
+
9
+ expect(screen.getByRole("button", { name: "Options" })).toHaveAttribute(
10
+ "aria-haspopup",
11
+ "menu",
12
+ );
13
+ });
14
+
15
+ it("forwards interactive props to the native button", () => {
16
+ const onClick = vi.fn();
17
+ render(
18
+ <MenuButton aria-expanded={true} onClick={onClick}>
19
+ Options
20
+ </MenuButton>,
21
+ );
22
+
23
+ const button = screen.getByRole("button", { name: "Options" });
24
+ fireEvent.click(button);
25
+
26
+ expect(onClick).toHaveBeenCalledTimes(1);
27
+ expect(button).toHaveAttribute("aria-expanded", "true");
28
+ });
29
+
30
+ it.each([
31
+ ["small", "min-h-control-sm"],
32
+ ["medium", "min-h-control-md"],
33
+ ])("maps the %s size to the shared control token", (size, className) => {
34
+ render(<MenuButton size={size}>Options</MenuButton>);
35
+
36
+ expect(screen.getByRole("button", { name: "Options" })).toHaveClass(
37
+ className,
38
+ );
39
+ });
40
+ });
@@ -0,0 +1,14 @@
1
+ import axe from "axe-core";
2
+
3
+ export async function expectNoAxeViolations(container) {
4
+ const results = await axe.run(container, {
5
+ rules: {
6
+ "color-contrast": { enabled: false },
7
+ },
8
+ });
9
+ const message = results.violations
10
+ .map((violation) => `${violation.id}: ${violation.help}`)
11
+ .join("\n");
12
+
13
+ expect(results.violations, message).toHaveLength(0);
14
+ }