diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index df7e078a..2950bae9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -47,7 +47,7 @@ jobs: run: yarn workspace react-responsive-modal build - name: Build docs - run: yarn workspace react-responsive-modal docz:build + run: yarn workspace website build - name: Cypress run uses: cypress-io/github-action@v2 @@ -56,4 +56,4 @@ jobs: install: false # Use monorepo project: ./react-responsive-modal - start: yarn dlx serve -l 3000 react-responsive-modal/.docz/dist + start: yarn workspace website start -p 3000 diff --git a/.gitignore b/.gitignore index 9a5c62d2..4fc93bf7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,10 @@ node_modules .cache dist coverage -.docz +.next +out .yarn/* !.yarn/releases !.yarn/plugins .pnp.* +.next diff --git a/.prettierignore b/.prettierignore index c8a40459..5de87c6c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,6 @@ dist -.docz +.next +out coverage .yarn CHANGELOG.md diff --git a/.yarnrc.yml b/.yarnrc.yml index 11777f05..fecc5ee8 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -4,8 +4,8 @@ npmAuthToken: "${NPM_TOKEN-''}" plugins: - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs - spec: "@yarnpkg/plugin-interactive-tools" + spec: '@yarnpkg/plugin-interactive-tools' - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs - spec: "@yarnpkg/plugin-workspace-tools" + spec: '@yarnpkg/plugin-workspace-tools' yarnPath: .yarn/releases/yarn-2.3.3.cjs diff --git a/README.md b/README.md deleted file mode 120000 index dfd5ce29..00000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -./react-responsive-modal/README.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..6b513de2 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# react-responsive-modal + +[![npm version](https://img.shields.io/npm/v/react-responsive-modal.svg)](https://www.npmjs.com/package/react-responsive-modal) +[![npm downloads per month](https://img.shields.io/npm/dm/react-responsive-modal.svg)](https://www.npmjs.com/package/react-responsive-modal) +[![codecov](https://img.shields.io/codecov/c/github/pradel/react-responsive-modal/master.svg)](https://codecov.io/gh/pradel/react-responsive-modal) + +A simple responsive and accessible react modal. + +- Focus trap inside the modal. +- Centered modals. +- Scrolling modals. +- Multiple modals. +- Accessible modals. +- Easily customizable via props. + +## Documentation + +- [Getting started](https://react-responsive-modal.leopradel.com/) + - [Installation](https://react-responsive-modal.leopradel.com/#installation) + - [Usage](https://react-responsive-modal.leopradel.com/#usage) + - [Props](https://react-responsive-modal.leopradel.com/#props) + - [Licence](https://react-responsive-modal.leopradel.com/#license) +- [Examples](https://react-responsive-modal.leopradel.com/examples) + - [Centered modal](https://react-responsive-modal.leopradel.com/examples#centered-modal) + - [Multiple modal](https://react-responsive-modal.leopradel.com/examples#multiple-modal) + - [Custom styling](https://react-responsive-modal.leopradel.com/examples#custom-styling) + - [Custom animation](https://react-responsive-modal.leopradel.com/examples#custom-animation) + - [Custom container](https://react-responsive-modal.leopradel.com/examples#custom-container) + +## Installation + +With npm: `npm install react-responsive-modal --save` + +Or with yarn: `yarn add react-responsive-modal` + +## Usage + +[![Edit react-responsive-modal](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/9jxp669j2o) + +```javascript +import React, { useState } from 'react'; +import ReactDOM from 'react-dom'; +import 'react-responsive-modal/styles.css'; +import { Modal } from 'react-responsive-modal'; + +const App = () => { + const [open, setOpen] = useState(false); + + const onOpenModal = () => setOpen(true); + const onCloseModal = () => setOpen(false); + + return ( +
+ + +

Simple centered modal

+
+
+ ); +}; + +ReactDOM.render(, document.getElementById('app')); +``` + +## Props + +Check the documentation: https://react-responsive-modal.leopradel.com/#props. + +## License + +MIT © [Léo Pradel](https://www.leopradel.com/) diff --git a/netlify.toml b/netlify.toml index b7180ab5..aabf49b4 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,7 +1,7 @@ [build] - publish = "react-responsive-modal/.docz/dist" - command = "yarn workspace react-responsive-modal docz:build" + publish = "website/out" + command = "yarn workspace react-responsive-modal build && yarn workspace website build && yarn workspace website export" [build.environment] NODE_VERSION = "12" diff --git a/package.json b/package.json index 7cbeb1eb..fdf31214 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "private": true, "scripts": { - "prettier": "prettier --write \"**/*.{js,ts,tsx,css,scss,json,md,mdx}\"" + "prettier": "prettier --write \"**/*.{js,ts,tsx,css,scss,json,md,mdx,yml}\"" }, "workspaces": [ - "react-responsive-modal" + "react-responsive-modal", + "website" ], "prettier": { "singleQuote": true @@ -15,7 +16,7 @@ } }, "lint-staged": { - "*.{js,ts,tsx,css,scss,json,md,mdx}": "prettier --write" + "*.{js,ts,tsx,css,scss,json,md,mdx,yml}": "prettier --write" }, "devDependencies": { "husky": "4.3.0", diff --git a/react-responsive-modal/cypress/integration/modal.spec.ts b/react-responsive-modal/cypress/integration/modal.spec.ts index 5f7ab5b3..a8f7184d 100644 --- a/react-responsive-modal/cypress/integration/modal.spec.ts +++ b/react-responsive-modal/cypress/integration/modal.spec.ts @@ -2,37 +2,37 @@ describe('simple modal', () => { beforeEach(() => { - cy.visit('http://localhost:3000/examples'); + cy.visit('http://localhost:3000'); // Page is heavy to load so we wait for it to be loaded cy.wait(500); }); it('should open modal when clicking open button', () => { - cy.get('button').eq(2).click(); + cy.get('button').eq(0).click(); cy.get('[data-testid=modal]').should('exist'); }); // TODO overlay not working, see how to fix // it('should close modal when clicking overlay', () => { - // cy.get('button').eq(2).click(); + // cy.get('button').eq(0).click(); // cy.get('[data-testid=overlay]').click(); // cy.get('[data-testid=modal]').should('not.exist'); // }); it('should close modal when clicking the close icon', () => { - cy.get('button').eq(2).click(); + cy.get('button').eq(0).click(); cy.get('[data-testid=close-button]').click(); cy.get('[data-testid=modal]').should('not.exist'); }); it('should close modal when pressing esc key', () => { - cy.get('button').eq(2).click(); + cy.get('button').eq(0).click(); cy.get('body').type('{esc}'); cy.get('[data-testid=modal]').should('not.exist'); }); it('should close only last modal when pressing esc key when multiple modals are opened', () => { - cy.get('button').eq(8).click(); + cy.get('button').eq(1).click(); cy.get('[data-testid=modal] button').eq(0).click(); cy.get('[data-testid=modal]').should('have.length', 2); cy.get('body').type('{esc}'); @@ -42,19 +42,19 @@ describe('simple modal', () => { }); it('should block the scroll when modal is opened', () => { - cy.get('button').eq(2).click(); + cy.get('button').eq(0).click(); cy.get('html').should('have.css', 'position', 'fixed'); }); it('should unblock the scroll when modal is closed', () => { - cy.get('button').eq(2).click(); + cy.get('button').eq(0).click(); cy.get('html').should('have.css', 'position', 'fixed'); cy.get('body').type('{esc}'); cy.get('html').should('not.have.css', 'position', 'fixed'); }); it('should unblock scroll only after last modal is closed when multiple modals are opened', () => { - cy.get('button').eq(8).click(); + cy.get('button').eq(1).click(); cy.get('[data-testid=modal] button').eq(0).click(); cy.get('[data-testid=modal]').should('have.length', 2); cy.get('html').should('have.css', 'position', 'fixed'); diff --git a/react-responsive-modal/docs/docs.css b/react-responsive-modal/docs/docs.css deleted file mode 100644 index 9550ea8e..00000000 --- a/react-responsive-modal/docs/docs.css +++ /dev/null @@ -1,13 +0,0 @@ -.button { - padding-left: 1.5rem; - padding-right: 1.5rem; - padding-top: 0.75rem; - padding-bottom: 0.75rem; - font-weight: 700; - border-radius: 0.25rem; - background-color: #fd4659; - color: #ffffff; - cursor: pointer; - border: none; - font-size: 0.8rem; -} diff --git a/react-responsive-modal/docs/examples.mdx b/react-responsive-modal/docs/examples.mdx deleted file mode 100644 index 3569e791..00000000 --- a/react-responsive-modal/docs/examples.mdx +++ /dev/null @@ -1,358 +0,0 @@ ---- -name: Examples -route: /examples -order: 2 ---- - -import { Playground } from 'docz'; -import './docs.css'; -import '../styles.css'; -import { Modal } from '../src/index.tsx'; - -# Examples - -## Centered modal - -If you want your modal to be centered in the screen just pass the `center` prop. - - - {() => { - const [open, setOpen] = React.useState(false); - return ( - <> - - setOpen(false)} center> -

Simple centered modal

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam - pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet - hendrerit risus, sed porttitor quam. -

-
- - ); - }} -
- -## Big modal - -When a big modal is open, you can scroll the content of the modal but you will see that the body scroll is locked until you actually close the modal. - - - {() => { - const [open, setOpen] = React.useState(false); - const lorem = ( -

- Mauris ac arcu sit amet dui interdum bibendum a sed diam. Praesent - rhoncus congue ipsum elementum lobortis. Ut ligula purus, ultrices id - condimentum quis, tincidunt quis purus. Proin quis enim metus. Nunc - feugiat odio at eros porta, ut rhoncus lorem tristique. Nunc et ipsum eu - ex vulputate consectetur vel eu nisi. Donec ultricies rutrum lectus, sit - ame feugiat est semper vitae. Proin varius imperdiet consequat. Proin eu - metus nisi. In hac habitasse platea dictumst. Vestibulum ac ultrices - risus. Pellentesque arcu sapien, aliquet sed orci sit amet, pulvinar - interdum velit. Nunc a rhoncus ipsum, maximus fermentum dolor. Praesent - aliquet justo vitae rutrum volutpat. Ut quis pulvinar est. -

- ); - return ( - <> - - setOpen(false)}> -

Big modal

- {lorem} - {lorem} - {lorem} - {lorem} - {lorem} - {lorem} - {lorem} - {lorem} - {lorem} -
- - ); - }} -
- -## Multiple modal - - - {() => { - const [openFirst, setOpenFirst] = React.useState(false); - const [openSecond, setOpenSecond] = React.useState(false); - const littleLorem = ( -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar - risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit - risus, sed porttitor quam. -

- ); - return ( - <> - - setOpenFirst(false)} center> -

First modal

- {littleLorem} - -
- setOpenSecond(false)} center> -

Second modal

- {littleLorem} -
- - ); - }} -
- -## Focus Trapped modal - -By default, when the modal open, the first focusable element will be focused. Press Tab to navigate between the focusable elements in the modal. You can notice that when the modal is open, you can't focus the elements outside of it. - - - {() => { - const [open, setOpen] = React.useState(false); - return ( - <> - - setOpen(false)}> -

Try tabbing/shift-tabbing thru elements

-
-

- -

-

- -

- - -
-
- - ); - }} -
- -## Custom styling with css - -import './examples/custom-styling.css'; - -It's really easy to add your own style to the modal. -For example if you add the following css to your app you will get the following result: - -```css -/* examples/custom-styling.css */ -.customOverlay { - background: rgba(36, 123, 160, 0.7); -} -.customModal { - background: #b2dbbf; - max-width: 500px; - width: 100%; -} -``` - - - {() => { - // import './examples/custom-styling.css'; - const [open, setOpen] = React.useState(false); - return ( - <> - - setOpen(false)} - center - classNames={{ - overlay: 'customOverlay', - modal: 'customModal', - }} - > -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam - pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet - hendrerit risus, sed porttitor quam. -

-
- - ); - }} -
- -## Custom animation - -import './examples/custom-animation.css'; - -If you want to change the default animation, you can do so by creating your own css animation. -For example if you add the following css to your app you will get the following result: - -```css -/* examples/custom-animation.css */ -@keyframes customEnterAnimation { - 0% { - transform: scale(0); - } - 100% { - transform: scale(1); - } -} - -@keyframes customLeaveAnimation { - 0% { - transform: scale(1); - } - 100% { - transform: scale(0); - } -} -``` - - - {() => { - // import './examples/custom-animation.css'; - const [open, setOpen] = React.useState(false); - return ( - <> - - setOpen(false)} - center - classNames={{ - animationIn: 'customEnterAnimation', - animationOut: 'customLeaveAnimation', - }} - animationDuration={1000} - > -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam - pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet - hendrerit risus, sed porttitor quam. -

-
- - ); - }} -
- -If you want to apply a custom animation to the modal body you can do like this: - - - {() => { - // import './examples/custom-animation.css'; - const [open, setOpen] = React.useState(false); - return ( - <> - - setOpen(false)} - center - styles={{ - modal: { - animation: `${ - open ? 'customEnterAnimation' : 'customLeaveAnimation' - } 500ms`, - }, - }} - > -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam - pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet - hendrerit risus, sed porttitor quam. -

-
- - ); - }} -
- -## Custom close icon - -You can specify a custom close icon (svg, img..). - - - {() => { - const [open, setOpen] = React.useState(false); - const closeIcon = ( - - - - ); - return ( - <> - - setOpen(false)} - center - closeIcon={closeIcon} - > -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam - pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet - hendrerit risus, sed porttitor quam. -

-
- - ); - }} -
- -## Custom container - -By default, the modal will be rendered at the end of the html body tag. If you want to render the modal in your own container, you can pass a valid `Element` to the `container` prop. - - - {() => { - const [open, setOpen] = React.useState(false); - const myRef = React.useRef(); - return ( - <> -
- - setOpen(false)} - center - container={myRef.current} - > -

- Take a look with the devtools, you can see that the modal is inside - the div we are targeting and not at the end of the body tag. -

-
- - ); - }} - diff --git a/react-responsive-modal/docs/index.mdx b/react-responsive-modal/docs/index.mdx deleted file mode 100644 index d60dbf00..00000000 --- a/react-responsive-modal/docs/index.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -name: Getting started -route: / -order: 1 ---- - -import { Props } from 'docz'; -import { Modal } from '../src/index.tsx'; - -# react-responsive-modal - -A simple responsive and accessible react modal. - -- Focus trap inside the modal. -- Centered modals. -- Scrolling modals. -- Multiple modals. -- Accessible modals. -- Easily customizable via props. - -## Installation - -With npm: - -`npm install react-responsive-modal --save` - -Or with yarn: - -`yarn add react-responsive-modal` - -## Usage - -[![Edit react-responsive-modal](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/9jxp669j2o) - -```javascript -import React from 'react'; -import ReactDOM from 'react-dom'; -import 'react-responsive-modal/styles.css'; -import { Modal } from 'react-responsive-modal'; - -export default class App extends React.Component { - state = { - open: false, - }; - - onOpenModal = () => { - this.setState({ open: true }); - }; - - onCloseModal = () => { - this.setState({ open: false }); - }; - - render() { - const { open } = this.state; - return ( -
- - -

Simple centered modal

-
-
- ); - } -} - -ReactDOM.render(, document.getElementById('app')); -``` - -## Props - - - -## License - -MIT © [Léo Pradel](https://www.leopradel.com/) diff --git a/react-responsive-modal/doczrc.js b/react-responsive-modal/doczrc.js deleted file mode 100644 index 2afeaff4..00000000 --- a/react-responsive-modal/doczrc.js +++ /dev/null @@ -1,4 +0,0 @@ -export default { - typescript: true, - files: 'docs/**/*.{md,markdown,mdx}', -}; diff --git a/react-responsive-modal/gatsby-config.js b/react-responsive-modal/gatsby-config.js deleted file mode 100644 index f8f1c741..00000000 --- a/react-responsive-modal/gatsby-config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - plugins: ['gatsby-theme-docz'], -}; diff --git a/react-responsive-modal/package.json b/react-responsive-modal/package.json index 2aaf74a6..b4e831e9 100644 --- a/react-responsive-modal/package.json +++ b/react-responsive-modal/package.json @@ -12,8 +12,6 @@ "test": "tsdx test --passWithNoTests", "lint": "tsdx lint", "prepare": "tsdx build", - "docz:dev": "docz dev", - "docz:build": "docz build", "size": "size-limit" }, "files": [ @@ -66,22 +64,20 @@ }, "devDependencies": { "@size-limit/preset-small-lib": "4.7.0", - "@testing-library/jest-dom": "5.11.5", - "@testing-library/react": "11.1.1", + "@testing-library/jest-dom": "5.11.6", + "@testing-library/react": "11.1.2", "@types/classnames": "2.2.11", "@types/no-scroll": "2.1.0", - "@types/node": "14.14.6", + "@types/node": "14.14.7", "@types/react": "16.9.56", "@types/react-dom": "16.9.9", "@types/react-transition-group": "4.4.0", - "cypress": "5.5.0", - "docz": "2.3.1", - "gatsby": "2.23.11", - "gatsby-theme-docz": "2.3.1", + "babel-jest": "26.6.3", + "cypress": "5.6.0", "husky": "4.3.0", "prettier": "2.1.2", - "react": "16.14.0", - "react-dom": "16.14.0", + "react": "17.0.1", + "react-dom": "17.0.1", "size-limit": "4.7.0", "tsdx": "0.14.1", "tslib": "2.0.3", diff --git a/react-responsive-modal/src/index.tsx b/react-responsive-modal/src/index.tsx index 55ad8f24..a84ba231 100644 --- a/react-responsive-modal/src/index.tsx +++ b/react-responsive-modal/src/index.tsx @@ -46,6 +46,8 @@ export interface ModalProps { blockScroll?: boolean; /** * Show the close icon. + * + * Default to true. */ showCloseIcon?: boolean; /** @@ -67,7 +69,7 @@ export interface ModalProps { * The portal will be rendered inside that element. * The default behavior will create a div node and render it at the at the end of document.body. */ - container?: Element; + container?: Element | null; /** * An object containing classNames to style the modal. */ diff --git a/website/next-env.d.ts b/website/next-env.d.ts new file mode 100644 index 00000000..10ac1f8f --- /dev/null +++ b/website/next-env.d.ts @@ -0,0 +1,4 @@ +/// +/// + +declare module 'babel-plugin-preval/macro'; diff --git a/website/next.config.js b/website/next.config.js new file mode 100644 index 00000000..ea1ed14d --- /dev/null +++ b/website/next.config.js @@ -0,0 +1,46 @@ +const remarkHighlight = require('remark-highlight.js'); +const rehypeSlug = require('rehype-slug'); +const rehypeHeadings = require('rehype-autolink-headings'); +const remarkCodeImport = require('remark-code-import'); +const withMDX = require('@next/mdx')({ + options: { + remarkPlugins: [remarkCodeImport, remarkHighlight], + rehypePlugins: [ + rehypeSlug, + [ + rehypeHeadings, + { + properties: { + ariaHidden: true, + tabIndex: -1, + className: 'anchor', + }, + content: { + type: 'element', + tagName: 'svg', + properties: { + xmlns: 'http://www.w3.org/2000/svg', + viewBox: '0 0 20 20', + fill: 'currentColor', + }, + children: [ + // Svg from https://heroicons.com/ + { + type: 'element', + tagName: 'path', + properties: { + fillRule: 'evenodd', + d: + 'M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z', + clipRule: 'evenodd', + }, + }, + ], + }, + }, + ], + ], + }, +}); + +module.exports = withMDX(); diff --git a/website/package.json b/website/package.json new file mode 100644 index 00000000..9cb13c73 --- /dev/null +++ b/website/package.json @@ -0,0 +1,35 @@ +{ + "name": "website", + "private": true, + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start", + "export": "next export", + "type-check": "tsc" + }, + "dependencies": { + "next": "10.0.1", + "react": "17.0.1", + "react-dom": "17.0.1", + "react-responsive-modal": "5.2.6" + }, + "devDependencies": { + "@mdx-js/loader": "1.6.21", + "@next/mdx": "10.0.1", + "@tailwindcss/typography": "0.2.0", + "@types/node": "14.14.7", + "@types/react": "16.9.56", + "@types/react-dom": "16.9.9", + "fathom-client": "3.0.0", + "highlight.js": "10.3.2", + "rehype-autolink-headings": "5.0.1", + "rehype-slug": "4.0.1", + "remark-code-import": "0.2.0", + "remark-highlight.js": "6.0.0", + "tailwindcss": "1.9.6", + "typeface-inter": "1.1.13", + "typescript": "4.0.5" + }, + "license": "MIT" +} diff --git a/website/postcss.config.js b/website/postcss.config.js new file mode 100644 index 00000000..9b6634e7 --- /dev/null +++ b/website/postcss.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: ['tailwindcss', 'autoprefixer'], +}; diff --git a/website/src/components/ExampleRendered.tsx b/website/src/components/ExampleRendered.tsx new file mode 100644 index 00000000..461bcb81 --- /dev/null +++ b/website/src/components/ExampleRendered.tsx @@ -0,0 +1,32 @@ +import Simple from '../examples/Simple'; +import ExampleMultiple from '../examples/Multiple'; +import LongContent from '../examples/LongContent'; +import FocusTrapped from '../examples/FocusTrapped'; +import CustomCssStyle from '../examples/CustomCssStyle'; +import CustomAnimation from '../examples/CustomAnimation'; +import CustomCloseIcon from '../examples/CustomCloseIcon'; +import CustomContainer from '../examples/CustomContainer'; + +const examples: Record JSX.Element> = { + simple: Simple, + multiple: ExampleMultiple, + longContent: LongContent, + focusTrapped: FocusTrapped, + customCssStyle: CustomCssStyle, + customAnimation: CustomAnimation, + customCloseIcon: CustomCloseIcon, + customContainer: CustomContainer, +}; + +interface ExampleRenderedProps { + name: string; +} + +export const ExampleRendered = ({ name }: ExampleRenderedProps) => { + const Example = examples[name]; + if (!Example) { + throw new Error('example not found'); + } + + return ; +}; diff --git a/website/src/components/Footer.tsx b/website/src/components/Footer.tsx new file mode 100644 index 00000000..27406402 --- /dev/null +++ b/website/src/components/Footer.tsx @@ -0,0 +1,77 @@ +import { config } from '../config'; + +export const Footer = () => { + return ( + + ); +}; diff --git a/website/src/components/Header.tsx b/website/src/components/Header.tsx new file mode 100644 index 00000000..078e5a07 --- /dev/null +++ b/website/src/components/Header.tsx @@ -0,0 +1,40 @@ +import packageJson from 'react-responsive-modal/package.json'; + +export const Header = () => { + return ( + + ); +}; diff --git a/website/src/config.ts b/website/src/config.ts new file mode 100644 index 00000000..c8958ab2 --- /dev/null +++ b/website/src/config.ts @@ -0,0 +1,5 @@ +export const config = { + websiteUrl: 'https://www.leopradel.com', + twitterUrl: 'https://twitter.com/leopradel', + githubUrl: 'https://github.com/pradel', +}; diff --git a/website/src/docs/index.mdx b/website/src/docs/index.mdx new file mode 100644 index 00000000..f93cdd32 --- /dev/null +++ b/website/src/docs/index.mdx @@ -0,0 +1,194 @@ +import { ExampleRendered } from '../components/ExampleRendered'; + +A simple responsive and accessible react modal. + +- Focus trap inside the modal. +- Centered modals. +- Scrolling modals. +- Multiple modals. +- Accessible modals. +- Easily customizable via props. + +## Installation + +Inside your project directory, install react-responsive-modal by running the following: + +```sh +npm install react-responsive-modal --save +# Or with yarn +yarn add react-responsive-modal +``` + +## Usage + +[![Edit react-responsive-modal](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/9jxp669j2o) + + + +```javascript +import React, { useState } from 'react'; +import ReactDOM from 'react-dom'; +import 'react-responsive-modal/styles.css'; +import { Modal } from 'react-responsive-modal'; + +const App = () => { + const [open, setOpen] = useState(false); + + const onOpenModal = () => setOpen(true); + const onCloseModal = () => setOpen(false); + + return ( +
+ + +

Simple centered modal

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam + pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet + hendrerit risus, sed porttitor quam. +

+
+
+ ); +}; + +ReactDOM.render(, document.getElementById('app')); +``` + +- If you are using Next.js, you need to import the styles in `pages/_app.js` or `pages/_app.tsx`. +- If you are using Create React App, you need to import the styles in `index.js` or `index.tsx`. + +### Multiple modals + +You can render multiple modals at the same time. Clicking on the overlay or pressing "esc" will only close the last modal. + + + +```js file=../examples/Multiple.tsx + +``` + +### Modal with a lot of content + +When a modal with a content overflowing the window size, you can scroll the content of the modal but you will see that the body scroll is locked until you actually close the modal. + + + +```js file=../examples/LongContent.tsx + +``` + +### Focus Trapped modal + +By default, when the modal open, the first focusable element will be focused. Press Tab to navigate between the focusable elements in the modal. You can notice that when the modal is open, you can't focus the elements outside of it. +If you want to disable this behavior, set the `focusTrapped` prop to `false`. + + + +```js file=../examples/FocusTrapped.tsx + +``` + +### Custom styling with css + +Customising the Modal style via css is really easy. For example if you add the following css to your app you will get the following result: + +```css file=../examples/custom-styling.css + +``` + + + +```js file=../examples/CustomCssStyle.tsx + +``` + +### Custom animation + +If you want to change the default animation, you can do so by creating your own css animation. For example if you add the following css to your app you will get the following result: + +```css file=../examples/custom-animation.css + +``` + + + +```js file=../examples/CustomAnimation.tsx + +``` + +If you want to apply a custom animation to the modal body you can do like this: + +### Custom close icon + +You can customise the close icon used by the Modal by providing your own image or svg. + + + +```js file=../examples/CustomCloseIcon.tsx + +``` + +### Custom container + +By default, the Modal will be rendered at the end of the html body tag. If you want to render the Modal in your own container, you can pass your own Element to the container prop. + + + +```js file=../examples/CustomContainer.tsx + +``` + +## Accessibility + +- Use the `aria-labelledby` and `aria-describedby` props to follow the [ARIA best practices](https://www.w3.org/TR/wai-aria-practices/#dialog_modal). + +```javascript + +

My Title

+

My Description

+
+``` + +- `aria-modal` is set to true automatically. +- When the modal is open the focus is trapped within it. +- Clicking on the overlay closes the Modal. +- Pressing the "Esc" key closes the Modal. +- When the modal is open the page scroll is blocked for the elements behind the modal. +- Closing the modal will unblock the scroll. +- The modal is rendered in a portal at the end of the `body`. + +## Props + +| Name | Type | Default | Description | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **open\*** | `boolean` | | Control if the modal is open or not. | +| **center** | `boolean` | false | Should the dialog be centered. | +| **closeOnEsc** | `boolean` | true | Is the modal closable when user press esc key. | +| **closeOnOverlayClick** | `boolean` | true | Is the modal closable when user click on overlay. | +| **blockScroll** | `boolean` | true | Whether to block scrolling when dialog is open. | +| **showCloseIcon** | `boolean` | true | Show the close icon. | +| **closeIconId** | `string` | | id attribute for the close icon button. | +| **closeIcon** | `React.ReactNode` | | Custom icon to render (svg, img, etc...). | +| **focusTrapped** | `boolean` | true | When the modal is open, trap focus within it. | +| **container** | `Element` | | You can specify a container prop which should be of type `Element`. The portal will be rendered inside that element. The default behavior will create a div node and render it at the at the end of document.body. | +| **classNames** | `{ overlay?: string; modal?: string; closeButton?: string; closeIcon?: string; animationIn?: string; animationOut?: string; }` | | An object containing classNames to style the modal. | +| **styles** | `{ overlay?: string; modal?: string; closeButton?: string; closeIcon?: string; }` | | An object containing the styles objects to style the modal. | +| **animationDuration** | `number` | 500 | Animation duration in milliseconds. | +| **role** | `string` | "dialog" | ARIA role for modal | +| **ariaLabelledby** | `string` | | ARIA label for modal | +| **ariaDescribedby** | `string` | | ARIA description for modal | +| **modalId** | `string` | | id attribute for modal | +| **onClose\*** | `() => void` | | Callback fired when the Modal is requested to be closed by a click on the overlay or when user press esc key. | +| **onEscKeyDown\*** | `(event: KeyboardEvent) => void` | | Callback fired when the escape key is pressed. | +| **onOverlayClick\*** | `(event: React.MouseEvent) => void` | | Callback fired when the overlay is clicked. | +| **onAnimationEnd\*** | `() => void` | | Callback fired when the Modal has exited and the animation is finished. | + +## License + +react-responsive-modal is licensed under the [MIT license](https://github.com/pradel/react-responsive-modal/blob/master/LICENSE). diff --git a/website/src/examples/CustomAnimation.tsx b/website/src/examples/CustomAnimation.tsx new file mode 100644 index 00000000..218ea209 --- /dev/null +++ b/website/src/examples/CustomAnimation.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Modal } from 'react-responsive-modal'; + +const App = () => { + // import './examples/custom-animation.css'; + const [open, setOpen] = React.useState(false); + + return ( + <> + + + setOpen(false)} + center + classNames={{ + animationIn: 'customEnterAnimation', + animationOut: 'customLeaveAnimation', + }} + animationDuration={1000} + > +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam + pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet + hendrerit risus, sed porttitor quam. +

+
+ + ); +}; + +export default App; diff --git a/website/src/examples/CustomCloseIcon.tsx b/website/src/examples/CustomCloseIcon.tsx new file mode 100644 index 00000000..dbc021f9 --- /dev/null +++ b/website/src/examples/CustomCloseIcon.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Modal } from 'react-responsive-modal'; + +const App = () => { + const [open, setOpen] = React.useState(false); + + const closeIcon = ( + + + + ); + + return ( + <> + + + setOpen(false)} + center + closeIcon={closeIcon} + > +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam + pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet + hendrerit risus, sed porttitor quam. +

+
+ + ); +}; + +export default App; diff --git a/website/src/examples/CustomContainer.tsx b/website/src/examples/CustomContainer.tsx new file mode 100644 index 00000000..def528ed --- /dev/null +++ b/website/src/examples/CustomContainer.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Modal } from 'react-responsive-modal'; + +const App = () => { + const [open, setOpen] = React.useState(false); + + const myRef = React.useRef(null); + + return ( + <> +
+ + setOpen(false)} + center + container={myRef.current} + > +

+ Take a look with the devtools, you can see that the modal is inside + the div we are targeting and not at the end of the body tag. +

+
+ + ); +}; + +export default App; diff --git a/website/src/examples/CustomCssStyle.tsx b/website/src/examples/CustomCssStyle.tsx new file mode 100644 index 00000000..d92d3671 --- /dev/null +++ b/website/src/examples/CustomCssStyle.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Modal } from 'react-responsive-modal'; + +const App = () => { + // import './examples/custom-styling.css'; + const [open, setOpen] = React.useState(false); + + return ( + <> + + + setOpen(false)} + center + classNames={{ + overlay: 'customOverlay', + modal: 'customModal', + }} + > +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam + pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet + hendrerit risus, sed porttitor quam. +

+
+ + ); +}; + +export default App; diff --git a/website/src/examples/FocusTrapped.tsx b/website/src/examples/FocusTrapped.tsx new file mode 100644 index 00000000..10d79fca --- /dev/null +++ b/website/src/examples/FocusTrapped.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Modal } from 'react-responsive-modal'; + +const App = () => { + const [open, setOpen] = React.useState(false); + + return ( + <> + + + setOpen(false)}> +

Try tabbing/shift-tabbing thru elements

+
+

+ +

+

+ +

+ + +
+
+ + ); +}; + +export default App; diff --git a/website/src/examples/LongContent.tsx b/website/src/examples/LongContent.tsx new file mode 100644 index 00000000..0bb7a313 --- /dev/null +++ b/website/src/examples/LongContent.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Modal } from 'react-responsive-modal'; + +const App = () => { + const [open, setOpen] = React.useState(false); + + const lorem = ( +

+ Mauris ac arcu sit amet dui interdum bibendum a sed diam. Praesent rhoncus + congue ipsum elementum lobortis. Ut ligula purus, ultrices id condimentum + quis, tincidunt quis purus. Proin quis enim metus. Nunc feugiat odio at + eros porta, ut rhoncus lorem tristique. Nunc et ipsum eu ex vulputate + consectetur vel eu nisi. Donec ultricies rutrum lectus, sit ame feugiat + est semper vitae. Proin varius imperdiet consequat. Proin eu metus nisi. + In hac habitasse platea dictumst. Vestibulum ac ultrices risus. + Pellentesque arcu sapien, aliquet sed orci sit amet, pulvinar interdum + velit. Nunc a rhoncus ipsum, maximus fermentum dolor. Praesent aliquet + justo vitae rutrum volutpat. Ut quis pulvinar est. +

+ ); + + return ( + <> + + + setOpen(false)}> +

Big modal

+ {lorem} + {lorem} + {lorem} + {lorem} + {lorem} + {lorem} + {lorem} + {lorem} + {lorem} +
+ + ); +}; + +export default App; diff --git a/website/src/examples/Multiple.tsx b/website/src/examples/Multiple.tsx new file mode 100644 index 00000000..7e52f51a --- /dev/null +++ b/website/src/examples/Multiple.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Modal } from 'react-responsive-modal'; + +const App = () => { + const [openFirst, setOpenFirst] = React.useState(false); + const [openSecond, setOpenSecond] = React.useState(false); + + const littleLorem = ( +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar + risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit + risus, sed porttitor quam. +

+ ); + + return ( + <> + + + setOpenFirst(false)} center> +

First modal

+ {littleLorem} + +
+ setOpenSecond(false)} center> +

Second modal

+ {littleLorem} +
+ + ); +}; + +export default App; diff --git a/website/src/examples/Simple.tsx b/website/src/examples/Simple.tsx new file mode 100644 index 00000000..394989ee --- /dev/null +++ b/website/src/examples/Simple.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Modal } from 'react-responsive-modal'; + +const App = () => { + const [open, setOpen] = React.useState(false); + + const onOpenModal = () => setOpen(true); + const onCloseModal = () => setOpen(false); + + return ( +
+ + + +

Simple centered modal

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam + pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet + hendrerit risus, sed porttitor quam. +

+
+
+ ); +}; + +export default App; diff --git a/react-responsive-modal/docs/examples/custom-animation.css b/website/src/examples/custom-animation.css similarity index 85% rename from react-responsive-modal/docs/examples/custom-animation.css rename to website/src/examples/custom-animation.css index 99dcc0f1..0c116690 100644 --- a/react-responsive-modal/docs/examples/custom-animation.css +++ b/website/src/examples/custom-animation.css @@ -1,3 +1,4 @@ +/* examples/custom-animation.css */ @keyframes customEnterAnimation { 0% { transform: scale(0); @@ -6,7 +7,6 @@ transform: scale(1); } } - @keyframes customLeaveAnimation { 0% { transform: scale(1); diff --git a/react-responsive-modal/docs/examples/custom-styling.css b/website/src/examples/custom-styling.css similarity index 79% rename from react-responsive-modal/docs/examples/custom-styling.css rename to website/src/examples/custom-styling.css index 1d82302c..67f26b0a 100644 --- a/react-responsive-modal/docs/examples/custom-styling.css +++ b/website/src/examples/custom-styling.css @@ -1,3 +1,4 @@ +/* examples/custom-styling.css */ .customOverlay { background: rgba(36, 123, 160, 0.7); } diff --git a/website/src/pages/_app.tsx b/website/src/pages/_app.tsx new file mode 100644 index 00000000..319df70f --- /dev/null +++ b/website/src/pages/_app.tsx @@ -0,0 +1,29 @@ +import React, { useEffect } from 'react'; +import { AppProps } from 'next/app'; +import Router from 'next/router'; +import * as Fathom from 'fathom-client'; +import 'typeface-inter'; +import 'react-responsive-modal/styles.css'; +import '../examples/custom-styling.css'; +import '../examples/custom-animation.css'; +// highlight.js theme +import '../styles/atom-one-light.css'; +import '../styles/index.css'; + +// Record a pageview when route changes +Router.events.on('routeChangeComplete', () => { + Fathom.trackPageview(); +}); + +function MyApp({ Component, pageProps }: AppProps) { + // Initialize Fathom when the app loads + useEffect(() => { + Fathom.load('PIMHMGXF', { + excludedDomains: ['localhost'], + }); + }, []); + + return ; +} + +export default MyApp; diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx new file mode 100644 index 00000000..a797c113 --- /dev/null +++ b/website/src/pages/index.tsx @@ -0,0 +1,111 @@ +// @ts-ignore +import Content from '../docs/index.mdx'; +import { Header } from '../components/Header'; +import { Footer } from '../components/Footer'; + +const IndexPage = () => ( + <> +
+ + + +