diff --git a/.eslintrc.js b/.eslintrc.js index 4f77473..96b990b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,7 +7,6 @@ module.exports = { extends: [ 'plugin:react/recommended', 'airbnb', - // 'plugin:i18next/recommended', ], parser: '@typescript-eslint/parser', parserOptions: { @@ -21,6 +20,7 @@ module.exports = { 'react', '@typescript-eslint', 'i18next', + 'react-hooks', ], rules: { 'react/jsx-indent': [2, 4], @@ -42,24 +42,13 @@ module.exports = { 'import/no-extraneous-dependencies': 'off', 'no-underscore-dangle': 'off', 'no-tabs': 'off', - // 'i18next/no-literal-string': [ - // 'error', - // { - // markupOnly: true, - // ignoreAttribute: ['data-testid', 'to'], - // }, - // ], + 'jsx-a11y/no-static-element-interactions': 'off', + 'jsx-a11y/click-events-have-key-events': 'off', + 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks + 'react-hooks/exhaustive-deps': 'error', // Checks effect dependencies 'max-len': ['error', { ignoreComments: true, code: 180 }], }, globals: { __IS_DEV__: true, }, - // overrides: [ - // { - // files: ['**/src/**/*.test.{ts,tsx}'], - // rules: { - // 'i18next/no-literal-string': 'off', - // }, - // }, - // ], }; diff --git a/src/app/App.tsx b/src/app/App.tsx index 52d948f..420b66a 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,18 +1,30 @@ -import React, { Suspense, useEffect } from 'react'; +import React, { Suspense, useState } from 'react'; import './styles/index.scss'; import { classNames } from 'shared/lib/classNames/classNames'; import { useTheme } from 'app/providers/ThemeProvider'; import { AppRouter } from 'app/providers/router'; import { Navbar } from 'widgets/Navbar'; import { Sidebar } from 'widgets/Sidebar'; +import { Modal } from 'shared/ui/Modal/Modal'; function App() { const { theme } = useTheme(); + const [isOpen, setIsOpen] = useState(false); + return (
+ + setIsOpen(false)}> + Lorem ipsum dolor sit amet, consectetur adipisicing elit. A ab + accusantium aliquid architecto autem consequuntur debitis et facere, + laudantium modi nostrum nulla obcaecati odit quas quod sapiente, + veritatis? Illo, veritatis. +
diff --git a/src/app/styles/variables/global.scss b/src/app/styles/variables/global.scss index b7ee4c9..2f256bc 100644 --- a/src/app/styles/variables/global.scss +++ b/src/app/styles/variables/global.scss @@ -14,4 +14,10 @@ --navbar-height: 50px; --sidebar-width: 300px; --sidebar-width-collapsed: 80px; + + // z - Индексы + --modal-z-index: 10; + + // Цвета + --overlay-color: rgba(0 0 0 / 60%); } diff --git a/src/shared/ui/Modal/Modal.module.scss b/src/shared/ui/Modal/Modal.module.scss new file mode 100644 index 0000000..acb14cf --- /dev/null +++ b/src/shared/ui/Modal/Modal.module.scss @@ -0,0 +1,44 @@ +.Modal { + position: fixed; + top: 0; + bottom: 0; + right: 0; + left: 0; + opacity: 0; + pointer-events: none; + z-index: -1; +} + +.overlay { + width: 100%; + height: 100%; + background: var(--overlay-color); + display: flex; + justify-content: center; + align-items: center; +} + +.content { + padding: 20px; + border-radius: 12px; + background: var(--bg-color); + transition: 0.3s transform; + transform: scale(0.5); + max-width: 60%; +} + +.opened { + opacity: 1; + pointer-events: auto; + z-index: var(--modal-z-index); + + .content { + transform: scale(1); + } +} + +.isClosing { + .content { + transform: scale(0.2); + } +} diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx new file mode 100644 index 0000000..0441c9b --- /dev/null +++ b/src/shared/ui/Modal/Modal.tsx @@ -0,0 +1,76 @@ +import React, { + ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import { classNames } from 'shared/lib/classNames/classNames'; +import cls from './Modal.module.scss'; + +interface ModalProps { + className?: string; + children?: ReactNode; + isOpen?: boolean; + onClose?: () => void; +} + +const ANIMATION_DELAY = 300; + +export const Modal = (props: ModalProps) => { + const { + className, + children, + isOpen, + onClose, + } = props; + + const [isClosing, setIsClosing] = useState(false); + const timerRef = useRef>(); + + const closeHandler = useCallback(() => { + if (onClose) { + setIsClosing(true); + timerRef.current = setTimeout(() => { + onClose(); + setIsClosing(false); + }, ANIMATION_DELAY); + } + }, [onClose]); + + const onKeyDown = useCallback((e: KeyboardEvent) => { + if (e.key === 'Escape') { + closeHandler(); + } + }, [closeHandler]); + + useEffect(() => { + if (isOpen) { + window.addEventListener('keydown', onKeyDown); + } + + return (() => { + clearTimeout(timerRef.current); + window.removeEventListener('keydown', onKeyDown); + }); + }, [isOpen, onKeyDown]); + + const mods:Record = { + [cls.opened]: isOpen, + [cls.isClosing]: isClosing, + }; + + const onContentClick = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + return ( +
+
+
+ {children} +
+
+
+ ); +};