From 27023c0d3e5bac6349f9f006e259e0356da6d448 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 27 Apr 2020 15:03:51 +0200 Subject: [PATCH 1/7] withStyles -> makeStyles --- docs/src/modules/components/Demo.js | 203 ++++++++++++++-------------- 1 file changed, 103 insertions(+), 100 deletions(-) diff --git a/docs/src/modules/components/Demo.js b/docs/src/modules/components/Demo.js index cbf4104638c9be..7f6fe05039261d 100644 --- a/docs/src/modules/components/Demo.js +++ b/docs/src/modules/components/Demo.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import copy from 'clipboard-copy'; import { useSelector, useDispatch } from 'react-redux'; -import { withStyles, fade } from '@material-ui/core/styles'; +import { fade, makeStyles } from '@material-ui/core/styles'; import IconButton from '@material-ui/core/IconButton'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import { unstable_StrictModeCollapse as Collapse } from '@material-ui/core/Collapse'; @@ -42,102 +42,6 @@ function addHiddenInput(form, name, value) { form.appendChild(input); } -const styles = (theme) => ({ - root: { - marginBottom: 40, - marginLeft: -theme.spacing(2), - marginRight: -theme.spacing(2), - [theme.breakpoints.up('sm')]: { - padding: theme.spacing(0, 1), - marginLeft: 0, - marginRight: 0, - }, - }, - demo: { - position: 'relative', - outline: 0, - margin: 'auto', - display: 'flex', - justifyContent: 'center', - [theme.breakpoints.up('sm')]: { - borderRadius: theme.shape.borderRadius, - }, - '&:focus': { - outline: `2px dashed ${theme.palette.text.primary}`, - }, - }, - /* Isolate the demo with an outline. */ - demoBgOutlined: { - padding: theme.spacing(3), - border: `1px solid ${fade(theme.palette.action.active, 0.12)}`, - borderLeftWidth: 0, - borderRightWidth: 0, - [theme.breakpoints.up('sm')]: { - borderLeftWidth: 1, - borderRightWidth: 1, - }, - }, - /* Prepare the background to display an inner elevation. */ - demoBgTrue: { - padding: theme.spacing(3), - backgroundColor: theme.palette.background.level2, - }, - /* Make no difference between the demo and the markdown. */ - demoBgInline: { - // Maintain alignment with the markdown text - [theme.breakpoints.down('xs')]: { - padding: theme.spacing(3), - }, - }, - demoHiddenToolbar: { - paddingTop: theme.spacing(2), - [theme.breakpoints.up('sm')]: { - paddingTop: theme.spacing(3), - }, - }, - toolbar: { - display: 'none', - [theme.breakpoints.up('sm')]: { - display: 'flex', - flip: false, - top: 0, - right: theme.spacing(1), - height: theme.spacing(6), - }, - justifyContent: 'space-between', - }, - code: { - display: 'none', - padding: 0, - marginBottom: theme.spacing(1), - marginRight: 0, - [theme.breakpoints.up('sm')]: { - display: 'block', - }, - '& pre': { - overflow: 'auto', - lineHeight: 1.5, - margin: '0px !important', - maxHeight: 1000, - }, - }, - tooltip: { - zIndex: theme.zIndex.appBar - 1, - }, - anchorLink: { - marginTop: -64, // height of toolbar - position: 'absolute', - }, - initialFocus: { - position: 'absolute', - top: 0, - left: 0, - width: theme.spacing(4), - height: theme.spacing(4), - pointerEvents: 'none', - }, -}); - function getDemoName(location) { return location.replace(/(.+?)(\w+)\.\w+$$/, '$2'); } @@ -173,8 +77,108 @@ function useUniqueId(prefix) { return id ? `${prefix}${id}` : id; } +const useStyles = makeStyles( + (theme) => ({ + root: { + marginBottom: 40, + marginLeft: -theme.spacing(2), + marginRight: -theme.spacing(2), + [theme.breakpoints.up('sm')]: { + padding: theme.spacing(0, 1), + marginLeft: 0, + marginRight: 0, + }, + }, + demo: { + position: 'relative', + outline: 0, + margin: 'auto', + display: 'flex', + justifyContent: 'center', + [theme.breakpoints.up('sm')]: { + borderRadius: theme.shape.borderRadius, + }, + '&:focus': { + outline: `2px dashed ${theme.palette.text.primary}`, + }, + }, + /* Isolate the demo with an outline. */ + demoBgOutlined: { + padding: theme.spacing(3), + border: `1px solid ${fade(theme.palette.action.active, 0.12)}`, + borderLeftWidth: 0, + borderRightWidth: 0, + [theme.breakpoints.up('sm')]: { + borderLeftWidth: 1, + borderRightWidth: 1, + }, + }, + /* Prepare the background to display an inner elevation. */ + demoBgTrue: { + padding: theme.spacing(3), + backgroundColor: theme.palette.background.level2, + }, + /* Make no difference between the demo and the markdown. */ + demoBgInline: { + // Maintain alignment with the markdown text + [theme.breakpoints.down('xs')]: { + padding: theme.spacing(3), + }, + }, + demoHiddenToolbar: { + paddingTop: theme.spacing(2), + [theme.breakpoints.up('sm')]: { + paddingTop: theme.spacing(3), + }, + }, + toolbar: { + display: 'none', + [theme.breakpoints.up('sm')]: { + display: 'flex', + flip: false, + top: 0, + right: theme.spacing(1), + height: theme.spacing(6), + }, + justifyContent: 'space-between', + }, + code: { + display: 'none', + padding: 0, + marginBottom: theme.spacing(1), + marginRight: 0, + [theme.breakpoints.up('sm')]: { + display: 'block', + }, + '& pre': { + overflow: 'auto', + lineHeight: 1.5, + margin: '0px !important', + maxHeight: 1000, + }, + }, + tooltip: { + zIndex: theme.zIndex.appBar - 1, + }, + anchorLink: { + marginTop: -64, // height of toolbar + position: 'absolute', + }, + initialFocus: { + position: 'absolute', + top: 0, + left: 0, + width: theme.spacing(4), + height: theme.spacing(4), + pointerEvents: 'none', + }, + }), + { name: 'Demo' }, +); + function Demo(props) { - const { classes, demo, demoOptions, githubLocation } = props; + const { demo, demoOptions, githubLocation } = props; + const classes = useStyles(); const dispatch = useDispatch(); const t = useSelector((state) => state.options.t); const codeVariant = useSelector((state) => state.options.codeVariant); @@ -559,10 +563,9 @@ function Demo(props) { } Demo.propTypes = { - classes: PropTypes.object.isRequired, demo: PropTypes.object.isRequired, demoOptions: PropTypes.object.isRequired, githubLocation: PropTypes.string.isRequired, }; -export default withStyles(styles)(Demo); +export default Demo; From ccc21ebcdd313acf441bf341d4909adc58860a00 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 27 Apr 2020 15:32:58 +0200 Subject: [PATCH 2/7] Extract DemoToolbar --- docs/src/modules/components/Demo.js | 665 +++++++++++++++------------- 1 file changed, 361 insertions(+), 304 deletions(-) diff --git a/docs/src/modules/components/Demo.js b/docs/src/modules/components/Demo.js index 7f6fe05039261d..4dc29a93cce86a 100644 --- a/docs/src/modules/components/Demo.js +++ b/docs/src/modules/components/Demo.js @@ -77,6 +77,352 @@ function useUniqueId(prefix) { return id ? `${prefix}${id}` : id; } +const useDemoToolbarStyles = makeStyles( + (theme) => { + return { + root: { + display: 'none', + [theme.breakpoints.up('sm')]: { + display: 'flex', + flip: false, + top: 0, + right: theme.spacing(1), + height: theme.spacing(6), + }, + justifyContent: 'space-between', + }, + tooltip: { + zIndex: theme.zIndex.appBar - 1, + }, + }; + }, + { name: 'DemoToolbar' }, +); + +function DemoToolbar(props) { + const { + codeOpen, + codeVariant, + demo, + demoData, + demoHovered, + demoName, + demoOptions, + demoSourceId, + initialFocusRef, + onCodeOpenChange, + onResetDemoClick, + openDemoSource, + showPreview, + } = props; + + const classes = useDemoToolbarStyles(); + + const dispatch = useDispatch(); + const t = useSelector((state) => state.options.t); + + const handleCodeLanguageClick = (event, clickedCodeVariant) => { + if (codeVariant !== clickedCodeVariant) { + dispatch({ + type: ACTION_TYPES.OPTIONS_CHANGE, + payload: { + codeVariant: clickedCodeVariant, + }, + }); + } + }; + + const handleCodeSandboxClick = () => { + const demoConfig = getDemoConfig(demoData); + const parameters = compress({ + files: { + 'package.json': { + content: { + title: demoConfig.title, + description: demoConfig.description, + dependencies: demoConfig.dependencies, + devDependencies: { + 'react-scripts': 'latest', + ...demoConfig.devDependencies, + }, + main: demoConfig.main, + scripts: demoConfig.scripts, + }, + }, + ...Object.keys(demoConfig.files).reduce((files, name) => { + files[name] = { content: demoConfig.files[name] }; + return files; + }, {}), + }, + }); + + const form = document.createElement('form'); + form.method = 'POST'; + form.target = '_blank'; + form.action = 'https://codeSandbox.io/api/v1/sandboxes/define'; + addHiddenInput(form, 'parameters', parameters); + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); + }; + + const [anchorEl, setAnchorEl] = React.useState(null); + const handleMoreClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleMoreClose = () => { + setAnchorEl(null); + }; + + const [snackbarOpen, setSnackbarOpen] = React.useState(false); + const [snackbarMessage, setSnackbarMessage] = React.useState(undefined); + + const handleSnackbarClose = () => { + setSnackbarOpen(false); + }; + const handleCopyClick = async () => { + try { + await copy(demoData.raw); + setSnackbarMessage(t('copiedSource')); + setSnackbarOpen(true); + } finally { + handleMoreClose(); + } + }; + + const handleStackBlitzClick = () => { + const demoConfig = getDemoConfig(demoData); + const form = document.createElement('form'); + form.method = 'POST'; + form.target = '_blank'; + form.action = 'https://stackblitz.com/run'; + addHiddenInput(form, 'project[template]', 'javascript'); + addHiddenInput(form, 'project[title]', demoConfig.title); + addHiddenInput(form, 'project[description]', demoConfig.description); + addHiddenInput(form, 'project[dependencies]', JSON.stringify(demoConfig.dependencies)); + addHiddenInput(form, 'project[devDependencies]', JSON.stringify(demoConfig.devDependencies)); + Object.keys(demoConfig.files).forEach((key) => { + const value = demoConfig.files[key]; + addHiddenInput(form, `project[files][${key}]`, value); + }); + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); + handleMoreClose(); + }; + + const createHandleCodeSourceLink = (anchor) => async () => { + try { + await copy(`${window.location.href.split('#')[0]}#${anchor}`); + setSnackbarMessage(t('copiedSourceLink')); + setSnackbarOpen(true); + } finally { + handleMoreClose(); + } + }; + + const [sourceHintSeen, setSourceHintSeen] = React.useState(false); + React.useEffect(() => { + setSourceHintSeen(getCookie('sourceHintSeen')); + }, []); + const handleCodeOpenClick = () => { + document.cookie = `sourceHintSeen=true;path=/;max-age=31536000`; + onCodeOpenChange(); + setSourceHintSeen(true); + }; + + function handleResetFocusClick() { + initialFocusRef.current.focusVisible(); + } + + const showSourceHint = demoHovered && !sourceHintSeen; + + let showCodeLabel; + if (codeOpen) { + showCodeLabel = showPreview ? t('hideFullSource') : t('hideSource'); + } else { + showCodeLabel = showPreview ? t('showFullSource') : t('showSource'); + } + + const atLeastMediumViewport = useMediaQuery((theme) => theme.breakpoints.up('sm')); + + return ( + +
+ + +
+ + + + + + {demoOptions.hideEditButton ? null : ( + + + + + + )} + + + + + + + + + + + + + + + + + + + + + {t('viewGitHub')} + + {demoOptions.hideEditButton ? null : ( + + {t('stackblitz')} + + )} + + {t('copySourceLinkJS')} + + + {t('copySourceLinkTS')} + + +
+
+
+ +
+ ); +} + +DemoToolbar.propTypes = { + codeOpen: PropTypes.bool.isRequired, + codeVariant: PropTypes.string.isRequired, + demo: PropTypes.object.isRequired, + demoData: PropTypes.object.isRequired, + demoHovered: PropTypes.bool.isRequired, + demoName: PropTypes.string.isRequired, + demoOptions: PropTypes.object.isRequired, + demoSourceId: PropTypes.string, + initialFocusRef: PropTypes.shape({ current: PropTypes.object }).isRequired, + onCodeOpenChange: PropTypes.func.isRequired, + onResetDemoClick: PropTypes.func.isRequired, + openDemoSource: PropTypes.bool.isRequired, + showPreview: PropTypes.bool.isRequired, +}; + const useStyles = makeStyles( (theme) => ({ root: { @@ -131,17 +477,6 @@ const useStyles = makeStyles( paddingTop: theme.spacing(3), }, }, - toolbar: { - display: 'none', - [theme.breakpoints.up('sm')]: { - display: 'flex', - flip: false, - top: 0, - right: theme.spacing(1), - height: theme.spacing(6), - }, - justifyContent: 'space-between', - }, code: { display: 'none', padding: 0, @@ -157,9 +492,6 @@ const useStyles = makeStyles( maxHeight: 1000, }, }, - tooltip: { - zIndex: theme.zIndex.appBar - 1, - }, anchorLink: { marginTop: -64, // height of toolbar position: 'absolute', @@ -179,114 +511,15 @@ const useStyles = makeStyles( function Demo(props) { const { demo, demoOptions, githubLocation } = props; const classes = useStyles(); - const dispatch = useDispatch(); const t = useSelector((state) => state.options.t); const codeVariant = useSelector((state) => state.options.codeVariant); const demoData = getDemoData(codeVariant, demo, githubLocation); - const [sourceHintSeen, setSourceHintSeen] = React.useState(false); - React.useEffect(() => { - setSourceHintSeen(getCookie('sourceHintSeen')); - }, []); - const [demoHovered, setDemoHovered] = React.useState(false); const handleDemoHover = (event) => { setDemoHovered(event.type === 'mouseenter'); }; - const [snackbarOpen, setSnackbarOpen] = React.useState(false); - const [snackbarMessage, setSnackbarMessage] = React.useState(undefined); - - const handleSnackbarClose = () => { - setSnackbarOpen(false); - }; - - const handleCodeLanguageClick = (event, clickedCodeVariant) => { - if (codeVariant !== clickedCodeVariant) { - dispatch({ - type: ACTION_TYPES.OPTIONS_CHANGE, - payload: { - codeVariant: clickedCodeVariant, - }, - }); - } - }; - - const handleCodeSandboxClick = () => { - const demoConfig = getDemoConfig(demoData); - const parameters = compress({ - files: { - 'package.json': { - content: { - title: demoConfig.title, - description: demoConfig.description, - dependencies: demoConfig.dependencies, - devDependencies: { - 'react-scripts': 'latest', - ...demoConfig.devDependencies, - }, - main: demoConfig.main, - scripts: demoConfig.scripts, - }, - }, - ...Object.keys(demoConfig.files).reduce((files, name) => { - files[name] = { content: demoConfig.files[name] }; - return files; - }, {}), - }, - }); - - const form = document.createElement('form'); - form.method = 'POST'; - form.target = '_blank'; - form.action = 'https://codeSandbox.io/api/v1/sandboxes/define'; - addHiddenInput(form, 'parameters', parameters); - document.body.appendChild(form); - form.submit(); - document.body.removeChild(form); - }; - - const [anchorEl, setAnchorEl] = React.useState(null); - const handleMoreClick = (event) => { - setAnchorEl(event.currentTarget); - }; - - const handleMoreClose = () => { - setAnchorEl(null); - }; - - const handleCopyClick = async () => { - try { - await copy(demoData.raw); - setSnackbarMessage(t('copiedSource')); - setSnackbarOpen(true); - } finally { - handleMoreClose(); - } - }; - - const handleStackBlitzClick = () => { - const demoConfig = getDemoConfig(demoData); - const form = document.createElement('form'); - form.method = 'POST'; - form.target = '_blank'; - form.action = 'https://stackblitz.com/run'; - addHiddenInput(form, 'project[template]', 'javascript'); - addHiddenInput(form, 'project[title]', demoConfig.title); - addHiddenInput(form, 'project[description]', demoConfig.description); - addHiddenInput(form, 'project[dependencies]', JSON.stringify(demoConfig.dependencies)); - addHiddenInput(form, 'project[devDependencies]', JSON.stringify(demoConfig.devDependencies)); - Object.keys(demoConfig.files).forEach((key) => { - const value = demoConfig.files[key]; - addHiddenInput(form, `project[files][${key}]`, value); - }); - document.body.appendChild(form); - form.submit(); - document.body.removeChild(form); - handleMoreClose(); - }; - - const showSourceHint = demoHovered && !sourceHintSeen; const DemoComponent = demoData.Component; const demoName = getDemoName(demoData.githubLocation); const demoSandboxedStyle = React.useMemo( @@ -305,16 +538,6 @@ function Demo(props) { demoOptions.bg = true; } - const createHandleCodeSourceLink = (anchor) => async () => { - try { - await copy(`${window.location.href.split('#')[0]}#${anchor}`); - setSnackbarMessage(t('copiedSourceLink')); - setSnackbarOpen(true); - } finally { - handleMoreClose(); - } - }; - const [codeOpen, setCodeOpen] = React.useState(demoOptions.defaultCodeOpen || false); React.useEffect(() => { @@ -324,14 +547,6 @@ function Demo(props) { } }, [demoName]); - const handleCodeOpenClick = () => { - document.cookie = `sourceHintSeen=true;path=/;max-age=31536000`; - setCodeOpen((open) => !open); - setSourceHintSeen(setSourceHintSeen(true)); - }; - - const match = useMediaQuery((theme) => theme.breakpoints.up('sm')); - const jsx = getJsxPreview(demoData.raw || ''); const showPreview = !demoOptions.hideToolbar && @@ -339,22 +554,12 @@ function Demo(props) { jsx !== demoData.raw && jsx.split(/\n/).length <= 17; - let showCodeLabel; - if (codeOpen) { - showCodeLabel = showPreview ? t('hideFullSource') : t('hideSource'); - } else { - showCodeLabel = showPreview ? t('showFullSource') : t('showSource'); - } - const [demoKey, resetDemo] = React.useReducer((key) => key + 1, 0); const demoSourceId = useUniqueId(`demo-`); const openDemoSource = codeOpen || showPreview; const initialFocusRef = React.useRef(null); - function handleResetFocusClick() { - initialFocusRef.current.focusVisible(); - } return (
@@ -386,163 +591,21 @@ function Demo(props) {
{demoOptions.hideToolbar ? null : ( -
- - -
- - - - - - {demoOptions.hideEditButton ? null : ( - - - - - - )} - - - - - - - - - - - - - - - - - - - - - {t('viewGitHub')} - - {demoOptions.hideEditButton ? null : ( - - {t('stackblitz')} - - )} - - {t('copySourceLinkJS')} - - - {t('copySourceLinkTS')} - - -
-
-
+ setCodeOpen((open) => !open)} + onResetDemoClick={resetDemo} + openDemoSource={openDemoSource} + showPreview={showPreview} + /> )} -
); } From 66f538fecf687cc7d5580005f937ab4a5d24c576 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 27 Apr 2020 15:39:02 +0200 Subject: [PATCH 3/7] fix: use aria-controls were appropriate --- docs/src/modules/components/Demo.js | 8 ++------ docs/src/modules/components/HighlightedCode.js | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/src/modules/components/Demo.js b/docs/src/modules/components/Demo.js index 4dc29a93cce86a..6b1aee89c00668 100644 --- a/docs/src/modules/components/Demo.js +++ b/docs/src/modules/components/Demo.js @@ -248,12 +248,7 @@ function DemoToolbar(props) { return ( -
+
{ return prism(code.trim(), language); }, [code, language]); return ( - +
         
Date: Mon, 27 Apr 2020 15:49:02 +0200
Subject: [PATCH 4/7] add aria-controls to resetDemo

---
 docs/src/modules/components/Demo.js | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/docs/src/modules/components/Demo.js b/docs/src/modules/components/Demo.js
index 6b1aee89c00668..63f679094531db 100644
--- a/docs/src/modules/components/Demo.js
+++ b/docs/src/modules/components/Demo.js
@@ -105,6 +105,7 @@ function DemoToolbar(props) {
     codeVariant,
     demo,
     demoData,
+    demoId,
     demoHovered,
     demoName,
     demoOptions,
@@ -319,6 +320,7 @@ function DemoToolbar(props) {
             
             
                key + 1, 0);
 
-  const demoSourceId = useUniqueId(`demo-`);
+  const demoId = useUniqueId('demo-');
+  const demoSourceId = useUniqueId(`demoSource-`);
   const openDemoSource = codeOpen || showPreview;
 
   const initialFocusRef = React.useRef(null);
@@ -566,6 +570,7 @@ function Demo(props) {
           [classes.demoBgTrue]: demoOptions.bg === true,
           [classes.demoBgInline]: demoOptions.bg === 'inline',
         })}
+        id={demoId}
         onMouseEnter={handleDemoHover}
         onMouseLeave={handleDemoHover}
       >
@@ -593,6 +598,7 @@ function Demo(props) {
           demo={demo}
           demoData={demoData}
           demoHovered={demoHovered}
+          demoId={demoId}
           demoName={demoName}
           demoOptions={demoOptions}
           demoSourceId={demoSourceId}

From 7a069c2f9a0571210815b32ed981c987973b8b2d Mon Sep 17 00:00:00 2001
From: Sebastian Silbermann 
Date: Mon, 27 Apr 2020 16:27:52 +0200
Subject: [PATCH 5/7] Inline DemoLanguages

---
 docs/src/modules/components/Demo.js          | 56 ++++++++++++---
 docs/src/modules/components/DemoLanguages.js | 75 --------------------
 2 files changed, 48 insertions(+), 83 deletions(-)
 delete mode 100644 docs/src/modules/components/DemoLanguages.js

diff --git a/docs/src/modules/components/Demo.js b/docs/src/modules/components/Demo.js
index 63f679094531db..f998112d32b25c 100644
--- a/docs/src/modules/components/Demo.js
+++ b/docs/src/modules/components/Demo.js
@@ -8,6 +8,10 @@ import { fade, makeStyles } from '@material-ui/core/styles';
 import IconButton from '@material-ui/core/IconButton';
 import useMediaQuery from '@material-ui/core/useMediaQuery';
 import { unstable_StrictModeCollapse as Collapse } from '@material-ui/core/Collapse';
+import { unstable_StrictModeFade as Fade } from '@material-ui/core/Fade';
+import ToggleButton from '@material-ui/lab/ToggleButton';
+import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
+import { JavaScript as JavaScriptIcon, TypeScript as TypeScriptIcon } from '@material-ui/docs';
 import NoSsr from '@material-ui/core/NoSsr';
 import EditIcon from '@material-ui/icons/Edit';
 import CodeIcon from '@material-ui/icons/Code';
@@ -21,7 +25,6 @@ import RefreshIcon from '@material-ui/icons/Refresh';
 import ResetFocusIcon from '@material-ui/icons/CenterFocusWeak';
 import HighlightedCode from 'docs/src/modules/components/HighlightedCode';
 import DemoSandboxed from 'docs/src/modules/components/DemoSandboxed';
-import DemoLanguages from 'docs/src/modules/components/DemoLanguages';
 import getDemoConfig from 'docs/src/modules/utils/getDemoConfig';
 import getJsxPreview from 'docs/src/modules/utils/getJsxPreview';
 import { getCookie } from 'docs/src/modules/utils/helpers';
@@ -91,6 +94,12 @@ const useDemoToolbarStyles = makeStyles(
         },
         justifyContent: 'space-between',
       },
+      toggleButtonGroup: {
+        margin: '8px 0',
+      },
+      toggleButton: {
+        height: 32,
+      },
       tooltip: {
         zIndex: theme.zIndex.appBar - 1,
       },
@@ -122,6 +131,14 @@ function DemoToolbar(props) {
   const dispatch = useDispatch();
   const t = useSelector((state) => state.options.t);
 
+  const hasTSVariant = demo.rawTS;
+  const renderedCodeVariant = () => {
+    if (codeVariant === CODE_VARIANTS.TS && hasTSVariant) {
+      return CODE_VARIANTS.TS;
+    }
+    return CODE_VARIANTS.JS;
+  };
+
   const handleCodeLanguageClick = (event, clickedCodeVariant) => {
     if (codeVariant !== clickedCodeVariant) {
       dispatch({
@@ -251,13 +268,36 @@ function DemoToolbar(props) {
     
       
- + + + + + + + + + +
state.options.t); - - function renderedCodeVariant() { - if (codeVariant === CODE_VARIANTS.TS && hasTSVariant) { - return CODE_VARIANTS.TS; - } - return CODE_VARIANTS.JS; - } - - return ( - - - - - - - - - - - ); -} - -DemoLanguages.propTypes = { - classes: PropTypes.object.isRequired, - codeOpen: PropTypes.bool.isRequired, - codeVariant: PropTypes.string.isRequired, - demo: PropTypes.object.isRequired, - gaEventLabel: PropTypes.string.isRequired, - onLanguageClick: PropTypes.func, -}; - -export default withStyles(styles)(DemoLanguages); From b920524e933c0e6c8d3cf188a5e14ed704087eaf Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 27 Apr 2020 17:30:30 +0200 Subject: [PATCH 6/7] [docs] Implement keyboard navigation for demo toolbar --- docs/src/modules/components/Demo.js | 159 +++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 2 deletions(-) diff --git a/docs/src/modules/components/Demo.js b/docs/src/modules/components/Demo.js index f998112d32b25c..b5837463bacd01 100644 --- a/docs/src/modules/components/Demo.js +++ b/docs/src/modules/components/Demo.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import copy from 'clipboard-copy'; import { useSelector, useDispatch } from 'react-redux'; -import { fade, makeStyles } from '@material-ui/core/styles'; +import { fade, makeStyles, useTheme } from '@material-ui/core/styles'; import IconButton from '@material-ui/core/IconButton'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import { unstable_StrictModeCollapse as Collapse } from '@material-ui/core/Collapse'; @@ -108,6 +108,134 @@ const useDemoToolbarStyles = makeStyles( { name: 'DemoToolbar' }, ); +const alwaysTrue = () => true; + +/** + * + * @param {React.Ref[]} controlRefs + * @param {object} [options] + * @param {(index: number) => boolean} [options.isFocusableControl] In case certain controls become unfocusable + * @param {number} [options.defaultActiveIndex] + */ +function useToolbar(controlRefs, options = {}) { + const { defaultActiveIndex = 0, isFocusableControl = alwaysTrue } = options; + const [activeControlIndex, setActiveControlIndex] = React.useState(defaultActiveIndex); + + // TODO: do we need to do this during layout practically? It's technically + // a bit too late since we allow user interaction between layout and passive effects + React.useEffect(() => { + setActiveControlIndex((currentActiveControlIndex) => { + if (!isFocusableControl(currentActiveControlIndex)) { + return defaultActiveIndex; + } + return currentActiveControlIndex; + }); + }, [defaultActiveIndex, isFocusableControl]); + + // controlRefs.findIndex(controlRef => controlRef.current = element) + function findControlIndex(element) { + let controlIndex = -1; + controlRefs.forEach((controlRef, index) => { + if (controlRef.current === element) { + controlIndex = index; + } + }); + return controlIndex; + } + + function handleControlFocus(event) { + const nextActiveControlIndex = findControlIndex(event.target); + if (nextActiveControlIndex !== -1) { + setActiveControlIndex(nextActiveControlIndex); + } else { + // make sure DCE works + // eslint-disable-next-line no-lonely-if + if (process.env.NODE_ENV !== 'production') { + console.error( + 'Material-UI: The toolbar contains a focusable element that is not controlled by the toolbar. ' + + 'Make sure you have attached `getControlProps(index)` to every focusable element within this toolbar.', + ); + } + } + } + + let handleToolbarFocus; + if (process.env.NODE_ENV !== 'production') { + handleToolbarFocus = (event) => { + if (findControlIndex(event.target) === -1) { + console.error( + 'Material-UI: The toolbar contains a focusable element that is not controlled by the toolbar. ' + + 'Make sure you have attached `getControlProps(index)` to every focusable element within this toolbar.', + ); + } + }; + } + + const { direction } = useTheme(); + + function handleToolbarKeyDown(event) { + // We handle toolbars where controls can be hidden temporarily. + // When a control is hidden we can't move focus to it and have to exclude + // it from the order. + let currentFocusableControlIndex = -1; + const focusableControls = []; + controlRefs.forEach((controlRef, index) => { + const { current: control } = controlRef; + if (index === activeControlIndex) { + currentFocusableControlIndex = focusableControls.length; + } + if (control !== null && isFocusableControl(index)) { + focusableControls.push(control); + } + }); + + const prevControlKey = direction === 'ltr' ? 'ArrowLeft' : 'ArrowRight'; + const nextControlKey = direction === 'ltr' ? 'ArrowRight' : 'ArrowLeft'; + + let nextFocusableIndex = -1; + switch (event.key) { + case prevControlKey: + nextFocusableIndex = + (currentFocusableControlIndex - 1 + focusableControls.length) % focusableControls.length; + break; + case nextControlKey: + nextFocusableIndex = (currentFocusableControlIndex + 1) % focusableControls.length; + break; + case 'Home': + nextFocusableIndex = 0; + break; + case 'End': + nextFocusableIndex = focusableControls.length - 1; + break; + default: + break; + } + + if (nextFocusableIndex !== -1) { + event.preventDefault(); + focusableControls[nextFocusableIndex].focus(); + } + } + + function getControlProps(index) { + return { + onFocus: handleControlFocus, + ref: controlRefs[index], + tabIndex: index === activeControlIndex ? 0 : -1, + }; + } + + return { + getControlProps, + toolbarProps: { + // TODO: good opportunity to warn on missing `aria-label` + onFocus: handleToolbarFocus, + onKeyDown: handleToolbarKeyDown, + role: 'toolbar', + }, + }; +} + function DemoToolbar(props) { const { codeOpen, @@ -264,9 +392,28 @@ function DemoToolbar(props) { const atLeastMediumViewport = useMediaQuery((theme) => theme.breakpoints.up('sm')); + const controlRefs = [ + React.useRef(null), + React.useRef(null), + React.useRef(null), + React.useRef(null), + React.useRef(null), + React.useRef(null), + React.useRef(null), + React.useRef(null), + ]; + // if the code is not open we hide the first two language controls + const isFocusableControl = React.useCallback((index) => (codeOpen ? true : index >= 2), [ + codeOpen, + ]); + const { getControlProps, toolbarProps } = useToolbar(controlRefs, { + defaultActiveIndex: 2, + isFocusableControl, + }); + return ( -
+
@@ -293,6 +441,7 @@ function DemoToolbar(props) { data-ga-event-category="demo" data-ga-event-action="source-ts" data-ga-event-label={demoOptions.demo} + {...getControlProps(1)} > @@ -315,6 +464,7 @@ function DemoToolbar(props) { data-ga-event-action="expand" onClick={handleCodeOpenClick} color={demoHovered ? 'primary' : 'default'} + {...getControlProps(2)} > @@ -331,6 +481,7 @@ function DemoToolbar(props) { data-ga-event-label={demoOptions.demo} data-ga-event-action="codesandbox" onClick={handleCodeSandboxClick} + {...getControlProps(3)} > @@ -343,6 +494,7 @@ function DemoToolbar(props) { data-ga-event-label={demoOptions.demo} data-ga-event-action="copy" onClick={handleCopyClick} + {...getControlProps(4)} > @@ -354,6 +506,7 @@ function DemoToolbar(props) { data-ga-event-label={demoOptions.demo} data-ga-event-action="reset-focus" onClick={handleResetFocusClick} + {...getControlProps(5)} > @@ -366,6 +519,7 @@ function DemoToolbar(props) { data-ga-event-label={demoOptions.demo} data-ga-event-action="reset" onClick={onResetDemoClick} + {...getControlProps(6)} > @@ -375,6 +529,7 @@ function DemoToolbar(props) { aria-owns={anchorEl ? 'demo-menu-more' : undefined} aria-haspopup="true" aria-label={t('seeMore')} + {...getControlProps(7)} > From 0a0704cde5300e0ac76622e3e47ece034253211a Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Wed, 29 Apr 2020 21:13:14 +0200 Subject: [PATCH 7/7] Apply suggestions from code review Co-Authored-By: Olivier Tassinari --- docs/src/modules/components/Demo.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/modules/components/Demo.js b/docs/src/modules/components/Demo.js index b5837463bacd01..94aa3e284e4216 100644 --- a/docs/src/modules/components/Demo.js +++ b/docs/src/modules/components/Demo.js @@ -390,7 +390,7 @@ function DemoToolbar(props) { showCodeLabel = showPreview ? t('showFullSource') : t('showSource'); } - const atLeastMediumViewport = useMediaQuery((theme) => theme.breakpoints.up('sm')); + const atLeastSmallViewport = useMediaQuery((theme) => theme.breakpoints.up('sm')); const controlRefs = [ React.useRef(null), @@ -451,7 +451,7 @@ function DemoToolbar(props) {