Skip to content

Commit

Permalink
Create IconButton component (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
germain-gg authored Aug 21, 2023
1 parent 826cb9a commit 70dfb38
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 0 deletions.
82 changes: 82 additions & 0 deletions src/components/IconButton/IconButton.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

.icon-button {
--cpd-icon-button-indicator-border-size: calc(
var(--cpd-icon-button-size) * 0.0625
);

width: var(--cpd-icon-button-size);
padding: calc(var(--cpd-icon-button-size) * 0.125);
aspect-ratio: 1 / 1;
color: var(--cpd-color-icon-tertiary);
border: 0;
appearance: none;
cursor: pointer;
border-radius: 50%;
position: relative;
background: transparent;
line-height: 0;
}

.icon-button:disabled {
color: var(--cpd-color-icon-disabled);
cursor: not-allowed;
}

.icon-button svg {
width: 100%;
height: 100%;
}

.icon-button[data-indicator] svg {
mask-image: url("./indicator-mask.svg");
mask-position: center center;
mask-repeat: no-repeat;
mask-size: calc(var(--cpd-icon-button-size) * 0.75);
}

.icon-button[data-indicator]::before {
content: "";
position: absolute;
top: var(--cpd-icon-button-indicator-border-size);
right: var(--cpd-icon-button-indicator-border-size);
width: calc(var(--cpd-icon-button-size) * 0.25);
height: calc(var(--cpd-icon-button-size) * 0.25);
border-radius: 50%;
background: currentColor;
}

.icon-button[data-indicator="highlight"]::before {
background: var(--cpd-color-icon-success-primary);
}

/**
* Hover state
*/

.icon-button:hover {
color: var(--cpd-color-icon-primary);
background: var(--cpd-color-bg-subtle-primary);
}

.icon-button[data-indicator]:is(:hover)::before {
/* Same colour as the background */
border: var(--cpd-icon-button-indicator-border-size) solid
var(--cpd-color-bg-subtle-primary);
top: 0;
right: 0;
}
58 changes: 58 additions & 0 deletions src/components/IconButton/IconButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from "react";
import { Meta, StoryFn } from "@storybook/react";

import { IconButton as IconButtonComponent } from "./IconButton";

import UserIcon from "@vector-im/compound-design-tokens/icons/user.svg";

export default {
title: "IconButton",
component: IconButtonComponent,
argTypes: {},
args: {},
} as Meta<typeof IconButtonComponent>;

const Template: StoryFn<typeof IconButtonComponent> = (args) => (
<>
<IconButtonComponent {...args} size="32px">
<UserIcon />
</IconButtonComponent>

<IconButtonComponent {...args} size="48px">
<UserIcon />
</IconButtonComponent>

<IconButtonComponent {...args} size="64px">
<UserIcon />
</IconButtonComponent>
</>
);

export const Normal = Template.bind({});
Normal.args = {};

export const WithIndicator = Template.bind({});
WithIndicator.args = {
indicator: "default",
};

export const WithHighlightIndicator = Template.bind({});
WithHighlightIndicator.args = {
indicator: "highlight",
};
34 changes: 34 additions & 0 deletions src/components/IconButton/IconButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { describe, it, expect } from "vitest";
import { render } from "@testing-library/react";
import React from "react";

import { IconButton } from "./IconButton";

import UserIcon from "@vector-im/compound-design-tokens/icons/user.svg";

describe("IconButton", () => {
it("renders", () => {
const { asFragment } = render(
<IconButton>
<UserIcon />
</IconButton>,
);
expect(asFragment()).toMatchSnapshot();
});
});
65 changes: 65 additions & 0 deletions src/components/IconButton/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { PropsWithChildren, forwardRef } from "react";
import classnames from "classnames";

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

type IconButtonProps = JSX.IntrinsicElements["button"] & {
/**
* The CSS class name.
*/
className?: string;
/**
* The avatar size in CSS units, e.g. `"24px"`.
* @default 32px
*/
size?: CSSStyleDeclaration["height"];
/**
* The icon button indicator displayed on the top right
*/
indicator?: "default" | "highlight";
};

/**
* Display an icon as a button. Can render an indicator
*/
export const IconButton = forwardRef<
HTMLButtonElement,
PropsWithChildren<IconButtonProps>
>(function IconButton(
{ children, className, indicator, size = "32px", style, ...props },
ref,
) {
const classes = classnames(styles["icon-button"], className);
return (
<button
ref={ref}
className={classes}
style={
{
"--cpd-icon-button-size": size,
...style,
} as React.CSSProperties
}
{...props}
data-indicator={indicator}
>
{React.Children.only(children)}
</button>
);
});
26 changes: 26 additions & 0 deletions src/components/IconButton/__snapshots__/IconButton.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`IconButton > renders 1`] = `
<DocumentFragment>
<button
class="_icon-button_1d4528"
style="--cpd-icon-button-size: 32px;"
>
<svg
class="cpd-icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m5.84 17.108.008.01.01-.008a10.422 10.422 0 0 1 2.846-1.536A9.725 9.725 0 0 1 12 15.012c1.149 0 2.247.188 3.296.562a10.42 10.42 0 0 1 2.846 1.536l.01.007.008-.009a7.742 7.742 0 0 0 1.364-2.329A7.85 7.85 0 0 0 20.012 12c0-2.22-.78-4.11-2.34-5.671C16.11 4.768 14.22 3.988 12 3.988c-2.22 0-4.11.78-5.671 2.34C4.768 7.89 3.988 9.78 3.988 12c0 .985.162 1.911.488 2.78.325.867.78 1.644 1.364 2.328Zm6.16-4.12c-.98 0-1.806-.337-2.479-1.01-.672-.672-1.009-1.498-1.009-2.478 0-.98.337-1.806 1.01-2.479.672-.672 1.498-1.008 2.478-1.008.98 0 1.806.336 2.479 1.008.672.673 1.009 1.499 1.009 2.479s-.337 1.806-1.01 2.479c-.672.672-1.498 1.009-2.478 1.009Zm0 9a9.725 9.725 0 0 1-3.895-.787 10.087 10.087 0 0 1-3.171-2.135 10.087 10.087 0 0 1-2.135-3.171A9.725 9.725 0 0 1 2.013 12c0-1.382.262-2.68.786-3.895a10.086 10.086 0 0 1 2.135-3.171 10.086 10.086 0 0 1 3.171-2.135A9.725 9.725 0 0 1 12 2.013c1.382 0 2.68.262 3.895.786a10.087 10.087 0 0 1 3.171 2.135c.899.899 1.61 1.956 2.135 3.171A9.725 9.725 0 0 1 21.988 12a9.73 9.73 0 0 1-.787 3.895 10.087 10.087 0 0 1-2.135 3.171 10.087 10.087 0 0 1-3.171 2.135 9.725 9.725 0 0 1-3.895.787Z"
fill="currentColor"
stroke="currentColor"
stroke-width="0.025"
/>
</svg>
</button>
</DocumentFragment>
`;
3 changes: 3 additions & 0 deletions src/components/IconButton/indicator-mask.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export {
H5,
H6,
} from "./components/Typography/Heading";
export { IconButton } from "./components/IconButton/IconButton";
export { Label } from "./components/Form/Label";
export { Link } from "./components/Link/Link";
export { Message } from "./components/Form/Message";
Expand Down

0 comments on commit 70dfb38

Please sign in to comment.