Skip to content

Commit

Permalink
Feat: [HOP-103] ClearButton component (#264)
Browse files Browse the repository at this point in the history
  • Loading branch information
vicky-comeau committed May 15, 2024
2 parents 4fc5bf5 + 2821295 commit 72dada0
Show file tree
Hide file tree
Showing 10 changed files with 408 additions and 0 deletions.
44 changes: 44 additions & 0 deletions packages/components/src/buttons/docs/ClearButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Meta, StoryObj } from "@storybook/react";

import { ClearButton } from "../src/ClearButton.tsx";

/**
* ClearButtons are used to initialize an action. ClearButton labels express what action will occur when the user interacts with it.
*
* [View repository](https://github.com/gsoft-inc/wl-hopper/tree/main/packages/components/src/ClearButtons/src)
* -
* [View ARIA pattern](https://www.w3.org/WAI/ARIA/apg/patterns/clearButton/)
* -
* [View package](https://www.npmjs.com/package/@hopper-ui/components)
* -
* View storybook TODO
*/
const meta = {
title: "Docs/Buttons/ClearButton",
tags: ["autodocs"],
parameters: {
// Disables Chromatic's snapshotting on documentation stories
chromatic: { disableSnapshot: true }
},
component: ClearButton
} satisfies Meta<typeof ClearButton>;

export default meta;

type Story = StoryObj<typeof meta>;

/**
* A default clearButton.
*/
export const Default: Story = {
};

/**
* A clearButton can be disabled.
*/
export const Disabled: Story = {
...Default,
args: {
isDisabled: true
}
};
83 changes: 83 additions & 0 deletions packages/components/src/buttons/src/ClearButton.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
.hop-ClearButton {
--hop-ClearButton-border-radius: var(--hop-shape-rounded-sm);
--hop-ClearButton-focus-ring-color: var(--hop-primary-border-focus);
--hop-ClearButton-color: var(--hop-neutral-icon-weak);
--hop-ClearButton-color-hover: var(--hop-neutral-icon-weak-hover);
--hop-ClearButton-color-pressed: var(--hop-neutral-icon-weak-press);
--hop-ClearButton-color-disabled: var(--hop-neutral-icon-disabled);
--hop-ClearButton-background-color: transparent;
--hop-ClearButton-background-color-hover: var(--hop-neutral-surface-hover);
--hop-ClearButton-background-color-pressed: var(--hop-neutral-surface-press);
--hop-ClearButton-background-color-disabled: transparent;
--hop-ClearButton-spinner-color: var(--hop-neutral-icon);
--hop-ClearButton-height: 1rem;
--hop-ClearButton-width: 1rem;

/* Internal variable */
--inline-size: var(--hop-ClearButton-width);
--block-size: var(--hop-ClearButton-height);
--spinner: var(--hop-primary-icon-strong);
--padding-inline: 0;
--padding-block: 0;
--background-color: var(--hop-ClearButton-background-color);
--color: var(--hop-ClearButton-color);
--border: var(--hop-ClearButton-border);
--spinner-color: var(--hop-ClearButton-spinner-color);
--transition: var(--hop-easing-duration-2) var(--hop-easing-productive);

/** styles */
cursor: pointer;

position: relative;

overflow: hidden;
display: flex;
column-gap: var(--hop-space-inline-xs);
align-items: center;
justify-content: center;

box-sizing: border-box;
inline-size: var(--inline-size);
block-size: var(--block-size);
padding-block: var(--padding-block);
padding-inline: var(--padding-inline);

color: var(--color);
text-decoration: none;
white-space: nowrap;

background-color: var(--background-color);
border: none;
border-radius: var(--hop-ClearButton-border-radius);
outline: none;

transition:
color var(--transition),
background-color var(--transition);
}

/** Focus Ring */
.hop-ClearButton[data-focus-visible] {
outline: 0.125rem solid var(--hop-ClearButton-focus-ring-color);
}

.hop-ClearButton[data-disabled] {
--background-color: var(--hop-ClearButton-background-color-disabled);
--color: var(--hop-ClearButton-color-disabled);
--border: var(--hop-ClearButton-border-disabled);

cursor: not-allowed;
}

.hop-ClearButton[data-hovered]:not([data-disabled]),
.hop-ClearButton[data-focus-visible]:not([data-disabled]) {
--background-color: var(--hop-ClearButton-background-color-hover);
--color: var(--hop-ClearButton-color-hover);
--border: var(--hop-ClearButton-border-hover);
}

.hop-ClearButton[data-pressed] {
--background-color: var(--hop-ClearButton-background-color-pressed);
--color: var(--hop-ClearButton-color-pressed);
--border: var(--hop-ClearButton-border-pressed);
}
83 changes: 83 additions & 0 deletions packages/components/src/buttons/src/ClearButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { DismissIcon } from "@hopper-ui/icons";
import {
type StyledComponentProps,
useStyledSystem
} from "@hopper-ui/styled-system";
import { type ForwardedRef, forwardRef } from "react";
import {
Button as RACButton,
type ButtonProps as RACButtonProps,
composeRenderProps,
useContextProps
} from "react-aria-components";

import { useLocalizedString } from "../../i18n/index.ts";
import {
composeClassnameRenderProps,
cssModule
} from "../../utils/index.ts";

import { ClearButtonContext } from "./ClearButtonContext.ts";

import styles from "./ClearButton.module.css";

export const GlobalClearButtonCssSelector = "hop-ClearButton";

export interface ClearButtonProps extends StyledComponentProps<Omit<RACButtonProps, "children">> {}

function ClearButton(props: ClearButtonProps, ref: ForwardedRef<HTMLButtonElement>) {
[props, ref] = useContextProps(props, ref, ClearButtonContext);

const { stylingProps, ...ownProps } = useStyledSystem(props);
const stringFormatter = useLocalizedString();

const {
"aria-label": ariaLabel = stringFormatter.format("ClearButton.clearAriaLabel"),
className,
isDisabled,
style: styleProp,
...otherProps
} = ownProps;

const classNames = composeClassnameRenderProps(
className,
GlobalClearButtonCssSelector,
cssModule(
styles,
"hop-ClearButton"
),
stylingProps.className
);

const style = composeRenderProps(styleProp, prev => {
return {
...stylingProps.style,
...prev
};
});


return (
<RACButton
ref={ref}
className={classNames}
style={style}
isDisabled={isDisabled}
aria-label={ariaLabel}
{...otherProps}
>
<DismissIcon size="sm" />
</RACButton>
);
}

/**
* ClearButtons are used to initialize an action. ClearButton labels express what action will occur when the user interacts with it.
*
* [View Documentation](TODO)
*/
const _ClearButton = forwardRef<HTMLButtonElement, ClearButtonProps>(ClearButton);

_ClearButton.displayName = "ClearButton";

export { _ClearButton as ClearButton };
8 changes: 8 additions & 0 deletions packages/components/src/buttons/src/ClearButtonContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createContext } from "react";
import type { ContextValue } from "react-aria-components";

import type { ClearButtonProps } from "./ClearButton.tsx";

export const ClearButtonContext = createContext<ContextValue<ClearButtonProps, HTMLButtonElement>>({});

ClearButtonContext.displayName = "ClearButtonContext";
2 changes: 2 additions & 0 deletions packages/components/src/buttons/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from "./Button.tsx";
export * from "./ButtonContext.ts";
export * from "./ClearButton.tsx";
export * from "./ClearButtonContext.ts";
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Div } from "@hopper-ui/styled-system";
import type { Meta, StoryObj } from "@storybook/react";
import { within } from "@storybook/test";

import { Inline, Stack } from "../../../layout/index.ts";
import { ClearButton, type ClearButtonProps } from "../../src/ClearButton.tsx";

const meta = {
title: "Components/Buttons/ClearButton",
component: ClearButton
} satisfies Meta<typeof ClearButton>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
render: args => {
return (
<Stack>
<Stack>
<h1>Default</h1>
<Inline alignY="end">
<ClearButton {...args} />
</Inline>
</Stack>
<Stack>
<h1>Zoom</h1>
<Inline alignY="end">
<Div className="zoom-in">
<ClearButton {...args} />
</Div>
<Div className="zoom-out'">
<ClearButton {...args} />
</Div>
</Inline>
</Stack>
</Stack>
);
}
};

const StateTemplate = (args: Partial<ClearButtonProps>) => (
<Inline alignY="end">
<ClearButton {...args} />
</Inline>
);

export const DefaultStates: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const buttons = canvas.getAllByRole("button");

buttons.forEach(button => {
if (button.getAttribute("disabled") !== "") { // don't try and force states on a disabled input
if (button.getAttribute("data-chromatic-force-focus")) {
button.setAttribute("data-focus-visible", "true");
button.removeAttribute("data-chromatic-force-focus");
}

if (button.getAttribute("data-chromatic-force-hover")) {
button.setAttribute("data-hovered", "true");
button.removeAttribute("data-chromatic-force-hover");
}

if (button.getAttribute("data-chromatic-force-press")) {
button.setAttribute("data-pressed", "true");
button.removeAttribute("data-chromatic-force-press");
}
}
});
},
render: args => {
return (
<Stack>
<h1>Default</h1>
<StateTemplate {...args} />
<h1>Disabled</h1>
<StateTemplate {...args} isDisabled />
<h1>Pressed</h1>
<StateTemplate {...args} data-chromatic-force-press />
<h1>Focus Visible</h1>
<StateTemplate {...args} data-chromatic-force-focus />
<h1>Hovered</h1>
<StateTemplate {...args} data-chromatic-force-hover />
<h1>Focus Visible and Hovered</h1>
<StateTemplate {...args} data-chromatic-force-focus data-chromatic-force-hover />
</Stack>
);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @jest-environment node
*/
import { renderToString } from "react-dom/server";

import { ClearButton } from "../../src/ClearButton.tsx";

describe("ClearButton", () => {
it("should render on the server", () => {
const renderOnServer = () =>
renderToString(
<ClearButton />
);

expect(renderOnServer).not.toThrow();
});
});

Loading

0 comments on commit 72dada0

Please sign in to comment.