packages/bonito-ui/src/components/form/tab-selector.tsx (111 lines of code) (raw):

import { FormValues, ParameterDependencies, ParameterName, } from "@azure/bonito-core/lib/form"; import { delayedCallback } from "@azure/bonito-core/lib/util"; import { IPivotStyles, Pivot, PivotItem } from "@fluentui/react/lib/Pivot"; import * as React from "react"; import { useFormParameter, useUniqueId } from "../../hooks"; import { FormControlProps } from "./form-control"; export interface TabSelectorProps< V extends FormValues, K extends ParameterName<V>, D extends ParameterDependencies<V> = ParameterDependencies<V>, > extends FormControlProps<V, K, D> { overflowBehavior?: "none" | "menu" | "wrap"; options: TabOption<V, K>[]; valueToKey?: (value?: V[K]) => string; } export interface TabOption<V extends FormValues, K extends ParameterName<V>> { key?: string; value: V[K]; label?: string; } const undefinedKey = "<<<No selection>>>"; const nullKey = "<<<None>>>"; /** * A tab selection form control supporting single selection */ export function TabSelector< V extends FormValues, K extends ParameterName<V>, D extends ParameterDependencies<V> = ParameterDependencies<V>, >(props: TabSelectorProps<V, K, D>): JSX.Element { const { ariaLabel, className, // disabled, onFocus, onBlur, onChange, options, param, style, valueToKey, overflowBehavior, } = props; const id = useUniqueId("tab-selector", props.id); const { setDirty } = useFormParameter(param); const [hasFocused, setHasFocused] = React.useState<boolean>(false); // Default to first option if the parameter is required if (param.required && param.value == null && options.length > 0) { // Do this asynchronously so that the current render finishes first delayedCallback(() => { param.value = options[0].value; }); } const transformedOptions = _transformOptions(options, valueToKey); const indexByKey: Record<string, number> = {}; let idx = 0; const pivotItems = transformedOptions.map((opt) => { if (!opt.key) { console.warn(`Tab option ${opt} has no key`); return <></>; } indexByKey[opt.key] = idx++; return ( <PivotItem key={opt.key} itemKey={opt.key} headerText={opt.label ?? opt.key} /> ); }); const toKey = valueToKey ?? defaultValueToKey; let pivotOverflow: "none" | "menu" = "menu"; let pivotStyles: Partial<IPivotStyles> | undefined = undefined; if (overflowBehavior === "wrap") { pivotOverflow = "none"; pivotStyles = { root: { display: "flex", flexWrap: "wrap" } }; } else if (overflowBehavior) { pivotOverflow = overflowBehavior; } return ( <Pivot id={id} aria-label={ariaLabel ?? param.label} className={className} style={style} styles={pivotStyles} overflowBehavior={pivotOverflow} // TODO: Support disabled, errorMessage // disabled={disabled || param.disabled} // errorMessage={validationError} selectedKey={param.value == null ? undefined : toKey(param.value)} onFocus={(event) => { setHasFocused(true); if (onFocus) { onFocus(event); } }} onBlur={onBlur} onLinkClick={(item, event) => { if (hasFocused) { setDirty(true); } const itemKey = item?.props.itemKey; if (!itemKey) { console.warn("PivotItem does not have an itemKey property"); return; } const selectionIndex = indexByKey[itemKey]; param.value = transformedOptions[selectionIndex].value; if (onChange) { // KLUDGE: Revisit what event type onChange needs // to take for the portal form API. // Maybe just use SyntheticEvent instead? onChange(event as React.FormEvent, param.value); } }} > {pivotItems} </Pivot> ); } function defaultValueToKey<V>(value?: V): string { if (value === undefined) { return undefinedKey; } if (value === null) { return nullKey; } const stringValue = String(value); if (stringValue === undefinedKey || stringValue === nullKey) { throw new Error( `Invalid key "${stringValue}". Cannot use a key which is reserved for null or undefined values.` ); } return stringValue; } function _transformOptions<V extends FormValues, K extends ParameterName<V>>( options: TabOption<V, K>[], valueToKey?: (value?: V[K]) => string ): TabOption<V, K>[] { const toKey = valueToKey ?? defaultValueToKey; return options.map((option) => { const key = toKey(option.value); return { key: key, label: option.label ?? key, value: option.value, }; }); }