From ff5f4717dff14c891a1f2d1240486bd4b07b84a3 Mon Sep 17 00:00:00 2001 From: Jordan Phillips Date: Tue, 23 Jul 2024 16:46:15 +1000 Subject: [PATCH] feat: add form legend and fieldset components (#158) --- .../src/components/form/Form.stories.tsx | 6 +-- packages/react/src/components/form/Form.tsx | 4 +- .../src/components/form/FormFeedback.tsx | 7 ++- .../src/components/form/FormFieldSet.tsx | 49 +++++++++++++++++++ .../react/src/components/form/FormGroup.tsx | 10 ++-- .../react/src/components/form/FormLabel.tsx | 2 +- .../react/src/components/form/FormLegend.tsx | 49 +++++++++++++++++++ .../src/components/form/component.parts.ts | 2 + packages/react/src/components/form/index.ts | 2 + .../components/form/use-form-group.hook.tsx | 10 ++-- packages/theme/src/components/form.ts | 30 +++++++++--- 11 files changed, 146 insertions(+), 25 deletions(-) create mode 100644 packages/react/src/components/form/FormFieldSet.tsx create mode 100644 packages/react/src/components/form/FormLegend.tsx diff --git a/packages/react/src/components/form/Form.stories.tsx b/packages/react/src/components/form/Form.stories.tsx index ef84f81..66e08b4 100644 --- a/packages/react/src/components/form/Form.stories.tsx +++ b/packages/react/src/components/form/Form.stories.tsx @@ -18,11 +18,11 @@ export const Default: StoryFn = (args) => ( Email address - + $ - + USD - + A unique string of characters that identifies an email account diff --git a/packages/react/src/components/form/Form.tsx b/packages/react/src/components/form/Form.tsx index b9ba2d9..3b522e9 100644 --- a/packages/react/src/components/form/Form.tsx +++ b/packages/react/src/components/form/Form.tsx @@ -26,11 +26,11 @@ const Component: ComponentType = React.forwardRef( props: ComponentProps, ref: Polymophic.Ref ) => { - const { as, children, className, status, ...rest } = props + const { as, children, className, color, ...rest } = props const Element = as ?? Form - const slots = React.useMemo(() => form({ status }), [status]) + const slots = React.useMemo(() => form({ color }), [color]) const component = React.useMemo( () => ({ diff --git a/packages/react/src/components/form/FormFeedback.tsx b/packages/react/src/components/form/FormFeedback.tsx index d9ef0e4..fcae66c 100644 --- a/packages/react/src/components/form/FormFeedback.tsx +++ b/packages/react/src/components/form/FormFeedback.tsx @@ -2,14 +2,13 @@ import React from 'react' +import type { FeedbackType } from '~/components/form/use-form-group.hook' import type * as Polymophic from '~/utilities/polymorphic' import { useFormGroupContext } from '~/components/form/use-form-group.hook' import { cn } from '~/utilities' const __ELEMENT_TYPE__ = 'span' -type FeedbackType = 'success' | 'warning' | 'error' - type ComponentOwnProps = { type: FeedbackType } @@ -38,7 +37,7 @@ const Component: ComponentType = React.forwardRef( () => ({ className: context?.slots.feedback({ class: cn(className, { hidden: type !== context.feedback }), - status: context.status, + color: context.status, }), ...rest, }), @@ -53,5 +52,5 @@ const Component: ComponentType = React.forwardRef( } ) -export type { ComponentOwnProps as FormFeedbackOwnProps, ComponentProps as FormFeedbackProps, FeedbackType } +export type { ComponentOwnProps as FormFeedbackOwnProps, ComponentProps as FormFeedbackProps } export default Component diff --git a/packages/react/src/components/form/FormFieldSet.tsx b/packages/react/src/components/form/FormFieldSet.tsx new file mode 100644 index 0000000..8e276a4 --- /dev/null +++ b/packages/react/src/components/form/FormFieldSet.tsx @@ -0,0 +1,49 @@ +'use client' + +import React from 'react' +import { form } from '@giantnodes/theme' + +import type * as Polymophic from '~/utilities/polymorphic' + +const __ELEMENT_TYPE__ = 'fieldset' + +type ComponentOwnProps = unknown + +type ComponentProps = Polymophic.ComponentPropsWithRef< + TElement, + ComponentOwnProps +> + +type ComponentType = ( + props: ComponentProps +) => React.ReactNode + +const Component: ComponentType = React.forwardRef( + ( + props: ComponentProps, + ref: Polymophic.Ref + ) => { + const { as, children, className, ...rest } = props + + const Element = as ?? __ELEMENT_TYPE__ + + const slots = React.useMemo(() => form({}), []) + + const component = React.useMemo( + () => ({ + className: slots.fieldset({ className }), + ...rest, + }), + [className, slots, rest] + ) + + return ( + + {children} + + ) + } +) + +export type { ComponentOwnProps as FormFieldSetOwnProps, ComponentProps as FormFieldSetProps } +export default Component diff --git a/packages/react/src/components/form/FormGroup.tsx b/packages/react/src/components/form/FormGroup.tsx index a1a02eb..3242457 100644 --- a/packages/react/src/components/form/FormGroup.tsx +++ b/packages/react/src/components/form/FormGroup.tsx @@ -29,7 +29,7 @@ const Component: ComponentType = React.forwardRef( props: ComponentProps, ref: Polymophic.Ref ) => { - const { as, children, className, name, success, warning, error, onChange, onBlur, ...rest } = props + const { as, children, className, name, success, info, warning, error, onChange, onBlur, ...rest } = props const Element = as ?? __ELEMENT_TYPE__ @@ -45,21 +45,23 @@ const Component: ComponentType = React.forwardRef( const component = React.useMemo>( () => ({ - 'data-error': error ?? undefined, 'data-success': success ?? undefined, + 'data-info': info ?? undefined, 'data-warning': warning ?? undefined, + 'data-error': error ?? undefined, className: context.slots.group({ className }), ...rest, }), - [className, context.slots, error, rest, success, warning] + [className, context.slots, error, info, rest, success, warning] ) React.useEffect(() => { if (success) context.setFeedback('success') + if (info) context.setFeedback('info') if (warning) context.setFeedback('warning') if (error) context.setFeedback('error') else context.setFeedback(null) - }, [context, success, warning, error]) + }, [context, error, info, success, warning]) return ( diff --git a/packages/react/src/components/form/FormLabel.tsx b/packages/react/src/components/form/FormLabel.tsx index a9541ef..709963f 100644 --- a/packages/react/src/components/form/FormLabel.tsx +++ b/packages/react/src/components/form/FormLabel.tsx @@ -33,7 +33,7 @@ const Component: ComponentType = React.forwardRef( const component = React.useMemo( () => ({ - className: context?.slots.label({ className, status: context.status }), + className: context?.slots.label({ className, color: context.status }), ...context?.labelProps, ...rest, }), diff --git a/packages/react/src/components/form/FormLegend.tsx b/packages/react/src/components/form/FormLegend.tsx new file mode 100644 index 0000000..2e57079 --- /dev/null +++ b/packages/react/src/components/form/FormLegend.tsx @@ -0,0 +1,49 @@ +'use client' + +import React from 'react' +import { form } from '@giantnodes/theme' + +import type * as Polymophic from '~/utilities/polymorphic' + +const __ELEMENT_TYPE__ = 'legend' + +type ComponentOwnProps = unknown + +type ComponentProps = Polymophic.ComponentPropsWithRef< + TElement, + ComponentOwnProps +> + +type ComponentType = ( + props: ComponentProps +) => React.ReactNode + +const Component: ComponentType = React.forwardRef( + ( + props: ComponentProps, + ref: Polymophic.Ref + ) => { + const { as, children, className, ...rest } = props + + const Element = as ?? __ELEMENT_TYPE__ + + const slots = React.useMemo(() => form({}), []) + + const component = React.useMemo( + () => ({ + className: slots.legend({ className }), + ...rest, + }), + [className, slots, rest] + ) + + return ( + + {children} + + ) + } +) + +export type { ComponentOwnProps as FormLegendOwnProps, ComponentProps as FormLegendSetProps } +export default Component diff --git a/packages/react/src/components/form/component.parts.ts b/packages/react/src/components/form/component.parts.ts index 4303a18..2e76d28 100644 --- a/packages/react/src/components/form/component.parts.ts +++ b/packages/react/src/components/form/component.parts.ts @@ -1,5 +1,7 @@ export { default as Root } from '~/components/form/Form' export { default as Caption } from '~/components/form/FormCaption' export { default as Feedback } from '~/components/form/FormFeedback' +export { default as FieldSet } from '~/components/form/FormFieldSet' export { default as Group } from '~/components/form/FormGroup' export { default as Label } from '~/components/form/FormLabel' +export { default as Legend } from '~/components/form/FormLegend' diff --git a/packages/react/src/components/form/index.ts b/packages/react/src/components/form/index.ts index a2e1ef9..c708397 100644 --- a/packages/react/src/components/form/index.ts +++ b/packages/react/src/components/form/index.ts @@ -1,7 +1,9 @@ export type * from '~/components/form/Form' export type * from '~/components/form/FormCaption' export type * from '~/components/form/FormFeedback' +export type * from '~/components/form/FormFieldSet' export type * from '~/components/form/FormGroup' export type * from '~/components/form/FormLabel' +export type * from '~/components/form/FormLegend' export * as Form from '~/components/form/component.parts' diff --git a/packages/react/src/components/form/use-form-group.hook.tsx b/packages/react/src/components/form/use-form-group.hook.tsx index 746b4b3..110ec72 100644 --- a/packages/react/src/components/form/use-form-group.hook.tsx +++ b/packages/react/src/components/form/use-form-group.hook.tsx @@ -5,13 +5,15 @@ import type { LabelAria } from 'react-aria' import React from 'react' import { form } from '@giantnodes/theme' -import type { FeedbackType } from '~/components/form/FormFeedback' import type { ChangeHandler } from '~/utilities/types' import { createContext } from '~/utilities/context' +export type FeedbackType = 'success' | 'info' | 'warning' | 'error' + type UseFormGroupProps = LabelAria & { ref?: React.RefObject name?: string + color?: FormVariantProps['color'] onChange?: ChangeHandler onBlur?: ChangeHandler } @@ -23,10 +25,12 @@ export const useFormGroup = (props: UseFormGroupProps) => { const [feedback, setFeedback] = React.useState(null) - const status = React.useMemo(() => { + const status = React.useMemo(() => { switch (feedback) { case 'success': return 'success' + case 'info': + return 'info' case 'warning': return 'warning' case 'error': @@ -36,7 +40,7 @@ export const useFormGroup = (props: UseFormGroupProps) => { } }, [feedback]) - const slots = React.useMemo(() => form({ status }), [status]) + const slots = React.useMemo(() => form({ color: status }), [status]) return { ref, diff --git a/packages/theme/src/components/form.ts b/packages/theme/src/components/form.ts index 00b4066..0c0137e 100644 --- a/packages/theme/src/components/form.ts +++ b/packages/theme/src/components/form.ts @@ -4,35 +4,49 @@ import { tv } from 'tailwind-variants' export const form = tv({ slots: { form: [], - group: ['group', 'flex flex-col gap-2', 'w-full'], - label: ['text-sm', 'text-title'], + group: ['group', 'flex flex-col gap-1', 'w-full'], + label: ['text-sm'], + legend: ['w-full', 'mb-1'], + fieldset: ['flex flex-row gap-2', 'text-sm'], caption: ['text-xs', 'text-subtitle'], feedback: ['text-xs'], }, variants: { - status: { + color: { neutral: { + label: ['text-title'], + fieldset: ['text-title'], feedback: ['text-content'], }, brand: { + label: ['text-brand'], + fieldset: ['text-brand'], feedback: ['text-brand'], }, success: { - feedback: ['text-green'], + label: ['text-success'], + fieldset: ['text-success'], + feedback: ['text-success'], }, info: { - feedback: ['text-blue'], + label: ['text-info'], + fieldset: ['text-info'], + feedback: ['text-info'], }, warning: { - feedback: ['text-yellow'], + label: ['text-warning'], + fieldset: ['text-warning'], + feedback: ['text-warning'], }, danger: { - feedback: ['text-red'], + label: ['text-danger'], + fieldset: ['text-danger'], + feedback: ['text-danger'], }, }, }, defaultVariants: { - status: 'neutral', + color: 'neutral', }, })