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 @@
import * as React from 'react';

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
export { };

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/utils/OptionCollection.types.ts"],"sourcesContent":["export type OptionValue = {\n /** The disabled state of the option. */\n disabled?: boolean;\n\n /** The `id` attribute of the option. */\n id: string;\n\n /** The `text` string for the option. */\n text: string;\n\n /** The value string of the option. */\n value: string;\n};\n\nexport type OptionCollectionState = {\n /**\n * @deprecated - no longer used internally\n */\n getIndexOfId(id: string): number;\n\n /**\n * @deprecated - no longer used internally\n */\n getOptionAtIndex(index: number): OptionValue | undefined;\n\n /**\n * @deprecated - no longer used internally\n */\n getOptionsMatchingText(matcher: (text: string) => boolean): OptionValue[];\n\n /** The total number of options in the collection. */\n getCount: () => number;\n\n /** Returns the option data by key. */\n getOptionById(id: string): OptionValue | undefined;\n\n /** Returns an array of options filtered by a value matching function against the option's value string. */\n getOptionsMatchingValue(matcher: (value: string) => boolean): OptionValue[];\n\n /** The unordered option data. */\n options: OptionValue[];\n\n /** A function that child options call to register their values. Returns a function to unregister the option. */\n registerOption: (option: OptionValue, element: HTMLElement) => () => void;\n};\n"],"names":[],"mappings":"AAcA,WA8BE"}

View File

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

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/utils/Selection.types.ts"],"sourcesContent":["import * as React from 'react';\nimport { OptionValue } from './OptionCollection.types';\n\nexport type SelectionProps = {\n /**\n * For an uncontrolled component, sets the initial selection.\n * If this is set, the `defaultValue` prop MUST also be set.\n */\n defaultSelectedOptions?: string[];\n\n /**\n * Sets the selection type to multiselect.\n * Set this to true for multiselect, even if fully controlling selection state.\n * This enables styles and accessibility properties to be set.\n * @default false\n */\n multiselect?: boolean;\n\n /** Callback when an option is selected */\n // eslint-disable-next-line @nx/workspace-consistent-callback-type -- can't change type of existing callback\n onOptionSelect?: (event: SelectionEvents, data: OptionOnSelectData) => void;\n\n /**\n * An array of selected option keys.\n * Use this with `onOptionSelect` to directly control the selected option(s)\n * If this is set, the `value` prop MUST also be controlled.\n */\n selectedOptions?: string[];\n};\n\n/** Values returned by the useSelection hook */\nexport type SelectionState = {\n clearSelection: (event: SelectionEvents) => void;\n selectedOptions: string[];\n selectOption: (event: SelectionEvents, option: OptionValue) => void;\n};\n\n/**\n * Data for the onOptionSelect callback.\n * `optionValue` and `optionText` will be undefined if multiple options are modified at once.\n */\nexport type OptionOnSelectData = {\n optionValue: string | undefined;\n optionText: string | undefined;\n selectedOptions: string[];\n};\n\n/** Possible event types for onOptionSelect */\nexport type SelectionEvents =\n | React.ChangeEvent<HTMLElement>\n | React.KeyboardEvent<HTMLElement>\n | React.MouseEvent<HTMLElement>;\n"],"names":["React"],"mappings":"AAAA,YAAYA,WAAW,QAAQ"}

View File

@@ -0,0 +1,55 @@
import * as keys from '@fluentui/keyboard-keys';
import * as React from 'react';
/**
* Converts a keyboard interaction into a defined action
*/ export function getDropdownActionFromKey(e, options = {}) {
const { open = true, multiselect = false } = options;
const code = e.key;
const { altKey, ctrlKey, key, metaKey } = e;
// typing action occurs whether open or closed
if (key.length === 1 && code !== keys.Space && !altKey && !ctrlKey && !metaKey) {
return 'Type';
}
// handle opening the dropdown if closed
if (!open) {
if (code === keys.ArrowDown || code === keys.ArrowUp || code === keys.Enter || code === keys.Space) {
return 'Open';
}
// if the dropdown is closed and an action did not match the above, do nothing
return 'None';
}
// select or close actions
if (code === keys.ArrowUp && altKey || code === keys.Enter || !multiselect && code === keys.Space) {
return 'CloseSelect';
}
if (multiselect && code === keys.Space) {
return 'Select';
}
if (code === keys.Escape) {
return 'Close';
}
// navigation interactions
if (code === keys.ArrowDown) {
return 'Next';
}
if (code === keys.ArrowUp) {
return 'Previous';
}
if (code === keys.Home) {
return 'First';
}
if (code === keys.End) {
return 'Last';
}
if (code === keys.PageUp) {
return 'PageUp';
}
if (code === keys.PageDown) {
return 'PageDown';
}
if (code === keys.Tab) {
return 'Tab';
}
// if nothing matched, return none
return 'None';
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/utils/dropdownKeyActions.ts"],"sourcesContent":["import * as keys from '@fluentui/keyboard-keys';\nimport * as React from 'react';\n\n/**\n * enum of actions available in any type of managed dropdown control\n * e.g. combobox, select, datepicker, menu\n */\nexport type DropdownActions =\n | 'Close'\n | 'CloseSelect'\n | 'First'\n | 'Last'\n | 'Next'\n | 'None'\n | 'Open'\n | 'PageDown'\n | 'PageUp'\n | 'Previous'\n | 'Select'\n | 'Tab'\n | 'Type';\n\nexport interface DropdownActionOptions {\n open?: boolean;\n multiselect?: boolean;\n}\n\n/**\n * Converts a keyboard interaction into a defined action\n */\nexport function getDropdownActionFromKey(\n e: KeyboardEvent | React.KeyboardEvent,\n options: DropdownActionOptions = {},\n): DropdownActions {\n const { open = true, multiselect = false } = options;\n const code = e.key;\n const { altKey, ctrlKey, key, metaKey } = e;\n\n // typing action occurs whether open or closed\n if (key.length === 1 && code !== keys.Space && !altKey && !ctrlKey && !metaKey) {\n return 'Type';\n }\n\n // handle opening the dropdown if closed\n if (!open) {\n if (code === keys.ArrowDown || code === keys.ArrowUp || code === keys.Enter || code === keys.Space) {\n return 'Open';\n }\n\n // if the dropdown is closed and an action did not match the above, do nothing\n return 'None';\n }\n\n // select or close actions\n if ((code === keys.ArrowUp && altKey) || code === keys.Enter || (!multiselect && code === keys.Space)) {\n return 'CloseSelect';\n }\n if (multiselect && code === keys.Space) {\n return 'Select';\n }\n if (code === keys.Escape) {\n return 'Close';\n }\n\n // navigation interactions\n if (code === keys.ArrowDown) {\n return 'Next';\n }\n if (code === keys.ArrowUp) {\n return 'Previous';\n }\n if (code === keys.Home) {\n return 'First';\n }\n if (code === keys.End) {\n return 'Last';\n }\n if (code === keys.PageUp) {\n return 'PageUp';\n }\n if (code === keys.PageDown) {\n return 'PageDown';\n }\n if (code === keys.Tab) {\n return 'Tab';\n }\n\n // if nothing matched, return none\n return 'None';\n}\n"],"names":["keys","React","getDropdownActionFromKey","e","options","open","multiselect","code","key","altKey","ctrlKey","metaKey","length","Space","ArrowDown","ArrowUp","Enter","Escape","Home","End","PageUp","PageDown","Tab"],"mappings":"AAAA,YAAYA,UAAU,0BAA0B;AAChD,YAAYC,WAAW,QAAQ;AA0B/B;;CAEC,GACD,OAAO,SAASC,yBACdC,CAAsC,EACtCC,UAAiC,CAAC,CAAC;IAEnC,MAAM,EAAEC,OAAO,IAAI,EAAEC,cAAc,KAAK,EAAE,GAAGF;IAC7C,MAAMG,OAAOJ,EAAEK,GAAG;IAClB,MAAM,EAAEC,MAAM,EAAEC,OAAO,EAAEF,GAAG,EAAEG,OAAO,EAAE,GAAGR;IAE1C,8CAA8C;IAC9C,IAAIK,IAAII,MAAM,KAAK,KAAKL,SAASP,KAAKa,KAAK,IAAI,CAACJ,UAAU,CAACC,WAAW,CAACC,SAAS;QAC9E,OAAO;IACT;IAEA,wCAAwC;IACxC,IAAI,CAACN,MAAM;QACT,IAAIE,SAASP,KAAKc,SAAS,IAAIP,SAASP,KAAKe,OAAO,IAAIR,SAASP,KAAKgB,KAAK,IAAIT,SAASP,KAAKa,KAAK,EAAE;YAClG,OAAO;QACT;QAEA,8EAA8E;QAC9E,OAAO;IACT;IAEA,0BAA0B;IAC1B,IAAI,AAACN,SAASP,KAAKe,OAAO,IAAIN,UAAWF,SAASP,KAAKgB,KAAK,IAAK,CAACV,eAAeC,SAASP,KAAKa,KAAK,EAAG;QACrG,OAAO;IACT;IACA,IAAIP,eAAeC,SAASP,KAAKa,KAAK,EAAE;QACtC,OAAO;IACT;IACA,IAAIN,SAASP,KAAKiB,MAAM,EAAE;QACxB,OAAO;IACT;IAEA,0BAA0B;IAC1B,IAAIV,SAASP,KAAKc,SAAS,EAAE;QAC3B,OAAO;IACT;IACA,IAAIP,SAASP,KAAKe,OAAO,EAAE;QACzB,OAAO;IACT;IACA,IAAIR,SAASP,KAAKkB,IAAI,EAAE;QACtB,OAAO;IACT;IACA,IAAIX,SAASP,KAAKmB,GAAG,EAAE;QACrB,OAAO;IACT;IACA,IAAIZ,SAASP,KAAKoB,MAAM,EAAE;QACxB,OAAO;IACT;IACA,IAAIb,SAASP,KAAKqB,QAAQ,EAAE;QAC1B,OAAO;IACT;IACA,IAAId,SAASP,KAAKsB,GAAG,EAAE;QACrB,OAAO;IACT;IAEA,kCAAkC;IAClC,OAAO;AACT"}

View File

@@ -0,0 +1,5 @@
export const iconSizes = {
small: '16px',
medium: '20px',
large: '24px'
};

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/utils/internalTokens.ts"],"sourcesContent":["export const iconSizes = {\n small: '16px',\n medium: '20px',\n large: '24px',\n};\n"],"names":["iconSizes","small","medium","large"],"mappings":"AAAA,OAAO,MAAMA,YAAY;IACvBC,OAAO;IACPC,QAAQ;IACRC,OAAO;AACT,EAAE"}

View File

@@ -0,0 +1,194 @@
'use client';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { useControllableState, useEventCallback, useFirstMount } from '@fluentui/react-utilities';
import { useOptionCollection } from '../utils/useOptionCollection';
import { useSelection } from '../utils/useSelection';
/**
* State shared between Combobox and Dropdown components
*
* @internal
*/ export const useComboboxBaseState = (props)=>{
'use no memo';
const { appearance = 'outline', disableAutoFocus, children, clearable = false, editable = false, inlinePopup = false, mountNode = undefined, multiselect, onOpenChange, size = 'medium', activeDescendantController, freeform = false, disabled = false, onActiveOptionChange = null } = props;
const optionCollection = useOptionCollection();
const { getOptionsMatchingValue } = optionCollection;
const { getOptionById } = optionCollection;
const getActiveOption = React.useCallback(()=>{
const activeOptionId = activeDescendantController.active();
return activeOptionId ? getOptionById(activeOptionId) : undefined;
}, [
activeDescendantController,
getOptionById
]);
// Keeping some kind of backwards compatible functionality here
// eslint-disable-next-line @typescript-eslint/naming-convention
const UNSAFE_activeOption = getActiveOption();
// eslint-disable-next-line @typescript-eslint/naming-convention
const UNSAFE_setActiveOption = React.useCallback((option)=>{
let nextOption = undefined;
if (typeof option === 'function') {
const activeOption = getActiveOption();
nextOption = option(activeOption);
}
if (nextOption) {
activeDescendantController.focus(nextOption.id);
} else {
activeDescendantController.blur();
}
}, [
activeDescendantController,
getActiveOption
]);
// track whether keyboard focus outline should be shown
// tabster/keyborg doesn't work here, since the actual keyboard focus target doesn't move
const [focusVisible, setFocusVisible] = React.useState(false);
// track focused state to conditionally render collapsed listbox
// when the trigger is focused - the listbox should but hidden until the open state is changed
const [hasFocus, setHasFocus] = React.useState(false);
const ignoreNextBlur = React.useRef(false);
// calculate value based on props, internal value changes, and selected options
const isFirstMount = useFirstMount();
const [controllableValue, setValue] = useControllableState({
state: props.value,
initialState: undefined
});
const { selectedOptions, selectOption: baseSelectOption, clearSelection } = useSelection(props);
// reset any typed value when an option is selected
const selectOption = React.useCallback((ev, option)=>{
ReactDOM.unstable_batchedUpdates(()=>{
setValue(undefined);
baseSelectOption(ev, option);
});
}, [
setValue,
baseSelectOption
]);
const value = React.useMemo(()=>{
// don't compute the value if it is defined through props or setValue,
if (controllableValue !== undefined) {
return controllableValue;
}
// handle defaultValue here, so it is overridden by selection
if (isFirstMount && props.defaultValue !== undefined) {
return props.defaultValue;
}
const selectedOptionsText = getOptionsMatchingValue((optionValue)=>{
return selectedOptions.includes(optionValue);
}).map((option)=>option.text);
if (multiselect) {
// editable inputs should not display multiple selected options in the input as text
return editable ? '' : selectedOptionsText.join(', ');
}
return selectedOptionsText[0];
// do not change value after isFirstMount changes,
// we do not want to accidentally override defaultValue on a second render
// unless another value is intentionally set
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
controllableValue,
editable,
getOptionsMatchingValue,
multiselect,
selectedOptions
]);
// Handle open state, which is shared with options in context
const [open, setOpenState] = useControllableState({
state: props.open,
defaultState: props.defaultOpen,
initialState: false
});
const setOpen = React.useCallback((event, newState)=>{
if (disabled) {
return;
}
onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(event, {
open: newState
});
ReactDOM.unstable_batchedUpdates(()=>{
if (!newState && !freeform) {
setValue(undefined);
}
setOpenState(newState);
});
}, [
onOpenChange,
setOpenState,
setValue,
freeform,
disabled
]);
// update active option based on change in open state
React.useEffect(()=>{
if (open) {
// if it is single-select and there is a selected option, start at the selected option
if (!multiselect && selectedOptions.length > 0) {
const selectedOption = getOptionsMatchingValue((v)=>v === selectedOptions[0]).pop();
if (selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.id) {
activeDescendantController.focus(selectedOption.id);
}
}
} else {
activeDescendantController.blur();
}
// this should only be run in response to changes in the open state
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
open,
activeDescendantController
]);
// Fallback focus when children are updated in an open popover results in no item being focused
React.useEffect(()=>{
if (open && !disableAutoFocus && !activeDescendantController.active()) {
activeDescendantController.first();
}
// this should only be run in response to changes in the open state or children
}, [
open,
children,
disableAutoFocus,
activeDescendantController,
getOptionById
]);
const onActiveDescendantChange = useEventCallback((event)=>{
const previousOption = event.detail.previousId ? optionCollection.getOptionById(event.detail.previousId) : null;
const nextOption = optionCollection.getOptionById(event.detail.id);
onActiveOptionChange === null || onActiveOptionChange === void 0 ? void 0 : onActiveOptionChange(event, {
event,
type: 'change',
previousOption,
nextOption
});
});
return {
...optionCollection,
freeform,
disabled,
selectOption,
clearSelection,
selectedOptions,
activeOption: UNSAFE_activeOption,
appearance,
clearable,
focusVisible,
ignoreNextBlur,
inlinePopup,
mountNode,
open,
hasFocus,
setActiveOption: UNSAFE_setActiveOption,
setFocusVisible,
setHasFocus,
setOpen,
setValue,
size,
value,
multiselect,
onOptionClick: useEventCallback((e)=>{
if (!multiselect) {
setOpen(e, false);
}
}),
onActiveDescendantChange
};
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,32 @@
'use client';
import { resolvePositioningShorthand, usePositioning } from '@fluentui/react-positioning';
import * as React from 'react';
export function useComboboxPositioning(props) {
const { positioning } = props;
// Set a default set of fallback positions to try if the dropdown does not fit on screen
const fallbackPositions = [
'above',
'after',
'after-top',
'before',
'before-top'
];
// popper options
const popperOptions = {
position: 'below',
align: 'start',
offset: {
crossAxis: 0,
mainAxis: 2
},
fallbackPositions,
matchTargetSize: 'width',
autoSize: true,
...resolvePositioningShorthand(positioning)
};
const { targetRef, containerRef } = usePositioning(popperOptions);
return [
containerRef,
targetRef
];
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/utils/useComboboxPositioning.ts"],"sourcesContent":["'use client';\n\nimport { PositioningShorthandValue, resolvePositioningShorthand, usePositioning } from '@fluentui/react-positioning';\nimport type { ComboboxBaseProps } from './ComboboxBase.types';\nimport * as React from 'react';\n\nexport function useComboboxPositioning(props: ComboboxBaseProps): [\n // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-deprecated\n listboxRef: React.MutableRefObject<any>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-deprecated\n triggerRef: React.MutableRefObject<any>,\n] {\n const { positioning } = props;\n\n // Set a default set of fallback positions to try if the dropdown does not fit on screen\n const fallbackPositions: PositioningShorthandValue[] = ['above', 'after', 'after-top', 'before', 'before-top'];\n\n // popper options\n const popperOptions = {\n position: 'below' as const,\n align: 'start' as const,\n offset: { crossAxis: 0, mainAxis: 2 },\n fallbackPositions,\n matchTargetSize: 'width' as const,\n autoSize: true,\n ...resolvePositioningShorthand(positioning),\n };\n\n const { targetRef, containerRef } = usePositioning(popperOptions);\n\n return [containerRef, targetRef];\n}\n"],"names":["resolvePositioningShorthand","usePositioning","React","useComboboxPositioning","props","positioning","fallbackPositions","popperOptions","position","align","offset","crossAxis","mainAxis","matchTargetSize","autoSize","targetRef","containerRef"],"mappings":"AAAA;AAEA,SAAoCA,2BAA2B,EAAEC,cAAc,QAAQ,8BAA8B;AAErH,YAAYC,WAAW,QAAQ;AAE/B,OAAO,SAASC,uBAAuBC,KAAwB;IAM7D,MAAM,EAAEC,WAAW,EAAE,GAAGD;IAExB,wFAAwF;IACxF,MAAME,oBAAiD;QAAC;QAAS;QAAS;QAAa;QAAU;KAAa;IAE9G,iBAAiB;IACjB,MAAMC,gBAAgB;QACpBC,UAAU;QACVC,OAAO;QACPC,QAAQ;YAAEC,WAAW;YAAGC,UAAU;QAAE;QACpCN;QACAO,iBAAiB;QACjBC,UAAU;QACV,GAAGd,4BAA4BK,YAAY;IAC7C;IAEA,MAAM,EAAEU,SAAS,EAAEC,YAAY,EAAE,GAAGf,eAAeM;IAEnD,OAAO;QAACS;QAAcD;KAAU;AAClC"}

View File

@@ -0,0 +1,49 @@
'use client';
import * as React from 'react';
import { useFieldControlProps_unstable } from '@fluentui/react-field';
import { mergeCallbacks, useId, useEventCallback, slot, isResolvedShorthand, useMergedRefs } from '@fluentui/react-utilities';
import { Listbox } from '../Listbox';
/**
* @internal
* @returns listbox slot with desired behaviour and props
*/ export function useListboxSlot(listboxSlotFromProp, ref, options) {
const { state: { multiselect }, triggerRef, defaultProps } = options;
const listboxId = useId('fluent-listbox', isResolvedShorthand(listboxSlotFromProp) ? listboxSlotFromProp.id : undefined);
const listboxSlot = slot.optional(listboxSlotFromProp, {
renderByDefault: true,
elementType: Listbox,
defaultProps: {
id: listboxId,
multiselect,
tabIndex: undefined,
...defaultProps
}
});
const fieldControlProps = useFieldControlProps_unstable({
id: listboxId
}, {
supportsLabelFor: true
});
// Use the field's label to provide an accessible name for the listbox if it doesn't already have one
if (listboxSlot && !listboxSlot['aria-label'] && !listboxSlot['aria-labelledby'] && fieldControlProps['aria-labelledby']) {
listboxSlot['aria-labelledby'] = fieldControlProps['aria-labelledby'];
}
/**
* Clicking on the listbox should never blur the trigger
* in a combobox
*/ const onMouseDown = useEventCallback(mergeCallbacks((event)=>{
event.preventDefault();
}, listboxSlot === null || listboxSlot === void 0 ? void 0 : listboxSlot.onMouseDown));
const onClick = useEventCallback(mergeCallbacks((event)=>{
var _triggerRef_current;
event.preventDefault();
(_triggerRef_current = triggerRef.current) === null || _triggerRef_current === void 0 ? void 0 : _triggerRef_current.focus();
}, listboxSlot === null || listboxSlot === void 0 ? void 0 : listboxSlot.onClick));
const listboxRef = useMergedRefs(listboxSlot === null || listboxSlot === void 0 ? void 0 : listboxSlot.ref, ref);
if (listboxSlot) {
listboxSlot.ref = listboxRef;
listboxSlot.onMouseDown = onMouseDown;
listboxSlot.onClick = onClick;
}
return listboxSlot;
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/utils/useListboxSlot.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { type FieldControlProps, useFieldControlProps_unstable } from '@fluentui/react-field';\nimport {\n mergeCallbacks,\n useId,\n useEventCallback,\n slot,\n isResolvedShorthand,\n useMergedRefs,\n} from '@fluentui/react-utilities';\nimport type { ExtractSlotProps, Slot, SlotComponentType } from '@fluentui/react-utilities';\nimport type { ComboboxBaseState } from './ComboboxBase.types';\nimport { Listbox } from '../Listbox';\nimport { ListboxProps } from '../Listbox';\n\nexport type UseListboxSlotState = Pick<ComboboxBaseState, 'multiselect'>;\n\ntype UseListboxSlotOptions = {\n state: UseListboxSlotState;\n triggerRef:\n | React.RefObject<HTMLInputElement | null>\n | React.RefObject<HTMLButtonElement | null>\n | React.RefObject<HTMLButtonElement | HTMLInputElement | null>;\n defaultProps?: Partial<ListboxProps>;\n};\n\n/**\n * @internal\n * @returns listbox slot with desired behaviour and props\n */\nexport function useListboxSlot(\n listboxSlotFromProp: Slot<typeof Listbox> | undefined,\n ref: React.Ref<HTMLDivElement>,\n options: UseListboxSlotOptions,\n): SlotComponentType<ExtractSlotProps<Slot<typeof Listbox>>> | undefined {\n const {\n state: { multiselect },\n triggerRef,\n defaultProps,\n } = options;\n\n const listboxId = useId(\n 'fluent-listbox',\n isResolvedShorthand(listboxSlotFromProp) ? listboxSlotFromProp.id : undefined,\n );\n\n const listboxSlot = slot.optional(listboxSlotFromProp, {\n renderByDefault: true,\n elementType: Listbox,\n defaultProps: {\n id: listboxId,\n multiselect,\n tabIndex: undefined,\n ...defaultProps,\n },\n });\n\n const fieldControlProps = useFieldControlProps_unstable({ id: listboxId } as FieldControlProps, {\n supportsLabelFor: true,\n });\n\n // Use the field's label to provide an accessible name for the listbox if it doesn't already have one\n if (\n listboxSlot &&\n !listboxSlot['aria-label'] &&\n !listboxSlot['aria-labelledby'] &&\n fieldControlProps['aria-labelledby']\n ) {\n listboxSlot['aria-labelledby'] = fieldControlProps['aria-labelledby'];\n }\n\n /**\n * Clicking on the listbox should never blur the trigger\n * in a combobox\n */\n const onMouseDown = useEventCallback(\n mergeCallbacks((event: React.MouseEvent<HTMLDivElement>) => {\n event.preventDefault();\n }, listboxSlot?.onMouseDown),\n );\n\n const onClick = useEventCallback(\n mergeCallbacks((event: React.MouseEvent<HTMLDivElement>) => {\n event.preventDefault();\n triggerRef.current?.focus();\n }, listboxSlot?.onClick),\n );\n\n const listboxRef = useMergedRefs(listboxSlot?.ref, ref);\n if (listboxSlot) {\n listboxSlot.ref = listboxRef;\n listboxSlot.onMouseDown = onMouseDown;\n listboxSlot.onClick = onClick;\n }\n\n return listboxSlot;\n}\n"],"names":["React","useFieldControlProps_unstable","mergeCallbacks","useId","useEventCallback","slot","isResolvedShorthand","useMergedRefs","Listbox","useListboxSlot","listboxSlotFromProp","ref","options","state","multiselect","triggerRef","defaultProps","listboxId","id","undefined","listboxSlot","optional","renderByDefault","elementType","tabIndex","fieldControlProps","supportsLabelFor","onMouseDown","event","preventDefault","onClick","current","focus","listboxRef"],"mappings":"AAAA;AAEA,YAAYA,WAAW,QAAQ;AAC/B,SAAiCC,6BAA6B,QAAQ,wBAAwB;AAC9F,SACEC,cAAc,EACdC,KAAK,EACLC,gBAAgB,EAChBC,IAAI,EACJC,mBAAmB,EACnBC,aAAa,QACR,4BAA4B;AAGnC,SAASC,OAAO,QAAQ,aAAa;AAcrC;;;CAGC,GACD,OAAO,SAASC,eACdC,mBAAqD,EACrDC,GAA8B,EAC9BC,OAA8B;IAE9B,MAAM,EACJC,OAAO,EAAEC,WAAW,EAAE,EACtBC,UAAU,EACVC,YAAY,EACb,GAAGJ;IAEJ,MAAMK,YAAYd,MAChB,kBACAG,oBAAoBI,uBAAuBA,oBAAoBQ,EAAE,GAAGC;IAGtE,MAAMC,cAAcf,KAAKgB,QAAQ,CAACX,qBAAqB;QACrDY,iBAAiB;QACjBC,aAAaf;QACbQ,cAAc;YACZE,IAAID;YACJH;YACAU,UAAUL;YACV,GAAGH,YAAY;QACjB;IACF;IAEA,MAAMS,oBAAoBxB,8BAA8B;QAAEiB,IAAID;IAAU,GAAwB;QAC9FS,kBAAkB;IACpB;IAEA,qGAAqG;IACrG,IACEN,eACA,CAACA,WAAW,CAAC,aAAa,IAC1B,CAACA,WAAW,CAAC,kBAAkB,IAC/BK,iBAAiB,CAAC,kBAAkB,EACpC;QACAL,WAAW,CAAC,kBAAkB,GAAGK,iBAAiB,CAAC,kBAAkB;IACvE;IAEA;;;GAGC,GACD,MAAME,cAAcvB,iBAClBF,eAAe,CAAC0B;QACdA,MAAMC,cAAc;IACtB,GAAGT,wBAAAA,kCAAAA,YAAaO,WAAW;IAG7B,MAAMG,UAAU1B,iBACdF,eAAe,CAAC0B;YAEdb;QADAa,MAAMC,cAAc;SACpBd,sBAAAA,WAAWgB,OAAO,cAAlBhB,0CAAAA,oBAAoBiB,KAAK;IAC3B,GAAGZ,wBAAAA,kCAAAA,YAAaU,OAAO;IAGzB,MAAMG,aAAa1B,cAAca,wBAAAA,kCAAAA,YAAaT,GAAG,EAAEA;IACnD,IAAIS,aAAa;QACfA,YAAYT,GAAG,GAAGsB;QAClBb,YAAYO,WAAW,GAAGA;QAC1BP,YAAYU,OAAO,GAAGA;IACxB;IAEA,OAAOV;AACT"}

View File

@@ -0,0 +1,45 @@
'use client';
import * as React from 'react';
/**
* A hook for managing a collection of child Options
*/ export const useOptionCollection = ()=>{
const optionsById = React.useRef(new Map());
const collectionAPI = React.useMemo(()=>{
const getCount = ()=>optionsById.current.size;
// index searches are no longer used
const getOptionAtIndex = ()=>undefined;
const getIndexOfId = ()=>-1;
const getOptionById = (id)=>{
return optionsById.current.get(id);
};
const getOptionsMatchingText = (matcher)=>{
return Array.from(optionsById.current.values()).filter(({ text })=>matcher(text));
};
const getOptionsMatchingValue = (matcher)=>{
const matches = [];
for (const option of optionsById.current.values()){
if (matcher(option.value)) {
matches.push(option);
}
}
return matches;
};
return {
getCount,
getOptionAtIndex,
getIndexOfId,
getOptionById,
getOptionsMatchingText,
getOptionsMatchingValue
};
}, []);
const registerOption = React.useCallback((option)=>{
optionsById.current.set(option.id, option);
return ()=>optionsById.current.delete(option.id);
}, []);
return {
...collectionAPI,
options: Array.from(optionsById.current.values()),
registerOption
};
};

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/utils/useOptionCollection.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport type { OptionCollectionState, OptionValue } from './OptionCollection.types';\n\n/**\n * A hook for managing a collection of child Options\n */\nexport const useOptionCollection = (): OptionCollectionState => {\n const optionsById = React.useRef(new Map<string, OptionValue>());\n\n const collectionAPI = React.useMemo(() => {\n const getCount = () => optionsById.current.size;\n\n // index searches are no longer used\n const getOptionAtIndex = () => undefined;\n const getIndexOfId = () => -1;\n\n const getOptionById = (id: string) => {\n return optionsById.current.get(id);\n };\n const getOptionsMatchingText = (matcher: (text: string) => boolean) => {\n return Array.from(optionsById.current.values()).filter(({ text }) => matcher(text));\n };\n\n const getOptionsMatchingValue = (matcher: (value: string) => boolean) => {\n const matches: OptionValue[] = [];\n for (const option of optionsById.current.values()) {\n if (matcher(option.value)) {\n matches.push(option);\n }\n }\n\n return matches;\n };\n\n return {\n getCount,\n getOptionAtIndex,\n getIndexOfId,\n getOptionById,\n getOptionsMatchingText,\n getOptionsMatchingValue,\n };\n }, []);\n\n const registerOption = React.useCallback((option: OptionValue) => {\n optionsById.current.set(option.id, option);\n\n return () => optionsById.current.delete(option.id);\n }, []);\n\n return {\n ...collectionAPI,\n options: Array.from(optionsById.current.values()),\n registerOption,\n };\n};\n"],"names":["React","useOptionCollection","optionsById","useRef","Map","collectionAPI","useMemo","getCount","current","size","getOptionAtIndex","undefined","getIndexOfId","getOptionById","id","get","getOptionsMatchingText","matcher","Array","from","values","filter","text","getOptionsMatchingValue","matches","option","value","push","registerOption","useCallback","set","delete","options"],"mappings":"AAAA;AAEA,YAAYA,WAAW,QAAQ;AAG/B;;CAEC,GACD,OAAO,MAAMC,sBAAsB;IACjC,MAAMC,cAAcF,MAAMG,MAAM,CAAC,IAAIC;IAErC,MAAMC,gBAAgBL,MAAMM,OAAO,CAAC;QAClC,MAAMC,WAAW,IAAML,YAAYM,OAAO,CAACC,IAAI;QAE/C,oCAAoC;QACpC,MAAMC,mBAAmB,IAAMC;QAC/B,MAAMC,eAAe,IAAM,CAAC;QAE5B,MAAMC,gBAAgB,CAACC;YACrB,OAAOZ,YAAYM,OAAO,CAACO,GAAG,CAACD;QACjC;QACA,MAAME,yBAAyB,CAACC;YAC9B,OAAOC,MAAMC,IAAI,CAACjB,YAAYM,OAAO,CAACY,MAAM,IAAIC,MAAM,CAAC,CAAC,EAAEC,IAAI,EAAE,GAAKL,QAAQK;QAC/E;QAEA,MAAMC,0BAA0B,CAACN;YAC/B,MAAMO,UAAyB,EAAE;YACjC,KAAK,MAAMC,UAAUvB,YAAYM,OAAO,CAACY,MAAM,GAAI;gBACjD,IAAIH,QAAQQ,OAAOC,KAAK,GAAG;oBACzBF,QAAQG,IAAI,CAACF;gBACf;YACF;YAEA,OAAOD;QACT;QAEA,OAAO;YACLjB;YACAG;YACAE;YACAC;YACAG;YACAO;QACF;IACF,GAAG,EAAE;IAEL,MAAMK,iBAAiB5B,MAAM6B,WAAW,CAAC,CAACJ;QACxCvB,YAAYM,OAAO,CAACsB,GAAG,CAACL,OAAOX,EAAE,EAAEW;QAEnC,OAAO,IAAMvB,YAAYM,OAAO,CAACuB,MAAM,CAACN,OAAOX,EAAE;IACnD,GAAG,EAAE;IAEL,OAAO;QACL,GAAGT,aAAa;QAChB2B,SAASd,MAAMC,IAAI,CAACjB,YAAYM,OAAO,CAACY,MAAM;QAC9CQ;IACF;AACF,EAAE"}

View File

@@ -0,0 +1,62 @@
'use client';
import * as React from 'react';
import { useControllableState } from '@fluentui/react-utilities';
export const useSelection = (props)=>{
const { defaultSelectedOptions, multiselect, onOptionSelect } = props;
const [selectedOptions, setSelectedOptions] = useControllableState({
state: props.selectedOptions,
defaultState: defaultSelectedOptions,
initialState: []
});
const selectOption = React.useCallback((event, option)=>{
// if the option is disabled, do nothing
if (option.disabled) {
return;
}
// for single-select, always return the selected option
let newSelection = [
option.value
];
// toggle selected state of the option for multiselect
if (multiselect) {
const selectedIndex = selectedOptions.findIndex((o)=>o === option.value);
if (selectedIndex > -1) {
// deselect option
newSelection = [
...selectedOptions.slice(0, selectedIndex),
...selectedOptions.slice(selectedIndex + 1)
];
} else {
// select option
newSelection = [
...selectedOptions,
option.value
];
}
}
setSelectedOptions(newSelection);
onOptionSelect === null || onOptionSelect === void 0 ? void 0 : onOptionSelect(event, {
optionValue: option.value,
optionText: option.text,
selectedOptions: newSelection
});
}, [
onOptionSelect,
multiselect,
selectedOptions,
setSelectedOptions
]);
const clearSelection = (event)=>{
setSelectedOptions([]);
onOptionSelect === null || onOptionSelect === void 0 ? void 0 : onOptionSelect(event, {
optionValue: undefined,
optionText: undefined,
selectedOptions: []
});
};
return {
clearSelection,
selectOption,
selectedOptions
};
};

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/utils/useSelection.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { useControllableState } from '@fluentui/react-utilities';\nimport { OptionValue } from './OptionCollection.types';\nimport { SelectionEvents, SelectionProps, SelectionState } from './Selection.types';\n\nexport const useSelection = (props: SelectionProps): SelectionState => {\n const { defaultSelectedOptions, multiselect, onOptionSelect } = props;\n\n const [selectedOptions, setSelectedOptions] = useControllableState({\n state: props.selectedOptions,\n defaultState: defaultSelectedOptions,\n initialState: [],\n });\n\n const selectOption = React.useCallback(\n (event: SelectionEvents, option: OptionValue) => {\n // if the option is disabled, do nothing\n if (option.disabled) {\n return;\n }\n\n // for single-select, always return the selected option\n let newSelection = [option.value];\n\n // toggle selected state of the option for multiselect\n if (multiselect) {\n const selectedIndex = selectedOptions.findIndex(o => o === option.value);\n if (selectedIndex > -1) {\n // deselect option\n newSelection = [...selectedOptions.slice(0, selectedIndex), ...selectedOptions.slice(selectedIndex + 1)];\n } else {\n // select option\n newSelection = [...selectedOptions, option.value];\n }\n }\n\n setSelectedOptions(newSelection);\n onOptionSelect?.(event, { optionValue: option.value, optionText: option.text, selectedOptions: newSelection });\n },\n [onOptionSelect, multiselect, selectedOptions, setSelectedOptions],\n );\n\n const clearSelection = (event: SelectionEvents) => {\n setSelectedOptions([]);\n onOptionSelect?.(event, { optionValue: undefined, optionText: undefined, selectedOptions: [] });\n };\n\n return { clearSelection, selectOption, selectedOptions };\n};\n"],"names":["React","useControllableState","useSelection","props","defaultSelectedOptions","multiselect","onOptionSelect","selectedOptions","setSelectedOptions","state","defaultState","initialState","selectOption","useCallback","event","option","disabled","newSelection","value","selectedIndex","findIndex","o","slice","optionValue","optionText","text","clearSelection","undefined"],"mappings":"AAAA;AAEA,YAAYA,WAAW,QAAQ;AAC/B,SAASC,oBAAoB,QAAQ,4BAA4B;AAIjE,OAAO,MAAMC,eAAe,CAACC;IAC3B,MAAM,EAAEC,sBAAsB,EAAEC,WAAW,EAAEC,cAAc,EAAE,GAAGH;IAEhE,MAAM,CAACI,iBAAiBC,mBAAmB,GAAGP,qBAAqB;QACjEQ,OAAON,MAAMI,eAAe;QAC5BG,cAAcN;QACdO,cAAc,EAAE;IAClB;IAEA,MAAMC,eAAeZ,MAAMa,WAAW,CACpC,CAACC,OAAwBC;QACvB,wCAAwC;QACxC,IAAIA,OAAOC,QAAQ,EAAE;YACnB;QACF;QAEA,uDAAuD;QACvD,IAAIC,eAAe;YAACF,OAAOG,KAAK;SAAC;QAEjC,sDAAsD;QACtD,IAAIb,aAAa;YACf,MAAMc,gBAAgBZ,gBAAgBa,SAAS,CAACC,CAAAA,IAAKA,MAAMN,OAAOG,KAAK;YACvE,IAAIC,gBAAgB,CAAC,GAAG;gBACtB,kBAAkB;gBAClBF,eAAe;uBAAIV,gBAAgBe,KAAK,CAAC,GAAGH;uBAAmBZ,gBAAgBe,KAAK,CAACH,gBAAgB;iBAAG;YAC1G,OAAO;gBACL,gBAAgB;gBAChBF,eAAe;uBAAIV;oBAAiBQ,OAAOG,KAAK;iBAAC;YACnD;QACF;QAEAV,mBAAmBS;QACnBX,2BAAAA,qCAAAA,eAAiBQ,OAAO;YAAES,aAAaR,OAAOG,KAAK;YAAEM,YAAYT,OAAOU,IAAI;YAAElB,iBAAiBU;QAAa;IAC9G,GACA;QAACX;QAAgBD;QAAaE;QAAiBC;KAAmB;IAGpE,MAAMkB,iBAAiB,CAACZ;QACtBN,mBAAmB,EAAE;QACrBF,2BAAAA,qCAAAA,eAAiBQ,OAAO;YAAES,aAAaI;YAAWH,YAAYG;YAAWpB,iBAAiB,EAAE;QAAC;IAC/F;IAEA,OAAO;QAAEmB;QAAgBd;QAAcL;IAAgB;AACzD,EAAE"}

View File

@@ -0,0 +1,142 @@
'use client';
import * as React from 'react';
import { useSetKeyboardNavigation } from '@fluentui/react-tabster';
import { mergeCallbacks, slot, useEventCallback, useMergedRefs } from '@fluentui/react-utilities';
import { getDropdownActionFromKey } from '../utils/dropdownKeyActions';
/**
* Shared trigger behaviour for combobox and dropdown
* @returns trigger slot with desired behaviour and props
*/ export function useTriggerSlot(triggerSlotFromProp, ref, options) {
const { state: { open, setOpen, setHasFocus }, defaultProps, elementType, activeDescendantController } = options;
const trigger = slot.always(triggerSlotFromProp, {
defaultProps: {
type: 'text',
'aria-expanded': open,
role: 'combobox',
...typeof defaultProps === 'object' && defaultProps
},
elementType
});
// handle trigger focus/blur
const triggerRef = React.useRef(null);
trigger.ref = useMergedRefs(triggerRef, trigger.ref, ref);
// the trigger should open/close the popup on click or blur
trigger.onBlur = mergeCallbacks((event)=>{
setOpen(event, false);
setHasFocus(false);
}, trigger.onBlur);
trigger.onFocus = mergeCallbacks((event)=>{
if (event.target === event.currentTarget) {
setHasFocus(true);
}
}, trigger.onFocus);
trigger.onClick = mergeCallbacks((event)=>{
setOpen(event, !open);
}, trigger.onClick);
// handle combobox keyboard interaction
trigger.onKeyDown = mergeCallbacks(useTriggerKeydown({
activeDescendantController,
...options.state
}), trigger.onKeyDown);
return trigger;
}
function useTriggerKeydown(options) {
const { activeDescendantController, getOptionById, setOpen, selectOption, multiselect, open } = options;
const getActiveOption = React.useCallback(()=>{
const activeOptionId = activeDescendantController.active();
return activeOptionId ? getOptionById(activeOptionId) : undefined;
}, [
activeDescendantController,
getOptionById
]);
const first = ()=>{
activeDescendantController.first();
};
const last = ()=>{
activeDescendantController.last();
};
const next = (activeOption)=>{
if (activeOption) {
activeDescendantController.next();
} else {
activeDescendantController.first();
}
};
const previous = (activeOption)=>{
if (activeOption) {
activeDescendantController.prev();
} else {
activeDescendantController.first();
}
};
const pageUp = ()=>{
for(let i = 0; i < 10; i++){
activeDescendantController.prev();
}
};
const pageDown = ()=>{
for(let i = 0; i < 10; i++){
activeDescendantController.next();
}
};
const setKeyboardNavigation = useSetKeyboardNavigation();
return useEventCallback((e)=>{
const action = getDropdownActionFromKey(e, {
open,
multiselect
});
const activeOption = getActiveOption();
switch(action){
case 'First':
case 'Last':
case 'Next':
case 'Previous':
case 'PageDown':
case 'PageUp':
case 'Open':
case 'Close':
case 'CloseSelect':
case 'Select':
e.preventDefault();
break;
}
setKeyboardNavigation(true);
switch(action){
case 'First':
first();
break;
case 'Last':
last();
break;
case 'Next':
next(activeOption);
break;
case 'Previous':
previous(activeOption);
break;
case 'PageDown':
pageDown();
break;
case 'PageUp':
pageUp();
break;
case 'Open':
setOpen(e, true);
break;
case 'Close':
// stop propagation for escape key to avoid dismissing any parent popups
e.stopPropagation();
setOpen(e, false);
break;
case 'CloseSelect':
!multiselect && !(activeOption === null || activeOption === void 0 ? void 0 : activeOption.disabled) && setOpen(e, false);
// fallthrough
case 'Select':
activeOption && selectOption(e, activeOption);
break;
case 'Tab':
!multiselect && activeOption && selectOption(e, activeOption);
break;
}
});
}

File diff suppressed because one or more lines are too long