Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 useToastController(): {
dispatchToast: (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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import { useStyles } from './useTimerStyles.styles';

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

React.useEffect(() => {
if (ref.current) {
const timerElement = ref.current;
timerElement.addEventListener('animationend', onTimeout);
return () => timerElement.removeEventListener('animationend', onTimeout);
}
}, [onTimeout]);

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

if (timeout < 0) {
return null;
}

return <span ref={ref} style={style} className={styles.progress} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Timer';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { makeStyles } from '@griffel/react';

export const useStyles = makeStyles({
progress: {
animationName: {
from: {
opacity: 0,
},
to: {
opacity: 0,
},
},
},
});
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}
<Timer onTimeout={close} timeout={timeout ?? -1} 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 { getToastsToRender, isToastVisible } = useToaster();

const styles = useStyles();

return (
<Portal>
<div>
{getToastsToRender((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 { useToastController } 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 './useToastController';
export { getPositionStyles } from './vanilla';
25 changes: 25 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,25 @@
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>;
}

export type ToastEventListenerGeneric<K extends keyof ToastEventMap> = (e: ToastEventMap[K]) => void;
export type ToastShowEventListener = ToastEventListenerGeneric<typeof EVENTS.show>;
export type ToastEventListener = ToastShowEventListener;
18 changes: 18 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,18 @@
import * as React from 'react';
import { Toast } from './vanilla/toast';
import { useForceUpdate } from '@fluentui/react-utilities';

export function useToast() {
const forceRender = useForceUpdate();
const [toast] = React.useState(() => {
const newToast = new Toast();
newToast.onUpdate = forceRender;
return newToast;
});

return {
play: toast.play,
pause: toast.pause,
running: toast.running,
};
}
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 { dispatchToast as dispatchToastVanilla } from './vanilla/dispatchToast';
import * as React from 'react';
import { ToastOptions } from './types';

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

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

return {
dispatchToast,
};
}
Loading