diff options
Diffstat (limited to 'src/components/atoms/buttons')
| -rw-r--r-- | src/components/atoms/buttons/button.stories.tsx | 120 | ||||
| -rw-r--r-- | src/components/atoms/buttons/button.test.tsx | 18 | ||||
| -rw-r--r-- | src/components/atoms/buttons/button.tsx | 53 | ||||
| -rw-r--r-- | src/components/atoms/buttons/buttons.module.scss | 154 |
4 files changed, 345 insertions, 0 deletions
diff --git a/src/components/atoms/buttons/button.stories.tsx b/src/components/atoms/buttons/button.stories.tsx new file mode 100644 index 0000000..5af61bd --- /dev/null +++ b/src/components/atoms/buttons/button.stories.tsx @@ -0,0 +1,120 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import ButtonComponent from './button'; + +export default { + title: 'Atoms/Buttons', + component: ButtonComponent, + args: { + disabled: false, + kind: 'secondary', + type: 'button', + }, + argTypes: { + 'aria-label': { + control: { + type: 'text', + }, + description: 'An accessible label.', + table: { + category: 'Accessibility', + }, + type: { + name: 'string', + required: false, + }, + }, + children: { + control: { + type: 'text', + }, + description: 'The button body.', + type: { + name: 'string', + required: true, + }, + }, + disabled: { + control: { + type: 'boolean', + }, + description: 'Render button as disabled.', + table: { + category: 'Options', + defaultValue: { summary: false }, + }, + type: { + name: 'boolean', + required: false, + }, + }, + kind: { + control: { + type: 'select', + }, + description: 'Button kind.', + options: ['primary', 'secondary', 'tertiary'], + table: { + category: 'Options', + defaultValue: { summary: 'secondary' }, + }, + type: { + name: 'string', + required: false, + }, + }, + onClick: { + control: { + type: null, + }, + description: 'A callback function to handle click.', + table: { + category: 'Events', + }, + type: { + name: 'function', + required: false, + }, + }, + type: { + control: { + type: 'select', + }, + description: 'Button type attribute.', + options: ['button', 'reset', 'submit'], + table: { + category: 'Options', + defaultValue: { summary: 'button' }, + }, + type: { + name: 'string', + required: false, + }, + }, + }, +} as ComponentMeta<typeof ButtonComponent>; + +const Template: ComponentStory<typeof ButtonComponent> = (args) => { + const { children, type, ...props } = args; + + const getBody = () => { + if (children) return children; + + switch (type) { + case 'reset': + return 'Reset'; + case 'submit': + return 'Submit'; + case 'button': + default: + return 'Button'; + } + }; + + return ( + <ButtonComponent type={type} {...props}> + {getBody()} + </ButtonComponent> + ); +}; + +export const Button = Template.bind({}); diff --git a/src/components/atoms/buttons/button.test.tsx b/src/components/atoms/buttons/button.test.tsx new file mode 100644 index 0000000..57c79c6 --- /dev/null +++ b/src/components/atoms/buttons/button.test.tsx @@ -0,0 +1,18 @@ +import { render, screen } from '@test-utils'; +import Button from './button'; + +describe('Button', () => { + it('renders the Button component', () => { + render(<Button onClick={() => null}>Button</Button>); + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + it('renders the Button component with disabled state', () => { + render( + <Button onClick={() => null} disabled={true}> + Disabled Button + </Button> + ); + expect(screen.getByRole('button')).toBeDisabled(); + }); +}); diff --git a/src/components/atoms/buttons/button.tsx b/src/components/atoms/buttons/button.tsx new file mode 100644 index 0000000..420ee74 --- /dev/null +++ b/src/components/atoms/buttons/button.tsx @@ -0,0 +1,53 @@ +import { FC, MouseEventHandler } from 'react'; +import styles from './buttons.module.scss'; + +export type ButtonProps = { + /** + * Button accessible label. + */ + 'aria-label'?: string; + /** + * Button state. Default: false. + */ + disabled?: boolean; + /** + * Button kind. Default: secondary. + */ + kind?: 'primary' | 'secondary' | 'tertiary'; + /** + * A callback function to handle click. + */ + onClick?: MouseEventHandler<HTMLButtonElement>; + /** + * Button type attribute. Default: button. + */ + type?: 'button' | 'reset' | 'submit'; +}; + +/** + * Button component + * + * Use a button as call to action. + */ +const Button: FC<ButtonProps> = ({ + children, + disabled = false, + kind = 'secondary', + type = 'button', + ...props +}) => { + const kindClass = styles[`btn--${kind}`]; + + return ( + <button + type={type} + disabled={disabled} + className={`${styles.btn} ${kindClass}`} + {...props} + > + {children} + </button> + ); +}; + +export default Button; diff --git a/src/components/atoms/buttons/buttons.module.scss b/src/components/atoms/buttons/buttons.module.scss new file mode 100644 index 0000000..d6a488d --- /dev/null +++ b/src/components/atoms/buttons/buttons.module.scss @@ -0,0 +1,154 @@ +@use "@styles/abstracts/functions" as fun; + +.btn { + display: block; + max-width: max-content; + padding: var(--spacing-2xs) var(--spacing-md); + border: none; + border-radius: fun.convert-px(5); + font-size: var(--font-size-md); + font-weight: 600; + transition: all 0.3s ease-in-out 0s; + + &:disabled { + cursor: wait; + } + + &--primary { + background: var(--color-primary); + border: fun.convert-px(2) solid var(--color-bg); + box-shadow: 0 0 0 fun.convert-px(2) var(--color-primary), + 0 0 0 fun.convert-px(3) var(--color-primary-darker), + fun.convert-px(2) fun.convert-px(2) 0 fun.convert-px(3) + var(--color-primary-dark); + color: var(--color-fg-inverted); + text-shadow: fun.convert-px(2) fun.convert-px(2) 0 var(--color-shadow); + + &:disabled { + background: var(--color-primary-darker); + } + + &:not(:disabled) { + &:hover, + &:focus { + background: var(--color-primary-light); + box-shadow: 0 0 0 fun.convert-px(2) var(--color-primary-light), + 0 0 0 fun.convert-px(3) var(--color-primary-darker), + fun.convert-px(7) fun.convert-px(7) 0 fun.convert-px(2) + var(--color-primary-dark); + transform: translateX(#{fun.convert-px(-4)}) + translateY(#{fun.convert-px(-4)}); + } + + &:focus { + text-decoration: underline solid var(--color-fg-inverted) + fun.convert-px(2); + } + + &:active { + background: var(--color-primary-dark); + box-shadow: 0 0 0 fun.convert-px(2) var(--color-primary), + 0 0 0 fun.convert-px(3) var(--color-primary-darker), + 0 0 0 0 var(--color-primary-dark); + text-decoration: none; + transform: translateX(#{fun.convert-px(4)}) + translateY(#{fun.convert-px(4)}); + } + } + } + + &--secondary { + background: var(--color-bg); + border: fun.convert-px(3) solid var(--color-primary); + box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) + var(--color-shadow), + fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2) + var(--color-shadow), + fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4) + var(--color-shadow); + color: var(--color-primary); + text-decoration: underline transparent 0; + + &:disabled { + border-color: var(--color-border-dark); + color: var(--color-fg-light); + } + + &:not(:disabled) { + &:hover, + &:focus { + border-color: var(--color-primary-light); + color: var(--color-primary-light); + box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) + var(--color-shadow-light), + fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) + fun.convert-px(-2) var(--color-shadow-light), + fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) + fun.convert-px(-4) var(--color-shadow-light), + fun.convert-px(7) fun.convert-px(10) fun.convert-px(12) + fun.convert-px(-3) var(--color-shadow-light); + transform: scale(1.1); + } + + &:focus { + text-decoration: underline var(--color-primary-light) fun.convert-px(3); + } + + &:active { + border-color: var(--color-primary-dark); + box-shadow: 0 0 0 0 var(--color-shadow); + color: var(--color-primary-dark); + text-decoration: underline transparent 0; + transform: scale(0.94); + } + } + } + + &--tertiary { + background: var(--color-bg); + border: fun.convert-px(3) solid var(--color-primary); + box-shadow: fun.convert-px(2) fun.convert-px(2) 0 0 var(--color-bg), + fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-primary-dark), + fun.convert-px(5) fun.convert-px(5) 0 0 var(--color-bg), + fun.convert-px(6) fun.convert-px(6) 0 0 var(--color-primary-dark); + color: var(--color-primary); + + &:disabled { + color: var(--color-fg-light); + border-color: var(--color-border-dark); + box-shadow: fun.convert-px(2) fun.convert-px(2) 0 0 var(--color-bg), + fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-primary-darker), + fun.convert-px(5) fun.convert-px(5) 0 0 var(--color-bg), + fun.convert-px(6) fun.convert-px(6) 0 0 var(--color-primary-darker); + } + + &:not(:disabled) { + &:hover, + &:focus { + border-color: var(--color-primary-light); + box-shadow: fun.convert-px(2) fun.convert-px(3) 0 0 var(--color-bg), + fun.convert-px(4) fun.convert-px(5) 0 0 var(--color-primary), + fun.convert-px(6) fun.convert-px(8) 0 0 var(--color-bg), + fun.convert-px(8) fun.convert-px(10) 0 0 var(--color-primary), + fun.convert-px(10) fun.convert-px(12) fun.convert-px(1) 0 + var(--color-shadow-light), + fun.convert-px(10) fun.convert-px(12) fun.convert-px(5) + fun.convert-px(1) var(--color-shadow-light); + color: var(--color-primary-light); + transform: translateX(#{fun.convert-px(-3)}) + translateY(#{fun.convert-px(-5)}); + } + + &:focus { + text-decoration: underline var(--color-primary) fun.convert-px(2); + } + + &:active { + box-shadow: 0 0 0 0 var(--color-shadow); + text-decoration: none; + transform: translateX(#{fun.convert-px(5)}) + translateY(#{fun.convert-px(6)}); + } + } + } +} |
