From 72ef9e1701e312e6278cd7bdbb8ee62400614634 Mon Sep 17 00:00:00 2001 From: atomiks Date: Mon, 15 Jul 2024 14:22:57 +1000 Subject: [PATCH 001/108] [Field] Create new Field components --- .../UnstyledFieldIntroduction/css/index.js | 137 ++++++++++++++++++ .../UnstyledFieldIntroduction/css/index.tsx | 137 ++++++++++++++++++ .../UnstyledFieldIntroduction/system/index.js | 87 +++++++++++ .../system/index.tsx | 87 +++++++++++ .../system/index.tsx.preview | 13 ++ .../tailwind/index.js | 96 ++++++++++++ .../tailwind/index.tsx | 85 +++++++++++ .../tailwind/index.tsx.preview | 16 ++ docs/data/base/components/field/field.md | 17 +++ docs/data/base/components/label/label.md | 11 ++ docs/data/base/pagesApi.js | 19 +++ docs/pages/base-ui/api/checkbox-root.json | 1 + docs/pages/base-ui/api/field-description.json | 19 +++ docs/pages/base-ui/api/field-label.json | 17 +++ docs/pages/base-ui/api/field-root.json | 17 +++ docs/pages/base-ui/api/label.json | 14 ++ docs/pages/base-ui/api/use-checkbox-root.json | 9 ++ .../base-ui/api/use-field-description.json | 8 + docs/pages/base-ui/api/use-field-label.json | 8 + docs/pages/base-ui/api/use-label.json | 8 + .../base-ui/react-field/[docsTab]/index.js | 64 ++++++++ docs/pages/base-ui/react-field/index.js | 13 ++ .../base-ui/react-label/[docsTab]/index.js | 40 +++++ docs/pages/base-ui/react-label/index.js | 13 ++ .../api-docs/checkbox-root/checkbox-root.json | 1 + .../field-description/field-description.json | 10 ++ .../api-docs/field-label/field-label.json | 10 ++ .../api-docs/field-root/field-root.json | 10 ++ docs/translations/api-docs/label/label.json | 10 ++ .../use-checkbox-root/use-checkbox-root.json | 5 +- .../use-field-description.json | 1 + .../use-field-label/use-field-label.json | 1 + .../api-docs/use-label/use-label.json | 1 + .../src/Checkbox/Root/CheckboxRoot.tsx | 13 +- .../src/Checkbox/Root/CheckboxRoot.types.ts | 14 +- .../src/Checkbox/Root/useCheckboxRoot.ts | 25 +++- .../Description/FieldDescription.test.tsx | 16 ++ .../Field/Description/FieldDescription.tsx | 64 ++++++++ .../Description/FieldDescription.types.tsx | 6 + .../Field/Description/useFieldDescription.ts | 42 ++++++ .../src/Field/Label/FieldLabel.test.tsx | 16 ++ .../mui-base/src/Field/Label/FieldLabel.tsx | 60 ++++++++ .../src/Field/Label/FieldLabel.types.ts | 5 + .../mui-base/src/Field/Label/useFieldLabel.ts | 32 ++++ .../src/Field/Root/FieldRoot.test.tsx | 14 ++ .../mui-base/src/Field/Root/FieldRoot.tsx | 69 +++++++++ .../src/Field/Root/FieldRoot.types.ts | 13 ++ .../src/Field/Root/FieldRootContext.ts | 13 ++ packages/mui-base/src/Field/index.barrel.ts | 7 + packages/mui-base/src/Field/index.ts | 16 ++ packages/mui-base/src/Label/Label.tsx | 46 ++++++ packages/mui-base/src/Label/Label.types.ts | 5 + packages/mui-base/src/Label/index.barrel.ts | 3 + packages/mui-base/src/Label/index.ts | 3 + packages/mui-base/src/Label/useLabel.ts | 26 ++++ .../src/utils/defaultRenderFunctions.tsx | 4 + 56 files changed, 1490 insertions(+), 7 deletions(-) create mode 100644 docs/data/base/components/field/UnstyledFieldIntroduction/css/index.js create mode 100644 docs/data/base/components/field/UnstyledFieldIntroduction/css/index.tsx create mode 100644 docs/data/base/components/field/UnstyledFieldIntroduction/system/index.js create mode 100644 docs/data/base/components/field/UnstyledFieldIntroduction/system/index.tsx create mode 100644 docs/data/base/components/field/UnstyledFieldIntroduction/system/index.tsx.preview create mode 100644 docs/data/base/components/field/UnstyledFieldIntroduction/tailwind/index.js create mode 100644 docs/data/base/components/field/UnstyledFieldIntroduction/tailwind/index.tsx create mode 100644 docs/data/base/components/field/UnstyledFieldIntroduction/tailwind/index.tsx.preview create mode 100644 docs/data/base/components/field/field.md create mode 100644 docs/data/base/components/label/label.md create mode 100644 docs/pages/base-ui/api/field-description.json create mode 100644 docs/pages/base-ui/api/field-label.json create mode 100644 docs/pages/base-ui/api/field-root.json create mode 100644 docs/pages/base-ui/api/label.json create mode 100644 docs/pages/base-ui/api/use-field-description.json create mode 100644 docs/pages/base-ui/api/use-field-label.json create mode 100644 docs/pages/base-ui/api/use-label.json create mode 100644 docs/pages/base-ui/react-field/[docsTab]/index.js create mode 100644 docs/pages/base-ui/react-field/index.js create mode 100644 docs/pages/base-ui/react-label/[docsTab]/index.js create mode 100644 docs/pages/base-ui/react-label/index.js create mode 100644 docs/translations/api-docs/field-description/field-description.json create mode 100644 docs/translations/api-docs/field-label/field-label.json create mode 100644 docs/translations/api-docs/field-root/field-root.json create mode 100644 docs/translations/api-docs/label/label.json create mode 100644 docs/translations/api-docs/use-field-description/use-field-description.json create mode 100644 docs/translations/api-docs/use-field-label/use-field-label.json create mode 100644 docs/translations/api-docs/use-label/use-label.json create mode 100644 packages/mui-base/src/Field/Description/FieldDescription.test.tsx create mode 100644 packages/mui-base/src/Field/Description/FieldDescription.tsx create mode 100644 packages/mui-base/src/Field/Description/FieldDescription.types.tsx create mode 100644 packages/mui-base/src/Field/Description/useFieldDescription.ts create mode 100644 packages/mui-base/src/Field/Label/FieldLabel.test.tsx create mode 100644 packages/mui-base/src/Field/Label/FieldLabel.tsx create mode 100644 packages/mui-base/src/Field/Label/FieldLabel.types.ts create mode 100644 packages/mui-base/src/Field/Label/useFieldLabel.ts create mode 100644 packages/mui-base/src/Field/Root/FieldRoot.test.tsx create mode 100644 packages/mui-base/src/Field/Root/FieldRoot.tsx create mode 100644 packages/mui-base/src/Field/Root/FieldRoot.types.ts create mode 100644 packages/mui-base/src/Field/Root/FieldRootContext.ts create mode 100644 packages/mui-base/src/Field/index.barrel.ts create mode 100644 packages/mui-base/src/Field/index.ts create mode 100644 packages/mui-base/src/Label/Label.tsx create mode 100644 packages/mui-base/src/Label/Label.types.ts create mode 100644 packages/mui-base/src/Label/index.barrel.ts create mode 100644 packages/mui-base/src/Label/index.ts create mode 100644 packages/mui-base/src/Label/useLabel.ts diff --git a/docs/data/base/components/field/UnstyledFieldIntroduction/css/index.js b/docs/data/base/components/field/UnstyledFieldIntroduction/css/index.js new file mode 100644 index 000000000..cd38cbc76 --- /dev/null +++ b/docs/data/base/components/field/UnstyledFieldIntroduction/css/index.js @@ -0,0 +1,137 @@ +import * as React from 'react'; +import * as Checkbox from '@base_ui/react/Checkbox'; +import { useTheme } from '@mui/system'; +import Check from '@mui/icons-material/Check'; + +export default function UnstyledCheckboxIntroduction() { + return ( +
+ + + + + + + + + + + + + + + + + + + + + +
+ ); +} + +const grey = { + 100: '#E5EAF2', + 300: '#C7D0DD', + 500: '#9DA8B7', + 600: '#6B7A90', + 800: '#303740', + 900: '#1C2025', +}; + +function useIsDarkMode() { + const theme = useTheme(); + return theme.palette.mode === 'dark'; +} + +function Styles() { + // Replace this with your app logic for determining dark mode + const isDarkMode = useIsDarkMode(); + + return ( + + ); +} diff --git a/docs/data/base/components/field/UnstyledFieldIntroduction/css/index.tsx b/docs/data/base/components/field/UnstyledFieldIntroduction/css/index.tsx new file mode 100644 index 000000000..cd38cbc76 --- /dev/null +++ b/docs/data/base/components/field/UnstyledFieldIntroduction/css/index.tsx @@ -0,0 +1,137 @@ +import * as React from 'react'; +import * as Checkbox from '@base_ui/react/Checkbox'; +import { useTheme } from '@mui/system'; +import Check from '@mui/icons-material/Check'; + +export default function UnstyledCheckboxIntroduction() { + return ( +
+ + + + + + + + + + + + + + + + + + + + + +
+ ); +} + +const grey = { + 100: '#E5EAF2', + 300: '#C7D0DD', + 500: '#9DA8B7', + 600: '#6B7A90', + 800: '#303740', + 900: '#1C2025', +}; + +function useIsDarkMode() { + const theme = useTheme(); + return theme.palette.mode === 'dark'; +} + +function Styles() { + // Replace this with your app logic for determining dark mode + const isDarkMode = useIsDarkMode(); + + return ( + + ); +} diff --git a/docs/data/base/components/field/UnstyledFieldIntroduction/system/index.js b/docs/data/base/components/field/UnstyledFieldIntroduction/system/index.js new file mode 100644 index 000000000..897da19d9 --- /dev/null +++ b/docs/data/base/components/field/UnstyledFieldIntroduction/system/index.js @@ -0,0 +1,87 @@ +import * as React from 'react'; +import { styled } from '@mui/system'; +import * as Field from '@base_ui/react/Field'; +import * as BaseCheckbox from '@base_ui/react/Checkbox'; +import Check from '@mui/icons-material/Check'; + +export default function UnstyledSwitchIntroduction() { + return ( + +
+ + + + + + Accept Terms and Conditions +
+ + In order to proceed, you must accept the Terms and Conditions. + +
+ ); +} + +const grey = { + 100: '#E5EAF2', + 600: '#4F5665', +}; + +const blue = { + 400: '#3399FF', + 600: '#0072E6', + 800: '#004C99', +}; + +const Checkbox = styled(BaseCheckbox.Root)( + ({ theme }) => ` + width: 24px; + height: 24px; + padding: 0; + border-radius: 4px; + border: 2px solid ${blue[600]}; + background: none; + transition-property: background, border-color; + transition-duration: 0.15s; + outline: none; + + &[data-disabled] { + opacity: 0.4; + cursor: not-allowed; + } + + &:focus-visible { + outline: 2px solid ${theme.palette.mode === 'dark' ? blue[800] : blue[400]}; + outline-offset: 2px; + } + + &[data-state="checked"], &[data-state="mixed"] { + border-color: transparent; + background: ${blue[600]}; + } + `, +); + +const CheckIcon = styled(Check)` + width: 100%; + height: 100%; +`; + +const Indicator = styled(BaseCheckbox.Indicator)` + color: ${grey[100]}; + height: 100%; + display: inline-block; + visibility: hidden; + + &[data-state='checked'], + &[data-state='mixed'] { + visibility: visible; + } +`; + +export const FieldLabel = styled(Field.Label)``; + +export const FieldDescription = styled(Field.Description)` + font-size: 90%; + color: ${grey[600]}; +`; diff --git a/docs/data/base/components/field/UnstyledFieldIntroduction/system/index.tsx b/docs/data/base/components/field/UnstyledFieldIntroduction/system/index.tsx new file mode 100644 index 000000000..897da19d9 --- /dev/null +++ b/docs/data/base/components/field/UnstyledFieldIntroduction/system/index.tsx @@ -0,0 +1,87 @@ +import * as React from 'react'; +import { styled } from '@mui/system'; +import * as Field from '@base_ui/react/Field'; +import * as BaseCheckbox from '@base_ui/react/Checkbox'; +import Check from '@mui/icons-material/Check'; + +export default function UnstyledSwitchIntroduction() { + return ( + +
+ + + + + + Accept Terms and Conditions +
+ + In order to proceed, you must accept the Terms and Conditions. + +
+ ); +} + +const grey = { + 100: '#E5EAF2', + 600: '#4F5665', +}; + +const blue = { + 400: '#3399FF', + 600: '#0072E6', + 800: '#004C99', +}; + +const Checkbox = styled(BaseCheckbox.Root)( + ({ theme }) => ` + width: 24px; + height: 24px; + padding: 0; + border-radius: 4px; + border: 2px solid ${blue[600]}; + background: none; + transition-property: background, border-color; + transition-duration: 0.15s; + outline: none; + + &[data-disabled] { + opacity: 0.4; + cursor: not-allowed; + } + + &:focus-visible { + outline: 2px solid ${theme.palette.mode === 'dark' ? blue[800] : blue[400]}; + outline-offset: 2px; + } + + &[data-state="checked"], &[data-state="mixed"] { + border-color: transparent; + background: ${blue[600]}; + } + `, +); + +const CheckIcon = styled(Check)` + width: 100%; + height: 100%; +`; + +const Indicator = styled(BaseCheckbox.Indicator)` + color: ${grey[100]}; + height: 100%; + display: inline-block; + visibility: hidden; + + &[data-state='checked'], + &[data-state='mixed'] { + visibility: visible; + } +`; + +export const FieldLabel = styled(Field.Label)``; + +export const FieldDescription = styled(Field.Description)` + font-size: 90%; + color: ${grey[600]}; +`; diff --git a/docs/data/base/components/field/UnstyledFieldIntroduction/system/index.tsx.preview b/docs/data/base/components/field/UnstyledFieldIntroduction/system/index.tsx.preview new file mode 100644 index 000000000..c355b3a85 --- /dev/null +++ b/docs/data/base/components/field/UnstyledFieldIntroduction/system/index.tsx.preview @@ -0,0 +1,13 @@ + +
+ + + + + + Accept Terms and Conditions +
+ + In order to proceed, you must accept the Terms and Conditions. + +
\ No newline at end of file diff --git a/docs/data/base/components/field/UnstyledFieldIntroduction/tailwind/index.js b/docs/data/base/components/field/UnstyledFieldIntroduction/tailwind/index.js new file mode 100644 index 000000000..fc2d4bb98 --- /dev/null +++ b/docs/data/base/components/field/UnstyledFieldIntroduction/tailwind/index.js @@ -0,0 +1,96 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import * as BaseCheckbox from '@base_ui/react/Checkbox'; +import { useTheme } from '@mui/system'; +import Check from '@mui/icons-material/Check'; + +function classNames(...classes) { + return classes.filter(Boolean).join(' '); +} + +function useIsDarkMode() { + const theme = useTheme(); + return theme.palette.mode === 'dark'; +} + +export default function UnstyledCheckboxIntroduction() { + // Replace this with your app logic for determining dark mode + const isDarkMode = useIsDarkMode(); + + return ( +
+ + + + + + + + + + + + +
+ ); +} + +const Checkbox = React.forwardRef(function Checkbox(props, ref) { + return ( + + classNames( + 'w-6 h-6 p-0 rounded-md', + 'border-2 border-solid border-purple-500', + 'outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-purple-500 focus-visible:ring-opacity-60', + 'transition-colors duration-150', + state.disabled && 'opacity-40 cursor-not-allowed', + state.checked && 'bg-purple-500', + !state.checked && 'bg-transparent', + typeof props.className === 'function' + ? props.className(state) + : props.className, + ) + } + /> + ); +}); + +Checkbox.propTypes = { + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), +}; + +const Indicator = React.forwardRef(function Indicator(props, ref) { + return ( + + classNames( + 'h-full inline-block invisible data-[state=checked]:visible text-gray-100', + typeof props.className === 'function' + ? props.className(state) + : props.className, + ) + } + > + + + ); +}); + +Indicator.propTypes = { + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), +}; diff --git a/docs/data/base/components/field/UnstyledFieldIntroduction/tailwind/index.tsx b/docs/data/base/components/field/UnstyledFieldIntroduction/tailwind/index.tsx new file mode 100644 index 000000000..5ba7093ea --- /dev/null +++ b/docs/data/base/components/field/UnstyledFieldIntroduction/tailwind/index.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import * as BaseCheckbox from '@base_ui/react/Checkbox'; +import { useTheme } from '@mui/system'; +import Check from '@mui/icons-material/Check'; + +function classNames(...classes: Array) { + return classes.filter(Boolean).join(' '); +} + +function useIsDarkMode() { + const theme = useTheme(); + return theme.palette.mode === 'dark'; +} + +export default function UnstyledCheckboxIntroduction() { + // Replace this with your app logic for determining dark mode + const isDarkMode = useIsDarkMode(); + + return ( +
+ + + + + + + + + + + + +
+ ); +} + +const Checkbox = React.forwardRef( + function Checkbox(props, ref) { + return ( + + classNames( + 'w-6 h-6 p-0 rounded-md', + 'border-2 border-solid border-purple-500', + 'outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-purple-500 focus-visible:ring-opacity-60', + 'transition-colors duration-150', + state.disabled && 'opacity-40 cursor-not-allowed', + state.checked && 'bg-purple-500', + !state.checked && 'bg-transparent', + typeof props.className === 'function' + ? props.className(state) + : props.className, + ) + } + /> + ); + }, +); + +const Indicator = React.forwardRef( + function Indicator(props, ref) { + return ( + + classNames( + 'h-full inline-block invisible data-[state=checked]:visible text-gray-100', + typeof props.className === 'function' + ? props.className(state) + : props.className, + ) + } + > + + + ); + }, +); diff --git a/docs/data/base/components/field/UnstyledFieldIntroduction/tailwind/index.tsx.preview b/docs/data/base/components/field/UnstyledFieldIntroduction/tailwind/index.tsx.preview new file mode 100644 index 000000000..8a53b1cf9 --- /dev/null +++ b/docs/data/base/components/field/UnstyledFieldIntroduction/tailwind/index.tsx.preview @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/data/base/components/field/field.md b/docs/data/base/components/field/field.md new file mode 100644 index 000000000..4229f42f6 --- /dev/null +++ b/docs/data/base/components/field/field.md @@ -0,0 +1,17 @@ +--- +productId: base-ui +title: React Field component and hook +components: FieldRoot, FieldLabel, FieldDescription +githubLabel: 'component: field' +packageName: '@base_ui/react' +--- + +# Field + +

Fields represent an individual section of a form containing an associated control, label, an optional description, and potential error messages.

+ +{{"component": "@mui/docs/ComponentLinkHeader", "design": false}} + +{{"component": "modules/components/ComponentPageTabs.js"}} + +{{"demo": "UnstyledFieldIntroduction", "defaultCodeOpen": false, "bg": "gradient"}} diff --git a/docs/data/base/components/label/label.md b/docs/data/base/components/label/label.md new file mode 100644 index 000000000..f3374e21a --- /dev/null +++ b/docs/data/base/components/label/label.md @@ -0,0 +1,11 @@ +--- +productId: base-ui +title: React Label component and hook +components: Label +githubLabel: 'component: label' +packageName: '@base_ui/react' +--- + +# Label + +

A label labels an element.

diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js index 1f62a5d77..09a047219 100644 --- a/docs/data/base/pagesApi.js +++ b/docs/data/base/pagesApi.js @@ -68,6 +68,18 @@ module.exports = [ pathname: '/base-ui/react-dialog/components-api/#dialog-trigger', title: 'DialogTrigger', }, + { + pathname: '/base-ui/react-field/components-api/#field-description', + title: 'FieldDescription', + }, + { + pathname: '/base-ui/react-field/components-api/#field-label', + title: 'FieldLabel', + }, + { + pathname: '/base-ui/react-field/components-api/#field-root', + title: 'FieldRoot', + }, { pathname: '/base-ui/react-focus-trap/components-api/#focus-trap', title: 'FocusTrap', @@ -77,6 +89,13 @@ module.exports = [ title: 'FormControl', }, { pathname: '/base-ui/react-menu/components-api/#menu-arrow', title: 'MenuArrow' }, + { pathname: '/base-ui/react-input/components-api/#input', title: 'Input' }, + { pathname: '/base-ui/react-label/components-api/#label', title: 'Label' }, + { pathname: '/base-ui/react-menu/components-api/#menu', title: 'Menu' }, + { + pathname: '/base-ui/react-menu/components-api/#menu-button', + title: 'MenuButton', + }, { pathname: '/base-ui/react-menu/components-api/#menu-item', title: 'MenuItem' }, { pathname: '/base-ui/react-menu/components-api/#menu-popup', title: 'MenuPopup' }, { diff --git a/docs/pages/base-ui/api/checkbox-root.json b/docs/pages/base-ui/api/checkbox-root.json index e1571b6be..1c781a51c 100644 --- a/docs/pages/base-ui/api/checkbox-root.json +++ b/docs/pages/base-ui/api/checkbox-root.json @@ -5,6 +5,7 @@ "className": { "type": { "name": "union", "description": "func
| string" } }, "defaultChecked": { "type": { "name": "bool" }, "default": "false" }, "disabled": { "type": { "name": "bool" }, "default": "false" }, + "id": { "type": { "name": "string" } }, "indeterminate": { "type": { "name": "bool" }, "default": "false" }, "inputRef": { "type": { "name": "union", "description": "func
| { current?: object }" } diff --git a/docs/pages/base-ui/api/field-description.json b/docs/pages/base-ui/api/field-description.json new file mode 100644 index 000000000..10f4d3e66 --- /dev/null +++ b/docs/pages/base-ui/api/field-description.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "FieldDescription", + "imports": [ + "import * as Field from '@base_ui/react/Field';\nconst FieldDescription = Field.Description;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "FieldDescription", + "forwardsRefTo": "HTMLParagraphElement", + "filename": "/packages/mui-base/src/Field/Description/FieldDescription.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/field-label.json b/docs/pages/base-ui/api/field-label.json new file mode 100644 index 000000000..b965b8fac --- /dev/null +++ b/docs/pages/base-ui/api/field-label.json @@ -0,0 +1,17 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "FieldLabel", + "imports": ["import * as Field from '@base_ui/react/Field';\nconst FieldLabel = Field.Label;"], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "FieldLabel", + "forwardsRefTo": "HTMLLabelElement", + "filename": "/packages/mui-base/src/Field/Label/FieldLabel.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/field-root.json b/docs/pages/base-ui/api/field-root.json new file mode 100644 index 000000000..57fbb05c6 --- /dev/null +++ b/docs/pages/base-ui/api/field-root.json @@ -0,0 +1,17 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "FieldRoot", + "imports": ["import * as Field from '@base_ui/react/Field';\nconst FieldRoot = Field.Root;"], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "FieldRoot", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Field/Root/FieldRoot.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/label.json b/docs/pages/base-ui/api/label.json new file mode 100644 index 000000000..9e1a1a3c5 --- /dev/null +++ b/docs/pages/base-ui/api/label.json @@ -0,0 +1,14 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "Label", + "imports": ["import { Label } from '@base_ui/react/Label';"], + "classes": [], + "muiName": "Label", + "filename": "/packages/mui-base/src/Label/Label.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/use-checkbox-root.json b/docs/pages/base-ui/api/use-checkbox-root.json index 76ad0de13..2e40a2226 100644 --- a/docs/pages/base-ui/api/use-checkbox-root.json +++ b/docs/pages/base-ui/api/use-checkbox-root.json @@ -1,12 +1,21 @@ { "parameters": { + "setControlId": { + "type": { + "name": "React.Dispatch<React.SetStateAction<string | undefined>>", + "description": "React.Dispatch<React.SetStateAction<string | undefined>>" + }, + "required": true + }, "autoFocus": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" }, "checked": { "type": { "name": "boolean", "description": "boolean" }, "default": "undefined" }, "defaultChecked": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" }, + "descriptionId": { "type": { "name": "string", "description": "string" } }, "disabled": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" }, + "id": { "type": { "name": "string", "description": "string" } }, "indeterminate": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" diff --git a/docs/pages/base-ui/api/use-field-description.json b/docs/pages/base-ui/api/use-field-description.json new file mode 100644 index 000000000..2ffa339b2 --- /dev/null +++ b/docs/pages/base-ui/api/use-field-description.json @@ -0,0 +1,8 @@ +{ + "parameters": {}, + "returnValue": {}, + "name": "useFieldDescription", + "filename": "/packages/mui-base/src/Field/Description/useFieldDescription.ts", + "imports": ["import { useFieldDescription } from '@base_ui/react/Field';"], + "demos": "
    " +} diff --git a/docs/pages/base-ui/api/use-field-label.json b/docs/pages/base-ui/api/use-field-label.json new file mode 100644 index 000000000..d9fb21e0d --- /dev/null +++ b/docs/pages/base-ui/api/use-field-label.json @@ -0,0 +1,8 @@ +{ + "parameters": {}, + "returnValue": {}, + "name": "useFieldLabel", + "filename": "/packages/mui-base/src/Field/Label/useFieldLabel.ts", + "imports": ["import { useFieldLabel } from '@base_ui/react/Field';"], + "demos": "
      " +} diff --git a/docs/pages/base-ui/api/use-label.json b/docs/pages/base-ui/api/use-label.json new file mode 100644 index 000000000..7fecbb2c3 --- /dev/null +++ b/docs/pages/base-ui/api/use-label.json @@ -0,0 +1,8 @@ +{ + "parameters": {}, + "returnValue": {}, + "name": "useLabel", + "filename": "/packages/mui-base/src/Label/useLabel.ts", + "imports": ["import { useLabel } from '@base_ui/react/Label';"], + "demos": "
        " +} diff --git a/docs/pages/base-ui/react-field/[docsTab]/index.js b/docs/pages/base-ui/react-field/[docsTab]/index.js new file mode 100644 index 000000000..a8b446e94 --- /dev/null +++ b/docs/pages/base-ui/react-field/[docsTab]/index.js @@ -0,0 +1,64 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; +import AppFrame from 'docs/src/modules/components/AppFrame'; +import * as pageProps from 'docs-base/data/base/components/field/field.md?@mui/markdown'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import FieldDescriptionApiJsonPageContent from '../../api/field-description.json'; +import FieldLabelApiJsonPageContent from '../../api/field-label.json'; +import FieldRootApiJsonPageContent from '../../api/field-root.json'; + +export default function Page(props) { + const { userLanguage, ...other } = props; + return ; +} + +Page.getLayout = (page) => { + return {page}; +}; + +export const getStaticPaths = () => { + return { + paths: [{ params: { docsTab: 'components-api' } }, { params: { docsTab: 'hooks-api' } }], + fallback: false, // can also be true or 'blocking' + }; +}; + +export const getStaticProps = () => { + const FieldDescriptionApiReq = require.context( + 'docs-base/translations/api-docs/field-description', + false, + /\.\/field-description.*.json$/, + ); + const FieldDescriptionApiDescriptions = mapApiPageTranslations(FieldDescriptionApiReq); + + const FieldLabelApiReq = require.context( + 'docs-base/translations/api-docs/field-label', + false, + /\.\/field-label.*.json$/, + ); + const FieldLabelApiDescriptions = mapApiPageTranslations(FieldLabelApiReq); + + const FieldRootApiReq = require.context( + 'docs-base/translations/api-docs/field-root', + false, + /\.\/field-root.*.json$/, + ); + const FieldRootApiDescriptions = mapApiPageTranslations(FieldRootApiReq); + + return { + props: { + componentsApiDescriptions: { + FieldDescription: FieldDescriptionApiDescriptions, + FieldLabel: FieldLabelApiDescriptions, + FieldRoot: FieldRootApiDescriptions, + }, + componentsApiPageContents: { + FieldDescription: FieldDescriptionApiJsonPageContent, + FieldLabel: FieldLabelApiJsonPageContent, + FieldRoot: FieldRootApiJsonPageContent, + }, + hooksApiDescriptions: {}, + hooksApiPageContents: {}, + }, + }; +}; diff --git a/docs/pages/base-ui/react-field/index.js b/docs/pages/base-ui/react-field/index.js new file mode 100644 index 000000000..40e8d0329 --- /dev/null +++ b/docs/pages/base-ui/react-field/index.js @@ -0,0 +1,13 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; +import AppFrame from 'docs/src/modules/components/AppFrame'; +import * as pageProps from 'docs-base/data/base/components/field/field.md?@mui/markdown'; + +export default function Page(props) { + const { userLanguage, ...other } = props; + return ; +} + +Page.getLayout = (page) => { + return {page}; +}; diff --git a/docs/pages/base-ui/react-label/[docsTab]/index.js b/docs/pages/base-ui/react-label/[docsTab]/index.js new file mode 100644 index 000000000..99687717e --- /dev/null +++ b/docs/pages/base-ui/react-label/[docsTab]/index.js @@ -0,0 +1,40 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; +import AppFrame from 'docs/src/modules/components/AppFrame'; +import * as pageProps from 'docs-base/data/base/components/label/label.md?@mui/markdown'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import LabelApiJsonPageContent from '../../api/label.json'; + +export default function Page(props) { + const { userLanguage, ...other } = props; + return ; +} + +Page.getLayout = (page) => { + return {page}; +}; + +export const getStaticPaths = () => { + return { + paths: [{ params: { docsTab: 'components-api' } }, { params: { docsTab: 'hooks-api' } }], + fallback: false, // can also be true or 'blocking' + }; +}; + +export const getStaticProps = () => { + const LabelApiReq = require.context( + 'docs-base/translations/api-docs/label', + false, + /\.\/label.*.json$/, + ); + const LabelApiDescriptions = mapApiPageTranslations(LabelApiReq); + + return { + props: { + componentsApiDescriptions: { Label: LabelApiDescriptions }, + componentsApiPageContents: { Label: LabelApiJsonPageContent }, + hooksApiDescriptions: {}, + hooksApiPageContents: {}, + }, + }; +}; diff --git a/docs/pages/base-ui/react-label/index.js b/docs/pages/base-ui/react-label/index.js new file mode 100644 index 000000000..5834acf87 --- /dev/null +++ b/docs/pages/base-ui/react-label/index.js @@ -0,0 +1,13 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; +import AppFrame from 'docs/src/modules/components/AppFrame'; +import * as pageProps from 'docs-base/data/base/components/label/label.md?@mui/markdown'; + +export default function Page(props) { + const { userLanguage, ...other } = props; + return ; +} + +Page.getLayout = (page) => { + return {page}; +}; diff --git a/docs/translations/api-docs/checkbox-root/checkbox-root.json b/docs/translations/api-docs/checkbox-root/checkbox-root.json index 5b80fd96a..32b2ed1a4 100644 --- a/docs/translations/api-docs/checkbox-root/checkbox-root.json +++ b/docs/translations/api-docs/checkbox-root/checkbox-root.json @@ -10,6 +10,7 @@ "description": "The default checked state. Use when the component is not controlled." }, "disabled": { "description": "If true, the component is disabled." }, + "id": { "description": "The id of the input element." }, "indeterminate": { "description": "If true, the checkbox will be indeterminate." }, "inputRef": { "description": "The ref to the input element." }, "name": { "description": "Name of the underlying input element." }, diff --git a/docs/translations/api-docs/field-description/field-description.json b/docs/translations/api-docs/field-description/field-description.json new file mode 100644 index 000000000..8c46ee3d0 --- /dev/null +++ b/docs/translations/api-docs/field-description/field-description.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "The field's description.", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/field-label/field-label.json b/docs/translations/api-docs/field-label/field-label.json new file mode 100644 index 000000000..2f62e5811 --- /dev/null +++ b/docs/translations/api-docs/field-label/field-label.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "The field's label.", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/field-root/field-root.json b/docs/translations/api-docs/field-root/field-root.json new file mode 100644 index 000000000..d9f25d6f9 --- /dev/null +++ b/docs/translations/api-docs/field-root/field-root.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "The foundation for building custom-styled fields.", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/label/label.json b/docs/translations/api-docs/label/label.json new file mode 100644 index 000000000..4bc12cf1e --- /dev/null +++ b/docs/translations/api-docs/label/label.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/use-checkbox-root/use-checkbox-root.json b/docs/translations/api-docs/use-checkbox-root/use-checkbox-root.json index 65eff0de0..8d06721ee 100644 --- a/docs/translations/api-docs/use-checkbox-root/use-checkbox-root.json +++ b/docs/translations/api-docs/use-checkbox-root/use-checkbox-root.json @@ -6,7 +6,9 @@ "defaultChecked": { "description": "The default checked state. Use when the component is not controlled." }, + "descriptionId": { "description": "The id of the description element." }, "disabled": { "description": "If true, the component is disabled." }, + "id": { "description": "The id of the input element." }, "indeterminate": { "description": "If true, the checkbox will be indeterminate." }, "inputRef": { "description": "The ref to the input element." }, "name": { "description": "Name of the underlying input element." }, @@ -14,7 +16,8 @@ "readOnly": { "description": "If true, the component is read only." }, "required": { "description": "If true, the input element is required." - } + }, + "setControlId": { "description": "Sets the id of the input element." } }, "returnValueDescriptions": { "checked": { "description": "If true, the checkbox is checked." }, diff --git a/docs/translations/api-docs/use-field-description/use-field-description.json b/docs/translations/api-docs/use-field-description/use-field-description.json new file mode 100644 index 000000000..e3eb65c6e --- /dev/null +++ b/docs/translations/api-docs/use-field-description/use-field-description.json @@ -0,0 +1 @@ +{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/api-docs/use-field-label/use-field-label.json b/docs/translations/api-docs/use-field-label/use-field-label.json new file mode 100644 index 000000000..e3eb65c6e --- /dev/null +++ b/docs/translations/api-docs/use-field-label/use-field-label.json @@ -0,0 +1 @@ +{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/api-docs/use-label/use-label.json b/docs/translations/api-docs/use-label/use-label.json new file mode 100644 index 000000000..e3eb65c6e --- /dev/null +++ b/docs/translations/api-docs/use-label/use-label.json @@ -0,0 +1 @@ +{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/packages/mui-base/src/Checkbox/Root/CheckboxRoot.tsx b/packages/mui-base/src/Checkbox/Root/CheckboxRoot.tsx index a59beee09..1d90bb74d 100644 --- a/packages/mui-base/src/Checkbox/Root/CheckboxRoot.tsx +++ b/packages/mui-base/src/Checkbox/Root/CheckboxRoot.tsx @@ -8,6 +8,7 @@ import { resolveClassName } from '../../utils/resolveClassName'; import { evaluateRenderProp } from '../../utils/evaluateRenderProp'; import { useRenderPropForkRef } from '../../utils/useRenderPropForkRef'; import { defaultRenderFunctions } from '../../utils/defaultRenderFunctions'; +import { useFieldRootContext } from '../../Field/Root/FieldRootContext'; /** * The foundation for building custom-styled checkboxes. @@ -39,7 +40,13 @@ const CheckboxRoot = React.forwardRef(function CheckboxRoot( } = props; const render = renderProp ?? defaultRenderFunctions.button; - const { checked, getInputProps, getButtonProps } = useCheckboxRoot(props); + const { setControlId, descriptionId } = useFieldRootContext(); + + const { checked, getInputProps, getButtonProps } = useCheckboxRoot({ + ...props, + descriptionId, + setControlId, + }); const ownerState: CheckboxOwnerState = React.useMemo( () => ({ @@ -108,6 +115,10 @@ CheckboxRoot.propTypes /* remove-proptypes */ = { * @default false */ disabled: PropTypes.bool, + /** + * The id of the input element. + */ + id: PropTypes.string, /** * If `true`, the checkbox will be indeterminate. * diff --git a/packages/mui-base/src/Checkbox/Root/CheckboxRoot.types.ts b/packages/mui-base/src/Checkbox/Root/CheckboxRoot.types.ts index a03fb1ad4..e94c8481f 100644 --- a/packages/mui-base/src/Checkbox/Root/CheckboxRoot.types.ts +++ b/packages/mui-base/src/Checkbox/Root/CheckboxRoot.types.ts @@ -10,12 +10,20 @@ export type CheckboxOwnerState = { }; export interface CheckboxRootProps - extends UseCheckboxRootParameters, + extends Omit, Omit, 'onChange'> {} export type CheckboxContextValue = CheckboxOwnerState; export interface UseCheckboxRootParameters { + /** + * The id of the input element. + */ + id?: string; + /** + * The id of the description element. + */ + descriptionId?: string; /** * Name of the underlying input element. * @@ -75,6 +83,10 @@ export interface UseCheckboxRootParameters { * The ref to the input element. */ inputRef?: React.Ref; + /** + * Sets the `id` of the input element. + */ + setControlId: React.Dispatch>; } export interface UseCheckboxRootReturnValue { diff --git a/packages/mui-base/src/Checkbox/Root/useCheckboxRoot.ts b/packages/mui-base/src/Checkbox/Root/useCheckboxRoot.ts index fd55da502..b6a85f142 100644 --- a/packages/mui-base/src/Checkbox/Root/useCheckboxRoot.ts +++ b/packages/mui-base/src/Checkbox/Root/useCheckboxRoot.ts @@ -5,6 +5,8 @@ import { visuallyHidden } from '../../utils/visuallyHidden'; import { useForkRef } from '../../utils/useForkRef'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { useEventCallback } from '../../utils/useEventCallback'; +import { useId } from '../../utils/useId'; +import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; /** * The basic building block for creating custom checkboxes. @@ -19,6 +21,9 @@ import { useEventCallback } from '../../utils/useEventCallback'; */ export function useCheckboxRoot(params: UseCheckboxRootParameters): UseCheckboxRootReturnValue { const { + id: idProp, + descriptionId, + setControlId, checked: externalChecked, inputRef: externalInputRef, name, @@ -32,6 +37,14 @@ export function useCheckboxRoot(params: UseCheckboxRootParameters): UseCheckboxR } = params; const onCheckedChange = useEventCallback(onCheckedChangeProp); + const id = useId(idProp); + + useEnhancedEffect(() => { + setControlId(id); + return () => { + setControlId(undefined); + }; + }, [id, setControlId]); const inputRef = React.useRef(null); const mergedInputRef = useForkRef(externalInputRef, inputRef); @@ -58,6 +71,8 @@ export function useCheckboxRoot(params: UseCheckboxRootParameters): UseCheckboxR 'aria-checked': indeterminate ? 'mixed' : checked, 'aria-disabled': disabled || undefined, 'aria-readonly': readOnly || undefined, + 'aria-labelledby': id, + 'aria-describedby': descriptionId, ...externalProps, onClick(event) { if (event.defaultPrevented || readOnly) { @@ -69,12 +84,13 @@ export function useCheckboxRoot(params: UseCheckboxRootParameters): UseCheckboxR inputRef.current?.click(); }, }), - [checked, disabled, indeterminate, readOnly], + [indeterminate, checked, disabled, readOnly, id, descriptionId], ); const getInputProps: UseCheckboxRootReturnValue['getInputProps'] = React.useCallback( (externalProps = {}) => mergeReactProps<'input'>(externalProps, { + id, checked, disabled, name, @@ -98,14 +114,15 @@ export function useCheckboxRoot(params: UseCheckboxRootParameters): UseCheckboxR }, }), [ - autoFocus, + id, checked, disabled, name, - onCheckedChange, required, - setCheckedState, + autoFocus, mergedInputRef, + setCheckedState, + onCheckedChange, ], ); diff --git a/packages/mui-base/src/Field/Description/FieldDescription.test.tsx b/packages/mui-base/src/Field/Description/FieldDescription.test.tsx new file mode 100644 index 000000000..4880c2bec --- /dev/null +++ b/packages/mui-base/src/Field/Description/FieldDescription.test.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { createRenderer } from '@mui/internal-test-utils'; +import * as Field from '@base_ui/react/Field'; +import { describeConformance } from '../../../test/describeConformance'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'p', + refInstanceof: window.HTMLParagraphElement, + render(node) { + return render({node}); + }, + })); +}); diff --git a/packages/mui-base/src/Field/Description/FieldDescription.tsx b/packages/mui-base/src/Field/Description/FieldDescription.tsx new file mode 100644 index 000000000..6b1f30fd6 --- /dev/null +++ b/packages/mui-base/src/Field/Description/FieldDescription.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import type { FieldDescriptionProps } from './FieldDescription.types'; +import { useFieldRootContext } from '../Root/FieldRootContext'; +import { useFieldDescription } from './useFieldDescription'; + +/** + * The field's description. + * + * Demos: + * + * - [Field](https://mui.com/base-ui/react-field/) + * + * API: + * + * - [FieldDescription API](https://mui.com/base-ui/react-field/components-api/#field-description) + */ +const FieldDescription = React.forwardRef(function FieldDescription( + props: FieldDescriptionProps, + forwardedRef: React.ForwardedRef, +) { + const { render, id, className, ...otherProps } = props; + + const { setDescriptionId } = useFieldRootContext(); + + const { getDescriptionProps } = useFieldDescription({ setDescriptionId, id }); + + const { renderElement } = useComponentRenderer({ + propGetter: getDescriptionProps, + render: render ?? 'p', + ref: forwardedRef, + className, + ownerState: {}, + extraProps: otherProps, + }); + + return renderElement(); +}); + +FieldDescription.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * @ignore + */ + id: PropTypes.string, + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { FieldDescription }; diff --git a/packages/mui-base/src/Field/Description/FieldDescription.types.tsx b/packages/mui-base/src/Field/Description/FieldDescription.types.tsx new file mode 100644 index 000000000..107680ec8 --- /dev/null +++ b/packages/mui-base/src/Field/Description/FieldDescription.types.tsx @@ -0,0 +1,6 @@ +import type { BaseUIComponentProps } from '../../utils/types'; + +export type FieldDescriptionOwnerState = {}; + +export interface FieldDescriptionProps + extends BaseUIComponentProps<'p', FieldDescriptionOwnerState> {} diff --git a/packages/mui-base/src/Field/Description/useFieldDescription.ts b/packages/mui-base/src/Field/Description/useFieldDescription.ts new file mode 100644 index 000000000..955319526 --- /dev/null +++ b/packages/mui-base/src/Field/Description/useFieldDescription.ts @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { useId } from '../../utils/useId'; +import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; + +interface UseFieldDescriptionParameters { + id: string | undefined; + setDescriptionId: React.Dispatch>; +} +/** + * + * API: + * + * - [useFieldDescription API](https://mui.com/base-ui/api/use-field-description/) + */ +export function useFieldDescription(params: UseFieldDescriptionParameters) { + const { setDescriptionId, id: idProp } = params; + + const id = useId(idProp); + + useEnhancedEffect(() => { + setDescriptionId(id); + return () => { + setDescriptionId(undefined); + }; + }, [id, setDescriptionId]); + + const getDescriptionProps = React.useCallback( + (externalProps = {}) => + mergeReactProps<'p'>(externalProps, { + id, + }), + [id], + ); + + return React.useMemo( + () => ({ + getDescriptionProps, + }), + [getDescriptionProps], + ); +} diff --git a/packages/mui-base/src/Field/Label/FieldLabel.test.tsx b/packages/mui-base/src/Field/Label/FieldLabel.test.tsx new file mode 100644 index 000000000..359ac697c --- /dev/null +++ b/packages/mui-base/src/Field/Label/FieldLabel.test.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { createRenderer } from '@mui/internal-test-utils'; +import * as Field from '@base_ui/react/Field'; +import { describeConformance } from '../../../test/describeConformance'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'label', + refInstanceof: window.HTMLLabelElement, + render(node) { + return render({node}); + }, + })); +}); diff --git a/packages/mui-base/src/Field/Label/FieldLabel.tsx b/packages/mui-base/src/Field/Label/FieldLabel.tsx new file mode 100644 index 000000000..5d88fe0fc --- /dev/null +++ b/packages/mui-base/src/Field/Label/FieldLabel.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import type { FieldLabelProps } from './FieldLabel.types'; +import { useFieldRootContext } from '../Root/FieldRootContext'; +import { useFieldLabel } from './useFieldLabel'; + +/** + * The field's label. + * + * Demos: + * + * - [Field](https://mui.com/base-ui/react-field/) + * + * API: + * + * - [FieldLabel API](https://mui.com/base-ui/react-field/components-api/#field-label) + */ +const FieldLabel = React.forwardRef(function FieldLabel( + props: FieldLabelProps, + forwardedRef: React.ForwardedRef, +) { + const { render, className, ...otherProps } = props; + + const { controlId } = useFieldRootContext(); + + const { getLabelProps } = useFieldLabel({ controlId }); + + const { renderElement } = useComponentRenderer({ + propGetter: getLabelProps, + render: render ?? 'label', + ref: forwardedRef, + className, + ownerState: {}, + extraProps: otherProps, + }); + + return renderElement(); +}); + +FieldLabel.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { FieldLabel }; diff --git a/packages/mui-base/src/Field/Label/FieldLabel.types.ts b/packages/mui-base/src/Field/Label/FieldLabel.types.ts new file mode 100644 index 000000000..a541eaf7b --- /dev/null +++ b/packages/mui-base/src/Field/Label/FieldLabel.types.ts @@ -0,0 +1,5 @@ +import type { BaseUIComponentProps } from '../../utils/types'; + +export type FieldLabelOwnerState = {}; + +export interface FieldLabelProps extends BaseUIComponentProps<'div', FieldLabelOwnerState> {} diff --git a/packages/mui-base/src/Field/Label/useFieldLabel.ts b/packages/mui-base/src/Field/Label/useFieldLabel.ts new file mode 100644 index 000000000..2128335e2 --- /dev/null +++ b/packages/mui-base/src/Field/Label/useFieldLabel.ts @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { mergeReactProps } from '../../utils/mergeReactProps'; + +interface UseFieldLabelParameters { + controlId: string | undefined; +} +/** + * + * API: + * + * - [useFieldLabel API](https://mui.com/base-ui/api/use-field-label/) + */ +export function useFieldLabel(params: UseFieldLabelParameters) { + const { controlId } = params; + + const getLabelProps = React.useCallback( + (externalProps = {}) => + mergeReactProps<'label'>(externalProps, { + htmlFor: controlId, + onMouseDown: (e) => e.preventDefault(), + ...externalProps, + }), + [controlId], + ); + + return React.useMemo( + () => ({ + getLabelProps, + }), + [getLabelProps], + ); +} diff --git a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx new file mode 100644 index 000000000..60988c7ce --- /dev/null +++ b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { createRenderer } from '@mui/internal-test-utils'; +import * as Field from '@base_ui/react/Field'; +import { describeConformance } from '../../../test/describeConformance'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'div', + refInstanceof: window.HTMLDivElement, + render, + })); +}); diff --git a/packages/mui-base/src/Field/Root/FieldRoot.tsx b/packages/mui-base/src/Field/Root/FieldRoot.tsx new file mode 100644 index 000000000..e40df122a --- /dev/null +++ b/packages/mui-base/src/Field/Root/FieldRoot.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import type { FieldRootContextValue, FieldRootProps } from './FieldRoot.types'; +import { FieldRootContext } from './FieldRootContext'; + +/** + * The foundation for building custom-styled fields. + * + * Demos: + * + * - [Field](https://mui.com/base-ui/react-field/) + * + * API: + * + * - [FieldRoot API](https://mui.com/base-ui/react-field/components-api/#field-root) + */ +const FieldRoot = React.forwardRef(function FieldRoot( + props: FieldRootProps, + forwardedRef: React.ForwardedRef, +) { + const { render, className, ...otherProps } = props; + + const [controlId, setControlId] = React.useState(undefined); + const [descriptionId, setDescriptionId] = React.useState(undefined); + + const { renderElement } = useComponentRenderer({ + render: render ?? 'div', + ref: forwardedRef, + className, + ownerState: {}, + extraProps: otherProps, + }); + + const contextValue: FieldRootContextValue = React.useMemo( + () => ({ + controlId, + setControlId, + descriptionId, + setDescriptionId, + }), + [controlId, descriptionId], + ); + + return ( + {renderElement()} + ); +}); + +FieldRoot.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { FieldRoot }; diff --git a/packages/mui-base/src/Field/Root/FieldRoot.types.ts b/packages/mui-base/src/Field/Root/FieldRoot.types.ts new file mode 100644 index 000000000..51c047a47 --- /dev/null +++ b/packages/mui-base/src/Field/Root/FieldRoot.types.ts @@ -0,0 +1,13 @@ +import type * as React from 'react'; +import type { BaseUIComponentProps } from '../../utils/types'; + +export interface FieldRootContextValue { + controlId: string | undefined; + setControlId: React.Dispatch>; + descriptionId: string | undefined; + setDescriptionId: React.Dispatch>; +} + +export type FieldRootOwnerState = {}; + +export interface FieldRootProps extends BaseUIComponentProps<'div', FieldRootOwnerState> {} diff --git a/packages/mui-base/src/Field/Root/FieldRootContext.ts b/packages/mui-base/src/Field/Root/FieldRootContext.ts new file mode 100644 index 000000000..aecf59606 --- /dev/null +++ b/packages/mui-base/src/Field/Root/FieldRootContext.ts @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { FieldRootContextValue } from './FieldRoot.types'; + +export const FieldRootContext = React.createContext({ + controlId: undefined, + setControlId: () => {}, + descriptionId: undefined, + setDescriptionId: () => {}, +}); + +export function useFieldRootContext() { + return React.useContext(FieldRootContext); +} diff --git a/packages/mui-base/src/Field/index.barrel.ts b/packages/mui-base/src/Field/index.barrel.ts new file mode 100644 index 000000000..0bb89d0e6 --- /dev/null +++ b/packages/mui-base/src/Field/index.barrel.ts @@ -0,0 +1,7 @@ +export * from './Root/FieldRoot'; +export * from './Label/FieldLabel'; +export * from './Description/FieldDescription'; + +export type { FieldRootProps, FieldRootOwnerState } from './Root/FieldRoot.types'; +export type * from './Label/FieldLabel.types'; +export type * from './Description/FieldDescription.types'; diff --git a/packages/mui-base/src/Field/index.ts b/packages/mui-base/src/Field/index.ts new file mode 100644 index 000000000..3fc41ac49 --- /dev/null +++ b/packages/mui-base/src/Field/index.ts @@ -0,0 +1,16 @@ +export { FieldRoot as Root } from './Root/FieldRoot'; +export { FieldLabel as Label } from './Label/FieldLabel'; +export { FieldDescription as Description } from './Description/FieldDescription'; + +export type { + FieldRootProps as RootProps, + FieldRootOwnerState as RootOwnerState, +} from './Root/FieldRoot.types'; +export type { + FieldLabelProps as LabelProps, + FieldLabelOwnerState as LabelOwnerState, +} from './Label/FieldLabel.types'; +export type { + FieldDescriptionProps as DescriptionProps, + FieldDescriptionOwnerState as DescriptionOwnerState, +} from './Description/FieldDescription.types'; diff --git a/packages/mui-base/src/Label/Label.tsx b/packages/mui-base/src/Label/Label.tsx new file mode 100644 index 000000000..cd464ca1c --- /dev/null +++ b/packages/mui-base/src/Label/Label.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useComponentRenderer } from '../utils/useComponentRenderer'; +import { LabelProps } from './Label.types'; +import { useLabel } from './useLabel'; + +const Label = React.forwardRef(function Label( + props: LabelProps, + forwardedRef: React.ForwardedRef, +) { + const { render, className, ...otherProps } = props; + + const { getLabelProps } = useLabel(); + + const { renderElement } = useComponentRenderer({ + propGetter: getLabelProps, + render: render ?? 'label', + ref: forwardedRef, + className, + ownerState: {}, + extraProps: otherProps, + }); + + return renderElement(); +}); + +Label.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { Label }; diff --git a/packages/mui-base/src/Label/Label.types.ts b/packages/mui-base/src/Label/Label.types.ts new file mode 100644 index 000000000..d26ee8e5d --- /dev/null +++ b/packages/mui-base/src/Label/Label.types.ts @@ -0,0 +1,5 @@ +import type { BaseUIComponentProps } from '../utils/types'; + +export type LabelOwnerState = {}; + +export interface LabelProps extends BaseUIComponentProps<'label', LabelOwnerState> {} diff --git a/packages/mui-base/src/Label/index.barrel.ts b/packages/mui-base/src/Label/index.barrel.ts new file mode 100644 index 000000000..509afc0ae --- /dev/null +++ b/packages/mui-base/src/Label/index.barrel.ts @@ -0,0 +1,3 @@ +export { Label } from './Label'; + +export type { LabelProps, LabelOwnerState } from './Label.types'; diff --git a/packages/mui-base/src/Label/index.ts b/packages/mui-base/src/Label/index.ts new file mode 100644 index 000000000..e73fb655b --- /dev/null +++ b/packages/mui-base/src/Label/index.ts @@ -0,0 +1,3 @@ +export { Label as Root } from './Label'; + +export type { LabelProps as RootProps, LabelOwnerState as RootOwnerState } from './Label.types'; diff --git a/packages/mui-base/src/Label/useLabel.ts b/packages/mui-base/src/Label/useLabel.ts new file mode 100644 index 000000000..d240043da --- /dev/null +++ b/packages/mui-base/src/Label/useLabel.ts @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { mergeReactProps } from '../utils/mergeReactProps'; +/** + * + * API: + * + * - [useLabel API](https://mui.com/base-ui/api/use-label/) + */ +export function useLabel() { + const getLabelProps = React.useCallback( + (externalProps = {}) => + mergeReactProps<'label'>(externalProps, { + onMouseDown(e) { + e.preventDefault(); + }, + }), + [], + ); + + return React.useMemo( + () => ({ + getLabelProps, + }), + [getLabelProps], + ); +} diff --git a/packages/mui-base/src/utils/defaultRenderFunctions.tsx b/packages/mui-base/src/utils/defaultRenderFunctions.tsx index f00e1f1ab..df40b828e 100644 --- a/packages/mui-base/src/utils/defaultRenderFunctions.tsx +++ b/packages/mui-base/src/utils/defaultRenderFunctions.tsx @@ -24,4 +24,8 @@ export const defaultRenderFunctions = { // eslint-disable-next-line jsx-a11y/anchor-has-content return ; }, + label: (props: React.ComponentPropsWithRef<'label'>) => { + // eslint-disable-next-line jsx-a11y/label-has-associated-control + return ", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/use-field-control.json b/docs/pages/base-ui/api/use-field-control.json new file mode 100644 index 000000000..046107b51 --- /dev/null +++ b/docs/pages/base-ui/api/use-field-control.json @@ -0,0 +1,8 @@ +{ + "parameters": {}, + "returnValue": {}, + "name": "useFieldControl", + "filename": "/packages/mui-base/src/Field/Control/useFieldControl.ts", + "imports": ["import { useFieldControl } from '@base_ui/react/Field';"], + "demos": "
          " +} diff --git a/docs/pages/base-ui/react-field/[docsTab]/index.js b/docs/pages/base-ui/react-field/[docsTab]/index.js index a8b446e94..a4bcc728b 100644 --- a/docs/pages/base-ui/react-field/[docsTab]/index.js +++ b/docs/pages/base-ui/react-field/[docsTab]/index.js @@ -3,6 +3,7 @@ import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; import AppFrame from 'docs/src/modules/components/AppFrame'; import * as pageProps from 'docs-base/data/base/components/field/field.md?@mui/markdown'; import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import FieldControlApiJsonPageContent from '../../api/field-control.json'; import FieldDescriptionApiJsonPageContent from '../../api/field-description.json'; import FieldLabelApiJsonPageContent from '../../api/field-label.json'; import FieldRootApiJsonPageContent from '../../api/field-root.json'; @@ -24,6 +25,13 @@ export const getStaticPaths = () => { }; export const getStaticProps = () => { + const FieldControlApiReq = require.context( + 'docs-base/translations/api-docs/field-control', + false, + /\.\/field-control.*.json$/, + ); + const FieldControlApiDescriptions = mapApiPageTranslations(FieldControlApiReq); + const FieldDescriptionApiReq = require.context( 'docs-base/translations/api-docs/field-description', false, @@ -48,11 +56,13 @@ export const getStaticProps = () => { return { props: { componentsApiDescriptions: { + FieldControl: FieldControlApiDescriptions, FieldDescription: FieldDescriptionApiDescriptions, FieldLabel: FieldLabelApiDescriptions, FieldRoot: FieldRootApiDescriptions, }, componentsApiPageContents: { + FieldControl: FieldControlApiJsonPageContent, FieldDescription: FieldDescriptionApiJsonPageContent, FieldLabel: FieldLabelApiJsonPageContent, FieldRoot: FieldRootApiJsonPageContent, diff --git a/docs/translations/api-docs/field-control/field-control.json b/docs/translations/api-docs/field-control/field-control.json new file mode 100644 index 000000000..04a41c52f --- /dev/null +++ b/docs/translations/api-docs/field-control/field-control.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "The field's control element.", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/use-field-control/use-field-control.json b/docs/translations/api-docs/use-field-control/use-field-control.json new file mode 100644 index 000000000..e3eb65c6e --- /dev/null +++ b/docs/translations/api-docs/use-field-control/use-field-control.json @@ -0,0 +1 @@ +{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/packages/mui-base/src/Field/Control/FieldControl.test.tsx b/packages/mui-base/src/Field/Control/FieldControl.test.tsx new file mode 100644 index 000000000..675213cb0 --- /dev/null +++ b/packages/mui-base/src/Field/Control/FieldControl.test.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { createRenderer } from '@mui/internal-test-utils'; +import * as Field from '@base_ui/react/Field'; +import { describeConformance } from '../../../test/describeConformance'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'input', + refInstanceof: window.HTMLInputElement, + render(node) { + return render({node}); + }, + })); +}); diff --git a/packages/mui-base/src/Field/Control/FieldControl.tsx b/packages/mui-base/src/Field/Control/FieldControl.tsx new file mode 100644 index 000000000..1ed2f58c1 --- /dev/null +++ b/packages/mui-base/src/Field/Control/FieldControl.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import type { FieldControlProps } from './FieldControl.types'; +import { useFieldRootContext } from '../Root/FieldRootContext'; +import { useFieldControl } from './useFieldControl'; + +/** + * The field's control element. + * + * Demos: + * + * - [Field](https://mui.com/base-ui/react-field/) + * + * API: + * + * - [FieldControl API](https://mui.com/base-ui/react-field/components-api/#field-control) + */ +const FieldControl = React.forwardRef(function FieldControl( + props: FieldControlProps, + forwardedRef: React.ForwardedRef, +) { + const { render, id, className, ...otherProps } = props; + + const { setControlId, descriptionId } = useFieldRootContext(); + + const { getControlProps } = useFieldControl({ setControlId, descriptionId }); + + const { renderElement } = useComponentRenderer({ + propGetter: getControlProps, + render: render ?? 'input', + ref: forwardedRef, + className, + ownerState: {}, + extraProps: otherProps, + }); + + return renderElement(); +}); + +FieldControl.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { FieldControl }; diff --git a/packages/mui-base/src/Field/Control/FieldControl.types.ts b/packages/mui-base/src/Field/Control/FieldControl.types.ts new file mode 100644 index 000000000..254aa42f9 --- /dev/null +++ b/packages/mui-base/src/Field/Control/FieldControl.types.ts @@ -0,0 +1,5 @@ +import { BaseUIComponentProps } from '@base_ui/react/utils/types'; + +export type FieldControlOwnerState = {}; + +export interface FieldControlProps extends BaseUIComponentProps {} diff --git a/packages/mui-base/src/Field/Control/useFieldControl.ts b/packages/mui-base/src/Field/Control/useFieldControl.ts new file mode 100644 index 000000000..990523181 --- /dev/null +++ b/packages/mui-base/src/Field/Control/useFieldControl.ts @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; +import { useId } from '../../utils/useId'; + +interface UseFieldControlParameters { + descriptionId: string | undefined; + setControlId: (id: string | undefined) => void; + id?: string; +} + +/** + * + * API: + * + * - [useFieldControl API](https://mui.com/base-ui/api/use-field-control/) + */ +export function useFieldControl(params: UseFieldControlParameters) { + const { setControlId, descriptionId, id: idProp } = params; + + const id = useId(idProp); + + useEnhancedEffect(() => { + setControlId(id); + return () => { + setControlId(undefined); + }; + }, [id, setControlId]); + + const getControlProps = React.useCallback( + (externalProps = {}) => + mergeReactProps(externalProps, { + id, + 'aria-describedby': descriptionId, + }), + [id, descriptionId], + ); + + return React.useMemo( + () => ({ + getControlProps, + }), + [getControlProps], + ); +} diff --git a/packages/mui-base/src/Field/Description/FieldDescription.test.tsx b/packages/mui-base/src/Field/Description/FieldDescription.test.tsx index 4880c2bec..3b177590e 100644 --- a/packages/mui-base/src/Field/Description/FieldDescription.test.tsx +++ b/packages/mui-base/src/Field/Description/FieldDescription.test.tsx @@ -3,7 +3,7 @@ import { createRenderer } from '@mui/internal-test-utils'; import * as Field from '@base_ui/react/Field'; import { describeConformance } from '../../../test/describeConformance'; -describe('', () => { +describe('', () => { const { render } = createRenderer(); describeConformance(, () => ({ diff --git a/packages/mui-base/src/Field/index.barrel.ts b/packages/mui-base/src/Field/index.barrel.ts index 0bb89d0e6..f13a3a464 100644 --- a/packages/mui-base/src/Field/index.barrel.ts +++ b/packages/mui-base/src/Field/index.barrel.ts @@ -1,7 +1,9 @@ export * from './Root/FieldRoot'; export * from './Label/FieldLabel'; export * from './Description/FieldDescription'; +export * from './Control/FieldControl'; export type { FieldRootProps, FieldRootOwnerState } from './Root/FieldRoot.types'; export type * from './Label/FieldLabel.types'; export type * from './Description/FieldDescription.types'; +export type * from './Control/FieldControl.types'; diff --git a/packages/mui-base/src/Field/index.ts b/packages/mui-base/src/Field/index.ts index 3fc41ac49..c0d2af91f 100644 --- a/packages/mui-base/src/Field/index.ts +++ b/packages/mui-base/src/Field/index.ts @@ -1,6 +1,7 @@ export { FieldRoot as Root } from './Root/FieldRoot'; export { FieldLabel as Label } from './Label/FieldLabel'; export { FieldDescription as Description } from './Description/FieldDescription'; +export { FieldControl as Control } from './Control/FieldControl'; export type { FieldRootProps as RootProps, @@ -14,3 +15,7 @@ export type { FieldDescriptionProps as DescriptionProps, FieldDescriptionOwnerState as DescriptionOwnerState, } from './Description/FieldDescription.types'; +export type { + FieldControlProps as ControlProps, + FieldControlOwnerState as ControlOwnerState, +} from './Control/FieldControl.types'; diff --git a/packages/mui-base/src/utils/defaultRenderFunctions.tsx b/packages/mui-base/src/utils/defaultRenderFunctions.tsx index df40b828e..d6dd04091 100644 --- a/packages/mui-base/src/utils/defaultRenderFunctions.tsx +++ b/packages/mui-base/src/utils/defaultRenderFunctions.tsx @@ -28,4 +28,7 @@ export const defaultRenderFunctions = { // eslint-disable-next-line jsx-a11y/label-has-associated-control return