Skip to content

Commit

Permalink
feat(snackbar): add snackbar positionning
Browse files Browse the repository at this point in the history
  • Loading branch information
soykje committed Mar 7, 2024
1 parent ef09637 commit 093a00c
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 19 deletions.
6 changes: 6 additions & 0 deletions packages/components/snackbar/src/Snackbar.doc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,9 @@ Use `design` prop to set prop to set the different look and feels of the snackba
Use `intent` prop to set the color intent of the snackbar.

<Canvas of={stories.Intent} />

### Position

Use `position` prop to set the position for all snackbars. This is set globally to guanrantee visual consistency.

<Canvas of={stories.Position} />
40 changes: 39 additions & 1 deletion packages/components/snackbar/src/Snackbar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Button } from '@spark-ui/button'
import { RadioGroup } from '@spark-ui/radio-group'
import { Meta, StoryFn } from '@storybook/react'
import { useState } from 'react'

import { addSnackbar, type AddSnackbarArgs, Snackbar } from '.'
import { addSnackbar, type AddSnackbarArgs, Snackbar, type SnackbarProps } from '.'

const meta: Meta<typeof Snackbar> = {
title: 'Experimental/Snackbar',
Expand All @@ -25,6 +27,15 @@ const intents: AddSnackbarArgs['intent'][] = [
'inverse',
]

const positions: SnackbarProps['position'][] = [
'top',
'top-right',
'top-left',
'bottom',
'bottom-right',
'bottom-left',
]

export const Default: StoryFn = _args => {
return (
<div>
Expand Down Expand Up @@ -76,3 +87,30 @@ export const Intent: StoryFn = _args => {
</div>
)
}

export const Position: StoryFn = _args => {
const [position, setPosition] = useState<SnackbarProps['position']>('bottom')

return (
<div>
<Snackbar position={position} />

<div>
<RadioGroup
className="mb-xl flex gap-xl"
value={`${position}`}
orientation="horizontal"
onValueChange={value => setPosition(value as ExcludeNull<SnackbarProps>['position'])}
>
{positions.map(position => (
<RadioGroup.Radio key={position} value={`${position}`} className="capitalize">
{position?.replace('-', ' ')}
</RadioGroup.Radio>
))}
</RadioGroup>

<Button onClick={() => addSnackbar({ message: "You're done!" })}>Display snackbar</Button>
</div>
</div>
)
}
27 changes: 21 additions & 6 deletions packages/components/snackbar/src/Snackbar.styles.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import { cva } from 'class-variance-authority'
import { cva, type VariantProps } from 'class-variance-authority'

export const snackbarRegionVariant = cva([
'fixed bottom-lg inset-x-none z-toast',
'outline-none pointer-events-none',
'flex flex-col items-center gap-lg',
])
export const snackbarRegionVariant = cva(
['fixed inset-x-lg z-toast group', 'outline-none pointer-events-none', 'flex flex-col gap-lg'],
{
variants: {
position: {
top: 'top-lg items-center',
'top-right': 'top-lg items-end',
'top-left': 'top-lg items-start',
bottom: 'bottom-lg items-center',
'bottom-right': 'bottom-lg items-end',
'bottom-left': 'bottom-lg items-start',
},
},
defaultVariants: {
position: 'bottom',
},
}
)

export type SnackbarRegionVariantProps = VariantProps<typeof snackbarRegionVariant>
20 changes: 16 additions & 4 deletions packages/components/snackbar/src/Snackbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@react-stately/toast'
import {
cloneElement,
type ComponentPropsWithoutRef,
forwardRef,
type ReactElement,
type RefObject,
Expand All @@ -14,12 +15,15 @@ import {
} from 'react'
import { createPortal } from 'react-dom'

import { snackbarRegionVariant } from './Snackbar.styles'
import { snackbarRegionVariant, type SnackbarRegionVariantProps } from './Snackbar.styles'
import { SnackbarItem, type SnackbarItemProps, type SnackbarItemValue } from './SnackbarItem'
import { SnackbarItemContext } from './SnackBarItemContext'
import { useSnackbarGlobalStore } from './useSnackbarGlobalStore'

export interface SnackbarProps extends AriaToastRegionProps {
export interface SnackbarProps
extends ComponentPropsWithoutRef<'div'>,
AriaToastRegionProps,
SnackbarRegionVariantProps {
/**
* The component/template used to display each snackbar from the queue
* @default 'Snackbar.Item'
Expand Down Expand Up @@ -60,7 +64,10 @@ const GLOBAL_SNACKBAR_STORE = {
}

export const Snackbar = forwardRef<HTMLDivElement, SnackbarProps>(
({ children = <SnackbarItem />, ...rest }, forwardedRef): ReactElement | null => {
(
{ children = <SnackbarItem />, position = 'bottom', className, ...rest },
forwardedRef
): ReactElement | null => {
const innerRef = useRef<HTMLDivElement>(null)
const ref = forwardedRef && typeof forwardedRef !== 'function' ? forwardedRef : innerRef

Expand All @@ -78,7 +85,12 @@ export const Snackbar = forwardRef<HTMLDivElement, SnackbarProps>(

return ref === provider && state.visibleToasts.length > 0
? createPortal(
<div {...regionProps} ref={ref} className={snackbarRegionVariant()}>
<div
{...regionProps}
ref={ref}
data-position={position}
className={snackbarRegionVariant({ position, className })}
>
{state.visibleToasts.map(toast => (
<SnackbarItemContext.Provider key={toast.key} value={{ toast, state }}>
{cloneElement(children, { key: toast.key })}
Expand Down
18 changes: 16 additions & 2 deletions packages/components/snackbar/src/SnackbarItem.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,23 @@ export const snackbarItemVariant = cva(
'max-w-[600px]',
'pointer-events-auto',
// Animation and opacity
'data-[animation=entering]:animate-slide-in-bottom data-[animation=entering]:spark-anime-fill-forwards data-[animation=entering]:spark-anime-easing-decelerate-back',
'data-[animation=exiting]:animate-slide-out-bottom data-[animation=exiting]:spark-anime-fill-forwards data-[animation=exiting]:spark-anime-easing-standard',
'data-[animation=queued]:opacity-none data-[animation=exiting]:opacity-0 transition-opacity duration-400',
'data-[animation=entering]:spark-anime-fill-forwards data-[animation=entering]:spark-anime-easing-decelerate-back',
'data-[animation=exiting]:spark-anime-fill-forwards data-[animation=exiting]:spark-anime-easing-standard',
// Parent position bottom|bottom-left|bottom-right
'group-data-[position=bottom]:data-[animation=entering]:animate-slide-in-bottom',
'group-data-[position=bottom]:data-[animation=exiting]:animate-slide-out-bottom',
'group-data-[position=bottom-left]:data-[animation=entering]:animate-slide-in-bottom',
'group-data-[position=bottom-left]:data-[animation=exiting]:animate-slide-out-bottom',
'group-data-[position=bottom-right]:data-[animation=entering]:animate-slide-in-bottom',
'group-data-[position=bottom-right]:data-[animation=exiting]:animate-slide-out-bottom',
// Parent position top|top-left|top-right
'group-data-[position=top]:data-[animation=entering]:animate-slide-in-top',
'group-data-[position=top]:data-[animation=exiting]:animate-slide-out-top',
'group-data-[position=top-left]:data-[animation=entering]:animate-slide-in-top',
'group-data-[position=top-left]:data-[animation=exiting]:animate-slide-out-top',
'group-data-[position=top-right]:data-[animation=entering]:animate-slide-in-top',
'group-data-[position=top-right]:data-[animation=exiting]:animate-slide-out-top',
],
{
variants: {
Expand Down
10 changes: 4 additions & 6 deletions packages/components/snackbar/src/SnackbarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,10 @@ export const SnackbarItem = ({
{...toastProps}
{...rest}
data-animation={toast.animation}
onAnimationEnd={() => {
// Remove the toast when the exiting animation completes.
if (toast.animation === 'exiting') {
state.remove(toast.key)
}
}}
{...(toast.animation === 'exiting' && {
// Remove snackbar when the exiting animation completes
onAnimationEnd: () => state.remove(toast.key),
})}
className={snackbarItemVariant({ design, intent, className })}
>
<p className="px-md py-lg text-body-2" {...titleProps}>
Expand Down

0 comments on commit 093a00c

Please sign in to comment.