import React, { useState, useContext, useEffect } from 'react'; import { Label, Div } from '@cloudflare/elements'; import { createStyledComponent, filterStyleProps } from '@cloudflare/style-container'; import { Trans } from '@cloudflare/intl-react'; import { isMobile } from '@cloudflare/util-responsive'; import { useRenderOnResize } from '@cloudflare/util-hooks'; import Error from './Error'; import FormContext from './FormContext'; import { FieldValidator } from './types'; // Label hiding: https://www.w3.org/WAI/tutorials/forms/labels/#hiding-the-label-element // Screen reader only: https://webaim.org/techniques/css/invisiblecontent/ // role="alert": https://www.w3.org/TR/WCAG20-TECHS/ARIA19.html type Without = { [P in Exclude]?: never }; type XOR = T | U extends object ? (Without & U) | (Without & T) : T | U; type LabelProps = React.ComponentProps; type DivProps = React.ComponentProps; // Placeholders break accessibility. Do not allow them. export type BaseProps = Omit< ComponentProps, 'placeholder' | keyof DivProps > & // label properties and labelledBy are mutually exclusive XOR< { label: React.ReactNode; labelProps?: LabelProps; hideLabel?: boolean; }, { labelledBy: string; } > & // error properties and erroredBy are mutually exclusive XOR< { error?: React.ReactNode; errorProps?: DivProps; }, { // id of the element displaying the error erroredBy?: string; } > & XOR< { description?: React.ReactNode; descriptionProps?: DivProps; }, { // id of the element displaying the description describedBy?: string; } > & { name?: string; required?: boolean | ((value: ValueType) => boolean); requiredIndicator?: React.ReactNode; requiredMessage?: React.ReactNode; requiredValidate?: boolean; labelFirst?: boolean; componentStyleProps?: DivProps; invalid?: boolean; children?: React.ReactNode; className?: string; placeholder?: string; unwrapped?: boolean; validate?: FieldValidator; } & DivProps; type InternalBaseProps = BaseProps< ComponentProps, ValueType > & { Component: React.ComponentType; }; const screenReaderOnlyProps: Partial = { position: 'absolute', left: '-10000px', top: 'auto', width: '1px', height: '1px', overflow: 'hidden' }; let count = 0; const makeIds = () => ({ input: `cf-form-input${++count}`, label: `cf-form-label${count}`, error: `cf-form-error${count}`, description: `cf-form-description${count}` }); export function UnstyledBase({ name, label, labelProps, hideLabel, labelledBy, description, descriptionProps, describedBy, error, errorProps, erroredBy, required = false, requiredIndicator = , requiredMessage, labelFirst = true, children, Component, invalid, className, unwrapped, validate, requiredValidate = true, // Required validation can be disabled by setting false componentStyleProps, ...componentProps }: InternalBaseProps) { const [ids] = useState(makeIds); const context = useContext(FormContext); const isRow = !labelFirst || !!children; const wrapChildren = isRow && !isMobile(); // Force rerender on screen width change. useRenderOnResize(); // Handle field level validation useEffect(() => { // Field validation is disabled if the form has a validator if (!context.hasValidation && name) { let validator: FieldValidator | undefined = validate; if (requiredValidate && (required || requiredMessage)) { const requiredValidator = (val: any) => val ? undefined : requiredMessage || ( }} /> ); validator = validate ? Array.isArray(validate) ? [requiredValidator, ...validate] : [requiredValidator, validate] : requiredValidator; } validator && context.fieldValidators.set(name, validator); return () => { context.fieldValidators.delete(name); }; } }, [ name, validate, requiredValidate, context.hasValidation, required, requiredMessage, // Ensure effect depends on the contents of the validate array, rather than // the array itself, since arrays will most likely be recreated on each render ...(Array.isArray(validate) ? validate : [validate]) ]); const labelContainer = !labelledBy && ( ); erroredBy = (name && context.fieldNameToErrorId.get(name)) || erroredBy; const describedBys = [ erroredBy ?? (error ? ids.error : undefined), describedBy ?? (description ? ids.description : undefined) ] .filter(Boolean) .join(' '); const components = ( <> {/* @ts-ignore */} {!labelFirst && labelContainer} {children} ); const desc = !describedBy && description && (
{description}
); const content = ( <> {labelFirst && labelContainer} {wrapChildren ? (
{components}
) : ( components )} {!erroredBy && error && ( {error} )} {desc} ); return unwrapped ? content :
{content}
; } const StyledBase = createStyledComponent( () => ({ marginBottom: 3 }), UnstyledBase ); export function Base( props: InternalBaseProps ) { // @ts-ignore return ; }