-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
AlphaIconButton
component (#2200)
<!-- How to write a good PR title: - Follow [the Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/). - Give as much context as necessary and as little as possible - Prefix it with [WIP] while it’s a work in progress --> ## Self Checklist - [x] I wrote a PR title in **English** and added an appropriate **label** to the PR. - [x] I wrote the commit message in **English** and to follow [**the Conventional Commits specification**](https://www.conventionalcommits.org/en/v1.0.0/). - [x] I [added the **changeset**](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md) about the changes that needed to be released. (or didn't have to) - [x] I wrote or updated **documentation** related to the changes. (or didn't have to) - [x] I wrote or updated **tests** related to the changes. (or didn't have to) - [x] I tested the changes in various browsers. (or didn't have to) - Windows: Chrome, Edge, (Optional) Firefox - macOS: Chrome, Edge, Safari, (Optional) Firefox ## Related Issue <!-- Please link to issue if one exists --> <!-- Fixes #0000 --> - Fixes #2199 ## Summary <!-- Please brief explanation of the changes made --> - 아이콘만 있는 `AlphaIconButton` 컴포넌트를 구현합니다. ## Details <!-- Please elaborate description of the changes --> - 스타일은 버튼 컴포넌트에서 거의 그대로 가져왔습니다.#2180 까지는 복붙으로 구현한 후에 후속작업에서 버튼 컴포넌트의 스타일을 background-color, color, hover effect 별로 믹스인으로 분리하고 이를 재사용하는 형태로 리팩토링할 예정입니다. - 아이콘만 있는 경우 접근성을 위해 적절히 라벨을 주는 것이 필요합니다. 이를 위해서 (i) hidden text 를 주는 방법 (ii) aria-label 을 주는 방법 등이 있는데, hidden text 를 주면 문제가 발생한 경우가 있기도 하고 aria-label 을 주는 것이 간편해보여서 이 방법으로 하려고 합니다. ([#](#1310)). 다만 aria-label을 required로 해야할 지는 의문입니다. 다른 라이브러리를 찾아보니 라이브러리마다 다른 부분이기도 하고, 베지어에서는 접근성 관련 속성을 required로 한 적이 없어서 optional이 좋겠다는 생각입니다. - `IconButtonStyleVariant`, `IconButtonColor` 타입을 export 할 필요는 없는 것 같아서 하지 않았습니다. 이전에는 enum으로 사용하고 있어서 export 해야만 했는데 현재는 그렇지 않아서 `IconButtonProps['variant']` 처럼 사용하는 것이 가능합니다. 이거는 V2 컴포넌트 전반적으로 컨벤션이 맞춰지면 좋을 것 같아서 의견 부탁드립니다. (@sungik-choi) ### Breaking change? (Yes/No) <!-- If Yes, please describe the impact and migration path for users --> - No ## References <!-- Please list any other resources or points the reviewer should be aware of --> - https://www.sarasoueidan.com/blog/accessible-icon-buttons/ - https://v2.chakra-ui.com/docs/components/icon-button#usage - https://primer.style/components/icon-button/react/alpha#iconbutton
- Loading branch information
1 parent
9bb4810
commit 75a4f37
Showing
7 changed files
with
450 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@channel.io/bezier-react": patch | ||
--- | ||
|
||
Add `AlphaIconButton` component. |
29 changes: 29 additions & 0 deletions
29
packages/bezier-react/src/components/AlphaIconButton/AlphaIconButton.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { PlusIcon } from '@channel.io/bezier-icons' | ||
import { type Meta, type StoryObj } from '@storybook/react' | ||
|
||
import { | ||
AlphaIconButton, | ||
type AlphaIconButtonProps, | ||
} from '~/src/components/AlphaIconButton' | ||
|
||
const meta: Meta<typeof AlphaIconButton> = { | ||
component: AlphaIconButton, | ||
argTypes: { | ||
onClick: { action: 'onClick' }, | ||
}, | ||
} | ||
export default meta | ||
|
||
export const Playground: StoryObj<AlphaIconButtonProps> = { | ||
args: { | ||
disabled: false, | ||
active: false, | ||
loading: false, | ||
icon: PlusIcon, | ||
shape: 'rectangle', | ||
'aria-label': 'invite', | ||
size: 'm', | ||
variant: 'primary', | ||
color: 'blue', | ||
}, | ||
} |
250 changes: 250 additions & 0 deletions
250
packages/bezier-react/src/components/AlphaIconButton/IconButton.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,250 @@ | ||
@use '../../styles/mixins/dimension'; | ||
|
||
$chromatic-colors: 'blue', 'red', 'green', 'cobalt', 'orange', 'pink', 'purple'; | ||
|
||
.IconButton { | ||
position: relative; | ||
box-sizing: border-box; | ||
transition: background-color var(--transition-s); | ||
|
||
/* dimension */ | ||
&:where(.size-xs) { | ||
@include dimension.square(20px); | ||
|
||
padding: 4px; | ||
} | ||
|
||
&:where(.size-s) { | ||
@include dimension.square(24px); | ||
|
||
padding: 4px; | ||
} | ||
|
||
&:where(.size-m) { | ||
@include dimension.square(36px); | ||
|
||
padding: 8px; | ||
} | ||
|
||
&:where(.size-l) { | ||
@include dimension.square(44px); | ||
|
||
padding: 12px; | ||
} | ||
|
||
&:where(.size-xl) { | ||
@include dimension.square(54px); | ||
|
||
padding: 15px; | ||
} | ||
|
||
/* background-color */ | ||
&:where(.variant-primary) { | ||
$background-color-by-color: ( | ||
blue: var(--alpha-color-primary-bg-normal), | ||
cobalt: var(--alpha-color-accent-bg-normal), | ||
red: var(--alpha-color-critical-bg-normal), | ||
orange: var(--alpha-color-warning-bg-normal), | ||
green: var(--alpha-color-success-bg-normal), | ||
pink: var(--alpha-color-bg-pink-normal), | ||
purple: var(--alpha-color-bg-purple-normal), | ||
dark-grey: var(--alpha-color-bg-grey-darkest), | ||
light-grey: var(--alpha-color-bg-black-dark), | ||
); | ||
|
||
@each $color, $background-color in $background-color-by-color { | ||
&:where(.color-#{$color}) { | ||
background-color: $background-color; | ||
} | ||
} | ||
} | ||
|
||
&:where(.variant-secondary) { | ||
$background-color-by-color: ( | ||
blue: var(--alpha-color-primary-bg-lightest), | ||
cobalt: var(--alpha-color-accent-bg-lightest), | ||
red: var(--alpha-color-critical-bg-lightest), | ||
orange: var(--alpha-color-warning-bg-lightest), | ||
green: var(--alpha-color-success-bg-lightest), | ||
pink: var(--alpha-color-bg-pink-lightest), | ||
purple: var(--alpha-color-bg-purple-lightest), | ||
dark-grey: var(--alpha-color-bg-black-lighter), | ||
light-grey: var(--alpha-color-bg-black-lighter), | ||
); | ||
|
||
@each $color, $background-color in $background-color-by-color { | ||
&:where(.color-#{$color}) { | ||
background-color: $background-color; | ||
} | ||
} | ||
} | ||
|
||
&:where(.variant-tertiary) { | ||
background-color: initial; | ||
} | ||
|
||
/* color */ | ||
/* stylelint-disable-next-line no-duplicate-selectors */ | ||
&:where(.variant-primary) { | ||
color: var(--alpha-color-fg-absolute-white-dark); | ||
|
||
&:where(.color-dark-grey) { | ||
color: var(--alpha-color-fg-white-normal); | ||
} | ||
|
||
&:where(.color-light-grey) { | ||
color: var(--alpha-color-fg-absolute-white-normal); | ||
} | ||
} | ||
|
||
&:where(.variant-secondary, .variant-tertiary) { | ||
$color-map: ( | ||
blue: var(--alpha-color-primary-fg-normal), | ||
cobalt: var(--alpha-color-accent-fg-normal), | ||
red: var(--alpha-color-critical-fg-normal), | ||
orange: var(--alpha-color-warning-fg-normal), | ||
green: var(--alpha-color-success-fg-normal), | ||
pink: var(--alpha-color-fg-pink-normal), | ||
purple: var(--alpha-color-fg-purple-normal), | ||
dark-grey: var(--alpha-color-fg-black-darkest), | ||
light-grey: var(--alpha-color-fg-black-darker), | ||
); | ||
|
||
@each $button-color, $color in $color-map { | ||
&:where(.color-#{$button-color}) { | ||
color: $color; | ||
} | ||
} | ||
|
||
&:where(.color-dark-grey) { | ||
& :where(.ButtonIcon) { | ||
color: var(--alpha-color-fg-black-darker); | ||
} | ||
} | ||
|
||
&:where(.color-light-grey) { | ||
& :where(.ButtonIcon) { | ||
color: var(--alpha-color-fg-black-dark); | ||
} | ||
} | ||
} | ||
|
||
&:where(.variant-tertiary.color-white) { | ||
& :where(.ButtonIcon) { | ||
color: var(--alpha-color-fg-absolute-white-normal); | ||
} | ||
} | ||
|
||
/* border-radius */ | ||
&:where(.shape-rectangle) { | ||
$border-radius-by-size: ( | ||
xs: var(--alpha-dimension-6), | ||
s: var(--alpha-dimension-7), | ||
m: var(--alpha-dimension-10), | ||
l: var(--alpha-dimension-12), | ||
xl: var(--alpha-dimension-14), | ||
); | ||
|
||
@each $size, $border-radius in $border-radius-by-size { | ||
&:where(.size-#{$size}) { | ||
border-radius: $border-radius; | ||
} | ||
} | ||
} | ||
|
||
&:where(.shape-circle) { | ||
border-radius: 9999px; | ||
} | ||
|
||
/* TODO: use v2 token when design is specified */ | ||
|
||
/* visual effect on interaction */ | ||
&:where(.active, :hover):where(:not(:disabled)) { | ||
&:where(.variant-primary) { | ||
@each $color in $chromatic-colors { | ||
&:where(.color-#{$color}) { | ||
background-color: var(--bgtxt-#{$color}-dark); | ||
} | ||
} | ||
|
||
&:where(.color-dark-grey) { | ||
background-color: var(--bg-grey-darkest); | ||
} | ||
|
||
&:where(.color-light-grey) { | ||
background-color: var(--bg-black-darker); | ||
} | ||
} | ||
|
||
&:where(.variant-secondary) { | ||
@each $color in $chromatic-colors { | ||
&:where(.color-#{$color}) { | ||
background-color: var(--bgtxt-#{$color}-lighter); | ||
} | ||
} | ||
|
||
&:where(.color-dark-grey, .color-light-grey) { | ||
background-color: var(--bg-black-light); | ||
} | ||
} | ||
|
||
&:where(.variant-tertiary) { | ||
@each $color in $chromatic-colors { | ||
&:where(.color-#{$color}) { | ||
background-color: var(--bgtxt-#{$color}-lightest); | ||
} | ||
} | ||
|
||
&:where(.color-dark-grey, .color-light-grey, .color-white) { | ||
background-color: var(--bg-black-lighter); | ||
} | ||
} | ||
|
||
&:where(.color-dark-grey):where(.variant-secondary, .variant-tertiary) { | ||
& :is(.ButtonIcon, .ButtonLoader) { | ||
color: var(--txt-black-darkest); | ||
} | ||
} | ||
|
||
&:where(.color-light-grey):where(.variant-secondary, .variant-tertiary) { | ||
& :is(.ButtonIcon, .ButtonLoader) { | ||
color: var(--txt-black-darker); | ||
} | ||
} | ||
|
||
&:where(.color-white.variant-tertiary) { | ||
& :where(.ButtonIcon) { | ||
color: var(--alpha-color-fg-absolute-white-normal); | ||
} | ||
} | ||
} | ||
|
||
&:where(.variant-primary.color-blue:focus-visible) { | ||
outline: 3px solid var(--bgtxt-blue-light); | ||
} | ||
|
||
&:disabled { | ||
cursor: not-allowed; | ||
opacity: var(--alpha-opacity-disabled); | ||
} | ||
|
||
/* internal components */ | ||
.ButtonContent { | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
|
||
&:where(.loading) { | ||
visibility: hidden; | ||
} | ||
} | ||
|
||
.ButtonLoader { | ||
position: absolute; | ||
inset: 0; | ||
|
||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
} | ||
} |
94 changes: 94 additions & 0 deletions
94
packages/bezier-react/src/components/AlphaIconButton/IconButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import React, { forwardRef } from 'react' | ||
|
||
import classNames from 'classnames' | ||
|
||
import { type AlphaIconButtonProps } from '~/src/components/AlphaIconButton' | ||
import { BaseButton } from '~/src/components/BaseButton' | ||
import { type ButtonSize } from '~/src/components/Button' | ||
import { Icon } from '~/src/components/Icon' | ||
import { Spinner } from '~/src/components/Spinner' | ||
|
||
import styles from './IconButton.module.scss' | ||
|
||
function getIconSize(size: ButtonSize) { | ||
return ( | ||
{ | ||
xs: 'xxs', | ||
s: 'xs', | ||
m: 's', | ||
l: 's', | ||
xl: 'm', | ||
} as const | ||
)[size] | ||
} | ||
|
||
function getSpinnerSize(size: ButtonSize) { | ||
return ( | ||
{ | ||
xs: 'xs', | ||
s: 'xs', | ||
m: 's', | ||
l: 's', | ||
xl: 's', | ||
} as const | ||
)[size] | ||
} | ||
|
||
export const IconButton = forwardRef<HTMLButtonElement, AlphaIconButtonProps>( | ||
function IconButton( | ||
{ | ||
as = BaseButton, | ||
color = 'blue', | ||
variant = 'primary', | ||
size = 'm', | ||
disabled, | ||
active, | ||
shape = 'rectangle', | ||
icon, | ||
loading, | ||
className, | ||
...rest | ||
}, | ||
forwardedRef | ||
) { | ||
const Comp = as as typeof BaseButton | ||
|
||
return ( | ||
<Comp | ||
ref={forwardedRef} | ||
className={classNames( | ||
styles.IconButton, | ||
styles[`size-${size}`], | ||
styles[`variant-${variant}`], | ||
styles[`color-${color}`], | ||
styles[`shape-${shape}`], | ||
active && styles.active, | ||
className | ||
)} | ||
{...rest} | ||
> | ||
<div | ||
className={classNames( | ||
styles.ButtonContent, | ||
loading && styles.loading | ||
)} | ||
> | ||
{icon && ( | ||
<Icon | ||
size={getIconSize(size)} | ||
source={icon} | ||
className={styles.ButtonIcon} | ||
/> | ||
)} | ||
</div> | ||
|
||
{/* TODO: use AlphaSpinner */} | ||
{loading && ( | ||
<div className={styles.ButtonLoader}> | ||
<Spinner size={getSpinnerSize(size)} /> | ||
</div> | ||
)} | ||
</Comp> | ||
) | ||
} | ||
) |
Oops, something went wrong.