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,20 @@
'use client';
import { useControllableState } from '@fluentui/react-utilities';
import * as React from 'react';
import { ImmutableSet } from '../utils/ImmutableSet';
/**
* @internal
*/ export function useControllableOpenItems(props) {
return useControllableState({
state: React.useMemo(()=>props.openItems && ImmutableSet.from(props.openItems), [
props.openItems
]),
defaultState: props.defaultOpenItems && (()=>ImmutableSet.from(props.defaultOpenItems)),
initialState: ImmutableSet.empty
});
}
/**
* @internal
*/ export function createNextOpenItems(data, previousOpenItems) {
return data.open ? previousOpenItems.add(data.value) : previousOpenItems.delete(data.value);
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useControllableOpenItems.ts"],"sourcesContent":["'use client';\n\nimport { useControllableState } from '@fluentui/react-utilities';\nimport * as React from 'react';\nimport { ImmutableSet } from '../utils/ImmutableSet';\nimport type { TreeItemValue } from '../components/TreeItem/TreeItem.types';\nimport { TreeOpenChangeData, TreeProps } from '../Tree';\n\n/**\n * @internal\n */\nexport function useControllableOpenItems(\n props: Pick<TreeProps, 'openItems' | 'defaultOpenItems'>,\n): [ImmutableSet<TreeItemValue>, React.Dispatch<React.SetStateAction<ImmutableSet<TreeItemValue>>>] {\n return useControllableState({\n state: React.useMemo(() => props.openItems && ImmutableSet.from(props.openItems), [props.openItems]),\n defaultState: props.defaultOpenItems && (() => ImmutableSet.from(props.defaultOpenItems)),\n initialState: ImmutableSet.empty,\n });\n}\n\n/**\n * @internal\n */\nexport function createNextOpenItems(\n data: Pick<TreeOpenChangeData, 'value' | 'open'>,\n previousOpenItems: ImmutableSet<TreeItemValue>,\n): ImmutableSet<TreeItemValue> {\n return data.open ? previousOpenItems.add(data.value) : previousOpenItems.delete(data.value);\n}\n"],"names":["useControllableState","React","ImmutableSet","useControllableOpenItems","props","state","useMemo","openItems","from","defaultState","defaultOpenItems","initialState","empty","createNextOpenItems","data","previousOpenItems","open","add","value","delete"],"mappings":"AAAA;AAEA,SAASA,oBAAoB,QAAQ,4BAA4B;AACjE,YAAYC,WAAW,QAAQ;AAC/B,SAASC,YAAY,QAAQ,wBAAwB;AAIrD;;CAEC,GACD,OAAO,SAASC,yBACdC,KAAwD;IAExD,OAAOJ,qBAAqB;QAC1BK,OAAOJ,MAAMK,OAAO,CAAC,IAAMF,MAAMG,SAAS,IAAIL,aAAaM,IAAI,CAACJ,MAAMG,SAAS,GAAG;YAACH,MAAMG,SAAS;SAAC;QACnGE,cAAcL,MAAMM,gBAAgB,IAAK,CAAA,IAAMR,aAAaM,IAAI,CAACJ,MAAMM,gBAAgB,CAAA;QACvFC,cAAcT,aAAaU,KAAK;IAClC;AACF;AAEA;;CAEC,GACD,OAAO,SAASC,oBACdC,IAAgD,EAChDC,iBAA8C;IAE9C,OAAOD,KAAKE,IAAI,GAAGD,kBAAkBE,GAAG,CAACH,KAAKI,KAAK,IAAIH,kBAAkBI,MAAM,CAACL,KAAKI,KAAK;AAC5F"}

View File

@@ -0,0 +1,116 @@
'use client';
import { useEventCallback, useMergedRefs } from '@fluentui/react-utilities';
import { nextTypeAheadElement } from '../utils/nextTypeAheadElement';
import { treeDataTypes } from '../utils/tokens';
import { useRovingTabIndex } from './useRovingTabIndexes';
import { dataTreeItemValueAttrName } from '../utils/getTreeItemValueFromElement';
import * as React from 'react';
import { useHTMLElementWalkerRef } from './useHTMLElementWalkerRef';
import { useFocusFinders } from '@fluentui/react-tabster';
import { treeItemLayoutClassNames } from '../TreeItemLayout';
export function useFlatTreeNavigation(navigationMode = 'tree') {
'use no memo';
const { walkerRef, rootRef: walkerRootRef } = useHTMLElementWalkerRef();
const { rove, forceUpdate: forceUpdateRovingTabIndex, initialize: initializeRovingTabIndex } = useRovingTabIndex();
const { findFirstFocusable } = useFocusFinders();
const rootRefCallback = React.useCallback((root)=>{
if (walkerRef.current && root) {
initializeRovingTabIndex(walkerRef.current);
}
}, [
initializeRovingTabIndex,
walkerRef
]);
function getNextElement(data) {
if (!walkerRef.current) {
return null;
}
switch(data.type){
case treeDataTypes.Click:
return data.target;
case treeDataTypes.TypeAhead:
walkerRef.current.currentElement = data.target;
return nextTypeAheadElement(walkerRef.current, data.event.key);
case treeDataTypes.ArrowLeft:
{
const actions = queryActions(data.target);
if (navigationMode === 'treegrid' && (actions === null || actions === void 0 ? void 0 : actions.contains(data.target.ownerDocument.activeElement))) {
return data.target;
}
const nextElement = parentElement(data.parentValue, walkerRef.current);
if (!nextElement && process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.warn(`@fluentui/react-tree [useFlatTreeNavigation]:
\'ArrowLeft\' navigation was not possible.
No parent element found for the current element:`, data.target);
}
return nextElement;
}
case treeDataTypes.ArrowRight:
{
if (navigationMode === 'treegrid') {
const actions = queryActions(data.target);
if (actions) {
var _findFirstFocusable;
(_findFirstFocusable = findFirstFocusable(actions)) === null || _findFirstFocusable === void 0 ? void 0 : _findFirstFocusable.focus();
}
return null;
}
walkerRef.current.currentElement = data.target;
const nextElement = firstChild(data.target, walkerRef.current);
if (!nextElement && process.env.NODE_ENV !== 'production') {
const ariaLevel = Number(data.target.getAttribute('aria-level'));
// eslint-disable-next-line no-console
console.warn(`@fluentui/react-tree [useFlatTreeNavigation]:
\'ArrowRight\' navigation was not possible.
No element with "aria-posinset=1" and "aria-level=${ariaLevel + 1}"
was found after the current element!`, data.target);
}
return nextElement;
}
case treeDataTypes.End:
walkerRef.current.currentElement = walkerRef.current.root;
return walkerRef.current.lastChild();
case treeDataTypes.Home:
walkerRef.current.currentElement = walkerRef.current.root;
return walkerRef.current.firstChild();
case treeDataTypes.ArrowDown:
walkerRef.current.currentElement = data.target;
return walkerRef.current.nextElement();
case treeDataTypes.ArrowUp:
walkerRef.current.currentElement = data.target;
return walkerRef.current.previousElement();
}
}
const navigate = useEventCallback((data)=>{
const nextElement = getNextElement(data);
if (nextElement) {
rove(nextElement);
}
});
return {
navigate,
rootRef: useMergedRefs(walkerRootRef, rootRefCallback),
forceUpdateRovingTabIndex
};
}
function firstChild(target, treeWalker) {
const nextElement = treeWalker.nextElement();
if (!nextElement) {
return null;
}
const nextElementAriaPosInSet = nextElement.getAttribute('aria-posinset');
const nextElementAriaLevel = nextElement.getAttribute('aria-level');
const targetAriaLevel = target.getAttribute('aria-level');
if (nextElementAriaPosInSet === '1' && Number(nextElementAriaLevel) === Number(targetAriaLevel) + 1) {
return nextElement;
}
return null;
}
function parentElement(parentValue, treeWalker) {
if (parentValue === undefined) {
return null;
}
return treeWalker.root.querySelector(`[${dataTreeItemValueAttrName}="${parentValue}"]`);
}
const queryActions = (target)=>target.querySelector(`:scope > .${treeItemLayoutClassNames.root} > .${treeItemLayoutClassNames.actions}`);

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,18 @@
'use client';
import * as React from 'react';
import { useFluent_unstable } from '@fluentui/react-shared-contexts';
import { createHTMLElementWalker } from '../utils/createHTMLElementWalker';
import { treeItemFilter } from '../utils/treeItemFilter';
export function useHTMLElementWalkerRef() {
const { targetDocument } = useFluent_unstable();
const walkerRef = React.useRef(undefined);
const rootRef = React.useCallback((root)=>{
walkerRef.current = targetDocument && root ? createHTMLElementWalker(root, targetDocument, treeItemFilter) : undefined;
}, [
targetDocument
]);
return {
walkerRef,
rootRef
};
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useHTMLElementWalkerRef.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { useFluent_unstable } from '@fluentui/react-shared-contexts';\nimport { HTMLElementWalker, createHTMLElementWalker } from '../utils/createHTMLElementWalker';\nimport { treeItemFilter } from '../utils/treeItemFilter';\n\nexport function useHTMLElementWalkerRef(): {\n // eslint-disable-next-line @typescript-eslint/no-deprecated\n walkerRef: React.MutableRefObject<HTMLElementWalker | undefined>;\n rootRef: React.Ref<HTMLElement>;\n} {\n const { targetDocument } = useFluent_unstable();\n\n const walkerRef = React.useRef<HTMLElementWalker>(undefined);\n\n const rootRef: React.Ref<HTMLElement> = React.useCallback(\n (root: HTMLElement) => {\n walkerRef.current =\n targetDocument && root ? createHTMLElementWalker(root, targetDocument, treeItemFilter) : undefined;\n },\n [targetDocument],\n );\n return { walkerRef, rootRef } as const;\n}\n"],"names":["React","useFluent_unstable","createHTMLElementWalker","treeItemFilter","useHTMLElementWalkerRef","targetDocument","walkerRef","useRef","undefined","rootRef","useCallback","root","current"],"mappings":"AAAA;AAEA,YAAYA,WAAW,QAAQ;AAC/B,SAASC,kBAAkB,QAAQ,kCAAkC;AACrE,SAA4BC,uBAAuB,QAAQ,mCAAmC;AAC9F,SAASC,cAAc,QAAQ,0BAA0B;AAEzD,OAAO,SAASC;IAKd,MAAM,EAAEC,cAAc,EAAE,GAAGJ;IAE3B,MAAMK,YAAYN,MAAMO,MAAM,CAAoBC;IAElD,MAAMC,UAAkCT,MAAMU,WAAW,CACvD,CAACC;QACCL,UAAUM,OAAO,GACfP,kBAAkBM,OAAOT,wBAAwBS,MAAMN,gBAAgBF,kBAAkBK;IAC7F,GACA;QAACH;KAAe;IAElB,OAAO;QAAEC;QAAWG;IAAQ;AAC9B"}

View File

@@ -0,0 +1,113 @@
'use client';
import { getIntrinsicElementProps, useEventCallback, slot } from '@fluentui/react-utilities';
import * as React from 'react';
import { Collapse } from '@fluentui/react-motion-components-preview';
import { createCheckedItems } from '../utils/createCheckedItems';
import { treeDataTypes } from '../utils/tokens';
import { createNextOpenItems } from './useControllableOpenItems';
import { ImmutableSet } from '../utils/ImmutableSet';
import { ImmutableMap } from '../utils/ImmutableMap';
/**
* Create the state required to render the root level tree.
*
* @param props - props from this instance of tree
* @param ref - reference to root HTMLElement of tree
*/ export function useRootTree(props, ref) {
warnIfNoProperPropsRootTree(props);
const { appearance = 'subtle', size = 'medium', selectionMode = 'none' } = props;
const openItems = React.useMemo(()=>ImmutableSet.from(props.openItems), [
props.openItems
]);
const checkedItems = React.useMemo(()=>createCheckedItems(props.checkedItems), [
props.checkedItems
]);
const requestOpenChange = (request)=>{
var _props_onOpenChange;
(_props_onOpenChange = props.onOpenChange) === null || _props_onOpenChange === void 0 ? void 0 : _props_onOpenChange.call(props, request.event, {
...request,
openItems: ImmutableSet.dangerouslyGetInternalSet(createNextOpenItems(request, openItems))
});
};
const requestCheckedChange = (request)=>{
var _props_onCheckedChange;
if (selectionMode === 'none') {
return;
}
(_props_onCheckedChange = props.onCheckedChange) === null || _props_onCheckedChange === void 0 ? void 0 : _props_onCheckedChange.call(props, request.event, {
...request,
selectionMode,
checkedItems: ImmutableMap.dangerouslyGetInternalMap(checkedItems)
});
};
const requestNavigation = (request)=>{
var _props_onNavigation;
let isScrollPrevented = false;
(_props_onNavigation = props.onNavigation) === null || _props_onNavigation === void 0 ? void 0 : _props_onNavigation.call(props, request.event, {
...request,
preventScroll: ()=>{
isScrollPrevented = true;
},
isScrollPrevented: ()=>isScrollPrevented
});
switch(request.type){
case treeDataTypes.ArrowDown:
case treeDataTypes.ArrowUp:
case treeDataTypes.Home:
case treeDataTypes.End:
// stop the default behavior of the event
// which is to scroll the page
request.event.preventDefault();
}
};
const requestTreeResponse = useEventCallback((request)=>{
switch(request.requestType){
case 'navigate':
return requestNavigation(request);
case 'open':
return requestOpenChange(request);
case 'selection':
return requestCheckedChange(request);
}
});
var _props_navigationMode;
return {
components: {
root: 'div',
collapseMotion: Collapse
},
contextType: 'root',
selectionMode,
navigationMode: (_props_navigationMode = props.navigationMode) !== null && _props_navigationMode !== void 0 ? _props_navigationMode : 'tree',
open: true,
appearance,
size,
level: 1,
openItems,
checkedItems,
requestTreeResponse,
forceUpdateRovingTabIndex: ()=>{
// noop
},
root: slot.always(getIntrinsicElementProps('div', {
// FIXME:
// `ref` is wrongly assigned to be `HTMLElement` instead of `HTMLDivElement`
// but since it would be a breaking change to fix it, we are casting ref to it's proper type
ref: ref,
role: 'tree',
'aria-multiselectable': selectionMode === 'multiselect' ? true : undefined,
...props
}), {
elementType: 'div'
}),
collapseMotion: undefined
};
}
function warnIfNoProperPropsRootTree(props) {
if (process.env.NODE_ENV === 'development') {
if (!props['aria-label'] && !props['aria-labelledby']) {
// eslint-disable-next-line no-console
console.warn(`@fluentui/react-tree [useRootTree]:
Tree must have either a \`aria-label\` or \`aria-labelledby\` property defined`);
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,67 @@
'use client';
import * as React from 'react';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
import { useFocusedElementChange } from '@fluentui/react-tabster';
const findTreeItemRoot = (element)=>{
let parent = element.parentElement;
while(parent && parent.getAttribute('role') !== 'tree'){
parent = parent.parentElement;
}
return parent;
};
/**
* https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex
*
* @internal
*/ export function useRovingTabIndex() {
const currentElementRef = React.useRef(null);
const walkerRef = React.useRef(null);
const { targetDocument } = useFluent();
useFocusedElementChange((element)=>{
if ((element === null || element === void 0 ? void 0 : element.getAttribute('role')) === 'treeitem' && walkerRef.current && walkerRef.current.root.contains(element)) {
const treeitemRoot = findTreeItemRoot(element);
if (walkerRef.current.root !== treeitemRoot) {
return;
}
rove(element);
}
});
const initialize = React.useCallback((walker)=>{
walkerRef.current = walker;
walker.currentElement = walker.root;
let tabbableChild = walker.firstChild((element)=>element.tabIndex === 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP);
walker.currentElement = walker.root;
tabbableChild !== null && tabbableChild !== void 0 ? tabbableChild : tabbableChild = walker.firstChild();
if (!tabbableChild) {
return;
}
tabbableChild.tabIndex = 0;
currentElementRef.current = tabbableChild;
let nextElement = null;
while((nextElement = walker.nextElement()) && nextElement !== tabbableChild){
nextElement.tabIndex = -1;
}
}, []);
const rove = React.useCallback((nextElement, focusOptions)=>{
if (!currentElementRef.current) {
return;
}
currentElementRef.current.tabIndex = -1;
nextElement.tabIndex = 0;
nextElement.focus(focusOptions);
currentElementRef.current = nextElement;
}, []);
const forceUpdate = React.useCallback(()=>{
if ((currentElementRef.current === null || !(targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.body.contains(currentElementRef.current))) && walkerRef.current) {
initialize(walkerRef.current);
}
}, [
targetDocument,
initialize
]);
return {
rove,
initialize,
forceUpdate
};
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useRovingTabIndexes.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';\nimport { HTMLElementWalker } from '../utils/createHTMLElementWalker';\nimport { useFocusedElementChange } from '@fluentui/react-tabster';\n\nconst findTreeItemRoot = (element: HTMLElement) => {\n let parent = element.parentElement;\n while (parent && parent.getAttribute('role') !== 'tree') {\n parent = parent.parentElement;\n }\n return parent;\n};\n\n/**\n * https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex\n *\n * @internal\n */\nexport function useRovingTabIndex(): {\n rove: (nextElement: HTMLElement, focusOptions?: FocusOptions) => void;\n initialize: (walker: HTMLElementWalker) => void;\n forceUpdate: () => void;\n} {\n const currentElementRef = React.useRef<HTMLElement | null>(null);\n const walkerRef = React.useRef<HTMLElementWalker | null>(null);\n const { targetDocument } = useFluent();\n\n useFocusedElementChange(element => {\n if (element?.getAttribute('role') === 'treeitem' && walkerRef.current && walkerRef.current.root.contains(element)) {\n const treeitemRoot = findTreeItemRoot(element);\n if (walkerRef.current.root !== treeitemRoot) {\n return;\n }\n rove(element);\n }\n });\n\n const initialize = React.useCallback((walker: HTMLElementWalker) => {\n walkerRef.current = walker;\n walker.currentElement = walker.root;\n let tabbableChild = walker.firstChild(element =>\n element.tabIndex === 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP,\n );\n walker.currentElement = walker.root;\n tabbableChild ??= walker.firstChild();\n if (!tabbableChild) {\n return;\n }\n tabbableChild.tabIndex = 0;\n currentElementRef.current = tabbableChild;\n let nextElement: HTMLElement | null = null;\n while ((nextElement = walker.nextElement()) && nextElement !== tabbableChild) {\n nextElement.tabIndex = -1;\n }\n }, []);\n const rove = React.useCallback((nextElement: HTMLElement, focusOptions?: FocusOptions) => {\n if (!currentElementRef.current) {\n return;\n }\n currentElementRef.current.tabIndex = -1;\n nextElement.tabIndex = 0;\n nextElement.focus(focusOptions);\n currentElementRef.current = nextElement;\n }, []);\n\n const forceUpdate = React.useCallback(() => {\n if (\n (currentElementRef.current === null || !targetDocument?.body.contains(currentElementRef.current)) &&\n walkerRef.current\n ) {\n initialize(walkerRef.current);\n }\n }, [targetDocument, initialize]);\n\n return {\n rove,\n initialize,\n forceUpdate,\n };\n}\n"],"names":["React","useFluent_unstable","useFluent","useFocusedElementChange","findTreeItemRoot","element","parent","parentElement","getAttribute","useRovingTabIndex","currentElementRef","useRef","walkerRef","targetDocument","current","root","contains","treeitemRoot","rove","initialize","useCallback","walker","currentElement","tabbableChild","firstChild","tabIndex","NodeFilter","FILTER_ACCEPT","FILTER_SKIP","nextElement","focusOptions","focus","forceUpdate","body"],"mappings":"AAAA;AAEA,YAAYA,WAAW,QAAQ;AAC/B,SAASC,sBAAsBC,SAAS,QAAQ,kCAAkC;AAElF,SAASC,uBAAuB,QAAQ,0BAA0B;AAElE,MAAMC,mBAAmB,CAACC;IACxB,IAAIC,SAASD,QAAQE,aAAa;IAClC,MAAOD,UAAUA,OAAOE,YAAY,CAAC,YAAY,OAAQ;QACvDF,SAASA,OAAOC,aAAa;IAC/B;IACA,OAAOD;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASG;IAKd,MAAMC,oBAAoBV,MAAMW,MAAM,CAAqB;IAC3D,MAAMC,YAAYZ,MAAMW,MAAM,CAA2B;IACzD,MAAM,EAAEE,cAAc,EAAE,GAAGX;IAE3BC,wBAAwBE,CAAAA;QACtB,IAAIA,CAAAA,oBAAAA,8BAAAA,QAASG,YAAY,CAAC,aAAY,cAAcI,UAAUE,OAAO,IAAIF,UAAUE,OAAO,CAACC,IAAI,CAACC,QAAQ,CAACX,UAAU;YACjH,MAAMY,eAAeb,iBAAiBC;YACtC,IAAIO,UAAUE,OAAO,CAACC,IAAI,KAAKE,cAAc;gBAC3C;YACF;YACAC,KAAKb;QACP;IACF;IAEA,MAAMc,aAAanB,MAAMoB,WAAW,CAAC,CAACC;QACpCT,UAAUE,OAAO,GAAGO;QACpBA,OAAOC,cAAc,GAAGD,OAAON,IAAI;QACnC,IAAIQ,gBAAgBF,OAAOG,UAAU,CAACnB,CAAAA,UACpCA,QAAQoB,QAAQ,KAAK,IAAIC,WAAWC,aAAa,GAAGD,WAAWE,WAAW;QAE5EP,OAAOC,cAAc,GAAGD,OAAON,IAAI;QACnCQ,0BAAAA,2BAAAA,gBAAAA,gBAAkBF,OAAOG,UAAU;QACnC,IAAI,CAACD,eAAe;YAClB;QACF;QACAA,cAAcE,QAAQ,GAAG;QACzBf,kBAAkBI,OAAO,GAAGS;QAC5B,IAAIM,cAAkC;QACtC,MAAO,AAACA,CAAAA,cAAcR,OAAOQ,WAAW,EAAC,KAAMA,gBAAgBN,cAAe;YAC5EM,YAAYJ,QAAQ,GAAG,CAAC;QAC1B;IACF,GAAG,EAAE;IACL,MAAMP,OAAOlB,MAAMoB,WAAW,CAAC,CAACS,aAA0BC;QACxD,IAAI,CAACpB,kBAAkBI,OAAO,EAAE;YAC9B;QACF;QACAJ,kBAAkBI,OAAO,CAACW,QAAQ,GAAG,CAAC;QACtCI,YAAYJ,QAAQ,GAAG;QACvBI,YAAYE,KAAK,CAACD;QAClBpB,kBAAkBI,OAAO,GAAGe;IAC9B,GAAG,EAAE;IAEL,MAAMG,cAAchC,MAAMoB,WAAW,CAAC;QACpC,IACE,AAACV,CAAAA,kBAAkBI,OAAO,KAAK,QAAQ,EAACD,2BAAAA,qCAAAA,eAAgBoB,IAAI,CAACjB,QAAQ,CAACN,kBAAkBI,OAAO,EAAA,KAC/FF,UAAUE,OAAO,EACjB;YACAK,WAAWP,UAAUE,OAAO;QAC9B;IACF,GAAG;QAACD;QAAgBM;KAAW;IAE/B,OAAO;QACLD;QACAC;QACAa;IACF;AACF"}

View File

@@ -0,0 +1,42 @@
'use client';
import * as React from 'react';
import { useSubtreeContext_unstable, useTreeItemContext_unstable } from '../contexts/index';
import { getIntrinsicElementProps, useMergedRefs, slot } from '@fluentui/react-utilities';
import { Collapse } from '@fluentui/react-motion-components-preview';
import { presenceMotionSlot } from '@fluentui/react-motion';
/**
* Create the state required to render a sub-level tree.
*
* @param props - props from this instance of tree
* @param ref - reference to root HTMLElement of tree
*/ export function useSubtree(props, ref) {
const subtreeRef = useTreeItemContext_unstable((ctx)=>ctx.subtreeRef);
const { level: parentLevel } = useSubtreeContext_unstable();
const open = useTreeItemContext_unstable((ctx)=>ctx.open);
return {
contextType: 'subtree',
open,
components: {
root: 'div',
collapseMotion: Collapse
},
level: parentLevel + 1,
root: slot.always(getIntrinsicElementProps('div', {
// FIXME:
// `ref` is wrongly assigned to be `HTMLElement` instead of `HTMLDivElement`
// but since it would be a breaking change to fix it, we are casting ref to it's proper type
ref: useMergedRefs(ref, subtreeRef),
role: 'group',
...props
}), {
elementType: 'div'
}),
collapseMotion: presenceMotionSlot(props.collapseMotion, {
elementType: Collapse,
defaultProps: {
visible: open,
unmountOnExit: true
}
})
};
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useSubtree.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { TreeProps, TreeState } from '../Tree';\nimport { SubtreeContextValue, useSubtreeContext_unstable, useTreeItemContext_unstable } from '../contexts/index';\nimport { getIntrinsicElementProps, useMergedRefs, slot } from '@fluentui/react-utilities';\nimport { Collapse } from '@fluentui/react-motion-components-preview';\nimport { presenceMotionSlot } from '@fluentui/react-motion';\n\n/**\n * Create the state required to render a sub-level tree.\n *\n * @param props - props from this instance of tree\n * @param ref - reference to root HTMLElement of tree\n */\nexport function useSubtree(\n props: TreeProps,\n ref: React.Ref<HTMLElement>,\n): Omit<TreeState & SubtreeContextValue, 'treeType'> {\n const subtreeRef = useTreeItemContext_unstable(ctx => ctx.subtreeRef);\n\n const { level: parentLevel } = useSubtreeContext_unstable();\n\n const open = useTreeItemContext_unstable(ctx => ctx.open);\n\n return {\n contextType: 'subtree',\n open,\n components: {\n root: 'div',\n collapseMotion: Collapse,\n },\n level: parentLevel + 1,\n root: slot.always(\n getIntrinsicElementProps('div', {\n // FIXME:\n // `ref` is wrongly assigned to be `HTMLElement` instead of `HTMLDivElement`\n // but since it would be a breaking change to fix it, we are casting ref to it's proper type\n ref: useMergedRefs(ref, subtreeRef) as React.Ref<HTMLDivElement>,\n role: 'group',\n ...props,\n }),\n { elementType: 'div' },\n ),\n collapseMotion: presenceMotionSlot(props.collapseMotion, {\n elementType: Collapse,\n defaultProps: {\n visible: open,\n unmountOnExit: true,\n },\n }),\n };\n}\n"],"names":["React","useSubtreeContext_unstable","useTreeItemContext_unstable","getIntrinsicElementProps","useMergedRefs","slot","Collapse","presenceMotionSlot","useSubtree","props","ref","subtreeRef","ctx","level","parentLevel","open","contextType","components","root","collapseMotion","always","role","elementType","defaultProps","visible","unmountOnExit"],"mappings":"AAAA;AAEA,YAAYA,WAAW,QAAQ;AAE/B,SAA8BC,0BAA0B,EAAEC,2BAA2B,QAAQ,oBAAoB;AACjH,SAASC,wBAAwB,EAAEC,aAAa,EAAEC,IAAI,QAAQ,4BAA4B;AAC1F,SAASC,QAAQ,QAAQ,4CAA4C;AACrE,SAASC,kBAAkB,QAAQ,yBAAyB;AAE5D;;;;;CAKC,GACD,OAAO,SAASC,WACdC,KAAgB,EAChBC,GAA2B;IAE3B,MAAMC,aAAaT,4BAA4BU,CAAAA,MAAOA,IAAID,UAAU;IAEpE,MAAM,EAAEE,OAAOC,WAAW,EAAE,GAAGb;IAE/B,MAAMc,OAAOb,4BAA4BU,CAAAA,MAAOA,IAAIG,IAAI;IAExD,OAAO;QACLC,aAAa;QACbD;QACAE,YAAY;YACVC,MAAM;YACNC,gBAAgBb;QAClB;QACAO,OAAOC,cAAc;QACrBI,MAAMb,KAAKe,MAAM,CACfjB,yBAAyB,OAAO;YAC9B,SAAS;YACT,4EAA4E;YAC5E,4FAA4F;YAC5FO,KAAKN,cAAcM,KAAKC;YACxBU,MAAM;YACN,GAAGZ,KAAK;QACV,IACA;YAAEa,aAAa;QAAM;QAEvBH,gBAAgBZ,mBAAmBE,MAAMU,cAAc,EAAE;YACvDG,aAAahB;YACbiB,cAAc;gBACZC,SAAST;gBACTU,eAAe;YACjB;QACF;IACF;AACF"}

View File

@@ -0,0 +1,92 @@
'use client';
import { nextTypeAheadElement } from '../utils/nextTypeAheadElement';
import { treeDataTypes } from '../utils/tokens';
import { useRovingTabIndex } from './useRovingTabIndexes';
import * as React from 'react';
import { useHTMLElementWalkerRef } from './useHTMLElementWalkerRef';
import { useMergedRefs } from '@fluentui/react-utilities';
import { treeItemLayoutClassNames } from '../TreeItemLayout';
import { useFocusFinders } from '@fluentui/react-tabster';
/***
* Hook used to manage navigation in the tree.
*
* @param navigationMode - the navigation mode of the tree, 'tree' (default) or 'treegrid'
*/ export function useTreeNavigation(navigationMode = 'tree') {
'use no memo';
const { rove, initialize: initializeRovingTabIndex, forceUpdate: forceUpdateRovingTabIndex } = useRovingTabIndex();
const { findFirstFocusable } = useFocusFinders();
const { walkerRef, rootRef: walkerRootRef } = useHTMLElementWalkerRef();
const rootRefCallback = React.useCallback((root)=>{
if (root && walkerRef.current) {
initializeRovingTabIndex(walkerRef.current);
}
}, [
walkerRef,
initializeRovingTabIndex
]);
const getNextElement = (data)=>{
if (!walkerRef.current) {
return null;
}
switch(data.type){
case treeDataTypes.Click:
return data.target;
case treeDataTypes.TypeAhead:
walkerRef.current.currentElement = data.target;
return nextTypeAheadElement(walkerRef.current, data.event.key);
case treeDataTypes.ArrowLeft:
{
const actions = queryActions(data.target);
if (navigationMode === 'treegrid' && (actions === null || actions === void 0 ? void 0 : actions.contains(data.target.ownerDocument.activeElement))) {
return data.target;
}
walkerRef.current.currentElement = data.target;
return walkerRef.current.parentElement();
}
case treeDataTypes.ArrowRight:
if (navigationMode === 'treegrid') {
const actions = queryActions(data.target);
if (actions) {
var _findFirstFocusable;
(_findFirstFocusable = findFirstFocusable(actions)) === null || _findFirstFocusable === void 0 ? void 0 : _findFirstFocusable.focus();
}
return null;
}
walkerRef.current.currentElement = data.target;
return walkerRef.current.firstChild();
case treeDataTypes.End:
walkerRef.current.currentElement = walkerRef.current.root;
return lastChildRecursive(walkerRef.current);
case treeDataTypes.Home:
walkerRef.current.currentElement = walkerRef.current.root;
return walkerRef.current.firstChild();
case treeDataTypes.ArrowDown:
walkerRef.current.currentElement = data.target;
return walkerRef.current.nextElement();
case treeDataTypes.ArrowUp:
walkerRef.current.currentElement = data.target;
return walkerRef.current.previousElement();
}
};
function navigate(data, focusOptions) {
const nextElement = getNextElement(data);
if (nextElement) {
rove(nextElement, focusOptions);
}
return nextElement;
}
return {
navigate,
treeRef: useMergedRefs(walkerRootRef, rootRefCallback),
forceUpdateRovingTabIndex
};
}
function lastChildRecursive(walker) {
let lastElement = null;
let nextElement = null;
while(nextElement = walker.lastChild()){
lastElement = nextElement;
}
return lastElement;
}
const queryActions = (target)=>target.querySelector(`:scope > .${treeItemLayoutClassNames.root} > .${treeItemLayoutClassNames.actions}`);

File diff suppressed because one or more lines are too long