diff --git a/.changeset/tonic-ui-form-control.md b/.changeset/tonic-ui-form-control.md
new file mode 100644
index 0000000000..43def8b35d
--- /dev/null
+++ b/.changeset/tonic-ui-form-control.md
@@ -0,0 +1,5 @@
+---
+"@tonic-ui/react": minor
+---
+
+feat: Add `FormControl` components with comprehensive accessibility support
diff --git a/packages/react-docs/config/sidebar-routes.js b/packages/react-docs/config/sidebar-routes.js
index b3b9bec244..fd837de584 100644
--- a/packages/react-docs/config/sidebar-routes.js
+++ b/packages/react-docs/config/sidebar-routes.js
@@ -89,7 +89,6 @@ export const routes = [
{ title: 'Getting started', path: 'experiments' },
{ title: 'FORM CONTROLS', heading: true },
- { title: 'FormControl', path: 'experiments/form-control' },
{ title: 'ButtonBox', path: 'experiments/button-box' },
{ title: 'Dropdown', path: 'experiments/dropdown' },
{ title: 'DropdownBase', path: 'experiments/dropdown-base' },
@@ -222,6 +221,7 @@ export const routes = [
},
},
{ title: 'CheckboxGroup', path: 'components/checkbox-group' },
+ { title: 'FormControl', path: 'components/form-control' },
{
title: 'Input',
path: 'components/input',
diff --git a/packages/react-docs/experiments/form-control/FormCharacterCount.js b/packages/react-docs/experiments/form-control/FormCharacterCount.js
deleted file mode 100644
index 05c3881f8e..0000000000
--- a/packages/react-docs/experiments/form-control/FormCharacterCount.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Text, Flex } from '@tonic-ui/react';
-import { useId } from '@tonic-ui/react-hooks';
-import { ensureString } from 'ensure-type';
-import React, { forwardRef } from 'react';
-import useFormControl from './useFormControl';
-import {
- useFormCharacterCountColors,
- useFormCharacterCountStyle,
-} from './styles';
-
-const FormCharacterCount = forwardRef((
- {
- value = '',
- max = 0,
- ...rest
- },
- ref
-) => {
- const defaultId = useId();
- const { formCharacterCountId } = useFormControl() ?? {};
- const id = formCharacterCountId ?? defaultId;
- const characterCount = ensureString(value).length;
- const isOverLimit = characterCount > max;
- const { lengthColor, maxColor } = useFormCharacterCountColors({
- isOverLimit,
- });
- const styleProps = useFormCharacterCountStyle();
-
- return (
-
- {characterCount}
- /{max}
-
- );
-});
-
-FormCharacterCount.displayName = 'FormCharacterCount';
-
-export default FormCharacterCount;
diff --git a/packages/react-docs/experiments/form-control/FormControl.js b/packages/react-docs/experiments/form-control/FormControl.js
deleted file mode 100644
index ceda9015c3..0000000000
--- a/packages/react-docs/experiments/form-control/FormControl.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { Stack } from '@tonic-ui/react';
-import { useId } from '@tonic-ui/react-hooks';
-import React, { forwardRef, useMemo } from 'react';
-import { FormControlContext } from './context';
-import { useFormControlStyle } from './styles';
-
-const FormControl = forwardRef((
- {
- children,
- disabled = false,
- error = false,
- readOnly = false,
- orientation = 'vertical',
- ...rest
- },
- ref
-) => {
- const styleProps = useFormControlStyle({ orientation });
- const formCharacterCountId = useId();
- const formErrorMessageId = useId();
- const formHelperTextId = useId();
- const formInputId = useId();
-
- const contextValue = useMemo(() => ({
- disabled,
- error,
- readOnly,
- formCharacterCountId,
- formErrorMessageId,
- formHelperTextId,
- formInputId,
- orientation,
- }), [
- disabled,
- error,
- readOnly,
- formCharacterCountId,
- formErrorMessageId,
- formHelperTextId,
- formInputId,
- orientation,
- ]);
-
- return (
-
-
- {children}
-
-
- );
-});
-
-FormControl.displayName = 'FormControl';
-
-export default FormControl;
diff --git a/packages/react-docs/experiments/form-control/FormInput.js b/packages/react-docs/experiments/form-control/FormInput.js
deleted file mode 100644
index d9bd4da30d..0000000000
--- a/packages/react-docs/experiments/form-control/FormInput.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { Input } from '@tonic-ui/react';
-import { useId } from '@tonic-ui/react-hooks';
-import { ariaAttr } from '@tonic-ui/utils';
-import React, { forwardRef } from 'react';
-import useFormControl from './useFormControl';
-
-const FormInput = forwardRef((props, ref) => {
- const defaultId = useId();
- const {
- disabled,
- error,
- readOnly,
-
- // IDs for associated form control elements
- formInputId,
- formErrorMessageId,
- formHelperTextId,
- formCharacterCountId,
- } = useFormControl() ?? {};
- const id = formInputId ?? defaultId;
- const describedByIds = [];
- if (formErrorMessageId) {
- describedByIds.push(formErrorMessageId);
- }
- if (formHelperTextId) {
- describedByIds.push(formHelperTextId);
- }
- if (formCharacterCountId) {
- describedByIds.push(formCharacterCountId);
- }
-
- return (
-
- );
-});
-
-FormInput.displayName = 'FormInput';
-
-export default FormInput;
diff --git a/packages/react-docs/experiments/form-control/index.js b/packages/react-docs/experiments/form-control/index.js
deleted file mode 100644
index 5e9bd43b7d..0000000000
--- a/packages/react-docs/experiments/form-control/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import FormCharacterCount from './FormCharacterCount';
-import FormControl from './FormControl';
-import FormErrorMessage from './FormErrorMessage';
-import FormHelperText from './FormHelperText';
-import FormInput from './FormInput';
-import FormLabel from './FormLabel';
-import useFormControl from './useFormControl';
-
-export {
- FormCharacterCount,
- FormControl,
- FormErrorMessage,
- FormHelperText,
- FormInput,
- FormLabel,
- useFormControl,
-};
diff --git a/packages/react-docs/pages/components/form-control/basic.js b/packages/react-docs/pages/components/form-control/basic.js
new file mode 100644
index 0000000000..464722bd16
--- /dev/null
+++ b/packages/react-docs/pages/components/form-control/basic.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import {
+ FormControl,
+ FormLabel,
+ FormInput,
+ FormHelperText,
+} from '@tonic-ui/react';
+
+const App = () => {
+ return (
+
+ Username
+
+
+ Choose a unique username that others will see
+
+
+ );
+};
+
+export default App;
diff --git a/packages/react-docs/pages/components/form-control/character-count.js b/packages/react-docs/pages/components/form-control/character-count.js
new file mode 100644
index 0000000000..4e0b87db3c
--- /dev/null
+++ b/packages/react-docs/pages/components/form-control/character-count.js
@@ -0,0 +1,26 @@
+import React, { useState } from 'react';
+import {
+ FormControl,
+ FormLabel,
+ FormInput,
+ FormCharacterCount,
+} from '@tonic-ui/react';
+
+const App = () => {
+ const [bio, setBio] = useState('');
+ const maxChars = 50;
+
+ return (
+
+ Bio
+ setBio(e.target.value)}
+ />
+
+
+ );
+};
+
+export default App;
diff --git a/packages/react-docs/pages/experiments/form-control/advanced-setup.js b/packages/react-docs/pages/components/form-control/complex-form.js
similarity index 76%
rename from packages/react-docs/pages/experiments/form-control/advanced-setup.js
rename to packages/react-docs/pages/components/form-control/complex-form.js
index 82960bb7d9..9bd8d7c8d1 100644
--- a/packages/react-docs/pages/experiments/form-control/advanced-setup.js
+++ b/packages/react-docs/pages/components/form-control/complex-form.js
@@ -1,18 +1,26 @@
import React, { useState } from 'react';
import { ensureArray } from 'ensure-type';
-import { Box, Stack, Button, Text } from '@tonic-ui/react';
import {
+ Box,
+ Stack,
+ Button,
+ Text,
+ Flex,
FormControl,
- FormInput,
FormLabel,
+ FormInput,
+ FormTextarea,
FormErrorMessage,
FormHelperText,
-} from '@/experiments/form-control';
+ FormCharacterCount,
+} from '@tonic-ui/react';
const App = () => {
const [formData, setFormData] = useState({
email: '',
password: '',
+ country: '',
+ bio: '',
});
const [errors, setErrors] = useState({});
@@ -39,6 +47,19 @@ const App = () => {
}
return passwordErrors;
}
+ case 'country': {
+ return !value ? 'Please select your experience level' : '';
+ }
+ case 'bio': {
+ const bioErrors = [];
+ if (value.length < 10) {
+ bioErrors.push('Bio must be at least 10 characters');
+ }
+ if (value.length > 200) {
+ bioErrors.push('Bio must not exceed 200 characters');
+ }
+ return bioErrors;
+ }
default:
return '';
}
@@ -135,6 +156,26 @@ const App = () => {
+ {/* Bio textarea with character validation */}
+
+ Bio
+
+
+
+
+ Write a brief bio (10-200 characters)
+
+
+
+
+
{/* Submit Button */}