Skip to content

Commit

Permalink
feat(toast): use toasts from context
Browse files Browse the repository at this point in the history
Enables use of different ToastAnchors
throughout the application sharing the
toasts.
  • Loading branch information
danielkaxis committed Sep 13, 2022
1 parent 1f95b34 commit 1ca733d
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 45 deletions.
18 changes: 8 additions & 10 deletions packages/core/src/Practical.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Theme, defaultTheme } from './theme'
import {
ToastsProvider,
ToastsAnchor,
ToastsPlacement,
ToastsAnchorProps,
} from './Toast/ToastsProvider'
import { SimpleToastsDurations } from './Toast'

Expand All @@ -24,11 +24,7 @@ const PracticalContext = createContext<PracticalContextType>({
rootEl: undefined,
})

export interface ToastsOptions {
/**
* Position of toasts on the page.
*/
readonly placement?: ToastsPlacement
export interface ToastsOptions extends Pick<ToastsAnchorProps, 'placement'> {
/**
* Determine if toasts should be always on top (above any other modal layers),
* or not (above the main application layer but below any other modal layers).
Expand All @@ -42,8 +38,7 @@ export interface ToastsOptions {
readonly defaultDurations?: SimpleToastsDurations
}

const DEFAULT_TOASTS_OPTIONS: Required<ToastsOptions> = {
placement: { justify: 'right', top: '0' },
const DEFAULT_TOASTS_OPTIONS: ToastsOptions = {
alwaysOnTop: true,
defaultDurations: {
success: 4000,
Expand Down Expand Up @@ -82,8 +77,11 @@ export const PracticalProvider: FC<PracticalProviderProps> = ({
<ToastsProvider {...mergedOptions.defaultDurations}>
<PracticalRoot id="practical-root" ref={rootRef}>
<Layer>{children}</Layer>
<Layer zIndex={mergedOptions.alwaysOnTop ? 1 : 0}>
<ToastsAnchor placement={mergedOptions.placement} />
<Layer zIndex={mergedOptions.alwaysOnTop === true ? 1 : 0}>
<ToastsAnchor
placement={mergedOptions.placement}
alwaysOnTop={false}
/>
</Layer>
</PracticalRoot>
</ToastsProvider>
Expand Down
54 changes: 39 additions & 15 deletions packages/core/src/Toast/ToastsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
useContext,
useRef,
FC,
Dispatch,
isValidElement,
cloneElement,
ReactNode,
Expand All @@ -17,22 +16,37 @@ import { BaseToast } from './Toast'
import { toastReducer } from './toastReducer'

import { useToastCallbacks, SimpleToastsDurations } from './useToasts'
import { ToastAction, NI, ToastsContext } from './context'
import { ToastsContext } from './context'

export const DEFAULT_TOAST_PLACEMENT: ToastsPlacement = {
justify: 'right',
top: '0',
}

export interface ToastsPlacement {
readonly justify: 'center' | 'right'
readonly top: string
}

const ToastsWrapper = styled.div<ToastsPlacement>`
const ToastsWrapper = styled.div<{
readonly placement: ToastsPlacement
readonly zIndex?: number
}>`
position: fixed;
top: ${({ top }) => top};
top: ${({ placement }) => placement.top};
width: 360px;
border-radius: ${shape.radius.medium};
margin: ${spacing.medium} ${spacing.large};
${({ justify }) =>
justify === 'center'
${({ zIndex }) =>
zIndex !== undefined
? css`
z-index: 1;
`
: undefined}
${({ placement }) =>
placement.justify === 'center'
? css`
left: 50%;
transform: translateX(-50%);
Expand Down Expand Up @@ -64,12 +78,12 @@ export const ToastsProvider: FC<ToastsProviderProps> = ({
children,
...toastsOptions
}) => {
const __dispatchRef = useRef<Dispatch<ToastAction>>(NI)
const [toasts, dispatch] = useReducer(toastReducer, new Map())

const callbacks = useToastCallbacks(__dispatchRef, toastsOptions)
const callbacks = useToastCallbacks(dispatch, toastsOptions)

return (
<ToastsContext.Provider value={{ ...callbacks, __dispatchRef }}>
<ToastsContext.Provider value={{ ...callbacks, dispatch, toasts }}>
{children}
</ToastsContext.Provider>
)
Expand Down Expand Up @@ -102,16 +116,26 @@ export interface ToastsAnchorProps {
*
* Default: top right
*/
readonly placement: ToastsPlacement
readonly placement?: ToastsPlacement
/**
* Determine if toasts should be always on top
*
* @default true
*/
readonly alwaysOnTop?: boolean
}

export const ToastsAnchor: FC<ToastsAnchorProps> = ({ placement }) => {
const [toasts, dispatch] = useReducer(toastReducer, new Map())
const { hideToast, __dispatchRef } = useContext(ToastsContext)
__dispatchRef.current = dispatch
export const ToastsAnchor: FC<ToastsAnchorProps> = ({
placement,
alwaysOnTop = true,
}) => {
const { hideToast, toasts } = useContext(ToastsContext)

return (
<ToastsWrapper {...placement}>
<ToastsWrapper
placement={placement ?? DEFAULT_TOAST_PLACEMENT}
zIndex={alwaysOnTop ? 1 : undefined}
>
<TransitionGroup component={null}>
{[...toasts.entries()].map(([id, props], index) => (
<ToastTransition key={id}>
Expand Down
10 changes: 7 additions & 3 deletions packages/core/src/Toast/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createContext, ReactNode, MutableRefObject, Dispatch } from 'react'
import { createContext, ReactNode, Dispatch } from 'react'
import { IconType } from '../Icon'
import { ALinkProps, ButtonLinkProps } from '../Link'

Expand All @@ -25,6 +25,8 @@ export interface ProgressToast extends SimpleToast {
}
}

export type ToastsMap = ReadonlyMap<ToastId, BaseToastValue>

export type ShowToastHandler = (toast: BaseToastValue, id?: ToastId) => ToastId
export type HideToastHandler = (id: ToastId) => void
export type SimpleToastCreator = (toast: SimpleToast, id?: ToastId) => ToastId
Expand Down Expand Up @@ -88,7 +90,8 @@ export const NI = () => {
throw new Error(`Not implemented: no ToastContext set`)
}
export interface ToastContextType extends ToastCallbacks {
readonly __dispatchRef: MutableRefObject<Dispatch<ToastAction>>
readonly dispatch: Dispatch<ToastAction>
readonly toasts: ToastsMap
}

export const ToastsContext = createContext<ToastContextType>({
Expand All @@ -102,5 +105,6 @@ export const ToastsContext = createContext<ToastContextType>({
showLoadingToast: NI,
showProgressToast: NI,
showActionToast: NI,
__dispatchRef: { current: NI },
dispatch: NI,
toasts: new Map(),
})
1 change: 1 addition & 0 deletions packages/core/src/Toast/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export {
ToastIconWrapper,
ToastIconType,
} from './toastCreators'
export { ToastsAnchor } from './ToastsProvider'
9 changes: 1 addition & 8 deletions packages/core/src/Toast/toastReducer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import {
BaseToastValue,
ToastAction,
ToastActionType,
ToastId,
} from './context'

export type ToastsMap = ReadonlyMap<ToastId, BaseToastValue>
import { ToastAction, ToastActionType, ToastsMap } from './context'

/**
* Given the current state (toast map) and an action,
Expand Down
18 changes: 9 additions & 9 deletions packages/core/src/Toast/useToasts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MutableRefObject, Dispatch, useContext, useCallback } from 'react'
import { Dispatch, useContext, useCallback } from 'react'

import { createToast, removeToast, removeAllToasts } from './toastActions'
import {
Expand Down Expand Up @@ -30,29 +30,29 @@ export interface SimpleToastsDurations {
}

export const useToastCallbacks = (
dispatchRef: MutableRefObject<Dispatch<ToastAction>>,
dispatch: Dispatch<ToastAction>,
{ success, error, warning, info }: SimpleToastsDurations = {}
): ToastCallbacks => {
const showToast: ShowToastHandler = useCallback(
(toast, id) => {
const toastAction = createToast(toast, id)
dispatchRef.current(toastAction)
dispatch(toastAction)
return toastAction.id
},
[dispatchRef]
[dispatch]
)

const hideToast: HideToastHandler = useCallback(
id => {
const toastAction = removeToast(id)
dispatchRef.current(toastAction)
dispatch(toastAction)
},
[dispatchRef]
[dispatch]
)

const clearToasts = useCallback(() => {
dispatchRef.current(removeAllToasts)
}, [dispatchRef])
dispatch(removeAllToasts)
}, [dispatch])

const showSuccessToast: SimpleToastCreator = useCallback(
(toast, id) =>
Expand Down Expand Up @@ -111,7 +111,7 @@ export const useToastCallbacks = (
}

export const useToasts = () => {
const { __dispatchRef, ...callbacks } = useContext(ToastsContext)
const { dispatch, toasts, ...callbacks } = useContext(ToastsContext)

return callbacks
}

0 comments on commit 1ca733d

Please sign in to comment.