-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement Toaster according to composition API (#28039)
* feat: Implement Toaster according to composition API Implements the Toaster using the composition hook pattern. The Toaster component has one single root slot but it is mapped to potentially 4 different DOM elements (one for each toast position). The toaster does not support ref forwarding for this reason. * delete old styles * use internal slots instead * improve render * slots should only render when there are toasts * add comment * fix test * pr feedback
- Loading branch information
Showing
19 changed files
with
361 additions
and
94 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
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 @@ | ||
export * from './components/Toaster/index'; |
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
12 changes: 0 additions & 12 deletions
12
packages/react-components/react-toast/src/components/Toaster.styles.ts
This file was deleted.
Oops, something went wrong.
56 changes: 0 additions & 56 deletions
56
packages/react-components/react-toast/src/components/Toaster.tsx
This file was deleted.
Oops, something went wrong.
29 changes: 29 additions & 0 deletions
29
packages/react-components/react-toast/src/components/Toaster/Toaster.test.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 * as React from 'react'; | ||
import { render } from '@testing-library/react'; | ||
import { Toaster } from './Toaster'; | ||
import { isConformant } from '../../testing/isConformant'; | ||
import { ToasterProps } from './Toaster.types'; | ||
|
||
describe('Toaster', () => { | ||
const testid = 'test'; | ||
isConformant<ToasterProps>({ | ||
Component: Toaster, | ||
displayName: 'Toaster', | ||
requiredProps: { 'data-testid': testid } as ToasterProps, | ||
getTargetElement: result => result.getByTestId(testid), | ||
disabledTests: [ | ||
// The component does not forward refs | ||
'component-has-root-ref', | ||
'component-handles-ref', | ||
// FIXME: can't find a way to dispatch a toast during a conformance test | ||
'component-has-static-classnames-object', | ||
'component-handles-classname', | ||
'make-styles-overrides-win', | ||
], | ||
}); | ||
|
||
it('renders a default state', () => { | ||
const result = render(<Toaster />); | ||
expect(result.container).toMatchSnapshot(); | ||
}); | ||
}); |
17 changes: 17 additions & 0 deletions
17
packages/react-components/react-toast/src/components/Toaster/Toaster.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,17 @@ | ||
import * as React from 'react'; | ||
import { useToaster_unstable } from './useToaster'; | ||
import { renderToaster_unstable } from './renderToaster'; | ||
import { useToasterStyles_unstable } from './useToasterStyles.styles'; | ||
import type { ToasterProps } from './Toaster.types'; | ||
|
||
/** | ||
* Toaster component - renders a collection of toasts dispatched imperatively | ||
*/ | ||
export const Toaster: React.FC<ToasterProps> = props => { | ||
const state = useToaster_unstable(props); | ||
|
||
useToasterStyles_unstable(state); | ||
return renderToaster_unstable(state); | ||
}; | ||
|
||
Toaster.displayName = 'Toaster'; |
39 changes: 39 additions & 0 deletions
39
packages/react-components/react-toast/src/components/Toaster/Toaster.types.ts
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,39 @@ | ||
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; | ||
import { ToasterOptions } from '../../state/types'; | ||
import { Announce, AriaLiveProps } from '../AriaLive'; | ||
|
||
export type ToasterSlots = { | ||
/** | ||
* NOTE: This root slot maps in exactly the same way to the containers rendered for each toast position | ||
* There is no intention (currently) to let users customize the div for each toast position. | ||
*/ | ||
root: Slot<'div'>; | ||
}; | ||
|
||
export type ToasterSlotsInternal = ToasterSlots & { | ||
bottomRight?: Slot<'div'>; | ||
bottomLeft?: Slot<'div'>; | ||
topRight?: Slot<'div'>; | ||
topLeft?: Slot<'div'>; | ||
}; | ||
|
||
/** | ||
* Toaster Props | ||
*/ | ||
export type ToasterProps = Omit<ComponentProps<ToasterSlots>, 'children'> & | ||
Partial<ToasterOptions> & { | ||
/** | ||
* User override API for aria-live narration for toasts | ||
*/ | ||
announce?: Announce; | ||
}; | ||
|
||
/** | ||
* State used in rendering Toaster | ||
*/ | ||
export type ToasterState = ComponentState<ToasterSlotsInternal> & | ||
Pick<AriaLiveProps, 'announceRef'> & | ||
Pick<Required<ToasterProps>, 'announce'> & { | ||
offset: ToasterOptions['offset'] | undefined; | ||
renderAriaLive: boolean; | ||
}; |
14 changes: 14 additions & 0 deletions
14
...s/react-components/react-toast/src/components/Toaster/__snapshots__/Toaster.test.tsx.snap
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,14 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`Toaster renders a default state 1`] = ` | ||
<div> | ||
<div | ||
aria-live="assertive" | ||
class="fui-AriaLive__assertive" | ||
/> | ||
<div | ||
aria-live="polite" | ||
class="fui-AriaLive__polite" | ||
/> | ||
</div> | ||
`; |
5 changes: 5 additions & 0 deletions
5
packages/react-components/react-toast/src/components/Toaster/index.ts
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 @@ | ||
export * from './Toaster'; | ||
export * from './Toaster.types'; | ||
export * from './renderToaster'; | ||
export * from './useToaster'; | ||
export * from './useToasterStyles.styles'; |
33 changes: 33 additions & 0 deletions
33
packages/react-components/react-toast/src/components/Toaster/renderToaster.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,33 @@ | ||
/** @jsxRuntime classic */ | ||
/** @jsxFrag Fragment */ | ||
/** @jsx createElement */ | ||
|
||
import { createElement, Fragment } from '@fluentui/react-jsx-runtime'; | ||
import { getSlotsNext } from '@fluentui/react-utilities'; | ||
import { Portal } from '@fluentui/react-portal'; | ||
import type { ToasterState, ToasterSlotsInternal } from './Toaster.types'; | ||
import { AriaLive } from '../AriaLive'; | ||
|
||
/** | ||
* Render the final JSX of Toaster | ||
*/ | ||
export const renderToaster_unstable = (state: ToasterState) => { | ||
const { announceRef, renderAriaLive } = state; | ||
const { slots, slotProps } = getSlotsNext<ToasterSlotsInternal>(state); | ||
|
||
const hasToasts = !!slots.bottomLeft || !!slots.bottomRight || !!slots.topLeft || !!slots.topRight; | ||
|
||
return ( | ||
<> | ||
{renderAriaLive ? <AriaLive announceRef={announceRef} /> : null} | ||
{hasToasts ? ( | ||
<Portal> | ||
{slots.bottomLeft ? <slots.bottomLeft {...slotProps.bottomLeft} /> : null} | ||
{slots.bottomRight ? <slots.bottomRight {...slotProps.bottomRight} /> : null} | ||
{slots.topLeft ? <slots.topLeft {...slotProps.topLeft} /> : null} | ||
{slots.topRight ? <slots.topRight {...slotProps.topRight} /> : null} | ||
</Portal> | ||
) : null} | ||
</> | ||
); | ||
}; |
52 changes: 52 additions & 0 deletions
52
packages/react-components/react-toast/src/components/Toaster/useToaster.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,52 @@ | ||
import * as React from 'react'; | ||
import { ExtractSlotProps, Slot, getNativeElementProps, resolveShorthand } from '@fluentui/react-utilities'; | ||
import type { ToasterProps, ToasterState } from './Toaster.types'; | ||
import { TOAST_POSITIONS, ToastPosition, useToaster } from '../../state'; | ||
import { Announce } from '../AriaLive'; | ||
import { Toast } from '../Toast'; | ||
|
||
/** | ||
* Create the state required to render Toaster. | ||
* | ||
* @param props - props from this instance of Toaster | ||
*/ | ||
export const useToaster_unstable = (props: ToasterProps): ToasterState => { | ||
const { offset, announce: announceProp, ...rest } = props; | ||
const announceRef = React.useRef<Announce>(() => null); | ||
const { toastsToRender, isToastVisible } = useToaster<HTMLDivElement>(rest); | ||
const announce = React.useCallback<Announce>((message, options) => announceRef.current(message, options), []); | ||
|
||
const rootProps = getNativeElementProps('div', rest); | ||
|
||
const createPositionSlot = (toastPosition: ToastPosition) => | ||
resolveShorthand(toastsToRender.has(toastPosition) ? rootProps : null, { | ||
defaultProps: { | ||
children: toastsToRender.get(toastPosition)?.map(toast => ( | ||
<Toast {...toast} announce={announce} key={toast.toastId} visible={isToastVisible(toast.toastId)}> | ||
{toast.content as React.ReactNode} | ||
</Toast> | ||
)), | ||
'data-toaster-position': toastPosition, | ||
// Explicitly casting because our slot types can't handle data attributes | ||
} as ExtractSlotProps<Slot<'div'>>, | ||
}); | ||
|
||
return { | ||
components: { | ||
root: 'div', | ||
bottomLeft: 'div', | ||
bottomRight: 'div', | ||
topLeft: 'div', | ||
topRight: 'div', | ||
}, | ||
root: resolveShorthand(rootProps, { required: true }), | ||
bottomLeft: createPositionSlot(TOAST_POSITIONS.bottomLeft), | ||
bottomRight: createPositionSlot(TOAST_POSITIONS.bottomRight), | ||
topLeft: createPositionSlot(TOAST_POSITIONS.topLeft), | ||
topRight: createPositionSlot(TOAST_POSITIONS.topRight), | ||
announceRef, | ||
offset, | ||
announce: announceProp ?? announce, | ||
renderAriaLive: !announceProp, | ||
}; | ||
}; |
52 changes: 52 additions & 0 deletions
52
packages/react-components/react-toast/src/components/Toaster/useToasterStyles.styles.ts
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,52 @@ | ||
import { makeStyles, mergeClasses } from '@griffel/react'; | ||
import type { ToasterSlots, ToasterState } from './Toaster.types'; | ||
import type { SlotClassNames } from '@fluentui/react-utilities'; | ||
import { TOAST_POSITIONS, getPositionStyles } from '../../state/index'; | ||
|
||
export const toasterClassNames: SlotClassNames<ToasterSlots> = { | ||
root: 'fui-Toaster', | ||
}; | ||
|
||
/** | ||
* Styles for the root slot | ||
*/ | ||
const useStyles = makeStyles({ | ||
root: { | ||
position: 'fixed', | ||
width: '292px', | ||
pointerEvents: 'none', | ||
}, | ||
}); | ||
|
||
/** | ||
* Apply styling to the Toaster slots based on the state | ||
*/ | ||
export const useToasterStyles_unstable = (state: ToasterState): ToasterState => { | ||
const styles = useStyles(); | ||
const className = mergeClasses(toasterClassNames.root, styles.root, state.root.className); | ||
if (state.bottomLeft) { | ||
state.bottomLeft.className = className; | ||
state.bottomLeft.style ??= {}; | ||
Object.assign(state.bottomLeft.style, getPositionStyles(TOAST_POSITIONS.bottomLeft, state.offset)); | ||
} | ||
|
||
if (state.bottomRight) { | ||
state.bottomRight.className = className; | ||
state.bottomRight.style ??= {}; | ||
Object.assign(state.bottomRight.style, getPositionStyles(TOAST_POSITIONS.bottomRight, state.offset)); | ||
} | ||
|
||
if (state.topLeft) { | ||
state.topLeft.className = className; | ||
state.topLeft.style ??= {}; | ||
Object.assign(state.topLeft.style, getPositionStyles(TOAST_POSITIONS.topLeft, state.offset)); | ||
} | ||
|
||
if (state.topRight) { | ||
state.topRight.className = className; | ||
state.topRight.style ??= {}; | ||
Object.assign(state.topRight.style, getPositionStyles(TOAST_POSITIONS.topRight, state.offset)); | ||
} | ||
|
||
return state; | ||
}; |
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
Oops, something went wrong.