
* Introduce Modal, add some styles from the webapp * Remove bootstrap from LoadingScreen/WelcomeScreen * Migrate add/edit server modal to Modal, remove bootstrap * Migrate remove server modal to Modal, remove bootstrap * Migrate certificate modal to Modal, remove bootstrap * Migrate login and permission modals to Modal, remove Bootstrap * Misc fixes * E2E tests for current modals * Update src/renderer/components/Modal.tsx Co-authored-by: Daniel Espino García <larkox@gmail.com> * Update src/renderer/components/Modal.tsx Co-authored-by: Daniel Espino García <larkox@gmail.com> * PR feedback --------- Co-authored-by: Daniel Espino García <larkox@gmail.com> Co-authored-by: Mattermost Build <build@mattermost.com>
275 lines
8.9 KiB
TypeScript
275 lines
8.9 KiB
TypeScript
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||
// See LICENSE.txt for license information.
|
||
|
||
import classNames from 'classnames';
|
||
import React, {useState, useRef, useEffect, useCallback} from 'react';
|
||
import {FormattedMessage} from 'react-intl';
|
||
|
||
import 'renderer/css/components/Modal.scss';
|
||
|
||
export type Props = {
|
||
id: string;
|
||
children: React.ReactNode;
|
||
onExited: () => void;
|
||
|
||
className?: string;
|
||
modalHeaderText?: React.ReactNode;
|
||
modalSubheaderText?: React.ReactNode;
|
||
show?: boolean;
|
||
handleCancel?: () => void;
|
||
handleConfirm?: () => void;
|
||
handleEnterKeyPress?: () => void;
|
||
handleKeydown?: (event?: React.KeyboardEvent<HTMLDivElement>) => void;
|
||
confirmButtonText?: React.ReactNode;
|
||
confirmButtonClassName?: string;
|
||
cancelButtonText?: React.ReactNode;
|
||
cancelButtonClassName?: string;
|
||
isConfirmDisabled?: boolean;
|
||
isDeleteModal?: boolean;
|
||
autoCloseOnCancelButton?: boolean;
|
||
autoCloseOnConfirmButton?: boolean;
|
||
ariaLabel?: string;
|
||
errorText?: string | React.ReactNode;
|
||
tabIndex?: number;
|
||
autoFocusConfirmButton?: boolean;
|
||
headerInput?: React.ReactNode;
|
||
bodyPadding?: boolean;
|
||
bodyDivider?: boolean;
|
||
footerContent?: React.ReactNode;
|
||
footerDivider?: boolean;
|
||
appendedContent?: React.ReactNode;
|
||
headerButton?: React.ReactNode;
|
||
};
|
||
|
||
export const Modal: React.FC<Props> = ({
|
||
id = 'modal',
|
||
children,
|
||
onExited,
|
||
className,
|
||
modalHeaderText,
|
||
modalSubheaderText,
|
||
show = true,
|
||
handleCancel,
|
||
handleConfirm,
|
||
handleEnterKeyPress,
|
||
handleKeydown,
|
||
confirmButtonText,
|
||
confirmButtonClassName,
|
||
cancelButtonText,
|
||
cancelButtonClassName,
|
||
isConfirmDisabled,
|
||
isDeleteModal,
|
||
autoCloseOnCancelButton = true,
|
||
autoCloseOnConfirmButton = true,
|
||
ariaLabel,
|
||
errorText,
|
||
tabIndex,
|
||
autoFocusConfirmButton,
|
||
headerInput,
|
||
bodyPadding = true,
|
||
bodyDivider,
|
||
footerContent,
|
||
footerDivider,
|
||
appendedContent,
|
||
headerButton,
|
||
}) => {
|
||
const [showState, setShowState] = useState<boolean>();
|
||
const backdropRef = useRef<HTMLDivElement>(null);
|
||
|
||
useEffect(() => {
|
||
setShowState(show ?? true);
|
||
}, [show]);
|
||
|
||
const onHide = () => {
|
||
return new Promise<void>((resolve) => {
|
||
backdropRef.current?.addEventListener('transitionend', () => {
|
||
resolve();
|
||
}, {once: true});
|
||
setShowState(false);
|
||
});
|
||
};
|
||
|
||
const onClose = useCallback(async () => {
|
||
await onHide();
|
||
onExited();
|
||
}, [onExited]);
|
||
|
||
const handleCancelClick = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||
event.preventDefault();
|
||
if (autoCloseOnCancelButton) {
|
||
await onHide();
|
||
}
|
||
handleCancel?.();
|
||
};
|
||
|
||
const handleConfirmClick = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||
event.preventDefault();
|
||
if (autoCloseOnConfirmButton) {
|
||
await onHide();
|
||
}
|
||
handleConfirm?.();
|
||
};
|
||
|
||
const onEnterKeyDown = async (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||
if (event.key === 'Enter') {
|
||
if (event.nativeEvent.isComposing) {
|
||
return;
|
||
}
|
||
if (autoCloseOnConfirmButton) {
|
||
await onHide();
|
||
}
|
||
if (handleEnterKeyPress) {
|
||
handleEnterKeyPress();
|
||
}
|
||
}
|
||
handleKeydown?.(event);
|
||
};
|
||
|
||
let confirmButton;
|
||
if (handleConfirm) {
|
||
const isConfirmOrDeleteClassName = isDeleteModal ? 'delete' : 'confirm';
|
||
let confirmButtonTextNode: React.ReactNode = (
|
||
<FormattedMessage
|
||
id='modal.confirm'
|
||
defaultMessage='Confirm'
|
||
/>
|
||
);
|
||
if (confirmButtonText) {
|
||
confirmButtonTextNode = confirmButtonText;
|
||
}
|
||
|
||
confirmButton = (
|
||
<button
|
||
id={`${id}_confirm`}
|
||
autoFocus={autoFocusConfirmButton}
|
||
type='submit'
|
||
className={classNames('Modal__button btn btn-primary', isConfirmOrDeleteClassName, confirmButtonClassName, {
|
||
disabled: isConfirmDisabled,
|
||
})}
|
||
onClick={handleConfirmClick}
|
||
disabled={isConfirmDisabled}
|
||
>
|
||
{confirmButtonTextNode}
|
||
</button>
|
||
);
|
||
}
|
||
|
||
let cancelButton;
|
||
if (handleCancel) {
|
||
let cancelButtonTextNode: React.ReactNode = (
|
||
<FormattedMessage
|
||
id='modal.cancel'
|
||
defaultMessage='Cancel'
|
||
/>
|
||
);
|
||
if (cancelButtonText) {
|
||
cancelButtonTextNode = cancelButtonText;
|
||
}
|
||
|
||
cancelButton = (
|
||
<button
|
||
id={`${id}_cancel`}
|
||
type='button'
|
||
className={classNames('Modal__button btn btn-tertiary', cancelButtonClassName)}
|
||
onClick={handleCancelClick}
|
||
>
|
||
{cancelButtonTextNode}
|
||
</button>
|
||
);
|
||
}
|
||
|
||
const headerText = modalHeaderText && (
|
||
<div className='Modal__header'>
|
||
<h1
|
||
id='modalLabel'
|
||
className='Modal_title'
|
||
>
|
||
{modalHeaderText}
|
||
</h1>
|
||
{headerButton}
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<>
|
||
<div
|
||
ref={backdropRef}
|
||
className={classNames('Modal_backdrop fade', {show: showState})}
|
||
/>
|
||
<div
|
||
role='dialog'
|
||
className={classNames('Modal fade', {show: showState})}
|
||
onClick={onClose}
|
||
>
|
||
<div
|
||
id={id}
|
||
role='dialog'
|
||
aria-label={ariaLabel}
|
||
aria-labelledby={ariaLabel ? undefined : 'modalLabel'}
|
||
className={classNames(
|
||
'Modal_dialog Modal__compassDesign',
|
||
className,
|
||
)}
|
||
onClick={useCallback((event) => event.stopPropagation(), [])}
|
||
>
|
||
<div
|
||
onKeyDown={onEnterKeyDown}
|
||
tabIndex={tabIndex || 0}
|
||
className='Modal_content'
|
||
>
|
||
<div className='Modal_header'>
|
||
<div className='Modal__header__text_container'>
|
||
{headerText}
|
||
{headerInput}
|
||
{
|
||
modalSubheaderText &&
|
||
<div className='Modal_subheading-container'>
|
||
<p
|
||
id='Modal_subHeading'
|
||
className='Modal_subheading'
|
||
>
|
||
{modalSubheaderText}
|
||
</p>
|
||
</div>
|
||
}
|
||
</div>
|
||
<button
|
||
type='button'
|
||
className='close'
|
||
onClick={onClose}
|
||
>
|
||
<span aria-hidden='true'>{'×'}</span>
|
||
<span className='sr-only'>{'Close'}</span>
|
||
</button>
|
||
</div>
|
||
<div className={classNames('Modal_body', {divider: bodyDivider})}>
|
||
{errorText && (
|
||
<div className='Modal_error'>
|
||
<i className='icon icon-alert-outline'/>
|
||
<span>{errorText}</span>
|
||
</div>
|
||
)}
|
||
<div className={classNames('Modal__body', {padding: bodyPadding})}>
|
||
{children}
|
||
</div>
|
||
</div>
|
||
{(cancelButton || confirmButton || footerContent) && (
|
||
<div className={classNames('Modal_footer', {divider: footerDivider})}>
|
||
{(cancelButton || confirmButton) ? (
|
||
<>
|
||
{cancelButton}
|
||
{confirmButton}
|
||
</>
|
||
) : (
|
||
footerContent
|
||
)}
|
||
</div>
|
||
)}
|
||
{Boolean(appendedContent) && appendedContent}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
};
|