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

4
node_modules/@fluentui/react-menu/lib/utils/index.js generated vendored Normal file
View File

@@ -0,0 +1,4 @@
export { MENU_ENTER_EVENT, dispatchMenuEnterEvent, useOnMenuMouseEnter } from './useOnMenuEnter';
export { useIsSubmenu } from './useIsSubmenu';
export { useValidateNesting } from './useValidateNesting';
export { MENU_SAFEZONE_TIMEOUT_EVENT, useOnMenuSafeZoneTimeout } from './useOnMenuSafeZoneTimeout';

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/utils/index.ts"],"sourcesContent":["export { MENU_ENTER_EVENT, dispatchMenuEnterEvent, useOnMenuMouseEnter } from './useOnMenuEnter';\nexport { useIsSubmenu } from './useIsSubmenu';\nexport { useValidateNesting } from './useValidateNesting';\nexport { MENU_SAFEZONE_TIMEOUT_EVENT, useOnMenuSafeZoneTimeout } from './useOnMenuSafeZoneTimeout';\n"],"names":["MENU_ENTER_EVENT","dispatchMenuEnterEvent","useOnMenuMouseEnter","useIsSubmenu","useValidateNesting","MENU_SAFEZONE_TIMEOUT_EVENT","useOnMenuSafeZoneTimeout"],"mappings":"AAAA,SAASA,gBAAgB,EAAEC,sBAAsB,EAAEC,mBAAmB,QAAQ,mBAAmB;AACjG,SAASC,YAAY,QAAQ,iBAAiB;AAC9C,SAASC,kBAAkB,QAAQ,uBAAuB;AAC1D,SAASC,2BAA2B,EAAEC,wBAAwB,QAAQ,6BAA6B"}

View File

@@ -0,0 +1,16 @@
'use client';
import { useHasParentContext } from '@fluentui/react-context-selector';
import { useMenuContext_unstable } from '../contexts/menuContext';
import { MenuListContext } from '../contexts/menuListContext';
/**
* A component can be a part of a submenu whether its menu context `isSubmenu` flag is true
* or whether it is a part of a `MenuList`
*
* A simple hook to check box contexts easily
*
* @returns whether the component is part of a submenu
*/ export function useIsSubmenu() {
const menuContextValue = useMenuContext_unstable((context)=>context.isSubmenu);
const hasMenuListContext = useHasParentContext(MenuListContext);
return menuContextValue || hasMenuListContext;
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/utils/useIsSubmenu.ts"],"sourcesContent":["'use client';\n\nimport { useHasParentContext } from '@fluentui/react-context-selector';\nimport { useMenuContext_unstable } from '../contexts/menuContext';\nimport { MenuListContext } from '../contexts/menuListContext';\n\n/**\n * A component can be a part of a submenu whether its menu context `isSubmenu` flag is true\n * or whether it is a part of a `MenuList`\n *\n * A simple hook to check box contexts easily\n *\n * @returns whether the component is part of a submenu\n */\nexport function useIsSubmenu(): boolean {\n const menuContextValue = useMenuContext_unstable(context => context.isSubmenu);\n const hasMenuListContext = useHasParentContext(MenuListContext);\n\n return menuContextValue || hasMenuListContext;\n}\n"],"names":["useHasParentContext","useMenuContext_unstable","MenuListContext","useIsSubmenu","menuContextValue","context","isSubmenu","hasMenuListContext"],"mappings":"AAAA;AAEA,SAASA,mBAAmB,QAAQ,mCAAmC;AACvE,SAASC,uBAAuB,QAAQ,0BAA0B;AAClE,SAASC,eAAe,QAAQ,8BAA8B;AAE9D;;;;;;;CAOC,GACD,OAAO,SAASC;IACd,MAAMC,mBAAmBH,wBAAwBI,CAAAA,UAAWA,QAAQC,SAAS;IAC7E,MAAMC,qBAAqBP,oBAAoBE;IAE/C,OAAOE,oBAAoBG;AAC7B"}

View File

@@ -0,0 +1,60 @@
'use client';
import * as React from 'react';
import { useEventCallback, elementContains } from '@fluentui/react-utilities';
/**
* Name of the custom event
*/ export const MENU_ENTER_EVENT = 'fuimenuenter';
/**
* This hook works similarly to @see useOnClickOutside
*
* Problem: Trying to behave the same as system menus:
* When the mouse leaves a stack of nested menus the stack should not dismiss.
* However if the mouse leaves a stack of menus and enters a parent menu all its children menu should dismiss.
*
* We don't use the native mouseenter event because it would trigger too many times in the document
* Instead, dispatch custom DOM event from the menu so that it can bubble
* Each nested menu can use the listener to check if the event is from a child or parent menu
*/ export const useOnMenuMouseEnter = (options)=>{
const { refs, callback, element, disabled } = options;
// Keep mouse event here because this is essentially a custom 'mouseenter' event
const listener = useEventCallback((ev)=>{
const popoverRef = refs[0];
const someMenuPopover = ev.target;
var _popoverRef_current;
// someMenu is a child -> will always be contained because of vParents
// someMenu is a parent -> will always not be contained because no vParent
// someMenu is the current popover -> it will contain itself
const isOutsidePopover = !elementContains((_popoverRef_current = popoverRef.current) !== null && _popoverRef_current !== void 0 ? _popoverRef_current : null, someMenuPopover);
if (isOutsidePopover && !disabled) {
callback(ev);
}
});
React.useEffect(()=>{
// eslint-disable-next-line eqeqeq
if (element == null) {
return;
}
if (!disabled) {
element.addEventListener(MENU_ENTER_EVENT, listener);
}
return ()=>{
element.removeEventListener(MENU_ENTER_EVENT, listener);
};
}, [
listener,
element,
disabled
]);
};
/**
* Dispatches the custom MouseEvent enter event. Similar to calling `el.click()`
* @param el - element for the event target
* @param nativeEvent - the native mouse event this is mapped to
*/ export const dispatchMenuEnterEvent = (el, nativeEvent)=>{
el.dispatchEvent(new CustomEvent(MENU_ENTER_EVENT, {
bubbles: true,
detail: {
nativeEvent
}
}));
};

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/utils/useOnMenuEnter.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { useEventCallback, elementContains } from '@fluentui/react-utilities';\nimport type { UseOnClickOrScrollOutsideOptions } from '@fluentui/react-utilities';\n\n/**\n * Name of the custom event\n */\nexport const MENU_ENTER_EVENT = 'fuimenuenter';\n\n/**\n * This hook works similarly to @see useOnClickOutside\n *\n * Problem: Trying to behave the same as system menus:\n * When the mouse leaves a stack of nested menus the stack should not dismiss.\n * However if the mouse leaves a stack of menus and enters a parent menu all its children menu should dismiss.\n *\n * We don't use the native mouseenter event because it would trigger too many times in the document\n * Instead, dispatch custom DOM event from the menu so that it can bubble\n * Each nested menu can use the listener to check if the event is from a child or parent menu\n */\nexport const useOnMenuMouseEnter = (options: UseOnClickOrScrollOutsideOptions): void => {\n const { refs, callback, element, disabled } = options;\n\n // Keep mouse event here because this is essentially a custom 'mouseenter' event\n const listener = useEventCallback((ev: MouseEvent) => {\n const popoverRef = refs[0];\n const someMenuPopover = ev.target as HTMLElement;\n\n // someMenu is a child -> will always be contained because of vParents\n // someMenu is a parent -> will always not be contained because no vParent\n // someMenu is the current popover -> it will contain itself\n const isOutsidePopover = !elementContains(popoverRef.current ?? null, someMenuPopover);\n\n if (isOutsidePopover && !disabled) {\n callback(ev);\n }\n });\n\n React.useEffect(() => {\n // eslint-disable-next-line eqeqeq\n if (element == null) {\n return;\n }\n\n /**\n * Because `addEventListener` type override falls back to 2nd definition (evt name is unknown string literal)\n * evt is being typed as a base class of MouseEvent -> `Event`.\n * This type is used to override `listener` calls to make TS happy\n */\n\n type ListenerOverride = (evt: Event) => void;\n\n if (!disabled) {\n element.addEventListener(MENU_ENTER_EVENT, listener as ListenerOverride);\n }\n\n return () => {\n element.removeEventListener(MENU_ENTER_EVENT, listener as ListenerOverride);\n };\n }, [listener, element, disabled]);\n};\n\n/**\n * Dispatches the custom MouseEvent enter event. Similar to calling `el.click()`\n * @param el - element for the event target\n * @param nativeEvent - the native mouse event this is mapped to\n */\nexport const dispatchMenuEnterEvent = (el: HTMLElement, nativeEvent: MouseEvent): void => {\n el.dispatchEvent(new CustomEvent(MENU_ENTER_EVENT, { bubbles: true, detail: { nativeEvent } }));\n};\n"],"names":["React","useEventCallback","elementContains","MENU_ENTER_EVENT","useOnMenuMouseEnter","options","refs","callback","element","disabled","listener","ev","popoverRef","someMenuPopover","target","isOutsidePopover","current","useEffect","addEventListener","removeEventListener","dispatchMenuEnterEvent","el","nativeEvent","dispatchEvent","CustomEvent","bubbles","detail"],"mappings":"AAAA;AAEA,YAAYA,WAAW,QAAQ;AAC/B,SAASC,gBAAgB,EAAEC,eAAe,QAAQ,4BAA4B;AAG9E;;CAEC,GACD,OAAO,MAAMC,mBAAmB,eAAe;AAE/C;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,sBAAsB,CAACC;IAClC,MAAM,EAAEC,IAAI,EAAEC,QAAQ,EAAEC,OAAO,EAAEC,QAAQ,EAAE,GAAGJ;IAE9C,gFAAgF;IAChF,MAAMK,WAAWT,iBAAiB,CAACU;QACjC,MAAMC,aAAaN,IAAI,CAAC,EAAE;QAC1B,MAAMO,kBAAkBF,GAAGG,MAAM;YAKSF;QAH1C,sEAAsE;QACtE,0EAA0E;QAC1E,4DAA4D;QAC5D,MAAMG,mBAAmB,CAACb,gBAAgBU,CAAAA,sBAAAA,WAAWI,OAAO,cAAlBJ,iCAAAA,sBAAsB,MAAMC;QAEtE,IAAIE,oBAAoB,CAACN,UAAU;YACjCF,SAASI;QACX;IACF;IAEAX,MAAMiB,SAAS,CAAC;QACd,kCAAkC;QAClC,IAAIT,WAAW,MAAM;YACnB;QACF;QAUA,IAAI,CAACC,UAAU;YACbD,QAAQU,gBAAgB,CAACf,kBAAkBO;QAC7C;QAEA,OAAO;YACLF,QAAQW,mBAAmB,CAAChB,kBAAkBO;QAChD;IACF,GAAG;QAACA;QAAUF;QAASC;KAAS;AAClC,EAAE;AAEF;;;;CAIC,GACD,OAAO,MAAMW,yBAAyB,CAACC,IAAiBC;IACtDD,GAAGE,aAAa,CAAC,IAAIC,YAAYrB,kBAAkB;QAAEsB,SAAS;QAAMC,QAAQ;YAAEJ;QAAY;IAAE;AAC9F,EAAE"}

View File

@@ -0,0 +1,19 @@
'use client';
import * as React from 'react';
/**
* Name of the custom event
*/ export const MENU_SAFEZONE_TIMEOUT_EVENT = 'fuimenusafezonetimeout';
/**
* This hook listeners on a menu trigger for a custom event for an indication that a safe zone was closed over the
* matching item.
*/ export const useOnMenuSafeZoneTimeout = (listener)=>{
const elementRef = React.useRef(null);
return React.useCallback((element)=>{
var _elementRef_current;
(_elementRef_current = elementRef.current) === null || _elementRef_current === void 0 ? void 0 : _elementRef_current.removeEventListener(MENU_SAFEZONE_TIMEOUT_EVENT, listener);
element === null || element === void 0 ? void 0 : element.addEventListener(MENU_SAFEZONE_TIMEOUT_EVENT, listener);
elementRef.current = element;
}, [
listener
]);
};

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/utils/useOnMenuSafeZoneTimeout.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\n\n/**\n * Name of the custom event\n */\nexport const MENU_SAFEZONE_TIMEOUT_EVENT = 'fuimenusafezonetimeout';\n\n/**\n * This hook listeners on a menu trigger for a custom event for an indication that a safe zone was closed over the\n * matching item.\n */\nexport const useOnMenuSafeZoneTimeout = (listener: () => void): ((element: HTMLElement | null) => void) => {\n const elementRef = React.useRef<HTMLElement | null>(null);\n\n return React.useCallback(\n (element: HTMLElement | null) => {\n elementRef.current?.removeEventListener(MENU_SAFEZONE_TIMEOUT_EVENT, listener);\n element?.addEventListener(MENU_SAFEZONE_TIMEOUT_EVENT, listener);\n\n elementRef.current = element;\n },\n [listener],\n );\n};\n"],"names":["React","MENU_SAFEZONE_TIMEOUT_EVENT","useOnMenuSafeZoneTimeout","listener","elementRef","useRef","useCallback","element","current","removeEventListener","addEventListener"],"mappings":"AAAA;AAEA,YAAYA,WAAW,QAAQ;AAE/B;;CAEC,GACD,OAAO,MAAMC,8BAA8B,yBAAyB;AAEpE;;;CAGC,GACD,OAAO,MAAMC,2BAA2B,CAACC;IACvC,MAAMC,aAAaJ,MAAMK,MAAM,CAAqB;IAEpD,OAAOL,MAAMM,WAAW,CACtB,CAACC;YACCH;SAAAA,sBAAAA,WAAWI,OAAO,cAAlBJ,0CAAAA,oBAAoBK,mBAAmB,CAACR,6BAA6BE;QACrEI,oBAAAA,8BAAAA,QAASG,gBAAgB,CAACT,6BAA6BE;QAEvDC,WAAWI,OAAO,GAAGD;IACvB,GACA;QAACJ;KAAS;AAEd,EAAE"}

View File

@@ -0,0 +1,80 @@
'use client';
import * as React from 'react';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
import { useMenuContext_unstable } from '../contexts/menuContext';
export const useValidateNesting = (componentName)=>{
'use no memo';
const { targetDocument } = useFluent();
const triggerRef = useMenuContext_unstable((context)=>context.triggerRef);
const inline = useMenuContext_unstable((context)=>context.inline);
const ref = React.useRef(null);
if (process.env.NODE_ENV !== 'production') {
// This check should run only in development mode
// It's okay to disable the ESLint rule because we ar checking env variable statically (not at runtime)
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(()=>{
let ancestor = ref.current;
let ancestorComponentName = '';
do {
var _ancestor_parentElement;
ancestor = (_ancestor_parentElement = ancestor === null || ancestor === void 0 ? void 0 : ancestor.parentElement) !== null && _ancestor_parentElement !== void 0 ? _ancestor_parentElement : null;
if (ancestor === null || ancestor === void 0 ? void 0 : ancestor.classList.contains('fui-MenuList')) {
break;
} else if (ancestor === null || ancestor === void 0 ? void 0 : ancestor.classList.contains('fui-MenuGrid')) {
ancestorComponentName = 'MenuGrid';
} else if (ancestor === null || ancestor === void 0 ? void 0 : ancestor.classList.contains('fui-MenuGridItem')) {
ancestorComponentName = 'MenuGridItem';
} else if (ancestor === null || ancestor === void 0 ? void 0 : ancestor.classList.contains('fui-MenuGridRow')) {
ancestorComponentName = 'MenuGridRow';
} else if (ancestor === null || ancestor === void 0 ? void 0 : ancestor.classList.contains('fui-MenuGridCell')) {
ancestorComponentName = 'MenuGridCell';
}
if ([
'MenuItem',
'MenuItemCheckbox',
'MenuItemRadio'
].includes(componentName)) {
if ([
'MenuGrid',
'MenuGridItem',
'MenuGridRow',
'MenuGridCell'
].includes(ancestorComponentName)) {
throw new Error(`${componentName} is incorrectly nested within ${ancestorComponentName}. You probably want to wrap it in a MenuList instead.`);
}
} else if (componentName === 'MenuList') {
if (ancestorComponentName === 'MenuGridCell') {
if (inline && getCellOfTrigger(triggerRef.current, targetDocument) === ancestor) {
break;
}
throw new Error(`MenuList is incorrectly nested within MenuGridCell.`);
} else if ([
'MenuGrid',
'MenuGridItem',
'MenuGridRow'
].includes(ancestorComponentName)) {
throw new Error(`MenuList is incorrectly nested within ${ancestorComponentName}.`);
}
}
}while (ancestor && ancestor !== (targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.body))
}, [
componentName,
ref,
triggerRef,
inline,
targetDocument
]);
}
return ref;
};
const getCellOfTrigger = (trigger, targetDocument)=>{
let ancestor = trigger === null || trigger === void 0 ? void 0 : trigger.parentElement;
while(ancestor && ancestor !== (targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.body)){
if (ancestor === null || ancestor === void 0 ? void 0 : ancestor.classList.contains('fui-MenuGridCell')) {
return ancestor;
}
var _ancestor_parentElement;
ancestor = (_ancestor_parentElement = ancestor === null || ancestor === void 0 ? void 0 : ancestor.parentElement) !== null && _ancestor_parentElement !== void 0 ? _ancestor_parentElement : null;
}
return null;
};

File diff suppressed because one or more lines are too long