mirror of https://github.com/apache/superset.git
feat: Draggable and Resizable Modal (#16394)
* Implement resizable prop * Implement resizableConfig * Implement fluid Syntax Highlighter * Implement draggable * Destroy on close * Implement draggableConfig * Enhance with footer calculation * Add new line * Make whole header draggable trigger
This commit is contained in:
parent
93c60e4021
commit
db11c3e6c8
|
@ -147,6 +147,7 @@
|
|||
"react-dnd": "^11.1.3",
|
||||
"react-dnd-html5-backend": "^11.1.3",
|
||||
"react-dom": "^16.13.0",
|
||||
"react-draggable": "^4.4.3",
|
||||
"react-gravatar": "^2.6.1",
|
||||
"react-hot-loader": "^4.12.20",
|
||||
"react-js-cron": "^1.2.0",
|
||||
|
|
|
@ -34,6 +34,8 @@ InteractiveModal.args = {
|
|||
primaryButtonType: 'danger',
|
||||
show: true,
|
||||
title: "I'm a modal!",
|
||||
resizable: false,
|
||||
draggable: false,
|
||||
};
|
||||
|
||||
InteractiveModal.argTypes = {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { isNil } from 'lodash';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import { css } from '@emotion/react';
|
||||
|
@ -25,6 +25,13 @@ import {
|
|||
ModalProps as AntdModalProps,
|
||||
} from 'src/common/components';
|
||||
import Button from 'src/components/Button';
|
||||
import { Resizable, ResizableProps } from 're-resizable';
|
||||
import Draggable, {
|
||||
DraggableBounds,
|
||||
DraggableData,
|
||||
DraggableEvent,
|
||||
DraggableProps,
|
||||
} from 'react-draggable';
|
||||
|
||||
export interface ModalProps {
|
||||
className?: string;
|
||||
|
@ -46,6 +53,10 @@ export interface ModalProps {
|
|||
wrapProps?: object;
|
||||
height?: string;
|
||||
closable?: boolean;
|
||||
resizable?: boolean;
|
||||
resizableConfig?: ResizableProps;
|
||||
draggable?: boolean;
|
||||
draggableConfig?: DraggableProps;
|
||||
destroyOnClose?: boolean;
|
||||
}
|
||||
|
||||
|
@ -54,8 +65,19 @@ interface StyledModalProps {
|
|||
responsive?: boolean;
|
||||
height?: string;
|
||||
hideFooter?: boolean;
|
||||
draggable?: boolean;
|
||||
resizable?: boolean;
|
||||
}
|
||||
|
||||
const MODAL_HEADER_HEIGHT = 55;
|
||||
const MODAL_MIN_CONTENT_HEIGHT = 54;
|
||||
const MODAL_FOOTER_HEIGHT = 65;
|
||||
|
||||
const RESIZABLE_MIN_HEIGHT = MODAL_HEADER_HEIGHT + MODAL_MIN_CONTENT_HEIGHT;
|
||||
const RESIZABLE_MIN_WIDTH = '380px';
|
||||
const RESIZABLE_MAX_HEIGHT = '100vh';
|
||||
const RESIZABLE_MAX_WIDTH = '100vw';
|
||||
|
||||
const BaseModal = (props: AntdModalProps) => (
|
||||
// Removes mask animation. Fixed in 4.6.0.
|
||||
// https://github.com/ant-design/ant-design/issues/27192
|
||||
|
@ -101,9 +123,8 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
|
|||
.ant-modal-body {
|
||||
padding: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
overflow: auto;
|
||||
${({ height }) => height && `height: ${height};`}
|
||||
${({ resizable, height }) => !resizable && height && `height: ${height};`}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
border-top: ${({ theme }) => theme.gridUnit / 4}px solid
|
||||
${({ theme }) => theme.colors.grayscale.light2};
|
||||
|
@ -129,6 +150,44 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
|
|||
&.no-content-padding .ant-modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
${({ draggable, theme }) =>
|
||||
draggable &&
|
||||
`
|
||||
.ant-modal-header {
|
||||
padding: 0;
|
||||
.draggable-trigger {
|
||||
cursor: move;
|
||||
padding: ${theme.gridUnit * 4}px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`};
|
||||
|
||||
${({ resizable, hideFooter }) =>
|
||||
resizable &&
|
||||
`
|
||||
.resizable {
|
||||
pointer-events: all;
|
||||
|
||||
.resizable-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
height: 100%;
|
||||
|
||||
.ant-modal-body {
|
||||
/* 100% - header height - footer height */
|
||||
height: ${
|
||||
hideFooter
|
||||
? `calc(100% - ${MODAL_HEADER_HEIGHT}px);`
|
||||
: `calc(100% - ${MODAL_HEADER_HEIGHT}px - ${MODAL_FOOTER_HEIGHT}px);`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const CustomModal = ({
|
||||
|
@ -148,8 +207,33 @@ const CustomModal = ({
|
|||
footer,
|
||||
hideFooter,
|
||||
wrapProps,
|
||||
draggable = false,
|
||||
resizable = false,
|
||||
resizableConfig = {
|
||||
maxHeight: RESIZABLE_MAX_HEIGHT,
|
||||
maxWidth: RESIZABLE_MAX_WIDTH,
|
||||
minHeight: hideFooter
|
||||
? RESIZABLE_MIN_HEIGHT
|
||||
: RESIZABLE_MIN_HEIGHT + MODAL_FOOTER_HEIGHT,
|
||||
minWidth: RESIZABLE_MIN_WIDTH,
|
||||
enable: {
|
||||
bottom: true,
|
||||
bottomLeft: false,
|
||||
bottomRight: true,
|
||||
left: false,
|
||||
top: false,
|
||||
topLeft: false,
|
||||
topRight: false,
|
||||
right: true,
|
||||
},
|
||||
},
|
||||
draggableConfig,
|
||||
destroyOnClose,
|
||||
...rest
|
||||
}: ModalProps) => {
|
||||
const draggableRef = useRef<HTMLDivElement>(null);
|
||||
const [bounds, setBounds] = useState<DraggableBounds>();
|
||||
const [dragDisabled, setDragDisabled] = useState<boolean>(true);
|
||||
const modalFooter = isNil(footer)
|
||||
? [
|
||||
<Button key="back" onClick={onHide} cta data-test="modal-cancel-button">
|
||||
|
@ -169,6 +253,35 @@ const CustomModal = ({
|
|||
: footer;
|
||||
|
||||
const modalWidth = width || (responsive ? '100vw' : '600px');
|
||||
const shouldShowMask = !(resizable || draggable);
|
||||
|
||||
const onDragStart = (_: DraggableEvent, uiData: DraggableData) => {
|
||||
const { clientWidth, clientHeight } = window?.document?.documentElement;
|
||||
const targetRect = draggableRef?.current?.getBoundingClientRect();
|
||||
|
||||
if (targetRect) {
|
||||
setBounds({
|
||||
left: -targetRect?.left + uiData?.x,
|
||||
right: clientWidth - (targetRect?.right - uiData?.x),
|
||||
top: -targetRect?.top + uiData?.y,
|
||||
bottom: clientHeight - (targetRect?.bottom - uiData?.y),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const ModalTitle = () =>
|
||||
draggable ? (
|
||||
<div
|
||||
className="draggable-trigger"
|
||||
onMouseOver={() => dragDisabled && setDragDisabled(false)}
|
||||
onMouseOut={() => !dragDisabled && setDragDisabled(true)}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
) : (
|
||||
<>{title}</>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
centered={!!centered}
|
||||
|
@ -178,14 +291,41 @@ const CustomModal = ({
|
|||
maxWidth={maxWidth}
|
||||
responsive={responsive}
|
||||
visible={show}
|
||||
title={title}
|
||||
title={<ModalTitle />}
|
||||
closeIcon={
|
||||
<span className="close" aria-hidden="true">
|
||||
×
|
||||
</span>
|
||||
}
|
||||
footer={!hideFooter ? modalFooter : null}
|
||||
hideFooter={hideFooter}
|
||||
wrapProps={{ 'data-test': `${name || title}-modal`, ...wrapProps }}
|
||||
modalRender={modal =>
|
||||
resizable || draggable ? (
|
||||
<Draggable
|
||||
disabled={!draggable || dragDisabled}
|
||||
bounds={bounds}
|
||||
onStart={(event, uiData) => onDragStart(event, uiData)}
|
||||
{...draggableConfig}
|
||||
>
|
||||
{resizable ? (
|
||||
<Resizable className="resizable" {...resizableConfig}>
|
||||
<div className="resizable-wrapper" ref={draggableRef}>
|
||||
{modal}
|
||||
</div>
|
||||
</Resizable>
|
||||
) : (
|
||||
<div ref={draggableRef}>{modal}</div>
|
||||
)}
|
||||
</Draggable>
|
||||
) : (
|
||||
modal
|
||||
)
|
||||
}
|
||||
mask={shouldShowMask}
|
||||
draggable={draggable}
|
||||
resizable={resizable}
|
||||
destroyOnClose={destroyOnClose || resizable || draggable}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -33,6 +33,8 @@ interface IModalTriggerProps {
|
|||
width?: string;
|
||||
maxWidth?: string;
|
||||
responsive?: boolean;
|
||||
draggable?: boolean;
|
||||
resizable?: boolean;
|
||||
}
|
||||
|
||||
export default {
|
||||
|
@ -53,4 +55,6 @@ InteractiveModalTrigger.args = {
|
|||
width: '600px',
|
||||
maxWidth: '1000px',
|
||||
responsive: true,
|
||||
draggable: false,
|
||||
resizable: false,
|
||||
};
|
||||
|
|
|
@ -35,6 +35,10 @@ const propTypes = {
|
|||
width: PropTypes.string,
|
||||
maxWidth: PropTypes.string,
|
||||
responsive: PropTypes.bool,
|
||||
resizable: PropTypes.bool,
|
||||
resizableConfig: PropTypes.object,
|
||||
draggable: PropTypes.bool,
|
||||
draggableConfig: PropTypes.object,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
|
@ -43,6 +47,8 @@ const defaultProps = {
|
|||
isButton: false,
|
||||
className: '',
|
||||
modalTitle: '',
|
||||
resizable: false,
|
||||
draggable: false,
|
||||
};
|
||||
|
||||
export default class ModalTrigger extends React.Component {
|
||||
|
@ -79,6 +85,10 @@ export default class ModalTrigger extends React.Component {
|
|||
width={this.props.width}
|
||||
maxWidth={this.props.maxWidth}
|
||||
responsive={this.props.responsive}
|
||||
resizable={this.props.resizable}
|
||||
resizableConfig={this.props.resizableConfig}
|
||||
draggable={this.props.draggable}
|
||||
draggableConfig={this.props.draggableConfig}
|
||||
>
|
||||
{this.props.modalBody}
|
||||
</Modal>
|
||||
|
|
|
@ -91,6 +91,8 @@ const ExploreAdditionalActionsMenu = props => {
|
|||
latestQueryFormData={props.latestQueryFormData}
|
||||
/>
|
||||
}
|
||||
draggable
|
||||
resizable
|
||||
responsive
|
||||
/>
|
||||
</Menu.Item>
|
||||
|
|
|
@ -50,6 +50,14 @@ type Result = {
|
|||
language: string;
|
||||
};
|
||||
|
||||
const StyledSyntaxContainer = styled.div`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledSyntaxHighlighter = styled(SyntaxHighlighter)`
|
||||
height: calc(100% - 26px); // 100% - clipboard height
|
||||
`;
|
||||
|
||||
const ViewQueryModal: React.FC<Props> = props => {
|
||||
const [result, setResult] = useState<Result[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
@ -93,7 +101,7 @@ const ViewQueryModal: React.FC<Props> = props => {
|
|||
<>
|
||||
{result.map(item =>
|
||||
item.query ? (
|
||||
<div>
|
||||
<StyledSyntaxContainer key={item.query}>
|
||||
<CopyToClipboard
|
||||
text={item.query}
|
||||
shouldShowText={false}
|
||||
|
@ -103,13 +111,13 @@ const ViewQueryModal: React.FC<Props> = props => {
|
|||
</CopyButtonViewQuery>
|
||||
}
|
||||
/>
|
||||
<SyntaxHighlighter
|
||||
<StyledSyntaxHighlighter
|
||||
language={item.language || undefined}
|
||||
style={github}
|
||||
>
|
||||
{item.query}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</StyledSyntaxHighlighter>
|
||||
</StyledSyntaxContainer>
|
||||
) : null,
|
||||
)}
|
||||
</>
|
||||
|
|
Loading…
Reference in New Issue