[component lib] input forward ref react-hook-form (#30)

This commit is contained in:
Vivian Phung 2024-05-14 16:03:29 -04:00 committed by GitHub
commit f8908c1c06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 239 additions and 109 deletions

View File

@ -1,11 +1,18 @@
import { ReactNode, useMemo } from 'react'; import {
import { ComponentPropsWithoutRef } from 'react'; forwardRef,
import { InputTheme, inputTheme } from './Input.theme'; ReactNode,
useMemo,
ComponentPropsWithoutRef,
} from 'react';
import { FieldValues, UseFormRegister } from 'react-hook-form';
import { WarningIcon } from 'components/shared/CustomIcon'; import { WarningIcon } from 'components/shared/CustomIcon';
import { cloneIcon } from 'utils/cloneIcon'; import { cloneIcon } from 'utils/cloneIcon';
import { cn } from 'utils/classnames'; import { cn } from 'utils/classnames';
export interface InputProps import { InputTheme, inputTheme } from './Input.theme';
export interface InputProps<T extends FieldValues = FieldValues>
extends InputTheme, extends InputTheme,
Omit<ComponentPropsWithoutRef<'input'>, 'size'> { Omit<ComponentPropsWithoutRef<'input'>, 'size'> {
label?: string; label?: string;
@ -13,93 +20,108 @@ export interface InputProps
leftIcon?: ReactNode; leftIcon?: ReactNode;
rightIcon?: ReactNode; rightIcon?: ReactNode;
helperText?: string; helperText?: string;
// react-hook-form optional register
register?: ReturnType<UseFormRegister<T>>;
} }
export const Input = ({ const Input = forwardRef<HTMLInputElement, InputProps>(
className, (
label, {
description, className,
leftIcon, label,
rightIcon, description,
helperText, leftIcon,
size, rightIcon,
state, helperText,
appearance, register,
...props size,
}: InputProps) => { state,
const styleProps = useMemo( appearance,
() => ({ ...props
size: size || 'md', },
state: state || 'default', ref,
appearance, // Pass appearance to inputTheme ) => {
}), const styleProps = useMemo(
[size, state, appearance], () => ({
); size: size || 'md',
state: state || 'default',
appearance, // Pass appearance to inputTheme
}),
[size, state, appearance],
);
const { const {
container: containerCls, container: containerCls,
label: labelCls, label: labelCls,
description: descriptionCls, description: descriptionCls,
input: inputCls, input: inputCls,
icon: iconCls, icon: iconCls,
iconContainer: iconContainerCls, iconContainer: iconContainerCls,
helperText: helperTextCls, helperText: helperTextCls,
helperIcon: helperIconCls, helperIcon: helperIconCls,
} = inputTheme({ ...styleProps }); } = inputTheme({ ...styleProps });
const renderLabels = useMemo(() => {
if (!label && !description) return null;
return (
<div className="flex flex-col gap-y-1">
<p className={labelCls()}>{label}</p>
<p className={descriptionCls()}>{description}</p>
</div>
);
}, [labelCls, descriptionCls, label, description]);
const renderLeftIcon = useMemo(() => {
return (
<div className={iconContainerCls({ class: 'left-0 pl-4' })}>
{cloneIcon(leftIcon, { className: iconCls(), 'aria-hidden': true })}
</div>
);
}, [cloneIcon, iconCls, iconContainerCls, leftIcon]);
const renderRightIcon = useMemo(() => {
return (
<div className={iconContainerCls({ class: 'pr-4 right-0' })}>
{cloneIcon(rightIcon, { className: iconCls(), 'aria-hidden': true })}
</div>
);
}, [cloneIcon, iconCls, iconContainerCls, rightIcon]);
const renderHelperText = useMemo(() => {
if (!helperText) return null;
return (
<div className={helperTextCls()}>
{state &&
cloneIcon(<WarningIcon className={helperIconCls()} />, {
'aria-hidden': true,
})}
<p>{helperText}</p>
</div>
);
}, [cloneIcon, state, helperIconCls, helperText, helperTextCls]);
const renderLabels = useMemo(() => {
if (!label && !description) return null;
return ( return (
<div className="flex flex-col gap-y-1"> <div className="flex flex-col gap-y-2 w-full">
<p className={labelCls()}>{label}</p> {renderLabels}
<p className={descriptionCls()}>{description}</p> <div className={containerCls({ class: className })}>
{leftIcon && renderLeftIcon}
<input
{...(register ? register : {})}
className={cn(inputCls(), {
'pl-10': leftIcon,
})}
{...props}
ref={ref}
/>
{rightIcon && renderRightIcon}
</div>
{renderHelperText}
</div> </div>
); );
}, [labelCls, descriptionCls, label, description]); },
);
const renderLeftIcon = useMemo(() => { Input.displayName = 'Input';
return (
<div className={iconContainerCls({ class: 'left-0 pl-4' })}>
{cloneIcon(leftIcon, { className: iconCls(), 'aria-hidden': true })}
</div>
);
}, [cloneIcon, iconCls, iconContainerCls, leftIcon]);
const renderRightIcon = useMemo(() => { export { Input };
return (
<div className={iconContainerCls({ class: 'pr-4 right-0' })}>
{cloneIcon(rightIcon, { className: iconCls(), 'aria-hidden': true })}
</div>
);
}, [cloneIcon, iconCls, iconContainerCls, rightIcon]);
const renderHelperText = useMemo(() => {
if (!helperText) return null;
return (
<div className={helperTextCls()}>
{state &&
cloneIcon(<WarningIcon className={helperIconCls()} />, {
'aria-hidden': true,
})}
<p>{helperText}</p>
</div>
);
}, [cloneIcon, state, helperIconCls, helperText, helperTextCls]);
return (
<div className="flex flex-col gap-y-2 w-full">
{renderLabels}
<div className={containerCls({ class: className })}>
{leftIcon && renderLeftIcon}
<input
className={cn(inputCls(), {
'pl-10': leftIcon,
})}
{...props}
/>
{rightIcon && renderRightIcon}
</div>
{renderHelperText}
</div>
);
};

View File

@ -0,0 +1,51 @@
import React from 'react';
const Header: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<thead className="text-left">{children}</thead>
);
const Body: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<tbody className="text-left">{children}</tbody>
);
const Row: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<tr className="text-left">{children}</tr>
);
const ColumnHeaderCell: React.FC<{ children: React.ReactNode }> = ({
children,
}) => (
<th className="px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider">
{children}
</th>
);
const RowHeaderCell: React.FC<{ children: React.ReactNode }> = ({
children,
}) => (
<th
scope="row"
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"
>
{children}
</th>
);
const Cell: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{children}
</td>
);
const Table: React.FC<{ children: React.ReactNode }> & {
Header: typeof Header;
Body: typeof Body;
Row: typeof Row;
ColumnHeaderCell: typeof ColumnHeaderCell;
RowHeaderCell: typeof RowHeaderCell;
Cell: typeof Cell;
} = ({ children }) => <table className="min-w-full">{children}</table>;
Table.Header = Header;
Table.Body = Body;
Table.Row = Row;
Table.ColumnHeaderCell = ColumnHeaderCell;
Table.RowHeaderCell = RowHeaderCell;
Table.Cell = Cell;
export { Table };

View File

@ -0,0 +1 @@
export * from './Table';

View File

@ -1,12 +1,13 @@
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { import {
Typography,
Alert, Alert,
Button, Button,
} from '@snowballtools/material-tailwind-react-fork'; } from '@snowballtools/material-tailwind-react-fork';
import { useGQLClient } from '../../../../../../../context/GQLClientContext'; import { useGQLClient } from '../../../../../../../context/GQLClientContext';
import { Heading } from 'components/shared/Heading';
import { Table } from 'components/shared/Table';
const Config = () => { const Config = () => {
const { id, orgSlug } = useParams(); const { id, orgSlug } = useParams();
@ -38,37 +39,44 @@ const Config = () => {
} }
}; };
// TODO: Figure out DNS Provider if possible and update appropriatly
return ( return (
<div className="flex flex-col gap-6 w-full"> <div className="flex flex-col gap-6 w-full">
<div> <div>
<Typography variant="h5">Configure DNS</Typography> <Heading className="text-sky-950 text-lg font-medium leading-normal">
<Typography variant="small"> Setup domain name
</Heading>
<p className="text-blue-gray-500">
Add the following records to your domain.&nbsp; Add the following records to your domain.&nbsp;
<a href="https://www.namecheap.com/" target="_blank" rel="noreferrer"> <a href="https://www.namecheap.com/" target="_blank" rel="noreferrer">
<span className="underline">Go to NameCheap</span> ^ <span className="underline">Go to NameCheap</span>
</a> </a>
</Typography> </p>
</div> </div>
<table className="rounded-lg w-3/4 text-blue-gray-600"> <Table>
<tbody> <Table.Header>
<tr className="border-b-2 border-gray-300"> <Table.Row>
<th className="text-left p-2">Type</th> <Table.ColumnHeaderCell>Type</Table.ColumnHeaderCell>
<th className="text-left p-2">Name</th> <Table.ColumnHeaderCell>Host</Table.ColumnHeaderCell>
<th className="text-left p-2">Value</th> <Table.ColumnHeaderCell>Value</Table.ColumnHeaderCell>
</tr> </Table.Row>
<tr className="border-b-2 border-gray-300"> </Table.Header>
<td className="text-left p-2">A</td>
<td className="text-left p-2">@</td> <Table.Body>
<td className="text-left p-2">56.49.19.21</td> <Table.Row>
</tr> <Table.RowHeaderCell>A</Table.RowHeaderCell>
<tr> <Table.Cell>@</Table.Cell>
<td className="text-left p-2">CNAME</td> <Table.Cell>56.49.19.21</Table.Cell>
<td className="text-left p-2">www</td> </Table.Row>
<td className="text-left p-2">cname.snowballtools.xyz</td>
</tr> <Table.Row>
</tbody> <Table.RowHeaderCell>CNAME</Table.RowHeaderCell>
</table> <Table.Cell>www</Table.Cell>
<Table.Cell>cname.snowballtools.xyz</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
<Alert color="blue"> <Alert color="blue">
<i>^</i>It can take up to 48 hours for these updates to reflect <i>^</i>It can take up to 48 hours for these updates to reflect

View File

@ -0,0 +1,48 @@
import { StoryObj, Meta } from '@storybook/react';
import { Table } from 'components/shared/Table';
const meta: Meta<typeof Table> = {
title: 'Components/Table',
component: Table,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof Table>;
export const Default: Story = {
render: ({}) => (
<Table>
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell>Full name</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell>Email</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell>Group</Table.ColumnHeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.RowHeaderCell>Danilo Sousa</Table.RowHeaderCell>
<Table.Cell>danilo@example.com</Table.Cell>
<Table.Cell>Developer</Table.Cell>
</Table.Row>
<Table.Row>
<Table.RowHeaderCell>Zahra Ambessa</Table.RowHeaderCell>
<Table.Cell>zahra@example.com</Table.Cell>
<Table.Cell>Admin</Table.Cell>
</Table.Row>
<Table.Row>
<Table.RowHeaderCell>Jasper Eriksson</Table.RowHeaderCell>
<Table.Cell>jasper@example.com</Table.Cell>
<Table.Cell>Developer</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
),
args: {},
};