import { Children, createContext, FormHTMLAttributes, forwardRef, isValidElement, ReactElement, ReactNode, useContext, useEffect, useRef, useState, } from 'react'; import styled from 'styled-components'; import { spacing, Stack, Wrap } from '../../spacing'; import { convertRemToPixels } from '../../utils'; import { Box } from '../box/Box'; import { Icon, IconName } from '../icon/Icon.component'; import { IconHelp } from '../iconhelper/IconHelper'; import { ScrollbarWrapper } from '../scrollbarwrapper/ScrollbarWrapper.component'; import { HelperText, Text } from '../text/Text.component'; const DESCRIPTION_PREFIX = 'describe-'; const LABEL_PREFIX = 'label-'; const maxWidthTooltip = { maxWidth: '20rem' }; type FormProps = Omit< FormHTMLAttributes, 'noValidate' | 'formNoValidate' > & { children: ReactNode | ReactNode[]; requireMode?: 'all' | 'partial'; leftActions?: ReactNode; rightActions?: ReactNode; banner?: ReactNode; }; type PageFormProps = { layout: { kind: 'page'; title: string; subTitle?: string; icon?: IconName; }; } & FormProps; type TabFormProps = { layout: { kind: 'tab' } } & FormProps; const StyledForm = styled.form` display: flex; flex-direction: column; align-items: stretch; height: 100%; background-color: ${(props) => props.layout.kind === 'page' && props.theme.backgroundLevel4}; `; const BasicPageLayout = styled.div<{ layoutKind: 'page' | 'tab' }>` margin: 0 auto; ${(props) => props.layoutKind === 'page' ? ` width: 45rem; padding-right: ${spacing.f16}; ` : ` width: 100%; padding-bottom: ${spacing.r24};`} `; const FixedHeader = styled(BasicPageLayout)` ${(props) => props.layoutKind === 'page' ? ` border-bottom: 1px solid ${props.theme.border}; ` : ``} `; const FixedFooter = styled(BasicPageLayout)` border-top: 1px solid ${(props) => props.theme.border}; `; const PaddedContent = styled.div` padding: ${spacing.f16} 0 ${spacing.f16} ${spacing.f16}; `; const PaddedForHeaderAndFooterContent = styled.div` padding: ${spacing.f16}; `; const ScrollArea = styled(BasicPageLayout)` flex-grow: 1; align-self: stretch; overflow-y: auto; `; const LabelContext = createContext<{ maxLabelWidth: number; setMaxLabelWidth: (setter: (value: number) => number) => void; } | null>(null); const RequireModeContext = createContext<'all' | 'partial'>('partial'); type ContentProps = { helper: string; error: string; }; type FormGroupProps = { label: string; id: string; content: ReactElement; direction?: 'vertical' | 'horizontal'; labelHelpTooltip?: ReactNode; help?: string; error?: string; required?: boolean; helpErrorPosition?: 'right' | 'bottom'; disabled?: boolean; }; const FormGroup = ({ direction = 'horizontal', label, id, labelHelpTooltip, content, help, error, required, helpErrorPosition = 'right', disabled, }: FormGroupProps) => { const ctxt = useContext(LabelContext); if (!ctxt) { //intentionaly breaking rules of hooks here throw new Error('FormGroup cannot be used outside of FormSection'); } const { maxLabelWidth, setMaxLabelWidth } = ctxt; const requireMode = useContext(RequireModeContext); const labelRef = useRef(null); useEffect(() => { if (labelRef.current) { const width = labelRef.current.getBoundingClientRect().width; setMaxLabelWidth((currentMaxLabelWidth) => { const additionalWdth = labelHelpTooltip ? convertRemToPixels(2) : 0; if (width + additionalWdth > currentMaxLabelWidth) { return width + additionalWdth; } return currentMaxLabelWidth; }); } }, [labelRef, labelHelpTooltip, setMaxLabelWidth]); const value = { disabled: disabled || false, error: error || undefined, }; return (
{content} {error ? ( {error} ) : help ? (
{help}
) : (   )}
); }; type FormSectionProps = { children: ReactElement | ReactElement[]; title?: { name: string; icon?: IconName; helpTooltip?: string }; forceLabelWidth?: number; rightActions?: ReactNode; }; const FormSection = ({ children, title, forceLabelWidth, rightActions, }: FormSectionProps) => { const [maxLabelWidth, setMaxLabelWidth] = useState( forceLabelWidth || 0, ); //If all the formgroup are not required, add `(optional)` next to form section title. const groupNotOptional = Children.toArray(children).find((child) => isValidElement(child) ? child.props.required === true : false, ); return ( {title && ( {title.icon && } {groupNotOptional ? `${title.name}` : `${title.name} (optional)`} {title.helpTooltip && ( )}
{rightActions}
)} {children}
); }; const PageForm = forwardRef( ( { layout, leftActions, rightActions, children, banner, ...formProps }, ref, ) => { const requireMode = useContext(RequireModeContext); return ( {layout.icon && ( )}{' '} {layout.title} {layout.subTitle && ( {layout.subTitle} )} {requireMode === 'partial' && ( * are required fields )}
{banner}
{Children.toArray(children)}
{leftActions}
{rightActions}
); }, ); const TabForm = forwardRef( ({ leftActions, rightActions, children, banner, ...formProps }, ref) => { return (
{leftActions}
{rightActions}
{banner} {Children.toArray(children)}
); }, ); const Form = forwardRef( ({ layout, requireMode, ...formProps }, ref) => { return ( {layout.kind === 'page' ? ( ) : ( )} ); }, ); type FieldState = { error?: string; disabled?: boolean; required?: boolean; }; const FieldContext = createContext(null); const useFieldContext = () => { const fieldContext = useContext(FieldContext); if (!fieldContext) { return { isContextAvailable: false }; } return { ...fieldContext, isContextAvailable: true }; }; export { Form, FormSection, FormGroup, useFieldContext, DESCRIPTION_PREFIX, LABEL_PREFIX, };