mirror of
https://github.com/Sonarr/Sonarr
synced 2025-12-06 16:32:24 +01:00
Convert Modal components to TypeScript
This commit is contained in:
parent
ee1a0a1f71
commit
9d0acba000
17 changed files with 340 additions and 444 deletions
|
|
@ -4,6 +4,7 @@ import React, { Component, ErrorInfo } from 'react';
|
||||||
interface ErrorBoundaryProps {
|
interface ErrorBoundaryProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
errorComponent: React.ElementType;
|
errorComponent: React.ElementType;
|
||||||
|
onModalClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorBoundaryState {
|
interface ErrorBoundaryState {
|
||||||
|
|
@ -32,11 +33,17 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children, errorComponent: ErrorComponent } = this.props;
|
const {
|
||||||
|
children,
|
||||||
|
errorComponent: ErrorComponent,
|
||||||
|
onModalClose,
|
||||||
|
} = this.props;
|
||||||
const { error, info } = this.state;
|
const { error, info } = this.state;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorComponent error={error} info={info} />;
|
return (
|
||||||
|
<ErrorComponent error={error} info={info} onModalClose={onModalClose} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import Portal from 'Components/Portal';
|
import Portal from 'Components/Portal';
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||||
import { icons, scrollDirections, sizes } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import ArrayElement from 'typings/Helpers/ArrayElement';
|
import ArrayElement from 'typings/Helpers/ArrayElement';
|
||||||
import { EnhancedSelectInputChanged, InputChanged } from 'typings/inputs';
|
import { EnhancedSelectInputChanged, InputChanged } from 'typings/inputs';
|
||||||
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||||
|
|
@ -563,14 +563,14 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<Modal
|
<Modal
|
||||||
className={styles.optionsModal}
|
className={styles.optionsModal}
|
||||||
size={sizes.EXTRA_SMALL}
|
size="extraSmall"
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onModalClose={handleOptionsModalClose}
|
onModalClose={handleOptionsModalClose}
|
||||||
>
|
>
|
||||||
<ModalBody
|
<ModalBody
|
||||||
className={styles.optionsModalBody}
|
className={styles.optionsModalBody}
|
||||||
innerClassName={styles.optionsInnerModalBody}
|
innerClassName={styles.optionsInnerModalBody}
|
||||||
scrollDirection={scrollDirections.NONE}
|
scrollDirection="none"
|
||||||
>
|
>
|
||||||
<Scroller className={styles.optionsModalScroller}>
|
<Scroller className={styles.optionsModalScroller}>
|
||||||
<div className={styles.mobileCloseButtonContainer}>
|
<div className={styles.mobileCloseButtonContainer}>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal, { ModalProps } from 'Components/Modal/Modal';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
|
import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
|
||||||
import { Kind } from 'Helpers/Props/kinds';
|
import { Kind } from 'Helpers/Props/kinds';
|
||||||
import { Size } from 'Helpers/Props/sizes';
|
|
||||||
|
|
||||||
interface ConfirmModalProps {
|
interface ConfirmModalProps extends Omit<ModalProps, 'onModalClose'> {
|
||||||
className?: string;
|
|
||||||
isOpen: boolean;
|
|
||||||
kind?: Kind;
|
kind?: Kind;
|
||||||
size?: Size;
|
|
||||||
title: string;
|
title: string;
|
||||||
message: React.ReactNode;
|
message: React.ReactNode;
|
||||||
confirmLabel?: string;
|
confirmLabel?: string;
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,12 @@
|
||||||
* Sizes
|
* Sizes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.extraSmall {
|
||||||
|
composes: modal;
|
||||||
|
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
.small {
|
.small {
|
||||||
composes: modal;
|
composes: modal;
|
||||||
|
|
||||||
|
|
@ -63,7 +69,6 @@
|
||||||
width: 1280px;
|
width: 1280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.extraExtraLarge {
|
.extraExtraLarge {
|
||||||
composes: modal;
|
composes: modal;
|
||||||
|
|
||||||
|
|
|
||||||
1
frontend/src/Components/Modal/Modal.css.d.ts
vendored
1
frontend/src/Components/Modal/Modal.css.d.ts
vendored
|
|
@ -3,6 +3,7 @@
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'extraExtraLarge': string;
|
'extraExtraLarge': string;
|
||||||
'extraLarge': string;
|
'extraLarge': string;
|
||||||
|
'extraSmall': string;
|
||||||
'large': string;
|
'large': string;
|
||||||
'medium': string;
|
'medium': string;
|
||||||
'modal': string;
|
'modal': string;
|
||||||
|
|
|
||||||
|
|
@ -1,235 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import elementClass from 'element-class';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import FocusLock from 'react-focus-lock';
|
|
||||||
import ErrorBoundary from 'Components/Error/ErrorBoundary';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
import { isIOS } from 'Utilities/browser';
|
|
||||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
|
||||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
|
||||||
import { setScrollLock } from 'Utilities/scrollLock';
|
|
||||||
import ModalError from './ModalError';
|
|
||||||
import styles from './Modal.css';
|
|
||||||
|
|
||||||
const openModals = [];
|
|
||||||
|
|
||||||
function removeFromOpenModals(id) {
|
|
||||||
const index = openModals.indexOf(id);
|
|
||||||
|
|
||||||
if (index >= 0) {
|
|
||||||
openModals.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Modal extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._node = document.getElementById('portal-root');
|
|
||||||
this._backgroundRef = null;
|
|
||||||
this._modalId = getUniqueElememtId();
|
|
||||||
this._bodyScrollTop = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (this.props.isOpen) {
|
|
||||||
this._openModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
isOpen
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!prevProps.isOpen && isOpen) {
|
|
||||||
this._openModal();
|
|
||||||
} else if (prevProps.isOpen && !isOpen) {
|
|
||||||
this._closeModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.props.isOpen) {
|
|
||||||
this._closeModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
_setBackgroundRef = (ref) => {
|
|
||||||
this._backgroundRef = ref;
|
|
||||||
};
|
|
||||||
|
|
||||||
_openModal() {
|
|
||||||
openModals.push(this._modalId);
|
|
||||||
window.addEventListener('keydown', this.onKeyDown);
|
|
||||||
|
|
||||||
if (openModals.length === 1) {
|
|
||||||
if (isIOS()) {
|
|
||||||
setScrollLock(true);
|
|
||||||
const scrollTop = document.body.scrollTop;
|
|
||||||
this._bodyScrollTop = scrollTop;
|
|
||||||
elementClass(document.body).add(styles.modalOpenIOS);
|
|
||||||
} else {
|
|
||||||
elementClass(document.body).add(styles.modalOpen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_closeModal() {
|
|
||||||
removeFromOpenModals(this._modalId);
|
|
||||||
window.removeEventListener('keydown', this.onKeyDown);
|
|
||||||
|
|
||||||
if (openModals.length === 0) {
|
|
||||||
setScrollLock(false);
|
|
||||||
|
|
||||||
if (isIOS()) {
|
|
||||||
elementClass(document.body).remove(styles.modalOpenIOS);
|
|
||||||
document.body.scrollTop = this._bodyScrollTop;
|
|
||||||
} else {
|
|
||||||
elementClass(document.body).remove(styles.modalOpen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_isBackdropTarget(event) {
|
|
||||||
const targetElement = this._findEventTarget(event);
|
|
||||||
|
|
||||||
if (targetElement) {
|
|
||||||
const backgroundElement = ReactDOM.findDOMNode(this._backgroundRef);
|
|
||||||
|
|
||||||
return backgroundElement.isEqualNode(targetElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_findEventTarget(event) {
|
|
||||||
const changedTouches = event.changedTouches;
|
|
||||||
|
|
||||||
if (!changedTouches) {
|
|
||||||
return event.target;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changedTouches.length === 1) {
|
|
||||||
const touch = changedTouches[0];
|
|
||||||
|
|
||||||
return document.elementFromPoint(touch.clientX, touch.clientY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onBackdropBeginPress = (event) => {
|
|
||||||
this._isBackdropPressed = this._isBackdropTarget(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
onBackdropEndPress = (event) => {
|
|
||||||
const {
|
|
||||||
closeOnBackgroundClick,
|
|
||||||
onModalClose
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
this._isBackdropPressed &&
|
|
||||||
this._isBackdropTarget(event) &&
|
|
||||||
closeOnBackgroundClick
|
|
||||||
) {
|
|
||||||
onModalClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._isBackdropPressed = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
onKeyDown = (event) => {
|
|
||||||
const keyCode = event.keyCode;
|
|
||||||
|
|
||||||
if (keyCode === keyCodes.ESCAPE) {
|
|
||||||
if (openModals.indexOf(this._modalId) === openModals.length - 1) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
this.props.onModalClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
backdropClassName,
|
|
||||||
size,
|
|
||||||
children,
|
|
||||||
isOpen,
|
|
||||||
onModalClose
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!isOpen) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ReactDOM.createPortal(
|
|
||||||
<FocusLock disabled={false}>
|
|
||||||
<div
|
|
||||||
className={styles.modalContainer}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={this._setBackgroundRef}
|
|
||||||
className={backdropClassName}
|
|
||||||
onMouseDown={this.onBackdropBeginPress}
|
|
||||||
onMouseUp={this.onBackdropEndPress}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
styles[size]
|
|
||||||
)}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<ErrorBoundary
|
|
||||||
errorComponent={ModalError}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FocusLock>,
|
|
||||||
this._node
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Modal.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
style: PropTypes.object,
|
|
||||||
backdropClassName: PropTypes.string,
|
|
||||||
size: PropTypes.oneOf(sizes.all),
|
|
||||||
children: PropTypes.node,
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
closeOnBackgroundClick: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
Modal.defaultProps = {
|
|
||||||
className: styles.modal,
|
|
||||||
backdropClassName: styles.modalBackdrop,
|
|
||||||
size: sizes.LARGE,
|
|
||||||
closeOnBackgroundClick: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Modal;
|
|
||||||
190
frontend/src/Components/Modal/Modal.tsx
Normal file
190
frontend/src/Components/Modal/Modal.tsx
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import elementClass from 'element-class';
|
||||||
|
import React, {
|
||||||
|
MouseEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useId,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import FocusLock from 'react-focus-lock';
|
||||||
|
import ErrorBoundary from 'Components/Error/ErrorBoundary';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { Size } from 'Helpers/Props/sizes';
|
||||||
|
import { isIOS } from 'Utilities/browser';
|
||||||
|
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||||
|
import { setScrollLock } from 'Utilities/scrollLock';
|
||||||
|
import ModalError from './ModalError';
|
||||||
|
import styles from './Modal.css';
|
||||||
|
|
||||||
|
const openModals: string[] = [];
|
||||||
|
const node = document.getElementById('portal-root');
|
||||||
|
|
||||||
|
function removeFromOpenModals(id: string) {
|
||||||
|
const index = openModals.indexOf(id);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
openModals.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findEventTarget(event: TouchEvent | MouseEvent) {
|
||||||
|
if ('changedTouches' in event) {
|
||||||
|
const changedTouches = event.changedTouches;
|
||||||
|
|
||||||
|
if (!changedTouches) {
|
||||||
|
return event.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedTouches.length === 1) {
|
||||||
|
const touch = changedTouches[0];
|
||||||
|
|
||||||
|
return document.elementFromPoint(touch.clientX, touch.clientY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return event.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalProps {
|
||||||
|
className?: string;
|
||||||
|
style?: object;
|
||||||
|
backdropClassName?: string;
|
||||||
|
size?: Extract<Size, keyof typeof styles>;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
isOpen: boolean;
|
||||||
|
closeOnBackgroundClick?: boolean;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Modal({
|
||||||
|
className = styles.modal,
|
||||||
|
style,
|
||||||
|
backdropClassName = styles.modalBackdrop,
|
||||||
|
size = 'large',
|
||||||
|
children,
|
||||||
|
isOpen,
|
||||||
|
closeOnBackgroundClick = true,
|
||||||
|
onModalClose,
|
||||||
|
}: ModalProps) {
|
||||||
|
const backgroundRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isBackdropPressed = useRef(false);
|
||||||
|
const bodyScrollTop = useRef(0);
|
||||||
|
const wasOpen = usePrevious(isOpen);
|
||||||
|
const modalId = useId();
|
||||||
|
|
||||||
|
const isTargetBackdrop = useCallback((event: TouchEvent | MouseEvent) => {
|
||||||
|
const targetElement = findEventTarget(event);
|
||||||
|
|
||||||
|
if (targetElement) {
|
||||||
|
return backgroundRef.current?.isEqualNode(targetElement as Node) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleBackdropBeginPress = useCallback(
|
||||||
|
(event: MouseEvent<HTMLDivElement>) => {
|
||||||
|
isBackdropPressed.current = isTargetBackdrop(event);
|
||||||
|
},
|
||||||
|
[isTargetBackdrop]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBackdropEndPress = useCallback(
|
||||||
|
(event: MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (
|
||||||
|
isBackdropPressed.current &&
|
||||||
|
isTargetBackdrop(event) &&
|
||||||
|
closeOnBackgroundClick
|
||||||
|
) {
|
||||||
|
onModalClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
isBackdropPressed.current = false;
|
||||||
|
},
|
||||||
|
[closeOnBackgroundClick, isTargetBackdrop, onModalClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent) => {
|
||||||
|
if (event.keyCode === keyCodes.ESCAPE) {
|
||||||
|
if (openModals.indexOf(modalId) === openModals.length - 1) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
onModalClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[modalId, onModalClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && !wasOpen) {
|
||||||
|
openModals.push(modalId);
|
||||||
|
|
||||||
|
if (openModals.length === 1) {
|
||||||
|
if (isIOS()) {
|
||||||
|
setScrollLock(true);
|
||||||
|
bodyScrollTop.current = document.body.scrollTop;
|
||||||
|
elementClass(document.body).add(styles.modalOpenIOS);
|
||||||
|
} else {
|
||||||
|
elementClass(document.body).add(styles.modalOpen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!isOpen && wasOpen) {
|
||||||
|
removeFromOpenModals(modalId);
|
||||||
|
|
||||||
|
if (openModals.length === 0) {
|
||||||
|
setScrollLock(false);
|
||||||
|
|
||||||
|
if (isIOS()) {
|
||||||
|
elementClass(document.body).remove(styles.modalOpenIOS);
|
||||||
|
document.body.scrollTop = bodyScrollTop.current;
|
||||||
|
} else {
|
||||||
|
elementClass(document.body).remove(styles.modalOpen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isOpen, wasOpen, modalId, handleKeyDown]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [isOpen, handleKeyDown]);
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<FocusLock disabled={false}>
|
||||||
|
<div className={styles.modalContainer}>
|
||||||
|
<div
|
||||||
|
ref={backgroundRef}
|
||||||
|
className={backdropClassName}
|
||||||
|
onMouseDown={handleBackdropBeginPress}
|
||||||
|
onMouseUp={handleBackdropEndPress}
|
||||||
|
>
|
||||||
|
<div className={classNames(className, styles[size])} style={style}>
|
||||||
|
<ErrorBoundary
|
||||||
|
errorComponent={ModalError}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FocusLock>,
|
||||||
|
node!
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
|
||||||
import styles from './ModalBody.css';
|
|
||||||
|
|
||||||
class ModalBody extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
innerClassName,
|
|
||||||
scrollDirection,
|
|
||||||
children,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
let className = this.props.className;
|
|
||||||
const hasScroller = scrollDirection !== scrollDirections.NONE;
|
|
||||||
|
|
||||||
if (!className) {
|
|
||||||
className = hasScroller ? styles.modalScroller : styles.modalBody;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Scroller
|
|
||||||
className={className}
|
|
||||||
scrollDirection={scrollDirection}
|
|
||||||
scrollTop={0}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
hasScroller ?
|
|
||||||
<div className={innerClassName}>
|
|
||||||
{children}
|
|
||||||
</div> :
|
|
||||||
children
|
|
||||||
}
|
|
||||||
</Scroller>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ModalBody.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
innerClassName: PropTypes.string,
|
|
||||||
children: PropTypes.node,
|
|
||||||
scrollDirection: PropTypes.oneOf(scrollDirections.all)
|
|
||||||
};
|
|
||||||
|
|
||||||
ModalBody.defaultProps = {
|
|
||||||
innerClassName: styles.innerModalBody,
|
|
||||||
scrollDirection: scrollDirections.VERTICAL
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalBody;
|
|
||||||
42
frontend/src/Components/Modal/ModalBody.tsx
Normal file
42
frontend/src/Components/Modal/ModalBody.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
|
import { ScrollDirection } from 'Helpers/Props/scrollDirections';
|
||||||
|
import styles from './ModalBody.css';
|
||||||
|
|
||||||
|
interface ModalBodyProps {
|
||||||
|
className?: string;
|
||||||
|
innerClassName?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
scrollDirection?: ScrollDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModalBody({
|
||||||
|
innerClassName = styles.innerModalBody,
|
||||||
|
scrollDirection = 'vertical',
|
||||||
|
children,
|
||||||
|
...otherProps
|
||||||
|
}: ModalBodyProps) {
|
||||||
|
let className = otherProps.className;
|
||||||
|
const hasScroller = scrollDirection !== 'none';
|
||||||
|
|
||||||
|
if (!className) {
|
||||||
|
className = hasScroller ? styles.modalScroller : styles.modalBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Scroller
|
||||||
|
{...otherProps}
|
||||||
|
className={className}
|
||||||
|
scrollDirection={scrollDirection}
|
||||||
|
scrollTop={0}
|
||||||
|
>
|
||||||
|
{hasScroller ? (
|
||||||
|
<div className={innerClassName}>{children}</div>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</Scroller>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalBody;
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './ModalContent.css';
|
|
||||||
|
|
||||||
function ModalContent(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
showCloseButton,
|
|
||||||
onModalClose,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={className}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
showCloseButton &&
|
|
||||||
<Link
|
|
||||||
className={styles.closeButton}
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={icons.CLOSE}
|
|
||||||
size={18}
|
|
||||||
title={translate('Close')}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ModalContent.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
children: PropTypes.node,
|
|
||||||
showCloseButton: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
ModalContent.defaultProps = {
|
|
||||||
className: styles.modalContent,
|
|
||||||
showCloseButton: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalContent;
|
|
||||||
35
frontend/src/Components/Modal/ModalContent.tsx
Normal file
35
frontend/src/Components/Modal/ModalContent.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './ModalContent.css';
|
||||||
|
|
||||||
|
interface ModalContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
showCloseButton?: boolean;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModalContent({
|
||||||
|
className = styles.modalContent,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
}: ModalContentProps) {
|
||||||
|
return (
|
||||||
|
<div className={className} {...otherProps}>
|
||||||
|
{showCloseButton && (
|
||||||
|
<Link className={styles.closeButton} onPress={onModalClose}>
|
||||||
|
<Icon name={icons.CLOSE} size={18} title={translate('Close')} />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalContent;
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
|
import ErrorBoundaryError, {
|
||||||
|
ErrorBoundaryErrorProps,
|
||||||
|
} from 'Components/Error/ErrorBoundaryError';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
|
@ -9,40 +10,29 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './ModalError.css';
|
import styles from './ModalError.css';
|
||||||
|
|
||||||
function ModalError(props) {
|
interface ModalErrorProps extends ErrorBoundaryErrorProps {
|
||||||
const {
|
onModalClose: () => void;
|
||||||
onModalClose,
|
}
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
|
function ModalError({ onModalClose, ...otherProps }: ModalErrorProps) {
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>{translate('Error')}</ModalHeader>
|
||||||
{translate('Error')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<ErrorBoundaryError
|
<ErrorBoundaryError
|
||||||
|
{...otherProps}
|
||||||
messageClassName={styles.message}
|
messageClassName={styles.message}
|
||||||
detailsClassName={styles.details}
|
detailsClassName={styles.details}
|
||||||
{...otherProps}
|
|
||||||
message={translate('ErrorLoadingItem')}
|
message={translate('ErrorLoadingItem')}
|
||||||
/>
|
/>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ModalError.propTypes = {
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalError;
|
export default ModalError;
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import styles from './ModalFooter.css';
|
|
||||||
|
|
||||||
class ModalFooter extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
children,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.modalFooter}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ModalFooter.propTypes = {
|
|
||||||
children: PropTypes.node
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalFooter;
|
|
||||||
16
frontend/src/Components/Modal/ModalFooter.tsx
Normal file
16
frontend/src/Components/Modal/ModalFooter.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './ModalFooter.css';
|
||||||
|
|
||||||
|
interface ModalFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModalFooter({ children, ...otherProps }: ModalFooterProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles.modalFooter} {...otherProps}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalFooter;
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import styles from './ModalHeader.css';
|
|
||||||
|
|
||||||
class ModalHeader extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
children,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.modalHeader}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ModalHeader.propTypes = {
|
|
||||||
children: PropTypes.node
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalHeader;
|
|
||||||
16
frontend/src/Components/Modal/ModalHeader.tsx
Normal file
16
frontend/src/Components/Modal/ModalHeader.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './ModalHeader.css';
|
||||||
|
|
||||||
|
interface ModalHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModalHeader({ children, ...otherProps }: ModalHeaderProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles.modalHeader} {...otherProps}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalHeader;
|
||||||
10
frontend/typings/element-class.ts
Normal file
10
frontend/typings/element-class.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
declare module 'element-class' {
|
||||||
|
function elementClass(element: HTMLElement): ElementClass;
|
||||||
|
|
||||||
|
export = elementClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ElementClass {
|
||||||
|
add: (className: string) => void;
|
||||||
|
remove: (className: string) => void;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue