diff --git a/.changeset/wild-ducks-nail.md b/.changeset/wild-ducks-nail.md new file mode 100644 index 000000000..49483acc9 --- /dev/null +++ b/.changeset/wild-ducks-nail.md @@ -0,0 +1,5 @@ +--- +'@channel.io/bezier-react': patch +--- + +Add `ToggleEmojiButtonGroup` and `ToggleEmojiButtonSource` component. diff --git a/packages/bezier-react/src/components/AlphaToggleButton/AlphaToggleButton.stories.tsx b/packages/bezier-react/src/components/AlphaToggleButton/AlphaToggleButton.stories.tsx index c81365909..709ce9e53 100644 --- a/packages/bezier-react/src/components/AlphaToggleButton/AlphaToggleButton.stories.tsx +++ b/packages/bezier-react/src/components/AlphaToggleButton/AlphaToggleButton.stories.tsx @@ -16,7 +16,6 @@ export const Primary = { args: { text: 'Invite', selected: false, - loading: false, prefixContent: GiftIcon, suffixContent: ArrowRightIcon, size: 'm', diff --git a/packages/bezier-react/src/components/AlphaToggleButton/ToggleButton.module.scss b/packages/bezier-react/src/components/AlphaToggleButton/ToggleButton.module.scss index 1216d7122..cb24ff9a0 100644 --- a/packages/bezier-react/src/components/AlphaToggleButton/ToggleButton.module.scss +++ b/packages/bezier-react/src/components/AlphaToggleButton/ToggleButton.module.scss @@ -1,8 +1,3 @@ -@use '../../styles/mixins/dimension'; -@use 'sass:map'; - -@import '../Icon/Icon.module'; - .Button { position: relative; @@ -89,25 +84,6 @@ display: flex; align-items: center; justify-content: center; - - &:where(.loading) { - visibility: hidden; - } - } - - & :where(.ButtonLoader) { - position: absolute; - inset: 0; - - display: flex; - align-items: center; - justify-content: center; - - &:where(.size-s) { - & :is(.Loader) { - @include dimension.square(#{map.get($size-map, 's')}px); - } - } } /* NOTE: this fixes container width when bold toggles */ diff --git a/packages/bezier-react/src/components/AlphaToggleButton/ToggleButton.tsx b/packages/bezier-react/src/components/AlphaToggleButton/ToggleButton.tsx index 2e69e6cc2..8bcc4e232 100644 --- a/packages/bezier-react/src/components/AlphaToggleButton/ToggleButton.tsx +++ b/packages/bezier-react/src/components/AlphaToggleButton/ToggleButton.tsx @@ -6,7 +6,6 @@ import { isBezierIcon } from '@channel.io/bezier-icons' import * as TogglePrimitive from '@radix-ui/react-toggle' import classNames from 'classnames' -import { AlphaLoader } from '~/src/components/AlphaLoader' import { useToggleButtonContext } from '~/src/components/AlphaToggleButton/ToggleButtonContext' import { BaseButton } from '~/src/components/BaseButton' import { Icon, type IconSize } from '~/src/components/Icon' @@ -45,7 +44,6 @@ export const ToggleButton = forwardRef( shape: shapeProps, size = 'm', className, - loading, onSelectedChange, ...rest }, @@ -72,12 +70,7 @@ export const ToggleButton = forwardRef( className )} > -
+
( content={suffixContent} />
- - {loading && ( -
- -
- )} ) diff --git a/packages/bezier-react/src/components/AlphaToggleButton/ToggleButton.types.ts b/packages/bezier-react/src/components/AlphaToggleButton/ToggleButton.types.ts index 055ce20a3..b0aa26995 100644 --- a/packages/bezier-react/src/components/AlphaToggleButton/ToggleButton.types.ts +++ b/packages/bezier-react/src/components/AlphaToggleButton/ToggleButton.types.ts @@ -17,12 +17,6 @@ interface ToggleButtonOwnProps { */ text: string - /** - * If `loading` is true, spinner will be shown, replacing the content. - * @default false - */ - loading?: boolean - /** * Props that shows whether the button is selected. * @default false diff --git a/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/AlphaToggleEmojiButtonGroup.stories.tsx b/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/AlphaToggleEmojiButtonGroup.stories.tsx new file mode 100644 index 000000000..c76096027 --- /dev/null +++ b/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/AlphaToggleEmojiButtonGroup.stories.tsx @@ -0,0 +1,66 @@ +import React from 'react' + +import type { Meta, StoryFn, StoryObj } from '@storybook/react' + +import { Center } from '~/src/components/Center' + +import { + ToggleEmojiButtonGroup, + ToggleEmojiButtonSource, +} from './ToggleEmojiButtonGroup' +import type { + ToggleEmojiButtonGroupProps, + ToggleEmojiButtonSourceProps, +} from './ToggleEmojiButtonGroup.types' + +const meta: Meta = { + component: ToggleEmojiButtonGroup, +} +export default meta + +type ToggleButtonCompositionType = ToggleEmojiButtonGroupProps & + Pick + +const Template: StoryFn = ({ + fillDirection, + variant, +}) => { + return ( +
+ + + + +
+ ) +} + +export const Primary = { + render: Template, + + args: { + fillDirection: 'horizontal', + variant: 'primary', + }, + parameters: { + design: { + type: 'link', + url: 'https://www.figma.com/design/fPXP9zfjZU9NyARnhTWL6o/Input?node-id=425-281&t=ktusTVyr8cD3cTlt-1', + }, + }, +} satisfies StoryObj diff --git a/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/ToggleEmojiButtonGroup.module.scss b/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/ToggleEmojiButtonGroup.module.scss new file mode 100644 index 000000000..70a76542b --- /dev/null +++ b/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/ToggleEmojiButtonGroup.module.scss @@ -0,0 +1,76 @@ +@use '../../styles/mixins/dimension'; + +.ToggleEmojiButtonGroup { + display: flex; + gap: var(--b-toggle-emoji-button-group-gap); + align-items: center; + justify-content: center; + + /* stylelint-disable-next-line selector-class-pattern */ + &:where(.fillDirection-horizontal) { + width: 100%; + + & :where(.ToggleEmojiButtonSource) { + flex-grow: 1; + } + } + + /* stylelint-disable-next-line selector-class-pattern */ + &:where(.fillDirection-all) { + width: 100%; + height: 100%; + + & :is(.ToggleEmojiButtonSource) { + max-width: 160px; + max-height: 160px; + } + } +} + +.ToggleEmojiButtonSource { + position: relative; + + display: flex; + align-items: center; + justify-content: center; + + padding: 12px; + + border-radius: var(--radius-12); + + &:where(.size-m) { + @include dimension.square(var(--b-toggle-emoji-button-size)); + } + + &:where(.variant-primary) { + background-color: var(--alpha-color-bg-grey-lightest); + box-shadow: var(--alpha-shadow-input-default); + + &:where(&:hover) { + background-color: var(--alpha-color-bg-grey-lighter); + } + + &:where([data-state='on']) { + background-color: var(--alpha-color-bg-blue-lightest); + box-shadow: 0 0 0 1px var(--alpha-color-primary-bg-normal) inset; + } + } + + &:where(.variant-secondary) { + background-color: var(--alpha-color-bg-black-lightest); + + &:where(&:hover) { + background-color: var(--alpha-color-bg-black-lighter); + } + + &:where([data-state='on']) { + background-color: var(--alpha-color-primary-bg-lighter); + } + } + + & :where(.ButtonContent) { + display: flex; + align-items: center; + justify-content: center; + } +} diff --git a/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/ToggleEmojiButtonGroup.tsx b/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/ToggleEmojiButtonGroup.tsx new file mode 100644 index 000000000..855a0b119 --- /dev/null +++ b/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/ToggleEmojiButtonGroup.tsx @@ -0,0 +1,141 @@ +import React, { type CSSProperties, forwardRef, useState } from 'react' + +import * as ToggleGroup from '@radix-ui/react-toggle-group' +import classNames from 'classnames' + +import useMergeRefs from '~/src/hooks/useMergeRefs' +import { cssDimension } from '~/src/utils/style' + +import { + EMOJI_BUTTON_GROUP_GAP, + EMOJI_BUTTON_SIZE, + useToggleEmojiButtonSize, +} from '~/src/components/AlphaToggleEmojiButtonGroup/useToggleEmojiButtonSize' +import { BaseButton } from '~/src/components/BaseButton' +import { Emoji } from '~/src/components/Emoji' + +import { + type ToggleEmojiButtonGroupProps, + type ToggleEmojiButtonSourceProps, +} from './ToggleEmojiButtonGroup.types' + +import styles from './ToggleEmojiButtonGroup.module.scss' + +const EMOJI_SIZE = 30 + +/** + * Toggle Button that contains `Emoji` component with size fixed to 30. + * It should be used with `ToggleEmojiButtonGroup` component. + * @example + * ```tsx + * + * } + * /> + * ``` + */ +export const ToggleEmojiButtonSource = forwardRef< + HTMLButtonElement, + ToggleEmojiButtonSourceProps +>(function ToggleEmojiButtonSource( + { name, variant, className, selected, size = 'm', value, onResize, ...rest }, + forwardedRef +) { + return ( + + +
+ +
+
+
+ ) +}) + +/** + * Component for grouping `ToggleEmojiButtonSource`. + * @example + * ```tsx + * + * } /> + * } /> + * + * ``` + */ +export const ToggleEmojiButtonGroup = forwardRef< + HTMLDivElement, + ToggleEmojiButtonGroupProps +>(function ToggleEmojiButtonGroup( + { + fillDirection, + value, + className, + defaultValue, + children, + style, + dir = 'ltr', + onValueChange, + ...rest + }, + forwardedRef +) { + const [container, setContainer] = useState(null) + const mergedRefs = useMergeRefs(setContainer, forwardedRef) + const shouldResizeButton = fillDirection === 'all' + const resizedButtonSize = useToggleEmojiButtonSize({ + container, + enabled: shouldResizeButton, + buttonCount: React.Children.count(children), + }) + + return ( + + {children} + + ) +}) diff --git a/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/ToggleEmojiButtonGroup.types.ts b/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/ToggleEmojiButtonGroup.types.ts new file mode 100644 index 000000000..3ba86d11b --- /dev/null +++ b/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/ToggleEmojiButtonGroup.types.ts @@ -0,0 +1,62 @@ +import { type BezierComponentProps, type SizeProps } from '~/src/types/props' + +interface ToggleEmojiButtonSourceOwnProps { + /** + * Types of visual styles for button. + * @default 'primary' + */ + variant: 'primary' | 'secondary' + /** + * Props that shows whether the button is selected. + * @default false + */ + selected?: boolean + /** + * Name of emoji in the button. + */ + name: string + /** + * Controlled value of the button. + */ + value: string +} + +interface ToggleEmojiButtonGroupOwnProps { + /** + * Growing direction of button. + * @default undefined + */ + fillDirection?: 'horizontal' | 'all' + /** + * Controlled value of the button item to select. + * should be used with `onValueChange`. + */ + value?: string + /** + * The value of the button to show as selected when initially rendered. + * Use when you do not need to control the state of the items. + */ + defaultValue?: string + /** + * + * @default 'ltr' + * The reading direction of the toggle group. + */ + dir?: 'ltr' | 'rtl' + /** + * Event handler called when the value changes. + */ + onValueChange?: (value: string) => void +} + +export interface ToggleEmojiButtonGroupProps + extends Omit, 'dir' | 'defaultValue'>, + ToggleEmojiButtonGroupOwnProps {} + +export interface ToggleEmojiButtonSourceProps + extends Omit< + BezierComponentProps<'button'>, + keyof ToggleEmojiButtonSourceOwnProps + >, + SizeProps<'m'>, + ToggleEmojiButtonSourceOwnProps {} diff --git a/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/index.ts b/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/index.ts new file mode 100644 index 000000000..d70cb21f2 --- /dev/null +++ b/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/index.ts @@ -0,0 +1,9 @@ +export type { + ToggleEmojiButtonGroupProps as AlphaToggleEmojiButtonGroupProps, + ToggleEmojiButtonSourceProps as AlphaToggleEmojiButtonSourceProps, +} from './ToggleEmojiButtonGroup.types' + +export { + ToggleEmojiButtonGroup as AlphaToggleEmojiButtonGroup, + ToggleEmojiButtonSource as AlphaToggleEmojiButtonSource, +} from './ToggleEmojiButtonGroup' diff --git a/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/useToggleEmojiButtonSize.ts b/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/useToggleEmojiButtonSize.ts new file mode 100644 index 000000000..be8ba685a --- /dev/null +++ b/packages/bezier-react/src/components/AlphaToggleEmojiButtonGroup/useToggleEmojiButtonSize.ts @@ -0,0 +1,62 @@ +import { useCallback, useEffect, useState } from 'react' + +export const EMOJI_BUTTON_GROUP_GAP = 6 +export const EMOJI_BUTTON_SIZE = 54 + +interface UseToggleEmojiButtonSizeProps { + container: HTMLDivElement | null + enabled: boolean + buttonCount: number +} + +export function useToggleEmojiButtonSize({ + container, + enabled, + buttonCount, +}: UseToggleEmojiButtonSizeProps) { + const [buttonSize, setButtonSize] = useState(EMOJI_BUTTON_SIZE) + + const adjustButtonSize = useCallback(() => { + if (!container || !enabled) { + return + } + + const containerWidth = container.clientWidth + const containerHeight = container.clientHeight + const size = Math.max( + Math.min( + (containerWidth - EMOJI_BUTTON_GROUP_GAP * (buttonCount - 1)) / + buttonCount, + containerHeight - EMOJI_BUTTON_GROUP_GAP + ), + EMOJI_BUTTON_SIZE + ) + + setButtonSize(size) + }, [buttonCount, container, enabled]) + + useEffect( + function setResizeObserver() { + let resizeObserver: ResizeObserver | null = null + + if (enabled && container) { + resizeObserver = new ResizeObserver(() => { + adjustButtonSize() + }) + + resizeObserver.observe(container) + container.addEventListener('resize', adjustButtonSize) + } + + return () => { + if (container) { + resizeObserver?.unobserve(container) + container?.removeEventListener('resize', adjustButtonSize) + } + } + }, + [adjustButtonSize, container, enabled] + ) + + return buttonSize +} diff --git a/packages/bezier-react/src/index.ts b/packages/bezier-react/src/index.ts index b6484b3e0..42e2b7c31 100644 --- a/packages/bezier-react/src/index.ts +++ b/packages/bezier-react/src/index.ts @@ -16,6 +16,7 @@ export * from '~/src/components/AlphaLoader' export * from '~/src/components/AlphaStatusBadge' export * from '~/src/components/AlphaToggleButton' export * from '~/src/components/AlphaToggleButtonGroup' +export * from '~/src/components/AlphaToggleEmojiButtonGroup' export * from '~/src/components/AlphaTooltipPrimitive' export * from '~/src/components/AppProvider' export * from '~/src/components/AutoFocus'