Skip to content

Commit

Permalink
Merge pull request #56 from IQSS/feature/create-the-form-elements-of-…
Browse files Browse the repository at this point in the history
…the-design-system

31 - Create the form elements of the design system
  • Loading branch information
kcondon authored May 4, 2023
2 parents 78dde9c + ac0bb83 commit 3a6a77d
Show file tree
Hide file tree
Showing 37 changed files with 1,302 additions and 28 deletions.
7 changes: 7 additions & 0 deletions src/sections/ui/assets/styles/bootstrap-customized.scss
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,16 @@ $link-hover-color: $dv-link-hover-color;
// Badge
@import "bootstrap/scss/badge";

// Forms
$form-label-font-weight: $dv-font-weight-bold;

@import "bootstrap/scss/forms";


// Table
@import "bootstrap/scss/tables";


// Accordion
@import "bootstrap/scss/accordion";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Base colors
$dv-brand-color: #C55B28;
$dv-primary-color: #337AB7;
$dv-secondary-color: #e3e4ec;
$dv-secondary-color: #e0e0e0;
$dv-success-color: #3c763d;
$dv-danger-color: #a94442;
$dv-warning-color: #8a6d3b;
Expand Down
4 changes: 2 additions & 2 deletions src/sections/ui/button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode } from 'react'
import { MouseEvent, ReactNode } from 'react'
import styles from './Button.module.scss'
import { Button as ButtonBS } from 'react-bootstrap'
import { Icon } from '../icon.enum'
Expand All @@ -8,7 +8,7 @@ type ButtonVariant = 'primary' | 'secondary' | 'link'
interface ButtonProps {
variant?: ButtonVariant
disabled?: boolean
onClick?: () => void
onClick?: (event: MouseEvent<HTMLButtonElement>) => void
icon?: Icon
withSpacing?: boolean
children: ReactNode
Expand Down
22 changes: 22 additions & 0 deletions src/sections/ui/form/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FormEvent, PropsWithChildren } from 'react'
import { FormGroup } from './form-group/FormGroup'
import { Form as FormBS } from 'react-bootstrap'
import { FormGroupWithMultipleFields } from './form-group-multiple-fields/FormGroupWithMultipleFields'

interface FormProps {
validated?: boolean
onSubmit?: (event: FormEvent<HTMLFormElement>) => void
}

function Form({ validated, onSubmit, children }: PropsWithChildren<FormProps>) {
return (
<FormBS validated={validated} onSubmit={onSubmit}>
{children}
</FormBS>
)
}

Form.Group = FormGroup
Form.GroupWithMultipleFields = FormGroupWithMultipleFields

export { Form }
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@import "src/sections/ui/assets/styles/design-tokens/typography.module";

.title {
padding-top: calc(0.375rem + 1px);
padding-bottom: calc(0.375rem + 1px);
font-weight: $dv-font-weight-bold;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Row } from '../../grid/Row'
import { Col } from '../../grid/Col'
import { PropsWithChildren } from 'react'
import styles from './FormGroupWithMultipleFields.module.scss'
import { RequiredInputSymbol } from '../required-input-symbol/RequiredInputSymbol'
import { DynamicFieldsButtons } from './dynamic-fields-buttons/DynamicFieldsButtons'
import { useFields } from './useFields'
import { Tooltip } from '../../tooltip/Tooltip'

interface FormGroupWithMultipleFieldsProps {
title: string
withDynamicFields?: boolean
required?: boolean
message?: string
}

const Title = ({ title, required, message }: Partial<FormGroupWithMultipleFieldsProps>) => (
<span className={styles.title}>
{title} {required && <RequiredInputSymbol />}{' '}
{message && <Tooltip placement="right" message={message}></Tooltip>}
</span>
)

export function FormGroupWithMultipleFields({
title,
withDynamicFields,
required,
message,
children
}: PropsWithChildren<FormGroupWithMultipleFieldsProps>) {
const { fields, addField, removeField } = useFields(children, withDynamicFields)

return (
<>
{fields.map((field, index) => {
const isFirstField = index == 0

return (
<Row key={index}>
<Col sm={3}>
{isFirstField && <Title title={title} required={required} message={message} />}
</Col>
<Col sm={6}>{field}</Col>
<Col sm={3}>
{withDynamicFields && (
<DynamicFieldsButtons
originalField={isFirstField}
onAddButtonClick={() => addField(field)}
onRemoveButtonClick={() => removeField(index)}
/>
)}
</Col>
</Row>
)
})}
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.container {
display: flex;
margin: 1.8em;
}

.icon {
display: inline-block;
vertical-align: -0.125em;
}

.overlay-container {
width: fit-content;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Button } from '../../../button/Button'
import styles from './DynamicFieldsButtons.module.scss'
import { MouseEvent } from 'react'
import { Dash, Plus } from 'react-bootstrap-icons'
import { OverlayTrigger } from '../../../tooltip/overlay-trigger/OverlayTrigger'

interface AddFieldButtonsProps {
originalField?: boolean
onAddButtonClick: (event: MouseEvent<HTMLButtonElement>) => void
onRemoveButtonClick: (event: MouseEvent<HTMLButtonElement>) => void
}

export function DynamicFieldsButtons({
originalField,
onAddButtonClick,
onRemoveButtonClick
}: AddFieldButtonsProps) {
return (
<div className={styles.container}>
<OverlayTrigger placement="top" message="Add">
<div className={styles['overlay-container']}>
<Button variant="secondary" onClick={onAddButtonClick}>
<Plus className={styles.icon} title="Add" />
</Button>
</div>
</OverlayTrigger>
{!originalField && (
<OverlayTrigger placement="top" message="Delete">
<div className={styles['overlay-container']}>
<Button variant="secondary" withSpacing onClick={onRemoveButtonClick}>
<Dash className={styles.icon} title="Delete" />
</Button>
</div>
</OverlayTrigger>
)}
</div>
)
}
52 changes: 52 additions & 0 deletions src/sections/ui/form/form-group-multiple-fields/useFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { ReactElement, ReactNode, useState } from 'react'
import { FormGroup } from '../form-group/FormGroup'

function getFieldsWithIndex(fields: Array<ReactNode>) {
return fields.map((field, index) => getFieldWithIndex(field, index))
}

function getFieldWithIndex(field: ReactNode, fieldIndex: number) {
return React.Children.map(field, (child: ReactNode) => {
if (!React.isValidElement(child)) {
return child
}

/* eslint-disable @typescript-eslint/no-unsafe-assignment */
const childProps = getPropsWithFieldIndex(child, fieldIndex)

/* eslint-disable @typescript-eslint/no-unsafe-member-access */
if (child.props.children) {
/* eslint-disable @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */
childProps.children = getFieldWithIndex(child.props.children, fieldIndex)
}

/* eslint-disable @typescript-eslint/no-unsafe-argument */
return React.cloneElement(child, childProps)
})
}

function getPropsWithFieldIndex(child: ReactElement, fieldIndex: number) {
const isFormGroup = (child: ReactNode) => {
return React.isValidElement(child) && child.type === FormGroup
}

/* eslint-disable @typescript-eslint/no-unsafe-return */
return isFormGroup(child)
? { ...child.props, fieldIndex: fieldIndex.toString() }
: { ...child.props }
}

export function useFields(initialField: ReactNode | undefined, withDynamicFields?: boolean) {
const initialFieldWithIndex = withDynamicFields
? getFieldWithIndex(initialField, 0)
: initialField
const [fields, setFields] = useState([initialFieldWithIndex])

const addField = (field: ReactNode | undefined) =>
setFields(getFieldsWithIndex([...fields, field]))

const removeField = (fieldIndex: number) =>
setFields(getFieldsWithIndex(fields.filter((_, i) => i !== fieldIndex)))

return { fields, addField, removeField }
}
52 changes: 52 additions & 0 deletions src/sections/ui/form/form-group/FormGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { PropsWithChildren } from 'react'
import { Form as FormBS } from 'react-bootstrap'
import { FormInput } from './form-element/FormInput'
import { FormLabel } from './form-element/FormLabel'
import { FormText } from './form-element/FormText'
import { FormSelect } from './form-element/FormSelect'
import { FormTextArea } from './form-element/FormTextArea'
import { Col, ColProps } from '../../grid/Col'
import { Row } from '../../grid/Row'
import { FormCheckbox } from './form-element/FormCheckbox'

interface FormGroupProps extends ColProps {
as?: typeof Col | typeof Row
required?: boolean
controlId: string
fieldIndex?: string
}

function FormGroup({
as = Row,
required,
controlId,
fieldIndex,
children,
...props
}: PropsWithChildren<FormGroupProps>) {
const childrenWithRequiredProp = React.Children.map(children as JSX.Element, (child) => {
return React.cloneElement(child, {
required: required,
withinMultipleFieldsGroup: as === Col
})
})

return (
<FormBS.Group
controlId={fieldIndex ? `${controlId}-${fieldIndex}` : controlId}
className="mb-3"
as={as}
{...props}>
{childrenWithRequiredProp}
</FormBS.Group>
)
}

FormGroup.Label = FormLabel
FormGroup.Input = FormInput
FormGroup.Select = FormSelect
FormGroup.TextArea = FormTextArea
FormGroup.Text = FormText
FormGroup.Checkbox = FormCheckbox

export { FormGroup }
12 changes: 12 additions & 0 deletions src/sections/ui/form/form-group/form-element/FormCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Form as FormBS } from 'react-bootstrap'
import * as React from 'react'

interface FormCheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
id: string
label: string
name: string
}

export function FormCheckbox({ label, name, id, ...props }: FormCheckboxProps) {
return <FormBS.Check label={label} name={name} type="checkbox" id={id} {...props} />
}
12 changes: 12 additions & 0 deletions src/sections/ui/form/form-group/form-element/FormElementLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PropsWithChildren } from 'react'
import { Col } from '../../../grid/Col'

interface LayoutFormGroupElementProps {
withinMultipleFieldsGroup?: boolean
}
export function FormElementLayout({
withinMultipleFieldsGroup,
children
}: PropsWithChildren<LayoutFormGroupElementProps>) {
return withinMultipleFieldsGroup ? <>{children}</> : <Col sm={9}>{children}</Col>
}
40 changes: 40 additions & 0 deletions src/sections/ui/form/form-group/form-element/FormInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Form as FormBS, InputGroup } from 'react-bootstrap'
import { FormElementLayout } from './FormElementLayout'
import { PropsWithChildren } from 'react'
import * as React from 'react'

export type FormInputElement = HTMLInputElement | HTMLTextAreaElement

interface FormInputProps extends React.HTMLAttributes<FormInputElement> {
type?: 'text' | 'email' | 'password'
readOnly?: boolean
prefix?: string
withinMultipleFieldsGroup?: boolean
}

export function FormInput({
type = 'text',
readOnly,
prefix,
withinMultipleFieldsGroup,
...props
}: FormInputProps) {
const FormInputPrefix = ({ children }: PropsWithChildren) => {
return prefix ? (
<InputGroup className="mb-3">
<InputGroup.Text>{prefix}</InputGroup.Text>
{children}
</InputGroup>
) : (
<>{children}</>
)
}

return (
<FormElementLayout withinMultipleFieldsGroup={withinMultipleFieldsGroup}>
<FormInputPrefix>
<FormBS.Control type={type} readOnly={readOnly} plaintext={readOnly} {...props} />
</FormInputPrefix>
</FormElementLayout>
)
}
27 changes: 27 additions & 0 deletions src/sections/ui/form/form-group/form-element/FormLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { PropsWithChildren } from 'react'
import { Form as FormBS } from 'react-bootstrap'
import { RequiredInputSymbol } from '../../required-input-symbol/RequiredInputSymbol'
import { Tooltip } from '../../../tooltip/Tooltip'

interface FormLabelProps {
required?: boolean
message?: string
withinMultipleFieldsGroup?: boolean
}

export function FormLabel({
required,
message,
withinMultipleFieldsGroup,
children
}: PropsWithChildren<FormLabelProps>) {
const layoutProps = withinMultipleFieldsGroup ? {} : { column: true, sm: 3 }

return (
<FormBS.Label {...layoutProps}>
{children}
{required && <RequiredInputSymbol />}{' '}
{message && <Tooltip placement="right" message={message}></Tooltip>}
</FormBS.Label>
)
}
Loading

0 comments on commit 3a6a77d

Please sign in to comment.