packages/bonito-ui/src/components/form/string-list.tsx (162 lines of code) (raw):

import { FormValues, ParameterName } from "@azure/bonito-core/lib/form"; import { IconButton } from "@fluentui/react/lib/Button"; import { Stack } from "@fluentui/react/lib/Stack"; import { TextField } from "@fluentui/react/lib/TextField"; import * as React from "react"; import { useCallback, useMemo } from "react"; import { useFormParameter, useUniqueId } from "../../hooks"; import { FormControlProps } from "./form-control"; import { useAppTheme } from "../../theme"; import { translate } from "@azure/bonito-core"; export interface StringListValidationDetails { [key: number]: string; } export function StringList<V extends FormValues, K extends ParameterName<V>>( props: FormControlProps<V, K> ): JSX.Element { const { className, style, param, onChange } = props; const id = useUniqueId("form-control", props.id); const validationDetails = useFormParameter(param) .validationDetails as StringListValidationDetails; const items = useMemo<string[]>(() => { const items: string[] = []; if (param.value && Array.isArray(param.value)) { for (const item of param.value) { items.push(item); } } // Add an empty item at the end items.push(""); return items; }, [param.value]); const onItemChange = useCallback( (index: number, value: string) => { const newItems = [...items]; if (index === items.length - 1) { // Last item, add a new one newItems.push(""); } newItems[index] = value; param.value = newItems.slice(0, newItems.length - 1) as V[K]; onChange?.(null, param.value); }, [items, param, onChange] ); const onItemDelete = useCallback( (index: number) => { const newItems = [...items]; newItems.splice(index, 1); param.value = newItems.slice(0, newItems.length - 1) as V[K]; onChange?.(null, param.value); }, [items, param, onChange] ); return ( <Stack key={id} style={style} className={className}> {items.map((item, index) => { const errorMsg = validationDetails?.[index]; return ( <StringListItem key={index} index={index} value={item} label={param.label} errorMsg={errorMsg} placeholder={param.placeholder} onChange={onItemChange} onDelete={onItemDelete} disableDelete={index === items.length - 1} ></StringListItem> ); })} </Stack> ); } interface StringListItemProps { index: number; value: string; label?: string; onChange: (index: number, value: string) => void; onDelete: (index: number) => void; placeholder?: string; disableDelete?: boolean; errorMsg?: string; } function StringListItem(props: StringListItemProps) { const { index, value, label, onChange, onDelete, disableDelete, errorMsg, placeholder, } = props; const styles = useStringListItemStyles(props); const ariaLabel = `${label || ""} ${index + 1}`; return ( <Stack key={index} horizontal verticalAlign="center" styles={styles.container} > <Stack.Item grow={1}> <TextField styles={styles.input} value={value} ariaLabel={ariaLabel} placeholder={placeholder} errorMessage={errorMsg} onChange={(_, newValue) => { onChange(index, newValue || ""); }} /> </Stack.Item> <IconButton styles={styles.button} iconProps={{ iconName: "Delete" }} ariaLabel={`${translate("bonito.ui.form.delete")} ${ariaLabel}`} onClick={() => { onDelete(index); }} disabled={disableDelete} /> </Stack> ); } function useStringListItemStyles(props: StringListItemProps) { const theme = useAppTheme(); const { disableDelete } = props; return React.useMemo(() => { const itemPadding = { padding: "11px 8px 11px 12px", }; return { container: { root: { ":hover": { backgroundColor: theme.palette.neutralLighter, }, }, }, input: { root: { ...itemPadding, }, field: { height: "24px", }, fieldGroup: { height: "24px", "box-sizing": "content-box", }, }, button: { root: { ...itemPadding, visibility: disableDelete ? "hidden" : "initial", }, }, }; }, [theme.palette.neutralLighter, disableDelete]); }