diff --git a/docs/package.json b/docs/package.json index 23a74d62e74614..e74c5ac25569cc 100644 --- a/docs/package.json +++ b/docs/package.json @@ -55,6 +55,7 @@ "emotion-theming": "^10.0.14", "fg-loadcss": "^2.0.1", "final-form": "^4.18.2", + "flexsearch": "^0.6.30", "isomorphic-fetch": "^2.2.1", "json2mq": "^0.2.0", "jss-plugin-template": "^10.0.0-alpha.23", diff --git a/docs/pages/api/icon.md b/docs/pages/api/icon.md index 5abf9c11604607..17802ffc0ce7fb 100644 --- a/docs/pages/api/icon.md +++ b/docs/pages/api/icon.md @@ -60,4 +60,5 @@ The component is fully [StrictMode](https://reactjs.org/docs/strict-mode.html) c ## Demos - [Icons](/components/icons/) +- [Material Icons](/components/material-icons/) diff --git a/docs/pages/api/svg-icon.md b/docs/pages/api/svg-icon.md index 87794b206b3e8b..76460a23c997c8 100644 --- a/docs/pages/api/svg-icon.md +++ b/docs/pages/api/svg-icon.md @@ -64,4 +64,5 @@ The component is fully [StrictMode](https://reactjs.org/docs/strict-mode.html) c ## Demos - [Icons](/components/icons/) +- [Material Icons](/components/material-icons/) diff --git a/docs/pages/components/material-icons.js b/docs/pages/components/material-icons.js new file mode 100644 index 00000000000000..512db6c0ec387d --- /dev/null +++ b/docs/pages/components/material-icons.js @@ -0,0 +1,22 @@ +import React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import Markdown from 'docs/src/pages/components/material-icons/material-icons.md'; +import SearchIcons from 'docs/src/pages/components/material-icons/SearchIcons'; + +const req = name => { + const map = { + 'material-icons.md': Markdown, + 'SearchIcons.js': { + default: SearchIcons, + }, + }; + return map[name]; +}; +req.keys = () => ['material-icons.md', 'SearchIcons.js']; +const reqPrefix = 'pages/components/material-icons'; + +function Page() { + return {}} reqPrefix={reqPrefix} />; +} + +export default Page; diff --git a/docs/src/modules/components/MarkdownElement.js b/docs/src/modules/components/MarkdownElement.js index 166d0b16043134..02ebffdbff39a5 100644 --- a/docs/src/modules/components/MarkdownElement.js +++ b/docs/src/modules/components/MarkdownElement.js @@ -40,7 +40,12 @@ renderer.heading = (text, level) => { ); }; -const externs = ['https://material.io/', 'https://getbootstrap.com/', 'https://www.amazon.com']; +const externs = [ + 'https://material.io/', + 'https://getbootstrap.com/', + 'https://www.amazon.com', + 'https://materialdesignicons.com', +]; renderer.link = (href, title, text) => { let more = ''; @@ -148,7 +153,7 @@ const styles = theme => ({ '& h1': { ...theme.typography.h3, fontSize: 40, - margin: '24px 0 16px', + margin: '16px 0', }, '& .description': { ...theme.typography.h5, diff --git a/docs/src/pages.js b/docs/src/pages.js index e0543620c15fe1..3b8124d7d17638 100644 --- a/docs/src/pages.js +++ b/docs/src/pages.js @@ -85,6 +85,7 @@ const pages = [ { pathname: '/components/chips' }, { pathname: '/components/dividers' }, { pathname: '/components/icons' }, + { pathname: '/components/material-icons' }, { pathname: '/components/lists' }, { pathname: '/components/tables' }, { pathname: '/components/tooltips' }, diff --git a/docs/src/pages/components/icons/icons.md b/docs/src/pages/components/icons/icons.md index 139ff4e02b9b5a..6a66a7929ba3e1 100644 --- a/docs/src/pages/components/icons/icons.md +++ b/docs/src/pages/components/icons/icons.md @@ -30,23 +30,21 @@ Optionally, you can set the icon color using one of the theme color properties: ### SVG Material icons It's interesting to have the building blocks needed to implement custom icons, but what about presets? -We provide a separate npm package, -[@material-ui/icons](https://www.npmjs.com/package/@material-ui/icons), -that includes the 1,000+ official [Material icons](https://material.io/tools/icons/?style=baseline) converted to `SvgIcon` components. +[@material-ui/icons](https://www.npmjs.com/package/@material-ui/icons) is an npm package that includes the 1,000+ official [Material icons](https://material.io/tools/icons/?style=baseline) converted to `SvgIcon` components. - + Official material icons #### Usage -You can use [material.io/tools/icons](https://material.io/tools/icons/?style=baseline) to find a specific icon. +You can use our [internal search](/components/material-icons/) or [material.io/tools/icons](https://material.io/tools/icons/?style=baseline) to find a specific icon. When importing an icon, keep in mind that the names of the icons are `PascalCase`, for instance: - [`delete`](https://material.io/tools/icons/?icon=delete&style=baseline) is exposed as `@material-ui/icons/Delete` - [`delete forever`](https://material.io/tools/icons/?icon=delete_forever&style=baseline) is exposed as `@material-ui/icons/DeleteForever` -For *"themed"* icons, append the theme name to the icon name. For instance with the +For "themed" icons, append the theme name to the icon name. For instance with the - The Outlined [`delete`](https://material.io/tools/icons/?icon=delete&style=outline) icon is exposed as `@material-ui/icons/DeleteOutlined` - The Rounded [`delete`](https://material.io/tools/icons/?icon=delete&style=rounded) icon is exposed as `@material-ui/icons/DeleteRounded` @@ -63,18 +61,23 @@ There are three exceptions to this rule: #### Imports -- If your environment doesn't support tree-shaking, the **recommended** way to import the icons is the following: -```jsx -import AccessAlarmIcon from '@material-ui/icons/AccessAlarm'; -import ThreeDRotation from '@material-ui/icons/ThreeDRotation'; -``` +You can import the icons with one of these two options: -- If your environment support tree-shaking you can also import the icons this way: -```jsx -import { AccessAlarm, ThreeDRotation } from '@material-ui/icons'; -``` +- Option n°1: + + ```jsx + import AccessAlarmIcon from '@material-ui/icons/AccessAlarm'; + import ThreeDRotation from '@material-ui/icons/ThreeDRotation'; + ``` +- Option n2: + + ```jsx + import { AccessAlarm, ThreeDRotation } from '@material-ui/icons'; + ``` -Note: Importing named exports in this way will result in the code for *every icon* being included in your project, so is not recommended unless you configure [tree-shaking](https://webpack.js.org/guides/tree-shaking/). It may also impact Hot Module Reload performance. +The safest option is n°1 but option n°2 can yield the best experience. +Make sure you follow our [minimizing bundle size guide](/guides/minimizing-bundle-size/#option-2) before using the approach n°2. +We encourage the configuration of a Babel plugin. ### More SVG icons diff --git a/docs/src/pages/components/material-icons/SearchIcons.js b/docs/src/pages/components/material-icons/SearchIcons.js new file mode 100644 index 00000000000000..2ad18f84bd8902 --- /dev/null +++ b/docs/src/pages/components/material-icons/SearchIcons.js @@ -0,0 +1,492 @@ +/* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */ + +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Paper from '@material-ui/core/Paper'; +import clsx from 'clsx'; +import InputBase from '@material-ui/core/InputBase'; +import Typography from '@material-ui/core/Typography'; +import PropTypes from 'prop-types'; +import { debounce } from 'lodash'; +import Grid from '@material-ui/core/Grid'; +import Dialog from '@material-ui/core/Dialog'; +import MarkdownElement from 'docs/src/modules/components/MarkdownElement'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; +import Button from '@material-ui/core/Button'; +import FlexSearch from 'flexsearch'; +import SearchIcon from '@material-ui/icons/Search'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import RadioGroup from '@material-ui/core/RadioGroup'; +import Radio from '@material-ui/core/Radio'; +import Link from 'docs/src/modules/components/Link'; +import * as mui from '@material-ui/icons'; + +// Working on the logic? Uncomment these imports. +// It will be x10 faster than working with all of the icons. + +// import Menu from '@material-ui/icons/Menu'; +// import MenuOutlined from '@material-ui/icons/MenuOutlined'; +// import MenuRounded from '@material-ui/icons/MenuRounded'; +// import MenuTwoTone from '@material-ui/icons/MenuTwoTone'; +// import MenuSharp from '@material-ui/icons/MenuSharp'; +// import ExitToApp from '@material-ui/icons/ExitToApp'; +// import ExitToAppOutlined from '@material-ui/icons/ExitToAppOutlined'; +// import ExitToAppRounded from '@material-ui/icons/ExitToAppRounded'; +// import ExitToAppTwoTone from '@material-ui/icons/ExitToAppTwoTone'; +// import ExitToAppSharp from '@material-ui/icons/ExitToAppSharp'; +// import Delete from '@material-ui/icons/Delete'; +// import DeleteOutlined from '@material-ui/icons/DeleteOutlined'; +// import DeleteRounded from '@material-ui/icons/DeleteRounded'; +// import DeleteTwoTone from '@material-ui/icons/DeleteTwoTone'; +// import DeleteSharp from '@material-ui/icons/DeleteSharp'; +// import DeleteForever from '@material-ui/icons/DeleteForever'; +// import DeleteForeverOutlined from '@material-ui/icons/DeleteForeverOutlined'; +// import DeleteForeverRounded from '@material-ui/icons/DeleteForeverRounded'; +// import DeleteForeverTwoTone from '@material-ui/icons/DeleteForeverTwoTone'; +// import DeleteForeverSharp from '@material-ui/icons/DeleteForeverSharp'; + +// const mui = { +// ExitToApp, +// ExitToAppOutlined, +// ExitToAppRounded, +// ExitToAppTwoTone, +// ExitToAppSharp, +// Menu, +// MenuOutlined, +// MenuRounded, +// MenuTwoTone, +// MenuSharp, +// Delete, +// DeleteOutlined, +// DeleteRounded, +// DeleteTwoTone, +// DeleteSharp, +// DeleteForever, +// DeleteForeverOutlined, +// DeleteForeverRounded, +// DeleteForeverTwoTone, +// DeleteForeverSharp, +// }; + +function selectNode(node) { + // Clear any current selection + const selection = window.getSelection(); + selection.removeAllRanges(); + + // Select code + const range = document.createRange(); + range.selectNodeContents(node); + selection.addRange(range); +} + +let Icons = props => { + const { icons, classes, handleClickOpen } = props; + + const handleClick = event => { + selectNode(event.currentTarget); + }; + + return ( +
+ {icons.map(icon => { + return ( + + +

{icon.key}

+
+ ); + })} +
+ ); +}; + +Icons.propTypes = { + classes: PropTypes.object.isRequired, + handleClickOpen: PropTypes.func.isRequired, + icons: PropTypes.array.isRequired, +}; +Icons = React.memo(Icons); + +const useDialogStyles = makeStyles(theme => ({ + markdown: { + '& pre': { + borderRadius: 0, + margin: 0, + }, + }, + import: { + textAlign: 'right', + padding: theme.spacing(0.5, 1), + }, + container: { + marginBottom: theme.spacing(5), + }, + canvas: { + fontSize: 210, + marginTop: theme.spacing(2), + color: theme.palette.primary.dark, + backgroundSize: '30px 30px', + backgroundColor: '#fff', + backgroundPosition: '0 0, 0 15px, 15px -15px, -15px 0px', + backgroundImage: + 'linear-gradient(45deg, #f4f4f4 25%, transparent 25%), linear-gradient(-45deg, #f4f4f4 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #f4f4f4 75%), linear-gradient(-45deg, transparent 75%, #f4f4f4 75%)', + }, + fontSize: { + margin: theme.spacing(2), + }, + context: { + margin: theme.spacing(0.5), + padding: theme.spacing(1, 2), + borderRadius: theme.shape.borderRadius, + boxSizing: 'content-box', + }, + contextPrimary: { + color: theme.palette.primary.main, + }, + contextPrimaryInverse: { + color: theme.palette.primary.contrastText, + backgroundColor: theme.palette.primary.main, + }, + contextTextPrimary: { + color: theme.palette.text.primary, + }, + contextTextPrimaryInverse: { + color: theme.palette.background.paper, + backgroundColor: theme.palette.text.primary, + }, + contextTextSecondary: { + color: theme.palette.text.secondary, + }, + contextTextSecondaryInverse: { + color: theme.palette.background.paper, + backgroundColor: theme.palette.text.secondary, + }, +})); + +let DialogDetails = props => { + const classes = useDialogStyles(); + const { open, selectedIcon, handleClose } = props; + + const handleClick = event => { + selectNode(event.currentTarget); + }; + + return ( + + {selectedIcon ? ( + + {selectedIcon.key} + + + Learn more about the import + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( +
+ )} +
+ ); +}; + +DialogDetails.propTypes = { + handleClose: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, + selectedIcon: PropTypes.object, +}; +DialogDetails = React.memo(DialogDetails); + +const useStyles = makeStyles(theme => ({ + form: { + margin: theme.spacing(2, 0), + }, + paper: { + position: 'sticky', + top: 80, + padding: '2px 4px', + display: 'flex', + alignItems: 'center', + marginBottom: theme.spacing(2), + width: '100%', + }, + input: { + marginLeft: 8, + flex: 1, + }, + iconButton: { + padding: 10, + }, + icon: { + display: 'inline-block', + width: 86, + overflow: 'hidden', + textOverflow: 'ellipsis', + textAlign: 'center', + color: theme.palette.text.secondary, + margin: '0 4px', + fontSize: 12, + '& p': { + margin: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, + }, + iconSvg: { + boxSizing: 'content-box', + cursor: 'pointer', + color: theme.palette.text.primary, + borderRadius: theme.shape.borderRadius, + transition: theme.transitions.create(['background-color', 'box-shadow'], { + duration: theme.transitions.duration.shortest, + }), + fontSize: 40, + padding: theme.spacing(2), + margin: theme.spacing(0.5, 0), + '&:hover': { + backgroundColor: theme.palette.background.paper, + boxShadow: theme.shadows[1], + }, + }, + results: { + marginBottom: theme.spacing(1), + }, +})); + +const searchIndex = FlexSearch.create({ + async: true, + tokenize: 'full', +}); + +const synonyms = { + AccessAlarm: 'clock stopwatch', + AccessAlarms: 'clock stopwatch', + AccessTime: 'clock stopwatch', + AccountCircle: 'user', + AccountBox: 'user', + ArrowBack: 'left', + ArrowBackIos: 'left', + Assignment: 'clipboard', + AssignmentInd: 'clipboard', + AssignmentLate: 'clipboard', + AssignmentReturn: 'clipboard', + AssignmentReturned: 'clipboard', + AssignmentTurnedIn: 'clipboard', + Backspace: 'delete', + Backup: 'cloud', + ExitToApp: 'logout sign out', + KeyboardBackspace: 'left', + Menu: 'hamburger', +}; + +const allIconsMap = {}; +const allIcons = Object.keys(mui) + .sort() + .map(key => { + let tag; + if (key.indexOf('Outlined') !== -1) { + tag = 'Outlined'; + } else if (key.indexOf('TwoTone') !== -1) { + tag = 'Two tone'; + } else if (key.indexOf('Rounded') !== -1) { + tag = 'Rounded'; + } else if (key.indexOf('Sharp') !== -1) { + tag = 'Sharp'; + } else { + tag = 'Filled'; + } + + let searchable = key.replace(/(Outlined|TwoTone|Rounded|Sharp)$/, ''); + if (synonyms[searchable]) { + searchable += ` ${synonyms[searchable]}`; + } + searchIndex.add(key, searchable); + + const icon = { + key, + tag, + Icon: mui[key], + }; + allIconsMap[key] = icon; + return icon; + }); + +export default function SearchIcons() { + const classes = useStyles(); + const [tag, setTag] = React.useState('Filled'); + const [keys, setKeys] = React.useState(null); + const [open, setOpen] = React.useState(false); + const [selectedIcon, setSelectedIcon] = React.useState(null); + + const handleClickOpen = React.useCallback(event => { + setSelectedIcon(allIconsMap[event.currentTarget.getAttribute('title')]); + setOpen(true); + }, []); + + const handleClose = React.useCallback(() => { + setOpen(false); + }, []); + + const isMounted = React.useRef(false); + React.useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + const handleChange = React.useMemo( + () => + debounce(value => { + if (!isMounted.current) { + return; + } + + if (value === '') { + setKeys(null); + } else { + searchIndex.search(value).then(results => { + setKeys(results); + + // Keep track of the no results so we can add synonyms in the future. + if (value.length >= 4 && results.length === 0) { + window.ga('send', { + hitType: 'event', + eventCategory: 'material-icons', + eventAction: value, + eventLabel: 'no results', + }); + } + }); + } + }, 220), + [], + ); + + const icons = React.useMemo( + () => + (keys === null ? allIcons : keys.map(key => allIconsMap[key])).filter( + icon => tag === icon.tag, + ), + [tag, keys], + ); + + return ( + + +
+ + {['Filled', 'Outlined', 'Rounded', 'Two tone', 'Sharp'].map(key => { + return ( + setTag(key)} value={key} />} + label={key} + /> + ); + })} + +
+
+ + + + + + { + handleChange(event.target.value); + }} + className={classes.input} + placeholder="Search icons…" + inputProps={{ 'aria-label': 'search icons' }} + /> + + {`${icons.length} matching results`} + + + +
+ ); +} diff --git a/docs/src/pages/components/material-icons/material-icons.md b/docs/src/pages/components/material-icons/material-icons.md new file mode 100644 index 00000000000000..54582a54be7d13 --- /dev/null +++ b/docs/src/pages/components/material-icons/material-icons.md @@ -0,0 +1,14 @@ +--- +title: Material Icons +components: Icon, SvgIcon +--- + +# Material Icons + +

1,000+ React Material icons ready to use from the official website.

+ +The following npm package, +[@material-ui/icons](https://www.npmjs.com/package/@material-ui/icons), +includes the 1,000+ official [Material icons](https://material.io/tools/icons/?style=baseline) converted to [`SvgIcon`](/api/svg-icon/) components. + +{{"demo": "pages/components/material-icons/SearchIcons.js", "hideHeader": true}} diff --git a/yarn.lock b/yarn.lock index e2f13c12514188..b809e8796dabc5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6687,6 +6687,11 @@ flatted@^2.0.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916" integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg== +flexsearch@^0.6.30: + version "0.6.30" + resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.6.30.tgz#d3f14389c9a4e5758b12290b3bafcd383cdc53de" + integrity sha512-zDBhMWbM65TsJJPBYoxV+MENufDylNtMz38e6MLTShwwuHeRNBxRYGAxR0DlwSkC4u+X2S8mlcdROWXMDleNwQ== + flow-parser@0.*: version "0.98.0" resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.98.0.tgz#f7512d6a4531f0b48669633d0acf4346a17f5184"