diff --git a/.circleci/config.yml b/.circleci/config.yml index e8fd265bc9edab..ffebc6f31a5cc6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -161,6 +161,12 @@ jobs: - *restore_yarn_offline_mirror - *restore_yarn_cache - *install_js + - run: + name: Transpile TypeScript demos + command: yarn docs:typescript:formatted + - run: + name: Are the compiled TypeScript demos equivalent to the JavaScript demos? + command: git diff --exit-code - run: name: Can we generate the @material-ui/core build? command: cd packages/material-ui && yarn build diff --git a/.eslintignore b/.eslintignore index 68cab7b27ba9b9..e116eed0edf14f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,6 +6,7 @@ /examples/create-react-app-with-flow/flow /examples/create-react-app-with-flow/flow-typed /examples/gatsby/public +/packages/babel-plugin-unwrap-createStyles/test/__fixtures__/ /packages/material-ui-codemod/lib /packages/material-ui-codemod/src/*/*.test /packages/material-ui-codemod/src/*/*.test.js diff --git a/.size-limit.js b/.size-limit.js index d09b99535e6bca..f2ceb92d274eb0 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -74,7 +74,7 @@ module.exports = [ name: 'The main docs bundle', webpack: false, path: main.path, - limit: '191 KB', + limit: '193 KB', }, { name: 'The docs home page', diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 24cfa4f424988d..4b80468eb3f951 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,6 +50,8 @@ We will only accept a pull request for which all tests pass. Make sure the follo - If API documentation is being changed in the source, `yarn docs:api` was run. - If prop types were changed, the TypeScript declarations were updated. - If TypeScript declarations were changed, `yarn typescript` passed. +- If demos were changed, make sure `yarn docs:typescript:formatted` does not introduce changes. + See [About TypeScript demos](#about-typescript-demos). - The PR title follows the pattern `[Component] Imperative commit message`. (See: [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/#imperative) for a great explanation) ## Getting started @@ -80,6 +82,8 @@ yarn yarn docs:dev ``` You can now access the documentation site [locally](http://localhost:3000). +Changes to the docs will hot reload the site. If you make changes to TypeScript files +in the docs run `yarn docs:typescript --watch` in a separate terminal. Test coverage is limited at present, but where possible, please add tests for any changes you make. Tests can be run with `yarn test`. @@ -118,6 +122,14 @@ docs/src/pages/demos/buttons/ ``` And let's give it a name: `SuperButtons.js`. +We try to also document how to use this library with TypeScript. If you are familiar with +that language try writing that demo in TypeScript in a *.tsx file. When you're done +run `yarn docs:typescript:formatted` to automatically add a JavaScript version. + +Apart from the inherent pros and cons of TypeScript the demos are also used to test our +type declarations. This helps a lot in catching regressions when updating our type +declarations. + #### 2. Edit the page Markdown file. The Markdown file is the source for the website documentation. So, whatever you wrote there will be reflected on the website. @@ -155,6 +167,20 @@ Then, you will need to add the following code: In case you missed something, [we have a real example that can be used as a summary report]((https://github.com/mui-org/material-ui/pull/8922/files)). +### About TypeScript demos + +To help people use this library with TypeScript we try to provide equivalent demos +in TypeScript. + +Changing demos in JavaScript requires a manual update of the TypeScript +version. If you are not familiar with this language you can add the filepath +of the TS demo to `docs/ts-demo-ignore.json`. See `docs/babel.config.ts.js` for more +information. Otherwise our CI will fail the `test_build` job. +A contributor can later update the TypeScript version of that demo. + +If you are already familiar with TypeScript you can simply write the demo in TypeScript. +`yarn docs:typescript:formatted` will transpile it down to JavaScript. + ## How do I use my local distribution of material-ui in any project? Sometimes it is good to test your changes in a real world scenario, in order to do that you can install your local distribution of Material-UI in any project with [yarn link](https://yarnpkg.com/lang/en/docs/cli/link/). diff --git a/docs/babel.config.ts.js b/docs/babel.config.ts.js new file mode 100644 index 00000000000000..39c0751e420c9a --- /dev/null +++ b/docs/babel.config.ts.js @@ -0,0 +1,24 @@ +const path = require('path'); +const ignoredDemos = require('./ts-demo-ignore.json'); + +/** + * babel config to transpile tsx demos to js + * + * Can be used to spot differences between ts and js demos which might indicate that they + * do different things at runtime. + * + * Demos listed in ts-demo-ignore are not transpiled. Their path should be relative + * to `${workspaceRoot}/docs/src/pages/demos`. + */ + +const workspaceRoot = path.join(__dirname, '../'); +const ignore = ignoredDemos.map(demoPath => + path.join(workspaceRoot, 'docs/src/pages/demos', `${demoPath}.tsx`), +); + +module.exports = { + presets: ['@babel/preset-typescript'], + plugins: ['unwrap-createStyles'], + ignore, + generatorOpts: { retainLines: true }, +}; diff --git a/docs/scripts/formattedTSDemos.js b/docs/scripts/formattedTSDemos.js new file mode 100644 index 00000000000000..2a5998ce2ffba6 --- /dev/null +++ b/docs/scripts/formattedTSDemos.js @@ -0,0 +1,55 @@ +/** + * Transpiles and formats TS demos. + * Can be used to verify that JS and TS demos are equivalent. No introduced change + * would indicate equivalence. + */ +const childProcess = require('child_process'); +const fse = require('fs-extra'); +const path = require('path'); +const prettier = require('prettier'); +const util = require('util'); + +const exec = util.promisify(childProcess.exec); + +async function getUnstagedGitFiles() { + const { stdout } = await exec('git diff --name-only'); + const list = stdout.trim(); + + if (list === '') { + // "".split(" ") => [""] + return []; + } + + return list.split('\n'); +} + +function fixBabelGeneratorIssues(source) { + return source.replace(/,\n\n/g, ',\n'); +} + +exec('yarn docs:typescript') + .then(() => { + const prettierConfigPath = path.join(__dirname, '../../prettier.config.js'); + const prettierConfig = prettier.resolveConfig(process.cwd(), { config: prettierConfigPath }); + + return Promise.all([getUnstagedGitFiles(), prettierConfig]); + }) + .then(([changedDemos, prettierConfig]) => + Promise.all( + changedDemos.map(filename => { + const filepath = path.join(process.cwd(), filename); + + return fse.readFile(filepath).then(source => { + const prettified = prettier.format(source.toString(), { ...prettierConfig, filepath }); + const formatted = fixBabelGeneratorIssues(prettified); + + return fse.writeFile(filepath, formatted); + }); + }), + ), + ) + .catch(err => { + // eslint-disable-next-line no-console + console.error(err); + process.exit(1); + }); diff --git a/docs/src/modules/components/Demo.js b/docs/src/modules/components/Demo.js index 1ab5fd2fc11c2c..c1242a713a9b6a 100644 --- a/docs/src/modules/components/Demo.js +++ b/docs/src/modules/components/Demo.js @@ -15,11 +15,10 @@ import MenuItem from '@material-ui/core/MenuItem'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import Tooltip from '@material-ui/core/Tooltip'; import Github from '@material-ui/docs/svgIcons/GitHub'; -import JSLogo from '@material-ui/docs/svgIcons/JSLogo'; -import HookLogo from '@material-ui/docs/svgIcons/HookLogo'; import MarkdownElement from '@material-ui/docs/MarkdownElement'; -import { getDependencies } from 'docs/src/modules/utils/helpers'; import DemoFrame from 'docs/src/modules/components/DemoFrame'; +import DemoLanguages from 'docs/src/modules/components/DemoLanguages'; +import getDemoConfig from 'docs/src/modules/utils/getDemoConfig'; import { ACTION_TYPES, CODE_VARIANTS } from 'docs/src/modules/constants'; function compress(object) { @@ -37,55 +36,6 @@ function addHiddenInput(form, name, value) { form.appendChild(input); } -function getDemo(props, raw) { - const demo = { - title: 'Material demo', - description: props.githubLocation, - dependencies: getDependencies(raw, props.demoOptions.react), - files: { - 'demo.js': raw, - 'index.js': ` -import React from 'react'; -import ReactDOM from 'react-dom'; -import Demo from './demo'; - -ReactDOM.render(, document.querySelector('#root')); - `, - 'index.html': ` - - - -
- - `, - }, - }; - - if (props.codeVariant === CODE_VARIANTS.HOOK) { - demo.dependencies.react = 'next'; - demo.dependencies['react-dom'] = 'next'; - demo.dependencies['@material-ui/styles'] = 'latest'; - demo.files['index.js'] = ` -import React from 'react'; -import ReactDOM from 'react-dom'; -import Demo from './demo'; -import { createMuiTheme } from "@material-ui/core/styles"; -import { ThemeProvider } from "@material-ui/styles"; - -const theme = createMuiTheme({ typography: { useNextVariants: true } }); - -ReactDOM.render( - - - , - document.querySelector("#root") -); - `; - } - - return demo; -} - const styles = theme => ({ root: { position: 'relative', @@ -124,10 +74,10 @@ const styles = theme => ({ [theme.breakpoints.up('sm')]: { display: 'flex', flip: false, - position: 'absolute', top: 0, right: theme.spacing.unit, }, + justifyContent: 'space-between', }, code: { display: 'none', @@ -160,25 +110,26 @@ class Demo extends React.Component { }; handleClickCodeSandbox = () => { - const demo = getDemo(this.props, this.getDemoData().raw); + const demoConfig = getDemoConfig(this.getDemoData()); const parameters = compress({ files: { 'package.json': { content: { - title: demo.title, - description: demo.description, - dependencies: demo.dependencies, + title: demoConfig.title, + description: demoConfig.description, + dependencies: demoConfig.dependencies, + devDependencies: { + 'react-scripts': 'latest', + ...demoConfig.devDependencies, + }, + main: demoConfig.main, + scripts: demoConfig.scripts, }, }, - 'demo.js': { - content: demo.files['demo.js'], - }, - 'index.js': { - content: demo.files['index.js'], - }, - 'index.html': { - content: demo.files['index.html'], - }, + ...Object.keys(demoConfig.files).reduce((files, name) => { + files[name] = { content: demoConfig.files[name] }; + return files; + }, {}), }, }); @@ -201,8 +152,7 @@ class Demo extends React.Component { }; handleClickStackBlitz = () => { - const { codeVariant } = this.state; - const demo = getDemo(this.props, codeVariant); + const demo = getDemoConfig(this.getDemoData()); const form = document.createElement('form'); form.method = 'POST'; form.target = '_blank'; @@ -211,6 +161,7 @@ class Demo extends React.Component { addHiddenInput(form, 'project[title]', demo.title); addHiddenInput(form, 'project[description]', demo.description); addHiddenInput(form, 'project[dependencies]', JSON.stringify(demo.dependencies)); + addHiddenInput(form, 'project[devDependencies]', JSON.stringify(demo.devDependencies)); Object.keys(demo.files).forEach(key => { const value = demo.files[key]; addHiddenInput(form, `project[files][${key}]`, value); @@ -221,9 +172,7 @@ class Demo extends React.Component { this.handleCloseMore(); }; - handleCodeLanguageClick = event => { - const codeVariant = event.currentTarget.value; - + handleCodeLanguageClick = (event, codeVariant) => { if (this.props.codeVariant !== codeVariant) { document.cookie = `codeVariant=${codeVariant};path=/;max-age=31536000`; @@ -234,15 +183,6 @@ class Demo extends React.Component { }, }); } - - this.setState(prevState => ({ - /** - * if the the same code type is open, - * toggle the state, otherwise if it is - * another code type always open it. i.e, true - */ - codeOpen: this.props.codeVariant === codeVariant ? !prevState.codeOpen : true, - })); }; handleClickCodeOpen = () => { @@ -252,137 +192,133 @@ class Demo extends React.Component { }; getDemoData = () => { - const { codeVariant, demo } = this.props; - return codeVariant === CODE_VARIANTS.HOOK && demo.rawHooks - ? { - codeVariant: CODE_VARIANTS.HOOK, - raw: demo.rawHooks, - js: demo.jsHooks, - } - : { - codeVariant: CODE_VARIANTS.JS, - js: demo.js, - raw: demo.raw, - }; + const { codeVariant, demo, githubLocation } = this.props; + if (codeVariant === CODE_VARIANTS.HOOK && demo.rawHooks) { + return { + codeVariant: CODE_VARIANTS.HOOK, + githubLocation: githubLocation.replace(/\.jsx?$/, '.hooks.js'), + raw: demo.rawHooks, + js: demo.jsHooks, + }; + } + if (codeVariant === CODE_VARIANTS.TS && demo.rawTS) { + return { + codeVariant: CODE_VARIANTS.TS, + githubLocation: githubLocation.replace(/\.js$/, '.tsx'), + raw: demo.rawTS, + js: demo.js, + }; + } + + return { + codeVariant: CODE_VARIANTS.JS, + githubLocation, + raw: demo.raw, + js: demo.js, + }; }; render() { - const { classes, demo, demoOptions, githubLocation: githubLocationJS } = this.props; + const { classes, codeVariant, demo, demoOptions } = this.props; const { anchorEl, codeOpen } = this.state; const category = demoOptions.demo; const demoData = this.getDemoData(); const DemoComponent = demoData.js; - const githubLocation = - demoData.codeVariant === CODE_VARIANTS.HOOK - ? githubLocationJS.replace(/\.jsx?$/, '.hooks.js') - : githubLocationJS; + const sourceLanguage = demoData.codeVariant === CODE_VARIANTS.TS ? 'tsx' : 'jsx'; return (
{demoOptions.hideHeader ? null : (
- {demo.rawHooks && ( - + +
+ - + - )} - {demo.rawHooks && ( - + - + - )} - - - - - - + {demoOptions.hideEditButton ? null : ( + + + + + + )} - + - - {demoOptions.hideEditButton ? null : ( - - - - - - )} - - - - - - Copy the source - - {demoOptions.hideEditButton ? null : ( - Edit in StackBlitz + Copy the source - )} - + {demoOptions.hideEditButton ? null : ( + + Edit in StackBlitz (JS only) + + )} + +
diff --git a/docs/src/modules/components/DemoLanguages.js b/docs/src/modules/components/DemoLanguages.js new file mode 100644 index 00000000000000..7ea2b50b1f65f2 --- /dev/null +++ b/docs/src/modules/components/DemoLanguages.js @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Fade from '@material-ui/core/Fade'; +import ToggleButton from '@material-ui/lab/ToggleButton'; +import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup'; +import JavascriptIcon from '@material-ui/docs/svgIcons/Javascript'; +import TypescriptIcon from '@material-ui/docs/svgIcons/Typescript'; +import HooksIcon from '@material-ui/docs/svgIcons/Hooks'; +import { CODE_VARIANTS } from 'docs/src/modules/constants'; + +function DemoLanguages(props) { + const { codeOpen, codeVariant, demo, gaEventCategory, onLanguageClick } = props; + const hasHooksVariant = demo.rawHooks; + const hasTSVariant = demo.rawTS; + + function renderedCodeVariant() { + if (codeVariant === CODE_VARIANTS.TS && hasTSVariant) { + return CODE_VARIANTS.TS; + } + if (codeVariant === CODE_VARIANTS.HOOK && hasHooksVariant) { + return CODE_VARIANTS.HOOK; + } + return CODE_VARIANTS.JS; + } + + return ( + +
+ + + + + + + + + + + +
+
+ ); +} + +DemoLanguages.propTypes = { + codeOpen: PropTypes.bool.isRequired, + codeVariant: PropTypes.string.isRequired, + demo: PropTypes.object.isRequired, + gaEventCategory: PropTypes.string.isRequired, + onLanguageClick: PropTypes.func, +}; + +export default DemoLanguages; diff --git a/docs/src/modules/components/MarkdownDocs.js b/docs/src/modules/components/MarkdownDocs.js index 0913f14d5ab25f..3f358b1c984439 100644 --- a/docs/src/modules/components/MarkdownDocs.js +++ b/docs/src/modules/components/MarkdownDocs.js @@ -58,6 +58,7 @@ function MarkdownDocs(props) { if (req) { demos = {}; const markdowns = {}; + const sourceFiles = reqSource.keys(); req.keys().forEach(filename => { if (filename.indexOf('.md') !== -1) { const match = filename.match(/-([a-z]{2})\.md$/); @@ -72,10 +73,20 @@ function MarkdownDocs(props) { const isHooks = filename.indexOf('.hooks.js') !== -1; const jsType = isHooks ? 'jsHooks' : 'js'; const rawType = isHooks ? 'rawHooks' : 'raw'; + + const tsFilename = !isHooks + ? sourceFiles.find(sourceFileName => { + const isTSSourceFile = /\.tsx$/.test(sourceFileName); + const isTSVersionOfFile = sourceFileName.replace(/\.tsx$/, '.js') === filename; + return isTSSourceFile && isTSVersionOfFile; + }) + : undefined; + demos[demoName] = { - ...(demos[demoName] ? demos[demoName] : {}), + ...demos[demoName], [jsType]: req(filename).default, [rawType]: reqSource(filename), + rawTS: tsFilename ? reqSource(tsFilename) : undefined, }; } }); diff --git a/docs/src/modules/constants.js b/docs/src/modules/constants.js index 91115c75898c47..37865f5d85941a 100644 --- a/docs/src/modules/constants.js +++ b/docs/src/modules/constants.js @@ -1,5 +1,6 @@ export const CODE_VARIANTS = { JS: 'JS', + TS: 'TS', HOOK: 'HOOK', }; diff --git a/docs/src/modules/utils/getDemoConfig.js b/docs/src/modules/utils/getDemoConfig.js new file mode 100644 index 00000000000000..6c9197a99d3305 --- /dev/null +++ b/docs/src/modules/utils/getDemoConfig.js @@ -0,0 +1,111 @@ +import { getDependencies } from './helpers'; +import { CODE_VARIANTS } from 'docs/src/modules/constants'; + +function jsDemo(demoData) { + return { + dependencies: getDependencies(demoData.raw), + files: { + 'demo.js': demoData.raw, + 'index.js': ` +import React from 'react'; +import ReactDOM from 'react-dom'; +import Demo from './demo'; + +ReactDOM.render(, document.querySelector('#root')); + `, + }, + }; +} + +function hooksDemo(demoData) { + return { + dependencies: getDependencies(demoData.rawHooks, { reactVersion: 'next' }), + files: { + 'index.js': ` + import React from 'react'; + import ReactDOM from 'react-dom'; + import Demo from './demo'; + import { createMuiTheme } from "@material-ui/core/styles"; + import { ThemeProvider } from "@material-ui/styles"; + + const theme = createMuiTheme({ typography: { useNextVariants: true } }); + + ReactDOM.render( + + + , + document.querySelector("#root") + ); + `, + 'demo.js': demoData.raw, + }, + }; +} + +function tsDemo(demoData) { + return { + dependencies: getDependencies(demoData.raw, { codeLanguage: 'TS' }), + files: { + 'demo.tsx': demoData.raw, + 'index.tsx': ` +import React from 'react'; +import ReactDOM from 'react-dom'; +import Demo from './demo'; + +ReactDOM.render(, document.querySelector('#root')); + `, + 'tsconfig.json': `{ + "compilerOptions": { + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom"], + "sourceMap": true, + "jsx": "react", + "strict": true, + "esModuleInterop": true + } + }`, + }, + main: 'index.tsx', + scripts: { + start: 'react-scripts start', + }, + }; +} + +function getLanguageConfig(demoData) { + switch (demoData.codeVariant) { + case CODE_VARIANTS.TS: + return tsDemo(demoData); + case CODE_VARIANTS.HOOKS: + return hooksDemo(demoData); + default: + return jsDemo(demoData); + } +} + +export default function getDemo(demoData) { + const baseConfig = { + title: 'Material demo', + description: demoData.githubLocation, + files: { + 'index.html': ` + + + +
+ + `, + }, + }; + const languageConfig = getLanguageConfig(demoData); + + return { + ...baseConfig, + ...languageConfig, + files: { + ...baseConfig.files, + ...languageConfig.files, + }, + }; +} diff --git a/docs/src/modules/utils/helpers.js b/docs/src/modules/utils/helpers.js index 7c8d4004855d82..254cf8f3cda5ce 100644 --- a/docs/src/modules/utils/helpers.js +++ b/docs/src/modules/utils/helpers.js @@ -28,7 +28,51 @@ export function pageToTitle(page) { return titleize(name); } -export function getDependencies(raw, reactVersion = 'latest') { +/** + * @var + * set of packages that ship their own typings instead of using @types/ namespace + * Array because Set([iterable]) is not supported in IE11 + */ +const packagesWithBundledTypes = ['@material-ui/core', '@material-ui/lab']; + +/** + * WARNING: Always uses `latest` typings. + * + * Adds dependencies to @types packages only for packages that are not listed + * in packagesWithBundledTypes + * + * @see packagesWithBundledTypes in this module namespace + * + * @param {Record} deps - list of dependency as `name => version` + */ +function addTypeDeps(deps) { + const packagesWithDTPackage = Object.keys(deps).filter( + name => packagesWithBundledTypes.indexOf(name) === -1, + ); + + 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'; + }); + + return deps; +} + +/** + * @param {string} raw - ES6 source with es module imports + * @param {objects} options + * @param {'JS' | 'TS'} options.codeLanguage + * @param {'next' | 'latest'} options.reactVersion + * @returns {Record} map of packages with their required version + */ +export function getDependencies(raw, options = {}) { + const { codeLanguage = 'JS', reactVersion = 'latest' } = options; const deps = { 'react-dom': reactVersion, react: reactVersion, @@ -54,6 +98,11 @@ export function getDependencies(raw, reactVersion = 'latest') { deps[name] = versions[name] ? versions[name] : 'latest'; } } + + if (codeLanguage === 'TS') { + addTypeDeps(deps); + } + return deps; } diff --git a/docs/src/modules/utils/helpers.test.js b/docs/src/modules/utils/helpers.test.js index 7e654f497a4348..38ed1e91fd69fb 100644 --- a/docs/src/modules/utils/helpers.test.js +++ b/docs/src/modules/utils/helpers.test.js @@ -87,4 +87,18 @@ import { MuiPickersUtilsProvider, TimePicker, DatePicker } from 'material-ui-pic react: 'latest', }); }); + + it('can collect required @types packages', () => { + assert.deepEqual(getDependencies(s1, 'TS'), { + '@foo-bar/bip': 'latest', + '@material-ui/core': 'latest', + 'prop-types': 'latest', + 'react-dom': 'latest', + react: 'latest', + '@types/foo-bar__bip': 'latest', + '@types/prop-types': 'latest', + '@types/react-dom': 'latest', + '@types/react': 'latest', + }); + }); }); diff --git a/docs/src/pages/demos/app-bar/BottomAppBar.tsx b/docs/src/pages/demos/app-bar/BottomAppBar.tsx new file mode 100644 index 00000000000000..f2b34cc86143ff --- /dev/null +++ b/docs/src/pages/demos/app-bar/BottomAppBar.tsx @@ -0,0 +1,154 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { createStyles, Theme, withStyles, WithStyles } from '@material-ui/core/styles'; +import AppBar from '@material-ui/core/AppBar'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import IconButton from '@material-ui/core/IconButton'; +import Paper from '@material-ui/core/Paper'; +import Fab from '@material-ui/core/Fab'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemText from '@material-ui/core/ListItemText'; +import ListSubheader from '@material-ui/core/ListSubheader'; +import Avatar from '@material-ui/core/Avatar'; +import MenuIcon from '@material-ui/icons/Menu'; +import AddIcon from '@material-ui/icons/Add'; +import SearchIcon from '@material-ui/icons/Search'; +import MoreIcon from '@material-ui/icons/MoreVert'; + +const styles = (theme: Theme) => + createStyles({ + text: { + paddingTop: theme.spacing.unit * 2, + paddingLeft: theme.spacing.unit * 2, + paddingRight: theme.spacing.unit * 2, + }, + paper: { + paddingBottom: 50, + }, + list: { + marginBottom: theme.spacing.unit * 2, + }, + subHeader: { + backgroundColor: theme.palette.background.paper, + }, + appBar: { + top: 'auto', + bottom: 0, + }, + toolbar: { + alignItems: 'center', + justifyContent: 'space-between', + }, + fabButton: { + position: 'absolute', + zIndex: 1, + top: -30, + left: 0, + right: 0, + margin: '0 auto', + }, + }); + +const messages = [ + { + id: 1, + primary: 'Brunch this week?', + secondary: "I'll be in the neighbourhood this week. Let's grab a bite to eat", + person: '/static/images/avatar/5.jpg', + }, + { + id: 2, + primary: 'Birthday Gift', + secondary: `Do you have a suggestion for a good present for John on his work + anniversary. I am really confused & would love your thoughts on it.`, + person: '/static/images/avatar/1.jpg', + }, + { + id: 3, + primary: 'Recipe to try', + secondary: 'I am try out this new BBQ recipe, I think this might be amazing', + person: '/static/images/avatar/2.jpg', + }, + { + id: 4, + primary: 'Yes!', + secondary: 'I have the tickets to the ReactConf for this year.', + person: '/static/images/avatar/3.jpg', + }, + { + id: 5, + primary: "Doctor's Appointment", + secondary: 'My appointment for the doctor was rescheduled for next Saturday.', + person: '/static/images/avatar/4.jpg', + }, + { + id: 6, + primary: 'Discussion', + secondary: `Menus that are generated by the bottom app bar (such as a bottom + navigation drawer or overflow menu) open as bottom sheets at a higher elevation + than the bar.`, + person: '/static/images/avatar/5.jpg', + }, + { + id: 7, + primary: 'Summer BBQ', + secondary: `Who wants to have a cookout this weekend? I just got some furniture + for my backyard and would love to fire up the grill.`, + person: '/static/images/avatar/1.jpg', + }, +]; + +export interface Props extends WithStyles {} + +function BottomAppBar(props: Props) { + const { classes } = props; + return ( + + + + + Inbox + + + {messages.map(({ id, primary, secondary, person }) => ( + + {id === 1 && Today} + {id === 3 && Yesterday} + + + + + + ))} + + + + + + + + + + +
+ + + + + + +
+
+
+
+ ); +} + +BottomAppBar.propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(BottomAppBar); diff --git a/docs/src/pages/demos/app-bar/ButtonAppBar.tsx b/docs/src/pages/demos/app-bar/ButtonAppBar.tsx new file mode 100644 index 00000000000000..f8baad446bb5a9 --- /dev/null +++ b/docs/src/pages/demos/app-bar/ButtonAppBar.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { createStyles, withStyles, WithStyles } from '@material-ui/core/styles'; +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; +import MenuIcon from '@material-ui/icons/Menu'; + +const styles = createStyles({ + root: { + flexGrow: 1, + }, + grow: { + flexGrow: 1, + }, + menuButton: { + marginLeft: -12, + marginRight: 20, + }, +}); + +export interface Props extends WithStyles {} + +function ButtonAppBar(props: Props) { + const { classes } = props; + return ( +
+ + + + + + + News + + + + +
+ ); +} + +ButtonAppBar.propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(ButtonAppBar); diff --git a/docs/src/pages/demos/app-bar/DenseAppBar.tsx b/docs/src/pages/demos/app-bar/DenseAppBar.tsx new file mode 100644 index 00000000000000..0709fd96f6cc6c --- /dev/null +++ b/docs/src/pages/demos/app-bar/DenseAppBar.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { createStyles, withStyles, WithStyles } from '@material-ui/core/styles'; +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import IconButton from '@material-ui/core/IconButton'; +import MenuIcon from '@material-ui/icons/Menu'; + +const styles = createStyles({ + root: { + flexGrow: 1, + }, + menuButton: { + marginLeft: -18, + marginRight: 10, + }, +}); + +export interface Props extends WithStyles {} + +function DenseAppBar(props: Props) { + const { classes } = props; + return ( +
+ + + + + + + Photos + + + +
+ ); +} + +DenseAppBar.propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(DenseAppBar); diff --git a/docs/src/pages/demos/app-bar/MenuAppBar.tsx b/docs/src/pages/demos/app-bar/MenuAppBar.tsx new file mode 100644 index 00000000000000..6036d28fb13eaa --- /dev/null +++ b/docs/src/pages/demos/app-bar/MenuAppBar.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { createStyles, withStyles, WithStyles } from '@material-ui/core/styles'; +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import IconButton from '@material-ui/core/IconButton'; +import MenuIcon from '@material-ui/icons/Menu'; +import AccountCircle from '@material-ui/icons/AccountCircle'; +import Switch from '@material-ui/core/Switch'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import FormGroup from '@material-ui/core/FormGroup'; +import MenuItem from '@material-ui/core/MenuItem'; +import Menu from '@material-ui/core/Menu'; + +const styles = createStyles({ + root: { + flexGrow: 1, + }, + grow: { + flexGrow: 1, + }, + menuButton: { + marginLeft: -12, + marginRight: 20, + }, +}); + +export interface Props extends WithStyles {} + +export interface State { + auth: boolean; + anchorEl: null | HTMLElement; +} + +class MenuAppBar extends React.Component { + state: State = { + auth: true, + anchorEl: null, + }; + + handleChange = (event: React.ChangeEvent) => { + this.setState({ auth: event.target.checked }); + }; + + handleMenu = (event: React.MouseEvent) => { + this.setState({ anchorEl: event.currentTarget }); + }; + + handleClose = () => { + this.setState({ anchorEl: null }); + }; + + render() { + const { classes } = this.props; + const { auth, anchorEl } = this.state; + const open = Boolean(anchorEl); + + return ( +
+ + + } + label={auth ? 'Logout' : 'Login'} + /> + + + + + + + + Photos + + {auth && ( +
+ + + + + Profile + My account + +
+ )} +
+
+
+ ); + } +} + +(MenuAppBar as React.ComponentClass).propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(MenuAppBar); diff --git a/docs/src/pages/demos/app-bar/PrimarySearchAppBar.js b/docs/src/pages/demos/app-bar/PrimarySearchAppBar.js index 59f612fd6229e8..936edacf13aba5 100644 --- a/docs/src/pages/demos/app-bar/PrimarySearchAppBar.js +++ b/docs/src/pages/demos/app-bar/PrimarySearchAppBar.js @@ -135,9 +135,9 @@ class PrimarySearchAppBar extends React.Component { anchorOrigin={{ vertical: 'top', horizontal: 'right' }} transformOrigin={{ vertical: 'top', horizontal: 'right' }} open={isMobileMenuOpen} - onClose={this.handleMobileMenuClose} + onClose={this.handleMenuClose} > - + @@ -145,7 +145,7 @@ class PrimarySearchAppBar extends React.Component {

Messages

- + diff --git a/docs/src/pages/demos/app-bar/PrimarySearchAppBar.tsx b/docs/src/pages/demos/app-bar/PrimarySearchAppBar.tsx new file mode 100644 index 00000000000000..56ee618b011979 --- /dev/null +++ b/docs/src/pages/demos/app-bar/PrimarySearchAppBar.tsx @@ -0,0 +1,234 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import IconButton from '@material-ui/core/IconButton'; +import Typography from '@material-ui/core/Typography'; +import InputBase from '@material-ui/core/InputBase'; +import Badge from '@material-ui/core/Badge'; +import MenuItem from '@material-ui/core/MenuItem'; +import Menu from '@material-ui/core/Menu'; +import { fade } from '@material-ui/core/styles/colorManipulator'; +import { createStyles, Theme, withStyles, WithStyles } from '@material-ui/core/styles'; +import MenuIcon from '@material-ui/icons/Menu'; +import SearchIcon from '@material-ui/icons/Search'; +import AccountCircle from '@material-ui/icons/AccountCircle'; +import MailIcon from '@material-ui/icons/Mail'; +import NotificationsIcon from '@material-ui/icons/Notifications'; +import MoreIcon from '@material-ui/icons/MoreVert'; + +const styles = (theme: Theme) => + createStyles({ + root: { + width: '100%', + }, + grow: { + flexGrow: 1, + }, + menuButton: { + marginLeft: -12, + marginRight: 20, + }, + title: { + display: 'none', + [theme.breakpoints.up('sm')]: { + display: 'block', + }, + }, + search: { + position: 'relative', + borderRadius: theme.shape.borderRadius, + backgroundColor: fade(theme.palette.common.white, 0.15), + '&:hover': { + backgroundColor: fade(theme.palette.common.white, 0.25), + }, + marginRight: theme.spacing.unit * 2, + marginLeft: 0, + width: '100%', + [theme.breakpoints.up('sm')]: { + marginLeft: theme.spacing.unit * 3, + width: 'auto', + }, + }, + searchIcon: { + width: theme.spacing.unit * 9, + height: '100%', + position: 'absolute', + pointerEvents: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + inputRoot: { + color: 'inherit', + width: '100%', + }, + inputInput: { + paddingTop: theme.spacing.unit, + paddingRight: theme.spacing.unit, + paddingBottom: theme.spacing.unit, + paddingLeft: theme.spacing.unit * 10, + transition: theme.transitions.create('width'), + width: '100%', + [theme.breakpoints.up('md')]: { + width: 200, + }, + }, + sectionDesktop: { + display: 'none', + [theme.breakpoints.up('md')]: { + display: 'flex', + }, + }, + sectionMobile: { + display: 'flex', + [theme.breakpoints.up('md')]: { + display: 'none', + }, + }, + }); + +export interface Props extends WithStyles {} + +interface State { + anchorEl: null | HTMLElement; + mobileMoreAnchorEl: null | HTMLElement; +} + +class PrimarySearchAppBar extends React.Component { + state: State = { + anchorEl: null, + mobileMoreAnchorEl: null, + }; + + handleProfileMenuOpen = (event: React.MouseEvent) => { + this.setState({ anchorEl: event.currentTarget }); + }; + + handleMenuClose = () => { + this.setState({ anchorEl: null }); + this.handleMobileMenuClose(); + }; + + handleMobileMenuOpen = (event: React.MouseEvent) => { + this.setState({ mobileMoreAnchorEl: event.currentTarget }); + }; + + handleMobileMenuClose = () => { + this.setState({ mobileMoreAnchorEl: null }); + }; + + render() { + const { anchorEl, mobileMoreAnchorEl } = this.state; + const { classes } = this.props; + const isMenuOpen = Boolean(anchorEl); + const isMobileMenuOpen = Boolean(mobileMoreAnchorEl); + + const renderMenu = ( + + Profile + My account + + ); + + const renderMobileMenu = ( + + + + + + + +

Messages

+
+ + + + + + +

Notifications

+
+ + + + +

Profile

+
+
+ ); + + return ( +
+ + + + + + + Material-UI + +
+
+ +
+ +
+
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + {renderMenu} + {renderMobileMenu} +
+ ); + } +} + +(PrimarySearchAppBar as React.ComponentClass).propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(PrimarySearchAppBar); diff --git a/docs/src/pages/demos/app-bar/SearchAppBar.tsx b/docs/src/pages/demos/app-bar/SearchAppBar.tsx new file mode 100644 index 00000000000000..488d6d57c8c22a --- /dev/null +++ b/docs/src/pages/demos/app-bar/SearchAppBar.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import IconButton from '@material-ui/core/IconButton'; +import Typography from '@material-ui/core/Typography'; +import InputBase from '@material-ui/core/InputBase'; +import { fade } from '@material-ui/core/styles/colorManipulator'; +import { createStyles, Theme, withStyles, WithStyles } from '@material-ui/core/styles'; +import MenuIcon from '@material-ui/icons/Menu'; +import SearchIcon from '@material-ui/icons/Search'; + +const styles = (theme: Theme) => + createStyles({ + root: { + width: '100%', + }, + grow: { + flexGrow: 1, + }, + menuButton: { + marginLeft: -12, + marginRight: 20, + }, + title: { + display: 'none', + [theme.breakpoints.up('sm')]: { + display: 'block', + }, + }, + search: { + position: 'relative', + borderRadius: theme.shape.borderRadius, + backgroundColor: fade(theme.palette.common.white, 0.15), + '&:hover': { + backgroundColor: fade(theme.palette.common.white, 0.25), + }, + marginLeft: 0, + width: '100%', + [theme.breakpoints.up('sm')]: { + marginLeft: theme.spacing.unit, + width: 'auto', + }, + }, + searchIcon: { + width: theme.spacing.unit * 9, + height: '100%', + position: 'absolute', + pointerEvents: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + inputRoot: { + color: 'inherit', + width: '100%', + }, + inputInput: { + paddingTop: theme.spacing.unit, + paddingRight: theme.spacing.unit, + paddingBottom: theme.spacing.unit, + paddingLeft: theme.spacing.unit * 10, + transition: theme.transitions.create('width'), + width: '100%', + [theme.breakpoints.up('sm')]: { + width: 120, + '&:focus': { + width: 200, + }, + }, + }, + }); + +export interface Props extends WithStyles {} + +function SearchAppBar(props: Props) { + const { classes } = props; + return ( +
+ + + + + + + Material-UI + +
+
+
+ +
+ +
+ + +
+ ); +} + +SearchAppBar.propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(SearchAppBar); diff --git a/docs/src/pages/demos/app-bar/SimpleAppBar.tsx b/docs/src/pages/demos/app-bar/SimpleAppBar.tsx new file mode 100644 index 00000000000000..b181e24de5a743 --- /dev/null +++ b/docs/src/pages/demos/app-bar/SimpleAppBar.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles, WithStyles } from '@material-ui/core/styles'; +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; + +const styles = { + root: { + flexGrow: 1, + }, +}; + +export interface Props extends WithStyles {} + +function SimpleAppBar(props: Props) { + const { classes } = props; + + return ( +
+ + + + Photos + + + +
+ ); +} + +SimpleAppBar.propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(SimpleAppBar); diff --git a/docs/src/pages/demos/text-fields/ComposedTextField.js b/docs/src/pages/demos/text-fields/ComposedTextField.js index 5402e509f500a5..4264026a145469 100644 --- a/docs/src/pages/demos/text-fields/ComposedTextField.js +++ b/docs/src/pages/demos/text-fields/ComposedTextField.js @@ -49,6 +49,7 @@ class ComposedTextField extends React.Component { onChange={this.handleChange} aria-describedby="component-helper-text" /> + Some important helper text @@ -64,6 +65,7 @@ class ComposedTextField extends React.Component { onChange={this.handleChange} aria-describedby="component-error-text" /> + Error diff --git a/docs/src/pages/demos/text-fields/ComposedTextField.tsx b/docs/src/pages/demos/text-fields/ComposedTextField.tsx new file mode 100644 index 00000000000000..b49745de704cc9 --- /dev/null +++ b/docs/src/pages/demos/text-fields/ComposedTextField.tsx @@ -0,0 +1,107 @@ +import React, { ComponentClass } from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import { createStyles, Theme, withStyles, WithStyles } from '@material-ui/core/styles'; +import FilledInput from '@material-ui/core/FilledInput'; +import FormControl from '@material-ui/core/FormControl'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import Input from '@material-ui/core/Input'; +import InputLabel from '@material-ui/core/InputLabel'; +import OutlinedInput from '@material-ui/core/OutlinedInput'; + +const styles = (theme: Theme) => + createStyles({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + formControl: { + margin: theme.spacing.unit, + }, + }); + +export interface Props extends WithStyles {} + +interface State { + name: string; +} + +class ComposedTextField extends React.Component { + labelRef: HTMLElement | null | undefined; + + state = { + name: 'Composed TextField', + }; + + componentDidMount() { + this.forceUpdate(); + } + + handleChange = (event: React.ChangeEvent) => { + this.setState({ name: event.target.value }); + }; + + render() { + const { classes } = this.props; + + return ( +
+ + Name + + + + Name + + Some important helper text + + + Name + + Disabled + + + Name + + Error + + + { + this.labelRef = ReactDOM.findDOMNode(ref!) as HTMLLabelElement | null; + }} + htmlFor="component-outlined" + > + Name + + + + + Name + + +
+ ); + } +} + +(ComposedTextField as ComponentClass).propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(ComposedTextField); diff --git a/docs/src/pages/demos/text-fields/CustomizedInputs.js b/docs/src/pages/demos/text-fields/CustomizedInputs.js index 4e50373b804f5a..fa01bd82148575 100644 --- a/docs/src/pages/demos/text-fields/CustomizedInputs.js +++ b/docs/src/pages/demos/text-fields/CustomizedInputs.js @@ -120,12 +120,14 @@ function CustomizedInputs(props) { variant="outlined" id="custom-css-outlined-input" /> + + + createStyles({ + root: { + display: 'flex', + flexWrap: 'wrap', + }, + margin: { + margin: theme.spacing.unit, + }, + cssLabel: { + '&$cssFocused': { + color: purple[500], + }, + }, + cssFocused: {}, + cssUnderline: { + '&:after': { + borderBottomColor: purple[500], + }, + }, + cssOutlinedInput: { + '&$cssFocused $notchedOutline': { + borderColor: purple[500], + }, + }, + notchedOutline: {}, + bootstrapRoot: { + 'label + &': { + marginTop: theme.spacing.unit * 3, + }, + }, + bootstrapInput: { + borderRadius: 4, + position: 'relative', + backgroundColor: theme.palette.common.white, + border: '1px solid #ced4da', + fontSize: 16, + width: 'auto', + padding: '10px 12px', + transition: theme.transitions.create(['border-color', 'box-shadow']), + // Use the system font instead of the default Roboto font. + fontFamily: [ + '-apple-system', + 'BlinkMacSystemFont', + '"Segoe UI"', + 'Roboto', + '"Helvetica Neue"', + 'Arial', + 'sans-serif', + '"Apple Color Emoji"', + '"Segoe UI Emoji"', + '"Segoe UI Symbol"', + ].join(','), + '&:focus': { + borderRadius: 4, + borderColor: '#80bdff', + boxShadow: '0 0 0 0.2rem rgba(0,123,255,.25)', + }, + }, + bootstrapFormLabel: { + fontSize: 18, + }, + }); + +const theme = createMuiTheme({ + palette: { + primary: green, + }, + typography: { useNextVariants: true }, +}); + +export interface Props extends WithStyles {} + +function CustomizedInputs(props: Props) { + const { classes } = props; + + return ( +
+ + + Custom CSS + + + + + + + + + + + Bootstrap + + + + +
+ ); +} + +CustomizedInputs.propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(CustomizedInputs); diff --git a/docs/src/pages/demos/text-fields/FilledInputAdornments.js b/docs/src/pages/demos/text-fields/FilledInputAdornments.js index b2b6a59a0308db..9d137dc1e072fe 100644 --- a/docs/src/pages/demos/text-fields/FilledInputAdornments.js +++ b/docs/src/pages/demos/text-fields/FilledInputAdornments.js @@ -68,6 +68,7 @@ class FilledInputAdornments extends React.Component { startAdornment: Kg, }} /> + $, }} /> + Kg, }} /> + + createStyles({ + root: { + display: 'flex', + flexWrap: 'wrap', + }, + margin: { + margin: theme.spacing.unit, + }, + textField: { + flexBasis: 200, + }, + }); + +const ranges = [ + { + value: '0-20', + label: '0 to 20', + }, + { + value: '21-50', + label: '21 to 50', + }, + { + value: '51-100', + label: '51 to 100', + }, +]; + +export interface Props extends WithStyles {} + +interface State { + amount: string; + password: string; + weight: string; + weightRange: string; + showPassword: boolean; +} + +class FilledInputAdornments extends React.Component { + state = { + amount: '', + password: '', + weight: '', + weightRange: '', + showPassword: false, + }; + + handleChange = (prop: 'amount' | 'password' | 'weight' | 'weightRange') => ( + event: React.ChangeEvent, + ) => { + this.setState({ [prop]: event.target.value } as Pick); + }; + + handleClickShowPassword = () => { + this.setState(state => ({ showPassword: !state.showPassword })); + }; + + render() { + const { classes } = this.props; + + return ( +
+ Kg, + }} + /> + Kg, + }} + > + {ranges.map(option => ( + + {option.label} + + ))} + + $, + }} + /> + Kg, + }} + /> + + + {this.state.showPassword ? : } + + + ), + }} + /> +
+ ); + } +} + +(FilledInputAdornments as React.ComponentClass).propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(FilledInputAdornments); diff --git a/docs/src/pages/demos/text-fields/FilledTextFields.js b/docs/src/pages/demos/text-fields/FilledTextFields.js index 100b821f9d4d0f..0189030bd5cddf 100644 --- a/docs/src/pages/demos/text-fields/FilledTextFields.js +++ b/docs/src/pages/demos/text-fields/FilledTextFields.js @@ -69,6 +69,7 @@ class FilledTextFields extends React.Component { margin="normal" variant="filled" /> + + + + + + + + + + + + + + + + + + createStyles({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + textField: { + marginLeft: theme.spacing.unit, + marginRight: theme.spacing.unit, + }, + dense: { + marginTop: 16, + }, + menu: { + width: 200, + }, + }); + +const currencies = [ + { + value: 'USD', + label: '$', + }, + { + value: 'EUR', + label: '€', + }, + { + value: 'BTC', + label: '฿', + }, + { + value: 'JPY', + label: '¥', + }, +]; + +export interface Props extends WithStyles {} + +interface State { + name: string; + age: string; + multiline: string; + currency: string; +} + +class FilledTextFields extends React.Component { + state = { + name: 'Cat in the Hat', + age: '', + multiline: 'Controlled', + currency: 'EUR', + }; + + handleChange = (name: keyof State) => (event: React.ChangeEvent) => { + this.setState({ + [name]: event.target.value, + } as Pick); + }; + + render() { + const { classes } = this.props; + + return ( +
+ + + + + + + + + + + + + + + + + + {currencies.map(option => ( + + {option.label} + + ))} + + + {currencies.map(option => ( + + ))} + + + + + ); + } +} + +(FilledTextFields as React.ComponentClass).propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(FilledTextFields); diff --git a/docs/src/pages/demos/text-fields/FormattedInputs.tsx b/docs/src/pages/demos/text-fields/FormattedInputs.tsx new file mode 100644 index 00000000000000..e1bc3f7ca7bf0f --- /dev/null +++ b/docs/src/pages/demos/text-fields/FormattedInputs.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import MaskedInput from 'react-text-mask'; +import NumberFormat from 'react-number-format'; +import PropTypes from 'prop-types'; +import { createStyles, Theme, withStyles, WithStyles } from '@material-ui/core/styles'; +import Input from '@material-ui/core/Input'; +import InputLabel from '@material-ui/core/InputLabel'; +import TextField from '@material-ui/core/TextField'; +import FormControl from '@material-ui/core/FormControl'; + +const styles = (theme: Theme) => + createStyles({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + formControl: { + margin: theme.spacing.unit, + }, + }); + +export interface TextMaskCustomProps { + inputRef: (ref: HTMLInputElement | null) => void; +} + +function TextMaskCustom(props: TextMaskCustomProps) { + const { inputRef, ...other } = props; + + return ( + { + inputRef(ref ? ref.inputElement : null); + }} + mask={['(', /[1-9]/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/]} + placeholderChar={'\u2000'} + showMask + /> + ); +} + +TextMaskCustom.propTypes = { + inputRef: PropTypes.func.isRequired, +} as any; + +export interface NumberFormatCustomProps { + inputRef: (instance: NumberFormat | null) => void; + onChange: (event: { target: { value: string } }) => void; +} + +function NumberFormatCustom(props: NumberFormatCustomProps) { + const { inputRef, onChange, ...other } = props; + + return ( + { + onChange({ + target: { + value: values.value, + }, + }); + }} + thousandSeparator + prefix="$" + /> + ); +} + +NumberFormatCustom.propTypes = { + inputRef: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, +} as any; + +export interface Props extends WithStyles {} + +interface State { + textmask: string; + numberformat: string; +} + +class FormattedInputs extends React.Component { + state = { + textmask: '(1  )    -    ', + numberformat: '1320', + }; + + handleChange = (name: keyof State) => (event: React.ChangeEvent) => { + this.setState({ + [name]: event.target.value, + } as Pick); + }; + + render() { + const { classes } = this.props; + const { textmask, numberformat } = this.state; + + return ( +
+ + react-text-mask + + + +
+ ); + } +} + +(FormattedInputs as React.ComponentClass).propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(FormattedInputs); diff --git a/docs/src/pages/demos/text-fields/InputAdornments.js b/docs/src/pages/demos/text-fields/InputAdornments.js index 01714dfff66d22..cdd1f88103d158 100644 --- a/docs/src/pages/demos/text-fields/InputAdornments.js +++ b/docs/src/pages/demos/text-fields/InputAdornments.js @@ -74,6 +74,7 @@ class InputAdornments extends React.Component { startAdornment: Kg, }} /> + + Weight
diff --git a/docs/src/pages/demos/text-fields/InputAdornments.tsx b/docs/src/pages/demos/text-fields/InputAdornments.tsx new file mode 100644 index 00000000000000..4d409e25abb3fe --- /dev/null +++ b/docs/src/pages/demos/text-fields/InputAdornments.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import { createStyles, Theme, withStyles, WithStyles } from '@material-ui/core/styles'; +import IconButton from '@material-ui/core/IconButton'; +import Input from '@material-ui/core/Input'; +import InputLabel from '@material-ui/core/InputLabel'; +import InputAdornment from '@material-ui/core/InputAdornment'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import FormControl from '@material-ui/core/FormControl'; +import TextField from '@material-ui/core/TextField'; +import MenuItem from '@material-ui/core/MenuItem'; +import Visibility from '@material-ui/icons/Visibility'; +import VisibilityOff from '@material-ui/icons/VisibilityOff'; + +const styles = (theme: Theme) => + createStyles({ + root: { + display: 'flex', + flexWrap: 'wrap', + }, + margin: { + margin: theme.spacing.unit, + }, + withoutLabel: { + marginTop: theme.spacing.unit * 3, + }, + textField: { + flexBasis: 200, + }, + }); + +const ranges = [ + { + value: '0-20', + label: '0 to 20', + }, + { + value: '21-50', + label: '21 to 50', + }, + { + value: '51-100', + label: '51 to 100', + }, +]; + +export interface Props extends WithStyles {} + +interface State { + amount: string; + password: string; + weight: string; + weightRange: string; + showPassword: boolean; +} + +class InputAdornments extends React.Component { + state = { + amount: '', + password: '', + weight: '', + weightRange: '', + showPassword: false, + }; + + handleChange = (prop: 'amount' | 'password' | 'weight' | 'weightRange') => ( + event: React.ChangeEvent, + ) => { + this.setState({ [prop]: event.target.value } as Pick); + }; + + handleClickShowPassword = () => { + this.setState(state => ({ showPassword: !state.showPassword })); + }; + + render() { + const { classes } = this.props; + + return ( +
+ Kg, + }} + /> + Kg, + }} + > + {ranges.map(option => ( + + {option.label} + + ))} + + + Amount + $} + /> + + + Kg} + inputProps={{ + 'aria-label': 'Weight', + }} + /> + Weight + + + Password + + + {this.state.showPassword ? : } + + + } + /> + +
+ ); + } +} + +(InputAdornments as React.ComponentClass).propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(InputAdornments); diff --git a/docs/src/pages/demos/text-fields/InputWithIcon.js b/docs/src/pages/demos/text-fields/InputWithIcon.js index b59e8413f9bac0..d96bcabc23e79c 100644 --- a/docs/src/pages/demos/text-fields/InputWithIcon.js +++ b/docs/src/pages/demos/text-fields/InputWithIcon.js @@ -43,6 +43,7 @@ function InputWithIcon(props) { ), }} /> +
diff --git a/docs/src/pages/demos/text-fields/InputWithIcon.tsx b/docs/src/pages/demos/text-fields/InputWithIcon.tsx new file mode 100644 index 00000000000000..a3eeae0a7d4130 --- /dev/null +++ b/docs/src/pages/demos/text-fields/InputWithIcon.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { createStyles, Theme, withStyles, WithStyles } from '@material-ui/core/styles'; +import Input from '@material-ui/core/Input'; +import InputLabel from '@material-ui/core/InputLabel'; +import InputAdornment from '@material-ui/core/InputAdornment'; +import FormControl from '@material-ui/core/FormControl'; +import TextField from '@material-ui/core/TextField'; +import Grid from '@material-ui/core/Grid'; +import AccountCircle from '@material-ui/icons/AccountCircle'; + +const styles = (theme: Theme) => + createStyles({ + margin: { + margin: theme.spacing.unit, + }, + }); + +export interface Props extends WithStyles {} + +function InputWithIcon(props: Props) { + const { classes } = props; + + return ( +
+ + With a start adornment + + + + } + /> + + + + + ), + }} + /> +
+ + + + + + + + +
+
+ ); +} + +InputWithIcon.propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(InputWithIcon); diff --git a/docs/src/pages/demos/text-fields/Inputs.js b/docs/src/pages/demos/text-fields/Inputs.js index ba6f496c909dca..03b0eba7272b6b 100644 --- a/docs/src/pages/demos/text-fields/Inputs.js +++ b/docs/src/pages/demos/text-fields/Inputs.js @@ -24,6 +24,7 @@ function Inputs(props) { 'aria-label': 'Description', }} /> + + + + createStyles({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + input: { + margin: theme.spacing.unit, + }, + }); + +export interface Props extends WithStyles {} + +function Inputs(props: Props) { + const { classes } = props; + return ( +
+ + + + +
+ ); +} + +Inputs.propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(Inputs); diff --git a/docs/src/pages/demos/text-fields/OutlinedInputAdornments.js b/docs/src/pages/demos/text-fields/OutlinedInputAdornments.js index 82fe328588282d..9918a5776137df 100644 --- a/docs/src/pages/demos/text-fields/OutlinedInputAdornments.js +++ b/docs/src/pages/demos/text-fields/OutlinedInputAdornments.js @@ -68,6 +68,7 @@ class OutlinedInputAdornments extends React.Component { startAdornment: Kg, }} /> + $, }} /> + Kg, }} /> + + createStyles({ + root: { + display: 'flex', + flexWrap: 'wrap', + }, + margin: { + margin: theme.spacing.unit, + }, + textField: { + flexBasis: 200, + }, + }); + +const ranges = [ + { + value: '0-20', + label: '0 to 20', + }, + { + value: '21-50', + label: '21 to 50', + }, + { + value: '51-100', + label: '51 to 100', + }, +]; + +export interface Props extends WithStyles {} + +interface State { + amount: string; + password: string; + weight: string; + weightRange: string; + showPassword: boolean; +} + +class OutlinedInputAdornments extends React.Component { + state = { + amount: '', + password: '', + weight: '', + weightRange: '', + showPassword: false, + }; + + handleChange = (prop: 'amount' | 'password' | 'weight' | 'weightRange') => ( + event: React.ChangeEvent, + ) => { + this.setState({ [prop]: event.target.value } as Pick); + }; + + handleClickShowPassword = () => { + this.setState(state => ({ showPassword: !state.showPassword })); + }; + + render() { + const { classes } = this.props; + + return ( +
+ Kg, + }} + /> + Kg, + }} + > + {ranges.map(option => ( + + {option.label} + + ))} + + $, + }} + /> + Kg, + }} + /> + + + {this.state.showPassword ? : } + + + ), + }} + /> +
+ ); + } +} + +(OutlinedInputAdornments as React.ComponentClass).propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(OutlinedInputAdornments); diff --git a/docs/src/pages/demos/text-fields/OutlinedTextFields.js b/docs/src/pages/demos/text-fields/OutlinedTextFields.js index f742e199bde162..6b73fbe6816767 100644 --- a/docs/src/pages/demos/text-fields/OutlinedTextFields.js +++ b/docs/src/pages/demos/text-fields/OutlinedTextFields.js @@ -69,6 +69,7 @@ class OutlinedTextFields extends React.Component { margin="normal" variant="outlined" /> + + + + + + + + + + + + + + + + + + createStyles({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + textField: { + marginLeft: theme.spacing.unit, + marginRight: theme.spacing.unit, + }, + dense: { + marginTop: 16, + }, + menu: { + width: 200, + }, + }); + +const currencies = [ + { + value: 'USD', + label: '$', + }, + { + value: 'EUR', + label: '€', + }, + { + value: 'BTC', + label: '฿', + }, + { + value: 'JPY', + label: '¥', + }, +]; + +export interface Props extends WithStyles {} + +interface State { + name: string; + age: string; + multiline: string; + currency: string; +} + +class OutlinedTextFields extends React.Component { + state = { + name: 'Cat in the Hat', + age: '', + multiline: 'Controlled', + currency: 'EUR', + }; + + handleChange = (name: keyof State) => (event: React.ChangeEvent) => { + this.setState({ + [name]: event.target.value, + } as Pick); + }; + + render() { + const { classes } = this.props; + + return ( +
+ + + + + + + + + + + + + + + + + + {currencies.map(option => ( + + {option.label} + + ))} + + + {currencies.map(option => ( + + ))} + + + + + ); + } +} + +(OutlinedTextFields as React.ComponentClass).propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(OutlinedTextFields); diff --git a/docs/src/pages/demos/text-fields/TextFieldMargins.js b/docs/src/pages/demos/text-fields/TextFieldMargins.js index 20e7ca4b7e905a..61571745fbe3e8 100644 --- a/docs/src/pages/demos/text-fields/TextFieldMargins.js +++ b/docs/src/pages/demos/text-fields/TextFieldMargins.js @@ -27,6 +27,7 @@ const TextFieldMargins = props => { className={classes.textField} helperText="Some important text" /> + { helperText="Some important text" margin="dense" /> + + createStyles({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + textField: { + marginLeft: theme.spacing.unit, + marginRight: theme.spacing.unit, + width: 200, + }, + }); + +export interface Props extends WithStyles {} + +const TextFieldMargins = (props: Props) => { + const { classes } = props; + + return ( +
+ + + +
+ ); +}; + +TextFieldMargins.propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(TextFieldMargins); diff --git a/docs/src/pages/demos/text-fields/TextFields.js b/docs/src/pages/demos/text-fields/TextFields.js index 536e33e8b363cd..e0d1b2f66a294e 100644 --- a/docs/src/pages/demos/text-fields/TextFields.js +++ b/docs/src/pages/demos/text-fields/TextFields.js @@ -51,9 +51,7 @@ class TextFields extends React.Component { }; handleChange = name => event => { - this.setState({ - [name]: event.target.value, - }); + this.setState({ [name]: event.target.value }); }; render() { @@ -69,6 +67,7 @@ class TextFields extends React.Component { onChange={this.handleChange('name')} margin="normal" /> + + + + + + + + + + + + + + + + + createStyles({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + textField: { + marginLeft: theme.spacing.unit, + marginRight: theme.spacing.unit, + width: 200, + }, + dense: { + marginTop: 19, + }, + menu: { + width: 200, + }, + }); + +const currencies = [ + { + value: 'USD', + label: '$', + }, + { + value: 'EUR', + label: '€', + }, + { + value: 'BTC', + label: '฿', + }, + { + value: 'JPY', + label: '¥', + }, +]; + +export interface Props extends WithStyles {} + +export interface State { + name: string; + age: string; + multiline: string; + currency: string; +} + +class TextFields extends React.Component { + state: State = { + name: 'Cat in the Hat', + age: '', + multiline: 'Controlled', + currency: 'EUR', + }; + + handleChange = (name: keyof State) => (event: React.ChangeEvent) => { + this.setState({ [name]: event.target.value } as Pick); + }; + + render() { + const { classes } = this.props; + + return ( +
+ + + + + + + + + + + + + + + + + {currencies.map(option => ( + + {option.label} + + ))} + + + {currencies.map(option => ( + + ))} + + + + + ); + } +} + +(TextFields as React.ComponentClass).propTypes = { + classes: PropTypes.object.isRequired, +} as any; + +export default withStyles(styles)(TextFields); diff --git a/docs/ts-demo-ignore.json b/docs/ts-demo-ignore.json new file mode 100644 index 00000000000000..fe51488c7066f6 --- /dev/null +++ b/docs/ts-demo-ignore.json @@ -0,0 +1 @@ +[] diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 00000000000000..9c714464e57baa --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.json", + "include": ["types", "src/pages/**/*"] +} diff --git a/docs/tslint.json b/docs/tslint.json new file mode 100644 index 00000000000000..d35877747ecfb7 --- /dev/null +++ b/docs/tslint.json @@ -0,0 +1,8 @@ +{ + "extends": "../tslint.json", + "rules": { + "no-irregular-whitespace": false, + "no-object-literal-type-assertion": false, + "no-trailing-whitespace": [true, "ignore-template-strings"] + } +} diff --git a/docs/types/icons.d.ts b/docs/types/icons.d.ts new file mode 100644 index 00000000000000..acef22e4e68a6f --- /dev/null +++ b/docs/types/icons.d.ts @@ -0,0 +1,2 @@ +declare module '@material-ui/icons'; +declare module '@material-ui/icons/*'; diff --git a/package.json b/package.json index 7c9f28dcf2d0c4..047e7929353b1e 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ "docs:icons": "rimraf static/icons/* && babel-node ./docs/scripts/buildIcons.js", "docs:size-why": "DOCS_STATS_ENABLED=true yarn docs:build", "docs:start": "next start", + "docs:typescript": "cross-env NODE_ENV=development babel docs/src --config-file ./docs/babel.config.ts.js --extensions .tsx --out-dir docs/src", + "docs:typescript:formatted": "node docs/scripts/formattedTSDemos", "jsonlint": "yarn --silent jsonlint:files | xargs -n1 jsonlint -q -c && echo \"jsonlint: no lint errors\"", "jsonlint:files": "find . -name \"*.json\" | grep -v -f .eslintignore", "lint": "eslint . --cache --report-unused-disable-directives && echo \"eslint: no lint errors\"", @@ -50,7 +52,7 @@ "test:umd": "yarn babel-node packages/material-ui/test/umd/run.js", "test:unit": "cross-env NODE_ENV=test mocha 'packages/**/*.test.js' 'docs/**/*.test.js' --exclude '**/node_modules/**'", "test:watch": "yarn test:unit --watch", - "typescript": "lerna run typescript" + "typescript": "lerna run typescript && tslint -p docs/tsconfig.json" }, "peerDependencies": { "react": "*", @@ -66,27 +68,34 @@ "@babel/plugin-transform-runtime": "^7.2.0", "@babel/preset-env": "^7.2.0", "@babel/preset-react": "^7.0.0", + "@babel/preset-typescript": "^7.1.0", "@babel/register": "^7.0.0", "@date-io/date-fns": "^1.0.0", "@emotion/core": "^10.0.0", "@emotion/styled": "^10.0.0", + "@types/classnames": "^2.2.6", "@types/enzyme": "^3.1.4", "@types/react": "^16.7.10", + "@types/react-dom": "^16.0.9", "@types/react-router-dom": "^4.3.1", + "@types/react-text-mask": "^5.4.2", "@weco/next-plugin-transpile-modules": "0.0.2", "accept-language": "^3.0.18", "argos-cli": "^0.0.9", "autoprefixer": "^9.0.0", "autosuggest-highlight": "^3.1.1", + "babel-core": "^7.0.0-bridge.0", "babel-eslint": "^10.0.0", "babel-loader": "^8.0.0", "babel-plugin-istanbul": "^5.0.0", "babel-plugin-module-resolver": "^3.0.0", "babel-plugin-preval": "^2.0.0", "babel-plugin-react-remove-properties": "^0.2.5", + "babel-plugin-tester": "^5.5.1", "babel-plugin-transform-dev-warning": "^0.1.0", "babel-plugin-transform-react-constant-elements": "^6.23.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.21", + "babel-plugin-unwrap-createStyles": "link:packages/babel-plugin-unwrap-createStyles", "chai": "^4.1.2", "clean-css": "^4.1.11", "clipboard-copy": "^2.0.0", diff --git a/packages/babel-plugin-unwrap-createStyles/README.md b/packages/babel-plugin-unwrap-createStyles/README.md new file mode 100644 index 00000000000000..da7905ce6abbd7 --- /dev/null +++ b/packages/babel-plugin-unwrap-createStyles/README.md @@ -0,0 +1,41 @@ +## Motivation + +We write our demos in TS but want to offer the same demos to JS users. +Maintaining two versions of the demos is hard and therefore we simply transpile +the TS demos with babel to JS. + +However we have some TS only utility functions that are essentially identity function +with no side-effects whos only purpose is to defeat type widening. These add noise +to the JS files and bundle size for everyone. + +This plugin unwraps them + +## Recognized patterns + +```ts +import { createStyles, withStyles } from '@material-ui/core/styles' + +const styles = () => createStyles({}); + +export default withStyles(styles)(Component); +``` + +```js +import { withStyles } from '@material-ui/core/styles'; + +const styles = () => ({}); + +export default withStyles(styles)(Component); +``` + +### Missing + +- default import from `createStyles` +- aliased imports + +## Why not as a typescript transformer? + +Face these issues in chronological order +1. no config API i.e. transformers are only supported for programmatic transpilation/compilation +2. since we need to pipe prettier we might as well use a programmatic approach so back to transformer +3. typescript does not preserve blanklines (Microsoft/TypeScript#843) back to babel plugins diff --git a/packages/babel-plugin-unwrap-createStyles/package.json b/packages/babel-plugin-unwrap-createStyles/package.json new file mode 100644 index 00000000000000..703f0855c70c16 --- /dev/null +++ b/packages/babel-plugin-unwrap-createStyles/package.json @@ -0,0 +1,19 @@ +{ + "name": "babel-plugin-unwrap-createStyles", + "private": true, + "version": "0.0.1", + "description": "unwraps createStyles call", + "main": "src/index.js", + "author": "Material-UI Team", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/mui-org/material-ui.git" + }, + "bugs": { + "url": "https://github.com/mui-org/material-ui/issues" + }, + "scripts": { + "test": "cd ../../ && cross-env NODE_ENV=test mocha packages/babel-plugin-unwrap-createStyles/test" + } +} diff --git a/packages/babel-plugin-unwrap-createStyles/src/index.js b/packages/babel-plugin-unwrap-createStyles/src/index.js new file mode 100644 index 00000000000000..1a0a406406d8cc --- /dev/null +++ b/packages/babel-plugin-unwrap-createStyles/src/index.js @@ -0,0 +1 @@ +module.exports = require('./unwrapCreateStyles'); diff --git a/packages/babel-plugin-unwrap-createStyles/src/unwrapCreateStyles.js b/packages/babel-plugin-unwrap-createStyles/src/unwrapCreateStyles.js new file mode 100644 index 00000000000000..912d6b2c045cb3 --- /dev/null +++ b/packages/babel-plugin-unwrap-createStyles/src/unwrapCreateStyles.js @@ -0,0 +1,51 @@ +/** + * + * @param {babel.types.ImportDeclaration} param0 + */ +function isImportFromStyles({ source }) { + return source.value === '@material-ui/core/styles'; +} + +/** + * + * @param {babel.types.CallExpression} param0 + */ +function isCreateStylesCall({ callee }) { + return callee.name === 'createStyles'; +} + +/** + * + * @param {babel.types.ImportSpecifier} param0 + */ +function isCreateStylesImportSepcifier({ imported }) { + return imported.name === 'createStyles'; +} + +/** + * @param {babel.types.CallExpression} expression + */ +function unwrapCallExpression(expression) { + if (expression.arguments.length !== 1) { + throw new Error('need exactly one argument'); + } + + return expression.arguments[0]; +} + +module.exports = function unwrapCreateStyles({ types: t }) { + return { + visitor: { + CallExpression(path) { + if (isCreateStylesCall(path.node)) { + path.replaceWith(unwrapCallExpression(path.node, t)); + } + }, + ImportSpecifier(path) { + if (isImportFromStyles(path.parent) && isCreateStylesImportSepcifier(path.node)) { + path.remove(); + } + }, + }, + }; +}; diff --git a/packages/babel-plugin-unwrap-createStyles/test/__fixtures__/static/code.js b/packages/babel-plugin-unwrap-createStyles/test/__fixtures__/static/code.js new file mode 100644 index 00000000000000..a9eed91d6b9d8e --- /dev/null +++ b/packages/babel-plugin-unwrap-createStyles/test/__fixtures__/static/code.js @@ -0,0 +1,3 @@ +import { createStyles, withStyles } from '@material-ui/core/styles'; + +const styles = createStyles({}); diff --git a/packages/babel-plugin-unwrap-createStyles/test/__fixtures__/static/output.js b/packages/babel-plugin-unwrap-createStyles/test/__fixtures__/static/output.js new file mode 100644 index 00000000000000..1e35a7e9582756 --- /dev/null +++ b/packages/babel-plugin-unwrap-createStyles/test/__fixtures__/static/output.js @@ -0,0 +1,2 @@ +import { withStyles } from '@material-ui/core/styles'; +const styles = {}; diff --git a/packages/babel-plugin-unwrap-createStyles/test/__fixtures__/stylecallback/code.js b/packages/babel-plugin-unwrap-createStyles/test/__fixtures__/stylecallback/code.js new file mode 100644 index 00000000000000..1ba20f5df0dbf8 --- /dev/null +++ b/packages/babel-plugin-unwrap-createStyles/test/__fixtures__/stylecallback/code.js @@ -0,0 +1,8 @@ +import { createStyles, withStyles } from '@material-ui/core/styles'; + +const styles = theme => { + return createStyles({}); +}; + +const stylesShorthandStripped = theme => createStyles({}); +const stylesShorthand = theme => createStyles({ a: 1 }); diff --git a/packages/babel-plugin-unwrap-createStyles/test/__fixtures__/stylecallback/output.js b/packages/babel-plugin-unwrap-createStyles/test/__fixtures__/stylecallback/output.js new file mode 100644 index 00000000000000..64c5284d4fe111 --- /dev/null +++ b/packages/babel-plugin-unwrap-createStyles/test/__fixtures__/stylecallback/output.js @@ -0,0 +1,11 @@ +import { withStyles } from '@material-ui/core/styles'; + +const styles = theme => { + return {}; +}; + +const stylesShorthandStripped = theme => ({}); + +const stylesShorthand = theme => ({ + a: 1 +}); diff --git a/packages/babel-plugin-unwrap-createStyles/test/unwrapCreateStyles.test.js b/packages/babel-plugin-unwrap-createStyles/test/unwrapCreateStyles.test.js new file mode 100644 index 00000000000000..5bc50dce9d179f --- /dev/null +++ b/packages/babel-plugin-unwrap-createStyles/test/unwrapCreateStyles.test.js @@ -0,0 +1,12 @@ +import pluginTester from 'babel-plugin-tester'; +import * as path from 'path'; +import unwrapCreateStylesPlugin from '../src'; + +pluginTester({ + plugin: unwrapCreateStylesPlugin, + pluginName: 'unwrapCreateStylesPlugin', + fixtures: path.join(__dirname, '__fixtures__'), + babelOptions: { + root: __dirname, + }, +}); diff --git a/packages/material-ui-docs/src/svgIcons/HookLogo.js b/packages/material-ui-docs/src/svgIcons/HookLogo.js deleted file mode 100644 index 1a422e4d9974b9..00000000000000 --- a/packages/material-ui-docs/src/svgIcons/HookLogo.js +++ /dev/null @@ -1,19 +0,0 @@ -/* eslint-disable max-len */ - -import React from 'react'; -import SvgIcon from '@material-ui/core/SvgIcon'; - -function HookLogo(props) { - return ( - - - - - - - ); -} - -HookLogo.muiName = 'SvgIcon'; - -export default HookLogo; diff --git a/packages/material-ui-docs/src/svgIcons/Hooks.js b/packages/material-ui-docs/src/svgIcons/Hooks.js new file mode 100644 index 00000000000000..9c9e95f70e175b --- /dev/null +++ b/packages/material-ui-docs/src/svgIcons/Hooks.js @@ -0,0 +1,16 @@ +/* eslint-disable max-len */ + +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +function Hooks(props) { + return ( + + + + ); +} + +Hooks.muiName = 'SvgIcon'; + +export default Hooks; diff --git a/packages/material-ui-docs/src/svgIcons/JSLogo.js b/packages/material-ui-docs/src/svgIcons/JSLogo.js deleted file mode 100644 index afa99d7e1ee9a9..00000000000000 --- a/packages/material-ui-docs/src/svgIcons/JSLogo.js +++ /dev/null @@ -1,19 +0,0 @@ -/* eslint-disable max-len */ - -import React from 'react'; -import SvgIcon from '@material-ui/core/SvgIcon'; - -function JSLogo(props) { - return ( - - - - - - - ); -} - -JSLogo.muiName = 'SvgIcon'; - -export default JSLogo; diff --git a/packages/material-ui-docs/src/svgIcons/Javascript.js b/packages/material-ui-docs/src/svgIcons/Javascript.js new file mode 100644 index 00000000000000..44902e18ceb820 --- /dev/null +++ b/packages/material-ui-docs/src/svgIcons/Javascript.js @@ -0,0 +1,16 @@ +/* eslint-disable max-len */ + +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +function Javascript(props) { + return ( + + + + ); +} + +Javascript.muiName = 'SvgIcon'; + +export default Javascript; diff --git a/packages/material-ui-docs/src/svgIcons/Typescript.js b/packages/material-ui-docs/src/svgIcons/Typescript.js new file mode 100644 index 00000000000000..59a40a95470325 --- /dev/null +++ b/packages/material-ui-docs/src/svgIcons/Typescript.js @@ -0,0 +1,15 @@ +/* eslint-disable max-len */ + +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +function Typescript(props) { + return ( + + + + ); +} + +Typescript.muiName = 'SvgIcon'; +export default Typescript; diff --git a/packages/material-ui/src/InputLabel/InputLabel.d.ts b/packages/material-ui/src/InputLabel/InputLabel.d.ts index f66bbe14ec4835..6544668e27e710 100644 --- a/packages/material-ui/src/InputLabel/InputLabel.d.ts +++ b/packages/material-ui/src/InputLabel/InputLabel.d.ts @@ -26,6 +26,4 @@ export type InputLabelClassKey = | 'filled' | 'outlined'; -declare const InputLabel: React.ComponentType; - -export default InputLabel; +export default class InputLabel extends React.Component {} diff --git a/pages/demos/app-bar.js b/pages/demos/app-bar.js index 409b759a7d6979..98ae686094503b 100644 --- a/pages/demos/app-bar.js +++ b/pages/demos/app-bar.js @@ -4,7 +4,11 @@ import React from 'react'; import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; const req = require.context('docs/src/pages/demos/app-bar', false, /\.md|\.js$/); -const reqSource = require.context('!raw-loader!../../docs/src/pages/demos/app-bar', false, /\.js$/); +const reqSource = require.context( + '!raw-loader!../../docs/src/pages/demos/app-bar', + false, + /\.(js|tsx)$/, +); const reqPrefix = 'pages/demos/app-bar'; function Page() { diff --git a/pages/demos/text-fields.js b/pages/demos/text-fields.js index 1ddc3b075cc46e..e6ec5215556aca 100644 --- a/pages/demos/text-fields.js +++ b/pages/demos/text-fields.js @@ -7,7 +7,7 @@ const req = require.context('docs/src/pages/demos/text-fields', false, /\.md|\.j const reqSource = require.context( '!raw-loader!../../docs/src/pages/demos/text-fields', false, - /\.js$/, + /\.(js|tsx)$/, ); const reqPrefix = 'pages/demos/text-fields'; diff --git a/tsconfig.json b/tsconfig.json index a1aa50dce3f6de..58e81fa910f3d1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "noEmit": true, "experimentalDecorators": true, "baseUrl": "./packages", + "allowSyntheticDefaultImports": true, "paths": { "@material-ui/core": ["./material-ui/src"], "@material-ui/core/*": ["./material-ui/src/*"], diff --git a/yarn.lock b/yarn.lock index 3f6016ded50aa6..accdb1af86fa16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -404,6 +404,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-syntax-typescript@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.2.0.tgz#55d240536bd314dcbbec70fd949c5cabaed1de29" + integrity sha512-WhKr6yu6yGpGcNMVgIBuI9MkredpVc7Y3YR4UzEZmDztHoL6wV56YBHLhWnjO1EvId1B32HrD3DRFc+zSoKI1g== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-arrow-functions@^7.0.0", "@babel/plugin-transform-arrow-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550" @@ -668,6 +675,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-typescript@^7.1.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.2.0.tgz#bce7c06300434de6a860ae8acf6a442ef74a99d1" + integrity sha512-EnI7i2/gJ7ZNr2MuyvN2Hu+BHJENlxWte5XygPvfj/MbvtOkWor9zcnHpMMQL2YYaaCcqtIvJUyJ7QVfoGs7ew== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-typescript" "^7.2.0" + "@babel/plugin-transform-unicode-regex@^7.0.0", "@babel/plugin-transform-unicode-regex@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.2.0.tgz#4eb8db16f972f8abb5062c161b8b115546ade08b" @@ -790,6 +805,14 @@ "@babel/plugin-transform-react-jsx-self" "^7.0.0" "@babel/plugin-transform-react-jsx-source" "^7.0.0" +"@babel/preset-typescript@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.1.0.tgz#49ad6e2084ff0bfb5f1f7fb3b5e76c434d442c7f" + integrity sha512-LYveByuF9AOM8WrsNne5+N79k1YxjNB6gmpCQsnuSBAcV8QUeB+ZUxQzL7Rz7HksPbahymKkq2qBR+o36ggFZA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-typescript" "^7.1.0" + "@babel/register@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.0.0.tgz#fa634bae1bfa429f60615b754fc1f1d745edd827" @@ -1660,6 +1683,11 @@ resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.10.tgz#780d552467824be4a241b29510a7873a7432c4a6" integrity sha512-fOM/Jhv51iyugY7KOBZz2ThfT1gwvsGCfWxpLpZDgkGjpEO4Le9cld07OdskikLjDUQJ43dzDaVRSFwQlpdqVg== +"@types/classnames@^2.2.6": + version "2.2.6" + resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.6.tgz#dbe8a666156d556ed018e15a4c65f08937c3f628" + integrity sha512-XHcYvVdbtAxVstjKxuULYqYaWIzHR15yr1pZj4fnGChuBVJlIAp9StJna0ZJNSgxPh4Nac2FL4JM3M11Tm6fqQ== + "@types/enzyme@^3.1.4": version "3.1.15" resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.15.tgz#fc9a9695ba9f90cd50c4967e64a8c66ec96913d1" @@ -1725,6 +1753,13 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.1.tgz#48fd98c1561fe718b61733daed46ff115b496e18" integrity sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA== +"@types/react-dom@^16.0.9": + version "16.0.11" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.11.tgz#bd10ccb0d9260343f4b9a49d4f7a8330a5c1f081" + integrity sha512-x6zUx9/42B5Kl2Vl9HlopV8JF64wLpX3c+Pst9kc1HgzrsH+mkehe/zmHMQTplIrR48H2gpU7ZqurQolYu8XBA== + dependencies: + "@types/react" "*" + "@types/react-router-dom@^4.3.1": version "4.3.1" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.1.tgz#71fe2918f8f60474a891520def40a63997dafe04" @@ -1742,7 +1777,7 @@ "@types/history" "*" "@types/react" "*" -"@types/react-text-mask@^5.4.3": +"@types/react-text-mask@^5.4.2", "@types/react-text-mask@^5.4.3": version "5.4.3" resolved "https://registry.yarnpkg.com/@types/react-text-mask/-/react-text-mask-5.4.3.tgz#51351359306a1feaa4ffed017ba6c7fd4d7a6290" integrity sha512-+/uibOQ07X4om3+dPMpxUGdw76u4d4HQ9qCPs2syFtwmpTlnOyAJaIW89t+QX/DXI+Ar+SKch6A4stoLg5RdLw== @@ -2608,7 +2643,7 @@ babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: esutils "^2.0.2" js-tokens "^3.0.2" -babel-core@7.0.0-bridge.0: +babel-core@7.0.0-bridge.0, babel-core@^7.0.0-bridge.0: version "7.0.0-bridge.0" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg== @@ -2980,6 +3015,17 @@ babel-plugin-syntax-trailing-function-commas@^6.22.0: resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" integrity sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM= +babel-plugin-tester@^5.5.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/babel-plugin-tester/-/babel-plugin-tester-5.5.1.tgz#93648d140e67a81fa8474a73d3382b4fd8e91ca6" + integrity sha512-/Sc90Bv2aHGglPkM2Ezw9w/bKMk39h+Dj7G5+Qc9SM2sarnojQhe3NWwZG7cByCXWbp86Gqh4VoYj1YCERFpSg== + dependencies: + common-tags "^1.4.0" + invariant "^2.2.2" + lodash.merge "^4.6.0" + path-exists "^3.0.0" + strip-indent "^2.0.0" + babel-plugin-transform-async-generator-functions@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz#f058900145fd3e9907a6ddf28da59f215258a5db" @@ -3288,6 +3334,10 @@ babel-plugin-transform-strict-mode@^6.24.1: babel-runtime "^6.22.0" babel-types "^6.24.1" +"babel-plugin-unwrap-createStyles@link:packages/babel-plugin-unwrap-createStyles": + version "0.0.0" + uid "" + babel-preset-es2015@^6.9.0: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939" @@ -4347,6 +4397,11 @@ commander@~2.9.0: dependencies: graceful-readlink ">= 1.0.0" +common-tags@^1.4.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" + integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -9021,6 +9076,11 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= +lodash.merge@^4.6.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" + integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ== + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"