Private
Public Access
1
0

feat: Fluent UI Outlook Lite + connections mockup

This commit is contained in:
2026-04-14 18:52:25 +00:00
parent 1199eff6c3
commit dfa4010406
34820 changed files with 1003813 additions and 205 deletions

View File

@@ -0,0 +1,184 @@
'use client';
import * as React from 'react';
import { isAnimationRunning } from '../utils/isAnimationRunning';
export const DEFAULT_ANIMATION_OPTIONS = {
fill: 'forwards'
};
// A motion atom's default reduced motion is a simple 1 ms duration.
// But an atom can define a custom reduced motion, overriding keyframes and/or params like duration, easing, iterations, etc.
const DEFAULT_REDUCED_MOTION_ATOM = {
duration: 1
};
/**
* Creates an animation handle that controls multiple animations.
* Is used to avoid leaking "element" references from the hook.
*
* @param animations
*/ function createHandle(animations) {
return {
set playbackRate (rate){
animations.forEach((animation)=>{
animation.playbackRate = rate;
});
},
setMotionEndCallbacks (onfinish, oncancel) {
// Heads up!
// This could use "Animation:finished", but it's causing a memory leak in Chromium.
// See: https://issues.chromium.org/u/2/issues/383016426
const promises = animations.map((animation)=>{
return new Promise((resolve, reject)=>{
animation.onfinish = ()=>resolve();
animation.oncancel = ()=>reject();
});
});
Promise.all(promises).then(()=>{
onfinish();
}).catch(()=>{
oncancel();
});
},
isRunning () {
return animations.some((animation)=>isAnimationRunning(animation));
},
dispose: ()=>{
animations.length = 0;
},
cancel: ()=>{
animations.forEach((animation)=>{
animation.cancel();
});
},
pause: ()=>{
animations.forEach((animation)=>{
animation.pause();
});
},
play: ()=>{
animations.forEach((animation)=>{
animation.play();
});
},
finish: ()=>{
animations.forEach((animation)=>{
animation.finish();
});
},
reverse: ()=>{
// Heads up!
//
// This is used for the interruptible motion. If the animation is running, we need to reverse it.
//
// TODO: what do with animations that have "delay"?
// TODO: what do with animations that have different "durations"?
animations.forEach((animation)=>{
animation.reverse();
});
}
};
}
function useAnimateAtomsInSupportedEnvironment() {
var _window_Animation;
// eslint-disable-next-line @nx/workspace-no-restricted-globals
const SUPPORTS_PERSIST = typeof window !== 'undefined' && typeof ((_window_Animation = window.Animation) === null || _window_Animation === void 0 ? void 0 : _window_Animation.prototype.persist) === 'function';
return React.useCallback((element, value, options)=>{
const atoms = Array.isArray(value) ? value : [
value
];
const { isReducedMotion } = options;
const animations = atoms.map((motion)=>{
// Grab the custom reduced motion definition if it exists, or fall back to the default reduced motion.
const { keyframes: motionKeyframes, reducedMotion = DEFAULT_REDUCED_MOTION_ATOM, ...params } = motion;
// Grab the reduced motion keyframes if they exist, or fall back to the regular keyframes.
const { keyframes: reducedMotionKeyframes = motionKeyframes, ...reducedMotionParams } = reducedMotion;
const animationKeyframes = isReducedMotion ? reducedMotionKeyframes : motionKeyframes;
const animationParams = {
...DEFAULT_ANIMATION_OPTIONS,
...params,
// Use reduced motion overrides (e.g. duration, easing) when reduced motion is enabled
...isReducedMotion && reducedMotionParams
};
try {
// Firefox can throw an error when calling `element.animate()`.
// See: https://github.com/microsoft/fluentui/issues/33902
const animation = element.animate(animationKeyframes, animationParams);
if (SUPPORTS_PERSIST) {
// Chromium browsers can return null when calling `element.animate()`.
// See: https://github.com/microsoft/fluentui/issues/33902
animation === null || animation === void 0 ? void 0 : animation.persist();
} else {
const resultKeyframe = animationKeyframes[animationKeyframes.length - 1];
var _element_style;
Object.assign((_element_style = element.style) !== null && _element_style !== void 0 ? _element_style : {}, resultKeyframe);
}
return animation;
} catch (e) {
return null;
}
}).filter((animation)=>!!animation);
return createHandle(animations);
}, [
SUPPORTS_PERSIST
]);
}
/**
* In test environments, this hook is used to delay the execution of a callback until the next render. This is necessary
* to ensure that the callback is not executed synchronously, which would cause the test to fail.
*
* @see https://github.com/microsoft/fluentui/issues/31701
*/ function useAnimateAtomsInTestEnvironment() {
const [count, setCount] = React.useState(0);
const callbackRef = React.useRef(undefined);
const realAnimateAtoms = useAnimateAtomsInSupportedEnvironment();
React.useEffect(()=>{
if (count > 0) {
var _callbackRef_current;
(_callbackRef_current = callbackRef.current) === null || _callbackRef_current === void 0 ? void 0 : _callbackRef_current.call(callbackRef);
}
}, [
count
]);
return React.useCallback((element, value, options)=>{
const ELEMENT_SUPPORTS_WEB_ANIMATIONS = typeof element.animate === 'function';
// Heads up!
// If the environment supports Web Animations API, we can use the native implementation.
if (ELEMENT_SUPPORTS_WEB_ANIMATIONS) {
return realAnimateAtoms(element, value, options);
}
return {
setMotionEndCallbacks (onfinish) {
callbackRef.current = onfinish;
setCount((v)=>v + 1);
},
set playbackRate (rate){
/* no-op */ },
isRunning () {
return false;
},
dispose () {
/* no-op */ },
cancel () {
/* no-op */ },
pause () {
/* no-op */ },
play () {
/* no-op */ },
finish () {
/* no-op */ },
reverse () {
/* no-op */ }
};
}, [
realAnimateAtoms
]);
}
/**
* @internal
*/ export function useAnimateAtoms() {
'use no memo';
if (process.env.NODE_ENV === 'test') {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useAnimateAtomsInTestEnvironment();
}
// eslint-disable-next-line react-hooks/rules-of-hooks
return useAnimateAtomsInSupportedEnvironment();
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,32 @@
'use client';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
import { useIsomorphicLayoutEffect } from '@fluentui/react-utilities';
import * as React from 'react';
const REDUCED_MEDIA_QUERY = 'screen and (prefers-reduced-motion: reduce)';
// TODO: find a better approach there as each hook creates a separate subscription
export function useIsReducedMotion() {
const { targetDocument } = useFluent();
var _targetDocument_defaultView;
const targetWindow = (_targetDocument_defaultView = targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.defaultView) !== null && _targetDocument_defaultView !== void 0 ? _targetDocument_defaultView : null;
const queryValue = React.useRef(false);
const isEnabled = React.useCallback(()=>queryValue.current, []);
useIsomorphicLayoutEffect(()=>{
if (targetWindow === null || typeof targetWindow.matchMedia !== 'function') {
return;
}
const queryMatch = targetWindow.matchMedia(REDUCED_MEDIA_QUERY);
if (queryMatch.matches) {
queryValue.current = true;
}
const matchListener = (e)=>{
queryValue.current = e.matches;
};
queryMatch.addEventListener('change', matchListener);
return ()=>{
queryMatch.removeEventListener('change', matchListener);
};
}, [
targetWindow
]);
return isEnabled;
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useIsReducedMotion.ts"],"sourcesContent":["'use client';\n\nimport { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';\nimport { useIsomorphicLayoutEffect } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\nconst REDUCED_MEDIA_QUERY = 'screen and (prefers-reduced-motion: reduce)';\n\n// TODO: find a better approach there as each hook creates a separate subscription\n\nexport function useIsReducedMotion(): () => boolean {\n const { targetDocument } = useFluent();\n const targetWindow: Window | null = targetDocument?.defaultView ?? null;\n\n const queryValue = React.useRef<boolean>(false);\n const isEnabled = React.useCallback(() => queryValue.current, []);\n\n useIsomorphicLayoutEffect(() => {\n if (targetWindow === null || typeof targetWindow.matchMedia !== 'function') {\n return;\n }\n\n const queryMatch = targetWindow.matchMedia(REDUCED_MEDIA_QUERY);\n\n if (queryMatch.matches) {\n queryValue.current = true;\n }\n\n const matchListener = (e: MediaQueryListEvent) => {\n queryValue.current = e.matches;\n };\n\n queryMatch.addEventListener('change', matchListener);\n\n return () => {\n queryMatch.removeEventListener('change', matchListener);\n };\n }, [targetWindow]);\n\n return isEnabled;\n}\n"],"names":["useFluent_unstable","useFluent","useIsomorphicLayoutEffect","React","REDUCED_MEDIA_QUERY","useIsReducedMotion","targetDocument","targetWindow","defaultView","queryValue","useRef","isEnabled","useCallback","current","matchMedia","queryMatch","matches","matchListener","e","addEventListener","removeEventListener"],"mappings":"AAAA;AAEA,SAASA,sBAAsBC,SAAS,QAAQ,kCAAkC;AAClF,SAASC,yBAAyB,QAAQ,4BAA4B;AACtE,YAAYC,WAAW,QAAQ;AAE/B,MAAMC,sBAAsB;AAE5B,kFAAkF;AAElF,OAAO,SAASC;IACd,MAAM,EAAEC,cAAc,EAAE,GAAGL;QACSK;IAApC,MAAMC,eAA8BD,CAAAA,8BAAAA,2BAAAA,qCAAAA,eAAgBE,WAAW,cAA3BF,yCAAAA,8BAA+B;IAEnE,MAAMG,aAAaN,MAAMO,MAAM,CAAU;IACzC,MAAMC,YAAYR,MAAMS,WAAW,CAAC,IAAMH,WAAWI,OAAO,EAAE,EAAE;IAEhEX,0BAA0B;QACxB,IAAIK,iBAAiB,QAAQ,OAAOA,aAAaO,UAAU,KAAK,YAAY;YAC1E;QACF;QAEA,MAAMC,aAAaR,aAAaO,UAAU,CAACV;QAE3C,IAAIW,WAAWC,OAAO,EAAE;YACtBP,WAAWI,OAAO,GAAG;QACvB;QAEA,MAAMI,gBAAgB,CAACC;YACrBT,WAAWI,OAAO,GAAGK,EAAEF,OAAO;QAChC;QAEAD,WAAWI,gBAAgB,CAAC,UAAUF;QAEtC,OAAO;YACLF,WAAWK,mBAAmB,CAAC,UAAUH;QAC3C;IACF,GAAG;QAACV;KAAa;IAEjB,OAAOI;AACT"}

View File

@@ -0,0 +1,23 @@
'use client';
import * as React from 'react';
export function useMotionImperativeRef(imperativeRef) {
const animationRef = React.useRef(undefined);
React.useImperativeHandle(imperativeRef, ()=>({
setPlayState: (state)=>{
if (state === 'running') {
var _animationRef_current;
(_animationRef_current = animationRef.current) === null || _animationRef_current === void 0 ? void 0 : _animationRef_current.play();
}
if (state === 'paused') {
var _animationRef_current1;
(_animationRef_current1 = animationRef.current) === null || _animationRef_current1 === void 0 ? void 0 : _animationRef_current1.pause();
}
},
setPlaybackRate: (rate)=>{
if (animationRef.current) {
animationRef.current.playbackRate = rate;
}
}
}));
return animationRef;
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useMotionImperativeRef.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport type { AnimationHandle, MotionImperativeRef } from '../types';\n\nexport function useMotionImperativeRef(\n imperativeRef: React.Ref<MotionImperativeRef | undefined> | undefined,\n // eslint-disable-next-line @typescript-eslint/no-deprecated\n): React.MutableRefObject<AnimationHandle | undefined> {\n const animationRef = React.useRef<AnimationHandle | undefined>(undefined);\n\n React.useImperativeHandle(imperativeRef, () => ({\n setPlayState: state => {\n if (state === 'running') {\n animationRef.current?.play();\n }\n\n if (state === 'paused') {\n animationRef.current?.pause();\n }\n },\n setPlaybackRate: rate => {\n if (animationRef.current) {\n animationRef.current.playbackRate = rate;\n }\n },\n }));\n\n return animationRef;\n}\n"],"names":["React","useMotionImperativeRef","imperativeRef","animationRef","useRef","undefined","useImperativeHandle","setPlayState","state","current","play","pause","setPlaybackRate","rate","playbackRate"],"mappings":"AAAA;AAEA,YAAYA,WAAW,QAAQ;AAG/B,OAAO,SAASC,uBACdC,aAAqE;IAGrE,MAAMC,eAAeH,MAAMI,MAAM,CAA8BC;IAE/DL,MAAMM,mBAAmB,CAACJ,eAAe,IAAO,CAAA;YAC9CK,cAAcC,CAAAA;gBACZ,IAAIA,UAAU,WAAW;wBACvBL;qBAAAA,wBAAAA,aAAaM,OAAO,cAApBN,4CAAAA,sBAAsBO,IAAI;gBAC5B;gBAEA,IAAIF,UAAU,UAAU;wBACtBL;qBAAAA,yBAAAA,aAAaM,OAAO,cAApBN,6CAAAA,uBAAsBQ,KAAK;gBAC7B;YACF;YACAC,iBAAiBC,CAAAA;gBACf,IAAIV,aAAaM,OAAO,EAAE;oBACxBN,aAAaM,OAAO,CAACK,YAAY,GAAGD;gBACtC;YACF;QACF,CAAA;IAEA,OAAOV;AACT"}

View File

@@ -0,0 +1,27 @@
'use client';
import { useForceUpdate } from '@fluentui/react-utilities';
import * as React from 'react';
/**
* This hook manages the mounted state of a component, based on the "visible" and "unmountOnExit" props.
* It simulates the behavior of getDerivedStateFromProps(), which is not available in functional components.
*/ export function useMountedState(visible = false, unmountOnExit = false) {
const mountedRef = React.useRef(unmountOnExit ? visible : true);
const forceUpdate = useForceUpdate();
const setMounted = React.useCallback((newValue)=>{
if (mountedRef.current !== newValue) {
mountedRef.current = newValue;
forceUpdate();
}
}, [
forceUpdate
]);
React.useEffect(()=>{
if (visible) {
mountedRef.current = visible;
}
});
return [
visible || mountedRef.current,
setMounted
];
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useMountedState.ts"],"sourcesContent":["'use client';\n\nimport { useForceUpdate } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\n/**\n * This hook manages the mounted state of a component, based on the \"visible\" and \"unmountOnExit\" props.\n * It simulates the behavior of getDerivedStateFromProps(), which is not available in functional components.\n */\nexport function useMountedState(\n visible: boolean = false,\n unmountOnExit: boolean = false,\n): [boolean, (value: boolean) => void] {\n const mountedRef = React.useRef<boolean>(unmountOnExit ? visible : true);\n const forceUpdate = useForceUpdate();\n\n const setMounted = React.useCallback(\n (newValue: boolean) => {\n if (mountedRef.current !== newValue) {\n mountedRef.current = newValue;\n forceUpdate();\n }\n },\n [forceUpdate],\n );\n\n React.useEffect(() => {\n if (visible) {\n mountedRef.current = visible;\n }\n });\n\n return [visible || mountedRef.current, setMounted];\n}\n"],"names":["useForceUpdate","React","useMountedState","visible","unmountOnExit","mountedRef","useRef","forceUpdate","setMounted","useCallback","newValue","current","useEffect"],"mappings":"AAAA;AAEA,SAASA,cAAc,QAAQ,4BAA4B;AAC3D,YAAYC,WAAW,QAAQ;AAE/B;;;CAGC,GACD,OAAO,SAASC,gBACdC,UAAmB,KAAK,EACxBC,gBAAyB,KAAK;IAE9B,MAAMC,aAAaJ,MAAMK,MAAM,CAAUF,gBAAgBD,UAAU;IACnE,MAAMI,cAAcP;IAEpB,MAAMQ,aAAaP,MAAMQ,WAAW,CAClC,CAACC;QACC,IAAIL,WAAWM,OAAO,KAAKD,UAAU;YACnCL,WAAWM,OAAO,GAAGD;YACrBH;QACF;IACF,GACA;QAACA;KAAY;IAGfN,MAAMW,SAAS,CAAC;QACd,IAAIT,SAAS;YACXE,WAAWM,OAAO,GAAGR;QACvB;IACF;IAEA,OAAO;QAACA,WAAWE,WAAWM,OAAO;QAAEH;KAAW;AACpD"}