Skip to content

Commit

Permalink
[Field] Create new Field components (#477)
Browse files Browse the repository at this point in the history
  • Loading branch information
atomiks authored Aug 19, 2024
1 parent 6dc925b commit c51fb49
Show file tree
Hide file tree
Showing 135 changed files with 5,048 additions and 193 deletions.
149 changes: 149 additions & 0 deletions docs/data/base/components/field/UnstyledFieldAsync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import * as React from 'react';
import * as Field from '@base_ui/react/Field';
import { styled } from '@mui/system';

const cache = new Map();

function checkAvailability(name) {
const takenNames = ['admin', 'root', 'superuser'];
return new Promise((resolve) => {
setTimeout(() => {
const result = takenNames.includes(name) ? 'Name taken' : null;
cache.set(name, result);
resolve(result);
}, 500);
});
}

export default function UnstyledFieldAsync() {
const [loading, setLoading] = React.useState(false);

async function handleValidate(value) {
const name = value;

if (name === '') {
return null;
}

const isCached = cache.has(name);
if (isCached) {
return cache.get(name);
}

setLoading(true);

try {
const error = await checkAvailability(name);
setLoading(false);
return error;
} catch (e) {
setLoading(false);
return 'Failed to fetch name availability';
}
}

return (
<div>
<h3>Handle availability checker</h3>
<FieldRoot
validate={handleValidate}
validateOnChange
validateDebounceTime={500}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Field.Label>@</Field.Label>
<Field.Validity>
{(state) => (
<FieldControl
data-pending={state.value === '' || loading || undefined}
/>
)}
</Field.Validity>
</div>
<Field.Validity>
{(state) => {
if (loading) {
return <FieldDescription>Checking availability...</FieldDescription>;
}

if (state.value === '') {
return <FieldDescription>Enter a name</FieldDescription>;
}

if (!state.validity.customError) {
return (
<FieldDescription
data-type="success"
role="alert"
aria-live="polite"
>
<strong>@{state.value}</strong> is available
</FieldDescription>
);
}

return <FieldError show="customError" />;
}}
</Field.Validity>
</FieldRoot>
</div>
);
}

const FieldRoot = styled(Field.Root)`
width: 275px;
`;

const FieldControl = styled(Field.Control)`
border: 1px solid #ccc;
border-radius: 4px;
width: 100%;
padding: 6px;
font-size: 100%;
&[data-field='invalid']:not([data-pending]) {
border-color: red;
background-color: rgb(255 0 0 / 0.1);
}
&[data-field='valid']:not([data-pending]) {
border-color: green;
background-color: rgb(0 255 0 / 0.1);
}
&:focus {
outline: 0;
border-color: #0078d4;
box-shadow: 0 0 0 3px rgba(0 100 255 / 0.3);
&[data-field='invalid']:not([data-pending]) {
border-color: red;
box-shadow: 0 0 0 3px rgba(255 0 0 / 0.3);
}
&[data-field='valid']:not([data-pending]) {
box-shadow: 0 0 0 3px rgba(100 200 100 / 0.3);
}
}
`;

const FieldDescription = styled(Field.Description)`
font-size: 90%;
margin: 0;
margin-top: 4px;
line-height: 1.1;
color: grey;
&[data-type='success'] {
color: green;
}
`;

const FieldError = styled(Field.Error)`
display: block;
font-size: 90%;
margin: 0;
margin-top: 4px;
line-height: 1.1;
color: red;
`;
149 changes: 149 additions & 0 deletions docs/data/base/components/field/UnstyledFieldAsync.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import * as React from 'react';
import * as Field from '@base_ui/react/Field';
import { styled } from '@mui/system';

const cache = new Map<string, string | null>();

function checkAvailability(name: string) {
const takenNames = ['admin', 'root', 'superuser'];
return new Promise<string | null>((resolve) => {
setTimeout(() => {
const result = takenNames.includes(name) ? 'Name taken' : null;
cache.set(name, result);
resolve(result);
}, 500);
});
}

export default function UnstyledFieldAsync() {
const [loading, setLoading] = React.useState(false);

async function handleValidate(value: unknown) {
const name = value as string;

if (name === '') {
return null;
}

const isCached = cache.has(name);
if (isCached) {
return cache.get(name) as string | null;
}

setLoading(true);

try {
const error = await checkAvailability(name);
setLoading(false);
return error;
} catch (e) {
setLoading(false);
return 'Failed to fetch name availability';
}
}

return (
<div>
<h3>Handle availability checker</h3>
<FieldRoot
validate={handleValidate}
validateOnChange
validateDebounceTime={500}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Field.Label>@</Field.Label>
<Field.Validity>
{(state) => (
<FieldControl
data-pending={state.value === '' || loading || undefined}
/>
)}
</Field.Validity>
</div>
<Field.Validity>
{(state) => {
if (loading) {
return <FieldDescription>Checking availability...</FieldDescription>;
}

if (state.value === '') {
return <FieldDescription>Enter a name</FieldDescription>;
}

if (!state.validity.customError) {
return (
<FieldDescription
data-type="success"
role="alert"
aria-live="polite"
>
<strong>@{state.value as string}</strong> is available
</FieldDescription>
);
}

return <FieldError show="customError" />;
}}
</Field.Validity>
</FieldRoot>
</div>
);
}

const FieldRoot = styled(Field.Root)`
width: 275px;
`;

const FieldControl = styled(Field.Control)`
border: 1px solid #ccc;
border-radius: 4px;
width: 100%;
padding: 6px;
font-size: 100%;
&[data-field='invalid']:not([data-pending]) {
border-color: red;
background-color: rgb(255 0 0 / 0.1);
}
&[data-field='valid']:not([data-pending]) {
border-color: green;
background-color: rgb(0 255 0 / 0.1);
}
&:focus {
outline: 0;
border-color: #0078d4;
box-shadow: 0 0 0 3px rgba(0 100 255 / 0.3);
&[data-field='invalid']:not([data-pending]) {
border-color: red;
box-shadow: 0 0 0 3px rgba(255 0 0 / 0.3);
}
&[data-field='valid']:not([data-pending]) {
box-shadow: 0 0 0 3px rgba(100 200 100 / 0.3);
}
}
`;

const FieldDescription = styled(Field.Description)`
font-size: 90%;
margin: 0;
margin-top: 4px;
line-height: 1.1;
color: grey;
&[data-type='success'] {
color: green;
}
`;

const FieldError = styled(Field.Error)`
display: block;
font-size: 90%;
margin: 0;
margin-top: 4px;
line-height: 1.1;
color: red;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as React from 'react';
import * as Field from '@base_ui/react/Field';
import { styled } from '@mui/system';

export default function UnstyledFieldIntroduction() {
return (
<FieldRoot validate={(value) => (value === 'admin' ? 'Name not allowed' : null)}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Field.Label>Name</Field.Label>
<FieldControl required pattern="[a-zA-Z0-9]+" />
</div>
<Field.Validity>
{({ validity, value }) => {
if (
validity.valueMissing ||
validity.patternMismatch ||
value === 'admin'
) {
return null;
}

return (
<FieldDescription>
Your name will be visible on your profile.
</FieldDescription>
);
}}
</Field.Validity>
<FieldError show="customError" />
<FieldError show="valueMissing" />
<FieldError show="patternMismatch">
Only alphanumeric characters are allowed (a-z, A-Z, 0-9).
</FieldError>
</FieldRoot>
);
}

const FieldRoot = styled(Field.Root)`
width: 275px;
`;

const FieldControl = styled(Field.Control)`
border: 1px solid #ccc;
border-radius: 4px;
width: 100%;
padding: 6px;
font-size: 100%;
&[data-field='invalid'] {
border-color: red;
background-color: rgb(255 0 0 / 0.1);
}
&:focus {
outline: 0;
border-color: #0078d4;
box-shadow: 0 0 0 3px rgba(0 100 255 / 0.3);
&[data-field='invalid'] {
border-color: red;
box-shadow: 0 0 0 3px rgba(255 0 0 / 0.3);
}
}
`;

const FieldDescription = styled(Field.Description)`
font-size: 90%;
margin-bottom: 0;
margin-top: 4px;
line-height: 1.1;
color: grey;
&[data-error] {
color: red;
}
`;

const FieldError = styled(Field.Error)`
display: block;
font-size: 90%;
margin: 0;
margin-bottom: 0;
margin-top: 4px;
line-height: 1.1;
color: red;
`;
Loading

0 comments on commit c51fb49

Please sign in to comment.