Skip to content
Permalink
Browse files
feat(button): added support for disabled theme without disabling button
This is really helpful if you want a button to look disabled without
actually disabling it. A great example for this is creating async
buttons that show a loading progress while some background process or
API call is happening. If the button became `:disabled` during this,
keyboard focus would be lost which isn't ideal.
  • Loading branch information
mlaursen committed Aug 23, 2020
1 parent 0e14a09 commit 6a647e23831c7b3c97eb12baa47dfd5dd074271a
Show file tree
Hide file tree
Showing 4 changed files with 1,279 additions and 133 deletions.
@@ -76,6 +76,16 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
rippleClassName,
rippleContainerClassName,
enablePressedAndRipple: propEnablePressedAndRipple,
disablePressedFallback,
onClick,
onKeyUp,
onKeyDown,
onMouseUp,
onMouseDown,
onMouseLeave,
onTouchStart,
onTouchMove,
onTouchEnd,
...props
},
ref
@@ -84,19 +94,31 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
typeof propEnablePressedAndRipple === "boolean"
? propEnablePressedAndRipple
: themeType === "contained";
const propHandlers = {
onKeyUp,
onKeyDown,
onMouseUp,
onMouseDown,
onMouseLeave,
onTouchStart,
onTouchMove,
onTouchEnd,
};

const isDisabledTheme = theme === "disabled";
const { ripples, className, handlers } = useInteractionStates({
handlers: props,
handlers: propHandlers,
className: buttonThemeClassNames({
theme,
themeType,
buttonType,
disabled,
className: propClassName,
}),
disabled,
disabled: disabled || isDisabledTheme,
disableRipple,
disableProgrammaticRipple,
disablePressedFallback,
rippleTimeout,
rippleClassNames,
rippleClassName,
@@ -107,8 +129,10 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
return (
<FAB position={floating} {...floatingProps}>
<button
aria-disabled={isDisabledTheme || undefined}
{...props}
{...handlers}
{...(isDisabledTheme ? undefined : handlers)}
onClick={isDisabledTheme ? undefined : onClick}
ref={ref}
type={type}
className={className}
@@ -134,13 +158,15 @@ if (process.env.NODE_ENV !== "production") {
"secondary",
"warning",
"error",
"disabled",
]),
themeType: PropTypes.oneOf(["flat", "outline", "contained"]),
buttonType: PropTypes.oneOf(["text", "icon"]),
disabled: PropTypes.bool,
children: PropTypes.node,
disableRipple: PropTypes.bool,
disableProgrammaticRipple: PropTypes.bool,
disablePressedFallback: PropTypes.bool,
rippleTimeout: PropTypes.oneOfType([
PropTypes.number,
PropTypes.shape({
@@ -172,6 +198,15 @@ if (process.env.NODE_ENV !== "production") {
"bottom-right",
]),
floatingProps: PropTypes.object,
onClick: PropTypes.func,
onKeyUp: PropTypes.func,
onKeyDown: PropTypes.func,
onMouseUp: PropTypes.func,
onMouseDown: PropTypes.func,
onMouseLeave: PropTypes.func,
onTouchStart: PropTypes.func,
onTouchMove: PropTypes.func,
onTouchEnd: PropTypes.func,
};
} catch (e) {}
}
@@ -1,5 +1,5 @@
import React from "react";
import { render } from "@testing-library/react";
import { render, fireEvent } from "@testing-library/react";

import Button from "../Button";
import {
@@ -16,6 +16,7 @@ const themes: ButtonTheme[] = [
"secondary",
"warning",
"error",
"disabled",
];
const themeTypes: ButtonThemeType[] = ["flat", "contained", "outline"];
const buttonTypes: ButtonType[] = ["text", "icon"];
@@ -98,4 +99,110 @@ describe("Button", () => {
expect(button.className).toContain("rmd-button--contained");
expect(button.className).toContain("rmd-button--primary");
});

it("should correctly pass the event handlers down and fire them as expected", () => {
const onClick = jest.fn();
const onKeyUp = jest.fn();
const onKeyDown = jest.fn();
const onMouseUp = jest.fn();
const onMouseDown = jest.fn();
const onMouseLeave = jest.fn();
const onTouchStart = jest.fn();
const onTouchMove = jest.fn();
const onTouchEnd = jest.fn();
const handlers = {
onClick,
onKeyUp,
onKeyDown,
onMouseUp,
onMouseDown,
onMouseLeave,
onTouchStart,
onTouchMove,
onTouchEnd,
};

const { getByRole } = render(
<Button
{...handlers}
disableRipple
disablePressedFallback
enablePressedAndRipple={false}
>
Button
</Button>
);
const button = getByRole("button");

fireEvent.keyDown(button);
fireEvent.keyUp(button);
fireEvent.mouseDown(button);
fireEvent.mouseUp(button);
fireEvent.mouseLeave(button);
fireEvent.click(button);
fireEvent.touchStart(button);
fireEvent.touchMove(button);
fireEvent.touchEnd(button);

expect(onClick).toBeCalled();
expect(onKeyUp).toBeCalled();
expect(onKeyDown).toBeCalled();
expect(onMouseUp).toBeCalled();
expect(onMouseDown).toBeCalled();
expect(onMouseLeave).toBeCalled();
expect(onTouchStart).toBeCalled();
expect(onTouchMove).toBeCalled();
expect(onTouchEnd).toBeCalled();
});

it("should not allow for any of the interaction state handlers to be called while the theme is disabled", () => {
const onClick = jest.fn();
const onKeyUp = jest.fn();
const onKeyDown = jest.fn();
const onMouseUp = jest.fn();
const onMouseDown = jest.fn();
const onMouseLeave = jest.fn();
const onTouchStart = jest.fn();
const onTouchMove = jest.fn();
const onTouchEnd = jest.fn();
const handlers = {
onClick,
onKeyUp,
onKeyDown,
onMouseUp,
onMouseDown,
onMouseLeave,
onTouchStart,
onTouchMove,
onTouchEnd,
};

const { getByRole } = render(
<Button {...handlers} theme="disabled">
Button
</Button>
);
const button = getByRole("button");

fireEvent.keyDown(button);
fireEvent.keyUp(button);
fireEvent.mouseDown(button);
fireEvent.mouseUp(button);
fireEvent.click(button);
fireEvent.mouseLeave(button);
fireEvent.touchStart(button);
fireEvent.touchMove(button);
fireEvent.touchEnd(button);

expect(onClick).not.toBeCalled();
expect(onKeyUp).not.toBeCalled();
expect(onKeyDown).not.toBeCalled();
expect(onMouseUp).not.toBeCalled();
expect(onMouseDown).not.toBeCalled();
expect(onMouseLeave).not.toBeCalled();
expect(onTouchStart).not.toBeCalled();
expect(onTouchMove).not.toBeCalled();
expect(onTouchEnd).not.toBeCalled();
expect(button).toHaveAttribute("aria-disabled", "true");
});
});

0 comments on commit 6a647e2

Please sign in to comment.