Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
"@types/react-dom": "17.0.15",
"@types/react-is": "17.0.3",
"@types/react-test-renderer": "17.0.2",
"@types/react-transition-group": "4.4.6",
"@types/request-promise-native": "1.0.18",
"@types/scheduler": "0.16.2",
"@types/semver": "^6.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export const Animation = React.forwardRef<HTMLDivElement, AnimationProps>((props
className={!isChildrenFunction ? cx(animationClasses, className, (child as any)?.props?.className) : ''}
>
{isChildrenFunction ? (
// @ts-ignore - @types/react-transition-group doesn't actually include this API, nor is it documented
({ state }) => {
const childWithClasses = (children as AnimationChildrenProp)({
classes: cx(animationClasses, className, (child as any)?.props?.className),
Expand Down
14 changes: 14 additions & 0 deletions packages/react-components/react-toast/.storybook/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const rootMain = require('../../../../.storybook/main');

module.exports = /** @type {Omit<import('../../../../.storybook/main'), 'typescript'|'babel'>} */ ({
...rootMain,
stories: [...rootMain.stories, '../stories/**/*.stories.mdx', '../stories/**/index.stories.@(ts|tsx)'],
addons: [...rootMain.addons],
webpackFinal: (config, options) => {
const localConfig = { ...rootMain.webpackFinal(config, options) };

// add your own webpack tweaks if needed

return localConfig;
},
});
7 changes: 7 additions & 0 deletions packages/react-components/react-toast/.storybook/preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as rootPreview from '../../../../.storybook/preview';

/** @type {typeof rootPreview.decorators} */
export const decorators = [...rootPreview.decorators];

/** @type {typeof rootPreview.parameters} */
export const parameters = { ...rootPreview.parameters };
10 changes: 10 additions & 0 deletions packages/react-components/react-toast/.storybook/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "",
"allowJs": true,
"checkJs": true,
"types": ["static-assets", "environment", "storybook__addons"]
},
"include": ["../stories/**/*.stories.ts", "../stories/**/*.stories.tsx", "*.js"]
}
13 changes: 13 additions & 0 deletions packages/react-components/react-toast/etc/react-toast.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@

```ts

import * as React_2 from 'react';

// @public (undocumented)
export const Toaster: React_2.FC;

// @public (undocumented)
export type ToastPosition = 'top-right' | 'top-center' | 'top-left' | 'bottom-right' | 'bottom-center' | 'bottom-left';

// @public (undocumented)
export function useToastFactory(): {
createToast: (content: React_2.ReactNode, options?: ToastOptions | undefined) => void;
};

// (No @packageDocumentation comment for this package)

```
7 changes: 6 additions & 1 deletion packages/react-components/react-toast/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"test": "jest --passWithNoTests",
"test-ssr": "test-ssr ./stories/**/*.stories.tsx",
"type-check": "tsc -b tsconfig.json",
"generate-api": "just-scripts generate-api"
"generate-api": "just-scripts generate-api",
"storybook": "start-storybook",
"start": "yarn storybook"
},
"devDependencies": {
"@fluentui/eslint-plugin": "*",
Expand All @@ -31,7 +33,10 @@
"@fluentui/scripts-tasks": "*"
},
"dependencies": {
"react-transition-group": "^4.4.1",
"@fluentui/react-jsx-runtime": "9.0.0-alpha.2",
"@fluentui/react-portal": "^9.2.6",
"@fluentui/react-shared-contexts": "^9.3.3",
"@fluentui/react-theme": "^9.1.7",
"@fluentui/react-utilities": "^9.8.0",
"@griffel/react": "^1.5.2",
Expand Down
47 changes: 47 additions & 0 deletions packages/react-components/react-toast/src/components/Timer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from 'react';
import { makeStyles } from '@griffel/react';

const useStyles = makeStyles({
progress: {
animationName: {
from: {
opacity: 0,
},
to: {
opacity: 0,
},
},
},
});

export const Timer: React.FC<{
running: boolean;
timeout: number;
onTimeout: () => void;
}> = props => {
const styles = useStyles();
const { running, timeout, onTimeout } = props;
const cleanupRef = React.useRef<() => void>(() => null);

const bindEventListeners = React.useCallback(
(el: HTMLElement) => {
cleanupRef.current();
if (el) {
el.addEventListener('animationend', onTimeout);
cleanupRef.current = () => el.removeEventListener('animationend', onTimeout);
}
},
[onTimeout],
);

const style: React.CSSProperties = {
animationDuration: `${timeout}ms`,
animationPlayState: running ? 'running' : 'paused',
};

if (timeout < 0) {
return null;
}

return <span ref={bindEventListeners} style={style} className={styles.progress} />;
};
106 changes: 106 additions & 0 deletions packages/react-components/react-toast/src/components/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* ⚠️ This is temporary and WILL be removed
*/

import * as React from 'react';
import { Transition } from 'react-transition-group';
import { useIsomorphicLayoutEffect } from '@fluentui/react-utilities';
import { makeStyles, mergeClasses, shorthands } from '@griffel/react';
import { useToast, Toast as ToastProps } from '../state';
import { Timer } from './Timer';

const useStyles = makeStyles({
toast: {
...shorthands.border('2px', 'dashed', 'red'),
...shorthands.padding('4px'),
display: 'flex',
minHeight: '40px',
maxHeight: '40px',
minWidth: '200px',
maxWidth: '200px',
alignItems: 'center',
backgroundColor: 'white',
},

slide: {
animationDuration: '200ms, 400ms',
animationDelay: '0ms, 200ms',
animationName: [
{
from: {
height: '0',
minHeight: '0',
maxHeight: '0',
opacity: 0,
},
to: {
opacity: 0,
},
},
{
from: {
opacity: 0,
},
to: {
opacity: 1,
},
},
],
},

fadeOut: {
animationDuration: '400ms, 200ms',
animationDelay: '0ms, 400ms',
animationName: [
{
from: {
opacity: 1,
},
to: {
opacity: 0,
},
},
{
from: {
opacity: 0,
},
to: {
opacity: 0,
height: 0,
maxHeight: 0,
minHeight: 0,
},
},
],
},
});

export const Toast: React.FC<Omit<ToastProps, 'content'> & { visible: boolean }> = props => {
const styles = useStyles();
const { visible, children, close, remove, timeout } = props;
const { play, running } = useToast();
const toastRef = React.useRef<HTMLDivElement>(null);

// start the toast once it's fully in
useIsomorphicLayoutEffect(() => {
if (toastRef.current) {
const toast = toastRef.current;
toast.addEventListener('animationend', play, {
once: true,
});

return () => {
toast.removeEventListener('animationend', play);
};
}
}, [play, toastRef]);

return (
<Transition in={visible} unmountOnExit mountOnEnter timeout={500} onExited={remove} nodeRef={toastRef}>
<div ref={toastRef} className={mergeClasses(styles.toast, visible && styles.slide, !visible && styles.fadeOut)}>
{children as React.ReactNode}
<Timer onTimeout={close} timeout={!timeout ? -1 : timeout} running={running} />
</div>
</Transition>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* ⚠️ This is temporary and WILL be removed
*/

import * as React from 'react';
import { Portal } from '@fluentui/react-portal';
import { useToaster, getPositionStyles } from '../state';
import { Toast } from './Toast';
import { makeStyles, mergeClasses } from '@griffel/react';

const useStyles = makeStyles({
container: {
position: 'fixed',
},
});

export const Toaster: React.FC = () => {
const { getToastToRender, isToastVisible } = useToaster();

const styles = useStyles();

return (
<Portal>
<div>
{getToastToRender((position, toasts) => {
return (
<div key={position} style={getPositionStyles(position)} className={mergeClasses(styles.container)}>
{toasts.map(({ content, ...toastProps }) => {
return (
<Toast
{...toastProps}
key={`toast-${toastProps.toastId}`}
visible={isToastVisible(toastProps.toastId)}
>
{content as React.ReactNode}
</Toast>
);
})}
</div>
);
})}
</div>
</Portal>
);
};
6 changes: 4 additions & 2 deletions packages/react-components/react-toast/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
// TODO: replace with real exports
export {};
export { Toaster } from './components/Toaster';

export { useToastFactory } from './state';
export type { ToastPosition } from './state';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const EVENTS = {
show: 'fui-toast-show',
} as const;
5 changes: 5 additions & 0 deletions packages/react-components/react-toast/src/state/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './types';
export * from './useToaster';
export * from './useToast';
export * from './useToastFactory';
export { getPositionStyles } from './vanilla';
21 changes: 21 additions & 0 deletions packages/react-components/react-toast/src/state/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { EVENTS } from './constants';

export type ToastId = string;

export type ToastPosition = 'top-right' | 'top-center' | 'top-left' | 'bottom-right' | 'bottom-center' | 'bottom-left';

export interface ToastOptions {
toastId?: ToastId;
position?: ToastPosition;
content?: unknown;
timeout?: number;
}

export interface Toast extends Required<Omit<ToastOptions, 'toasterId'>> {
close: () => void;
remove: () => void;
}

export interface ToastEventMap {
[EVENTS.show]: CustomEvent<ToastOptions>;
}
25 changes: 25 additions & 0 deletions packages/react-components/react-toast/src/state/useToast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from 'react';
import { Toast } from './vanilla/toast';
import { useIsomorphicLayoutEffect } from '@fluentui/react-utilities';

export function useToast() {
const [toast] = React.useState(() => new Toast());
const [_, forceRender] = React.useReducer(() => ({}), {});
useIsomorphicLayoutEffect(() => {
toast.onUpdate = forceRender;
}, [toast]);

const play = React.useCallback(() => {
toast.play();
}, [toast]);

const pause = React.useCallback(() => {
toast.pause();
}, [toast]);

return {
play,
pause,
running: toast.running,
};
}
21 changes: 21 additions & 0 deletions packages/react-components/react-toast/src/state/useToastFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
import { createToast as createToastVanilla } from './vanilla/createToast';
import * as React from 'react';
import { ToastOptions } from './types';

export function useToastFactory() {
const { targetDocument } = useFluent();

const createToast = React.useCallback(
(content: React.ReactNode, options?: ToastOptions) => {
if (targetDocument) {
createToastVanilla(content, options, targetDocument);
}
},
[targetDocument],
);

return {
createToast,
};
}
Loading