Skip to content

Commit

Permalink
refactor(button): ♻️ split button components & add announcer
Browse files Browse the repository at this point in the history
  • Loading branch information
navin-moorthy committed Jul 12, 2021
1 parent b3dd6fa commit d5a449e
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 153 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
],
"scripts": {
"postinstall": "concurrently \"husky install\" \"patch-package\"",
"storybook": "start-storybook -p 6006",
"storybook": "cross-env TAILWIND_MODE=watch start-storybook -p 6006",
"test": "jest --config ./jest.config.ts --no-cache",
"check-types": "yarn build:types && yarn tsd",
"lint": "eslint . --ext .tsx,.ts,.jsx,.js",
Expand Down Expand Up @@ -82,6 +82,7 @@
]
},
"dependencies": {
"@react-aria/live-announcer": "^3.0.0",
"@renderlesskit/react": "^0.3.3",
"lodash-es": "^4.17.21",
"reakit": "^1.3.8"
Expand Down
170 changes: 20 additions & 150 deletions src/newButton/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ import * as React from "react";
import { cx } from "@renderlesskit/react";

import { useTheme } from "../theme";
import { Spinner } from "../spinner";
import { runIfFn, withIconA11y } from "../utils";
import { forwardRefWithAs } from "../utils/types";
import { rest, size } from "lodash-es";
import { spinner } from "../theme/defaultTheme/spinner";
import { usePrevious } from "../hooks";
import { ButtonChildren } from "./ButtonChildren";
import { forwardRefWithAs } from "../utils/types";
import { announce } from "@react-aria/live-announcer";

export type ButtonProps = Omit<ReakitButtonProps, "prefix"> & {
/**
Expand Down Expand Up @@ -83,165 +81,37 @@ export const Button = forwardRefWithAs<
? theme.newButton.size[size]
: theme.newButton.iconOnly.size[size],
theme.newButton.variant[variant],
_disabled ? "pointer-events-none" : "pointer-events-auto",
className,
);

const prevLoading = usePrevious(loading);

React.useEffect(() => {
if (loading) announce("Started loading...");

if (!loading && prevLoading) announce("Stopped loading...");
}, [loading, prevLoading]);

return (
<ReakitButton
ref={ref}
className={baseStyles}
aria-disabled={_disabled}
{...rest}
>
<span aria-live="assertive" className="sr-only">
{loading ? <FlashMessage>"Started loading"</FlashMessage> : ""}
{prevLoading && !loading ? (
<FlashMessage>"Stopped loading"</FlashMessage>
) : (
""
)}
</span>
{loading && !suffix && !prefix ? (
<>
<ButtonSpinnerWrapper>
<ButtonSpinner spinner={spinner} iconOnly={iconOnly} size={size} />
</ButtonSpinnerWrapper>
<div className="opacity-0">
<ButtonChildren
iconOnly={iconOnly}
suffix={suffix}
prefix={prefix}
size={size}
loading={loading}
spinner={spinner}
>
{children}
</ButtonChildren>
</div>
</>
) : (
<ButtonChildren
iconOnly={iconOnly}
suffix={suffix}
prefix={prefix}
size={size}
loading={loading}
spinner={spinner}
>
{children}
</ButtonChildren>
)}
<ButtonChildren
iconOnly={iconOnly}
suffix={suffix}
prefix={prefix}
size={size}
loading={loading}
spinner={spinner}
>
{children}
</ButtonChildren>
</ReakitButton>
);
});

Button.displayName = "Button";

interface ButtonChildrenCommonProps
extends Pick<
ButtonProps,
"suffix" | "prefix" | "spinner" | "size" | "loading"
> {
size: NonNullable<ButtonProps["size"]>;
}

interface ChildrenWithPrefixSuffixProps extends ButtonChildrenCommonProps {}

const ChildrenWithPrefixSuffix: React.FC<ChildrenWithPrefixSuffixProps> =
props => {
const { suffix, prefix, children, size, loading, spinner } = props;
const theme = useTheme();
const suffixStyles = cx(theme.newButton.suffix.size[size]);
const prefixStyles = cx(theme.newButton.prefix.size[size]);

return (
<>
{prefix ? (
loading && !suffix ? (
<ButtonSpinner spinner={spinner} size={size} />
) : (
runIfFn(withIconA11y(prefix, { className: prefixStyles }))
)
) : null}
<span>{children}</span>
{suffix ? (
loading ? (
<ButtonSpinner spinner={spinner} size={size} />
) : (
runIfFn(withIconA11y(suffix, { className: suffixStyles }))
)
) : null}
</>
);
};

interface ButtonChildrenProps extends ButtonChildrenCommonProps {
iconOnly?: ButtonProps["iconOnly"];
}

const ButtonChildren: React.FC<ButtonChildrenProps> = props => {
const { children, iconOnly, suffix, prefix, size, loading, spinner } = props;

if (iconOnly) {
// Removed ButtonIcon with span which causing small displacement
// If the icon is only a vaid element add the required accessibility attrs
// If they are passing a function meaning they are passing a custom icon
// which they need to add the custom styles
// @ts-ignore
return runIfFn(withIconA11y(iconOnly));
}

return (
<ChildrenWithPrefixSuffix
suffix={suffix}
prefix={prefix}
size={size}
loading={loading}
spinner={spinner}
>
{children}
</ChildrenWithPrefixSuffix>
);
};

interface ButtonSpinnerProps
extends Pick<ButtonChildrenCommonProps, "spinner" | "size"> {
iconOnly?: ButtonProps["iconOnly"];
}

const ButtonSpinner: React.FC<ButtonSpinnerProps> = props => {
const { spinner, iconOnly, size } = props;
const theme = useTheme();

if (spinner) return <ButtonSpinnerWrapper>{spinner}</ButtonSpinnerWrapper>;

const spinnerStyles = cx(
!iconOnly
? theme.newButton.spinner.size[size]
: theme.newButton.spinner.iconOnly.size[size],
);

return <Spinner className={spinnerStyles} size="em" />;
};

const ButtonSpinnerWrapper: React.FC = props => (
<div className="absolute flex items-center justify-center" {...props} />
);

export const FlashMessage: React.FC = props => {
const { children } = props;

const [message, setMessage] = React.useState(children);

React.useEffect(() => {
let timer = setTimeout(() => setMessage(""), 1000);

return () => {
clearTimeout(timer);
};
}, []);

return <span>{message}</span>;
};
76 changes: 76 additions & 0 deletions src/newButton/ButtonChildren.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as React from "react";
import { cx } from "@renderlesskit/react";

import { useTheme } from "../theme";
import { ButtonProps } from "./Button";
import { ButtonSpinner } from "./ButtonSpinner";
import { runIfFn, withIconA11y } from "../utils";

export interface ButtonChildrenProps
extends Pick<
ButtonProps,
"suffix" | "prefix" | "spinner" | "loading" | "iconOnly"
> {
size: NonNullable<ButtonProps["size"]>;
}

export const ButtonChildren: React.FC<ButtonChildrenProps> = props => {
const { children, iconOnly, suffix, prefix, size, loading, spinner } = props;

if (loading && !prefix && !suffix) {
return (
<>
<ButtonSpinner spinner={spinner} iconOnly={iconOnly} size={size} />
<div className="opacity-0">
{iconOnly ? runIfFn(withIconA11y(iconOnly)) : children}
</div>
</>
);
}

return (
<ChildrenWithPrefixSuffix
suffix={suffix}
prefix={prefix}
size={size}
loading={loading}
spinner={spinner}
>
{children}
</ChildrenWithPrefixSuffix>
);
};

export interface ChildrenWithPrefixSuffixProps
extends Pick<
ButtonChildrenProps,
"suffix" | "prefix" | "size" | "loading" | "spinner"
> {}

const ChildrenWithPrefixSuffix: React.FC<ChildrenWithPrefixSuffixProps> =
props => {
const { suffix, prefix, children, size, loading, spinner } = props;
const theme = useTheme();
const suffixStyles = cx(theme.newButton.suffix.size[size]);
const prefixStyles = cx(theme.newButton.prefix.size[size]);

return (
<>
{prefix ? (
loading && !suffix ? (
<ButtonSpinner spinner={spinner} size={size} />
) : (
runIfFn(withIconA11y(prefix, { className: prefixStyles }))
)
) : null}
<span>{children}</span>
{suffix ? (
loading ? (
<ButtonSpinner spinner={spinner} size={size} />
) : (
runIfFn(withIconA11y(suffix, { className: suffixStyles }))
)
) : null}
</>
);
};
32 changes: 32 additions & 0 deletions src/newButton/ButtonSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from "react";
import { cx } from "@renderlesskit/react";

import { useTheme } from "../theme";
import { Spinner } from "../spinner";
import { ButtonChildrenProps } from "./ButtonChildren";

export interface ButtonSpinnerProps
extends Pick<ButtonChildrenProps, "spinner" | "size" | "iconOnly"> {}

export const ButtonSpinner: React.FC<ButtonSpinnerProps> = props => {
const { spinner, iconOnly, size } = props;
const theme = useTheme();

if (spinner) return <ButtonSpinnerWrapper>{spinner}</ButtonSpinnerWrapper>;

const spinnerStyles = cx(
!iconOnly
? theme.newButton.spinner.size[size]
: theme.newButton.spinner.iconOnly.size[size],
);

return (
<ButtonSpinnerWrapper>
<Spinner className={spinnerStyles} size="em" />
</ButtonSpinnerWrapper>
);
};

export const ButtonSpinnerWrapper: React.FC = props => (
<div className="absolute flex items-center justify-center" {...props} />
);
3 changes: 1 addition & 2 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ const { preset } = require("./preset");
module.exports = preset({
mode: "jit",
purge: [
path.resolve(__dirname, "./src/theme/defaultTheme/**/*"),
path.resolve(__dirname, "./src/**/stories/*.stories.@(ts|tsx)"),
path.resolve(__dirname, "./src/**/*"),
path.resolve(__dirname, "./renderlesskit.config.ts"),
path.resolve(__dirname, "./.storybook/**/*"),
],
Expand Down

0 comments on commit d5a449e

Please sign in to comment.