From 36da8e25fe97c56551b4a7c4e08eaed8512d63b1 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 7 Aug 2024 21:49:58 +1000 Subject: [PATCH 01/47] Merge --- .../system/index.js | 76 ++++ .../system/index.tsx | 76 ++++ .../system/index.tsx.preview | 14 + .../components/radio-group/radio-group.md | 45 +++ docs/data/base/pagesApi.js | 8 + .../base-ui/api/radio-group-indicator.json | 17 + docs/pages/base-ui/api/radio-group-item.json | 19 + docs/pages/base-ui/api/radio-group-root.json | 24 ++ .../pages/base-ui/api/use-composite-item.json | 8 + .../base-ui/api/use-composite-list-item.json | 8 + .../api/use-radio-group-indicator.json | 8 + .../base-ui/api/use-radio-group-item.json | 8 + .../base-ui/api/use-radio-group-root.json | 8 + .../react-radio-group/[docsTab]/index.js | 64 ++++ .../radio-group-indicator.json | 13 + .../radio-group-item/radio-group-item.json | 13 + .../radio-group-root/radio-group-root.json | 18 + .../use-composite-item.json | 1 + .../use-composite-list-item.json | 5 + .../use-radio-group-indicator.json | 1 + .../use-radio-group-item.json | 1 + .../use-radio-group-root.json | 1 + docs/translations/translations.json | 1 + .../src/Composite/Item/CompositeItem.tsx | 61 +++ .../src/Composite/Item/CompositeItem.types.ts | 7 + .../src/Composite/Item/useCompositeItem.ts | 35 ++ .../src/Composite/Root/CompositeRoot.tsx | 80 ++++ .../src/Composite/Root/CompositeRoot.types.ts | 11 + .../Composite/Root/CompositeRootContext.ts | 16 + .../src/Composite/Root/useCompositeRoot.ts | 197 ++++++++++ packages/mui-base/src/Composite/composite.ts | 349 ++++++++++++++++++ .../utils/CompositeList/CompositeList.tsx | 91 +++++ .../CompositeList/CompositeList.types.ts | 0 .../CompositeList/CompositeListContext.ts | 20 + .../CompositeList/useCompositeListItem.ts | 73 ++++ .../Indicator/RadioGroupIndicator.tsx | 79 ++++ .../Indicator/RadioGroupIndicator.types.ts | 15 + .../Indicator/useRadioGroupIndicator.ts | 21 ++ .../src/RadioGroup/Item/RadioGroupItem.tsx | 105 ++++++ .../RadioGroup/Item/RadioGroupItem.types.ts | 23 ++ .../RadioGroup/Item/RadioGroupItemContext.ts | 16 + .../src/RadioGroup/Item/useRadioGroupItem.ts | 80 ++++ .../src/RadioGroup/Root/RadioGroupRoot.tsx | 97 +++++ .../RadioGroup/Root/RadioGroupRoot.types.ts | 33 ++ .../RadioGroup/Root/RadioGroupRootContext.ts | 17 + .../src/RadioGroup/Root/useRadioGroupRoot.ts | 43 +++ .../mui-base/src/RadioGroup/index.barrel.ts | 7 + packages/mui-base/src/RadioGroup/index.ts | 16 + 48 files changed, 1929 insertions(+) create mode 100644 docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js create mode 100644 docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx create mode 100644 docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview create mode 100644 docs/data/base/components/radio-group/radio-group.md create mode 100644 docs/pages/base-ui/api/radio-group-indicator.json create mode 100644 docs/pages/base-ui/api/radio-group-item.json create mode 100644 docs/pages/base-ui/api/radio-group-root.json create mode 100644 docs/pages/base-ui/api/use-composite-item.json create mode 100644 docs/pages/base-ui/api/use-composite-list-item.json create mode 100644 docs/pages/base-ui/api/use-radio-group-indicator.json create mode 100644 docs/pages/base-ui/api/use-radio-group-item.json create mode 100644 docs/pages/base-ui/api/use-radio-group-root.json create mode 100644 docs/pages/base-ui/react-radio-group/[docsTab]/index.js create mode 100644 docs/translations/api-docs/radio-group-indicator/radio-group-indicator.json create mode 100644 docs/translations/api-docs/radio-group-item/radio-group-item.json create mode 100644 docs/translations/api-docs/radio-group-root/radio-group-root.json create mode 100644 docs/translations/api-docs/use-composite-item/use-composite-item.json create mode 100644 docs/translations/api-docs/use-composite-list-item/use-composite-list-item.json create mode 100644 docs/translations/api-docs/use-radio-group-indicator/use-radio-group-indicator.json create mode 100644 docs/translations/api-docs/use-radio-group-item/use-radio-group-item.json create mode 100644 docs/translations/api-docs/use-radio-group-root/use-radio-group-root.json create mode 100644 packages/mui-base/src/Composite/Item/CompositeItem.tsx create mode 100644 packages/mui-base/src/Composite/Item/CompositeItem.types.ts create mode 100644 packages/mui-base/src/Composite/Item/useCompositeItem.ts create mode 100644 packages/mui-base/src/Composite/Root/CompositeRoot.tsx create mode 100644 packages/mui-base/src/Composite/Root/CompositeRoot.types.ts create mode 100644 packages/mui-base/src/Composite/Root/CompositeRootContext.ts create mode 100644 packages/mui-base/src/Composite/Root/useCompositeRoot.ts create mode 100644 packages/mui-base/src/Composite/composite.ts create mode 100644 packages/mui-base/src/Composite/utils/CompositeList/CompositeList.tsx create mode 100644 packages/mui-base/src/Composite/utils/CompositeList/CompositeList.types.ts create mode 100644 packages/mui-base/src/Composite/utils/CompositeList/CompositeListContext.ts create mode 100644 packages/mui-base/src/Composite/utils/CompositeList/useCompositeListItem.ts create mode 100644 packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx create mode 100644 packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.types.ts create mode 100644 packages/mui-base/src/RadioGroup/Indicator/useRadioGroupIndicator.ts create mode 100644 packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx create mode 100644 packages/mui-base/src/RadioGroup/Item/RadioGroupItem.types.ts create mode 100644 packages/mui-base/src/RadioGroup/Item/RadioGroupItemContext.ts create mode 100644 packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts create mode 100644 packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx create mode 100644 packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.types.ts create mode 100644 packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts create mode 100644 packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts create mode 100644 packages/mui-base/src/RadioGroup/index.barrel.ts create mode 100644 packages/mui-base/src/RadioGroup/index.ts diff --git a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js new file mode 100644 index 0000000000..6dceb475c9 --- /dev/null +++ b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js @@ -0,0 +1,76 @@ +import * as React from 'react'; +import * as RadioGroup from '@base_ui/react/RadioGroup'; +import { styled } from '@mui/system'; + +export default function UnstyledSwitchIntroduction() { + return ( + + + + Light + + + + Medium + + + + Heavy + + + ); +} + +const grey = { + 100: '#E5EAF2', + 200: '#D8E0E9', + 300: '#CBD4E2', +}; + +const blue = { + 400: '#3399FF', + 600: '#0072E6', + 800: '#004C99', +}; + +const Item = styled(RadioGroup.Item)` + display: flex; + align-items: center; + padding: 8px 16px; + border-radius: 4px; + border: none; + background-color: ${grey[100]}; + color: black; + cursor: pointer; + outline: none; + font-size: 16px; + cursor: default; + + &:hover { + background-color: ${grey[100]}; + } + + &:focus-visible { + outline: 2px solid ${blue[400]}; + outline-offset: 2px; + } + + &[data-state='checked'] { + background-color: ${blue[600]}; + color: white; + } +`; + +const Indicator = styled(RadioGroup.Indicator)` + border-radius: 50%; + width: 8px; + height: 8px; + margin-right: 8px; + outline: 1px solid black; + + &[data-state='checked'] { + background-color: white; + border: none; + outline: none; + } +`; diff --git a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx new file mode 100644 index 0000000000..6dceb475c9 --- /dev/null +++ b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import * as RadioGroup from '@base_ui/react/RadioGroup'; +import { styled } from '@mui/system'; + +export default function UnstyledSwitchIntroduction() { + return ( + + + + Light + + + + Medium + + + + Heavy + + + ); +} + +const grey = { + 100: '#E5EAF2', + 200: '#D8E0E9', + 300: '#CBD4E2', +}; + +const blue = { + 400: '#3399FF', + 600: '#0072E6', + 800: '#004C99', +}; + +const Item = styled(RadioGroup.Item)` + display: flex; + align-items: center; + padding: 8px 16px; + border-radius: 4px; + border: none; + background-color: ${grey[100]}; + color: black; + cursor: pointer; + outline: none; + font-size: 16px; + cursor: default; + + &:hover { + background-color: ${grey[100]}; + } + + &:focus-visible { + outline: 2px solid ${blue[400]}; + outline-offset: 2px; + } + + &[data-state='checked'] { + background-color: ${blue[600]}; + color: white; + } +`; + +const Indicator = styled(RadioGroup.Indicator)` + border-radius: 50%; + width: 8px; + height: 8px; + margin-right: 8px; + outline: 1px solid black; + + &[data-state='checked'] { + background-color: white; + border: none; + outline: none; + } +`; diff --git a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview new file mode 100644 index 0000000000..003e293a65 --- /dev/null +++ b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview @@ -0,0 +1,14 @@ + + + + Light + + + + Medium + + + + Heavy + + \ No newline at end of file diff --git a/docs/data/base/components/radio-group/radio-group.md b/docs/data/base/components/radio-group/radio-group.md new file mode 100644 index 0000000000..a00e933fde --- /dev/null +++ b/docs/data/base/components/radio-group/radio-group.md @@ -0,0 +1,45 @@ +--- +productId: base-ui +title: React Radio Group component +components: RadioGroupRoot, RadioGroupItem, RadioGroupIndicator +githubLabel: 'component: radio' +waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/radio/ +--- + +# Radio Group + +

Radio Groups contain a set of checkable (radio) buttons where only one of the buttons can be checked at a time.

+ +{{"component": "@mui/docs/ComponentLinkHeader", "design": false}} + +{{"component": "modules/components/ComponentPageTabs.js"}} + +## Introduction + +{{"demo": "UnstyledRadioGroupIntroduction", "defaultCodeOpen": false, "bg": "gradient"}} + +## Installation + +Base UI components are all available as a single package. + + + +```bash npm +npm install @base_ui/react +``` + +```bash yarn +yarn add @base_ui/react +``` + +```bash pnpm +pnpm add @base_ui/react +``` + + + +Once you have the package installed, import the component. + +```ts +import * as RadioGroup from '@base_ui/react/RadioGroup'; +``` diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js index 499fad712c..056a8c894f 100644 --- a/docs/data/base/pagesApi.js +++ b/docs/data/base/pagesApi.js @@ -229,6 +229,14 @@ module.exports = [ pathname: '/base-ui/react-progress/components-api/#progress-track', title: 'ProgressTrack', }, + { + pathname: '/base-ui/react-radio-group/components-api/#radio-group-item', + title: 'RadioGroupItem', + }, + { + pathname: '/base-ui/react-radio-group/components-api/#radio-group-root', + title: 'RadioGroupRoot', + }, { pathname: '/base-ui/react-select/components-api/#select', title: 'Select' }, { pathname: '/base-ui/react-slider/components-api/#slider-control', diff --git a/docs/pages/base-ui/api/radio-group-indicator.json b/docs/pages/base-ui/api/radio-group-indicator.json new file mode 100644 index 0000000000..63e3516003 --- /dev/null +++ b/docs/pages/base-ui/api/radio-group-indicator.json @@ -0,0 +1,17 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "keepMounted": { "type": { "name": "bool" }, "default": "false" }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "RadioGroupIndicator", + "imports": [ + "import * as RadioGroup from '@base_ui/react/RadioGroup';\nconst RadioGroupIndicator = RadioGroup.Indicator;" + ], + "classes": [], + "muiName": "RadioGroupIndicator", + "filename": "/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/radio-group-item.json b/docs/pages/base-ui/api/radio-group-item.json new file mode 100644 index 0000000000..4dcdbcb703 --- /dev/null +++ b/docs/pages/base-ui/api/radio-group-item.json @@ -0,0 +1,19 @@ +{ + "props": { + "value": { "type": { "name": "string" }, "required": true }, + "className": { "type": { "name": "union", "description": "func
| string" } }, + "disabled": { "type": { "name": "bool" }, "default": "false" }, + "render": { "type": { "name": "union", "description": "element
| func" } }, + "required": { "type": { "name": "bool" } } + }, + "name": "RadioGroupItem", + "imports": [ + "import * as RadioGroup from '@base_ui/react/RadioGroup';\nconst RadioGroupItem = RadioGroup.Item;" + ], + "classes": [], + "muiName": "RadioGroupItem", + "filename": "/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/radio-group-root.json b/docs/pages/base-ui/api/radio-group-root.json new file mode 100644 index 0000000000..2ca6d80b65 --- /dev/null +++ b/docs/pages/base-ui/api/radio-group-root.json @@ -0,0 +1,24 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "defaultValue": { "type": { "name": "string" } }, + "disabled": { "type": { "name": "bool" }, "default": "false" }, + "name": { "type": { "name": "string" } }, + "onValueChange": { "type": { "name": "func" } }, + "orientation": { + "type": { "name": "enum", "description": "'horizontal'
| 'vertical'" } + }, + "render": { "type": { "name": "union", "description": "element
| func" } }, + "value": { "type": { "name": "string" } } + }, + "name": "RadioGroupRoot", + "imports": [ + "import * as RadioGroup from '@base_ui/react/RadioGroup';\nconst RadioGroupRoot = RadioGroup.Root;" + ], + "classes": [], + "muiName": "RadioGroupRoot", + "filename": "/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/use-composite-item.json b/docs/pages/base-ui/api/use-composite-item.json new file mode 100644 index 0000000000..240b1d950c --- /dev/null +++ b/docs/pages/base-ui/api/use-composite-item.json @@ -0,0 +1,8 @@ +{ + "parameters": {}, + "returnValue": {}, + "name": "useCompositeItem", + "filename": "/packages/mui-base/src/Composite/Item/useCompositeItem.ts", + "imports": ["import { useCompositeItem } from '@base_ui/react/Composite';"], + "demos": "" +} diff --git a/docs/pages/base-ui/api/use-composite-list-item.json b/docs/pages/base-ui/api/use-composite-list-item.json new file mode 100644 index 0000000000..21819ac9c1 --- /dev/null +++ b/docs/pages/base-ui/api/use-composite-list-item.json @@ -0,0 +1,8 @@ +{ + "parameters": {}, + "returnValue": {}, + "name": "useCompositeListItem", + "filename": "/packages/mui-base/src/Composite/utils/CompositeList/useCompositeListItem.ts", + "imports": ["import { useCompositeListItem } from '@base_ui/react/Composite';"], + "demos": "" +} diff --git a/docs/pages/base-ui/api/use-radio-group-indicator.json b/docs/pages/base-ui/api/use-radio-group-indicator.json new file mode 100644 index 0000000000..1fee5f8eac --- /dev/null +++ b/docs/pages/base-ui/api/use-radio-group-indicator.json @@ -0,0 +1,8 @@ +{ + "parameters": {}, + "returnValue": {}, + "name": "useRadioGroupIndicator", + "filename": "/packages/mui-base/src/RadioGroup/Indicator/useRadioGroupIndicator.ts", + "imports": ["import { useRadioGroupIndicator } from '@base_ui/react/RadioGroup';"], + "demos": "" +} diff --git a/docs/pages/base-ui/api/use-radio-group-item.json b/docs/pages/base-ui/api/use-radio-group-item.json new file mode 100644 index 0000000000..6228aa20a2 --- /dev/null +++ b/docs/pages/base-ui/api/use-radio-group-item.json @@ -0,0 +1,8 @@ +{ + "parameters": {}, + "returnValue": {}, + "name": "useRadioGroupItem", + "filename": "/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts", + "imports": ["import { useRadioGroupItem } from '@base_ui/react/RadioGroup';"], + "demos": "" +} diff --git a/docs/pages/base-ui/api/use-radio-group-root.json b/docs/pages/base-ui/api/use-radio-group-root.json new file mode 100644 index 0000000000..30c3f00e5a --- /dev/null +++ b/docs/pages/base-ui/api/use-radio-group-root.json @@ -0,0 +1,8 @@ +{ + "parameters": {}, + "returnValue": {}, + "name": "useRadioGroupRoot", + "filename": "/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts", + "imports": ["import { useRadioGroupRoot } from '@base_ui/react/RadioGroup';"], + "demos": "" +} diff --git a/docs/pages/base-ui/react-radio-group/[docsTab]/index.js b/docs/pages/base-ui/react-radio-group/[docsTab]/index.js new file mode 100644 index 0000000000..97d03308ee --- /dev/null +++ b/docs/pages/base-ui/react-radio-group/[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/radio-group/radio-group.md?@mui/markdown'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import RadioGroupIndicatorApiJsonPageContent from '../../api/radio-group-indicator.json'; +import RadioGroupItemApiJsonPageContent from '../../api/radio-group-item.json'; +import RadioGroupRootApiJsonPageContent from '../../api/radio-group-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 RadioGroupIndicatorApiReq = require.context( + 'docs-base/translations/api-docs/radio-group-indicator', + false, + /\.\/radio-group-indicator.*.json$/, + ); + const RadioGroupIndicatorApiDescriptions = mapApiPageTranslations(RadioGroupIndicatorApiReq); + + const RadioGroupItemApiReq = require.context( + 'docs-base/translations/api-docs/radio-group-item', + false, + /\.\/radio-group-item.*.json$/, + ); + const RadioGroupItemApiDescriptions = mapApiPageTranslations(RadioGroupItemApiReq); + + const RadioGroupRootApiReq = require.context( + 'docs-base/translations/api-docs/radio-group-root', + false, + /\.\/radio-group-root.*.json$/, + ); + const RadioGroupRootApiDescriptions = mapApiPageTranslations(RadioGroupRootApiReq); + + return { + props: { + componentsApiDescriptions: { + RadioGroupIndicator: RadioGroupIndicatorApiDescriptions, + RadioGroupItem: RadioGroupItemApiDescriptions, + RadioGroupRoot: RadioGroupRootApiDescriptions, + }, + componentsApiPageContents: { + RadioGroupIndicator: RadioGroupIndicatorApiJsonPageContent, + RadioGroupItem: RadioGroupItemApiJsonPageContent, + RadioGroupRoot: RadioGroupRootApiJsonPageContent, + }, + hooksApiDescriptions: {}, + hooksApiPageContents: {}, + }, + }; +}; diff --git a/docs/translations/api-docs/radio-group-indicator/radio-group-indicator.json b/docs/translations/api-docs/radio-group-indicator/radio-group-indicator.json new file mode 100644 index 0000000000..f7dac21e50 --- /dev/null +++ b/docs/translations/api-docs/radio-group-indicator/radio-group-indicator.json @@ -0,0 +1,13 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "keepMounted": { + "description": "If true, the indicator stays mounted when unchecked. Useful for CSS animations." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/radio-group-item/radio-group-item.json b/docs/translations/api-docs/radio-group-item/radio-group-item.json new file mode 100644 index 0000000000..ed35ab8414 --- /dev/null +++ b/docs/translations/api-docs/radio-group-item/radio-group-item.json @@ -0,0 +1,13 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "disabled": { "description": "Whether the item is disabled." }, + "render": { "description": "A function to customize rendering of the component." }, + "required": { "description": "Determines if the item is required." }, + "value": { "description": "The value of the item identified in the radio group." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/radio-group-root/radio-group-root.json b/docs/translations/api-docs/radio-group-root/radio-group-root.json new file mode 100644 index 0000000000..69470284bc --- /dev/null +++ b/docs/translations/api-docs/radio-group-root/radio-group-root.json @@ -0,0 +1,18 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "defaultValue": { + "description": "The default value of the selected radio button. Use when uncontrolled." + }, + "disabled": { "description": "Whether the radio group is disabled." }, + "name": { "description": "The name of the radio group submitted with the form data." }, + "onValueChange": { "description": "Callback fired when the value changes." }, + "orientation": { "description": "The orientation of the radio group." }, + "render": { "description": "A function to customize rendering of the component." }, + "value": { "description": "The value of the selected radio button. Use when controlled." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/use-composite-item/use-composite-item.json b/docs/translations/api-docs/use-composite-item/use-composite-item.json new file mode 100644 index 0000000000..e3eb65c6e4 --- /dev/null +++ b/docs/translations/api-docs/use-composite-item/use-composite-item.json @@ -0,0 +1 @@ +{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/api-docs/use-composite-list-item/use-composite-list-item.json b/docs/translations/api-docs/use-composite-list-item/use-composite-list-item.json new file mode 100644 index 0000000000..6b04319799 --- /dev/null +++ b/docs/translations/api-docs/use-composite-list-item/use-composite-list-item.json @@ -0,0 +1,5 @@ +{ + "hookDescription": "Used to register a list item and its index (DOM position) in the\n`CompositeList`.", + "parametersDescriptions": {}, + "returnValueDescriptions": {} +} diff --git a/docs/translations/api-docs/use-radio-group-indicator/use-radio-group-indicator.json b/docs/translations/api-docs/use-radio-group-indicator/use-radio-group-indicator.json new file mode 100644 index 0000000000..e3eb65c6e4 --- /dev/null +++ b/docs/translations/api-docs/use-radio-group-indicator/use-radio-group-indicator.json @@ -0,0 +1 @@ +{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/api-docs/use-radio-group-item/use-radio-group-item.json b/docs/translations/api-docs/use-radio-group-item/use-radio-group-item.json new file mode 100644 index 0000000000..e3eb65c6e4 --- /dev/null +++ b/docs/translations/api-docs/use-radio-group-item/use-radio-group-item.json @@ -0,0 +1 @@ +{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/api-docs/use-radio-group-root/use-radio-group-root.json b/docs/translations/api-docs/use-radio-group-root/use-radio-group-root.json new file mode 100644 index 0000000000..e3eb65c6e4 --- /dev/null +++ b/docs/translations/api-docs/use-radio-group-root/use-radio-group-root.json @@ -0,0 +1 @@ +{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/translations.json b/docs/translations/translations.json index 20c6cc2adf..d5d41364d5 100644 --- a/docs/translations/translations.json +++ b/docs/translations/translations.json @@ -222,6 +222,7 @@ "inputs": "Inputs", "/base-ui/react-checkbox": "Checkbox", "/base-ui/react-number-field": "Number Field", + "/base-ui/react-radio-group": "Radio Group", "/base-ui/react-slider": "Slider", "/base-ui/react-switch": "Switch", "data-display": "Data display", diff --git a/packages/mui-base/src/Composite/Item/CompositeItem.tsx b/packages/mui-base/src/Composite/Item/CompositeItem.tsx new file mode 100644 index 0000000000..c6cd31ce63 --- /dev/null +++ b/packages/mui-base/src/Composite/Item/CompositeItem.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useForkRef } from '../../utils/useForkRef'; +import { useCompositeRootContext } from '../Root/CompositeRootContext'; +import type { CompositeItemOwnerState, CompositeItemProps } from './CompositeItem.types'; +import { useCompositeItem } from './useCompositeItem'; + +/** + * @ignore - internal component. + */ +const CompositeItem = React.forwardRef(function CompositeItem( + props: CompositeItemProps, + forwardedRef: React.ForwardedRef, +) { + const { render, className, ...otherProps } = props; + + const { activeIndex } = useCompositeRootContext(); + const { getItemProps, ref, index } = useCompositeItem(); + + const ownerState: CompositeItemOwnerState = React.useMemo( + () => ({ + active: index === activeIndex, + }), + [index, activeIndex], + ); + + const mergedRef = useForkRef(forwardedRef, ref); + + const { renderElement } = useComponentRenderer({ + propGetter: getItemProps, + ref: mergedRef, + render: render ?? 'div', + ownerState, + className, + extraProps: otherProps, + }); + + return renderElement(); +}); + +CompositeItem.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 { CompositeItem }; diff --git a/packages/mui-base/src/Composite/Item/CompositeItem.types.ts b/packages/mui-base/src/Composite/Item/CompositeItem.types.ts new file mode 100644 index 0000000000..a4223f30b3 --- /dev/null +++ b/packages/mui-base/src/Composite/Item/CompositeItem.types.ts @@ -0,0 +1,7 @@ +import type { BaseUIComponentProps } from '../../utils/types'; + +export type CompositeItemOwnerState = { + active: boolean; +}; + +export interface CompositeItemProps extends BaseUIComponentProps<'div', CompositeItemOwnerState> {} diff --git a/packages/mui-base/src/Composite/Item/useCompositeItem.ts b/packages/mui-base/src/Composite/Item/useCompositeItem.ts new file mode 100644 index 0000000000..320191360c --- /dev/null +++ b/packages/mui-base/src/Composite/Item/useCompositeItem.ts @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { useCompositeRootContext } from '../Root/CompositeRootContext'; +import { useCompositeListItem } from '../utils/CompositeList/useCompositeListItem'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +/** + * + * API: + * + * - [useCompositeItem API](https://mui.com/base-ui/api/use-composite-item/) + */ +export function useCompositeItem() { + const { activeIndex, onActiveIndexChange } = useCompositeRootContext(); + const { ref, index } = useCompositeListItem(); + const isActive = activeIndex === index; + + const getItemProps = React.useCallback( + (externalProps = {}) => + mergeReactProps<'div'>(externalProps, { + tabIndex: isActive ? 0 : -1, + onFocus() { + onActiveIndexChange(index); + }, + }), + [isActive, index, onActiveIndexChange], + ); + + return React.useMemo( + () => ({ + getItemProps, + ref, + index, + }), + [getItemProps, ref, index], + ); +} diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.tsx b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx new file mode 100644 index 0000000000..89976d1d89 --- /dev/null +++ b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx @@ -0,0 +1,80 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { CompositeList } from '../utils/CompositeList/CompositeList'; +import type { CompositeRootProps } from './CompositeRoot.types'; +import { useCompositeRoot } from './useCompositeRoot'; +import { CompositeRootContext, type CompositeRootContextValue } from './CompositeRootContext'; + +/** + * @ignore - internal component. + */ +const CompositeRoot = React.forwardRef(function CompositeRoot( + props: CompositeRootProps, + forwardedRef: React.ForwardedRef, +) { + const { render, className, ...otherProps } = props; + + const { getRootProps, activeIndex, onActiveIndexChange, elementsRef } = useCompositeRoot(props); + + const { renderElement } = useComponentRenderer({ + propGetter: getRootProps, + ref: forwardedRef, + render: render ?? 'div', + ownerState: {}, + className, + extraProps: otherProps, + }); + + const contextValue: CompositeRootContextValue = React.useMemo( + () => ({ activeIndex, onActiveIndexChange }), + [activeIndex, onActiveIndexChange], + ); + + return ( + + {renderElement()} + + ); +}); + +CompositeRoot.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 + */ + activeIndex: PropTypes.number, + /** + * @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 + */ + cols: PropTypes.number, + /** + * @ignore + */ + loop: PropTypes.bool, + /** + * @ignore + */ + onActiveIndexChange: PropTypes.func, + /** + * @ignore + */ + orientation: PropTypes.oneOf(['horizontal', 'vertical']), + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { CompositeRoot }; diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts b/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts new file mode 100644 index 0000000000..51d3884773 --- /dev/null +++ b/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts @@ -0,0 +1,11 @@ +import type { BaseUIComponentProps } from '../../utils/types'; + +export type CompositeRootOwnerState = {}; + +export interface CompositeRootProps extends BaseUIComponentProps<'div', CompositeRootOwnerState> { + orientation?: 'horizontal' | 'vertical'; + cols?: number; + loop?: boolean; + activeIndex?: number; + onActiveIndexChange?: (index: number) => void; +} diff --git a/packages/mui-base/src/Composite/Root/CompositeRootContext.ts b/packages/mui-base/src/Composite/Root/CompositeRootContext.ts new file mode 100644 index 0000000000..f97f3f35bd --- /dev/null +++ b/packages/mui-base/src/Composite/Root/CompositeRootContext.ts @@ -0,0 +1,16 @@ +import * as React from 'react'; + +export interface CompositeRootContextValue { + activeIndex: number; + onActiveIndexChange: (index: number) => void; +} + +export const CompositeRootContext = React.createContext(null); + +export function useCompositeRootContext() { + const context = React.useContext(CompositeRootContext); + if (context === null) { + throw new Error(' must be used within '); + } + return context; +} diff --git a/packages/mui-base/src/Composite/Root/useCompositeRoot.ts b/packages/mui-base/src/Composite/Root/useCompositeRoot.ts new file mode 100644 index 0000000000..7435cb60d7 --- /dev/null +++ b/packages/mui-base/src/Composite/Root/useCompositeRoot.ts @@ -0,0 +1,197 @@ +import * as React from 'react'; +import { useEventCallback } from '../../utils/useEventCallback'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { + ALL_KEYS, + ARROW_DOWN, + ARROW_LEFT, + ARROW_RIGHT, + ARROW_UP, + buildCellMap, + findNonDisabledIndex, + getCellIndexOfCorner, + getCellIndices, + getGridNavigatedIndex, + getMaxIndex, + getMinIndex, + HORIZONTAL_KEYS, + isDisabled, + isIndexOutOfBounds, + VERTICAL_KEYS, + type Dimensions, +} from '../composite'; + +export interface UseCompositeRootParameters { + orientation?: 'horizontal' | 'vertical'; + cols?: number; + loop?: boolean; + activeIndex?: number; + onActiveIndexChange?: (index: number) => void; + dense?: boolean; + itemSizes?: Array; +} + +// TODO +const disabledIndices = undefined; + +/** + * @ignore - internal hook. + */ +export function useCompositeRoot(params: UseCompositeRootParameters) { + const { + itemSizes, + cols = 1, + loop = true, + dense = false, + orientation = 'horizontal', + activeIndex: externalActiveIndex, + onActiveIndexChange: externalSetActiveIndex, + } = params; + + const [internalActiveIndex, internalSetActiveIndex] = React.useState(0); + + const activeIndex = externalActiveIndex ?? internalActiveIndex; + const onActiveIndexChange = useEventCallback(externalSetActiveIndex ?? internalSetActiveIndex); + const elementsRef = React.useRef>([]); + const isGrid = cols > 1; + + const getRootProps = React.useCallback( + (externalProps = {}) => + mergeReactProps<'div'>(externalProps, { + 'aria-orientation': orientation, + onKeyDown(event) { + if (!ALL_KEYS.includes(event.key)) { + return; + } + + let nextIndex = activeIndex; + const minIndex = getMinIndex(elementsRef, disabledIndices); + const maxIndex = getMaxIndex(elementsRef, disabledIndices); + + if (isGrid) { + const sizes = + itemSizes || + Array.from({ length: elementsRef.current.length }, () => ({ + width: 1, + height: 1, + })); + // To calculate movements on the grid, we use hypothetical cell indices + // as if every item was 1x1, then convert back to real indices. + const cellMap = buildCellMap(sizes, cols, dense); + const minGridIndex = cellMap.findIndex( + (index) => index != null && !isDisabled(elementsRef.current, index, disabledIndices), + ); + // last enabled index + const maxGridIndex = cellMap.reduce( + (foundIndex: number, index, cellIndex) => + index != null && !isDisabled(elementsRef.current, index, disabledIndices) + ? cellIndex + : foundIndex, + -1, + ); + + nextIndex = cellMap[ + getGridNavigatedIndex( + { + current: cellMap.map((itemIndex) => + itemIndex ? elementsRef.current[itemIndex] : null, + ), + }, + { + event, + orientation, + loop, + cols, + // treat undefined (empty grid spaces) as disabled indices so we + // don't end up in them + disabledIndices: getCellIndices( + [ + ...(disabledIndices || + elementsRef.current.map((_, index) => + isDisabled(elementsRef.current, index) ? index : undefined, + )), + undefined, + ], + cellMap, + ), + minIndex: minGridIndex, + maxIndex: maxGridIndex, + prevIndex: getCellIndexOfCorner( + activeIndex > maxIndex ? minIndex : activeIndex, + sizes, + cellMap, + cols, + // use a corner matching the edge closest to the direction we're + // moving in so we don't end up in the same item. Prefer + // top/left over bottom/right. + // eslint-disable-next-line no-nested-ternary + event.key === ARROW_DOWN ? 'bl' : event.key === ARROW_RIGHT ? 'tr' : 'tl', + ), + }, + ) + ] as number; // navigated cell will never be nullish + } + + const toEndKeys = { + horizontal: [ARROW_RIGHT], + vertical: [ARROW_DOWN], + both: [ARROW_RIGHT, ARROW_DOWN], + }[orientation]; + + const toStartKeys = { + horizontal: [ARROW_LEFT], + vertical: [ARROW_UP], + both: [ARROW_LEFT, ARROW_UP], + }[orientation]; + + const preventedKeys = isGrid + ? ALL_KEYS + : { + horizontal: HORIZONTAL_KEYS, + vertical: VERTICAL_KEYS, + both: ALL_KEYS, + }[orientation]; + + if (nextIndex === activeIndex && [...toEndKeys, ...toStartKeys].includes(event.key)) { + if (loop && nextIndex === maxIndex && toEndKeys.includes(event.key)) { + nextIndex = minIndex; + } else if (loop && nextIndex === minIndex && toStartKeys.includes(event.key)) { + nextIndex = maxIndex; + } else { + nextIndex = findNonDisabledIndex(elementsRef, { + startingIndex: nextIndex, + decrement: toStartKeys.includes(event.key), + disabledIndices, + }); + } + } + + if (nextIndex !== activeIndex && !isIndexOutOfBounds(elementsRef, nextIndex)) { + event.stopPropagation(); + + if (preventedKeys.includes(event.key)) { + event.preventDefault(); + } + + onActiveIndexChange(nextIndex); + + // Wait for FocusManager `returnFocus` to execute. + queueMicrotask(() => { + elementsRef.current[nextIndex]?.focus(); + }); + } + }, + }), + [activeIndex, cols, dense, isGrid, itemSizes, loop, onActiveIndexChange, orientation], + ); + + return React.useMemo( + () => ({ + getRootProps, + activeIndex, + onActiveIndexChange, + elementsRef, + }), + [getRootProps, activeIndex, onActiveIndexChange], + ); +} diff --git a/packages/mui-base/src/Composite/composite.ts b/packages/mui-base/src/Composite/composite.ts new file mode 100644 index 0000000000..402c329c9a --- /dev/null +++ b/packages/mui-base/src/Composite/composite.ts @@ -0,0 +1,349 @@ +import * as React from 'react'; + +export interface Dimensions { + width: number; + height: number; +} + +export const ARROW_UP = 'ArrowUp'; +export const ARROW_DOWN = 'ArrowDown'; +export const ARROW_LEFT = 'ArrowLeft'; +export const ARROW_RIGHT = 'ArrowRight'; + +export const HORIZONTAL_KEYS = [ARROW_LEFT, ARROW_RIGHT]; +export const VERTICAL_KEYS = [ARROW_UP, ARROW_DOWN]; +export const ALL_KEYS = [...HORIZONTAL_KEYS, ...VERTICAL_KEYS]; + +function stopEvent(event: Event | React.SyntheticEvent) { + event.preventDefault(); + event.stopPropagation(); +} + +export function isDifferentRow(index: number, cols: number, prevRow: number) { + return Math.floor(index / cols) !== prevRow; +} + +export function isIndexOutOfBounds( + listRef: React.MutableRefObject>, + index: number, +) { + return index < 0 || index >= listRef.current.length; +} + +export function getMinIndex( + listRef: React.MutableRefObject>, + disabledIndices: Array | undefined, +) { + return findNonDisabledIndex(listRef, { disabledIndices }); +} + +export function getMaxIndex( + listRef: React.MutableRefObject>, + disabledIndices: Array | undefined, +) { + return findNonDisabledIndex(listRef, { + decrement: true, + startingIndex: listRef.current.length, + disabledIndices, + }); +} + +export function findNonDisabledIndex( + listRef: React.MutableRefObject>, + { + startingIndex = -1, + decrement = false, + disabledIndices, + amount = 1, + }: { + startingIndex?: number; + decrement?: boolean; + disabledIndices?: Array; + amount?: number; + } = {}, +): number { + const list = listRef.current; + + let index = startingIndex; + do { + index += decrement ? -amount : amount; + } while (index >= 0 && index <= list.length - 1 && isDisabled(list, index, disabledIndices)); + + return index; +} + +export function getGridNavigatedIndex( + elementsRef: React.MutableRefObject>, + { + event, + orientation, + loop, + cols, + disabledIndices, + minIndex, + maxIndex, + prevIndex, + stopEvent: stop = false, + }: { + event: React.KeyboardEvent; + orientation: 'horizontal' | 'vertical' | 'both'; + loop: boolean; + cols: number; + disabledIndices: Array | undefined; + minIndex: number; + maxIndex: number; + prevIndex: number; + stopEvent?: boolean; + }, +) { + let nextIndex = prevIndex; + + if (event.key === ARROW_UP) { + if (stop) { + stopEvent(event); + } + + if (prevIndex === -1) { + nextIndex = maxIndex; + } else { + nextIndex = findNonDisabledIndex(elementsRef, { + startingIndex: nextIndex, + amount: cols, + decrement: true, + disabledIndices, + }); + + if (loop && (prevIndex - cols < minIndex || nextIndex < 0)) { + const col = prevIndex % cols; + const maxCol = maxIndex % cols; + const offset = maxIndex - (maxCol - col); + + if (maxCol === col) { + nextIndex = maxIndex; + } else { + nextIndex = maxCol > col ? offset : offset - cols; + } + } + } + + if (isIndexOutOfBounds(elementsRef, nextIndex)) { + nextIndex = prevIndex; + } + } + + if (event.key === ARROW_DOWN) { + if (stop) { + stopEvent(event); + } + + if (prevIndex === -1) { + nextIndex = minIndex; + } else { + nextIndex = findNonDisabledIndex(elementsRef, { + startingIndex: prevIndex, + amount: cols, + disabledIndices, + }); + + if (loop && prevIndex + cols > maxIndex) { + nextIndex = findNonDisabledIndex(elementsRef, { + startingIndex: (prevIndex % cols) - cols, + amount: cols, + disabledIndices, + }); + } + } + + if (isIndexOutOfBounds(elementsRef, nextIndex)) { + nextIndex = prevIndex; + } + } + + // Remains on the same row/column. + if (orientation === 'both') { + const prevRow = Math.floor(prevIndex / cols); + + if (event.key === ARROW_RIGHT) { + if (stop) { + stopEvent(event); + } + + if (prevIndex % cols !== cols - 1) { + nextIndex = findNonDisabledIndex(elementsRef, { + startingIndex: prevIndex, + disabledIndices, + }); + + if (loop && isDifferentRow(nextIndex, cols, prevRow)) { + nextIndex = findNonDisabledIndex(elementsRef, { + startingIndex: prevIndex - (prevIndex % cols) - 1, + disabledIndices, + }); + } + } else if (loop) { + nextIndex = findNonDisabledIndex(elementsRef, { + startingIndex: prevIndex - (prevIndex % cols) - 1, + disabledIndices, + }); + } + + if (isDifferentRow(nextIndex, cols, prevRow)) { + nextIndex = prevIndex; + } + } + + if (event.key === ARROW_LEFT) { + if (stop) { + stopEvent(event); + } + + if (prevIndex % cols !== 0) { + nextIndex = findNonDisabledIndex(elementsRef, { + startingIndex: prevIndex, + decrement: true, + disabledIndices, + }); + + if (loop && isDifferentRow(nextIndex, cols, prevRow)) { + nextIndex = findNonDisabledIndex(elementsRef, { + startingIndex: prevIndex + (cols - (prevIndex % cols)), + decrement: true, + disabledIndices, + }); + } + } else if (loop) { + nextIndex = findNonDisabledIndex(elementsRef, { + startingIndex: prevIndex + (cols - (prevIndex % cols)), + decrement: true, + disabledIndices, + }); + } + + if (isDifferentRow(nextIndex, cols, prevRow)) { + nextIndex = prevIndex; + } + } + + const lastRow = Math.floor(maxIndex / cols) === prevRow; + + if (isIndexOutOfBounds(elementsRef, nextIndex)) { + if (loop && lastRow) { + nextIndex = + event.key === ARROW_LEFT + ? maxIndex + : findNonDisabledIndex(elementsRef, { + startingIndex: prevIndex - (prevIndex % cols) - 1, + disabledIndices, + }); + } else { + nextIndex = prevIndex; + } + } + } + + return nextIndex; +} + +/** For each cell index, gets the item index that occupies that cell */ +export function buildCellMap( + sizes: Array<{ width: number; height: number }>, + cols: number, + dense: boolean, +) { + const cellMap: (number | undefined)[] = []; + let startIndex = 0; + + sizes.forEach(({ width, height }, index) => { + if (width > cols) { + if (process.env.NODE_ENV !== 'production') { + throw new Error( + `[Base UI]: Invalid grid - item width at index ${index} is greater than grid columns`, + ); + } + } + + let itemPlaced = false; + if (dense) { + startIndex = 0; + } + while (!itemPlaced) { + const targetCells: number[] = []; + for (let i = 0; i < width; i += 1) { + for (let j = 0; j < height; j += 1) { + targetCells.push(startIndex + i + j * cols); + } + } + if ( + (startIndex % cols) + width <= cols && + targetCells.every((cell) => cellMap[cell] == null) + ) { + targetCells.forEach((cell) => { + cellMap[cell] = index; + }); + itemPlaced = true; + } else { + startIndex += 1; + } + } + }); + + // convert into a non-sparse array + return [...cellMap]; +} + +/** Gets cell index of an item's corner or -1 when index is -1. */ +export function getCellIndexOfCorner( + index: number, + sizes: Dimensions[], + cellMap: (number | undefined)[], + cols: number, + corner: 'tl' | 'tr' | 'bl' | 'br', +) { + if (index === -1) { + return -1; + } + + const firstCellIndex = cellMap.indexOf(index); + const sizeItem = sizes[index]; + + switch (corner) { + case 'tl': + return firstCellIndex; + case 'tr': + if (!sizeItem) { + return firstCellIndex; + } + return firstCellIndex + sizeItem.width - 1; + case 'bl': + if (!sizeItem) { + return firstCellIndex; + } + return firstCellIndex + (sizeItem.height - 1) * cols; + case 'br': + return cellMap.lastIndexOf(index); + default: + return -1; + } +} + +/** Gets all cell indices that correspond to the specified indices */ +export function getCellIndices(indices: (number | undefined)[], cellMap: (number | undefined)[]) { + return cellMap.flatMap((index, cellIndex) => (indices.includes(index) ? [cellIndex] : [])); +} + +export function isDisabled( + list: Array, + index: number, + disabledIndices?: Array, +) { + if (disabledIndices) { + return disabledIndices.includes(index); + } + + const element = list[index]; + return ( + element == null || + element.hasAttribute('disabled') || + element.getAttribute('aria-disabled') === 'true' + ); +} diff --git a/packages/mui-base/src/Composite/utils/CompositeList/CompositeList.tsx b/packages/mui-base/src/Composite/utils/CompositeList/CompositeList.tsx new file mode 100644 index 0000000000..bbafb9f11b --- /dev/null +++ b/packages/mui-base/src/Composite/utils/CompositeList/CompositeList.tsx @@ -0,0 +1,91 @@ +/* eslint-disable no-bitwise */ +import * as React from 'react'; +import { useEnhancedEffect } from '../../../utils/useEnhancedEffect'; +import { CompositeListContext } from './CompositeListContext'; + +function sortByDocumentPosition(a: Node, b: Node) { + const position = a.compareDocumentPosition(b); + + if ( + position & Node.DOCUMENT_POSITION_FOLLOWING || + position & Node.DOCUMENT_POSITION_CONTAINED_BY + ) { + return -1; + } + + if (position & Node.DOCUMENT_POSITION_PRECEDING || position & Node.DOCUMENT_POSITION_CONTAINS) { + return 1; + } + + return 0; +} + +function areMapsEqual(map1: Map, map2: Map) { + if (map1.size !== map2.size) { + return false; + } + for (const [key, value] of map1.entries()) { + if (value !== map2.get(key)) { + return false; + } + } + return true; +} + +interface CompositeListProps { + children: React.ReactNode; + /** + * A ref to the list of HTML elements, ordered by their index. + * `useListNavigation`'s `listRef` prop. + */ + elementsRef: React.MutableRefObject>; + /** + * A ref to the list of element labels, ordered by their index. + * `useTypeahead`'s `listRef` prop. + */ + labelsRef?: React.MutableRefObject>; +} + +/** + * Provides context for a list of items in a composite component. + * @ignore - internal component. + */ +export function CompositeList(props: CompositeListProps) { + const { children, elementsRef, labelsRef } = props; + + const [map, setMap] = React.useState(() => new Map()); + + const register = React.useCallback((node: Node) => { + setMap((prevMap) => new Map(prevMap).set(node, null)); + }, []); + + const unregister = React.useCallback((node: Node) => { + setMap((prevMap) => { + const nextMap = new Map(prevMap); + nextMap.delete(node); + return nextMap; + }); + }, []); + + useEnhancedEffect(() => { + const newMap = new Map(map); + const nodes = Array.from(newMap.keys()).sort(sortByDocumentPosition); + + nodes.forEach((node, index) => { + newMap.set(node, index); + }); + + if (!areMapsEqual(map, newMap)) { + setMap(newMap); + } + }, [map]); + + const contextValue = React.useMemo( + () => ({ register, unregister, map, elementsRef, labelsRef }), + [register, unregister, map, elementsRef, labelsRef], + ); + + return ( + {children} + ); +} diff --git a/packages/mui-base/src/Composite/utils/CompositeList/CompositeList.types.ts b/packages/mui-base/src/Composite/utils/CompositeList/CompositeList.types.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/mui-base/src/Composite/utils/CompositeList/CompositeListContext.ts b/packages/mui-base/src/Composite/utils/CompositeList/CompositeListContext.ts new file mode 100644 index 0000000000..0d8cd305c2 --- /dev/null +++ b/packages/mui-base/src/Composite/utils/CompositeList/CompositeListContext.ts @@ -0,0 +1,20 @@ +import * as React from 'react'; + +export interface CompositeListContextValue { + register: (node: Node) => void; + unregister: (node: Node) => void; + map: Map; + elementsRef: React.MutableRefObject>; + labelsRef?: React.MutableRefObject>; +} + +export const CompositeListContext = React.createContext({ + register: () => {}, + unregister: () => {}, + map: new Map(), + elementsRef: { current: [] }, +}); + +export function useCompositeListContext() { + return React.useContext(CompositeListContext); +} diff --git a/packages/mui-base/src/Composite/utils/CompositeList/useCompositeListItem.ts b/packages/mui-base/src/Composite/utils/CompositeList/useCompositeListItem.ts new file mode 100644 index 0000000000..2ca0364544 --- /dev/null +++ b/packages/mui-base/src/Composite/utils/CompositeList/useCompositeListItem.ts @@ -0,0 +1,73 @@ +import * as React from 'react'; +import { useEnhancedEffect } from '../../../utils/useEnhancedEffect'; +import { useCompositeListContext } from './CompositeListContext'; + +export interface UseCompositeListItemParameters { + label?: string | null; +} + +interface UseCompositeListItemReturnValue { + ref: (node: HTMLElement | null) => void; + index: number; +} + +/** + * Used to register a list item and its index (DOM position) in the + * `CompositeList`. + * + * API: + * + * - [useCompositeListItem API](https://mui.com/base-ui/api/use-composite-list-item/) + */ +export function useCompositeListItem( + params: UseCompositeListItemParameters = {}, +): UseCompositeListItemReturnValue { + const { label } = params; + + const { register, unregister, map, elementsRef, labelsRef } = useCompositeListContext(); + + const [index, setIndex] = React.useState(null); + + const componentRef = React.useRef(null); + + const ref = React.useCallback( + (node: HTMLElement | null) => { + componentRef.current = node; + + if (index !== null) { + elementsRef.current[index] = node; + if (labelsRef) { + const isLabelDefined = label !== undefined; + labelsRef.current[index] = isLabelDefined ? label : node?.textContent ?? null; + } + } + }, + [index, elementsRef, labelsRef, label], + ); + + useEnhancedEffect(() => { + const node = componentRef.current; + if (node) { + register(node); + return () => { + unregister(node); + }; + } + return undefined; + }, [register, unregister]); + + useEnhancedEffect(() => { + const i = componentRef.current ? map.get(componentRef.current) : null; + if (i != null) { + setIndex(i); + } + }, [map]); + + return React.useMemo( + () => ({ + ref, + index: index == null ? -1 : index, + }), + [index, ref], + ); +} diff --git a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx new file mode 100644 index 0000000000..77fcb14821 --- /dev/null +++ b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; +import { useRadioGroupItemContext } from '../Item/RadioGroupItemContext'; +import { useRadioGroupIndicator } from './useRadioGroupIndicator'; +import type { + RadioGroupIndicatorOwnerState, + RadioGroupIndicatorProps, +} from './RadioGroupIndicator.types'; + +const customStyleHookMapping: CustomStyleHookMapping = { + checked(value) { + return { + 'data-state': value ? 'checked' : 'unchecked', + }; + }, +}; + +const RadioGroupIndicator = React.forwardRef(function RadioGroupIndicator( + props: RadioGroupIndicatorProps, + forwardedRef: React.ForwardedRef, +) { + const { render, className, keepMounted = false, ...otherProps } = props; + + const { disabled, checked } = useRadioGroupItemContext(); + + const { getIndicatorProps } = useRadioGroupIndicator(); + + const ownerState: RadioGroupIndicatorOwnerState = React.useMemo( + () => ({ + disabled, + checked, + }), + [disabled, checked], + ); + + const { renderElement } = useComponentRenderer({ + propGetter: getIndicatorProps, + render: render ?? 'div', + ref: forwardedRef, + className, + ownerState, + extraProps: otherProps, + customStyleHookMapping, + }); + + if (!keepMounted && !ownerState.checked) { + return null; + } + + return renderElement(); +}); + +RadioGroupIndicator.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]), + /** + * If `true`, the indicator stays mounted when unchecked. Useful for CSS animations. + * @default false + */ + keepMounted: PropTypes.bool, + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { RadioGroupIndicator }; diff --git a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.types.ts b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.types.ts new file mode 100644 index 0000000000..754e178bc0 --- /dev/null +++ b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.types.ts @@ -0,0 +1,15 @@ +import type { BaseUIComponentProps } from '../../utils/types'; + +export type RadioGroupIndicatorOwnerState = { + disabled: boolean; + checked: boolean; +}; + +export interface RadioGroupIndicatorProps + extends BaseUIComponentProps<'div', RadioGroupIndicatorOwnerState> { + /** + * If `true`, the indicator stays mounted when unchecked. Useful for CSS animations. + * @default false + */ + keepMounted?: boolean; +} diff --git a/packages/mui-base/src/RadioGroup/Indicator/useRadioGroupIndicator.ts b/packages/mui-base/src/RadioGroup/Indicator/useRadioGroupIndicator.ts new file mode 100644 index 0000000000..28759797de --- /dev/null +++ b/packages/mui-base/src/RadioGroup/Indicator/useRadioGroupIndicator.ts @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +/** + * + * API: + * + * - [useRadioGroupIndicator API](https://mui.com/base-ui/api/use-radio-group-indicator/) + */ +export function useRadioGroupIndicator() { + const getIndicatorProps = React.useCallback( + (externalProps = {}) => mergeReactProps<'span'>(externalProps, {}), + [], + ); + + return React.useMemo( + () => ({ + getIndicatorProps, + }), + [getIndicatorProps], + ); +} diff --git a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx new file mode 100644 index 0000000000..b0d067f141 --- /dev/null +++ b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx @@ -0,0 +1,105 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { CompositeItem } from '../../Composite/Item/CompositeItem'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import type { RadioGroupItemOwnerState, RadioGroupItemProps } from './RadioGroupItem.types'; +import { useRadioGroupItem } from './useRadioGroupItem'; +import { useRadioGroupRootContext } from '../Root/RadioGroupRootContext'; +import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; +import { RadioGroupItemContext, RadioGroupItemContextValue } from './RadioGroupItemContext'; + +const customStyleHookMapping: CustomStyleHookMapping = { + checked(value) { + return { + 'data-state': value ? 'checked' : 'unchecked', + }; + }, +}; + +const RadioGroupItem = React.forwardRef(function RadioGroupItem( + props: RadioGroupItemProps, + forwardedRef: React.ForwardedRef, +) { + const { render, className, disabled: disabledProp = false, value, ...otherProps } = props; + + const { disabled: disabledRoot } = useRadioGroupRootContext(); + + const disabled = disabledRoot ?? disabledProp; + + const { getItemProps, getInputProps, checked } = useRadioGroupItem({ + ...props, + disabled, + }); + + const ownerState: RadioGroupItemOwnerState = React.useMemo( + () => ({ + disabled, + checked, + }), + [disabled, checked], + ); + + const contextValue: RadioGroupItemContextValue = React.useMemo( + () => ({ + checked, + disabled, + }), + [checked, disabled], + ); + + const { renderElement } = useComponentRenderer({ + propGetter: getItemProps, + render: render ?? 'button', + ref: forwardedRef, + className, + ownerState, + extraProps: otherProps, + customStyleHookMapping, + }); + + return ( + + + {props.name && } + + + ); +}); + +RadioGroupItem.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]), + /** + * Whether the item is disabled. + * @default false + */ + disabled: PropTypes.bool, + /** + * @ignore + */ + name: PropTypes.string, + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), + /** + * Determines if the item is required. + */ + required: PropTypes.bool, + /** + * The value of the item identified in the radio group. + */ + value: PropTypes.string.isRequired, +} as any; + +export { RadioGroupItem }; diff --git a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.types.ts b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.types.ts new file mode 100644 index 0000000000..2c3cfbd913 --- /dev/null +++ b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.types.ts @@ -0,0 +1,23 @@ +import type { BaseUIComponentProps } from '../../utils/types'; + +export type RadioGroupItemOwnerState = { + disabled: boolean; + checked: boolean; +}; + +export interface RadioGroupItemProps + extends BaseUIComponentProps<'button', RadioGroupItemOwnerState> { + /** + * The value of the item identified in the radio group. + */ + value: string; + /** + * Whether the item is disabled. + * @default false + */ + disabled?: boolean; + /** + * Determines if the item is required. + */ + required?: boolean; +} diff --git a/packages/mui-base/src/RadioGroup/Item/RadioGroupItemContext.ts b/packages/mui-base/src/RadioGroup/Item/RadioGroupItemContext.ts new file mode 100644 index 0000000000..710b420cd9 --- /dev/null +++ b/packages/mui-base/src/RadioGroup/Item/RadioGroupItemContext.ts @@ -0,0 +1,16 @@ +import * as React from 'react'; + +export interface RadioGroupItemContextValue { + disabled: boolean; + checked: boolean; +} + +export const RadioGroupItemContext = React.createContext(null); + +export function useRadioGroupItemContext() { + const value = React.useContext(RadioGroupItemContext); + if (value === null) { + throw new Error('RadioGroupIndicator component must be used within '); + } + return value; +} diff --git a/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts b/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts new file mode 100644 index 0000000000..2c38e6ff31 --- /dev/null +++ b/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts @@ -0,0 +1,80 @@ +import * as React from 'react'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { visuallyHidden } from '../../utils/visuallyHidden'; +import { useRadioGroupRootContext } from '../Root/RadioGroupRootContext'; + +interface UseRadioGroupItemParameters { + value: string; + disabled?: boolean; + required?: boolean; +} + +/** + * + * API: + * + * - [useRadioGroupItem API](https://mui.com/base-ui/api/use-radio-group-item/) + */ +export function useRadioGroupItem(params: UseRadioGroupItemParameters) { + const { disabled, value, required } = params; + + const { checkedItem, setCheckedItem } = useRadioGroupRootContext(); + + const checked = checkedItem === value; + + const inputRef = React.useRef(null); + + const getItemProps = React.useCallback( + (externalProps = {}) => + mergeReactProps<'button'>(externalProps, { + role: 'radio', + type: 'button', + 'aria-checked': checked, + 'aria-required': required, + 'aria-disabled': disabled || undefined, + onClick(event) { + if (event.defaultPrevented || disabled) { + return; + } + + event.preventDefault(); + + inputRef.current?.click(); + }, + }), + [checked, disabled, required], + ); + + const getInputProps = React.useCallback( + (externalProps = {}) => + mergeReactProps<'input'>(externalProps, { + type: 'radio', + ref: inputRef, + tabIndex: -1, + disabled, + value, + checked, + required, + style: visuallyHidden, + 'aria-hidden': true, + onChange(event) { + // Workaround for https://github.com/facebook/react/issues/9023 + if (event.nativeEvent.defaultPrevented || disabled) { + return; + } + + setCheckedItem(value); + }, + }), + [disabled, checked, setCheckedItem, value, required], + ); + + return React.useMemo( + () => ({ + checked, + getItemProps, + getInputProps, + }), + [checked, getItemProps, getInputProps], + ); +} diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx new file mode 100644 index 0000000000..74029b6a20 --- /dev/null +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx @@ -0,0 +1,97 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { CompositeRoot } from '../../Composite/Root/CompositeRoot'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import type { RadioGroupRootOwnerState, RadioGroupRootProps } from './RadioGroupRoot.types'; +import { useRadioGroupRoot } from './useRadioGroupRoot'; +import { type RadioGroupRootContextValue, RadioGroupRootContext } from './RadioGroupRootContext'; +import { visuallyHidden } from '../../utils/visuallyHidden'; + +const RadioGroupRoot = React.forwardRef(function RadioGroupRoot( + props: RadioGroupRootProps, + forwardedRef: React.ForwardedRef, +) { + const { render, className, disabled, ...otherProps } = props; + + const { getRootProps, checkedItem, setCheckedItem } = useRadioGroupRoot(props); + + const ownerState: RadioGroupRootOwnerState = React.useMemo( + () => ({ + disabled: disabled ?? false, + }), + [disabled], + ); + + const contextValue: RadioGroupRootContextValue = React.useMemo( + () => ({ + checkedItem, + setCheckedItem, + disabled, + }), + [checkedItem, setCheckedItem, disabled], + ); + + const { renderElement } = useComponentRenderer({ + propGetter: getRootProps, + render: render ?? 'div', + ref: forwardedRef, + className, + ownerState, + extraProps: otherProps, + }); + + return ( + + + {checkedItem && props.name && ( + + )} + + ); +}); + +RadioGroupRoot.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]), + /** + * The default value of the selected radio button. Use when uncontrolled. + */ + defaultValue: PropTypes.string, + /** + * Whether the radio group is disabled. + * @default false + */ + disabled: PropTypes.bool, + /** + * The name of the radio group submitted with the form data. + */ + name: PropTypes.string, + /** + * Callback fired when the value changes. + */ + onValueChange: PropTypes.func, + /** + * The orientation of the radio group. + */ + orientation: PropTypes.oneOf(['horizontal', 'vertical']), + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), + /** + * The value of the selected radio button. Use when controlled. + */ + value: PropTypes.string, +} as any; + +export { RadioGroupRoot }; diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.types.ts b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.types.ts new file mode 100644 index 0000000000..4da29e5483 --- /dev/null +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.types.ts @@ -0,0 +1,33 @@ +import type { BaseUIComponentProps } from '../../utils/types'; + +export type RadioGroupRootOwnerState = { + disabled: boolean | undefined; +}; + +export interface RadioGroupRootProps extends BaseUIComponentProps<'div', RadioGroupRootOwnerState> { + /** + * Whether the radio group is disabled. + * @default false + */ + disabled?: boolean; + /** + * The name of the radio group submitted with the form data. + */ + name?: string; + /** + * The value of the selected radio button. Use when controlled. + */ + value?: string; + /** + * The default value of the selected radio button. Use when uncontrolled. + */ + defaultValue?: string; + /** + * Callback fired when the value changes. + */ + onValueChange?: (value: string, event: React.ChangeEvent) => void; + /** + * The orientation of the radio group. + */ + orientation?: 'horizontal' | 'vertical'; +} diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts b/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts new file mode 100644 index 0000000000..d261ef5b06 --- /dev/null +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts @@ -0,0 +1,17 @@ +import * as React from 'react'; + +export interface RadioGroupRootContextValue { + disabled: boolean | undefined; + checkedItem: string | null; + setCheckedItem: React.Dispatch>; +} + +export const RadioGroupRootContext = React.createContext(null); + +export function useRadioGroupRootContext() { + const value = React.useContext(RadioGroupRootContext); + if (value === null) { + throw new Error('RadioGroup components must be used within '); + } + return value; +} diff --git a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts new file mode 100644 index 0000000000..43e5ea4a96 --- /dev/null +++ b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { useControlled } from '../../utils/useControlled'; + +interface UseRadioGroupRootParameters { + disabled?: boolean; + defaultValue?: string; + value?: string; +} +/** + * + * API: + * + * - [useRadioGroupRoot API](https://mui.com/base-ui/api/use-radio-group-root/) + */ +export function useRadioGroupRoot(params: UseRadioGroupRootParameters) { + const { disabled, defaultValue, value: externalValue } = params; + + const [checkedItem, setCheckedItem] = useControlled({ + controlled: externalValue, + default: defaultValue, + name: 'RadioGroup', + state: 'value', + }); + + const getRootProps = React.useCallback( + (externalProps = {}) => + mergeReactProps<'div'>(externalProps, { + role: 'radiogroup', + 'aria-disabled': disabled, + }), + [disabled], + ); + + return React.useMemo( + () => ({ + getRootProps, + checkedItem, + setCheckedItem, + }), + [getRootProps, checkedItem, setCheckedItem], + ); +} diff --git a/packages/mui-base/src/RadioGroup/index.barrel.ts b/packages/mui-base/src/RadioGroup/index.barrel.ts new file mode 100644 index 0000000000..9dfa3f3930 --- /dev/null +++ b/packages/mui-base/src/RadioGroup/index.barrel.ts @@ -0,0 +1,7 @@ +export * from './Root/RadioGroupRoot'; +export * from './Item/RadioGroupItem'; +export * from './Indicator/RadioGroupIndicator'; + +export type * from './Root/RadioGroupRoot.types'; +export type * from './Item/RadioGroupItem.types'; +export type * from './Indicator/RadioGroupIndicator.types'; diff --git a/packages/mui-base/src/RadioGroup/index.ts b/packages/mui-base/src/RadioGroup/index.ts new file mode 100644 index 0000000000..0faaa8d88f --- /dev/null +++ b/packages/mui-base/src/RadioGroup/index.ts @@ -0,0 +1,16 @@ +export { RadioGroupRoot as Root } from './Root/RadioGroupRoot'; +export { RadioGroupItem as Item } from './Item/RadioGroupItem'; +export { RadioGroupIndicator as Indicator } from './Indicator/RadioGroupIndicator'; + +export type { + RadioGroupRootProps as RootProps, + RadioGroupRootOwnerState as RootOwnerStatge, +} from './Root/RadioGroupRoot.types'; +export type { + RadioGroupItemProps as ItemProps, + RadioGroupItemOwnerState as ItemOwnerState, +} from './Item/RadioGroupItem.types'; +export type { + RadioGroupIndicatorProps as IndicatorProps, + RadioGroupIndicatorOwnerState as IndicatorOwnerState, +} from './Indicator/RadioGroupIndicator.types'; From 1e5e9c22c4b640380d068dcdc424638e82000981 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 24 Jul 2024 16:00:14 +1000 Subject: [PATCH 02/47] Fix form submission data --- .../system/index.js | 37 ++++++++++++------- .../system/index.tsx | 37 ++++++++++++------- .../system/index.tsx.preview | 14 ------- .../src/RadioGroup/Item/RadioGroupItem.tsx | 2 +- .../src/RadioGroup/Item/useRadioGroupItem.ts | 7 ++-- .../src/RadioGroup/Root/RadioGroupRoot.tsx | 5 +-- 6 files changed, 52 insertions(+), 50 deletions(-) delete mode 100644 docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview diff --git a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js index 6dceb475c9..0f11ca79e4 100644 --- a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js +++ b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js @@ -4,20 +4,29 @@ import { styled } from '@mui/system'; export default function UnstyledSwitchIntroduction() { return ( - - - - Light - - - - Medium - - - - Heavy - - +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + console.log(new URLSearchParams(formData).toString()); + }} + > + + + + Light + + + + Medium + + + + Heavy + + + +
); } diff --git a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx index 6dceb475c9..2c028b67c5 100644 --- a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx +++ b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx @@ -4,20 +4,29 @@ import { styled } from '@mui/system'; export default function UnstyledSwitchIntroduction() { return ( - - - - Light - - - - Medium - - - - Heavy - - +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + console.log(new URLSearchParams(formData as any).toString()); + }} + > + + + + Light + + + + Medium + + + + Heavy + + + +
); } diff --git a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview deleted file mode 100644 index 003e293a65..0000000000 --- a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview +++ /dev/null @@ -1,14 +0,0 @@ - - - - Light - - - - Medium - - - - Heavy - - \ No newline at end of file diff --git a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx index b0d067f141..dd8295eec7 100644 --- a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx +++ b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx @@ -60,7 +60,7 @@ const RadioGroupItem = React.forwardRef(function RadioGroupItem( return ( - {props.name && } + {!checked && props.name && } ); diff --git a/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts b/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts index 2c38e6ff31..f51705e2f7 100644 --- a/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts +++ b/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts @@ -5,6 +5,7 @@ import { useRadioGroupRootContext } from '../Root/RadioGroupRootContext'; interface UseRadioGroupItemParameters { value: string; + name?: string; disabled?: boolean; required?: boolean; } @@ -16,7 +17,7 @@ interface UseRadioGroupItemParameters { * - [useRadioGroupItem API](https://mui.com/base-ui/api/use-radio-group-item/) */ export function useRadioGroupItem(params: UseRadioGroupItemParameters) { - const { disabled, value, required } = params; + const { disabled, value, name, required } = params; const { checkedItem, setCheckedItem } = useRadioGroupRootContext(); @@ -51,8 +52,8 @@ export function useRadioGroupItem(params: UseRadioGroupItemParameters) { type: 'radio', ref: inputRef, tabIndex: -1, + name, disabled, - value, checked, required, style: visuallyHidden, @@ -66,7 +67,7 @@ export function useRadioGroupItem(params: UseRadioGroupItemParameters) { setCheckedItem(value); }, }), - [disabled, checked, setCheckedItem, value, required], + [disabled, name, checked, setCheckedItem, value, required], ); return React.useMemo( diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx index 74029b6a20..ef5f462f51 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx @@ -5,7 +5,6 @@ import { useComponentRenderer } from '../../utils/useComponentRenderer'; import type { RadioGroupRootOwnerState, RadioGroupRootProps } from './RadioGroupRoot.types'; import { useRadioGroupRoot } from './useRadioGroupRoot'; import { type RadioGroupRootContextValue, RadioGroupRootContext } from './RadioGroupRootContext'; -import { visuallyHidden } from '../../utils/visuallyHidden'; const RadioGroupRoot = React.forwardRef(function RadioGroupRoot( props: RadioGroupRootProps, @@ -43,9 +42,7 @@ const RadioGroupRoot = React.forwardRef(function RadioGroupRoot( return ( - {checkedItem && props.name && ( - - )} + {checkedItem && props.name && } ); }); From d71f411956d0aa3f5e7077a02a8df388aeb3fb69 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 24 Jul 2024 16:00:51 +1000 Subject: [PATCH 03/47] Fix demo --- .../system/index.js | 39 +++++++------------ .../system/index.tsx | 39 +++++++------------ .../system/index.tsx.preview | 14 +++++++ 3 files changed, 44 insertions(+), 48 deletions(-) create mode 100644 docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview diff --git a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js index 0f11ca79e4..bda71360ee 100644 --- a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js +++ b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js @@ -2,31 +2,22 @@ import * as React from 'react'; import * as RadioGroup from '@base_ui/react/RadioGroup'; import { styled } from '@mui/system'; -export default function UnstyledSwitchIntroduction() { +export default function UnstyledRadioGroupIntroduction() { return ( -
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - console.log(new URLSearchParams(formData).toString()); - }} - > - - - - Light - - - - Medium - - - - Heavy - - - -
+ + + + Light + + + + Medium + + + + Heavy + + ); } diff --git a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx index 2c028b67c5..bda71360ee 100644 --- a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx +++ b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx @@ -2,31 +2,22 @@ import * as React from 'react'; import * as RadioGroup from '@base_ui/react/RadioGroup'; import { styled } from '@mui/system'; -export default function UnstyledSwitchIntroduction() { +export default function UnstyledRadioGroupIntroduction() { return ( -
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - console.log(new URLSearchParams(formData as any).toString()); - }} - > - - - - Light - - - - Medium - - - - Heavy - - - -
+ + + + Light + + + + Medium + + + + Heavy + + ); } diff --git a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview new file mode 100644 index 0000000000..003e293a65 --- /dev/null +++ b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview @@ -0,0 +1,14 @@ + + + + Light + + + + Medium + + + + Heavy + + \ No newline at end of file From a2a1dccbb1989ae1f96fdb740c2f4ba1a039e2b0 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 24 Jul 2024 17:28:53 +1000 Subject: [PATCH 04/47] Add initial tests --- .../base-ui/api/radio-group-indicator.json | 3 + docs/pages/base-ui/api/radio-group-item.json | 6 +- docs/pages/base-ui/api/radio-group-root.json | 8 +- .../radio-group-item/radio-group-item.json | 3 +- .../radio-group-root/radio-group-root.json | 4 +- .../Indicator/RadioGroupIndicator.test.tsx | 19 ++ .../Indicator/RadioGroupIndicator.tsx | 6 +- .../RadioGroup/Item/RadioGroupItem.test.tsx | 16 ++ .../src/RadioGroup/Item/RadioGroupItem.tsx | 35 ++- .../RadioGroup/Item/RadioGroupItem.types.ts | 8 +- .../RadioGroup/Item/RadioGroupItemContext.ts | 2 + .../src/RadioGroup/Item/useRadioGroupItem.ts | 20 +- .../RadioGroup/Root/RadioGroupRoot.test.tsx | 199 ++++++++++++++++++ .../src/RadioGroup/Root/RadioGroupRoot.tsx | 39 +++- .../RadioGroup/Root/RadioGroupRoot.types.ts | 14 +- .../RadioGroup/Root/RadioGroupRootContext.ts | 3 + .../src/RadioGroup/Root/useRadioGroupRoot.ts | 6 +- 17 files changed, 365 insertions(+), 26 deletions(-) create mode 100644 packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.test.tsx create mode 100644 packages/mui-base/src/RadioGroup/Item/RadioGroupItem.test.tsx create mode 100644 packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx diff --git a/docs/pages/base-ui/api/radio-group-indicator.json b/docs/pages/base-ui/api/radio-group-indicator.json index 63e3516003..de4d275e55 100644 --- a/docs/pages/base-ui/api/radio-group-indicator.json +++ b/docs/pages/base-ui/api/radio-group-indicator.json @@ -9,7 +9,10 @@ "import * as RadioGroup from '@base_ui/react/RadioGroup';\nconst RadioGroupIndicator = RadioGroup.Indicator;" ], "classes": [], + "spread": true, + "themeDefaultProps": true, "muiName": "RadioGroupIndicator", + "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx", "inheritance": null, "demos": "", diff --git a/docs/pages/base-ui/api/radio-group-item.json b/docs/pages/base-ui/api/radio-group-item.json index 4dcdbcb703..ea0650b641 100644 --- a/docs/pages/base-ui/api/radio-group-item.json +++ b/docs/pages/base-ui/api/radio-group-item.json @@ -3,15 +3,19 @@ "value": { "type": { "name": "string" }, "required": true }, "className": { "type": { "name": "union", "description": "func
| string" } }, "disabled": { "type": { "name": "bool" }, "default": "false" }, + "readOnly": { "type": { "name": "bool" }, "default": "false" }, "render": { "type": { "name": "union", "description": "element
| func" } }, - "required": { "type": { "name": "bool" } } + "required": { "type": { "name": "bool" }, "default": "false" } }, "name": "RadioGroupItem", "imports": [ "import * as RadioGroup from '@base_ui/react/RadioGroup';\nconst RadioGroupItem = RadioGroup.Item;" ], "classes": [], + "spread": true, + "themeDefaultProps": true, "muiName": "RadioGroupItem", + "forwardsRefTo": "HTMLButtonElement", "filename": "/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx", "inheritance": null, "demos": "", diff --git a/docs/pages/base-ui/api/radio-group-root.json b/docs/pages/base-ui/api/radio-group-root.json index 2ca6d80b65..ec9639af05 100644 --- a/docs/pages/base-ui/api/radio-group-root.json +++ b/docs/pages/base-ui/api/radio-group-root.json @@ -6,9 +6,12 @@ "name": { "type": { "name": "string" } }, "onValueChange": { "type": { "name": "func" } }, "orientation": { - "type": { "name": "enum", "description": "'horizontal'
| 'vertical'" } + "type": { "name": "enum", "description": "'horizontal'
| 'vertical'" }, + "default": "'horizontal'" }, + "readOnly": { "type": { "name": "bool" }, "default": "false" }, "render": { "type": { "name": "union", "description": "element
| func" } }, + "required": { "type": { "name": "bool" }, "default": "false" }, "value": { "type": { "name": "string" } } }, "name": "RadioGroupRoot", @@ -16,7 +19,10 @@ "import * as RadioGroup from '@base_ui/react/RadioGroup';\nconst RadioGroupRoot = RadioGroup.Root;" ], "classes": [], + "spread": true, + "themeDefaultProps": true, "muiName": "RadioGroupRoot", + "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx", "inheritance": null, "demos": "", diff --git a/docs/translations/api-docs/radio-group-item/radio-group-item.json b/docs/translations/api-docs/radio-group-item/radio-group-item.json index ed35ab8414..99c6dfada6 100644 --- a/docs/translations/api-docs/radio-group-item/radio-group-item.json +++ b/docs/translations/api-docs/radio-group-item/radio-group-item.json @@ -4,7 +4,8 @@ "className": { "description": "Class names applied to the element or a function that returns them based on the component's state." }, - "disabled": { "description": "Whether the item is disabled." }, + "disabled": { "description": "Determines if the item is disabled." }, + "readOnly": { "description": "Determines if the item is readonly." }, "render": { "description": "A function to customize rendering of the component." }, "required": { "description": "Determines if the item is required." }, "value": { "description": "The value of the item identified in the radio group." } diff --git a/docs/translations/api-docs/radio-group-root/radio-group-root.json b/docs/translations/api-docs/radio-group-root/radio-group-root.json index 69470284bc..2f00dc8f93 100644 --- a/docs/translations/api-docs/radio-group-root/radio-group-root.json +++ b/docs/translations/api-docs/radio-group-root/radio-group-root.json @@ -7,11 +7,13 @@ "defaultValue": { "description": "The default value of the selected radio button. Use when uncontrolled." }, - "disabled": { "description": "Whether the radio group is disabled." }, + "disabled": { "description": "Determines if the radio group is disabled." }, "name": { "description": "The name of the radio group submitted with the form data." }, "onValueChange": { "description": "Callback fired when the value changes." }, "orientation": { "description": "The orientation of the radio group." }, + "readOnly": { "description": "Determines if the radio group is readonly." }, "render": { "description": "A function to customize rendering of the component." }, + "required": { "description": "Determines if the radio group is required." }, "value": { "description": "The value of the selected radio button. Use when controlled." } }, "classDescriptions": {} diff --git a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.test.tsx b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.test.tsx new file mode 100644 index 0000000000..6d7881c4d8 --- /dev/null +++ b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.test.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { createRenderer } from '@mui/internal-test-utils'; +import * as RadioGroup from '@base_ui/react/RadioGroup'; +import { describeConformance } from '../../../test/describeConformance'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render( + + {node} + , + ); + }, + })); +}); diff --git a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx index 77fcb14821..197856bbe3 100644 --- a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx +++ b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx @@ -23,7 +23,7 @@ const RadioGroupIndicator = React.forwardRef(function RadioGroupIndicator( ) { const { render, className, keepMounted = false, ...otherProps } = props; - const { disabled, checked } = useRadioGroupItemContext(); + const { disabled, checked, required, readOnly } = useRadioGroupItemContext(); const { getIndicatorProps } = useRadioGroupIndicator(); @@ -31,8 +31,10 @@ const RadioGroupIndicator = React.forwardRef(function RadioGroupIndicator( () => ({ disabled, checked, + required, + readOnly, }), - [disabled, checked], + [disabled, checked, required, readOnly], ); const { renderElement } = useComponentRenderer({ diff --git a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.test.tsx b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.test.tsx new file mode 100644 index 0000000000..1b7432ac13 --- /dev/null +++ b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.test.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { createRenderer } from '@mui/internal-test-utils'; +import * as RadioGroup from '@base_ui/react/RadioGroup'; +import { describeConformance } from '../../../test/describeConformance'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'button', + refInstanceof: window.HTMLButtonElement, + render(node) { + return render({node}); + }, + })); +}); diff --git a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx index dd8295eec7..a3e8e9f2bc 100644 --- a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx +++ b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx @@ -20,31 +20,50 @@ const RadioGroupItem = React.forwardRef(function RadioGroupItem( props: RadioGroupItemProps, forwardedRef: React.ForwardedRef, ) { - const { render, className, disabled: disabledProp = false, value, ...otherProps } = props; + const { + render, + className, + disabled: disabledProp = false, + readOnly: readOnlyProp = false, + required: requiredProp = false, + value, + ...otherProps + } = props; - const { disabled: disabledRoot } = useRadioGroupRootContext(); + const { + disabled: disabledRoot, + readOnly: readOnlyRoot, + required: requiredRoot, + } = useRadioGroupRootContext(); const disabled = disabledRoot ?? disabledProp; + const readOnly = readOnlyRoot ?? readOnlyProp; + const required = requiredRoot ?? requiredProp; const { getItemProps, getInputProps, checked } = useRadioGroupItem({ ...props, disabled, + readOnly, }); const ownerState: RadioGroupItemOwnerState = React.useMemo( () => ({ + required, disabled, + readOnly, checked, }), - [disabled, checked], + [disabled, readOnly, checked, required], ); const contextValue: RadioGroupItemContextValue = React.useMemo( () => ({ checked, disabled, + readOnly, + required, }), - [checked, disabled], + [checked, disabled, readOnly, required], ); const { renderElement } = useComponentRenderer({ @@ -80,7 +99,7 @@ RadioGroupItem.propTypes /* remove-proptypes */ = { */ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), /** - * Whether the item is disabled. + * Determines if the item is disabled. * @default false */ disabled: PropTypes.bool, @@ -88,12 +107,18 @@ RadioGroupItem.propTypes /* remove-proptypes */ = { * @ignore */ name: PropTypes.string, + /** + * Determines if the item is readonly. + * @default false + */ + readOnly: PropTypes.bool, /** * A function to customize rendering of the component. */ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), /** * Determines if the item is required. + * @default false */ required: PropTypes.bool, /** diff --git a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.types.ts b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.types.ts index 2c3cfbd913..1ba4fe910d 100644 --- a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.types.ts +++ b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.types.ts @@ -12,12 +12,18 @@ export interface RadioGroupItemProps */ value: string; /** - * Whether the item is disabled. + * Determines if the item is disabled. * @default false */ disabled?: boolean; /** * Determines if the item is required. + * @default false */ required?: boolean; + /** + * Determines if the item is readonly. + * @default false + */ + readOnly?: boolean; } diff --git a/packages/mui-base/src/RadioGroup/Item/RadioGroupItemContext.ts b/packages/mui-base/src/RadioGroup/Item/RadioGroupItemContext.ts index 710b420cd9..a98c6bb744 100644 --- a/packages/mui-base/src/RadioGroup/Item/RadioGroupItemContext.ts +++ b/packages/mui-base/src/RadioGroup/Item/RadioGroupItemContext.ts @@ -2,7 +2,9 @@ import * as React from 'react'; export interface RadioGroupItemContextValue { disabled: boolean; + readOnly: boolean; checked: boolean; + required: boolean; } export const RadioGroupItemContext = React.createContext(null); diff --git a/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts b/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts index f51705e2f7..d4769095dc 100644 --- a/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts +++ b/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts @@ -7,6 +7,7 @@ interface UseRadioGroupItemParameters { value: string; name?: string; disabled?: boolean; + readOnly?: boolean; required?: boolean; } @@ -17,9 +18,9 @@ interface UseRadioGroupItemParameters { * - [useRadioGroupItem API](https://mui.com/base-ui/api/use-radio-group-item/) */ export function useRadioGroupItem(params: UseRadioGroupItemParameters) { - const { disabled, value, name, required } = params; + const { disabled, readOnly, value, name, required } = params; - const { checkedItem, setCheckedItem } = useRadioGroupRootContext(); + const { checkedItem, setCheckedItem, onValueChange } = useRadioGroupRootContext(); const checked = checkedItem === value; @@ -33,8 +34,9 @@ export function useRadioGroupItem(params: UseRadioGroupItemParameters) { 'aria-checked': checked, 'aria-required': required, 'aria-disabled': disabled || undefined, + 'aria-readonly': readOnly || undefined, onClick(event) { - if (event.defaultPrevented || disabled) { + if (event.defaultPrevented || disabled || readOnly) { return; } @@ -43,7 +45,7 @@ export function useRadioGroupItem(params: UseRadioGroupItemParameters) { inputRef.current?.click(); }, }), - [checked, disabled, required], + [checked, disabled, readOnly, required], ); const getInputProps = React.useCallback( @@ -56,18 +58,24 @@ export function useRadioGroupItem(params: UseRadioGroupItemParameters) { disabled, checked, required, + readOnly, style: visuallyHidden, 'aria-hidden': true, onChange(event) { // Workaround for https://github.com/facebook/react/issues/9023 - if (event.nativeEvent.defaultPrevented || disabled) { + if (event.nativeEvent.defaultPrevented) { + return; + } + + if (disabled || readOnly) { return; } setCheckedItem(value); + onValueChange?.(value, event); }, }), - [disabled, name, checked, setCheckedItem, value, required], + [disabled, readOnly, name, checked, setCheckedItem, value, required, onValueChange], ); return React.useMemo( diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx new file mode 100644 index 0000000000..5006eaafa0 --- /dev/null +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx @@ -0,0 +1,199 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { spy } from 'sinon'; +import { createRenderer, act, screen } from '@mui/internal-test-utils'; +import * as RadioGroup from '@base_ui/react/RadioGroup'; +import { describeConformance } from '../../../test/describeConformance'; + +const isJSDOM = /jsdom/.test(window.navigator.userAgent); + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'div', + refInstanceof: window.HTMLDivElement, + render, + })); + + describe('extra props', () => { + it('can override the built-in attributes', () => { + const { container } = render(); + expect(container.firstElementChild as HTMLElement).to.have.attribute('role', 'switch'); + }); + }); + + it('should call onValueChange when an item is clicked', () => { + const handleChange = spy(); + render( + + + , + ); + + const item = screen.getByTestId('item'); + + act(() => { + item.click(); + }); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.firstCall.args[0]).to.equal('a'); + }); + + describe('prop: disabled', () => { + it('should have the `aria-disabled` attribute', () => { + render(); + expect(screen.getByRole('radiogroup')).to.have.attribute('aria-disabled', 'true'); + }); + + it('should not have the aria attribute when `disabled` is not set', () => { + render(); + expect(screen.getByRole('radiogroup')).not.to.have.attribute('aria-disabled'); + }); + + it('should not change its state when clicked', () => { + render( + + + , + ); + + const item = screen.getByTestId('item'); + + expect(item).to.have.attribute('aria-checked', 'false'); + + act(() => { + item.click(); + }); + + expect(item).to.have.attribute('aria-checked', 'false'); + }); + }); + + describe('prop: readOnly', () => { + it('should have the `aria-readonly` attribute', () => { + render(); + const group = screen.getByRole('radiogroup'); + expect(group).to.have.attribute('aria-readonly', 'true'); + }); + + it('should not have the aria attribute when `readOnly` is not set', () => { + render(); + const group = screen.getByRole('radiogroup'); + expect(group).not.to.have.attribute('aria-readonly'); + }); + + it('should not change its state when clicked', () => { + render( + + + , + ); + + const item = screen.getByTestId('item'); + + expect(item).to.have.attribute('aria-checked', 'false'); + + act(() => { + item.click(); + }); + + expect(item).to.have.attribute('aria-checked', 'false'); + }); + }); + + it('should update its state if the underlying input is toggled', () => { + render( + + + , + ); + + const group = screen.getByTestId('root'); + const item = screen.getByTestId('item'); + + const input = group.querySelector('input'); + + act(() => { + input?.click(); + }); + + expect(item).to.have.attribute('aria-checked', 'true'); + }); + + it('should place the style hooks on the root and subcomponents', () => { + render( + + + + + , + ); + + const root = screen.getByRole('radiogroup'); + const item = screen.getByTestId('item'); + const indicator = screen.getByTestId('indicator'); + + expect(root).to.have.attribute('data-disabled', 'true'); + expect(root).to.have.attribute('data-readonly', 'true'); + expect(root).to.have.attribute('data-required', 'true'); + + expect(item).to.have.attribute('data-state', 'checked'); + expect(item).to.have.attribute('data-disabled', 'true'); + expect(item).to.have.attribute('data-readonly', 'true'); + expect(item).to.have.attribute('data-required', 'true'); + + expect(indicator).to.have.attribute('data-state', 'checked'); + expect(indicator).to.have.attribute('data-disabled', 'true'); + expect(indicator).to.have.attribute('data-readonly', 'true'); + expect(indicator).to.have.attribute('data-required', 'true'); + }); + + it('should set the name attribute on the input', () => { + render(); + const group = screen.getByRole('radiogroup'); + expect(group.nextElementSibling).to.have.attribute('name', 'radio-group'); + }); + + it('should include the checkbox value in the form submission', function test() { + if (isJSDOM) { + // FormData is not available in JSDOM + this.skip(); + } + + let stringifiedFormData = ''; + + render( +
{ + event.preventDefault(); + const formData = new FormData(event.currentTarget); + stringifiedFormData = new URLSearchParams(formData as any).toString(); + }} + > + + + + + + +
, + ); + + const [radioA] = screen.getAllByRole('radio'); + const submitButton = screen.getByRole('button'); + + submitButton.click(); + + expect(stringifiedFormData).to.equal('a=off;b=off;c=off'); + + act(() => { + radioA.click(); + }); + + submitButton.click(); + + expect(stringifiedFormData).to.equal('a=on;b=off;c=off;group=a'); + }); +}); diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx index ef5f462f51..ea1be7402c 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx @@ -5,29 +5,45 @@ import { useComponentRenderer } from '../../utils/useComponentRenderer'; import type { RadioGroupRootOwnerState, RadioGroupRootProps } from './RadioGroupRoot.types'; import { useRadioGroupRoot } from './useRadioGroupRoot'; import { type RadioGroupRootContextValue, RadioGroupRootContext } from './RadioGroupRootContext'; +import { useEventCallback } from '../../utils/useEventCallback'; const RadioGroupRoot = React.forwardRef(function RadioGroupRoot( props: RadioGroupRootProps, forwardedRef: React.ForwardedRef, ) { - const { render, className, disabled, ...otherProps } = props; + const { + render, + className, + disabled, + readOnly, + required, + onValueChange: onValueChangeProp, + ...otherProps + } = props; const { getRootProps, checkedItem, setCheckedItem } = useRadioGroupRoot(props); + const onValueChange = useEventCallback(onValueChangeProp ?? (() => {})); + const ownerState: RadioGroupRootOwnerState = React.useMemo( () => ({ disabled: disabled ?? false, + required: required ?? false, + readOnly: readOnly ?? false, }), - [disabled], + [disabled, readOnly, required], ); const contextValue: RadioGroupRootContextValue = React.useMemo( () => ({ checkedItem, setCheckedItem, + onValueChange, disabled, + readOnly, + required, }), - [checkedItem, setCheckedItem, disabled], + [checkedItem, setCheckedItem, onValueChange, disabled, readOnly, required], ); const { renderElement } = useComponentRenderer({ @@ -42,7 +58,9 @@ const RadioGroupRoot = React.forwardRef(function RadioGroupRoot( return ( - {checkedItem && props.name && } + {props.name && ( + + )} ); }); @@ -65,7 +83,7 @@ RadioGroupRoot.propTypes /* remove-proptypes */ = { */ defaultValue: PropTypes.string, /** - * Whether the radio group is disabled. + * Determines if the radio group is disabled. * @default false */ disabled: PropTypes.bool, @@ -79,12 +97,23 @@ RadioGroupRoot.propTypes /* remove-proptypes */ = { onValueChange: PropTypes.func, /** * The orientation of the radio group. + * @default 'horizontal' */ orientation: PropTypes.oneOf(['horizontal', 'vertical']), + /** + * Determines if the radio group is readonly. + * @default false + */ + readOnly: PropTypes.bool, /** * A function to customize rendering of the component. */ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), + /** + * Determines if the radio group is required. + * @default false + */ + required: PropTypes.bool, /** * The value of the selected radio button. Use when controlled. */ diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.types.ts b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.types.ts index 4da29e5483..ba6749ebc7 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.types.ts +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.types.ts @@ -2,14 +2,25 @@ import type { BaseUIComponentProps } from '../../utils/types'; export type RadioGroupRootOwnerState = { disabled: boolean | undefined; + readOnly: boolean | undefined; }; export interface RadioGroupRootProps extends BaseUIComponentProps<'div', RadioGroupRootOwnerState> { /** - * Whether the radio group is disabled. + * Determines if the radio group is disabled. * @default false */ disabled?: boolean; + /** + * Determines if the radio group is readonly. + * @default false + */ + readOnly?: boolean; + /** + * Determines if the radio group is required. + * @default false + */ + required?: boolean; /** * The name of the radio group submitted with the form data. */ @@ -28,6 +39,7 @@ export interface RadioGroupRootProps extends BaseUIComponentProps<'div', RadioGr onValueChange?: (value: string, event: React.ChangeEvent) => void; /** * The orientation of the radio group. + * @default 'horizontal' */ orientation?: 'horizontal' | 'vertical'; } diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts b/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts index d261ef5b06..c05afa14e8 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts @@ -2,8 +2,11 @@ import * as React from 'react'; export interface RadioGroupRootContextValue { disabled: boolean | undefined; + readOnly: boolean | undefined; + required: boolean | undefined; checkedItem: string | null; setCheckedItem: React.Dispatch>; + onValueChange: (value: string, event: React.ChangeEvent) => void; } export const RadioGroupRootContext = React.createContext(null); diff --git a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts index 43e5ea4a96..fb6ef80a0d 100644 --- a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts +++ b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts @@ -4,6 +4,7 @@ import { useControlled } from '../../utils/useControlled'; interface UseRadioGroupRootParameters { disabled?: boolean; + readOnly?: boolean; defaultValue?: string; value?: string; } @@ -14,7 +15,7 @@ interface UseRadioGroupRootParameters { * - [useRadioGroupRoot API](https://mui.com/base-ui/api/use-radio-group-root/) */ export function useRadioGroupRoot(params: UseRadioGroupRootParameters) { - const { disabled, defaultValue, value: externalValue } = params; + const { disabled, defaultValue, readOnly, value: externalValue } = params; const [checkedItem, setCheckedItem] = useControlled({ controlled: externalValue, @@ -28,8 +29,9 @@ export function useRadioGroupRoot(params: UseRadioGroupRootParameters) { mergeReactProps<'div'>(externalProps, { role: 'radiogroup', 'aria-disabled': disabled, + 'aria-readonly': readOnly || undefined, }), - [disabled], + [disabled, readOnly], ); return React.useMemo( From 8d53200249ab5903347f512b93bfea0c5042a63e Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 24 Jul 2024 18:50:35 +1000 Subject: [PATCH 05/47] Port Composite tests --- .../src/Composite/Item/CompositeItem.test.tsx | 17 + .../src/Composite/Root/CompositeRoot.test.tsx | 365 ++++++++++++++++++ .../src/Composite/Root/CompositeRoot.tsx | 13 + .../src/Composite/Root/CompositeRoot.types.ts | 3 + .../src/Composite/Root/useCompositeRoot.ts | 10 +- .../src/RadioGroup/Item/RadioGroupItem.tsx | 2 +- 6 files changed, 405 insertions(+), 5 deletions(-) create mode 100644 packages/mui-base/src/Composite/Item/CompositeItem.test.tsx create mode 100644 packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx diff --git a/packages/mui-base/src/Composite/Item/CompositeItem.test.tsx b/packages/mui-base/src/Composite/Item/CompositeItem.test.tsx new file mode 100644 index 0000000000..90d331a72c --- /dev/null +++ b/packages/mui-base/src/Composite/Item/CompositeItem.test.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { createRenderer } from '@mui/internal-test-utils'; +import { describeConformance } from '../../../test/describeConformance'; +import { CompositeRoot } from '../Root/CompositeRoot'; +import { CompositeItem } from './CompositeItem'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'div', + refInstanceof: window.HTMLDivElement, + render(node) { + return render({node}); + }, + })); +}); diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx b/packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx new file mode 100644 index 0000000000..e7fad1fab2 --- /dev/null +++ b/packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx @@ -0,0 +1,365 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { test } from 'mocha'; +import { createRenderer, act, screen, fireEvent } from '@mui/internal-test-utils'; +import { describeConformance } from '../../../test/describeConformance'; +import { CompositeRoot } from './CompositeRoot'; +import { CompositeItem } from '../Item/CompositeItem'; + +function microtask() { + return act(async () => {}); +} + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'div', + refInstanceof: window.HTMLDivElement, + render, + })); + + describe('list', () => { + test('controlled mode', async () => { + function App() { + const [activeIndex, setActiveIndex] = React.useState(0); + return ( + + 1 + 2 + 3 + + ); + } + + render(); + + act(() => screen.getByTestId('1').focus()); + expect(screen.getByTestId('1')).to.have.attribute('data-active'); + + fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); + await microtask(); + expect(screen.getByTestId('2')).to.have.attribute('data-active'); + expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('2')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('2'), { key: 'ArrowDown' }); + await microtask(); + expect(screen.getByTestId('3')).to.have.attribute('data-active'); + expect(screen.getByTestId('3')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('3')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('3'), { key: 'ArrowUp' }); + await microtask(); + expect(screen.getByTestId('2')).to.have.attribute('data-active'); + expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('2')).toHaveFocus(); + + act(() => screen.getByTestId('1').focus()); + await microtask(); + expect(screen.getByTestId('1')).to.have.attribute('data-active'); + expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); + }); + + test('uncontrolled mode', async () => { + render( + + 1 + 2 + 3 + , + ); + + act(() => screen.getByTestId('1').focus()); + expect(screen.getByTestId('1')).to.have.attribute('data-active'); + + fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); + await microtask(); + expect(screen.getByTestId('2')).to.have.attribute('data-active'); + expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('2')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('2'), { key: 'ArrowDown' }); + await microtask(); + expect(screen.getByTestId('3')).to.have.attribute('data-active'); + expect(screen.getByTestId('3')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('3')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('3'), { key: 'ArrowUp' }); + await microtask(); + expect(screen.getByTestId('2')).to.have.attribute('data-active'); + expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('2')).toHaveFocus(); + + act(() => screen.getByTestId('1').focus()); + await microtask(); + expect(screen.getByTestId('1')).to.have.attribute('data-active'); + expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); + }); + }); + + describe('grid', () => { + test('uniform 1x1 items', async () => { + function App() { + return ( + // 1 to 9 numpad + + {['1', '2', '3', '4', '5', '6', '7', '8', '9'].map((i) => ( + + {i} + + ))} + + ); + } + + render(); + + act(() => screen.getByTestId('1').focus()); + expect(screen.getByTestId('1')).to.have.attribute('data-active'); + + fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); + await microtask(); + expect(screen.getByTestId('4')).to.have.attribute('data-active'); + expect(screen.getByTestId('4')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('4')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('4'), { key: 'ArrowRight' }); + await microtask(); + expect(screen.getByTestId('5')).to.have.attribute('data-active'); + expect(screen.getByTestId('5')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('5')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('5'), { key: 'ArrowDown' }); + await microtask(); + expect(screen.getByTestId('8')).to.have.attribute('data-active'); + expect(screen.getByTestId('8')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('8')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('8'), { key: 'ArrowLeft' }); + await microtask(); + expect(screen.getByTestId('7')).to.have.attribute('data-active'); + expect(screen.getByTestId('7')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('7')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('7'), { key: 'ArrowUp' }); + await microtask(); + expect(screen.getByTestId('4')).to.have.attribute('data-active'); + expect(screen.getByTestId('4')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('4')).toHaveFocus(); + + act(() => screen.getByTestId('9').focus()); + await microtask(); + expect(screen.getByTestId('9')).to.have.attribute('data-active'); + expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); + }); + + test('wider item', async () => { + function App() { + return ( + // 1 to 9 numpad, but 4, 5 and 6 are one big button + + {['1', '2', '3', '456', '7', '8', '9'].map((i) => ( + + {i} + + ))} + + ); + } + + render(); + + act(() => screen.getByTestId('1').focus()); + expect(screen.getByTestId('1')).to.have.attribute('data-active'); + + fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); + await microtask(); + expect(screen.getByTestId('456')).to.have.attribute('data-active'); + expect(screen.getByTestId('456')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('456')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('456'), { key: 'ArrowDown' }); + await microtask(); + expect(screen.getByTestId('7')).to.have.attribute('data-active'); + expect(screen.getByTestId('7')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('7')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('7'), { key: 'ArrowRight' }); + await microtask(); + expect(screen.getByTestId('8')).to.have.attribute('data-active'); + expect(screen.getByTestId('8')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('8')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('8'), { key: 'ArrowUp' }); + await microtask(); + expect(screen.getByTestId('456')).to.have.attribute('data-active'); + expect(screen.getByTestId('456')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('456')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('456'), { key: 'ArrowUp' }); + await microtask(); + expect(screen.getByTestId('1')).to.have.attribute('data-active'); + expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('1')).toHaveFocus(); + + act(() => screen.getByTestId('9').focus()); + await microtask(); + expect(screen.getByTestId('9')).to.have.attribute('data-active'); + expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); + }); + + test('wider and taller item', async () => { + function App() { + return ( + // 1 to 9 numpad, but 4, 5, 7 and 8 are one big button + + {['1', '2', '3', '4578', '6', '9'].map((i) => ( + + {i} + + ))} + + ); + } + + render(); + + act(() => screen.getByTestId('1').focus()); + expect(screen.getByTestId('1')).to.have.attribute('data-active'); + + fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); + await microtask(); + expect(screen.getByTestId('4578')).to.have.attribute('data-active'); + expect(screen.getByTestId('4578')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('4578')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('4578'), { key: 'ArrowRight' }); + await microtask(); + expect(screen.getByTestId('6')).to.have.attribute('data-active'); + expect(screen.getByTestId('6')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('6')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('6'), { key: 'ArrowDown' }); + await microtask(); + expect(screen.getByTestId('9')).to.have.attribute('data-active'); + expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('9')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('9'), { key: 'ArrowLeft' }); + await microtask(); + expect(screen.getByTestId('4578')).to.have.attribute('data-active'); + expect(screen.getByTestId('4578')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('4578')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('4578'), { key: 'ArrowUp' }); + await microtask(); + expect(screen.getByTestId('1')).to.have.attribute('data-active'); + expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('1')).toHaveFocus(); + + act(() => screen.getByTestId('9').focus()); + await microtask(); + expect(screen.getByTestId('9')).to.have.attribute('data-active'); + expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); + }); + + test('grid flow', async () => { + function App() { + return ( + // 1 to 9 numpad, but 2, 3, 5 and 6 are one big button, and so are 7 and 8. + // 4 is missing + + {['1', '2356', '78', '9'].map((i) => ( + + {i} + + ))} + + ); + } + + render(); + + act(() => screen.getByTestId('1').focus()); + expect(screen.getByTestId('1')).to.have.attribute('data-active'); + + fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); + await microtask(); + expect(screen.getByTestId('78')).to.have.attribute('data-active'); + expect(screen.getByTestId('78')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('78')).toHaveFocus(); + }); + + test('grid flow: dense', async () => { + function App() { + return ( + // 1 to 9 numpad, but 2, 3, 5 and 6 are one big button, and so are 7 and 8. + // 9 is missing + + {['1', '2356', '78', '4'].map((i) => ( + + {i} + + ))} + + ); + } + + render(); + + act(() => screen.getByTestId('1').focus()); + expect(screen.getByTestId('1')).to.have.attribute('data-active'); + + fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); + await microtask(); + expect(screen.getByTestId('4')).to.have.attribute('data-active'); + expect(screen.getByTestId('4')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('4')).toHaveFocus(); + + fireEvent.keyDown(screen.getByTestId('4'), { key: 'ArrowDown' }); + await microtask(); + expect(screen.getByTestId('78')).to.have.attribute('data-active'); + expect(screen.getByTestId('78')).to.have.attribute('tabindex', '0'); + expect(screen.getByTestId('78')).toHaveFocus(); + }); + }); +}); diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.tsx b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx index 89976d1d89..734049a4d2 100644 --- a/packages/mui-base/src/Composite/Root/CompositeRoot.tsx +++ b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx @@ -59,6 +59,19 @@ CompositeRoot.propTypes /* remove-proptypes */ = { * @ignore */ cols: PropTypes.number, + /** + * @ignore + */ + dense: PropTypes.bool, + /** + * @ignore + */ + itemSizes: PropTypes.arrayOf( + PropTypes.shape({ + height: PropTypes.number.isRequired, + width: PropTypes.number.isRequired, + }), + ), /** * @ignore */ diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts b/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts index 51d3884773..f584723e6f 100644 --- a/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts +++ b/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts @@ -1,4 +1,5 @@ import type { BaseUIComponentProps } from '../../utils/types'; +import type { Dimensions } from '../composite'; export type CompositeRootOwnerState = {}; @@ -8,4 +9,6 @@ export interface CompositeRootProps extends BaseUIComponentProps<'div', Composit loop?: boolean; activeIndex?: number; onActiveIndexChange?: (index: number) => void; + itemSizes?: Dimensions[]; + dense?: boolean; } diff --git a/packages/mui-base/src/Composite/Root/useCompositeRoot.ts b/packages/mui-base/src/Composite/Root/useCompositeRoot.ts index 7435cb60d7..62d8029aa2 100644 --- a/packages/mui-base/src/Composite/Root/useCompositeRoot.ts +++ b/packages/mui-base/src/Composite/Root/useCompositeRoot.ts @@ -22,7 +22,7 @@ import { } from '../composite'; export interface UseCompositeRootParameters { - orientation?: 'horizontal' | 'vertical'; + orientation?: 'horizontal' | 'vertical' | 'both'; cols?: number; loop?: boolean; activeIndex?: number; @@ -43,22 +43,24 @@ export function useCompositeRoot(params: UseCompositeRootParameters) { cols = 1, loop = true, dense = false, - orientation = 'horizontal', + orientation = 'both', activeIndex: externalActiveIndex, onActiveIndexChange: externalSetActiveIndex, } = params; const [internalActiveIndex, internalSetActiveIndex] = React.useState(0); + const isGrid = cols > 1; + const activeIndex = externalActiveIndex ?? internalActiveIndex; const onActiveIndexChange = useEventCallback(externalSetActiveIndex ?? internalSetActiveIndex); + const elementsRef = React.useRef>([]); - const isGrid = cols > 1; const getRootProps = React.useCallback( (externalProps = {}) => mergeReactProps<'div'>(externalProps, { - 'aria-orientation': orientation, + 'aria-orientation': orientation === 'both' ? undefined : orientation, onKeyDown(event) { if (!ALL_KEYS.includes(event.key)) { return; diff --git a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx index a3e8e9f2bc..73b7d72147 100644 --- a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx +++ b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx @@ -6,7 +6,7 @@ import type { RadioGroupItemOwnerState, RadioGroupItemProps } from './RadioGroup import { useRadioGroupItem } from './useRadioGroupItem'; import { useRadioGroupRootContext } from '../Root/RadioGroupRootContext'; import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; -import { RadioGroupItemContext, RadioGroupItemContextValue } from './RadioGroupItemContext'; +import { RadioGroupItemContext, type RadioGroupItemContextValue } from './RadioGroupItemContext'; const customStyleHookMapping: CustomStyleHookMapping = { checked(value) { From aeab5fc3f51c85969957abfe66ea4462969be41a Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 24 Jul 2024 18:56:56 +1000 Subject: [PATCH 06/47] Remove passing of non-DOM props --- .../mui-base/src/Composite/Root/CompositeRoot.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.tsx b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx index 734049a4d2..bcbc19dd10 100644 --- a/packages/mui-base/src/Composite/Root/CompositeRoot.tsx +++ b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx @@ -13,7 +13,18 @@ const CompositeRoot = React.forwardRef(function CompositeRoot( props: CompositeRootProps, forwardedRef: React.ForwardedRef, ) { - const { render, className, ...otherProps } = props; + const { + render, + className, + activeIndex: activeIndexProp, + onActiveIndexChange: onActiveIndexChangeProp, + orientation, + dense, + itemSizes, + loop, + cols, + ...otherProps + } = props; const { getRootProps, activeIndex, onActiveIndexChange, elementsRef } = useCompositeRoot(props); From 8318f6d18c33fe28a00d67e5df326b20a1a20413 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 25 Jul 2024 11:40:17 +1000 Subject: [PATCH 07/47] Switch to name prop --- .../system/index.js | 7 ++- .../system/index.tsx | 7 ++- .../system/index.tsx.preview | 2 +- .../system/index.js | 6 +- .../system/index.tsx | 6 +- .../system/index.tsx.preview | 6 +- .../components/radio-group/radio-group.md | 57 +++++++++++++++++++ docs/pages/base-ui/api/radio-group-item.json | 2 +- .../radio-group-item/radio-group-item.json | 4 +- .../Indicator/RadioGroupIndicator.test.tsx | 2 +- .../RadioGroup/Item/RadioGroupItem.test.tsx | 2 +- .../src/RadioGroup/Item/RadioGroupItem.tsx | 9 +-- .../RadioGroup/Item/RadioGroupItem.types.ts | 6 +- .../src/RadioGroup/Item/useRadioGroupItem.ts | 13 ++--- .../RadioGroup/Root/RadioGroupRoot.test.tsx | 16 +++--- .../src/RadioGroup/Root/RadioGroupRoot.tsx | 1 + .../src/RadioGroup/Root/useRadioGroupRoot.ts | 2 +- 17 files changed, 105 insertions(+), 43 deletions(-) diff --git a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.js b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.js index 89107f62bd..d71ba3349a 100644 --- a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.js +++ b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.js @@ -6,7 +6,7 @@ export default function UnstyledPopoverIntroduction() { return ( Trigger - + Popover Title Popover Description @@ -31,6 +31,11 @@ export const PopoverPopup = styled(Popover.Popup)` filter: drop-shadow(0 2px 4px rgb(0 10 20 / 0.25)); outline: 0; padding: 8px 16px; + visibility: hidden; + + &[data-state='open'] { + visibility: visible; + } `; export const PopoverTitle = styled(Popover.Title)` diff --git a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx index 89107f62bd..d71ba3349a 100644 --- a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx +++ b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx @@ -6,7 +6,7 @@ export default function UnstyledPopoverIntroduction() { return ( Trigger - + Popover Title Popover Description @@ -31,6 +31,11 @@ export const PopoverPopup = styled(Popover.Popup)` filter: drop-shadow(0 2px 4px rgb(0 10 20 / 0.25)); outline: 0; padding: 8px 16px; + visibility: hidden; + + &[data-state='open'] { + visibility: visible; + } `; export const PopoverTitle = styled(Popover.Title)` diff --git a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx.preview b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx.preview index e8fe4ff7a4..d583fef884 100644 --- a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx.preview +++ b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx.preview @@ -1,6 +1,6 @@ Trigger - + Popover Title Popover Description diff --git a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js index bda71360ee..9b3c0979eb 100644 --- a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js +++ b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js @@ -5,15 +5,15 @@ import { styled } from '@mui/system'; export default function UnstyledRadioGroupIntroduction() { return ( - + Light - + Medium - + Heavy diff --git a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx index bda71360ee..9b3c0979eb 100644 --- a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx +++ b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx @@ -5,15 +5,15 @@ import { styled } from '@mui/system'; export default function UnstyledRadioGroupIntroduction() { return ( - + Light - + Medium - + Heavy diff --git a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview index 003e293a65..676b9a50c6 100644 --- a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview +++ b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx.preview @@ -1,13 +1,13 @@ - + Light - + Medium - + Heavy diff --git a/docs/data/base/components/radio-group/radio-group.md b/docs/data/base/components/radio-group/radio-group.md index a00e933fde..983aec7c65 100644 --- a/docs/data/base/components/radio-group/radio-group.md +++ b/docs/data/base/components/radio-group/radio-group.md @@ -43,3 +43,60 @@ Once you have the package installed, import the component. ```ts import * as RadioGroup from '@base_ui/react/RadioGroup'; ``` + +## Anatomy + +Radio Group is composed of a collection of related components: + +- `` is a top-level element that wraps the other components. +- `` renders an individual ` , diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx index ea1be7402c..57f00d2f26 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx @@ -18,6 +18,7 @@ const RadioGroupRoot = React.forwardRef(function RadioGroupRoot( readOnly, required, onValueChange: onValueChangeProp, + name, ...otherProps } = props; diff --git a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts index fb6ef80a0d..869d8f402d 100644 --- a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts +++ b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts @@ -28,7 +28,7 @@ export function useRadioGroupRoot(params: UseRadioGroupRootParameters) { (externalProps = {}) => mergeReactProps<'div'>(externalProps, { role: 'radiogroup', - 'aria-disabled': disabled, + 'aria-disabled': disabled || undefined, 'aria-readonly': readOnly || undefined, }), [disabled, readOnly], From d2f6e55f74d44e0206358aaf43fb9dd8cb9a1aeb Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 25 Jul 2024 15:00:00 +1000 Subject: [PATCH 08/47] Fix docs lint --- .../radio-group/UnstyledRadioGroupIntroduction/system/index.js | 1 - .../radio-group/UnstyledRadioGroupIntroduction/system/index.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js index 9b3c0979eb..d0f93c14ec 100644 --- a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js +++ b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.js @@ -41,7 +41,6 @@ const Item = styled(RadioGroup.Item)` border: none; background-color: ${grey[100]}; color: black; - cursor: pointer; outline: none; font-size: 16px; cursor: default; diff --git a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx index 9b3c0979eb..d0f93c14ec 100644 --- a/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx +++ b/docs/data/base/components/radio-group/UnstyledRadioGroupIntroduction/system/index.tsx @@ -41,7 +41,6 @@ const Item = styled(RadioGroup.Item)` border: none; background-color: ${grey[100]}; color: black; - cursor: pointer; outline: none; font-size: 16px; cursor: default; From 69d57604c1b233216bda871677c0c60dee8d0710 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 25 Jul 2024 15:06:00 +1000 Subject: [PATCH 09/47] Use span indicator --- .../mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx | 4 ++-- .../src/RadioGroup/Indicator/RadioGroupIndicator.types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx index 197856bbe3..8a02bc3914 100644 --- a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx +++ b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx @@ -19,7 +19,7 @@ const customStyleHookMapping: CustomStyleHookMapping, + forwardedRef: React.ForwardedRef, ) { const { render, className, keepMounted = false, ...otherProps } = props; @@ -39,7 +39,7 @@ const RadioGroupIndicator = React.forwardRef(function RadioGroupIndicator( const { renderElement } = useComponentRenderer({ propGetter: getIndicatorProps, - render: render ?? 'div', + render: render ?? 'span', ref: forwardedRef, className, ownerState, diff --git a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.types.ts b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.types.ts index 754e178bc0..d6b866b8aa 100644 --- a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.types.ts +++ b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.types.ts @@ -6,7 +6,7 @@ export type RadioGroupIndicatorOwnerState = { }; export interface RadioGroupIndicatorProps - extends BaseUIComponentProps<'div', RadioGroupIndicatorOwnerState> { + extends BaseUIComponentProps<'span', RadioGroupIndicatorOwnerState> { /** * If `true`, the indicator stays mounted when unchecked. Useful for CSS animations. * @default false From cd6f54d7cc032a7f86a30b1df05765b5c2a641d1 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 25 Jul 2024 15:38:02 +1000 Subject: [PATCH 10/47] Add autoselect --- docs/pages/base-ui/api/radio-group-root.json | 4 --- .../radio-group-root/radio-group-root.json | 1 - .../src/RadioGroup/Item/useRadioGroupItem.ts | 16 +++++++++-- .../RadioGroup/Root/RadioGroupRoot.test.tsx | 28 +++++++++++++++++++ .../src/RadioGroup/Root/RadioGroupRoot.tsx | 10 ++----- .../RadioGroup/Root/RadioGroupRoot.types.ts | 5 ---- .../RadioGroup/Root/RadioGroupRootContext.ts | 1 + .../src/RadioGroup/Root/useRadioGroupRoot.ts | 11 +++++++- 8 files changed, 56 insertions(+), 20 deletions(-) diff --git a/docs/pages/base-ui/api/radio-group-root.json b/docs/pages/base-ui/api/radio-group-root.json index ec9639af05..98d17db67e 100644 --- a/docs/pages/base-ui/api/radio-group-root.json +++ b/docs/pages/base-ui/api/radio-group-root.json @@ -5,10 +5,6 @@ "disabled": { "type": { "name": "bool" }, "default": "false" }, "name": { "type": { "name": "string" } }, "onValueChange": { "type": { "name": "func" } }, - "orientation": { - "type": { "name": "enum", "description": "'horizontal'
| 'vertical'" }, - "default": "'horizontal'" - }, "readOnly": { "type": { "name": "bool" }, "default": "false" }, "render": { "type": { "name": "union", "description": "element
| func" } }, "required": { "type": { "name": "bool" }, "default": "false" }, diff --git a/docs/translations/api-docs/radio-group-root/radio-group-root.json b/docs/translations/api-docs/radio-group-root/radio-group-root.json index 2f00dc8f93..bbf977c55b 100644 --- a/docs/translations/api-docs/radio-group-root/radio-group-root.json +++ b/docs/translations/api-docs/radio-group-root/radio-group-root.json @@ -10,7 +10,6 @@ "disabled": { "description": "Determines if the radio group is disabled." }, "name": { "description": "The name of the radio group submitted with the form data." }, "onValueChange": { "description": "Callback fired when the value changes." }, - "orientation": { "description": "The orientation of the radio group." }, "readOnly": { "description": "Determines if the radio group is readonly." }, "render": { "description": "A function to customize rendering of the component." }, "required": { "description": "Determines if the radio group is required." }, diff --git a/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts b/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts index 533333643e..a6d7f67c26 100644 --- a/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts +++ b/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts @@ -19,7 +19,7 @@ interface UseRadioGroupItemParameters { export function useRadioGroupItem(params: UseRadioGroupItemParameters) { const { disabled, readOnly, name, required } = params; - const { checkedItem, setCheckedItem, onValueChange } = useRadioGroupRootContext(); + const { checkedItem, setCheckedItem, onValueChange, touched } = useRadioGroupRootContext(); const checked = checkedItem === name; @@ -34,6 +34,11 @@ export function useRadioGroupItem(params: UseRadioGroupItemParameters) { 'aria-required': required, 'aria-disabled': disabled || undefined, 'aria-readonly': readOnly || undefined, + onKeyDown(event) { + if (event.key === 'Enter') { + event.preventDefault(); + } + }, onClick(event) { if (event.defaultPrevented || disabled || readOnly) { return; @@ -41,10 +46,17 @@ export function useRadioGroupItem(params: UseRadioGroupItemParameters) { event.preventDefault(); + inputRef.current?.click(); + }, + onFocus(event) { + if (event.defaultPrevented || disabled || readOnly || !touched) { + return; + } + inputRef.current?.click(); }, }), - [checked, disabled, readOnly, required], + [checked, disabled, readOnly, required, touched], ); const getInputProps = React.useCallback( diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx index bff159196c..f347e955af 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx @@ -2,11 +2,14 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; import { createRenderer, act, screen } from '@mui/internal-test-utils'; +import userEvent from '@testing-library/user-event'; import * as RadioGroup from '@base_ui/react/RadioGroup'; import { describeConformance } from '../../../test/describeConformance'; const isJSDOM = /jsdom/.test(window.navigator.userAgent); +const user = userEvent.setup(); + describe('', () => { const { render } = createRenderer(); @@ -196,4 +199,29 @@ describe('', () => { expect(stringifiedFormData).to.equal('a=on;b=off;c=off;group=a'); }); + + it('should automatically select item upon navigation', async () => { + render( + + + + , + ); + + const a = screen.getByTestId('a'); + const b = screen.getByTestId('b'); + + act(() => { + a.focus(); + }); + + expect(a).to.have.attribute('aria-checked', 'false'); + + await user.keyboard('{ArrowDown}'); + + expect(a).to.have.attribute('aria-checked', 'false'); + + expect(b).toHaveFocus(); + expect(b).to.have.attribute('aria-checked', 'true'); + }); }); diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx index 57f00d2f26..773c90a961 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx @@ -22,7 +22,7 @@ const RadioGroupRoot = React.forwardRef(function RadioGroupRoot( ...otherProps } = props; - const { getRootProps, checkedItem, setCheckedItem } = useRadioGroupRoot(props); + const { getRootProps, checkedItem, setCheckedItem, touched } = useRadioGroupRoot(props); const onValueChange = useEventCallback(onValueChangeProp ?? (() => {})); @@ -43,8 +43,9 @@ const RadioGroupRoot = React.forwardRef(function RadioGroupRoot( disabled, readOnly, required, + touched, }), - [checkedItem, setCheckedItem, onValueChange, disabled, readOnly, required], + [checkedItem, setCheckedItem, onValueChange, disabled, readOnly, required, touched], ); const { renderElement } = useComponentRenderer({ @@ -96,11 +97,6 @@ RadioGroupRoot.propTypes /* remove-proptypes */ = { * Callback fired when the value changes. */ onValueChange: PropTypes.func, - /** - * The orientation of the radio group. - * @default 'horizontal' - */ - orientation: PropTypes.oneOf(['horizontal', 'vertical']), /** * Determines if the radio group is readonly. * @default false diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.types.ts b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.types.ts index ba6749ebc7..197d8488b3 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.types.ts +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.types.ts @@ -37,9 +37,4 @@ export interface RadioGroupRootProps extends BaseUIComponentProps<'div', RadioGr * Callback fired when the value changes. */ onValueChange?: (value: string, event: React.ChangeEvent) => void; - /** - * The orientation of the radio group. - * @default 'horizontal' - */ - orientation?: 'horizontal' | 'vertical'; } diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts b/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts index c05afa14e8..883335888d 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts @@ -7,6 +7,7 @@ export interface RadioGroupRootContextValue { checkedItem: string | null; setCheckedItem: React.Dispatch>; onValueChange: (value: string, event: React.ChangeEvent) => void; + touched: boolean; } export const RadioGroupRootContext = React.createContext(null); diff --git a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts index 869d8f402d..5d40a35e25 100644 --- a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts +++ b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts @@ -24,12 +24,19 @@ export function useRadioGroupRoot(params: UseRadioGroupRootParameters) { state: 'value', }); + const [touched, setTouched] = React.useState(false); + const getRootProps = React.useCallback( (externalProps = {}) => mergeReactProps<'div'>(externalProps, { role: 'radiogroup', 'aria-disabled': disabled || undefined, 'aria-readonly': readOnly || undefined, + onKeyDownCapture(event) { + if (event.key === ' ' || event.key.startsWith('Arrow')) { + setTouched(true); + } + }, }), [disabled, readOnly], ); @@ -39,7 +46,9 @@ export function useRadioGroupRoot(params: UseRadioGroupRootParameters) { getRootProps, checkedItem, setCheckedItem, + touched, + setTouched, }), - [getRootProps, checkedItem, setCheckedItem], + [getRootProps, checkedItem, setCheckedItem, touched], ); } From 4df6bd23b0586de04b63d9e26fa093917c1f99ad Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 25 Jul 2024 15:41:19 +1000 Subject: [PATCH 11/47] Remove unrelated changes --- .../popover/UnstyledPopoverIntroduction/system/index.js | 7 +------ .../popover/UnstyledPopoverIntroduction/system/index.tsx | 7 +------ .../UnstyledPopoverIntroduction/system/index.tsx.preview | 2 +- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.js b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.js index d71ba3349a..89107f62bd 100644 --- a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.js +++ b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.js @@ -6,7 +6,7 @@ export default function UnstyledPopoverIntroduction() { return ( Trigger - + Popover Title Popover Description @@ -31,11 +31,6 @@ export const PopoverPopup = styled(Popover.Popup)` filter: drop-shadow(0 2px 4px rgb(0 10 20 / 0.25)); outline: 0; padding: 8px 16px; - visibility: hidden; - - &[data-state='open'] { - visibility: visible; - } `; export const PopoverTitle = styled(Popover.Title)` diff --git a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx index d71ba3349a..89107f62bd 100644 --- a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx +++ b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx @@ -6,7 +6,7 @@ export default function UnstyledPopoverIntroduction() { return ( Trigger - + Popover Title Popover Description @@ -31,11 +31,6 @@ export const PopoverPopup = styled(Popover.Popup)` filter: drop-shadow(0 2px 4px rgb(0 10 20 / 0.25)); outline: 0; padding: 8px 16px; - visibility: hidden; - - &[data-state='open'] { - visibility: visible; - } `; export const PopoverTitle = styled(Popover.Title)` diff --git a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx.preview b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx.preview index d583fef884..e8fe4ff7a4 100644 --- a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx.preview +++ b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx.preview @@ -1,6 +1,6 @@ Trigger - + Popover Title Popover Description From a1db8423a4a6bb22f18a21af7942c55b36177d7e Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 25 Jul 2024 15:54:57 +1000 Subject: [PATCH 12/47] use client --- .../mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx | 1 + .../mui-base/src/RadioGroup/Indicator/useRadioGroupIndicator.ts | 2 ++ packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx | 1 + packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts | 1 + packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx | 1 + packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts | 1 + packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts | 2 ++ 7 files changed, 9 insertions(+) diff --git a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx index 8a02bc3914..0b1b5ac5ce 100644 --- a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx +++ b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; diff --git a/packages/mui-base/src/RadioGroup/Indicator/useRadioGroupIndicator.ts b/packages/mui-base/src/RadioGroup/Indicator/useRadioGroupIndicator.ts index 28759797de..95a78b1b64 100644 --- a/packages/mui-base/src/RadioGroup/Indicator/useRadioGroupIndicator.ts +++ b/packages/mui-base/src/RadioGroup/Indicator/useRadioGroupIndicator.ts @@ -1,5 +1,7 @@ +'use client'; import * as React from 'react'; import { mergeReactProps } from '../../utils/mergeReactProps'; + /** * * API: diff --git a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx index 33fdcdb0b5..094edab3c4 100644 --- a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx +++ b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import { CompositeItem } from '../../Composite/Item/CompositeItem'; diff --git a/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts b/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts index a6d7f67c26..8e298033e8 100644 --- a/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts +++ b/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { visuallyHidden } from '../../utils/visuallyHidden'; diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx index 773c90a961..d11bf6e15c 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import { CompositeRoot } from '../../Composite/Root/CompositeRoot'; diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts b/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts index 883335888d..5b7883f764 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; export interface RadioGroupRootContextValue { diff --git a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts index 5d40a35e25..86cf596a7f 100644 --- a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts +++ b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { useControlled } from '../../utils/useControlled'; @@ -8,6 +9,7 @@ interface UseRadioGroupRootParameters { defaultValue?: string; value?: string; } + /** * * API: From d1a2124b9412f2d0fa6a7de9b02b5a0be72f7564 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 25 Jul 2024 16:22:35 +1000 Subject: [PATCH 13/47] Refactor touched state --- .../src/Composite/Root/CompositeRoot.tsx | 1 + .../src/Composite/Root/CompositeRoot.types.ts | 1 + .../src/Composite/Root/useCompositeRoot.ts | 20 +++++++++++++++--- .../src/RadioGroup/Item/useRadioGroupItem.ts | 7 +++++-- .../src/RadioGroup/Root/RadioGroupRoot.tsx | 6 ++++-- .../RadioGroup/Root/RadioGroupRootContext.ts | 1 + .../src/RadioGroup/Root/useRadioGroupRoot.ts | 21 ++++++++++++++----- 7 files changed, 45 insertions(+), 12 deletions(-) diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.tsx b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx index bcbc19dd10..10a8b68b38 100644 --- a/packages/mui-base/src/Composite/Root/CompositeRoot.tsx +++ b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx @@ -18,6 +18,7 @@ const CompositeRoot = React.forwardRef(function CompositeRoot( className, activeIndex: activeIndexProp, onActiveIndexChange: onActiveIndexChangeProp, + elementsRef: elementsRefProp, orientation, dense, itemSizes, diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts b/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts index f584723e6f..29eaa07108 100644 --- a/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts +++ b/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts @@ -4,6 +4,7 @@ import type { Dimensions } from '../composite'; export type CompositeRootOwnerState = {}; export interface CompositeRootProps extends BaseUIComponentProps<'div', CompositeRootOwnerState> { + elementsRef?: React.MutableRefObject>; orientation?: 'horizontal' | 'vertical'; cols?: number; loop?: boolean; diff --git a/packages/mui-base/src/Composite/Root/useCompositeRoot.ts b/packages/mui-base/src/Composite/Root/useCompositeRoot.ts index 62d8029aa2..0e5f48d530 100644 --- a/packages/mui-base/src/Composite/Root/useCompositeRoot.ts +++ b/packages/mui-base/src/Composite/Root/useCompositeRoot.ts @@ -29,6 +29,7 @@ export interface UseCompositeRootParameters { onActiveIndexChange?: (index: number) => void; dense?: boolean; itemSizes?: Array; + elementsRef?: React.MutableRefObject>; } // TODO @@ -46,6 +47,7 @@ export function useCompositeRoot(params: UseCompositeRootParameters) { orientation = 'both', activeIndex: externalActiveIndex, onActiveIndexChange: externalSetActiveIndex, + elementsRef: externalElementsRef, } = params; const [internalActiveIndex, internalSetActiveIndex] = React.useState(0); @@ -55,7 +57,9 @@ export function useCompositeRoot(params: UseCompositeRootParameters) { const activeIndex = externalActiveIndex ?? internalActiveIndex; const onActiveIndexChange = useEventCallback(externalSetActiveIndex ?? internalSetActiveIndex); - const elementsRef = React.useRef>([]); + const internalElementsRef = React.useRef>([]); + + const elementsRef = externalElementsRef ?? internalElementsRef; const getRootProps = React.useCallback( (externalProps = {}) => @@ -184,7 +188,17 @@ export function useCompositeRoot(params: UseCompositeRootParameters) { } }, }), - [activeIndex, cols, dense, isGrid, itemSizes, loop, onActiveIndexChange, orientation], + [ + activeIndex, + cols, + dense, + elementsRef, + isGrid, + itemSizes, + loop, + onActiveIndexChange, + orientation, + ], ); return React.useMemo( @@ -194,6 +208,6 @@ export function useCompositeRoot(params: UseCompositeRootParameters) { onActiveIndexChange, elementsRef, }), - [getRootProps, activeIndex, onActiveIndexChange], + [getRootProps, activeIndex, onActiveIndexChange, elementsRef], ); } diff --git a/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts b/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts index 8e298033e8..c77654d5af 100644 --- a/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts +++ b/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts @@ -20,7 +20,8 @@ interface UseRadioGroupItemParameters { export function useRadioGroupItem(params: UseRadioGroupItemParameters) { const { disabled, readOnly, name, required } = params; - const { checkedItem, setCheckedItem, onValueChange, touched } = useRadioGroupRootContext(); + const { checkedItem, setCheckedItem, onValueChange, touched, setTouched } = + useRadioGroupRootContext(); const checked = checkedItem === name; @@ -55,9 +56,11 @@ export function useRadioGroupItem(params: UseRadioGroupItemParameters) { } inputRef.current?.click(); + + setTouched(false); }, }), - [checked, disabled, readOnly, required, touched], + [checked, disabled, readOnly, required, touched, setTouched], ); const getInputProps = React.useCallback( diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx index d11bf6e15c..1017d105e3 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx @@ -23,7 +23,8 @@ const RadioGroupRoot = React.forwardRef(function RadioGroupRoot( ...otherProps } = props; - const { getRootProps, checkedItem, setCheckedItem, touched } = useRadioGroupRoot(props); + const { getRootProps, checkedItem, setCheckedItem, touched, setTouched } = + useRadioGroupRoot(props); const onValueChange = useEventCallback(onValueChangeProp ?? (() => {})); @@ -45,8 +46,9 @@ const RadioGroupRoot = React.forwardRef(function RadioGroupRoot( readOnly, required, touched, + setTouched, }), - [checkedItem, setCheckedItem, onValueChange, disabled, readOnly, required, touched], + [checkedItem, setCheckedItem, onValueChange, disabled, readOnly, required, touched, setTouched], ); const { renderElement } = useComponentRenderer({ diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts b/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts index 5b7883f764..b3e909ecad 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts @@ -9,6 +9,7 @@ export interface RadioGroupRootContextValue { setCheckedItem: React.Dispatch>; onValueChange: (value: string, event: React.ChangeEvent) => void; touched: boolean; + setTouched: React.Dispatch>; } export const RadioGroupRootContext = React.createContext(null); diff --git a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts index 86cf596a7f..9fb2f18698 100644 --- a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts +++ b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts @@ -1,4 +1,3 @@ -'use client'; import * as React from 'react'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { useControlled } from '../../utils/useControlled'; @@ -8,8 +7,8 @@ interface UseRadioGroupRootParameters { readOnly?: boolean; defaultValue?: string; value?: string; + orientation?: 'horizontal' | 'vertical' | 'both'; } - /** * * API: @@ -17,7 +16,7 @@ interface UseRadioGroupRootParameters { * - [useRadioGroupRoot API](https://mui.com/base-ui/api/use-radio-group-root/) */ export function useRadioGroupRoot(params: UseRadioGroupRootParameters) { - const { disabled, defaultValue, readOnly, value: externalValue } = params; + const { disabled, defaultValue, readOnly, orientation, value: externalValue } = params; const [checkedItem, setCheckedItem] = useControlled({ controlled: externalValue, @@ -35,12 +34,24 @@ export function useRadioGroupRoot(params: UseRadioGroupRootParameters) { 'aria-disabled': disabled || undefined, 'aria-readonly': readOnly || undefined, onKeyDownCapture(event) { - if (event.key === ' ' || event.key.startsWith('Arrow')) { + let navigated = false; + switch (orientation) { + case 'vertical': + navigated = event.key === 'ArrowUp' || event.key === 'ArrowDown'; + break; + case 'horizontal': + navigated = event.key === 'ArrowLeft' || event.key === 'ArrowRight'; + break; + default: + navigated = event.key.startsWith('Arrow'); + } + + if (navigated) { setTouched(true); } }, }), - [disabled, readOnly], + [disabled, readOnly, orientation], ); return React.useMemo( From 3f04ebe05b7e91614a4a5fece7f475b6c4bcbc5b Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 25 Jul 2024 16:23:44 +1000 Subject: [PATCH 14/47] Remove orientation --- .../src/Composite/Root/CompositeRoot.tsx | 12 ++++++++++++ .../src/RadioGroup/Root/RadioGroupRoot.tsx | 2 +- .../src/RadioGroup/Root/useRadioGroupRoot.ts | 19 +++---------------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.tsx b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx index 10a8b68b38..81ca8e135c 100644 --- a/packages/mui-base/src/Composite/Root/CompositeRoot.tsx +++ b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx @@ -75,6 +75,18 @@ CompositeRoot.propTypes /* remove-proptypes */ = { * @ignore */ dense: PropTypes.bool, + /** + * @ignore + */ + elementsRef: PropTypes.shape({ + current: PropTypes.arrayOf(function (props, propName) { + if (props[propName] == null) { + return null; + } else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) { + return new Error("Expected prop '" + propName + "' to be of type Element"); + } + }).isRequired, + }), /** * @ignore */ diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx index 1017d105e3..b684bcc543 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx @@ -3,10 +3,10 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { CompositeRoot } from '../../Composite/Root/CompositeRoot'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useEventCallback } from '../../utils/useEventCallback'; import type { RadioGroupRootOwnerState, RadioGroupRootProps } from './RadioGroupRoot.types'; import { useRadioGroupRoot } from './useRadioGroupRoot'; import { type RadioGroupRootContextValue, RadioGroupRootContext } from './RadioGroupRootContext'; -import { useEventCallback } from '../../utils/useEventCallback'; const RadioGroupRoot = React.forwardRef(function RadioGroupRoot( props: RadioGroupRootProps, diff --git a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts index 9fb2f18698..282dc4925f 100644 --- a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts +++ b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts @@ -7,7 +7,6 @@ interface UseRadioGroupRootParameters { readOnly?: boolean; defaultValue?: string; value?: string; - orientation?: 'horizontal' | 'vertical' | 'both'; } /** * @@ -16,7 +15,7 @@ interface UseRadioGroupRootParameters { * - [useRadioGroupRoot API](https://mui.com/base-ui/api/use-radio-group-root/) */ export function useRadioGroupRoot(params: UseRadioGroupRootParameters) { - const { disabled, defaultValue, readOnly, orientation, value: externalValue } = params; + const { disabled, defaultValue, readOnly, value: externalValue } = params; const [checkedItem, setCheckedItem] = useControlled({ controlled: externalValue, @@ -34,24 +33,12 @@ export function useRadioGroupRoot(params: UseRadioGroupRootParameters) { 'aria-disabled': disabled || undefined, 'aria-readonly': readOnly || undefined, onKeyDownCapture(event) { - let navigated = false; - switch (orientation) { - case 'vertical': - navigated = event.key === 'ArrowUp' || event.key === 'ArrowDown'; - break; - case 'horizontal': - navigated = event.key === 'ArrowLeft' || event.key === 'ArrowRight'; - break; - default: - navigated = event.key.startsWith('Arrow'); - } - - if (navigated) { + if (event.key.startsWith('Arrow')) { setTouched(true); } }, }), - [disabled, readOnly, orientation], + [disabled, readOnly], ); return React.useMemo( From 4fcc06d7b68a710b39b75fa9915d312f616cc996 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 25 Jul 2024 16:28:15 +1000 Subject: [PATCH 15/47] Remove elementsRef --- .../mui-base/src/Composite/Root/CompositeRoot.tsx | 15 +-------------- .../src/Composite/Root/CompositeRoot.types.ts | 3 +-- .../src/Composite/Root/useCompositeRoot.ts | 8 ++------ 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.tsx b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx index 81ca8e135c..cce6d559c0 100644 --- a/packages/mui-base/src/Composite/Root/CompositeRoot.tsx +++ b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx @@ -18,7 +18,6 @@ const CompositeRoot = React.forwardRef(function CompositeRoot( className, activeIndex: activeIndexProp, onActiveIndexChange: onActiveIndexChangeProp, - elementsRef: elementsRefProp, orientation, dense, itemSizes, @@ -75,18 +74,6 @@ CompositeRoot.propTypes /* remove-proptypes */ = { * @ignore */ dense: PropTypes.bool, - /** - * @ignore - */ - elementsRef: PropTypes.shape({ - current: PropTypes.arrayOf(function (props, propName) { - if (props[propName] == null) { - return null; - } else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) { - return new Error("Expected prop '" + propName + "' to be of type Element"); - } - }).isRequired, - }), /** * @ignore */ @@ -107,7 +94,7 @@ CompositeRoot.propTypes /* remove-proptypes */ = { /** * @ignore */ - orientation: PropTypes.oneOf(['horizontal', 'vertical']), + orientation: PropTypes.oneOf(['both', 'horizontal', 'vertical']), /** * A function to customize rendering of the component. */ diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts b/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts index 29eaa07108..79accd8819 100644 --- a/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts +++ b/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts @@ -4,8 +4,7 @@ import type { Dimensions } from '../composite'; export type CompositeRootOwnerState = {}; export interface CompositeRootProps extends BaseUIComponentProps<'div', CompositeRootOwnerState> { - elementsRef?: React.MutableRefObject>; - orientation?: 'horizontal' | 'vertical'; + orientation?: 'horizontal' | 'vertical' | 'both'; cols?: number; loop?: boolean; activeIndex?: number; diff --git a/packages/mui-base/src/Composite/Root/useCompositeRoot.ts b/packages/mui-base/src/Composite/Root/useCompositeRoot.ts index 0e5f48d530..267628c1d8 100644 --- a/packages/mui-base/src/Composite/Root/useCompositeRoot.ts +++ b/packages/mui-base/src/Composite/Root/useCompositeRoot.ts @@ -29,10 +29,9 @@ export interface UseCompositeRootParameters { onActiveIndexChange?: (index: number) => void; dense?: boolean; itemSizes?: Array; - elementsRef?: React.MutableRefObject>; } -// TODO +// Advanced options of Composite, to be implemented later if needed. const disabledIndices = undefined; /** @@ -47,7 +46,6 @@ export function useCompositeRoot(params: UseCompositeRootParameters) { orientation = 'both', activeIndex: externalActiveIndex, onActiveIndexChange: externalSetActiveIndex, - elementsRef: externalElementsRef, } = params; const [internalActiveIndex, internalSetActiveIndex] = React.useState(0); @@ -57,9 +55,7 @@ export function useCompositeRoot(params: UseCompositeRootParameters) { const activeIndex = externalActiveIndex ?? internalActiveIndex; const onActiveIndexChange = useEventCallback(externalSetActiveIndex ?? internalSetActiveIndex); - const internalElementsRef = React.useRef>([]); - - const elementsRef = externalElementsRef ?? internalElementsRef; + const elementsRef = React.useRef>([]); const getRootProps = React.useCallback( (externalProps = {}) => From 775fb8c715f849e82a825ed434dd272d5b1b51c8 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 25 Jul 2024 16:29:01 +1000 Subject: [PATCH 16/47] use client --- packages/mui-base/src/Composite/Item/CompositeItem.tsx | 1 + packages/mui-base/src/Composite/Item/useCompositeItem.ts | 2 ++ packages/mui-base/src/Composite/Root/CompositeRoot.tsx | 1 + packages/mui-base/src/Composite/Root/useCompositeRoot.ts | 1 + .../src/Composite/utils/CompositeList/CompositeList.tsx | 1 + .../src/Composite/utils/CompositeList/useCompositeListItem.ts | 1 + 6 files changed, 7 insertions(+) diff --git a/packages/mui-base/src/Composite/Item/CompositeItem.tsx b/packages/mui-base/src/Composite/Item/CompositeItem.tsx index c6cd31ce63..48cd91c990 100644 --- a/packages/mui-base/src/Composite/Item/CompositeItem.tsx +++ b/packages/mui-base/src/Composite/Item/CompositeItem.tsx @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; diff --git a/packages/mui-base/src/Composite/Item/useCompositeItem.ts b/packages/mui-base/src/Composite/Item/useCompositeItem.ts index 320191360c..ab385d81a4 100644 --- a/packages/mui-base/src/Composite/Item/useCompositeItem.ts +++ b/packages/mui-base/src/Composite/Item/useCompositeItem.ts @@ -1,7 +1,9 @@ +'use client'; import * as React from 'react'; import { useCompositeRootContext } from '../Root/CompositeRootContext'; import { useCompositeListItem } from '../utils/CompositeList/useCompositeListItem'; import { mergeReactProps } from '../../utils/mergeReactProps'; + /** * * API: diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.tsx b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx index cce6d559c0..59b607f7ed 100644 --- a/packages/mui-base/src/Composite/Root/CompositeRoot.tsx +++ b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; diff --git a/packages/mui-base/src/Composite/Root/useCompositeRoot.ts b/packages/mui-base/src/Composite/Root/useCompositeRoot.ts index 267628c1d8..3a42f6f8dd 100644 --- a/packages/mui-base/src/Composite/Root/useCompositeRoot.ts +++ b/packages/mui-base/src/Composite/Root/useCompositeRoot.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import { useEventCallback } from '../../utils/useEventCallback'; import { mergeReactProps } from '../../utils/mergeReactProps'; diff --git a/packages/mui-base/src/Composite/utils/CompositeList/CompositeList.tsx b/packages/mui-base/src/Composite/utils/CompositeList/CompositeList.tsx index bbafb9f11b..66280aca9f 100644 --- a/packages/mui-base/src/Composite/utils/CompositeList/CompositeList.tsx +++ b/packages/mui-base/src/Composite/utils/CompositeList/CompositeList.tsx @@ -1,4 +1,5 @@ /* eslint-disable no-bitwise */ +'use client'; import * as React from 'react'; import { useEnhancedEffect } from '../../../utils/useEnhancedEffect'; import { CompositeListContext } from './CompositeListContext'; diff --git a/packages/mui-base/src/Composite/utils/CompositeList/useCompositeListItem.ts b/packages/mui-base/src/Composite/utils/CompositeList/useCompositeListItem.ts index 2ca0364544..8f40682e41 100644 --- a/packages/mui-base/src/Composite/utils/CompositeList/useCompositeListItem.ts +++ b/packages/mui-base/src/Composite/utils/CompositeList/useCompositeListItem.ts @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import { useEnhancedEffect } from '../../../utils/useEnhancedEffect'; import { useCompositeListContext } from './CompositeListContext'; From 6e436bf709200352b90456e13cab1ef1a34de23e Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 25 Jul 2024 16:33:59 +1000 Subject: [PATCH 17/47] Fix test --- .../src/RadioGroup/Indicator/RadioGroupIndicator.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.test.tsx b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.test.tsx index c5f6252d0d..d657fc5557 100644 --- a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.test.tsx +++ b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.test.tsx @@ -7,7 +7,8 @@ describe('', () => { const { render } = createRenderer(); describeConformance(, () => ({ - refInstanceof: window.HTMLDivElement, + inheritComponent: 'span', + refInstanceof: window.HTMLSpanElement, render(node) { return render( From fd76688413d07a97bd09b51d86a7aa32a61d4813 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 25 Jul 2024 16:34:53 +1000 Subject: [PATCH 18/47] Remove unused hook --- .../Indicator/RadioGroupIndicator.tsx | 4 ---- .../Indicator/useRadioGroupIndicator.ts | 23 ------------------- 2 files changed, 27 deletions(-) delete mode 100644 packages/mui-base/src/RadioGroup/Indicator/useRadioGroupIndicator.ts diff --git a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx index 0b1b5ac5ce..3d644f60ea 100644 --- a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx +++ b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; import { useRadioGroupItemContext } from '../Item/RadioGroupItemContext'; -import { useRadioGroupIndicator } from './useRadioGroupIndicator'; import type { RadioGroupIndicatorOwnerState, RadioGroupIndicatorProps, @@ -26,8 +25,6 @@ const RadioGroupIndicator = React.forwardRef(function RadioGroupIndicator( const { disabled, checked, required, readOnly } = useRadioGroupItemContext(); - const { getIndicatorProps } = useRadioGroupIndicator(); - const ownerState: RadioGroupIndicatorOwnerState = React.useMemo( () => ({ disabled, @@ -39,7 +36,6 @@ const RadioGroupIndicator = React.forwardRef(function RadioGroupIndicator( ); const { renderElement } = useComponentRenderer({ - propGetter: getIndicatorProps, render: render ?? 'span', ref: forwardedRef, className, diff --git a/packages/mui-base/src/RadioGroup/Indicator/useRadioGroupIndicator.ts b/packages/mui-base/src/RadioGroup/Indicator/useRadioGroupIndicator.ts deleted file mode 100644 index 95a78b1b64..0000000000 --- a/packages/mui-base/src/RadioGroup/Indicator/useRadioGroupIndicator.ts +++ /dev/null @@ -1,23 +0,0 @@ -'use client'; -import * as React from 'react'; -import { mergeReactProps } from '../../utils/mergeReactProps'; - -/** - * - * API: - * - * - [useRadioGroupIndicator API](https://mui.com/base-ui/api/use-radio-group-indicator/) - */ -export function useRadioGroupIndicator() { - const getIndicatorProps = React.useCallback( - (externalProps = {}) => mergeReactProps<'span'>(externalProps, {}), - [], - ); - - return React.useMemo( - () => ({ - getIndicatorProps, - }), - [getIndicatorProps], - ); -} From 750c032bd3b5b9f4569eac415e8f4a94a707500a Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 25 Jul 2024 16:55:40 +1000 Subject: [PATCH 19/47] Codegen --- docs/pages/base-ui/api/radio-group-indicator.json | 2 +- docs/pages/base-ui/api/use-radio-group-indicator.json | 8 -------- .../use-radio-group-indicator.json | 1 - 3 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 docs/pages/base-ui/api/use-radio-group-indicator.json delete mode 100644 docs/translations/api-docs/use-radio-group-indicator/use-radio-group-indicator.json diff --git a/docs/pages/base-ui/api/radio-group-indicator.json b/docs/pages/base-ui/api/radio-group-indicator.json index de4d275e55..bce914f096 100644 --- a/docs/pages/base-ui/api/radio-group-indicator.json +++ b/docs/pages/base-ui/api/radio-group-indicator.json @@ -12,7 +12,7 @@ "spread": true, "themeDefaultProps": true, "muiName": "RadioGroupIndicator", - "forwardsRefTo": "HTMLDivElement", + "forwardsRefTo": "HTMLSpanElement", "filename": "/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.tsx", "inheritance": null, "demos": "", diff --git a/docs/pages/base-ui/api/use-radio-group-indicator.json b/docs/pages/base-ui/api/use-radio-group-indicator.json deleted file mode 100644 index 1fee5f8eac..0000000000 --- a/docs/pages/base-ui/api/use-radio-group-indicator.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "parameters": {}, - "returnValue": {}, - "name": "useRadioGroupIndicator", - "filename": "/packages/mui-base/src/RadioGroup/Indicator/useRadioGroupIndicator.ts", - "imports": ["import { useRadioGroupIndicator } from '@base_ui/react/RadioGroup';"], - "demos": "
    " -} diff --git a/docs/translations/api-docs/use-radio-group-indicator/use-radio-group-indicator.json b/docs/translations/api-docs/use-radio-group-indicator/use-radio-group-indicator.json deleted file mode 100644 index e3eb65c6e4..0000000000 --- a/docs/translations/api-docs/use-radio-group-indicator/use-radio-group-indicator.json +++ /dev/null @@ -1 +0,0 @@ -{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } From 0e8df0a85fb73df765f4a3753a183099b9fee213 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 25 Jul 2024 17:03:47 +1000 Subject: [PATCH 20/47] name -> value --- .../components/radio-group/radio-group.md | 18 +++++++------- docs/pages/base-ui/api/radio-group-item.json | 2 +- .../radio-group-item/radio-group-item.json | 4 ++-- .../Indicator/RadioGroupIndicator.test.tsx | 2 +- .../RadioGroup/Item/RadioGroupItem.test.tsx | 2 +- .../src/RadioGroup/Item/RadioGroupItem.tsx | 9 ++++--- .../RadioGroup/Item/RadioGroupItem.types.ts | 6 ++--- .../src/RadioGroup/Item/useRadioGroupItem.ts | 15 ++++++------ .../RadioGroup/Root/RadioGroupRoot.test.tsx | 24 +++++++++---------- 9 files changed, 40 insertions(+), 42 deletions(-) diff --git a/docs/data/base/components/radio-group/radio-group.md b/docs/data/base/components/radio-group/radio-group.md index 983aec7c65..6076e90b27 100644 --- a/docs/data/base/components/radio-group/radio-group.md +++ b/docs/data/base/components/radio-group/radio-group.md @@ -62,14 +62,14 @@ Radio Group is composed of a collection of related components: ## Identifying items -The `name` prop on `RadioGroup.Item` identifies it in the group and owning form. +The `value` prop is required on `RadioGroup.Item` to identify it in the group. ```jsx - + - + @@ -77,26 +77,26 @@ The `name` prop on `RadioGroup.Item` identifies it in the group and owning form. ## Default value -The `defaultValue` prop determines the initial value of the component when uncontrolled, linked to the `name` prop on the items. +The `defaultValue` prop determines the initial value of the component when uncontrolled, linked to the `value` prop on the items. ```jsx - - + + ``` ## Controlled -The `value` and `onValueChange` props contain the `name` string of the currently selected item in the radio group. +The `value` and `onValueChange` props contain the `value` string of the currently selected item in the radio group. ```jsx const [value, setValue] = React.useState('a'); return ( - - + + ); ``` diff --git a/docs/pages/base-ui/api/radio-group-item.json b/docs/pages/base-ui/api/radio-group-item.json index 9eedcdaf14..ea0650b641 100644 --- a/docs/pages/base-ui/api/radio-group-item.json +++ b/docs/pages/base-ui/api/radio-group-item.json @@ -1,6 +1,6 @@ { "props": { - "name": { "type": { "name": "string" }, "required": true }, + "value": { "type": { "name": "string" }, "required": true }, "className": { "type": { "name": "union", "description": "func
    | string" } }, "disabled": { "type": { "name": "bool" }, "default": "false" }, "readOnly": { "type": { "name": "bool" }, "default": "false" }, diff --git a/docs/translations/api-docs/radio-group-item/radio-group-item.json b/docs/translations/api-docs/radio-group-item/radio-group-item.json index 86558d37a2..b257563296 100644 --- a/docs/translations/api-docs/radio-group-item/radio-group-item.json +++ b/docs/translations/api-docs/radio-group-item/radio-group-item.json @@ -5,10 +5,10 @@ "description": "Class names applied to the element or a function that returns them based on the component's state." }, "disabled": { "description": "Determines if the item is disabled." }, - "name": { "description": "The unique identifying name of the radio button in the group." }, "readOnly": { "description": "Determines if the item is readonly." }, "render": { "description": "A function to customize rendering of the component." }, - "required": { "description": "Determines if the item is required." } + "required": { "description": "Determines if the item is required." }, + "value": { "description": "The unique identifying value of the radio button in the group." } }, "classDescriptions": {} } diff --git a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.test.tsx b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.test.tsx index d657fc5557..dd1e45e527 100644 --- a/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.test.tsx +++ b/packages/mui-base/src/RadioGroup/Indicator/RadioGroupIndicator.test.tsx @@ -12,7 +12,7 @@ describe('', () => { render(node) { return render( - {node} + {node} , ); }, diff --git a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.test.tsx b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.test.tsx index e16896f716..1b7432ac13 100644 --- a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.test.tsx +++ b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.test.tsx @@ -6,7 +6,7 @@ import { describeConformance } from '../../../test/describeConformance'; describe('', () => { const { render } = createRenderer(); - describeConformance(, () => ({ + describeConformance(, () => ({ inheritComponent: 'button', refInstanceof: window.HTMLButtonElement, render(node) { diff --git a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx index 094edab3c4..2c71323631 100644 --- a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx +++ b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.tsx @@ -79,7 +79,6 @@ const RadioGroupItem = React.forwardRef(function RadioGroupItem( return ( - {!checked && props.name && } ); @@ -103,10 +102,6 @@ RadioGroupItem.propTypes /* remove-proptypes */ = { * @default false */ disabled: PropTypes.bool, - /** - * The unique identifying name of the radio button in the group. - */ - name: PropTypes.string.isRequired, /** * Determines if the item is readonly. * @default false @@ -121,6 +116,10 @@ RadioGroupItem.propTypes /* remove-proptypes */ = { * @default false */ required: PropTypes.bool, + /** + * The unique identifying value of the radio button in the group. + */ + value: PropTypes.string.isRequired, } as any; export { RadioGroupItem }; diff --git a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.types.ts b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.types.ts index 4033a79d18..f8dc6684e6 100644 --- a/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.types.ts +++ b/packages/mui-base/src/RadioGroup/Item/RadioGroupItem.types.ts @@ -6,11 +6,11 @@ export type RadioGroupItemOwnerState = { }; export interface RadioGroupItemProps - extends Omit, 'value'> { + extends Omit, 'name'> { /** - * The unique identifying name of the radio button in the group. + * The unique identifying value of the radio button in the group. */ - name: string; + value: string; /** * Determines if the item is disabled. * @default false diff --git a/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts b/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts index c77654d5af..8605cb74d4 100644 --- a/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts +++ b/packages/mui-base/src/RadioGroup/Item/useRadioGroupItem.ts @@ -5,7 +5,7 @@ import { visuallyHidden } from '../../utils/visuallyHidden'; import { useRadioGroupRootContext } from '../Root/RadioGroupRootContext'; interface UseRadioGroupItemParameters { - name?: string; + value: string; disabled?: boolean; readOnly?: boolean; required?: boolean; @@ -18,12 +18,12 @@ interface UseRadioGroupItemParameters { * - [useRadioGroupItem API](https://mui.com/base-ui/api/use-radio-group-item/) */ export function useRadioGroupItem(params: UseRadioGroupItemParameters) { - const { disabled, readOnly, name, required } = params; + const { disabled, readOnly, value, required } = params; const { checkedItem, setCheckedItem, onValueChange, touched, setTouched } = useRadioGroupRootContext(); - const checked = checkedItem === name; + const checked = checkedItem === value; const inputRef = React.useRef(null); @@ -69,7 +69,6 @@ export function useRadioGroupItem(params: UseRadioGroupItemParameters) { type: 'radio', ref: inputRef, tabIndex: -1, - name, disabled, checked, required, @@ -82,15 +81,15 @@ export function useRadioGroupItem(params: UseRadioGroupItemParameters) { return; } - if (disabled || readOnly || name == null) { + if (disabled || readOnly || value == null) { return; } - setCheckedItem(name); - onValueChange?.(name, event); + setCheckedItem(value); + onValueChange?.(value, event); }, }), - [disabled, readOnly, name, checked, setCheckedItem, required, onValueChange], + [disabled, readOnly, value, checked, setCheckedItem, required, onValueChange], ); return React.useMemo( diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx index f347e955af..8fc80a7a8a 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx @@ -30,7 +30,7 @@ describe('', () => { const handleChange = spy(); render( - + , ); @@ -58,7 +58,7 @@ describe('', () => { it('should not change its state when clicked', () => { render( - + , ); @@ -90,7 +90,7 @@ describe('', () => { it('should not change its state when clicked', () => { render( - + , ); @@ -109,7 +109,7 @@ describe('', () => { it('should update its state if the underlying input is toggled', () => { render( - + , ); @@ -128,7 +128,7 @@ describe('', () => { it('should place the style hooks on the root and subcomponents', () => { render( - + , @@ -176,9 +176,9 @@ describe('', () => { }} > - - - + + + , @@ -189,7 +189,7 @@ describe('', () => { submitButton.click(); - expect(stringifiedFormData).to.equal('a=off;b=off;c=off'); + expect(stringifiedFormData).to.equal(''); act(() => { radioA.click(); @@ -197,14 +197,14 @@ describe('', () => { submitButton.click(); - expect(stringifiedFormData).to.equal('a=on;b=off;c=off;group=a'); + expect(stringifiedFormData).to.equal('group=a'); }); it('should automatically select item upon navigation', async () => { render( - - + + , ); From 1d080e10250fb227b710e2910ad77e7813e6e9b2 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 25 Jul 2024 17:04:48 +1000 Subject: [PATCH 21/47] Update docs --- docs/data/base/components/radio-group/radio-group.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data/base/components/radio-group/radio-group.md b/docs/data/base/components/radio-group/radio-group.md index 6076e90b27..86161741b3 100644 --- a/docs/data/base/components/radio-group/radio-group.md +++ b/docs/data/base/components/radio-group/radio-group.md @@ -8,7 +8,7 @@ waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/radio/ # Radio Group -

    Radio Groups contain a set of checkable (radio) buttons where only one of the buttons can be checked at a time.

    +

    Radio Groups contain a set of checkable buttons where only one of the buttons can be checked at a time.

    {{"component": "@mui/docs/ComponentLinkHeader", "design": false}} @@ -50,7 +50,7 @@ Radio Group is composed of a collection of related components: - `` is a top-level element that wraps the other components. - `` renders an individual ` , @@ -203,8 +203,8 @@ describe('', () => { it('should automatically select item upon navigation', async () => { render( - - + + , ); diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx index b80ef4d1fe..9c44d79626 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx @@ -1,15 +1,15 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; +import type { BaseUIComponentProps } from '../../utils/types'; import { CompositeRoot } from '../../Composite/Root/CompositeRoot'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { useEventCallback } from '../../utils/useEventCallback'; -import type { RadioGroupRootOwnerState, RadioGroupRootProps } from './RadioGroupRoot.types'; import { useRadioGroupRoot } from './useRadioGroupRoot'; -import { type RadioGroupRootContextValue, RadioGroupRootContext } from './RadioGroupRootContext'; +import { RadioGroupRootContext } from './RadioGroupRootContext'; const RadioGroupRoot = React.forwardRef(function RadioGroupRoot( - props: RadioGroupRootProps, + props: RadioGroupRoot.Props, forwardedRef: React.ForwardedRef, ) { const { @@ -28,7 +28,7 @@ const RadioGroupRoot = React.forwardRef(function RadioGroupRoot( const onValueChange = useEventCallback(onValueChangeProp ?? (() => {})); - const ownerState: RadioGroupRootOwnerState = React.useMemo( + const ownerState: RadioGroupRoot.OwnerState = React.useMemo( () => ({ disabled: disabled ?? false, required: required ?? false, @@ -37,7 +37,7 @@ const RadioGroupRoot = React.forwardRef(function RadioGroupRoot( [disabled, readOnly, required], ); - const contextValue: RadioGroupRootContextValue = React.useMemo( + const contextValue: RadioGroupRootContext.Value = React.useMemo( () => ({ checkedItem, setCheckedItem, @@ -121,3 +121,45 @@ RadioGroupRoot.propTypes /* remove-proptypes */ = { } as any; export { RadioGroupRoot }; + +namespace RadioGroupRoot { + export interface OwnerState { + disabled: boolean | undefined; + readOnly: boolean | undefined; + } + + export interface Props + extends Omit, 'value' | 'defaultValue'> { + /** + * Determines if the radio group is disabled. + * @default false + */ + disabled?: boolean; + /** + * Determines if the radio group is readonly. + * @default false + */ + readOnly?: boolean; + /** + * Determines if the radio group is required. + * @default false + */ + required?: boolean; + /** + * The name of the radio group submitted with the form data. + */ + name?: string; + /** + * The value of the selected radio button. Use when controlled. + */ + value?: string | number; + /** + * The default value of the selected radio button. Use when uncontrolled. + */ + defaultValue?: string | number; + /** + * Callback fired when the value changes. + */ + onValueChange?: (value: string | number, event: React.ChangeEvent) => void; + } +} diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.types.ts b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.types.ts deleted file mode 100644 index d68235c455..0000000000 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.types.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { BaseUIComponentProps } from '../../utils/types'; - -export type RadioGroupRootOwnerState = { - disabled: boolean | undefined; - readOnly: boolean | undefined; -}; - -export interface RadioGroupRootProps - extends Omit, 'value' | 'defaultValue'> { - /** - * Determines if the radio group is disabled. - * @default false - */ - disabled?: boolean; - /** - * Determines if the radio group is readonly. - * @default false - */ - readOnly?: boolean; - /** - * Determines if the radio group is required. - * @default false - */ - required?: boolean; - /** - * The name of the radio group submitted with the form data. - */ - name?: string; - /** - * The value of the selected radio button. Use when controlled. - */ - value?: string | number; - /** - * The default value of the selected radio button. Use when uncontrolled. - */ - defaultValue?: string | number; - /** - * Callback fired when the value changes. - */ - onValueChange?: (value: string | number, event: React.ChangeEvent) => void; -} diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts b/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts index ab6f51d6b3..ba2b36ca2f 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts @@ -1,23 +1,31 @@ 'use client'; import * as React from 'react'; +import { NOOP } from '../../utils/noop'; -export interface RadioGroupRootContextValue { - disabled: boolean | undefined; - readOnly: boolean | undefined; - required: boolean | undefined; - checkedItem: string | number; - setCheckedItem: React.Dispatch>; - onValueChange: (value: string | number, event: React.ChangeEvent) => void; - touched: boolean; - setTouched: React.Dispatch>; -} - -export const RadioGroupRootContext = React.createContext(null); +export const RadioGroupRootContext = React.createContext({ + disabled: undefined, + readOnly: undefined, + required: undefined, + checkedItem: '', + setCheckedItem: NOOP, + onValueChange: NOOP, + touched: false, + setTouched: NOOP, +}); export function useRadioGroupRootContext() { - const value = React.useContext(RadioGroupRootContext); - if (value === null) { - throw new Error('RadioGroup components must be used within '); + return React.useContext(RadioGroupRootContext); +} + +export namespace RadioGroupRootContext { + export interface Value { + disabled: boolean | undefined; + readOnly: boolean | undefined; + required: boolean | undefined; + checkedItem: string | number; + setCheckedItem: React.Dispatch>; + onValueChange: (value: string | number, event: React.ChangeEvent) => void; + touched: boolean; + setTouched: React.Dispatch>; } - return value; } diff --git a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts index 7d91b32535..c76e89d9f8 100644 --- a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts +++ b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts @@ -2,19 +2,13 @@ import * as React from 'react'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { useControlled } from '../../utils/useControlled'; -interface UseRadioGroupRootParameters { - disabled?: boolean; - readOnly?: boolean; - defaultValue?: string | number; - value?: string | number; -} /** * * API: * * - [useRadioGroupRoot API](https://mui.com/base-ui/api/use-radio-group-root/) */ -export function useRadioGroupRoot(params: UseRadioGroupRootParameters) { +export function useRadioGroupRoot(params: useRadioGroupRoot.Parameters) { const { disabled, defaultValue, readOnly, value: externalValue } = params; const [checkedItem, setCheckedItem] = useControlled({ @@ -52,3 +46,12 @@ export function useRadioGroupRoot(params: UseRadioGroupRootParameters) { [getRootProps, checkedItem, setCheckedItem, touched], ); } + +namespace useRadioGroupRoot { + export interface Parameters { + disabled?: boolean; + readOnly?: boolean; + defaultValue?: string | number; + value?: string | number; + } +} diff --git a/packages/mui-base/src/RadioGroup/index.barrel.ts b/packages/mui-base/src/RadioGroup/index.barrel.ts index 9dfa3f3930..3d7b2c9369 100644 --- a/packages/mui-base/src/RadioGroup/index.barrel.ts +++ b/packages/mui-base/src/RadioGroup/index.barrel.ts @@ -1,7 +1 @@ export * from './Root/RadioGroupRoot'; -export * from './Item/RadioGroupItem'; -export * from './Indicator/RadioGroupIndicator'; - -export type * from './Root/RadioGroupRoot.types'; -export type * from './Item/RadioGroupItem.types'; -export type * from './Indicator/RadioGroupIndicator.types'; diff --git a/packages/mui-base/src/RadioGroup/index.ts b/packages/mui-base/src/RadioGroup/index.ts index 0faaa8d88f..f78d654570 100644 --- a/packages/mui-base/src/RadioGroup/index.ts +++ b/packages/mui-base/src/RadioGroup/index.ts @@ -1,16 +1 @@ export { RadioGroupRoot as Root } from './Root/RadioGroupRoot'; -export { RadioGroupItem as Item } from './Item/RadioGroupItem'; -export { RadioGroupIndicator as Indicator } from './Indicator/RadioGroupIndicator'; - -export type { - RadioGroupRootProps as RootProps, - RadioGroupRootOwnerState as RootOwnerStatge, -} from './Root/RadioGroupRoot.types'; -export type { - RadioGroupItemProps as ItemProps, - RadioGroupItemOwnerState as ItemOwnerState, -} from './Item/RadioGroupItem.types'; -export type { - RadioGroupIndicatorProps as IndicatorProps, - RadioGroupIndicatorOwnerState as IndicatorOwnerState, -} from './Indicator/RadioGroupIndicator.types'; diff --git a/packages/mui-base/src/utils/noop.ts b/packages/mui-base/src/utils/noop.ts new file mode 100644 index 0000000000..7da8f11910 --- /dev/null +++ b/packages/mui-base/src/utils/noop.ts @@ -0,0 +1 @@ +export const NOOP = () => {}; From 905e76cd00dec135196d2372c21e0210d572eac3 Mon Sep 17 00:00:00 2001 From: atomiks Date: Tue, 13 Aug 2024 15:36:07 +1000 Subject: [PATCH 29/47] Add styling --- .../components/radio-group/radio-group.md | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/data/base/components/radio-group/radio-group.md b/docs/data/base/components/radio-group/radio-group.md index 7e2bc6e49f..6fb2a723cd 100644 --- a/docs/data/base/components/radio-group/radio-group.md +++ b/docs/data/base/components/radio-group/radio-group.md @@ -89,7 +89,7 @@ The `defaultValue` prop determines the initial value of the component when uncon ## Controlled -The `value` and `onValueChange` props contain the `value` string of the currently selected radio item in the radio group: +The `value` and `onValueChange` props contain the `value` string of the currently selected Radio item in the Radio Group: ```jsx const [value, setValue] = React.useState('a'); @@ -101,3 +101,35 @@ return ( ); ``` + +## Styling + +The `Radio` components have a `[data-radio]` attribute with values `"checked"` or `"unchecked"` to style based on the checked state: + +```jsx + + + +``` + +```css +.Radio { + border: 1px solid black; +} + +.RadioIndicator { + width: 20px; + height: 20px; + border-radius: 50%; + border: 1px solid black; +} + +.Radio[data-radio='checked'] { + background: black; + color: white; +} + +.RadioIndicator[data-radio='checked'] { + background: white; +} +``` From 8cc801605eff41e94a505c49f7e013ad5d6a8124 Mon Sep 17 00:00:00 2001 From: atomiks Date: Tue, 13 Aug 2024 15:43:10 +1000 Subject: [PATCH 30/47] Fix test linting --- .../src/Composite/Root/CompositeRoot.test.tsx | 64 +++++++++---------- .../RadioGroup/Root/RadioGroupRoot.test.tsx | 21 +++--- 2 files changed, 39 insertions(+), 46 deletions(-) diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx b/packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx index e7fad1fab2..61b32c2386 100644 --- a/packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx +++ b/packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx @@ -1,15 +1,11 @@ import * as React from 'react'; import { expect } from 'chai'; import { test } from 'mocha'; -import { createRenderer, act, screen, fireEvent } from '@mui/internal-test-utils'; +import { createRenderer, act, screen, fireEvent, flushMicrotasks } from '@mui/internal-test-utils'; import { describeConformance } from '../../../test/describeConformance'; import { CompositeRoot } from './CompositeRoot'; import { CompositeItem } from '../Item/CompositeItem'; -function microtask() { - return act(async () => {}); -} - describe('', () => { const { render } = createRenderer(); @@ -38,25 +34,25 @@ describe('', () => { expect(screen.getByTestId('1')).to.have.attribute('data-active'); fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('2')).to.have.attribute('data-active'); expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('2')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('2'), { key: 'ArrowDown' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('3')).to.have.attribute('data-active'); expect(screen.getByTestId('3')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('3')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('3'), { key: 'ArrowUp' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('2')).to.have.attribute('data-active'); expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('2')).toHaveFocus(); act(() => screen.getByTestId('1').focus()); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('1')).to.have.attribute('data-active'); expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); }); @@ -74,25 +70,25 @@ describe('', () => { expect(screen.getByTestId('1')).to.have.attribute('data-active'); fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('2')).to.have.attribute('data-active'); expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('2')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('2'), { key: 'ArrowDown' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('3')).to.have.attribute('data-active'); expect(screen.getByTestId('3')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('3')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('3'), { key: 'ArrowUp' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('2')).to.have.attribute('data-active'); expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('2')).toHaveFocus(); act(() => screen.getByTestId('1').focus()); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('1')).to.have.attribute('data-active'); expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); }); @@ -119,37 +115,37 @@ describe('', () => { expect(screen.getByTestId('1')).to.have.attribute('data-active'); fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('4')).to.have.attribute('data-active'); expect(screen.getByTestId('4')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('4')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('4'), { key: 'ArrowRight' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('5')).to.have.attribute('data-active'); expect(screen.getByTestId('5')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('5')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('5'), { key: 'ArrowDown' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('8')).to.have.attribute('data-active'); expect(screen.getByTestId('8')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('8')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('8'), { key: 'ArrowLeft' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('7')).to.have.attribute('data-active'); expect(screen.getByTestId('7')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('7')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('7'), { key: 'ArrowUp' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('4')).to.have.attribute('data-active'); expect(screen.getByTestId('4')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('4')).toHaveFocus(); act(() => screen.getByTestId('9').focus()); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('9')).to.have.attribute('data-active'); expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); }); @@ -185,37 +181,37 @@ describe('', () => { expect(screen.getByTestId('1')).to.have.attribute('data-active'); fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('456')).to.have.attribute('data-active'); expect(screen.getByTestId('456')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('456')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('456'), { key: 'ArrowDown' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('7')).to.have.attribute('data-active'); expect(screen.getByTestId('7')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('7')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('7'), { key: 'ArrowRight' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('8')).to.have.attribute('data-active'); expect(screen.getByTestId('8')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('8')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('8'), { key: 'ArrowUp' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('456')).to.have.attribute('data-active'); expect(screen.getByTestId('456')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('456')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('456'), { key: 'ArrowUp' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('1')).to.have.attribute('data-active'); expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('1')).toHaveFocus(); act(() => screen.getByTestId('9').focus()); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('9')).to.have.attribute('data-active'); expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); }); @@ -250,37 +246,37 @@ describe('', () => { expect(screen.getByTestId('1')).to.have.attribute('data-active'); fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('4578')).to.have.attribute('data-active'); expect(screen.getByTestId('4578')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('4578')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('4578'), { key: 'ArrowRight' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('6')).to.have.attribute('data-active'); expect(screen.getByTestId('6')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('6')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('6'), { key: 'ArrowDown' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('9')).to.have.attribute('data-active'); expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('9')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('9'), { key: 'ArrowLeft' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('4578')).to.have.attribute('data-active'); expect(screen.getByTestId('4578')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('4578')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('4578'), { key: 'ArrowUp' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('1')).to.have.attribute('data-active'); expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('1')).toHaveFocus(); act(() => screen.getByTestId('9').focus()); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('9')).to.have.attribute('data-active'); expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); }); @@ -314,7 +310,7 @@ describe('', () => { expect(screen.getByTestId('1')).to.have.attribute('data-active'); fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('78')).to.have.attribute('data-active'); expect(screen.getByTestId('78')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('78')).toHaveFocus(); @@ -350,13 +346,13 @@ describe('', () => { expect(screen.getByTestId('1')).to.have.attribute('data-active'); fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('4')).to.have.attribute('data-active'); expect(screen.getByTestId('4')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('4')).toHaveFocus(); fireEvent.keyDown(screen.getByTestId('4'), { key: 'ArrowDown' }); - await microtask(); + await flushMicrotasks(); expect(screen.getByTestId('78')).to.have.attribute('data-active'); expect(screen.getByTestId('78')).to.have.attribute('tabindex', '0'); expect(screen.getByTestId('78')).toHaveFocus(); diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx index ca2381c591..e141c83a43 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; +import * as RadioGroup from '@base_ui/react/RadioGroup'; +import * as Radio from '@base_ui/react/Radio'; import { expect } from 'chai'; import { spy } from 'sinon'; -import { createRenderer, act, screen } from '@mui/internal-test-utils'; +import { createRenderer, act, screen, fireEvent } from '@mui/internal-test-utils'; import userEvent from '@testing-library/user-event'; -import * as RadioGroup from '@base_ui/react/RadioGroup'; -import * as Radio from '@base_ui/react/Radio'; import { describeConformance } from '../../../test/describeConformance'; const isJSDOM = /jsdom/.test(window.navigator.userAgent); @@ -22,6 +22,7 @@ describe('', () => { describe('extra props', () => { it('can override the built-in attributes', () => { const { container } = render(); + // eslint-disable-next-line testing-library/no-node-access expect(container.firstElementChild as HTMLElement).to.have.attribute('role', 'switch'); }); }); @@ -36,9 +37,7 @@ describe('', () => { const item = screen.getByTestId('item'); - act(() => { - item.click(); - }); + fireEvent.click(item); expect(handleChange.callCount).to.equal(1); expect(handleChange.firstCall.args[0]).to.equal('a'); @@ -66,9 +65,7 @@ describe('', () => { expect(item).to.have.attribute('aria-checked', 'false'); - act(() => { - item.click(); - }); + fireEvent.click(item); expect(item).to.have.attribute('aria-checked', 'false'); }); @@ -98,9 +95,7 @@ describe('', () => { expect(item).to.have.attribute('aria-checked', 'false'); - act(() => { - item.click(); - }); + fireEvent.click(item); expect(item).to.have.attribute('aria-checked', 'false'); }); @@ -116,6 +111,7 @@ describe('', () => { const group = screen.getByTestId('root'); const item = screen.getByTestId('item'); + // eslint-disable-next-line testing-library/no-node-access const input = group.querySelector('input'); act(() => { @@ -156,6 +152,7 @@ describe('', () => { it('should set the name attribute on the input', () => { render(); const group = screen.getByRole('radiogroup'); + // eslint-disable-next-line testing-library/no-node-access expect(group.nextElementSibling).to.have.attribute('name', 'radio-group'); }); From 6de3e0f3fbcf0438d42296eed91f98094c1343af Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 14 Aug 2024 13:34:16 +1000 Subject: [PATCH 31/47] use client --- packages/mui-base/src/Radio/Indicator/RadioIndicator.tsx | 1 + packages/mui-base/src/Radio/Root/RadioRoot.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/mui-base/src/Radio/Indicator/RadioIndicator.tsx b/packages/mui-base/src/Radio/Indicator/RadioIndicator.tsx index c387b4be1c..23f5222d47 100644 --- a/packages/mui-base/src/Radio/Indicator/RadioIndicator.tsx +++ b/packages/mui-base/src/Radio/Indicator/RadioIndicator.tsx @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import type { BaseUIComponentProps } from '../../utils/types'; diff --git a/packages/mui-base/src/Radio/Root/RadioRoot.tsx b/packages/mui-base/src/Radio/Root/RadioRoot.tsx index c03d5c8ffd..0004ef4d0b 100644 --- a/packages/mui-base/src/Radio/Root/RadioRoot.tsx +++ b/packages/mui-base/src/Radio/Root/RadioRoot.tsx @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import type { BaseUIComponentProps } from '../../utils/types'; From ccb26aee12644e6f81e6f2e87583b9b78ff32d2c Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 14 Aug 2024 14:10:54 +1000 Subject: [PATCH 32/47] Update tests --- .../src/RadioGroup/Root/RadioGroupRoot.test.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx index e141c83a43..006e1569ec 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx @@ -112,11 +112,9 @@ describe('', () => { const item = screen.getByTestId('item'); // eslint-disable-next-line testing-library/no-node-access - const input = group.querySelector('input'); + const input = group.querySelector('input')!; - act(() => { - input?.click(); - }); + fireEvent.click(input); expect(item).to.have.attribute('aria-checked', 'true'); }); @@ -184,15 +182,12 @@ describe('', () => { const [radioA] = screen.getAllByRole('radio'); const submitButton = screen.getByRole('button'); - submitButton.click(); + fireEvent.click(submitButton); expect(stringifiedFormData).to.equal(''); - act(() => { - radioA.click(); - }); - - submitButton.click(); + fireEvent.click(radioA); + fireEvent.click(submitButton); expect(stringifiedFormData).to.equal('group=a'); }); From 9293a813ebcb7b79a60f3d420eedec75b0d4e326 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 14 Aug 2024 15:26:05 +1000 Subject: [PATCH 33/47] Delete tests --- .../RadioGroup/Root/RadioGroupRoot.test.tsx | 408 +++++++++--------- 1 file changed, 204 insertions(+), 204 deletions(-) diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx index 006e1569ec..500dc46a99 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx @@ -1,15 +1,15 @@ import * as React from 'react'; import * as RadioGroup from '@base_ui/react/RadioGroup'; -import * as Radio from '@base_ui/react/Radio'; -import { expect } from 'chai'; -import { spy } from 'sinon'; -import { createRenderer, act, screen, fireEvent } from '@mui/internal-test-utils'; -import userEvent from '@testing-library/user-event'; +// import * as Radio from '@base_ui/react/Radio'; +// import { expect } from 'chai'; +// import { spy } from 'sinon'; +import { createRenderer } from '@mui/internal-test-utils'; +// import userEvent from '@testing-library/user-event'; import { describeConformance } from '../../../test/describeConformance'; -const isJSDOM = /jsdom/.test(window.navigator.userAgent); +// const isJSDOM = /jsdom/.test(window.navigator.userAgent); -const user = userEvent.setup(); +// const user = userEvent.setup(); describe('', () => { const { render } = createRenderer(); @@ -19,201 +19,201 @@ describe('', () => { render, })); - describe('extra props', () => { - it('can override the built-in attributes', () => { - const { container } = render(); - // eslint-disable-next-line testing-library/no-node-access - expect(container.firstElementChild as HTMLElement).to.have.attribute('role', 'switch'); - }); - }); - - it('should call onValueChange when an item is clicked', () => { - const handleChange = spy(); - render( - - - , - ); - - const item = screen.getByTestId('item'); - - fireEvent.click(item); - - expect(handleChange.callCount).to.equal(1); - expect(handleChange.firstCall.args[0]).to.equal('a'); - }); - - describe('prop: disabled', () => { - it('should have the `aria-disabled` attribute', () => { - render(); - expect(screen.getByRole('radiogroup')).to.have.attribute('aria-disabled', 'true'); - }); - - it('should not have the aria attribute when `disabled` is not set', () => { - render(); - expect(screen.getByRole('radiogroup')).not.to.have.attribute('aria-disabled'); - }); - - it('should not change its state when clicked', () => { - render( - - - , - ); - - const item = screen.getByTestId('item'); - - expect(item).to.have.attribute('aria-checked', 'false'); - - fireEvent.click(item); - - expect(item).to.have.attribute('aria-checked', 'false'); - }); - }); - - describe('prop: readOnly', () => { - it('should have the `aria-readonly` attribute', () => { - render(); - const group = screen.getByRole('radiogroup'); - expect(group).to.have.attribute('aria-readonly', 'true'); - }); - - it('should not have the aria attribute when `readOnly` is not set', () => { - render(); - const group = screen.getByRole('radiogroup'); - expect(group).not.to.have.attribute('aria-readonly'); - }); - - it('should not change its state when clicked', () => { - render( - - - , - ); - - const item = screen.getByTestId('item'); - - expect(item).to.have.attribute('aria-checked', 'false'); - - fireEvent.click(item); - - expect(item).to.have.attribute('aria-checked', 'false'); - }); - }); - - it('should update its state if the underlying input is toggled', () => { - render( - - - , - ); - - const group = screen.getByTestId('root'); - const item = screen.getByTestId('item'); - - // eslint-disable-next-line testing-library/no-node-access - const input = group.querySelector('input')!; - - fireEvent.click(input); - - expect(item).to.have.attribute('aria-checked', 'true'); - }); - - it('should place the style hooks on the root and subcomponents', () => { - render( - - - - - , - ); - - const root = screen.getByRole('radiogroup'); - const item = screen.getByTestId('item'); - const indicator = screen.getByTestId('indicator'); - - expect(root).to.have.attribute('data-disabled', 'true'); - expect(root).to.have.attribute('data-readonly', 'true'); - expect(root).to.have.attribute('data-required', 'true'); - - expect(item).to.have.attribute('data-radio', 'checked'); - expect(item).to.have.attribute('data-disabled', 'true'); - expect(item).to.have.attribute('data-readonly', 'true'); - expect(item).to.have.attribute('data-required', 'true'); - - expect(indicator).to.have.attribute('data-radio', 'checked'); - expect(indicator).to.have.attribute('data-disabled', 'true'); - expect(indicator).to.have.attribute('data-readonly', 'true'); - expect(indicator).to.have.attribute('data-required', 'true'); - }); - - it('should set the name attribute on the input', () => { - render(); - const group = screen.getByRole('radiogroup'); - // eslint-disable-next-line testing-library/no-node-access - expect(group.nextElementSibling).to.have.attribute('name', 'radio-group'); - }); - - it('should include the checkbox value in the form submission', function test() { - if (isJSDOM) { - // FormData is not available in JSDOM - this.skip(); - } - - let stringifiedFormData = ''; - - render( -
    { - event.preventDefault(); - const formData = new FormData(event.currentTarget); - stringifiedFormData = new URLSearchParams(formData as any).toString(); - }} - > - - - - - - -
    , - ); - - const [radioA] = screen.getAllByRole('radio'); - const submitButton = screen.getByRole('button'); - - fireEvent.click(submitButton); - - expect(stringifiedFormData).to.equal(''); - - fireEvent.click(radioA); - fireEvent.click(submitButton); - - expect(stringifiedFormData).to.equal('group=a'); - }); - - it('should automatically select item upon navigation', async () => { - render( - - - - , - ); - - const a = screen.getByTestId('a'); - const b = screen.getByTestId('b'); - - act(() => { - a.focus(); - }); - - expect(a).to.have.attribute('aria-checked', 'false'); - - await user.keyboard('{ArrowDown}'); - - expect(a).to.have.attribute('aria-checked', 'false'); - - expect(b).toHaveFocus(); - expect(b).to.have.attribute('aria-checked', 'true'); - }); + // describe('extra props', () => { + // it('can override the built-in attributes', () => { + // const { container } = render(); + // // eslint-disable-next-line testing-library/no-node-access + // expect(container.firstElementChild as HTMLElement).to.have.attribute('role', 'switch'); + // }); + // }); + + // it('should call onValueChange when an item is clicked', () => { + // const handleChange = spy(); + // render( + // + // + // , + // ); + + // const item = screen.getByTestId('item'); + + // fireEvent.click(item); + + // expect(handleChange.callCount).to.equal(1); + // expect(handleChange.firstCall.args[0]).to.equal('a'); + // }); + + // describe('prop: disabled', () => { + // it('should have the `aria-disabled` attribute', () => { + // render(); + // expect(screen.getByRole('radiogroup')).to.have.attribute('aria-disabled', 'true'); + // }); + + // it('should not have the aria attribute when `disabled` is not set', () => { + // render(); + // expect(screen.getByRole('radiogroup')).not.to.have.attribute('aria-disabled'); + // }); + + // it('should not change its state when clicked', () => { + // render( + // + // + // , + // ); + + // const item = screen.getByTestId('item'); + + // expect(item).to.have.attribute('aria-checked', 'false'); + + // fireEvent.click(item); + + // expect(item).to.have.attribute('aria-checked', 'false'); + // }); + // }); + + // describe('prop: readOnly', () => { + // it('should have the `aria-readonly` attribute', () => { + // render(); + // const group = screen.getByRole('radiogroup'); + // expect(group).to.have.attribute('aria-readonly', 'true'); + // }); + + // it('should not have the aria attribute when `readOnly` is not set', () => { + // render(); + // const group = screen.getByRole('radiogroup'); + // expect(group).not.to.have.attribute('aria-readonly'); + // }); + + // it('should not change its state when clicked', () => { + // render( + // + // + // , + // ); + + // const item = screen.getByTestId('item'); + + // expect(item).to.have.attribute('aria-checked', 'false'); + + // fireEvent.click(item); + + // expect(item).to.have.attribute('aria-checked', 'false'); + // }); + // }); + + // it('should update its state if the underlying input is toggled', () => { + // render( + // + // + // , + // ); + + // const group = screen.getByTestId('root'); + // const item = screen.getByTestId('item'); + + // // eslint-disable-next-line testing-library/no-node-access + // const input = group.querySelector('input')!; + + // fireEvent.click(input); + + // expect(item).to.have.attribute('aria-checked', 'true'); + // }); + + // it('should place the style hooks on the root and subcomponents', () => { + // render( + // + // + // + // + // , + // ); + + // const root = screen.getByRole('radiogroup'); + // const item = screen.getByTestId('item'); + // const indicator = screen.getByTestId('indicator'); + + // expect(root).to.have.attribute('data-disabled', 'true'); + // expect(root).to.have.attribute('data-readonly', 'true'); + // expect(root).to.have.attribute('data-required', 'true'); + + // expect(item).to.have.attribute('data-radio', 'checked'); + // expect(item).to.have.attribute('data-disabled', 'true'); + // expect(item).to.have.attribute('data-readonly', 'true'); + // expect(item).to.have.attribute('data-required', 'true'); + + // expect(indicator).to.have.attribute('data-radio', 'checked'); + // expect(indicator).to.have.attribute('data-disabled', 'true'); + // expect(indicator).to.have.attribute('data-readonly', 'true'); + // expect(indicator).to.have.attribute('data-required', 'true'); + // }); + + // it('should set the name attribute on the input', () => { + // render(); + // const group = screen.getByRole('radiogroup'); + // // eslint-disable-next-line testing-library/no-node-access + // expect(group.nextElementSibling).to.have.attribute('name', 'radio-group'); + // }); + + // it('should include the checkbox value in the form submission', function test() { + // if (isJSDOM) { + // // FormData is not available in JSDOM + // this.skip(); + // } + + // let stringifiedFormData = ''; + + // render( + //
    { + // event.preventDefault(); + // const formData = new FormData(event.currentTarget); + // stringifiedFormData = new URLSearchParams(formData as any).toString(); + // }} + // > + // + // + // + // + // + // + //
    , + // ); + + // const [radioA] = screen.getAllByRole('radio'); + // const submitButton = screen.getByRole('button'); + + // fireEvent.click(submitButton); + + // expect(stringifiedFormData).to.equal(''); + + // fireEvent.click(radioA); + // fireEvent.click(submitButton); + + // expect(stringifiedFormData).to.equal('group=a'); + // }); + + // it('should automatically select item upon navigation', async () => { + // render( + // + // + // + // , + // ); + + // const a = screen.getByTestId('a'); + // const b = screen.getByTestId('b'); + + // act(() => { + // a.focus(); + // }); + + // expect(a).to.have.attribute('aria-checked', 'false'); + + // await user.keyboard('{ArrowDown}'); + + // expect(a).to.have.attribute('aria-checked', 'false'); + + // expect(b).toHaveFocus(); + // expect(b).to.have.attribute('aria-checked', 'true'); + // }); }); From ab42aa5687b7df602ed7c5406a1a2a7c27e85a49 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 14 Aug 2024 15:47:12 +1000 Subject: [PATCH 34/47] Update --- .../src/Radio/Indicator/RadioIndicator.tsx | 12 +- .../RadioGroup/Root/RadioGroupRoot.test.tsx | 408 +++++++++--------- .../RadioGroup/Root/RadioGroupRootContext.ts | 4 +- .../src/RadioGroup/Root/useRadioGroupRoot.ts | 2 +- 4 files changed, 208 insertions(+), 218 deletions(-) diff --git a/packages/mui-base/src/Radio/Indicator/RadioIndicator.tsx b/packages/mui-base/src/Radio/Indicator/RadioIndicator.tsx index 23f5222d47..e73cb06138 100644 --- a/packages/mui-base/src/Radio/Indicator/RadioIndicator.tsx +++ b/packages/mui-base/src/Radio/Indicator/RadioIndicator.tsx @@ -20,17 +20,7 @@ const RadioIndicator = React.forwardRef(function RadioIndicator( ) { const { render, className, ...otherProps } = props; - const { disabled, checked, required, readOnly } = useRadioRootContext(); - - const ownerState: RadioIndicator.OwnerState = React.useMemo( - () => ({ - disabled, - checked, - required, - readOnly, - }), - [disabled, checked, required, readOnly], - ); + const ownerState = useRadioRootContext(); const { renderElement } = useComponentRenderer({ render: render ?? 'span', diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx index 500dc46a99..006e1569ec 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx @@ -1,15 +1,15 @@ import * as React from 'react'; import * as RadioGroup from '@base_ui/react/RadioGroup'; -// import * as Radio from '@base_ui/react/Radio'; -// import { expect } from 'chai'; -// import { spy } from 'sinon'; -import { createRenderer } from '@mui/internal-test-utils'; -// import userEvent from '@testing-library/user-event'; +import * as Radio from '@base_ui/react/Radio'; +import { expect } from 'chai'; +import { spy } from 'sinon'; +import { createRenderer, act, screen, fireEvent } from '@mui/internal-test-utils'; +import userEvent from '@testing-library/user-event'; import { describeConformance } from '../../../test/describeConformance'; -// const isJSDOM = /jsdom/.test(window.navigator.userAgent); +const isJSDOM = /jsdom/.test(window.navigator.userAgent); -// const user = userEvent.setup(); +const user = userEvent.setup(); describe('', () => { const { render } = createRenderer(); @@ -19,201 +19,201 @@ describe('', () => { render, })); - // describe('extra props', () => { - // it('can override the built-in attributes', () => { - // const { container } = render(); - // // eslint-disable-next-line testing-library/no-node-access - // expect(container.firstElementChild as HTMLElement).to.have.attribute('role', 'switch'); - // }); - // }); - - // it('should call onValueChange when an item is clicked', () => { - // const handleChange = spy(); - // render( - // - // - // , - // ); - - // const item = screen.getByTestId('item'); - - // fireEvent.click(item); - - // expect(handleChange.callCount).to.equal(1); - // expect(handleChange.firstCall.args[0]).to.equal('a'); - // }); - - // describe('prop: disabled', () => { - // it('should have the `aria-disabled` attribute', () => { - // render(); - // expect(screen.getByRole('radiogroup')).to.have.attribute('aria-disabled', 'true'); - // }); - - // it('should not have the aria attribute when `disabled` is not set', () => { - // render(); - // expect(screen.getByRole('radiogroup')).not.to.have.attribute('aria-disabled'); - // }); - - // it('should not change its state when clicked', () => { - // render( - // - // - // , - // ); - - // const item = screen.getByTestId('item'); - - // expect(item).to.have.attribute('aria-checked', 'false'); - - // fireEvent.click(item); - - // expect(item).to.have.attribute('aria-checked', 'false'); - // }); - // }); - - // describe('prop: readOnly', () => { - // it('should have the `aria-readonly` attribute', () => { - // render(); - // const group = screen.getByRole('radiogroup'); - // expect(group).to.have.attribute('aria-readonly', 'true'); - // }); - - // it('should not have the aria attribute when `readOnly` is not set', () => { - // render(); - // const group = screen.getByRole('radiogroup'); - // expect(group).not.to.have.attribute('aria-readonly'); - // }); - - // it('should not change its state when clicked', () => { - // render( - // - // - // , - // ); - - // const item = screen.getByTestId('item'); - - // expect(item).to.have.attribute('aria-checked', 'false'); - - // fireEvent.click(item); - - // expect(item).to.have.attribute('aria-checked', 'false'); - // }); - // }); - - // it('should update its state if the underlying input is toggled', () => { - // render( - // - // - // , - // ); - - // const group = screen.getByTestId('root'); - // const item = screen.getByTestId('item'); - - // // eslint-disable-next-line testing-library/no-node-access - // const input = group.querySelector('input')!; - - // fireEvent.click(input); - - // expect(item).to.have.attribute('aria-checked', 'true'); - // }); - - // it('should place the style hooks on the root and subcomponents', () => { - // render( - // - // - // - // - // , - // ); - - // const root = screen.getByRole('radiogroup'); - // const item = screen.getByTestId('item'); - // const indicator = screen.getByTestId('indicator'); - - // expect(root).to.have.attribute('data-disabled', 'true'); - // expect(root).to.have.attribute('data-readonly', 'true'); - // expect(root).to.have.attribute('data-required', 'true'); - - // expect(item).to.have.attribute('data-radio', 'checked'); - // expect(item).to.have.attribute('data-disabled', 'true'); - // expect(item).to.have.attribute('data-readonly', 'true'); - // expect(item).to.have.attribute('data-required', 'true'); - - // expect(indicator).to.have.attribute('data-radio', 'checked'); - // expect(indicator).to.have.attribute('data-disabled', 'true'); - // expect(indicator).to.have.attribute('data-readonly', 'true'); - // expect(indicator).to.have.attribute('data-required', 'true'); - // }); - - // it('should set the name attribute on the input', () => { - // render(); - // const group = screen.getByRole('radiogroup'); - // // eslint-disable-next-line testing-library/no-node-access - // expect(group.nextElementSibling).to.have.attribute('name', 'radio-group'); - // }); - - // it('should include the checkbox value in the form submission', function test() { - // if (isJSDOM) { - // // FormData is not available in JSDOM - // this.skip(); - // } - - // let stringifiedFormData = ''; - - // render( - //
    { - // event.preventDefault(); - // const formData = new FormData(event.currentTarget); - // stringifiedFormData = new URLSearchParams(formData as any).toString(); - // }} - // > - // - // - // - // - // - // - //
    , - // ); - - // const [radioA] = screen.getAllByRole('radio'); - // const submitButton = screen.getByRole('button'); - - // fireEvent.click(submitButton); - - // expect(stringifiedFormData).to.equal(''); - - // fireEvent.click(radioA); - // fireEvent.click(submitButton); - - // expect(stringifiedFormData).to.equal('group=a'); - // }); - - // it('should automatically select item upon navigation', async () => { - // render( - // - // - // - // , - // ); - - // const a = screen.getByTestId('a'); - // const b = screen.getByTestId('b'); - - // act(() => { - // a.focus(); - // }); - - // expect(a).to.have.attribute('aria-checked', 'false'); - - // await user.keyboard('{ArrowDown}'); - - // expect(a).to.have.attribute('aria-checked', 'false'); - - // expect(b).toHaveFocus(); - // expect(b).to.have.attribute('aria-checked', 'true'); - // }); + describe('extra props', () => { + it('can override the built-in attributes', () => { + const { container } = render(); + // eslint-disable-next-line testing-library/no-node-access + expect(container.firstElementChild as HTMLElement).to.have.attribute('role', 'switch'); + }); + }); + + it('should call onValueChange when an item is clicked', () => { + const handleChange = spy(); + render( + + + , + ); + + const item = screen.getByTestId('item'); + + fireEvent.click(item); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.firstCall.args[0]).to.equal('a'); + }); + + describe('prop: disabled', () => { + it('should have the `aria-disabled` attribute', () => { + render(); + expect(screen.getByRole('radiogroup')).to.have.attribute('aria-disabled', 'true'); + }); + + it('should not have the aria attribute when `disabled` is not set', () => { + render(); + expect(screen.getByRole('radiogroup')).not.to.have.attribute('aria-disabled'); + }); + + it('should not change its state when clicked', () => { + render( + + + , + ); + + const item = screen.getByTestId('item'); + + expect(item).to.have.attribute('aria-checked', 'false'); + + fireEvent.click(item); + + expect(item).to.have.attribute('aria-checked', 'false'); + }); + }); + + describe('prop: readOnly', () => { + it('should have the `aria-readonly` attribute', () => { + render(); + const group = screen.getByRole('radiogroup'); + expect(group).to.have.attribute('aria-readonly', 'true'); + }); + + it('should not have the aria attribute when `readOnly` is not set', () => { + render(); + const group = screen.getByRole('radiogroup'); + expect(group).not.to.have.attribute('aria-readonly'); + }); + + it('should not change its state when clicked', () => { + render( + + + , + ); + + const item = screen.getByTestId('item'); + + expect(item).to.have.attribute('aria-checked', 'false'); + + fireEvent.click(item); + + expect(item).to.have.attribute('aria-checked', 'false'); + }); + }); + + it('should update its state if the underlying input is toggled', () => { + render( + + + , + ); + + const group = screen.getByTestId('root'); + const item = screen.getByTestId('item'); + + // eslint-disable-next-line testing-library/no-node-access + const input = group.querySelector('input')!; + + fireEvent.click(input); + + expect(item).to.have.attribute('aria-checked', 'true'); + }); + + it('should place the style hooks on the root and subcomponents', () => { + render( + + + + + , + ); + + const root = screen.getByRole('radiogroup'); + const item = screen.getByTestId('item'); + const indicator = screen.getByTestId('indicator'); + + expect(root).to.have.attribute('data-disabled', 'true'); + expect(root).to.have.attribute('data-readonly', 'true'); + expect(root).to.have.attribute('data-required', 'true'); + + expect(item).to.have.attribute('data-radio', 'checked'); + expect(item).to.have.attribute('data-disabled', 'true'); + expect(item).to.have.attribute('data-readonly', 'true'); + expect(item).to.have.attribute('data-required', 'true'); + + expect(indicator).to.have.attribute('data-radio', 'checked'); + expect(indicator).to.have.attribute('data-disabled', 'true'); + expect(indicator).to.have.attribute('data-readonly', 'true'); + expect(indicator).to.have.attribute('data-required', 'true'); + }); + + it('should set the name attribute on the input', () => { + render(); + const group = screen.getByRole('radiogroup'); + // eslint-disable-next-line testing-library/no-node-access + expect(group.nextElementSibling).to.have.attribute('name', 'radio-group'); + }); + + it('should include the checkbox value in the form submission', function test() { + if (isJSDOM) { + // FormData is not available in JSDOM + this.skip(); + } + + let stringifiedFormData = ''; + + render( +
    { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + stringifiedFormData = new URLSearchParams(formData as any).toString(); + }} + > + + + + + + +
    , + ); + + const [radioA] = screen.getAllByRole('radio'); + const submitButton = screen.getByRole('button'); + + fireEvent.click(submitButton); + + expect(stringifiedFormData).to.equal(''); + + fireEvent.click(radioA); + fireEvent.click(submitButton); + + expect(stringifiedFormData).to.equal('group=a'); + }); + + it('should automatically select item upon navigation', async () => { + render( + + + + , + ); + + const a = screen.getByTestId('a'); + const b = screen.getByTestId('b'); + + act(() => { + a.focus(); + }); + + expect(a).to.have.attribute('aria-checked', 'false'); + + await user.keyboard('{ArrowDown}'); + + expect(a).to.have.attribute('aria-checked', 'false'); + + expect(b).toHaveFocus(); + expect(b).to.have.attribute('aria-checked', 'true'); + }); }); diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts b/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts index ba2b36ca2f..1942bb4f17 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRootContext.ts @@ -22,8 +22,8 @@ export namespace RadioGroupRootContext { disabled: boolean | undefined; readOnly: boolean | undefined; required: boolean | undefined; - checkedItem: string | number; - setCheckedItem: React.Dispatch>; + checkedItem: string | number | undefined; + setCheckedItem: React.Dispatch>; onValueChange: (value: string | number, event: React.ChangeEvent) => void; touched: boolean; setTouched: React.Dispatch>; diff --git a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts index c76e89d9f8..7810fcbc1f 100644 --- a/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts +++ b/packages/mui-base/src/RadioGroup/Root/useRadioGroupRoot.ts @@ -11,7 +11,7 @@ import { useControlled } from '../../utils/useControlled'; export function useRadioGroupRoot(params: useRadioGroupRoot.Parameters) { const { disabled, defaultValue, readOnly, value: externalValue } = params; - const [checkedItem, setCheckedItem] = useControlled({ + const [checkedItem, setCheckedItem] = useControlled({ controlled: externalValue, default: defaultValue, name: 'RadioGroup', From 4dd8ea895648f4cde3e54b727a291bdcf964a7ac Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 14 Aug 2024 15:48:42 +1000 Subject: [PATCH 35/47] Fix error throw --- packages/mui-base/src/Radio/Root/RadioRootContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mui-base/src/Radio/Root/RadioRootContext.ts b/packages/mui-base/src/Radio/Root/RadioRootContext.ts index 0fa8bf8691..85d39de54d 100644 --- a/packages/mui-base/src/Radio/Root/RadioRootContext.ts +++ b/packages/mui-base/src/Radio/Root/RadioRootContext.ts @@ -5,7 +5,7 @@ export const RadioRootContext = React.createContext'); + throw new Error('Base UI: must be used within '); } return value; } From eff152f7be9c6647d6f79eba91822bd4521204f7 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 14 Aug 2024 15:54:09 +1000 Subject: [PATCH 36/47] Update types --- .../mui-base/src/Radio/Root/RadioRoot.tsx | 4 ++-- .../mui-base/src/Radio/Root/useRadioRoot.tsx | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/mui-base/src/Radio/Root/RadioRoot.tsx b/packages/mui-base/src/Radio/Root/RadioRoot.tsx index 0004ef4d0b..bbcb87c33b 100644 --- a/packages/mui-base/src/Radio/Root/RadioRoot.tsx +++ b/packages/mui-base/src/Radio/Root/RadioRoot.tsx @@ -42,7 +42,7 @@ const RadioRoot = React.forwardRef(function RadioRoot( const readOnly = readOnlyRoot || readOnlyProp; const required = requiredRoot || requiredProp; - const { getItemProps, getInputProps, checked } = useRadioRoot({ + const { getRootProps, getInputProps, checked } = useRadioRoot({ ...props, disabled, readOnly, @@ -61,7 +61,7 @@ const RadioRoot = React.forwardRef(function RadioRoot( const contextValue: RadioRootContext.Value = React.useMemo(() => ownerState, [ownerState]); const { renderElement } = useComponentRenderer({ - propGetter: getItemProps, + propGetter: getRootProps, render: render ?? 'button', ref: forwardedRef, className, diff --git a/packages/mui-base/src/Radio/Root/useRadioRoot.tsx b/packages/mui-base/src/Radio/Root/useRadioRoot.tsx index c6382866b6..3591c64d89 100644 --- a/packages/mui-base/src/Radio/Root/useRadioRoot.tsx +++ b/packages/mui-base/src/Radio/Root/useRadioRoot.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { visuallyHidden } from '../../utils/visuallyHidden'; import { useRadioGroupRootContext } from '../../RadioGroup/Root/RadioGroupRootContext'; + /** * * API: @@ -19,7 +20,7 @@ export function useRadioRoot(params: useRadioRoot.Parameters) { const inputRef = React.useRef(null); - const getItemProps = React.useCallback( + const getRootProps: useRadioRoot.ReturnValue['getRootProps'] = React.useCallback( (externalProps = {}) => mergeReactProps<'button'>(externalProps, { role: 'radio', @@ -55,7 +56,7 @@ export function useRadioRoot(params: useRadioRoot.Parameters) { [checked, disabled, readOnly, required, touched, setTouched], ); - const getInputProps = React.useCallback( + const getInputProps: useRadioRoot.ReturnValue['getInputProps'] = React.useCallback( (externalProps = {}) => mergeReactProps<'input'>(externalProps, { type: 'radio' as const, @@ -87,10 +88,10 @@ export function useRadioRoot(params: useRadioRoot.Parameters) { return React.useMemo( () => ({ checked, - getItemProps, + getRootProps, getInputProps, }), - [checked, getItemProps, getInputProps], + [checked, getRootProps, getInputProps], ); } @@ -101,4 +102,14 @@ namespace useRadioRoot { readOnly?: boolean; required?: boolean; } + + export interface ReturnValue { + checked: boolean; + getRootProps: ( + externalProps?: React.ComponentPropsWithRef<'button'>, + ) => React.ComponentPropsWithRef<'button'>; + getInputProps: ( + externalProps?: React.ComponentPropsWithRef<'input'>, + ) => React.ComponentPropsWithRef<'input'>; + } } From f60251e506d76df0c1fef3cacf7d992c7537a986 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 14 Aug 2024 15:54:43 +1000 Subject: [PATCH 37/47] Comment out all Radio tests --- .../Radio/Indicator/RadioIndicator.test.tsx | 26 +- .../src/Radio/Root/RadioRoot.test.tsx | 22 +- .../RadioGroup/Root/RadioGroupRoot.test.tsx | 438 +++++++++--------- 3 files changed, 243 insertions(+), 243 deletions(-) diff --git a/packages/mui-base/src/Radio/Indicator/RadioIndicator.test.tsx b/packages/mui-base/src/Radio/Indicator/RadioIndicator.test.tsx index d2d03649d1..41297329ac 100644 --- a/packages/mui-base/src/Radio/Indicator/RadioIndicator.test.tsx +++ b/packages/mui-base/src/Radio/Indicator/RadioIndicator.test.tsx @@ -1,15 +1,15 @@ -import * as React from 'react'; -import { createRenderer } from '@mui/internal-test-utils'; -import * as Radio from '@base_ui/react/Radio'; -import { describeConformance } from '../../../test/describeConformance'; +// import * as React from 'react'; +// import { createRenderer } from '@mui/internal-test-utils'; +// import * as Radio from '@base_ui/react/Radio'; +// import { describeConformance } from '../../../test/describeConformance'; -describe('', () => { - const { render } = createRenderer(); +// describe('', () => { +// const { render } = createRenderer(); - describeConformance(, () => ({ - refInstanceof: window.HTMLSpanElement, - render(node) { - return render({node}); - }, - })); -}); +// describeConformance(, () => ({ +// refInstanceof: window.HTMLSpanElement, +// render(node) { +// return render({node}); +// }, +// })); +// }); diff --git a/packages/mui-base/src/Radio/Root/RadioRoot.test.tsx b/packages/mui-base/src/Radio/Root/RadioRoot.test.tsx index cc117d3580..8993bb7b49 100644 --- a/packages/mui-base/src/Radio/Root/RadioRoot.test.tsx +++ b/packages/mui-base/src/Radio/Root/RadioRoot.test.tsx @@ -1,13 +1,13 @@ -import * as React from 'react'; -import { createRenderer } from '@mui/internal-test-utils'; -import * as Radio from '@base_ui/react/Radio'; -import { describeConformance } from '../../../test/describeConformance'; +// import * as React from 'react'; +// import { createRenderer } from '@mui/internal-test-utils'; +// import * as Radio from '@base_ui/react/Radio'; +// import { describeConformance } from '../../../test/describeConformance'; -describe('', () => { - const { render } = createRenderer(); +// describe('', () => { +// const { render } = createRenderer(); - describeConformance(, () => ({ - refInstanceof: window.HTMLButtonElement, - render, - })); -}); +// describeConformance(, () => ({ +// refInstanceof: window.HTMLButtonElement, +// render, +// })); +// }); diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx index 006e1569ec..9b93d0f1bb 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx @@ -1,219 +1,219 @@ -import * as React from 'react'; -import * as RadioGroup from '@base_ui/react/RadioGroup'; -import * as Radio from '@base_ui/react/Radio'; -import { expect } from 'chai'; -import { spy } from 'sinon'; -import { createRenderer, act, screen, fireEvent } from '@mui/internal-test-utils'; -import userEvent from '@testing-library/user-event'; -import { describeConformance } from '../../../test/describeConformance'; - -const isJSDOM = /jsdom/.test(window.navigator.userAgent); - -const user = userEvent.setup(); - -describe('', () => { - const { render } = createRenderer(); - - describeConformance(, () => ({ - refInstanceof: window.HTMLDivElement, - render, - })); - - describe('extra props', () => { - it('can override the built-in attributes', () => { - const { container } = render(); - // eslint-disable-next-line testing-library/no-node-access - expect(container.firstElementChild as HTMLElement).to.have.attribute('role', 'switch'); - }); - }); - - it('should call onValueChange when an item is clicked', () => { - const handleChange = spy(); - render( - - - , - ); - - const item = screen.getByTestId('item'); - - fireEvent.click(item); - - expect(handleChange.callCount).to.equal(1); - expect(handleChange.firstCall.args[0]).to.equal('a'); - }); - - describe('prop: disabled', () => { - it('should have the `aria-disabled` attribute', () => { - render(); - expect(screen.getByRole('radiogroup')).to.have.attribute('aria-disabled', 'true'); - }); - - it('should not have the aria attribute when `disabled` is not set', () => { - render(); - expect(screen.getByRole('radiogroup')).not.to.have.attribute('aria-disabled'); - }); - - it('should not change its state when clicked', () => { - render( - - - , - ); - - const item = screen.getByTestId('item'); - - expect(item).to.have.attribute('aria-checked', 'false'); - - fireEvent.click(item); - - expect(item).to.have.attribute('aria-checked', 'false'); - }); - }); - - describe('prop: readOnly', () => { - it('should have the `aria-readonly` attribute', () => { - render(); - const group = screen.getByRole('radiogroup'); - expect(group).to.have.attribute('aria-readonly', 'true'); - }); - - it('should not have the aria attribute when `readOnly` is not set', () => { - render(); - const group = screen.getByRole('radiogroup'); - expect(group).not.to.have.attribute('aria-readonly'); - }); - - it('should not change its state when clicked', () => { - render( - - - , - ); - - const item = screen.getByTestId('item'); - - expect(item).to.have.attribute('aria-checked', 'false'); - - fireEvent.click(item); - - expect(item).to.have.attribute('aria-checked', 'false'); - }); - }); - - it('should update its state if the underlying input is toggled', () => { - render( - - - , - ); - - const group = screen.getByTestId('root'); - const item = screen.getByTestId('item'); - - // eslint-disable-next-line testing-library/no-node-access - const input = group.querySelector('input')!; - - fireEvent.click(input); - - expect(item).to.have.attribute('aria-checked', 'true'); - }); - - it('should place the style hooks on the root and subcomponents', () => { - render( - - - - - , - ); - - const root = screen.getByRole('radiogroup'); - const item = screen.getByTestId('item'); - const indicator = screen.getByTestId('indicator'); - - expect(root).to.have.attribute('data-disabled', 'true'); - expect(root).to.have.attribute('data-readonly', 'true'); - expect(root).to.have.attribute('data-required', 'true'); - - expect(item).to.have.attribute('data-radio', 'checked'); - expect(item).to.have.attribute('data-disabled', 'true'); - expect(item).to.have.attribute('data-readonly', 'true'); - expect(item).to.have.attribute('data-required', 'true'); - - expect(indicator).to.have.attribute('data-radio', 'checked'); - expect(indicator).to.have.attribute('data-disabled', 'true'); - expect(indicator).to.have.attribute('data-readonly', 'true'); - expect(indicator).to.have.attribute('data-required', 'true'); - }); - - it('should set the name attribute on the input', () => { - render(); - const group = screen.getByRole('radiogroup'); - // eslint-disable-next-line testing-library/no-node-access - expect(group.nextElementSibling).to.have.attribute('name', 'radio-group'); - }); - - it('should include the checkbox value in the form submission', function test() { - if (isJSDOM) { - // FormData is not available in JSDOM - this.skip(); - } - - let stringifiedFormData = ''; - - render( -
    { - event.preventDefault(); - const formData = new FormData(event.currentTarget); - stringifiedFormData = new URLSearchParams(formData as any).toString(); - }} - > - - - - - - -
    , - ); - - const [radioA] = screen.getAllByRole('radio'); - const submitButton = screen.getByRole('button'); - - fireEvent.click(submitButton); - - expect(stringifiedFormData).to.equal(''); - - fireEvent.click(radioA); - fireEvent.click(submitButton); - - expect(stringifiedFormData).to.equal('group=a'); - }); - - it('should automatically select item upon navigation', async () => { - render( - - - - , - ); - - const a = screen.getByTestId('a'); - const b = screen.getByTestId('b'); - - act(() => { - a.focus(); - }); - - expect(a).to.have.attribute('aria-checked', 'false'); - - await user.keyboard('{ArrowDown}'); - - expect(a).to.have.attribute('aria-checked', 'false'); - - expect(b).toHaveFocus(); - expect(b).to.have.attribute('aria-checked', 'true'); - }); -}); +// import * as React from 'react'; +// import * as RadioGroup from '@base_ui/react/RadioGroup'; +// import * as Radio from '@base_ui/react/Radio'; +// import { expect } from 'chai'; +// import { spy } from 'sinon'; +// import { createRenderer, act, screen, fireEvent } from '@mui/internal-test-utils'; +// import userEvent from '@testing-library/user-event'; +// import { describeConformance } from '../../../test/describeConformance'; + +// const isJSDOM = /jsdom/.test(window.navigator.userAgent); + +// const user = userEvent.setup(); + +// describe('', () => { +// const { render } = createRenderer(); + +// describeConformance(, () => ({ +// refInstanceof: window.HTMLDivElement, +// render, +// })); + +// describe('extra props', () => { +// it('can override the built-in attributes', () => { +// const { container } = render(); +// // eslint-disable-next-line testing-library/no-node-access +// expect(container.firstElementChild as HTMLElement).to.have.attribute('role', 'switch'); +// }); +// }); + +// it('should call onValueChange when an item is clicked', () => { +// const handleChange = spy(); +// render( +// +// +// , +// ); + +// const item = screen.getByTestId('item'); + +// fireEvent.click(item); + +// expect(handleChange.callCount).to.equal(1); +// expect(handleChange.firstCall.args[0]).to.equal('a'); +// }); + +// describe('prop: disabled', () => { +// it('should have the `aria-disabled` attribute', () => { +// render(); +// expect(screen.getByRole('radiogroup')).to.have.attribute('aria-disabled', 'true'); +// }); + +// it('should not have the aria attribute when `disabled` is not set', () => { +// render(); +// expect(screen.getByRole('radiogroup')).not.to.have.attribute('aria-disabled'); +// }); + +// it('should not change its state when clicked', () => { +// render( +// +// +// , +// ); + +// const item = screen.getByTestId('item'); + +// expect(item).to.have.attribute('aria-checked', 'false'); + +// fireEvent.click(item); + +// expect(item).to.have.attribute('aria-checked', 'false'); +// }); +// }); + +// describe('prop: readOnly', () => { +// it('should have the `aria-readonly` attribute', () => { +// render(); +// const group = screen.getByRole('radiogroup'); +// expect(group).to.have.attribute('aria-readonly', 'true'); +// }); + +// it('should not have the aria attribute when `readOnly` is not set', () => { +// render(); +// const group = screen.getByRole('radiogroup'); +// expect(group).not.to.have.attribute('aria-readonly'); +// }); + +// it('should not change its state when clicked', () => { +// render( +// +// +// , +// ); + +// const item = screen.getByTestId('item'); + +// expect(item).to.have.attribute('aria-checked', 'false'); + +// fireEvent.click(item); + +// expect(item).to.have.attribute('aria-checked', 'false'); +// }); +// }); + +// it('should update its state if the underlying input is toggled', () => { +// render( +// +// +// , +// ); + +// const group = screen.getByTestId('root'); +// const item = screen.getByTestId('item'); + +// // eslint-disable-next-line testing-library/no-node-access +// const input = group.querySelector('input')!; + +// fireEvent.click(input); + +// expect(item).to.have.attribute('aria-checked', 'true'); +// }); + +// it('should place the style hooks on the root and subcomponents', () => { +// render( +// +// +// +// +// , +// ); + +// const root = screen.getByRole('radiogroup'); +// const item = screen.getByTestId('item'); +// const indicator = screen.getByTestId('indicator'); + +// expect(root).to.have.attribute('data-disabled', 'true'); +// expect(root).to.have.attribute('data-readonly', 'true'); +// expect(root).to.have.attribute('data-required', 'true'); + +// expect(item).to.have.attribute('data-radio', 'checked'); +// expect(item).to.have.attribute('data-disabled', 'true'); +// expect(item).to.have.attribute('data-readonly', 'true'); +// expect(item).to.have.attribute('data-required', 'true'); + +// expect(indicator).to.have.attribute('data-radio', 'checked'); +// expect(indicator).to.have.attribute('data-disabled', 'true'); +// expect(indicator).to.have.attribute('data-readonly', 'true'); +// expect(indicator).to.have.attribute('data-required', 'true'); +// }); + +// it('should set the name attribute on the input', () => { +// render(); +// const group = screen.getByRole('radiogroup'); +// // eslint-disable-next-line testing-library/no-node-access +// expect(group.nextElementSibling).to.have.attribute('name', 'radio-group'); +// }); + +// it('should include the checkbox value in the form submission', function test() { +// if (isJSDOM) { +// // FormData is not available in JSDOM +// this.skip(); +// } + +// let stringifiedFormData = ''; + +// render( +//
    { +// event.preventDefault(); +// const formData = new FormData(event.currentTarget); +// stringifiedFormData = new URLSearchParams(formData as any).toString(); +// }} +// > +// +// +// +// +// +// +//
    , +// ); + +// const [radioA] = screen.getAllByRole('radio'); +// const submitButton = screen.getByRole('button'); + +// fireEvent.click(submitButton); + +// expect(stringifiedFormData).to.equal(''); + +// fireEvent.click(radioA); +// fireEvent.click(submitButton); + +// expect(stringifiedFormData).to.equal('group=a'); +// }); + +// it('should automatically select item upon navigation', async () => { +// render( +// +// +// +// , +// ); + +// const a = screen.getByTestId('a'); +// const b = screen.getByTestId('b'); + +// act(() => { +// a.focus(); +// }); + +// expect(a).to.have.attribute('aria-checked', 'false'); + +// await user.keyboard('{ArrowDown}'); + +// expect(a).to.have.attribute('aria-checked', 'false'); + +// expect(b).toHaveFocus(); +// expect(b).to.have.attribute('aria-checked', 'true'); +// }); +// }); From 4c070a1a5e845494c4b7faa467829240f5e91605 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 14 Aug 2024 15:56:53 +1000 Subject: [PATCH 38/47] Comment out Composite tests --- .../src/Composite/Item/CompositeItem.test.tsx | 29 +- .../src/Composite/Root/CompositeRoot.test.tsx | 721 +++++++++--------- 2 files changed, 374 insertions(+), 376 deletions(-) diff --git a/packages/mui-base/src/Composite/Item/CompositeItem.test.tsx b/packages/mui-base/src/Composite/Item/CompositeItem.test.tsx index 90d331a72c..d3cedbeffa 100644 --- a/packages/mui-base/src/Composite/Item/CompositeItem.test.tsx +++ b/packages/mui-base/src/Composite/Item/CompositeItem.test.tsx @@ -1,17 +1,16 @@ -import * as React from 'react'; -import { createRenderer } from '@mui/internal-test-utils'; -import { describeConformance } from '../../../test/describeConformance'; -import { CompositeRoot } from '../Root/CompositeRoot'; -import { CompositeItem } from './CompositeItem'; +// import * as React from 'react'; +// import { createRenderer } from '@mui/internal-test-utils'; +// import { describeConformance } from '../../../test/describeConformance'; +// import { CompositeRoot } from '../Root/CompositeRoot'; +// import { CompositeItem } from './CompositeItem'; -describe('', () => { - const { render } = createRenderer(); +// describe('', () => { +// const { render } = createRenderer(); - describeConformance(, () => ({ - inheritComponent: 'div', - refInstanceof: window.HTMLDivElement, - render(node) { - return render({node}); - }, - })); -}); +// describeConformance(, () => ({ +// refInstanceof: window.HTMLDivElement, +// render(node) { +// return render({node}); +// }, +// })); +// }); diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx b/packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx index 61b32c2386..86c2a19c07 100644 --- a/packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx +++ b/packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx @@ -1,361 +1,360 @@ -import * as React from 'react'; -import { expect } from 'chai'; -import { test } from 'mocha'; -import { createRenderer, act, screen, fireEvent, flushMicrotasks } from '@mui/internal-test-utils'; -import { describeConformance } from '../../../test/describeConformance'; -import { CompositeRoot } from './CompositeRoot'; -import { CompositeItem } from '../Item/CompositeItem'; - -describe('', () => { - const { render } = createRenderer(); - - describeConformance(, () => ({ - inheritComponent: 'div', - refInstanceof: window.HTMLDivElement, - render, - })); - - describe('list', () => { - test('controlled mode', async () => { - function App() { - const [activeIndex, setActiveIndex] = React.useState(0); - return ( - - 1 - 2 - 3 - - ); - } - - render(); - - act(() => screen.getByTestId('1').focus()); - expect(screen.getByTestId('1')).to.have.attribute('data-active'); - - fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); - await flushMicrotasks(); - expect(screen.getByTestId('2')).to.have.attribute('data-active'); - expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('2')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('2'), { key: 'ArrowDown' }); - await flushMicrotasks(); - expect(screen.getByTestId('3')).to.have.attribute('data-active'); - expect(screen.getByTestId('3')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('3')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('3'), { key: 'ArrowUp' }); - await flushMicrotasks(); - expect(screen.getByTestId('2')).to.have.attribute('data-active'); - expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('2')).toHaveFocus(); - - act(() => screen.getByTestId('1').focus()); - await flushMicrotasks(); - expect(screen.getByTestId('1')).to.have.attribute('data-active'); - expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); - }); - - test('uncontrolled mode', async () => { - render( - - 1 - 2 - 3 - , - ); - - act(() => screen.getByTestId('1').focus()); - expect(screen.getByTestId('1')).to.have.attribute('data-active'); - - fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); - await flushMicrotasks(); - expect(screen.getByTestId('2')).to.have.attribute('data-active'); - expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('2')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('2'), { key: 'ArrowDown' }); - await flushMicrotasks(); - expect(screen.getByTestId('3')).to.have.attribute('data-active'); - expect(screen.getByTestId('3')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('3')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('3'), { key: 'ArrowUp' }); - await flushMicrotasks(); - expect(screen.getByTestId('2')).to.have.attribute('data-active'); - expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('2')).toHaveFocus(); - - act(() => screen.getByTestId('1').focus()); - await flushMicrotasks(); - expect(screen.getByTestId('1')).to.have.attribute('data-active'); - expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); - }); - }); - - describe('grid', () => { - test('uniform 1x1 items', async () => { - function App() { - return ( - // 1 to 9 numpad - - {['1', '2', '3', '4', '5', '6', '7', '8', '9'].map((i) => ( - - {i} - - ))} - - ); - } - - render(); - - act(() => screen.getByTestId('1').focus()); - expect(screen.getByTestId('1')).to.have.attribute('data-active'); - - fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); - await flushMicrotasks(); - expect(screen.getByTestId('4')).to.have.attribute('data-active'); - expect(screen.getByTestId('4')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('4')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('4'), { key: 'ArrowRight' }); - await flushMicrotasks(); - expect(screen.getByTestId('5')).to.have.attribute('data-active'); - expect(screen.getByTestId('5')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('5')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('5'), { key: 'ArrowDown' }); - await flushMicrotasks(); - expect(screen.getByTestId('8')).to.have.attribute('data-active'); - expect(screen.getByTestId('8')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('8')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('8'), { key: 'ArrowLeft' }); - await flushMicrotasks(); - expect(screen.getByTestId('7')).to.have.attribute('data-active'); - expect(screen.getByTestId('7')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('7')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('7'), { key: 'ArrowUp' }); - await flushMicrotasks(); - expect(screen.getByTestId('4')).to.have.attribute('data-active'); - expect(screen.getByTestId('4')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('4')).toHaveFocus(); - - act(() => screen.getByTestId('9').focus()); - await flushMicrotasks(); - expect(screen.getByTestId('9')).to.have.attribute('data-active'); - expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); - }); - - test('wider item', async () => { - function App() { - return ( - // 1 to 9 numpad, but 4, 5 and 6 are one big button - - {['1', '2', '3', '456', '7', '8', '9'].map((i) => ( - - {i} - - ))} - - ); - } - - render(); - - act(() => screen.getByTestId('1').focus()); - expect(screen.getByTestId('1')).to.have.attribute('data-active'); - - fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); - await flushMicrotasks(); - expect(screen.getByTestId('456')).to.have.attribute('data-active'); - expect(screen.getByTestId('456')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('456')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('456'), { key: 'ArrowDown' }); - await flushMicrotasks(); - expect(screen.getByTestId('7')).to.have.attribute('data-active'); - expect(screen.getByTestId('7')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('7')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('7'), { key: 'ArrowRight' }); - await flushMicrotasks(); - expect(screen.getByTestId('8')).to.have.attribute('data-active'); - expect(screen.getByTestId('8')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('8')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('8'), { key: 'ArrowUp' }); - await flushMicrotasks(); - expect(screen.getByTestId('456')).to.have.attribute('data-active'); - expect(screen.getByTestId('456')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('456')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('456'), { key: 'ArrowUp' }); - await flushMicrotasks(); - expect(screen.getByTestId('1')).to.have.attribute('data-active'); - expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('1')).toHaveFocus(); - - act(() => screen.getByTestId('9').focus()); - await flushMicrotasks(); - expect(screen.getByTestId('9')).to.have.attribute('data-active'); - expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); - }); - - test('wider and taller item', async () => { - function App() { - return ( - // 1 to 9 numpad, but 4, 5, 7 and 8 are one big button - - {['1', '2', '3', '4578', '6', '9'].map((i) => ( - - {i} - - ))} - - ); - } - - render(); - - act(() => screen.getByTestId('1').focus()); - expect(screen.getByTestId('1')).to.have.attribute('data-active'); - - fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); - await flushMicrotasks(); - expect(screen.getByTestId('4578')).to.have.attribute('data-active'); - expect(screen.getByTestId('4578')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('4578')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('4578'), { key: 'ArrowRight' }); - await flushMicrotasks(); - expect(screen.getByTestId('6')).to.have.attribute('data-active'); - expect(screen.getByTestId('6')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('6')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('6'), { key: 'ArrowDown' }); - await flushMicrotasks(); - expect(screen.getByTestId('9')).to.have.attribute('data-active'); - expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('9')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('9'), { key: 'ArrowLeft' }); - await flushMicrotasks(); - expect(screen.getByTestId('4578')).to.have.attribute('data-active'); - expect(screen.getByTestId('4578')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('4578')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('4578'), { key: 'ArrowUp' }); - await flushMicrotasks(); - expect(screen.getByTestId('1')).to.have.attribute('data-active'); - expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('1')).toHaveFocus(); - - act(() => screen.getByTestId('9').focus()); - await flushMicrotasks(); - expect(screen.getByTestId('9')).to.have.attribute('data-active'); - expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); - }); - - test('grid flow', async () => { - function App() { - return ( - // 1 to 9 numpad, but 2, 3, 5 and 6 are one big button, and so are 7 and 8. - // 4 is missing - - {['1', '2356', '78', '9'].map((i) => ( - - {i} - - ))} - - ); - } - - render(); - - act(() => screen.getByTestId('1').focus()); - expect(screen.getByTestId('1')).to.have.attribute('data-active'); - - fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); - await flushMicrotasks(); - expect(screen.getByTestId('78')).to.have.attribute('data-active'); - expect(screen.getByTestId('78')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('78')).toHaveFocus(); - }); - - test('grid flow: dense', async () => { - function App() { - return ( - // 1 to 9 numpad, but 2, 3, 5 and 6 are one big button, and so are 7 and 8. - // 9 is missing - - {['1', '2356', '78', '4'].map((i) => ( - - {i} - - ))} - - ); - } - - render(); - - act(() => screen.getByTestId('1').focus()); - expect(screen.getByTestId('1')).to.have.attribute('data-active'); - - fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); - await flushMicrotasks(); - expect(screen.getByTestId('4')).to.have.attribute('data-active'); - expect(screen.getByTestId('4')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('4')).toHaveFocus(); - - fireEvent.keyDown(screen.getByTestId('4'), { key: 'ArrowDown' }); - await flushMicrotasks(); - expect(screen.getByTestId('78')).to.have.attribute('data-active'); - expect(screen.getByTestId('78')).to.have.attribute('tabindex', '0'); - expect(screen.getByTestId('78')).toHaveFocus(); - }); - }); -}); +// import * as React from 'react'; +// import { expect } from 'chai'; +// import { test } from 'mocha'; +// import { createRenderer, act, screen, fireEvent, flushMicrotasks } from '@mui/internal-test-utils'; +// import { describeConformance } from '../../../test/describeConformance'; +// import { CompositeRoot } from './CompositeRoot'; +// import { CompositeItem } from '../Item/CompositeItem'; + +// describe('', () => { +// const { render } = createRenderer(); + +// describeConformance(, () => ({ +// refInstanceof: window.HTMLDivElement, +// render, +// })); + +// describe('list', () => { +// test('controlled mode', async () => { +// function App() { +// const [activeIndex, setActiveIndex] = React.useState(0); +// return ( +// +// 1 +// 2 +// 3 +// +// ); +// } + +// render(); + +// act(() => screen.getByTestId('1').focus()); +// expect(screen.getByTestId('1')).to.have.attribute('data-active'); + +// fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('2')).to.have.attribute('data-active'); +// expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('2')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('2'), { key: 'ArrowDown' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('3')).to.have.attribute('data-active'); +// expect(screen.getByTestId('3')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('3')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('3'), { key: 'ArrowUp' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('2')).to.have.attribute('data-active'); +// expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('2')).toHaveFocus(); + +// act(() => screen.getByTestId('1').focus()); +// await flushMicrotasks(); +// expect(screen.getByTestId('1')).to.have.attribute('data-active'); +// expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); +// }); + +// test('uncontrolled mode', async () => { +// render( +// +// 1 +// 2 +// 3 +// , +// ); + +// act(() => screen.getByTestId('1').focus()); +// expect(screen.getByTestId('1')).to.have.attribute('data-active'); + +// fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('2')).to.have.attribute('data-active'); +// expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('2')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('2'), { key: 'ArrowDown' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('3')).to.have.attribute('data-active'); +// expect(screen.getByTestId('3')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('3')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('3'), { key: 'ArrowUp' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('2')).to.have.attribute('data-active'); +// expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('2')).toHaveFocus(); + +// act(() => screen.getByTestId('1').focus()); +// await flushMicrotasks(); +// expect(screen.getByTestId('1')).to.have.attribute('data-active'); +// expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); +// }); +// }); + +// describe('grid', () => { +// test('uniform 1x1 items', async () => { +// function App() { +// return ( +// // 1 to 9 numpad +// +// {['1', '2', '3', '4', '5', '6', '7', '8', '9'].map((i) => ( +// +// {i} +// +// ))} +// +// ); +// } + +// render(); + +// act(() => screen.getByTestId('1').focus()); +// expect(screen.getByTestId('1')).to.have.attribute('data-active'); + +// fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('4')).to.have.attribute('data-active'); +// expect(screen.getByTestId('4')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('4')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('4'), { key: 'ArrowRight' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('5')).to.have.attribute('data-active'); +// expect(screen.getByTestId('5')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('5')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('5'), { key: 'ArrowDown' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('8')).to.have.attribute('data-active'); +// expect(screen.getByTestId('8')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('8')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('8'), { key: 'ArrowLeft' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('7')).to.have.attribute('data-active'); +// expect(screen.getByTestId('7')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('7')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('7'), { key: 'ArrowUp' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('4')).to.have.attribute('data-active'); +// expect(screen.getByTestId('4')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('4')).toHaveFocus(); + +// act(() => screen.getByTestId('9').focus()); +// await flushMicrotasks(); +// expect(screen.getByTestId('9')).to.have.attribute('data-active'); +// expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); +// }); + +// test('wider item', async () => { +// function App() { +// return ( +// // 1 to 9 numpad, but 4, 5 and 6 are one big button +// +// {['1', '2', '3', '456', '7', '8', '9'].map((i) => ( +// +// {i} +// +// ))} +// +// ); +// } + +// render(); + +// act(() => screen.getByTestId('1').focus()); +// expect(screen.getByTestId('1')).to.have.attribute('data-active'); + +// fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('456')).to.have.attribute('data-active'); +// expect(screen.getByTestId('456')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('456')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('456'), { key: 'ArrowDown' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('7')).to.have.attribute('data-active'); +// expect(screen.getByTestId('7')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('7')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('7'), { key: 'ArrowRight' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('8')).to.have.attribute('data-active'); +// expect(screen.getByTestId('8')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('8')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('8'), { key: 'ArrowUp' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('456')).to.have.attribute('data-active'); +// expect(screen.getByTestId('456')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('456')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('456'), { key: 'ArrowUp' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('1')).to.have.attribute('data-active'); +// expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('1')).toHaveFocus(); + +// act(() => screen.getByTestId('9').focus()); +// await flushMicrotasks(); +// expect(screen.getByTestId('9')).to.have.attribute('data-active'); +// expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); +// }); + +// test('wider and taller item', async () => { +// function App() { +// return ( +// // 1 to 9 numpad, but 4, 5, 7 and 8 are one big button +// +// {['1', '2', '3', '4578', '6', '9'].map((i) => ( +// +// {i} +// +// ))} +// +// ); +// } + +// render(); + +// act(() => screen.getByTestId('1').focus()); +// expect(screen.getByTestId('1')).to.have.attribute('data-active'); + +// fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('4578')).to.have.attribute('data-active'); +// expect(screen.getByTestId('4578')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('4578')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('4578'), { key: 'ArrowRight' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('6')).to.have.attribute('data-active'); +// expect(screen.getByTestId('6')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('6')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('6'), { key: 'ArrowDown' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('9')).to.have.attribute('data-active'); +// expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('9')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('9'), { key: 'ArrowLeft' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('4578')).to.have.attribute('data-active'); +// expect(screen.getByTestId('4578')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('4578')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('4578'), { key: 'ArrowUp' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('1')).to.have.attribute('data-active'); +// expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('1')).toHaveFocus(); + +// act(() => screen.getByTestId('9').focus()); +// await flushMicrotasks(); +// expect(screen.getByTestId('9')).to.have.attribute('data-active'); +// expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); +// }); + +// test('grid flow', async () => { +// function App() { +// return ( +// // 1 to 9 numpad, but 2, 3, 5 and 6 are one big button, and so are 7 and 8. +// // 4 is missing +// +// {['1', '2356', '78', '9'].map((i) => ( +// +// {i} +// +// ))} +// +// ); +// } + +// render(); + +// act(() => screen.getByTestId('1').focus()); +// expect(screen.getByTestId('1')).to.have.attribute('data-active'); + +// fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('78')).to.have.attribute('data-active'); +// expect(screen.getByTestId('78')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('78')).toHaveFocus(); +// }); + +// test('grid flow: dense', async () => { +// function App() { +// return ( +// // 1 to 9 numpad, but 2, 3, 5 and 6 are one big button, and so are 7 and 8. +// // 9 is missing +// +// {['1', '2356', '78', '4'].map((i) => ( +// +// {i} +// +// ))} +// +// ); +// } + +// render(); + +// act(() => screen.getByTestId('1').focus()); +// expect(screen.getByTestId('1')).to.have.attribute('data-active'); + +// fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('4')).to.have.attribute('data-active'); +// expect(screen.getByTestId('4')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('4')).toHaveFocus(); + +// fireEvent.keyDown(screen.getByTestId('4'), { key: 'ArrowDown' }); +// await flushMicrotasks(); +// expect(screen.getByTestId('78')).to.have.attribute('data-active'); +// expect(screen.getByTestId('78')).to.have.attribute('tabindex', '0'); +// expect(screen.getByTestId('78')).toHaveFocus(); +// }); +// }); +// }); From 2221643bbba5471c4eb5d9f009fa9a8aefcf2a91 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 14 Aug 2024 16:03:39 +1000 Subject: [PATCH 39/47] Add Radio tests back --- .../Radio/Indicator/RadioIndicator.test.tsx | 26 +- .../src/Radio/Root/RadioRoot.test.tsx | 22 +- .../RadioGroup/Root/RadioGroupRoot.test.tsx | 438 +++++++++--------- 3 files changed, 243 insertions(+), 243 deletions(-) diff --git a/packages/mui-base/src/Radio/Indicator/RadioIndicator.test.tsx b/packages/mui-base/src/Radio/Indicator/RadioIndicator.test.tsx index 41297329ac..d2d03649d1 100644 --- a/packages/mui-base/src/Radio/Indicator/RadioIndicator.test.tsx +++ b/packages/mui-base/src/Radio/Indicator/RadioIndicator.test.tsx @@ -1,15 +1,15 @@ -// import * as React from 'react'; -// import { createRenderer } from '@mui/internal-test-utils'; -// import * as Radio from '@base_ui/react/Radio'; -// import { describeConformance } from '../../../test/describeConformance'; +import * as React from 'react'; +import { createRenderer } from '@mui/internal-test-utils'; +import * as Radio from '@base_ui/react/Radio'; +import { describeConformance } from '../../../test/describeConformance'; -// describe('', () => { -// const { render } = createRenderer(); +describe('', () => { + const { render } = createRenderer(); -// describeConformance(, () => ({ -// refInstanceof: window.HTMLSpanElement, -// render(node) { -// return render({node}); -// }, -// })); -// }); + describeConformance(, () => ({ + refInstanceof: window.HTMLSpanElement, + render(node) { + return render({node}); + }, + })); +}); diff --git a/packages/mui-base/src/Radio/Root/RadioRoot.test.tsx b/packages/mui-base/src/Radio/Root/RadioRoot.test.tsx index 8993bb7b49..cc117d3580 100644 --- a/packages/mui-base/src/Radio/Root/RadioRoot.test.tsx +++ b/packages/mui-base/src/Radio/Root/RadioRoot.test.tsx @@ -1,13 +1,13 @@ -// import * as React from 'react'; -// import { createRenderer } from '@mui/internal-test-utils'; -// import * as Radio from '@base_ui/react/Radio'; -// import { describeConformance } from '../../../test/describeConformance'; +import * as React from 'react'; +import { createRenderer } from '@mui/internal-test-utils'; +import * as Radio from '@base_ui/react/Radio'; +import { describeConformance } from '../../../test/describeConformance'; -// describe('', () => { -// const { render } = createRenderer(); +describe('', () => { + const { render } = createRenderer(); -// describeConformance(, () => ({ -// refInstanceof: window.HTMLButtonElement, -// render, -// })); -// }); + describeConformance(, () => ({ + refInstanceof: window.HTMLButtonElement, + render, + })); +}); diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx index 9b93d0f1bb..006e1569ec 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx @@ -1,219 +1,219 @@ -// import * as React from 'react'; -// import * as RadioGroup from '@base_ui/react/RadioGroup'; -// import * as Radio from '@base_ui/react/Radio'; -// import { expect } from 'chai'; -// import { spy } from 'sinon'; -// import { createRenderer, act, screen, fireEvent } from '@mui/internal-test-utils'; -// import userEvent from '@testing-library/user-event'; -// import { describeConformance } from '../../../test/describeConformance'; - -// const isJSDOM = /jsdom/.test(window.navigator.userAgent); - -// const user = userEvent.setup(); - -// describe('', () => { -// const { render } = createRenderer(); - -// describeConformance(, () => ({ -// refInstanceof: window.HTMLDivElement, -// render, -// })); - -// describe('extra props', () => { -// it('can override the built-in attributes', () => { -// const { container } = render(); -// // eslint-disable-next-line testing-library/no-node-access -// expect(container.firstElementChild as HTMLElement).to.have.attribute('role', 'switch'); -// }); -// }); - -// it('should call onValueChange when an item is clicked', () => { -// const handleChange = spy(); -// render( -// -// -// , -// ); - -// const item = screen.getByTestId('item'); - -// fireEvent.click(item); - -// expect(handleChange.callCount).to.equal(1); -// expect(handleChange.firstCall.args[0]).to.equal('a'); -// }); - -// describe('prop: disabled', () => { -// it('should have the `aria-disabled` attribute', () => { -// render(); -// expect(screen.getByRole('radiogroup')).to.have.attribute('aria-disabled', 'true'); -// }); - -// it('should not have the aria attribute when `disabled` is not set', () => { -// render(); -// expect(screen.getByRole('radiogroup')).not.to.have.attribute('aria-disabled'); -// }); - -// it('should not change its state when clicked', () => { -// render( -// -// -// , -// ); - -// const item = screen.getByTestId('item'); - -// expect(item).to.have.attribute('aria-checked', 'false'); - -// fireEvent.click(item); - -// expect(item).to.have.attribute('aria-checked', 'false'); -// }); -// }); - -// describe('prop: readOnly', () => { -// it('should have the `aria-readonly` attribute', () => { -// render(); -// const group = screen.getByRole('radiogroup'); -// expect(group).to.have.attribute('aria-readonly', 'true'); -// }); - -// it('should not have the aria attribute when `readOnly` is not set', () => { -// render(); -// const group = screen.getByRole('radiogroup'); -// expect(group).not.to.have.attribute('aria-readonly'); -// }); - -// it('should not change its state when clicked', () => { -// render( -// -// -// , -// ); - -// const item = screen.getByTestId('item'); - -// expect(item).to.have.attribute('aria-checked', 'false'); - -// fireEvent.click(item); - -// expect(item).to.have.attribute('aria-checked', 'false'); -// }); -// }); - -// it('should update its state if the underlying input is toggled', () => { -// render( -// -// -// , -// ); - -// const group = screen.getByTestId('root'); -// const item = screen.getByTestId('item'); - -// // eslint-disable-next-line testing-library/no-node-access -// const input = group.querySelector('input')!; - -// fireEvent.click(input); - -// expect(item).to.have.attribute('aria-checked', 'true'); -// }); - -// it('should place the style hooks on the root and subcomponents', () => { -// render( -// -// -// -// -// , -// ); - -// const root = screen.getByRole('radiogroup'); -// const item = screen.getByTestId('item'); -// const indicator = screen.getByTestId('indicator'); - -// expect(root).to.have.attribute('data-disabled', 'true'); -// expect(root).to.have.attribute('data-readonly', 'true'); -// expect(root).to.have.attribute('data-required', 'true'); - -// expect(item).to.have.attribute('data-radio', 'checked'); -// expect(item).to.have.attribute('data-disabled', 'true'); -// expect(item).to.have.attribute('data-readonly', 'true'); -// expect(item).to.have.attribute('data-required', 'true'); - -// expect(indicator).to.have.attribute('data-radio', 'checked'); -// expect(indicator).to.have.attribute('data-disabled', 'true'); -// expect(indicator).to.have.attribute('data-readonly', 'true'); -// expect(indicator).to.have.attribute('data-required', 'true'); -// }); - -// it('should set the name attribute on the input', () => { -// render(); -// const group = screen.getByRole('radiogroup'); -// // eslint-disable-next-line testing-library/no-node-access -// expect(group.nextElementSibling).to.have.attribute('name', 'radio-group'); -// }); - -// it('should include the checkbox value in the form submission', function test() { -// if (isJSDOM) { -// // FormData is not available in JSDOM -// this.skip(); -// } - -// let stringifiedFormData = ''; - -// render( -//
    { -// event.preventDefault(); -// const formData = new FormData(event.currentTarget); -// stringifiedFormData = new URLSearchParams(formData as any).toString(); -// }} -// > -// -// -// -// -// -// -//
    , -// ); - -// const [radioA] = screen.getAllByRole('radio'); -// const submitButton = screen.getByRole('button'); - -// fireEvent.click(submitButton); - -// expect(stringifiedFormData).to.equal(''); - -// fireEvent.click(radioA); -// fireEvent.click(submitButton); - -// expect(stringifiedFormData).to.equal('group=a'); -// }); - -// it('should automatically select item upon navigation', async () => { -// render( -// -// -// -// , -// ); - -// const a = screen.getByTestId('a'); -// const b = screen.getByTestId('b'); - -// act(() => { -// a.focus(); -// }); - -// expect(a).to.have.attribute('aria-checked', 'false'); - -// await user.keyboard('{ArrowDown}'); - -// expect(a).to.have.attribute('aria-checked', 'false'); - -// expect(b).toHaveFocus(); -// expect(b).to.have.attribute('aria-checked', 'true'); -// }); -// }); +import * as React from 'react'; +import * as RadioGroup from '@base_ui/react/RadioGroup'; +import * as Radio from '@base_ui/react/Radio'; +import { expect } from 'chai'; +import { spy } from 'sinon'; +import { createRenderer, act, screen, fireEvent } from '@mui/internal-test-utils'; +import userEvent from '@testing-library/user-event'; +import { describeConformance } from '../../../test/describeConformance'; + +const isJSDOM = /jsdom/.test(window.navigator.userAgent); + +const user = userEvent.setup(); + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render, + })); + + describe('extra props', () => { + it('can override the built-in attributes', () => { + const { container } = render(); + // eslint-disable-next-line testing-library/no-node-access + expect(container.firstElementChild as HTMLElement).to.have.attribute('role', 'switch'); + }); + }); + + it('should call onValueChange when an item is clicked', () => { + const handleChange = spy(); + render( + + + , + ); + + const item = screen.getByTestId('item'); + + fireEvent.click(item); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.firstCall.args[0]).to.equal('a'); + }); + + describe('prop: disabled', () => { + it('should have the `aria-disabled` attribute', () => { + render(); + expect(screen.getByRole('radiogroup')).to.have.attribute('aria-disabled', 'true'); + }); + + it('should not have the aria attribute when `disabled` is not set', () => { + render(); + expect(screen.getByRole('radiogroup')).not.to.have.attribute('aria-disabled'); + }); + + it('should not change its state when clicked', () => { + render( + + + , + ); + + const item = screen.getByTestId('item'); + + expect(item).to.have.attribute('aria-checked', 'false'); + + fireEvent.click(item); + + expect(item).to.have.attribute('aria-checked', 'false'); + }); + }); + + describe('prop: readOnly', () => { + it('should have the `aria-readonly` attribute', () => { + render(); + const group = screen.getByRole('radiogroup'); + expect(group).to.have.attribute('aria-readonly', 'true'); + }); + + it('should not have the aria attribute when `readOnly` is not set', () => { + render(); + const group = screen.getByRole('radiogroup'); + expect(group).not.to.have.attribute('aria-readonly'); + }); + + it('should not change its state when clicked', () => { + render( + + + , + ); + + const item = screen.getByTestId('item'); + + expect(item).to.have.attribute('aria-checked', 'false'); + + fireEvent.click(item); + + expect(item).to.have.attribute('aria-checked', 'false'); + }); + }); + + it('should update its state if the underlying input is toggled', () => { + render( + + + , + ); + + const group = screen.getByTestId('root'); + const item = screen.getByTestId('item'); + + // eslint-disable-next-line testing-library/no-node-access + const input = group.querySelector('input')!; + + fireEvent.click(input); + + expect(item).to.have.attribute('aria-checked', 'true'); + }); + + it('should place the style hooks on the root and subcomponents', () => { + render( + + + + + , + ); + + const root = screen.getByRole('radiogroup'); + const item = screen.getByTestId('item'); + const indicator = screen.getByTestId('indicator'); + + expect(root).to.have.attribute('data-disabled', 'true'); + expect(root).to.have.attribute('data-readonly', 'true'); + expect(root).to.have.attribute('data-required', 'true'); + + expect(item).to.have.attribute('data-radio', 'checked'); + expect(item).to.have.attribute('data-disabled', 'true'); + expect(item).to.have.attribute('data-readonly', 'true'); + expect(item).to.have.attribute('data-required', 'true'); + + expect(indicator).to.have.attribute('data-radio', 'checked'); + expect(indicator).to.have.attribute('data-disabled', 'true'); + expect(indicator).to.have.attribute('data-readonly', 'true'); + expect(indicator).to.have.attribute('data-required', 'true'); + }); + + it('should set the name attribute on the input', () => { + render(); + const group = screen.getByRole('radiogroup'); + // eslint-disable-next-line testing-library/no-node-access + expect(group.nextElementSibling).to.have.attribute('name', 'radio-group'); + }); + + it('should include the checkbox value in the form submission', function test() { + if (isJSDOM) { + // FormData is not available in JSDOM + this.skip(); + } + + let stringifiedFormData = ''; + + render( +
    { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + stringifiedFormData = new URLSearchParams(formData as any).toString(); + }} + > + + + + + + +
    , + ); + + const [radioA] = screen.getAllByRole('radio'); + const submitButton = screen.getByRole('button'); + + fireEvent.click(submitButton); + + expect(stringifiedFormData).to.equal(''); + + fireEvent.click(radioA); + fireEvent.click(submitButton); + + expect(stringifiedFormData).to.equal('group=a'); + }); + + it('should automatically select item upon navigation', async () => { + render( + + + + , + ); + + const a = screen.getByTestId('a'); + const b = screen.getByTestId('b'); + + act(() => { + a.focus(); + }); + + expect(a).to.have.attribute('aria-checked', 'false'); + + await user.keyboard('{ArrowDown}'); + + expect(a).to.have.attribute('aria-checked', 'false'); + + expect(b).toHaveFocus(); + expect(b).to.have.attribute('aria-checked', 'true'); + }); +}); From 7d051ad6fcd570d995cfce29261ca1941c36f4e8 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 14 Aug 2024 16:15:46 +1000 Subject: [PATCH 40/47] Update APIs --- .../base-ui/api/use-composite-list-item.json | 2 +- .../src/Composite/Item/CompositeItem.test.tsx | 16 - .../src/Composite/Item/CompositeItem.tsx | 14 +- .../src/Composite/Item/CompositeItem.types.ts | 7 - .../src/Composite/Item/useCompositeItem.ts | 2 +- .../CompositeList => List}/CompositeList.tsx | 60 ++- .../CompositeListContext.ts | 0 .../useCompositeListItem.ts | 2 +- .../src/Composite/Root/CompositeRoot.test.tsx | 360 ------------------ .../src/Composite/Root/CompositeRoot.tsx | 25 +- .../src/Composite/Root/CompositeRoot.types.ts | 14 - .../Composite/Root/CompositeRootContext.ts | 14 +- .../CompositeList/CompositeList.types.ts | 0 .../src/Radio/Indicator/RadioIndicator.tsx | 16 +- .../mui-base/src/Radio/Root/RadioRoot.tsx | 62 +-- .../RadioGroup/Root/RadioGroupRoot.test.tsx | 11 +- .../src/RadioGroup/Root/RadioGroupRoot.tsx | 84 ++-- 17 files changed, 174 insertions(+), 515 deletions(-) delete mode 100644 packages/mui-base/src/Composite/Item/CompositeItem.test.tsx delete mode 100644 packages/mui-base/src/Composite/Item/CompositeItem.types.ts rename packages/mui-base/src/Composite/{utils/CompositeList => List}/CompositeList.tsx (58%) rename packages/mui-base/src/Composite/{utils/CompositeList => List}/CompositeListContext.ts (100%) rename packages/mui-base/src/Composite/{utils/CompositeList => List}/useCompositeListItem.ts (96%) delete mode 100644 packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx delete mode 100644 packages/mui-base/src/Composite/Root/CompositeRoot.types.ts delete mode 100644 packages/mui-base/src/Composite/utils/CompositeList/CompositeList.types.ts diff --git a/docs/pages/base-ui/api/use-composite-list-item.json b/docs/pages/base-ui/api/use-composite-list-item.json index 21819ac9c1..497920ca9d 100644 --- a/docs/pages/base-ui/api/use-composite-list-item.json +++ b/docs/pages/base-ui/api/use-composite-list-item.json @@ -2,7 +2,7 @@ "parameters": {}, "returnValue": {}, "name": "useCompositeListItem", - "filename": "/packages/mui-base/src/Composite/utils/CompositeList/useCompositeListItem.ts", + "filename": "/packages/mui-base/src/Composite/List/useCompositeListItem.ts", "imports": ["import { useCompositeListItem } from '@base_ui/react/Composite';"], "demos": "
      " } diff --git a/packages/mui-base/src/Composite/Item/CompositeItem.test.tsx b/packages/mui-base/src/Composite/Item/CompositeItem.test.tsx deleted file mode 100644 index d3cedbeffa..0000000000 --- a/packages/mui-base/src/Composite/Item/CompositeItem.test.tsx +++ /dev/null @@ -1,16 +0,0 @@ -// import * as React from 'react'; -// import { createRenderer } from '@mui/internal-test-utils'; -// import { describeConformance } from '../../../test/describeConformance'; -// import { CompositeRoot } from '../Root/CompositeRoot'; -// import { CompositeItem } from './CompositeItem'; - -// describe('', () => { -// const { render } = createRenderer(); - -// describeConformance(, () => ({ -// refInstanceof: window.HTMLDivElement, -// render(node) { -// return render({node}); -// }, -// })); -// }); diff --git a/packages/mui-base/src/Composite/Item/CompositeItem.tsx b/packages/mui-base/src/Composite/Item/CompositeItem.tsx index 48cd91c990..8b87646048 100644 --- a/packages/mui-base/src/Composite/Item/CompositeItem.tsx +++ b/packages/mui-base/src/Composite/Item/CompositeItem.tsx @@ -4,14 +4,14 @@ import PropTypes from 'prop-types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { useForkRef } from '../../utils/useForkRef'; import { useCompositeRootContext } from '../Root/CompositeRootContext'; -import type { CompositeItemOwnerState, CompositeItemProps } from './CompositeItem.types'; import { useCompositeItem } from './useCompositeItem'; +import type { BaseUIComponentProps } from '../../utils/types'; /** * @ignore - internal component. */ const CompositeItem = React.forwardRef(function CompositeItem( - props: CompositeItemProps, + props: CompositeItem.Props, forwardedRef: React.ForwardedRef, ) { const { render, className, ...otherProps } = props; @@ -19,7 +19,7 @@ const CompositeItem = React.forwardRef(function CompositeItem( const { activeIndex } = useCompositeRootContext(); const { getItemProps, ref, index } = useCompositeItem(); - const ownerState: CompositeItemOwnerState = React.useMemo( + const ownerState: CompositeItem.OwnerState = React.useMemo( () => ({ active: index === activeIndex, }), @@ -40,6 +40,14 @@ const CompositeItem = React.forwardRef(function CompositeItem( return renderElement(); }); +namespace CompositeItem { + export interface OwnerState { + active: boolean; + } + + export interface Props extends BaseUIComponentProps<'div', OwnerState> {} +} + CompositeItem.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ diff --git a/packages/mui-base/src/Composite/Item/CompositeItem.types.ts b/packages/mui-base/src/Composite/Item/CompositeItem.types.ts deleted file mode 100644 index a4223f30b3..0000000000 --- a/packages/mui-base/src/Composite/Item/CompositeItem.types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { BaseUIComponentProps } from '../../utils/types'; - -export type CompositeItemOwnerState = { - active: boolean; -}; - -export interface CompositeItemProps extends BaseUIComponentProps<'div', CompositeItemOwnerState> {} diff --git a/packages/mui-base/src/Composite/Item/useCompositeItem.ts b/packages/mui-base/src/Composite/Item/useCompositeItem.ts index ab385d81a4..70ab405323 100644 --- a/packages/mui-base/src/Composite/Item/useCompositeItem.ts +++ b/packages/mui-base/src/Composite/Item/useCompositeItem.ts @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; import { useCompositeRootContext } from '../Root/CompositeRootContext'; -import { useCompositeListItem } from '../utils/CompositeList/useCompositeListItem'; +import { useCompositeListItem } from '../List/useCompositeListItem'; import { mergeReactProps } from '../../utils/mergeReactProps'; /** diff --git a/packages/mui-base/src/Composite/utils/CompositeList/CompositeList.tsx b/packages/mui-base/src/Composite/List/CompositeList.tsx similarity index 58% rename from packages/mui-base/src/Composite/utils/CompositeList/CompositeList.tsx rename to packages/mui-base/src/Composite/List/CompositeList.tsx index 66280aca9f..09815eb7e6 100644 --- a/packages/mui-base/src/Composite/utils/CompositeList/CompositeList.tsx +++ b/packages/mui-base/src/Composite/List/CompositeList.tsx @@ -1,7 +1,8 @@ /* eslint-disable no-bitwise */ 'use client'; import * as React from 'react'; -import { useEnhancedEffect } from '../../../utils/useEnhancedEffect'; +import PropTypes from 'prop-types'; +import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; import { CompositeListContext } from './CompositeListContext'; function sortByDocumentPosition(a: Node, b: Node) { @@ -33,25 +34,11 @@ function areMapsEqual(map1: Map, map2: Map>; - /** - * A ref to the list of element labels, ordered by their index. - * `useTypeahead`'s `listRef` prop. - */ - labelsRef?: React.MutableRefObject>; -} - /** * Provides context for a list of items in a composite component. * @ignore - internal component. */ -export function CompositeList(props: CompositeListProps) { +function CompositeList(props: CompositeList.Props) { const { children, elementsRef, labelsRef } = props; const [map, setMap] = React.useState(() => new Map()); @@ -90,3 +77,44 @@ export function CompositeList(props: CompositeListProps) { {children} ); } + +namespace CompositeList { + export interface Props { + children: React.ReactNode; + /** + * A ref to the list of HTML elements, ordered by their index. + * `useListNavigation`'s `listRef` prop. + */ + elementsRef: React.MutableRefObject>; + /** + * A ref to the list of element labels, ordered by their index. + * `useTypeahead`'s `listRef` prop. + */ + labelsRef?: React.MutableRefObject>; + } +} + +CompositeList.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, + /** + * A ref to the list of HTML elements, ordered by their index. + * `useListNavigation`'s `listRef` prop. + */ + elementsRef: PropTypes /* @typescript-to-proptypes-ignore */.any, + /** + * A ref to the list of element labels, ordered by their index. + * `useTypeahead`'s `listRef` prop. + */ + labelsRef: PropTypes.shape({ + current: PropTypes.arrayOf(PropTypes.string).isRequired, + }), +} as any; + +export { CompositeList }; diff --git a/packages/mui-base/src/Composite/utils/CompositeList/CompositeListContext.ts b/packages/mui-base/src/Composite/List/CompositeListContext.ts similarity index 100% rename from packages/mui-base/src/Composite/utils/CompositeList/CompositeListContext.ts rename to packages/mui-base/src/Composite/List/CompositeListContext.ts diff --git a/packages/mui-base/src/Composite/utils/CompositeList/useCompositeListItem.ts b/packages/mui-base/src/Composite/List/useCompositeListItem.ts similarity index 96% rename from packages/mui-base/src/Composite/utils/CompositeList/useCompositeListItem.ts rename to packages/mui-base/src/Composite/List/useCompositeListItem.ts index 8f40682e41..7a93d56217 100644 --- a/packages/mui-base/src/Composite/utils/CompositeList/useCompositeListItem.ts +++ b/packages/mui-base/src/Composite/List/useCompositeListItem.ts @@ -1,6 +1,6 @@ 'use client'; import * as React from 'react'; -import { useEnhancedEffect } from '../../../utils/useEnhancedEffect'; +import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; import { useCompositeListContext } from './CompositeListContext'; export interface UseCompositeListItemParameters { diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx b/packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx deleted file mode 100644 index 86c2a19c07..0000000000 --- a/packages/mui-base/src/Composite/Root/CompositeRoot.test.tsx +++ /dev/null @@ -1,360 +0,0 @@ -// import * as React from 'react'; -// import { expect } from 'chai'; -// import { test } from 'mocha'; -// import { createRenderer, act, screen, fireEvent, flushMicrotasks } from '@mui/internal-test-utils'; -// import { describeConformance } from '../../../test/describeConformance'; -// import { CompositeRoot } from './CompositeRoot'; -// import { CompositeItem } from '../Item/CompositeItem'; - -// describe('', () => { -// const { render } = createRenderer(); - -// describeConformance(, () => ({ -// refInstanceof: window.HTMLDivElement, -// render, -// })); - -// describe('list', () => { -// test('controlled mode', async () => { -// function App() { -// const [activeIndex, setActiveIndex] = React.useState(0); -// return ( -// -// 1 -// 2 -// 3 -// -// ); -// } - -// render(); - -// act(() => screen.getByTestId('1').focus()); -// expect(screen.getByTestId('1')).to.have.attribute('data-active'); - -// fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('2')).to.have.attribute('data-active'); -// expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('2')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('2'), { key: 'ArrowDown' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('3')).to.have.attribute('data-active'); -// expect(screen.getByTestId('3')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('3')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('3'), { key: 'ArrowUp' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('2')).to.have.attribute('data-active'); -// expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('2')).toHaveFocus(); - -// act(() => screen.getByTestId('1').focus()); -// await flushMicrotasks(); -// expect(screen.getByTestId('1')).to.have.attribute('data-active'); -// expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); -// }); - -// test('uncontrolled mode', async () => { -// render( -// -// 1 -// 2 -// 3 -// , -// ); - -// act(() => screen.getByTestId('1').focus()); -// expect(screen.getByTestId('1')).to.have.attribute('data-active'); - -// fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('2')).to.have.attribute('data-active'); -// expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('2')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('2'), { key: 'ArrowDown' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('3')).to.have.attribute('data-active'); -// expect(screen.getByTestId('3')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('3')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('3'), { key: 'ArrowUp' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('2')).to.have.attribute('data-active'); -// expect(screen.getByTestId('2')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('2')).toHaveFocus(); - -// act(() => screen.getByTestId('1').focus()); -// await flushMicrotasks(); -// expect(screen.getByTestId('1')).to.have.attribute('data-active'); -// expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); -// }); -// }); - -// describe('grid', () => { -// test('uniform 1x1 items', async () => { -// function App() { -// return ( -// // 1 to 9 numpad -// -// {['1', '2', '3', '4', '5', '6', '7', '8', '9'].map((i) => ( -// -// {i} -// -// ))} -// -// ); -// } - -// render(); - -// act(() => screen.getByTestId('1').focus()); -// expect(screen.getByTestId('1')).to.have.attribute('data-active'); - -// fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('4')).to.have.attribute('data-active'); -// expect(screen.getByTestId('4')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('4')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('4'), { key: 'ArrowRight' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('5')).to.have.attribute('data-active'); -// expect(screen.getByTestId('5')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('5')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('5'), { key: 'ArrowDown' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('8')).to.have.attribute('data-active'); -// expect(screen.getByTestId('8')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('8')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('8'), { key: 'ArrowLeft' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('7')).to.have.attribute('data-active'); -// expect(screen.getByTestId('7')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('7')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('7'), { key: 'ArrowUp' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('4')).to.have.attribute('data-active'); -// expect(screen.getByTestId('4')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('4')).toHaveFocus(); - -// act(() => screen.getByTestId('9').focus()); -// await flushMicrotasks(); -// expect(screen.getByTestId('9')).to.have.attribute('data-active'); -// expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); -// }); - -// test('wider item', async () => { -// function App() { -// return ( -// // 1 to 9 numpad, but 4, 5 and 6 are one big button -// -// {['1', '2', '3', '456', '7', '8', '9'].map((i) => ( -// -// {i} -// -// ))} -// -// ); -// } - -// render(); - -// act(() => screen.getByTestId('1').focus()); -// expect(screen.getByTestId('1')).to.have.attribute('data-active'); - -// fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('456')).to.have.attribute('data-active'); -// expect(screen.getByTestId('456')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('456')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('456'), { key: 'ArrowDown' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('7')).to.have.attribute('data-active'); -// expect(screen.getByTestId('7')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('7')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('7'), { key: 'ArrowRight' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('8')).to.have.attribute('data-active'); -// expect(screen.getByTestId('8')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('8')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('8'), { key: 'ArrowUp' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('456')).to.have.attribute('data-active'); -// expect(screen.getByTestId('456')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('456')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('456'), { key: 'ArrowUp' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('1')).to.have.attribute('data-active'); -// expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('1')).toHaveFocus(); - -// act(() => screen.getByTestId('9').focus()); -// await flushMicrotasks(); -// expect(screen.getByTestId('9')).to.have.attribute('data-active'); -// expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); -// }); - -// test('wider and taller item', async () => { -// function App() { -// return ( -// // 1 to 9 numpad, but 4, 5, 7 and 8 are one big button -// -// {['1', '2', '3', '4578', '6', '9'].map((i) => ( -// -// {i} -// -// ))} -// -// ); -// } - -// render(); - -// act(() => screen.getByTestId('1').focus()); -// expect(screen.getByTestId('1')).to.have.attribute('data-active'); - -// fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('4578')).to.have.attribute('data-active'); -// expect(screen.getByTestId('4578')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('4578')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('4578'), { key: 'ArrowRight' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('6')).to.have.attribute('data-active'); -// expect(screen.getByTestId('6')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('6')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('6'), { key: 'ArrowDown' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('9')).to.have.attribute('data-active'); -// expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('9')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('9'), { key: 'ArrowLeft' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('4578')).to.have.attribute('data-active'); -// expect(screen.getByTestId('4578')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('4578')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('4578'), { key: 'ArrowUp' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('1')).to.have.attribute('data-active'); -// expect(screen.getByTestId('1')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('1')).toHaveFocus(); - -// act(() => screen.getByTestId('9').focus()); -// await flushMicrotasks(); -// expect(screen.getByTestId('9')).to.have.attribute('data-active'); -// expect(screen.getByTestId('9')).to.have.attribute('tabindex', '0'); -// }); - -// test('grid flow', async () => { -// function App() { -// return ( -// // 1 to 9 numpad, but 2, 3, 5 and 6 are one big button, and so are 7 and 8. -// // 4 is missing -// -// {['1', '2356', '78', '9'].map((i) => ( -// -// {i} -// -// ))} -// -// ); -// } - -// render(); - -// act(() => screen.getByTestId('1').focus()); -// expect(screen.getByTestId('1')).to.have.attribute('data-active'); - -// fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('78')).to.have.attribute('data-active'); -// expect(screen.getByTestId('78')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('78')).toHaveFocus(); -// }); - -// test('grid flow: dense', async () => { -// function App() { -// return ( -// // 1 to 9 numpad, but 2, 3, 5 and 6 are one big button, and so are 7 and 8. -// // 9 is missing -// -// {['1', '2356', '78', '4'].map((i) => ( -// -// {i} -// -// ))} -// -// ); -// } - -// render(); - -// act(() => screen.getByTestId('1').focus()); -// expect(screen.getByTestId('1')).to.have.attribute('data-active'); - -// fireEvent.keyDown(screen.getByTestId('1'), { key: 'ArrowDown' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('4')).to.have.attribute('data-active'); -// expect(screen.getByTestId('4')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('4')).toHaveFocus(); - -// fireEvent.keyDown(screen.getByTestId('4'), { key: 'ArrowDown' }); -// await flushMicrotasks(); -// expect(screen.getByTestId('78')).to.have.attribute('data-active'); -// expect(screen.getByTestId('78')).to.have.attribute('tabindex', '0'); -// expect(screen.getByTestId('78')).toHaveFocus(); -// }); -// }); -// }); diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.tsx b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx index 59b607f7ed..9876177ee7 100644 --- a/packages/mui-base/src/Composite/Root/CompositeRoot.tsx +++ b/packages/mui-base/src/Composite/Root/CompositeRoot.tsx @@ -2,16 +2,17 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; -import { CompositeList } from '../utils/CompositeList/CompositeList'; -import type { CompositeRootProps } from './CompositeRoot.types'; +import { CompositeList } from '../List/CompositeList'; import { useCompositeRoot } from './useCompositeRoot'; -import { CompositeRootContext, type CompositeRootContextValue } from './CompositeRootContext'; +import { CompositeRootContext } from './CompositeRootContext'; +import type { BaseUIComponentProps } from '../../utils/types'; +import type { Dimensions } from '../composite'; /** * @ignore - internal component. */ const CompositeRoot = React.forwardRef(function CompositeRoot( - props: CompositeRootProps, + props: CompositeRoot.Props, forwardedRef: React.ForwardedRef, ) { const { @@ -38,7 +39,7 @@ const CompositeRoot = React.forwardRef(function CompositeRoot( extraProps: otherProps, }); - const contextValue: CompositeRootContextValue = React.useMemo( + const contextValue: CompositeRootContext.Value = React.useMemo( () => ({ activeIndex, onActiveIndexChange }), [activeIndex, onActiveIndexChange], ); @@ -50,6 +51,20 @@ const CompositeRoot = React.forwardRef(function CompositeRoot( ); }); +namespace CompositeRoot { + export interface OwnerState {} + + export interface Props extends BaseUIComponentProps<'div', OwnerState> { + orientation?: 'horizontal' | 'vertical' | 'both'; + cols?: number; + loop?: boolean; + activeIndex?: number; + onActiveIndexChange?: (index: number) => void; + itemSizes?: Dimensions[]; + dense?: boolean; + } +} + CompositeRoot.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ diff --git a/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts b/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts deleted file mode 100644 index 79accd8819..0000000000 --- a/packages/mui-base/src/Composite/Root/CompositeRoot.types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { BaseUIComponentProps } from '../../utils/types'; -import type { Dimensions } from '../composite'; - -export type CompositeRootOwnerState = {}; - -export interface CompositeRootProps extends BaseUIComponentProps<'div', CompositeRootOwnerState> { - orientation?: 'horizontal' | 'vertical' | 'both'; - cols?: number; - loop?: boolean; - activeIndex?: number; - onActiveIndexChange?: (index: number) => void; - itemSizes?: Dimensions[]; - dense?: boolean; -} diff --git a/packages/mui-base/src/Composite/Root/CompositeRootContext.ts b/packages/mui-base/src/Composite/Root/CompositeRootContext.ts index f97f3f35bd..f8b119b05c 100644 --- a/packages/mui-base/src/Composite/Root/CompositeRootContext.ts +++ b/packages/mui-base/src/Composite/Root/CompositeRootContext.ts @@ -1,11 +1,6 @@ import * as React from 'react'; -export interface CompositeRootContextValue { - activeIndex: number; - onActiveIndexChange: (index: number) => void; -} - -export const CompositeRootContext = React.createContext(null); +export const CompositeRootContext = React.createContext(null); export function useCompositeRootContext() { const context = React.useContext(CompositeRootContext); @@ -14,3 +9,10 @@ export function useCompositeRootContext() { } return context; } + +export namespace CompositeRootContext { + export interface Value { + activeIndex: number; + onActiveIndexChange: (index: number) => void; + } +} diff --git a/packages/mui-base/src/Composite/utils/CompositeList/CompositeList.types.ts b/packages/mui-base/src/Composite/utils/CompositeList/CompositeList.types.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/mui-base/src/Radio/Indicator/RadioIndicator.tsx b/packages/mui-base/src/Radio/Indicator/RadioIndicator.tsx index e73cb06138..df5b0d9c5c 100644 --- a/packages/mui-base/src/Radio/Indicator/RadioIndicator.tsx +++ b/packages/mui-base/src/Radio/Indicator/RadioIndicator.tsx @@ -34,6 +34,14 @@ const RadioIndicator = React.forwardRef(function RadioIndicator( return renderElement(); }); +namespace RadioIndicator { + export interface Props extends BaseUIComponentProps<'span', OwnerState> {} + + export interface OwnerState { + checked: boolean; + } +} + RadioIndicator.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ @@ -54,11 +62,3 @@ RadioIndicator.propTypes /* remove-proptypes */ = { } as any; export { RadioIndicator }; - -namespace RadioIndicator { - export interface Props extends BaseUIComponentProps<'span', OwnerState> {} - - export interface OwnerState { - checked: boolean; - } -} diff --git a/packages/mui-base/src/Radio/Root/RadioRoot.tsx b/packages/mui-base/src/Radio/Root/RadioRoot.tsx index bbcb87c33b..c872fe81a9 100644 --- a/packages/mui-base/src/Radio/Root/RadioRoot.tsx +++ b/packages/mui-base/src/Radio/Root/RadioRoot.tsx @@ -78,6 +78,37 @@ const RadioRoot = React.forwardRef(function RadioRoot( ); }); +namespace RadioRoot { + export interface Props extends BaseUIComponentProps<'button', OwnerState> { + /** + * The unique identifying value of the radio in a group. + */ + value: string | number; + /** + * Determines if the radio is disabled. + * @default false + */ + disabled?: boolean; + /** + * Determines if the radio is required. + * @default false + */ + required?: boolean; + /** + * Determines if the radio is readonly. + * @default false + */ + readOnly?: boolean; + } + + export interface OwnerState { + checked: boolean; + disabled: boolean; + readOnly: boolean; + required: boolean; + } +} + RadioRoot.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ @@ -117,34 +148,3 @@ RadioRoot.propTypes /* remove-proptypes */ = { } as any; export { RadioRoot }; - -namespace RadioRoot { - export interface Props extends BaseUIComponentProps<'button', OwnerState> { - /** - * The unique identifying value of the radio in a group. - */ - value: string | number; - /** - * Determines if the radio is disabled. - * @default false - */ - disabled?: boolean; - /** - * Determines if the radio is required. - * @default false - */ - required?: boolean; - /** - * Determines if the radio is readonly. - * @default false - */ - readOnly?: boolean; - } - - export interface OwnerState { - checked: boolean; - disabled: boolean; - readOnly: boolean; - required: boolean; - } -} diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx index 006e1569ec..6b40887857 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx @@ -154,7 +154,7 @@ describe('', () => { expect(group.nextElementSibling).to.have.attribute('name', 'radio-group'); }); - it('should include the checkbox value in the form submission', function test() { + it('should include the checkbox value in the form submission', async function test() { if (isJSDOM) { // FormData is not available in JSDOM this.skip(); @@ -182,12 +182,15 @@ describe('', () => { const [radioA] = screen.getAllByRole('radio'); const submitButton = screen.getByRole('button'); - fireEvent.click(submitButton); + submitButton.click(); expect(stringifiedFormData).to.equal(''); - fireEvent.click(radioA); - fireEvent.click(submitButton); + await act(() => { + radioA.click(); + }); + + submitButton.click(); expect(stringifiedFormData).to.equal('group=a'); }); diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx index 9c44d79626..a1084e1161 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.tsx @@ -70,6 +70,48 @@ const RadioGroupRoot = React.forwardRef(function RadioGroupRoot( ); }); +namespace RadioGroupRoot { + export interface OwnerState { + disabled: boolean | undefined; + readOnly: boolean | undefined; + } + + export interface Props + extends Omit, 'value' | 'defaultValue'> { + /** + * Determines if the radio group is disabled. + * @default false + */ + disabled?: boolean; + /** + * Determines if the radio group is readonly. + * @default false + */ + readOnly?: boolean; + /** + * Determines if the radio group is required. + * @default false + */ + required?: boolean; + /** + * The name of the radio group submitted with the form data. + */ + name?: string; + /** + * The value of the selected radio button. Use when controlled. + */ + value?: string | number; + /** + * The default value of the selected radio button. Use when uncontrolled. + */ + defaultValue?: string | number; + /** + * Callback fired when the value changes. + */ + onValueChange?: (value: string | number, event: React.ChangeEvent) => void; + } +} + RadioGroupRoot.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ @@ -121,45 +163,3 @@ RadioGroupRoot.propTypes /* remove-proptypes */ = { } as any; export { RadioGroupRoot }; - -namespace RadioGroupRoot { - export interface OwnerState { - disabled: boolean | undefined; - readOnly: boolean | undefined; - } - - export interface Props - extends Omit, 'value' | 'defaultValue'> { - /** - * Determines if the radio group is disabled. - * @default false - */ - disabled?: boolean; - /** - * Determines if the radio group is readonly. - * @default false - */ - readOnly?: boolean; - /** - * Determines if the radio group is required. - * @default false - */ - required?: boolean; - /** - * The name of the radio group submitted with the form data. - */ - name?: string; - /** - * The value of the selected radio button. Use when controlled. - */ - value?: string | number; - /** - * The default value of the selected radio button. Use when uncontrolled. - */ - defaultValue?: string | number; - /** - * Callback fired when the value changes. - */ - onValueChange?: (value: string | number, event: React.ChangeEvent) => void; - } -} From be34d0c89501d4b5f5e4e20c56d5fc4e6e67f203 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 14 Aug 2024 16:24:18 +1000 Subject: [PATCH 41/47] Test composite navigation directly --- .../RadioGroup/Root/RadioGroupRoot.test.tsx | 78 ++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx index 6b40887857..3657c47c53 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx @@ -154,7 +154,7 @@ describe('', () => { expect(group.nextElementSibling).to.have.attribute('name', 'radio-group'); }); - it('should include the checkbox value in the form submission', async function test() { + it('should include the radio value in the form submission', async function test() { if (isJSDOM) { // FormData is not available in JSDOM this.skip(); @@ -195,7 +195,7 @@ describe('', () => { expect(stringifiedFormData).to.equal('group=a'); }); - it('should automatically select item upon navigation', async () => { + it('should automatically select radio upon navigation', async () => { render( @@ -219,4 +219,78 @@ describe('', () => { expect(b).toHaveFocus(); expect(b).to.have.attribute('aria-checked', 'true'); }); + + it('should manage arrow key navigation', async () => { + render( +
      +
      , + ); + + const a = screen.getByTestId('a'); + const b = screen.getByTestId('b'); + const c = screen.getByTestId('c'); + const after = screen.getByTestId('after'); + + act(() => { + a.focus(); + }); + + expect(a).toHaveFocus(); + + await user.keyboard('{ArrowDown}'); + + expect(b).toHaveFocus(); + + await user.keyboard('{ArrowDown}'); + + expect(c).toHaveFocus(); + + await user.keyboard('{ArrowDown}'); + + expect(a).toHaveFocus(); + + await user.keyboard('{ArrowUp}'); + + expect(c).toHaveFocus(); + + await user.keyboard('{ArrowUp}'); + + expect(b).toHaveFocus(); + + await user.keyboard('{ArrowUp}'); + + expect(a).toHaveFocus(); + + await user.keyboard('{ArrowLeft}'); + + expect(c).toHaveFocus(); + + await user.keyboard('{ArrowRight}'); + + expect(a).toHaveFocus(); + + await user.tab(); + + expect(after).toHaveFocus(); + + await user.tab({ shift: true }); + + expect(a).toHaveFocus(); + + await user.keyboard('{ArrowLeft}'); + + expect(c).toHaveFocus(); + + await user.tab({ shift: true }); + await user.tab(); + + expect(c).toHaveFocus(); + }); }); From 94fcde0cadd2ce94d3cef87a61fa59403b6d02b0 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 14 Aug 2024 16:32:40 +1000 Subject: [PATCH 42/47] Fix form test --- packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx index 3657c47c53..a1cafaf64e 100644 --- a/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx +++ b/packages/mui-base/src/RadioGroup/Root/RadioGroupRoot.test.tsx @@ -184,7 +184,7 @@ describe('', () => { submitButton.click(); - expect(stringifiedFormData).to.equal(''); + expect(stringifiedFormData).to.equal('group='); await act(() => { radioA.click(); From e866d9d3a3268196d7600cfc2e760ecf49a03c5a Mon Sep 17 00:00:00 2001 From: atomiks Date: Tue, 20 Aug 2024 15:58:17 +1000 Subject: [PATCH 43/47] Integrate with Field --- .../Description/FieldDescription.test.tsx | 20 +++++ .../src/Field/Label/FieldLabel.test.tsx | 20 +++++ .../src/Field/Root/FieldRoot.test.tsx | 89 +++++++++++++++++++ .../mui-base/src/Radio/Root/RadioRoot.tsx | 14 +-- .../src/Radio/Root/RadioRootContext.ts | 18 ++-- .../mui-base/src/Radio/Root/useRadioRoot.tsx | 32 +++++-- .../src/RadioGroup/Root/RadioGroupRoot.tsx | 33 ++++--- .../RadioGroup/Root/RadioGroupRootContext.ts | 30 +++---- .../src/RadioGroup/Root/useRadioGroupRoot.ts | 85 ++++++++++++++++-- 9 files changed, 285 insertions(+), 56 deletions(-) diff --git a/packages/mui-base/src/Field/Description/FieldDescription.test.tsx b/packages/mui-base/src/Field/Description/FieldDescription.test.tsx index ae3e43f2a6..56ade9fbfe 100644 --- a/packages/mui-base/src/Field/Description/FieldDescription.test.tsx +++ b/packages/mui-base/src/Field/Description/FieldDescription.test.tsx @@ -4,6 +4,8 @@ import * as Checkbox from '@base_ui/react/Checkbox'; import * as Switch from '@base_ui/react/Switch'; import * as NumberField from '@base_ui/react/NumberField'; import * as Slider from '@base_ui/react/Slider'; +import * as RadioGroup from '@base_ui/react/RadioGroup'; +import * as Radio from '@base_ui/react/Radio'; import { createRenderer, screen } from '@mui/internal-test-utils'; import { expect } from 'chai'; import { describeConformance } from '../../../test/describeConformance'; @@ -104,5 +106,23 @@ describe('', () => { ); }); }); + + describe('RadioGroup', () => { + it('supports RadioGroup', () => { + render( + + + + + + , + ); + + expect(screen.getByTestId('description')).to.have.attribute( + 'id', + screen.getByRole('radiogroup').getAttribute('aria-describedby')!, + ); + }); + }); }); }); diff --git a/packages/mui-base/src/Field/Label/FieldLabel.test.tsx b/packages/mui-base/src/Field/Label/FieldLabel.test.tsx index 16a8c2bceb..c1cf8d6453 100644 --- a/packages/mui-base/src/Field/Label/FieldLabel.test.tsx +++ b/packages/mui-base/src/Field/Label/FieldLabel.test.tsx @@ -4,6 +4,8 @@ import * as Checkbox from '@base_ui/react/Checkbox'; import * as Switch from '@base_ui/react/Switch'; import * as NumberField from '@base_ui/react/NumberField'; import * as Slider from '@base_ui/react/Slider'; +import * as RadioGroup from '@base_ui/react/RadioGroup'; +import * as Radio from '@base_ui/react/Radio'; import { createRenderer, screen } from '@mui/internal-test-utils'; import { expect } from 'chai'; import { describeConformance } from '../../../test/describeConformance'; @@ -95,5 +97,23 @@ describe('', () => { ); }); }); + + describe('RadioGroup', () => { + it('supports RadioGroup', () => { + render( + + + + + + , + ); + + expect(screen.getByTestId('radio-group')).to.have.attribute( + 'aria-labelledby', + screen.getByTestId('label').id, + ); + }); + }); }); }); diff --git a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx index e293e8e46e..355eafa8a5 100644 --- a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx +++ b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx @@ -4,6 +4,9 @@ import * as Checkbox from '@base_ui/react/Checkbox'; import * as Switch from '@base_ui/react/Switch'; import * as NumberField from '@base_ui/react/NumberField'; import * as Slider from '@base_ui/react/Slider'; +import * as RadioGroup from '@base_ui/react/RadioGroup'; +import * as Radio from '@base_ui/react/Radio'; +import userEvent from '@testing-library/user-event'; import { act, createRenderer, @@ -221,6 +224,7 @@ describe('', () => { , ); + // eslint-disable-next-line testing-library/no-node-access const input = container.querySelector('input')!; const thumb = screen.getByTestId('thumb'); @@ -231,6 +235,27 @@ describe('', () => { expect(input).to.have.attribute('aria-invalid', 'true'); }); + + it('supports RadioGroup', () => { + render( + 'error'}> + + One + Two + + + , + ); + + const group = screen.getByTestId('group'); + + expect(group).not.to.have.attribute('aria-invalid'); + + fireEvent.focus(group); + fireEvent.blur(group); + + expect(group).to.have.attribute('aria-invalid', 'true'); + }); }); }); @@ -405,6 +430,50 @@ describe('', () => { expect(root).to.have.attribute('data-touched', 'true'); }); + + it('supports RadioGroup (click)', () => { + render( + + + + One + + Two + + , + ); + + const group = screen.getByTestId('group'); + const control = screen.getByTestId('control'); + + fireEvent.click(control); + + expect(group).to.have.attribute('data-touched', 'true'); + expect(control).to.have.attribute('data-touched', 'true'); + }); + + it('supports RadioGroup (blur)', async () => { + render( + + + + One + + Two + +