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,29 @@
import * as React from 'react';
const defaultCompare = ()=>0;
const defaultRenderCell = ()=>{
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.warn('@fluentui/react-table: You are using the default column renderCell function that renders null');
}
return null;
};
const defaultRenderHeaderCell = ()=>{
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.warn('@fluentui/react-table: You are using the default column renderHeaderCell function that renders null');
}
return null;
};
/**
* Helper function to create column definition with defaults
* @param options - column definition options
* @returns - column definition with defaults
*/ export function createTableColumn(options) {
const { columnId, renderCell = defaultRenderCell, renderHeaderCell = defaultRenderHeaderCell, compare = defaultCompare } = options;
return {
columnId,
renderCell,
renderHeaderCell,
compare
};
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/createColumn.ts"],"sourcesContent":["import * as React from 'react';\nimport { CreateTableColumnOptions, TableColumnId } from './types';\n\nconst defaultCompare = () => 0;\n\nconst defaultRenderCell = () => {\n if (process.env.NODE_ENV !== 'production') {\n // eslint-disable-next-line no-console\n console.warn('@fluentui/react-table: You are using the default column renderCell function that renders null');\n }\n\n return null;\n};\n\nconst defaultRenderHeaderCell = () => {\n if (process.env.NODE_ENV !== 'production') {\n // eslint-disable-next-line no-console\n console.warn('@fluentui/react-table: You are using the default column renderHeaderCell function that renders null');\n }\n\n return null;\n};\n\n/**\n * Helper function to create column definition with defaults\n * @param options - column definition options\n * @returns - column definition with defaults\n */\nexport function createTableColumn<TItem>(options: CreateTableColumnOptions<TItem>): {\n columnId: TableColumnId;\n renderCell: (item: TItem) => React.ReactNode;\n renderHeaderCell: (data?: unknown) => React.ReactNode;\n compare: (a: TItem, b: TItem) => number;\n} {\n const {\n columnId,\n renderCell = defaultRenderCell,\n renderHeaderCell = defaultRenderHeaderCell,\n compare = defaultCompare,\n } = options;\n\n return {\n columnId,\n renderCell,\n renderHeaderCell,\n compare,\n };\n}\n"],"names":["React","defaultCompare","defaultRenderCell","process","env","NODE_ENV","console","warn","defaultRenderHeaderCell","createTableColumn","options","columnId","renderCell","renderHeaderCell","compare"],"mappings":"AAAA,YAAYA,WAAW,QAAQ;AAG/B,MAAMC,iBAAiB,IAAM;AAE7B,MAAMC,oBAAoB;IACxB,IAAIC,QAAQC,GAAG,CAACC,QAAQ,KAAK,cAAc;QACzC,sCAAsC;QACtCC,QAAQC,IAAI,CAAC;IACf;IAEA,OAAO;AACT;AAEA,MAAMC,0BAA0B;IAC9B,IAAIL,QAAQC,GAAG,CAACC,QAAQ,KAAK,cAAc;QACzC,sCAAsC;QACtCC,QAAQC,IAAI,CAAC;IACf;IAEA,OAAO;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASE,kBAAyBC,OAAwC;IAM/E,MAAM,EACJC,QAAQ,EACRC,aAAaV,iBAAiB,EAC9BW,mBAAmBL,uBAAuB,EAC1CM,UAAUb,cAAc,EACzB,GAAGS;IAEJ,OAAO;QACLC;QACAC;QACAC;QACAC;IACF;AACF"}

View File

@@ -0,0 +1,6 @@
export { defaultTableState, useTableFeatures } from './useTableFeatures';
export { defaultTableSortState, useTableSort, useTableSortState } from './useTableSort';
export { defaultTableSelectionState, useTableSelection, useTableSelectionState } from './useTableSelection';
export { createTableColumn } from './createColumn';
export { defaultColumnSizingState, useTableColumnSizing_unstable } from './useTableColumnSizing';
export { useTableCompositeNavigation } from './useTableCompositeNavigation';

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/index.ts"],"sourcesContent":["export type {\n ColumnResizeState,\n ColumnSizingTableCellProps,\n ColumnSizingTableHeaderCellProps,\n ColumnSizingTableProps,\n ColumnWidthState,\n CreateTableColumnOptions,\n EnableKeyboardModeOnChangeCallback,\n OnSelectionChangeData,\n RowEnhancer,\n SortState,\n TableColumnDefinition,\n TableColumnId,\n TableColumnSizingOptions,\n TableColumnSizingState,\n TableFeaturePlugin,\n TableFeaturesState,\n TableRowData,\n TableRowId,\n TableSelectionState,\n TableSortState,\n UseTableColumnSizingParams,\n UseTableFeaturesOptions,\n UseTableSortOptions,\n} from './types';\nexport { defaultTableState, useTableFeatures } from './useTableFeatures';\nexport { defaultTableSortState, useTableSort, useTableSortState } from './useTableSort';\nexport { defaultTableSelectionState, useTableSelection, useTableSelectionState } from './useTableSelection';\nexport { createTableColumn } from './createColumn';\nexport { defaultColumnSizingState, useTableColumnSizing_unstable } from './useTableColumnSizing';\nexport { useTableCompositeNavigation } from './useTableCompositeNavigation';\n"],"names":["defaultTableState","useTableFeatures","defaultTableSortState","useTableSort","useTableSortState","defaultTableSelectionState","useTableSelection","useTableSelectionState","createTableColumn","defaultColumnSizingState","useTableColumnSizing_unstable","useTableCompositeNavigation"],"mappings":"AAyBA,SAASA,iBAAiB,EAAEC,gBAAgB,QAAQ,qBAAqB;AACzE,SAASC,qBAAqB,EAAEC,YAAY,EAAEC,iBAAiB,QAAQ,iBAAiB;AACxF,SAASC,0BAA0B,EAAEC,iBAAiB,EAAEC,sBAAsB,QAAQ,sBAAsB;AAC5G,SAASC,iBAAiB,QAAQ,iBAAiB;AACnD,SAASC,wBAAwB,EAAEC,6BAA6B,QAAQ,yBAAyB;AACjG,SAASC,2BAA2B,QAAQ,gCAAgC"}

View File

@@ -0,0 +1 @@
import * as React from 'react';

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,137 @@
'use client';
import * as React from 'react';
import { ArrowLeft, ArrowRight, Enter, Escape, Shift, Space } from '@fluentui/keyboard-keys';
import { useEventCallback } from '@fluentui/react-utilities';
import { useFocusFinders, useTabsterAttributes } from '@fluentui/react-tabster';
const STEP = 20;
const PRECISION_MODIFIER = Shift;
const PRECISION_FACTOR = 1 / 4;
export function useKeyboardResizing(columnResizeState) {
const [columnId, setColumnId] = React.useState();
const onChangeRef = React.useRef(undefined);
const { findPrevFocusable } = useFocusFinders();
const columnResizeStateRef = React.useRef(columnResizeState);
React.useEffect(()=>{
columnResizeStateRef.current = columnResizeState;
}, [
columnResizeState
]);
const [resizeHandleRefs] = React.useState(()=>new Map());
const keyboardHandler = useEventCallback((event)=>{
if (!columnId) {
return;
}
const width = columnResizeStateRef.current.getColumnWidth(columnId);
const precisionModifier = event.getModifierState(PRECISION_MODIFIER);
const stopEvent = ()=>{
event.preventDefault();
event.stopPropagation();
};
switch(event.key){
case ArrowLeft:
stopEvent();
columnResizeStateRef.current.setColumnWidth(event.nativeEvent, {
columnId,
width: width - (precisionModifier ? STEP * PRECISION_FACTOR : STEP)
});
return;
case ArrowRight:
stopEvent();
columnResizeStateRef.current.setColumnWidth(event.nativeEvent, {
columnId,
width: width + (precisionModifier ? STEP * PRECISION_FACTOR : STEP)
});
return;
case Space:
case Enter:
case Escape:
var // Just blur here, the onBlur handler will take care of the rest (disableInteractiveMode).
_resizeHandleRefs_get_current, _resizeHandleRefs_get;
stopEvent();
(_resizeHandleRefs_get = resizeHandleRefs.get(columnId)) === null || _resizeHandleRefs_get === void 0 ? void 0 : (_resizeHandleRefs_get_current = _resizeHandleRefs_get.current) === null || _resizeHandleRefs_get_current === void 0 ? void 0 : _resizeHandleRefs_get_current.blur();
break;
}
});
const enableInteractiveMode = React.useCallback((colId)=>{
var _onChangeRef_current, _resizeHandleRefs_get;
setColumnId(colId);
(_onChangeRef_current = onChangeRef.current) === null || _onChangeRef_current === void 0 ? void 0 : _onChangeRef_current.call(onChangeRef, colId, true);
const handle = (_resizeHandleRefs_get = resizeHandleRefs.get(colId)) === null || _resizeHandleRefs_get === void 0 ? void 0 : _resizeHandleRefs_get.current;
if (handle) {
handle.setAttribute('tabindex', '-1');
handle.tabIndex = -1;
handle.focus();
}
}, [
resizeHandleRefs
]);
const disableInteractiveMode = React.useCallback(()=>{
var // Notify the onChange listener that we are disabling interactive mode.
_onChangeRef_current, _resizeHandleRefs_get;
if (!columnId) {
return;
}
(_onChangeRef_current = onChangeRef.current) === null || _onChangeRef_current === void 0 ? void 0 : _onChangeRef_current.call(onChangeRef, columnId, false);
// Find the previous focusable element (table header button) and focus it.
const el = (_resizeHandleRefs_get = resizeHandleRefs.get(columnId)) === null || _resizeHandleRefs_get === void 0 ? void 0 : _resizeHandleRefs_get.current;
if (el) {
var _findPrevFocusable;
(_findPrevFocusable = findPrevFocusable(el)) === null || _findPrevFocusable === void 0 ? void 0 : _findPrevFocusable.focus(); // Focus the previous focusable element (header button).
el.removeAttribute('tabindex');
}
setColumnId(undefined);
}, [
columnId,
findPrevFocusable,
resizeHandleRefs
]);
const toggleInteractiveMode = (colId, onChange)=>{
onChangeRef.current = onChange;
if (!columnId) {
enableInteractiveMode(colId);
} else if (colId && columnId !== colId) {
enableInteractiveMode(colId);
setColumnId(colId);
} else {
disableInteractiveMode();
}
};
const getKeyboardResizingRef = React.useCallback((colId)=>{
const ref = resizeHandleRefs.get(colId) || React.createRef();
resizeHandleRefs.set(colId, ref);
return ref;
}, [
resizeHandleRefs
]);
// This makes sure the left and right arrow keys are ignored in tabster,
// so that they can be used for resizing.
const tabsterAttrs = useTabsterAttributes({
focusable: {
ignoreKeydown: {
ArrowLeft: true,
ArrowRight: true
}
}
});
return {
toggleInteractiveMode,
columnId,
getKeyboardResizingProps: React.useCallback((colId, currentWidth)=>({
onKeyDown: keyboardHandler,
onBlur: disableInteractiveMode,
ref: getKeyboardResizingRef(colId),
role: 'separator',
'aria-label': 'Resize column',
'aria-valuetext': `${currentWidth} pixels`,
'aria-hidden': colId === columnId ? false : true,
tabIndex: colId === columnId ? 0 : undefined,
...tabsterAttrs
}), [
columnId,
disableInteractiveMode,
getKeyboardResizingRef,
keyboardHandler,
tabsterAttrs
])
};
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,70 @@
'use client';
import * as React from 'react';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
/**
* Provides a way of reporting element width.
* Returns
* `width` - current element width (0 by default),
* `measureElementRef` - a ref function to be passed as `ref` to the element you want to measure
*/ export function useMeasureElement() {
const [width, setWidth] = React.useState(0);
const container = React.useRef(undefined);
const resizeObserverRef = React.useRef(null);
const { targetDocument } = useFluent();
// the handler for resize observer
const handleResize = React.useCallback(()=>{
var _container_current;
const containerWidth = (_container_current = container.current) === null || _container_current === void 0 ? void 0 : _container_current.getBoundingClientRect().width;
setWidth(containerWidth || 0);
}, []);
const measureElementRef = React.useCallback((el)=>{
if (!targetDocument) {
return;
}
// if the element is removed, stop observing it
if (!el && resizeObserverRef.current && container.current) {
resizeObserverRef.current.unobserve(container.current);
}
container.current = undefined;
if (el === null || el === void 0 ? void 0 : el.parentElement) {
var _resizeObserverRef_current;
container.current = el.parentElement;
handleResize();
(_resizeObserverRef_current = resizeObserverRef.current) === null || _resizeObserverRef_current === void 0 ? void 0 : _resizeObserverRef_current.observe(container.current);
}
}, [
targetDocument,
handleResize
]);
React.useEffect(()=>{
resizeObserverRef.current = createResizeObserverFromDocument(targetDocument, handleResize);
if (!container.current || !resizeObserverRef.current) {
return;
}
resizeObserverRef.current.observe(container.current);
return ()=>{
var _resizeObserverRef_current;
(_resizeObserverRef_current = resizeObserverRef.current) === null || _resizeObserverRef_current === void 0 ? void 0 : _resizeObserverRef_current.disconnect();
};
}, [
handleResize,
targetDocument
]);
return {
width,
measureElementRef
};
}
/**
* FIXME - TS 3.8/3.9 don't have ResizeObserver types by default, move this to a shared utility once we bump the minbar
* A utility method that creates a ResizeObserver from a target document
* @param targetDocument - document to use to create the ResizeObserver
* @param callback - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver#callback
* @returns a ResizeObserver instance or null if the global does not exist on the document
*/ export function createResizeObserverFromDocument(targetDocument, callback) {
var _targetDocument_defaultView;
if (!(targetDocument === null || targetDocument === void 0 ? void 0 : (_targetDocument_defaultView = targetDocument.defaultView) === null || _targetDocument_defaultView === void 0 ? void 0 : _targetDocument_defaultView.ResizeObserver)) {
return null;
}
return new targetDocument.defaultView.ResizeObserver(callback);
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useMeasureElement.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';\n\n/**\n * Provides a way of reporting element width.\n * Returns\n * `width` - current element width (0 by default),\n * `measureElementRef` - a ref function to be passed as `ref` to the element you want to measure\n */\nexport function useMeasureElement<TElement extends HTMLElement = HTMLElement>(): {\n width: number;\n measureElementRef: (el: TElement | null) => void;\n} {\n const [width, setWidth] = React.useState(0);\n\n const container = React.useRef<HTMLElement | undefined>(undefined);\n\n const resizeObserverRef = React.useRef<ResizeObserver | null>(null);\n\n const { targetDocument } = useFluent();\n\n // the handler for resize observer\n const handleResize = React.useCallback(() => {\n const containerWidth = container.current?.getBoundingClientRect().width;\n setWidth(containerWidth || 0);\n }, []);\n\n const measureElementRef = React.useCallback(\n (el: TElement | null) => {\n if (!targetDocument) {\n return;\n }\n\n // if the element is removed, stop observing it\n if (!el && resizeObserverRef.current && container.current) {\n resizeObserverRef.current.unobserve(container.current);\n }\n\n container.current = undefined;\n\n if (el?.parentElement) {\n container.current = el.parentElement;\n handleResize();\n resizeObserverRef.current?.observe(container.current);\n }\n },\n [targetDocument, handleResize],\n );\n\n React.useEffect(() => {\n resizeObserverRef.current = createResizeObserverFromDocument(targetDocument, handleResize);\n\n if (!container.current || !resizeObserverRef.current) {\n return;\n }\n\n resizeObserverRef.current.observe(container.current);\n\n return () => {\n resizeObserverRef.current?.disconnect();\n };\n }, [handleResize, targetDocument]);\n\n return { width, measureElementRef };\n}\n\n/**\n * FIXME - TS 3.8/3.9 don't have ResizeObserver types by default, move this to a shared utility once we bump the minbar\n * A utility method that creates a ResizeObserver from a target document\n * @param targetDocument - document to use to create the ResizeObserver\n * @param callback - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver#callback\n * @returns a ResizeObserver instance or null if the global does not exist on the document\n */\nexport function createResizeObserverFromDocument(\n targetDocument: Document | null | undefined,\n callback: ResizeObserverCallback,\n): ResizeObserver | null {\n if (!targetDocument?.defaultView?.ResizeObserver) {\n return null;\n }\n\n return new targetDocument.defaultView.ResizeObserver(callback);\n}\n"],"names":["React","useFluent_unstable","useFluent","useMeasureElement","width","setWidth","useState","container","useRef","undefined","resizeObserverRef","targetDocument","handleResize","useCallback","containerWidth","current","getBoundingClientRect","measureElementRef","el","unobserve","parentElement","observe","useEffect","createResizeObserverFromDocument","disconnect","callback","defaultView","ResizeObserver"],"mappings":"AAAA;AAEA,YAAYA,WAAW,QAAQ;AAC/B,SAASC,sBAAsBC,SAAS,QAAQ,kCAAkC;AAElF;;;;;CAKC,GACD,OAAO,SAASC;IAId,MAAM,CAACC,OAAOC,SAAS,GAAGL,MAAMM,QAAQ,CAAC;IAEzC,MAAMC,YAAYP,MAAMQ,MAAM,CAA0BC;IAExD,MAAMC,oBAAoBV,MAAMQ,MAAM,CAAwB;IAE9D,MAAM,EAAEG,cAAc,EAAE,GAAGT;IAE3B,kCAAkC;IAClC,MAAMU,eAAeZ,MAAMa,WAAW,CAAC;YACdN;QAAvB,MAAMO,kBAAiBP,qBAAAA,UAAUQ,OAAO,cAAjBR,yCAAAA,mBAAmBS,qBAAqB,GAAGZ,KAAK;QACvEC,SAASS,kBAAkB;IAC7B,GAAG,EAAE;IAEL,MAAMG,oBAAoBjB,MAAMa,WAAW,CACzC,CAACK;QACC,IAAI,CAACP,gBAAgB;YACnB;QACF;QAEA,+CAA+C;QAC/C,IAAI,CAACO,MAAMR,kBAAkBK,OAAO,IAAIR,UAAUQ,OAAO,EAAE;YACzDL,kBAAkBK,OAAO,CAACI,SAAS,CAACZ,UAAUQ,OAAO;QACvD;QAEAR,UAAUQ,OAAO,GAAGN;QAEpB,IAAIS,eAAAA,yBAAAA,GAAIE,aAAa,EAAE;gBAGrBV;YAFAH,UAAUQ,OAAO,GAAGG,GAAGE,aAAa;YACpCR;aACAF,6BAAAA,kBAAkBK,OAAO,cAAzBL,iDAAAA,2BAA2BW,OAAO,CAACd,UAAUQ,OAAO;QACtD;IACF,GACA;QAACJ;QAAgBC;KAAa;IAGhCZ,MAAMsB,SAAS,CAAC;QACdZ,kBAAkBK,OAAO,GAAGQ,iCAAiCZ,gBAAgBC;QAE7E,IAAI,CAACL,UAAUQ,OAAO,IAAI,CAACL,kBAAkBK,OAAO,EAAE;YACpD;QACF;QAEAL,kBAAkBK,OAAO,CAACM,OAAO,CAACd,UAAUQ,OAAO;QAEnD,OAAO;gBACLL;aAAAA,6BAAAA,kBAAkBK,OAAO,cAAzBL,iDAAAA,2BAA2Bc,UAAU;QACvC;IACF,GAAG;QAACZ;QAAcD;KAAe;IAEjC,OAAO;QAAEP;QAAOa;IAAkB;AACpC;AAEA;;;;;;CAMC,GACD,OAAO,SAASM,iCACdZ,cAA2C,EAC3Cc,QAAgC;QAE3Bd;IAAL,IAAI,EAACA,2BAAAA,sCAAAA,8BAAAA,eAAgBe,WAAW,cAA3Bf,kDAAAA,4BAA6BgB,cAAc,GAAE;QAChD,OAAO;IACT;IAEA,OAAO,IAAIhB,eAAee,WAAW,CAACC,cAAc,CAACF;AACvD"}

View File

@@ -0,0 +1,77 @@
'use client';
import * as React from 'react';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
import { getEventClientCoords, isMouseEvent, isTouchEvent, useAnimationFrame } from '@fluentui/react-utilities';
export function useTableColumnResizeMouseHandler(columnResizeState) {
const mouseX = React.useRef(0);
const currentWidth = React.useRef(0);
const colId = React.useRef(undefined);
const [dragging, setDragging] = React.useState(false);
const { targetDocument } = useFluent();
const { getColumnWidth, setColumnWidth } = columnResizeState;
const recalculatePosition = React.useCallback((e)=>{
const { clientX } = getEventClientCoords(e);
const dx = clientX - mouseX.current;
// Update the local width for the column and set it
currentWidth.current += dx;
colId.current && setColumnWidth(e, {
columnId: colId.current,
width: currentWidth.current
});
mouseX.current = clientX;
}, [
setColumnWidth
]);
const [requestRecalcFrame] = useAnimationFrame();
const onDrag = React.useCallback((e)=>{
// Using requestAnimationFrame here drastically improves resizing experience on slower CPUs
requestRecalcFrame(()=>recalculatePosition(e));
}, [
requestRecalcFrame,
recalculatePosition
]);
const onDragEnd = React.useCallback((event)=>{
if (isMouseEvent(event)) {
targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.removeEventListener('mouseup', onDragEnd);
targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.removeEventListener('mousemove', onDrag);
}
if (isTouchEvent(event)) {
targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.removeEventListener('touchend', onDragEnd);
targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.removeEventListener('touchmove', onDrag);
}
setDragging(false);
}, [
onDrag,
targetDocument
]);
const getOnMouseDown = React.useCallback((columnId)=>(event)=>{
// Keep the width locally so that we decouple the calculation of the next with from rendering.
// This makes the whole experience much faster and more precise
currentWidth.current = getColumnWidth(columnId);
mouseX.current = getEventClientCoords(event).clientX;
colId.current = columnId;
if (isMouseEvent(event)) {
// ignore other buttons than primary mouse button
if (event.target !== event.currentTarget || event.button !== 0) {
return;
}
targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.addEventListener('mouseup', onDragEnd);
targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.addEventListener('mousemove', onDrag);
setDragging(true);
}
if (isTouchEvent(event)) {
targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.addEventListener('touchend', onDragEnd);
targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.addEventListener('touchmove', onDrag);
setDragging(true);
}
}, [
getColumnWidth,
onDrag,
onDragEnd,
targetDocument
]);
return {
getOnMouseDown,
dragging
};
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,118 @@
'use client';
import { useEventCallback, useIsomorphicLayoutEffect } from '@fluentui/react-utilities';
import * as React from 'react';
import { columnDefinitionsToState, adjustColumnWidthsToFitContainer, getColumnById, setColumnProperty, getColumnWidth } from '../utils/columnResizeUtils';
const createReducer = (autoFitColumns)=>(state, action)=>{
switch(action.type){
case 'CONTAINER_WIDTH_UPDATED':
return {
...state,
containerWidth: action.containerWidth,
columnWidthState: autoFitColumns ? adjustColumnWidthsToFitContainer(state.columnWidthState, action.containerWidth) : state.columnWidthState
};
case 'COLUMNS_UPDATED':
const newS = columnDefinitionsToState(action.columns, state.columnWidthState, state.columnSizingOptions);
return {
...state,
columns: action.columns,
columnWidthState: autoFitColumns ? adjustColumnWidthsToFitContainer(newS, state.containerWidth) : newS
};
case 'COLUMN_SIZING_OPTIONS_UPDATED':
const newState = columnDefinitionsToState(state.columns, state.columnWidthState, action.columnSizingOptions);
return {
...state,
columnSizingOptions: action.columnSizingOptions,
columnWidthState: autoFitColumns ? adjustColumnWidthsToFitContainer(newState, state.containerWidth) : newState
};
case 'SET_COLUMN_WIDTH':
const { columnId, width } = action;
const { containerWidth } = state;
const column = getColumnById(state.columnWidthState, columnId);
let newColumnWidthState = [
...state.columnWidthState
];
if (!column) {
return state;
}
// Adjust the column width and measure the new total width
newColumnWidthState = setColumnProperty(newColumnWidthState, columnId, 'width', width);
// Set this width as idealWidth, because its a deliberate change, not a recalculation because of container
newColumnWidthState = setColumnProperty(newColumnWidthState, columnId, 'idealWidth', width);
// Adjust the widths to the container size
if (autoFitColumns) {
newColumnWidthState = adjustColumnWidthsToFitContainer(newColumnWidthState, containerWidth);
}
return {
...state,
columnWidthState: newColumnWidthState
};
}
};
export function useTableColumnResizeState(columns, containerWidth, params = {}) {
const { onColumnResize, columnSizingOptions, autoFitColumns = true } = params;
const reducer = React.useMemo(()=>createReducer(autoFitColumns), [
autoFitColumns
]);
const [state, dispatch] = React.useReducer(reducer, {
columns,
containerWidth: 0,
columnWidthState: columnDefinitionsToState(columns, undefined, columnSizingOptions),
columnSizingOptions
});
useIsomorphicLayoutEffect(()=>{
dispatch({
type: 'CONTAINER_WIDTH_UPDATED',
containerWidth
});
}, [
containerWidth
]);
useIsomorphicLayoutEffect(()=>{
dispatch({
type: 'COLUMNS_UPDATED',
columns
});
}, [
columns
]);
useIsomorphicLayoutEffect(()=>{
dispatch({
type: 'COLUMN_SIZING_OPTIONS_UPDATED',
columnSizingOptions
});
}, [
columnSizingOptions
]);
const setColumnWidth = useEventCallback((event, data)=>{
let { width } = data;
const { columnId } = data;
const col = getColumnById(state.columnWidthState, columnId);
if (!col) {
return;
}
width = Math.max(col.minWidth || 0, width);
if (onColumnResize) {
onColumnResize(event, {
columnId,
width
});
}
dispatch({
type: 'SET_COLUMN_WIDTH',
columnId,
width
});
});
return {
getColumnById: React.useCallback((colId)=>getColumnById(state.columnWidthState, colId), [
state.columnWidthState
]),
getColumns: React.useCallback(()=>state.columnWidthState, [
state.columnWidthState
]),
getColumnWidth: React.useCallback((colId)=>getColumnWidth(state.columnWidthState, colId), [
state.columnWidthState
]),
setColumnWidth
};
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,118 @@
'use client';
import * as React from 'react';
import { TableResizeHandle } from '../TableResizeHandle';
import { useMeasureElement } from './useMeasureElement';
import { useTableColumnResizeMouseHandler } from './useTableColumnResizeMouseHandler';
import { useTableColumnResizeState } from './useTableColumnResizeState';
import { useKeyboardResizing } from './useKeyboardResizing';
export const defaultColumnSizingState = {
getColumnWidths: ()=>[],
getOnMouseDown: ()=>()=>null,
setColumnWidth: ()=>null,
getTableProps: ()=>({}),
getTableHeaderCellProps: ()=>({
style: {},
columnId: ''
}),
getTableCellProps: ()=>({
style: {},
columnId: ''
}),
enableKeyboardMode: ()=>()=>null
};
export function useTableColumnSizing_unstable(params) {
'use no memo';
// False positive, these plugin hooks are intended to be run on every render
return (tableState)=>// eslint-disable-next-line react-hooks/rules-of-hooks
useTableColumnSizingState(tableState, {
autoFitColumns: true,
...params
});
}
function getColumnStyles(column, dragging) {
const width = column.width;
return {
// native styles
width,
// non-native element styles (flex layout)
minWidth: width,
maxWidth: width,
// Fixed the unwanted sort: https://github.com/microsoft/fluentui/issues/27803
...dragging ? {
pointerEvents: 'none'
} : {}
};
}
function useTableColumnSizingState(tableState, params = {}) {
const { columns } = tableState;
// Gets the container width
const { width, measureElementRef } = useMeasureElement();
// Creates the state based on columns and available containerWidth
const columnResizeState = useTableColumnResizeState(columns, width + ((params === null || params === void 0 ? void 0 : params.containerWidthOffset) || 0), params);
// Creates the mouse handler and attaches the state to it
const mouseHandler = useTableColumnResizeMouseHandler(columnResizeState);
// Creates the keyboard handler for resizing columns
const { toggleInteractiveMode, getKeyboardResizingProps } = useKeyboardResizing(columnResizeState);
const { autoFitColumns } = params;
const enableKeyboardMode = React.useCallback((columnId, onChange)=>(e)=>{
e.preventDefault();
e.nativeEvent.stopPropagation();
toggleInteractiveMode(columnId, onChange);
}, [
toggleInteractiveMode
]);
const { getColumnById, setColumnWidth, getColumns } = columnResizeState;
const { getOnMouseDown, dragging } = mouseHandler;
return {
...tableState,
tableRef: measureElementRef,
// eslint-disable-next-line @typescript-eslint/naming-convention
columnSizing_unstable: {
getOnMouseDown,
setColumnWidth: (columnId, w)=>setColumnWidth(undefined, {
columnId,
width: w
}),
getColumnWidths: getColumns,
getTableProps: (props = {})=>{
return {
...props,
style: {
minWidth: 'fit-content',
...props.style || {}
}
};
},
getTableHeaderCellProps: React.useCallback((columnId)=>{
var _columns_;
const col = getColumnById(columnId);
const isLastColumn = ((_columns_ = columns[columns.length - 1]) === null || _columns_ === void 0 ? void 0 : _columns_.columnId) === columnId;
const aside = isLastColumn && autoFitColumns ? null : /*#__PURE__*/ React.createElement(TableResizeHandle, {
onMouseDown: getOnMouseDown(columnId),
onTouchStart: getOnMouseDown(columnId),
...getKeyboardResizingProps(columnId, (col === null || col === void 0 ? void 0 : col.width) || 0)
});
return col ? {
style: getColumnStyles(col, dragging),
aside
} : {};
}, [
getColumnById,
columns,
dragging,
getKeyboardResizingProps,
getOnMouseDown,
autoFitColumns
]),
getTableCellProps: React.useCallback((columnId)=>{
const col = getColumnById(columnId);
return col ? {
style: getColumnStyles(col)
} : {};
}, [
getColumnById
]),
enableKeyboardMode
}
};
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,75 @@
'use client';
import * as React from 'react';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
import { ArrowDown, ArrowRight, ArrowUp, ArrowLeft } from '@fluentui/keyboard-keys';
import { useArrowNavigationGroup, useFocusableGroup, useMergedTabsterAttributes_unstable, useFocusFinders, GroupperMoveFocusEvent, MoverMoveFocusEvent, GroupperMoveFocusActions, MoverKeys } from '@fluentui/react-tabster';
import { isHTMLElement } from '@fluentui/react-utilities';
export function useTableCompositeNavigation() {
const horizontalAttr = useArrowNavigationGroup({
axis: 'horizontal'
});
const gridAttr = useArrowNavigationGroup({
axis: 'grid'
});
const groupperAttr = useFocusableGroup({
tabBehavior: 'limited-trap-focus'
});
const { findFirstFocusable } = useFocusFinders();
const { targetDocument } = useFluent();
const rowAttr = useMergedTabsterAttributes_unstable(horizontalAttr, groupperAttr);
const onKeyDown = React.useCallback((e)=>{
if (!targetDocument) {
return;
}
let activeElement = targetDocument.activeElement;
if (!activeElement || !e.currentTarget.contains(activeElement)) {
return;
}
const activeElementRole = activeElement.getAttribute('role');
// Enter groupper when in row focus mode to navigate cells
if (e.key === ArrowRight && activeElementRole === 'row' && isHTMLElement(activeElement)) {
var _findFirstFocusable;
(_findFirstFocusable = findFirstFocusable(activeElement)) === null || _findFirstFocusable === void 0 ? void 0 : _findFirstFocusable.focus();
}
if (activeElementRole === 'row') {
return;
}
const isInCell = (()=>{
let cur = isHTMLElement(activeElement) ? activeElement : null;
while(cur){
const curRole = cur.getAttribute('role');
if (curRole === 'cell' || curRole === 'gridcell') {
return true;
}
cur = cur.parentElement;
}
return false;
})();
// Escape groupper focus trap before arrow left, arrow down or arrow up
if (e.key === ArrowLeft && isInCell) {
activeElement.dispatchEvent(new GroupperMoveFocusEvent({
action: GroupperMoveFocusActions.Escape
}));
return;
}
if ((e.key === ArrowDown || e.key === ArrowUp) && isInCell) {
activeElement.dispatchEvent(new GroupperMoveFocusEvent({
action: GroupperMoveFocusActions.Escape
}));
activeElement = targetDocument.activeElement;
if (activeElement) {
activeElement.dispatchEvent(new MoverMoveFocusEvent({
key: MoverKeys[e.key]
}));
}
}
}, [
targetDocument,
findFirstFocusable
]);
return {
onTableKeyDown: onKeyDown,
tableTabsterAttribute: gridAttr,
tableRowTabsterAttribute: rowAttr
};
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,44 @@
'use client';
import * as React from 'react';
import { defaultTableSelectionState } from './useTableSelection';
import { defaultTableSortState } from './useTableSort';
import { defaultColumnSizingState } from './useTableColumnSizing';
const defaultRowEnhancer = (row)=>row;
export const defaultTableState = {
selection: defaultTableSelectionState,
sort: defaultTableSortState,
getRows: ()=>[],
getRowId: ()=>'',
items: [],
columns: [],
// eslint-disable-next-line @typescript-eslint/naming-convention
columnSizing_unstable: defaultColumnSizingState,
tableRef: React.createRef()
};
export function useTableFeatures(options, plugins = []) {
const { items, getRowId, columns } = options;
const getRows = React.useCallback((rowEnhancer = defaultRowEnhancer)=>{
return items.map((item, i)=>{
var _getRowId;
return rowEnhancer({
item,
rowId: (_getRowId = getRowId === null || getRowId === void 0 ? void 0 : getRowId(item)) !== null && _getRowId !== void 0 ? _getRowId : i
});
});
}, [
items,
getRowId
]);
const initialState = {
getRowId,
items,
columns,
getRows,
selection: defaultTableSelectionState,
sort: defaultTableSortState,
// eslint-disable-next-line @typescript-eslint/naming-convention
columnSizing_unstable: defaultColumnSizingState,
tableRef: React.createRef()
};
return plugins.reduce((state, plugin)=>plugin(state), initialState);
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useTableFeatures.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport type {\n UseTableFeaturesOptions,\n TableFeaturesState,\n TableRowData,\n RowEnhancer,\n TableFeaturePlugin,\n TableSortState,\n} from './types';\nimport { defaultTableSelectionState } from './useTableSelection';\nimport { defaultTableSortState } from './useTableSort';\nimport { defaultColumnSizingState } from './useTableColumnSizing';\n\nconst defaultRowEnhancer: RowEnhancer<unknown, TableRowData<unknown>> = row => row;\n\nexport const defaultTableState: TableFeaturesState<unknown> = {\n selection: defaultTableSelectionState,\n sort: defaultTableSortState,\n getRows: () => [],\n getRowId: () => '',\n items: [],\n columns: [],\n // eslint-disable-next-line @typescript-eslint/naming-convention\n columnSizing_unstable: defaultColumnSizingState,\n tableRef: React.createRef<HTMLDivElement>(),\n};\n\nexport function useTableFeatures<TItem>(\n options: UseTableFeaturesOptions<TItem>,\n plugins: TableFeaturePlugin[] = [],\n): TableFeaturesState<TItem> {\n const { items, getRowId, columns } = options;\n\n const getRows = React.useCallback(\n <TRowState extends TableRowData<TItem>>(rowEnhancer = defaultRowEnhancer as RowEnhancer<TItem, TRowState>) => {\n return items.map((item, i) => rowEnhancer({ item, rowId: getRowId?.(item) ?? i }));\n },\n [items, getRowId],\n );\n\n const initialState: TableFeaturesState<TItem> = {\n getRowId,\n items,\n columns,\n getRows,\n selection: defaultTableSelectionState,\n sort: defaultTableSortState as TableSortState<TItem>,\n // eslint-disable-next-line @typescript-eslint/naming-convention\n columnSizing_unstable: defaultColumnSizingState,\n tableRef: React.createRef(),\n };\n\n return plugins.reduce((state, plugin) => plugin(state), initialState);\n}\n"],"names":["React","defaultTableSelectionState","defaultTableSortState","defaultColumnSizingState","defaultRowEnhancer","row","defaultTableState","selection","sort","getRows","getRowId","items","columns","columnSizing_unstable","tableRef","createRef","useTableFeatures","options","plugins","useCallback","rowEnhancer","map","item","i","rowId","initialState","reduce","state","plugin"],"mappings":"AAAA;AAEA,YAAYA,WAAW,QAAQ;AAS/B,SAASC,0BAA0B,QAAQ,sBAAsB;AACjE,SAASC,qBAAqB,QAAQ,iBAAiB;AACvD,SAASC,wBAAwB,QAAQ,yBAAyB;AAElE,MAAMC,qBAAkEC,CAAAA,MAAOA;AAE/E,OAAO,MAAMC,oBAAiD;IAC5DC,WAAWN;IACXO,MAAMN;IACNO,SAAS,IAAM,EAAE;IACjBC,UAAU,IAAM;IAChBC,OAAO,EAAE;IACTC,SAAS,EAAE;IACX,gEAAgE;IAChEC,uBAAuBV;IACvBW,UAAUd,MAAMe,SAAS;AAC3B,EAAE;AAEF,OAAO,SAASC,iBACdC,OAAuC,EACvCC,UAAgC,EAAE;IAElC,MAAM,EAAEP,KAAK,EAAED,QAAQ,EAAEE,OAAO,EAAE,GAAGK;IAErC,MAAMR,UAAUT,MAAMmB,WAAW,CAC/B,CAAwCC,cAAchB,kBAAmD;QACvG,OAAOO,MAAMU,GAAG,CAAC,CAACC,MAAMC;gBAAiCb;mBAA3BU,YAAY;gBAAEE;gBAAME,OAAOd,CAAAA,YAAAA,qBAAAA,+BAAAA,SAAWY,mBAAXZ,uBAAAA,YAAoBa;YAAE;;IACjF,GACA;QAACZ;QAAOD;KAAS;IAGnB,MAAMe,eAA0C;QAC9Cf;QACAC;QACAC;QACAH;QACAF,WAAWN;QACXO,MAAMN;QACN,gEAAgE;QAChEW,uBAAuBV;QACvBW,UAAUd,MAAMe,SAAS;IAC3B;IAEA,OAAOG,QAAQQ,MAAM,CAAC,CAACC,OAAOC,SAAWA,OAAOD,QAAQF;AAC1D"}

View File

@@ -0,0 +1,109 @@
'use client';
import * as React from 'react';
import { useEventCallback, useSelection } from '@fluentui/react-utilities';
const noop = ()=>undefined;
export const defaultTableSelectionState = {
allRowsSelected: false,
clearRows: noop,
deselectRow: noop,
isRowSelected: ()=>false,
selectRow: noop,
selectedRows: new Set(),
someRowsSelected: false,
toggleAllRows: noop,
toggleRow: noop,
selectionMode: 'multiselect'
};
export function useTableSelection(options) {
'use no memo';
// False positive, these plugin hooks are intended to be run on every render
// eslint-disable-next-line react-hooks/rules-of-hooks
return (tableState)=>useTableSelectionState(tableState, options);
}
export function useTableSelectionState(tableState, options) {
const { items, getRowId } = tableState;
const { selectionMode: selectionMode, defaultSelectedItems, selectedItems, onSelectionChange } = options;
const [selected, selectionMethods] = useSelection({
selectionMode,
defaultSelectedItems,
selectedItems,
onSelectionChange
});
// Selection state can contain obselete items (i.e. rows that are removed)
const selectableRowIds = React.useMemo(()=>{
const rowIds = new Set();
for(let i = 0; i < items.length; i++){
var _getRowId;
rowIds.add((_getRowId = getRowId === null || getRowId === void 0 ? void 0 : getRowId(items[i])) !== null && _getRowId !== void 0 ? _getRowId : i);
}
return rowIds;
}, [
items,
getRowId
]);
const allRowsSelected = React.useMemo(()=>{
if (selectionMode === 'single') {
const selectedRow = Array.from(selected)[0];
return selectableRowIds.has(selectedRow);
}
// multiselect case
if (selected.size < selectableRowIds.size) {
return false;
}
if (selectableRowIds.size === 0) {
return false;
}
let res = true;
selectableRowIds.forEach((selectableRowId)=>{
if (!selected.has(selectableRowId)) {
res = false;
}
});
return res;
}, [
selectableRowIds,
selected,
selectionMode
]);
const someRowsSelected = React.useMemo(()=>{
if (selected.size <= 0) {
return false;
}
let res = false;
selectableRowIds.forEach((selectableRowId)=>{
if (selected.has(selectableRowId)) {
res = true;
}
});
return res;
}, [
selectableRowIds,
selected
]);
const toggleAllRows = useEventCallback((e)=>{
selectionMethods.toggleAllItems(e, items.map((item, i)=>{
var _getRowId;
return (_getRowId = getRowId === null || getRowId === void 0 ? void 0 : getRowId(item)) !== null && _getRowId !== void 0 ? _getRowId : i;
}));
});
const toggleRow = useEventCallback((e, rowId)=>selectionMethods.toggleItem(e, rowId));
const deselectRow = useEventCallback((e, rowId)=>selectionMethods.deselectItem(e, rowId));
const selectRow = useEventCallback((e, rowId)=>selectionMethods.selectItem(e, rowId));
const isRowSelected = (rowId)=>selectionMethods.isSelected(rowId);
const clearRows = useEventCallback((e)=>selectionMethods.clearItems(e));
return {
...tableState,
selection: {
selectionMode,
someRowsSelected,
allRowsSelected,
selectedRows: selected,
toggleRow,
toggleAllRows,
clearRows,
deselectRow,
selectRow,
isRowSelected
}
};
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,88 @@
'use client';
import * as React from 'react';
import { useControllableState, useEventCallback } from '@fluentui/react-utilities';
const noop = ()=>undefined;
export const defaultTableSortState = {
getSortDirection: ()=>'ascending',
setColumnSort: noop,
sort: (rows)=>[
...rows
],
sortColumn: undefined,
sortDirection: 'ascending',
toggleColumnSort: noop
};
export function useTableSort(options) {
'use no memo';
// False positive, these plugin hooks are intended to be run on every render
// eslint-disable-next-line react-hooks/rules-of-hooks
return (tableState)=>useTableSortState(tableState, options);
}
export function useTableSortState(tableState, options) {
const { columns } = tableState;
const { sortState, defaultSortState, onSortChange: onSortChangeProp = noop } = options;
const [sorted, setSorted] = useControllableState({
initialState: {
sortDirection: 'ascending',
sortColumn: undefined
},
defaultState: defaultSortState,
state: sortState
});
const { sortColumn, sortDirection } = sorted;
const onSortChange = useEventCallback(onSortChangeProp);
const toggleColumnSort = React.useCallback((e, columnId)=>{
setSorted((s)=>{
const newState = {
...s,
sortColumn: columnId
};
if (s.sortColumn === columnId) {
newState.sortDirection = s.sortDirection === 'ascending' ? 'descending' : 'ascending';
} else {
newState.sortDirection = 'ascending';
}
onSortChange === null || onSortChange === void 0 ? void 0 : onSortChange(e, newState);
return newState;
});
}, [
onSortChange,
setSorted
]);
const setColumnSort = (e, nextSortColumn, nextSortDirection)=>{
const newState = {
sortColumn: nextSortColumn,
sortDirection: nextSortDirection
};
onSortChange === null || onSortChange === void 0 ? void 0 : onSortChange(e, newState);
setSorted(newState);
};
const sort = React.useCallback((rows)=>{
return rows.slice().sort((a, b)=>{
const sortColumnDef = columns.find((column)=>column.columnId === sortColumn);
if (!(sortColumnDef === null || sortColumnDef === void 0 ? void 0 : sortColumnDef.compare)) {
return 0;
}
const mod = sortDirection === 'ascending' ? 1 : -1;
return sortColumnDef.compare(a.item, b.item) * mod;
});
}, [
columns,
sortColumn,
sortDirection
]);
const getSortDirection = (columnId)=>{
return sortColumn === columnId ? sortDirection : undefined;
};
return {
...tableState,
sort: {
sort,
sortColumn,
sortDirection,
setColumnSort,
toggleColumnSort,
getSortDirection
}
};
}

File diff suppressed because one or more lines are too long