Co-authored-by: Alireza <ar.sedghi@gmail.com> Co-authored-by: Dan Rukas <dan.rukas@gmail.com> Co-authored-by: Tang Cheng <45505657+tctco@users.noreply.github.com>
This commit is contained in:
44 files changed
+516
-258
No files matched your search
@@ -14,6 +14,8 @@ const serviceImplementation = {
|
||||
console.warn('isEmpty() NOT IMPLEMENTED');
|
||||
return true;
|
||||
},
|
||||
_updatePosition: (id: string, position: { x: number; y: number }) =>
|
||||
console.warn('updatePosition() NOT IMPLEMENTED'),
|
||||
_customComponent: null,
|
||||
};
|
||||
|
||||
@@ -63,6 +65,16 @@ class UIDialogService {
|
||||
return serviceImplementation._isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the position of a specific dialog by id
|
||||
*
|
||||
* @param {string} id - The dialog id to update
|
||||
* @param {{ x: number; y: number }} position - The new position
|
||||
*/
|
||||
updatePosition(id: string, position: { x: number; y: number }): void {
|
||||
return serviceImplementation._updatePosition(id, position);
|
||||
}
|
||||
|
||||
/**
|
||||
* This provides flexibility in customizing the Modal's default component
|
||||
*
|
||||
@@ -75,7 +87,14 @@ class UIDialogService {
|
||||
/**
|
||||
* Set the service implementation
|
||||
*/
|
||||
setServiceImplementation({ show, hide, hideAll, isEmpty, customComponent }: any): void {
|
||||
setServiceImplementation({
|
||||
show,
|
||||
hide,
|
||||
hideAll,
|
||||
isEmpty,
|
||||
updatePosition,
|
||||
customComponent,
|
||||
}: any): void {
|
||||
if (show) {
|
||||
serviceImplementation._show = show;
|
||||
}
|
||||
@@ -88,6 +107,9 @@ class UIDialogService {
|
||||
if (isEmpty) {
|
||||
serviceImplementation._isEmpty = isEmpty;
|
||||
}
|
||||
if (updatePosition) {
|
||||
serviceImplementation._updatePosition = updatePosition;
|
||||
}
|
||||
if (customComponent) {
|
||||
serviceImplementation._customComponent = customComponent;
|
||||
}
|
||||
|
||||
@@ -139,4 +139,4 @@ export default function ComponentsList() {
|
||||
</BrowserOnly>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ export const PowerOff = (props: IconProps) => (
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 28"
|
||||
aria-labelledby="title"
|
||||
width="1em"
|
||||
height="1em"
|
||||
width="28px"
|
||||
height="28px"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -67,9 +67,10 @@ function ViewportPane({
|
||||
|
||||
{/* Border overlay */}
|
||||
<div
|
||||
className={classNames('pointer-events-none absolute inset-0', {
|
||||
'border-highlight rounded-md border': isActive,
|
||||
'group-hover:border-highlight/50 rounded-md border border-transparent': !isActive,
|
||||
className={classNames('pointer-events-none absolute inset-0 rounded-md border', {
|
||||
'border-highlight': isActive,
|
||||
'group-hover/pane:border-highlight/50 border-transparent': !isActive,
|
||||
'!border-secondary-light border-dashed': isHighlighted,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import React, { useState, createContext, useContext, useCallback, useEffect, useMemo } from 'react';
|
||||
import ManagedDialog, { ManagedDialogProps } from './ManagedDialog';
|
||||
import React, {
|
||||
useState,
|
||||
createContext,
|
||||
useContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import ManagedDialog, { ManagedDialogProps, ManagedDialogRef } from './ManagedDialog';
|
||||
|
||||
interface DialogContextType {
|
||||
show: (options: ManagedDialogProps) => string;
|
||||
hide: (id: string) => void;
|
||||
hideAll: () => void;
|
||||
isEmpty: () => boolean;
|
||||
updatePosition: (id: string, position: { x: number; y: number }) => void;
|
||||
}
|
||||
|
||||
interface DialogService {
|
||||
@@ -35,6 +44,7 @@ const DialogProvider: React.FC<DialogProviderProps> = ({
|
||||
service = null,
|
||||
}) => {
|
||||
const [dialogs, setDialogs] = useState<(ManagedDialogProps & { id: string })[]>([]);
|
||||
const dialogRefs = useRef<Map<string, ManagedDialogRef>>(new Map());
|
||||
|
||||
const show = useCallback((options: ManagedDialogProps) => {
|
||||
const id = options.id;
|
||||
@@ -44,22 +54,32 @@ const DialogProvider: React.FC<DialogProviderProps> = ({
|
||||
|
||||
const hide = useCallback((id: string) => {
|
||||
setDialogs(prev => prev.filter(dialog => dialog.id !== id));
|
||||
dialogRefs.current.delete(id);
|
||||
}, []);
|
||||
|
||||
const hideAll = useCallback(() => {
|
||||
setDialogs([]);
|
||||
dialogRefs.current.clear();
|
||||
}, []);
|
||||
|
||||
const isEmpty = useCallback(() => dialogs.length === 0, [dialogs]);
|
||||
|
||||
const updatePosition = useCallback((id: string, position: { x: number; y: number }) => {
|
||||
const dialogRef = dialogRefs.current.get(id);
|
||||
if (dialogRef) {
|
||||
dialogRef.updatePosition(position);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
show,
|
||||
hide,
|
||||
hideAll,
|
||||
isEmpty,
|
||||
updatePosition,
|
||||
}),
|
||||
[show, hide, hideAll, isEmpty]
|
||||
[show, hide, hideAll, isEmpty, updatePosition]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -76,6 +96,11 @@ const DialogProvider: React.FC<DialogProviderProps> = ({
|
||||
{dialogs.map(dialog => (
|
||||
<RenderedDialog
|
||||
key={dialog.id}
|
||||
ref={(ref: ManagedDialogRef) => {
|
||||
if (ref) {
|
||||
dialogRefs.current.set(dialog.id, ref);
|
||||
}
|
||||
}}
|
||||
onClose={hide}
|
||||
isOpen={true}
|
||||
{...dialog}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { useState, useEffect, useImperativeHandle, forwardRef, useCallback } from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../components/Dialog/Dialog';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
type Position = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export interface ManagedDialogProps {
|
||||
id: string;
|
||||
isOpen?: boolean;
|
||||
@@ -19,92 +24,134 @@ export interface ManagedDialogProps {
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
const ManagedDialog: React.FC<ManagedDialogProps> = ({
|
||||
id,
|
||||
isOpen,
|
||||
title,
|
||||
content: DialogContentComponent,
|
||||
contentProps,
|
||||
isDraggable,
|
||||
shouldCloseOnEsc = false,
|
||||
shouldCloseOnOverlayClick = false,
|
||||
showOverlay = true,
|
||||
defaultPosition,
|
||||
onClose,
|
||||
unstyled,
|
||||
containerClassName,
|
||||
}) => {
|
||||
// When a default position is provided, the assumption is that the position
|
||||
// is respected unless the position chosen results in the dialog being
|
||||
// clipped off-screen (i.e. part of the dialog is rendered outside the browser
|
||||
// window). When the dialog is clipped it will be repositioned about
|
||||
// the default position such that it is no longer clipped. To avoid a flash
|
||||
// during the reposition, we initially hide the dialog.
|
||||
const [contentVisibility, setContentVisibility] = useState(
|
||||
defaultPosition ? 'invisible' : 'visible'
|
||||
);
|
||||
export interface ManagedDialogRef {
|
||||
updatePosition: (position: Position) => void;
|
||||
}
|
||||
|
||||
// The callback to reposition an explicitly positioned dialog. Note that
|
||||
// if the dialog is larger than the window (in either dimension), the
|
||||
// dialog will still be clipped in some manner.
|
||||
const contentRef = useCallback(
|
||||
contentNode => {
|
||||
if (!contentNode) {
|
||||
return;
|
||||
}
|
||||
const _updatePosition = (
|
||||
contentNode: HTMLElement,
|
||||
desiredPosition: { x: number; y: number },
|
||||
setCurrentPosition: (pt: Position) => void
|
||||
) => {
|
||||
if (!contentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const boundingClientRect = contentNode.getBoundingClientRect();
|
||||
if (boundingClientRect.bottom > window.innerHeight) {
|
||||
defaultPosition.y = defaultPosition.y - boundingClientRect.height;
|
||||
}
|
||||
if (boundingClientRect.right > window.innerWidth) {
|
||||
defaultPosition.x = defaultPosition.x - boundingClientRect.width;
|
||||
}
|
||||
setContentVisibility('visible');
|
||||
},
|
||||
[defaultPosition]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
modal={false} // keep modal behavior off for independent windows
|
||||
onOpenChange={open => {
|
||||
if (!open) {
|
||||
onClose(id);
|
||||
}
|
||||
}}
|
||||
isDraggable={isDraggable}
|
||||
shouldCloseOnEsc={shouldCloseOnEsc}
|
||||
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
|
||||
showOverlay={showOverlay}
|
||||
>
|
||||
<DialogContent
|
||||
ref={contentRef}
|
||||
className={cn(unstyled ? 'p-0' : '', containerClassName, contentVisibility)}
|
||||
unstyled={unstyled}
|
||||
style={{
|
||||
...(defaultPosition
|
||||
? {
|
||||
position: 'fixed',
|
||||
left: `${defaultPosition.x}px`,
|
||||
top: `${defaultPosition.y}px`,
|
||||
transform: 'translate(0, 0)',
|
||||
margin: 0,
|
||||
animation: 'none',
|
||||
transition: 'none',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{!unstyled && <DialogHeader>{title && <DialogTitle>{title}</DialogTitle>}</DialogHeader>}
|
||||
<DialogContentComponent
|
||||
{...contentProps}
|
||||
hide={() => onClose(id)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
const boundingClientRect = contentNode.getBoundingClientRect();
|
||||
if (boundingClientRect.bottom > window.innerHeight) {
|
||||
desiredPosition.y = desiredPosition.y - boundingClientRect.height;
|
||||
}
|
||||
if (boundingClientRect.right > window.innerWidth) {
|
||||
desiredPosition.x = desiredPosition.x - boundingClientRect.width;
|
||||
}
|
||||
setCurrentPosition(desiredPosition);
|
||||
};
|
||||
|
||||
const ManagedDialog = forwardRef<ManagedDialogRef, ManagedDialogProps>(
|
||||
(
|
||||
{
|
||||
id,
|
||||
isOpen,
|
||||
title,
|
||||
content: DialogContentComponent,
|
||||
contentProps,
|
||||
isDraggable,
|
||||
shouldCloseOnEsc = false,
|
||||
shouldCloseOnOverlayClick = false,
|
||||
showOverlay = true,
|
||||
defaultPosition,
|
||||
onClose,
|
||||
unstyled,
|
||||
containerClassName,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [currentPosition, setCurrentPosition] = useState(defaultPosition);
|
||||
const [contentNode, setContentNode] = useState<HTMLElement | null>(null);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
updatePosition: (position: Position) => {
|
||||
_updatePosition(contentNode, position, setCurrentPosition);
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPosition(defaultPosition);
|
||||
}, [defaultPosition]);
|
||||
|
||||
// When a default position is provided, the assumption is that the position
|
||||
// is respected unless the position chosen results in the dialog being
|
||||
// clipped off-screen (i.e. part of the dialog is rendered outside the browser
|
||||
// window). When the dialog is clipped it will be repositioned about
|
||||
// the default position such that it is no longer clipped. To avoid a flash
|
||||
// during the reposition, we initially hide the dialog.
|
||||
const [contentVisibility, setContentVisibility] = useState(
|
||||
defaultPosition ? 'invisible' : 'visible'
|
||||
);
|
||||
|
||||
// The callback to reposition an explicitly positioned dialog. Note that
|
||||
// if the dialog is larger than the window (in either dimension), the
|
||||
// dialog will still be clipped in some manner.
|
||||
const contentRef = useCallback(
|
||||
contentNode => {
|
||||
if (!contentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
setContentNode(contentNode);
|
||||
_updatePosition(contentNode, defaultPosition, setCurrentPosition);
|
||||
setContentVisibility('visible');
|
||||
},
|
||||
[defaultPosition]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
modal={false} // keep modal behavior off for independent windows
|
||||
onOpenChange={open => {
|
||||
if (!open) {
|
||||
onClose(id);
|
||||
}
|
||||
}}
|
||||
isDraggable={isDraggable}
|
||||
shouldCloseOnEsc={shouldCloseOnEsc}
|
||||
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
|
||||
showOverlay={showOverlay}
|
||||
>
|
||||
<DialogContent
|
||||
ref={contentRef}
|
||||
className={cn(unstyled ? 'p-0' : '', containerClassName, contentVisibility)}
|
||||
unstyled={unstyled}
|
||||
style={{
|
||||
...(currentPosition
|
||||
? {
|
||||
position: 'fixed',
|
||||
left: `${currentPosition.x}px`,
|
||||
top: `${currentPosition.y}px`,
|
||||
transform: 'translate(0, 0)',
|
||||
margin: 0,
|
||||
animation: 'none',
|
||||
transition: 'none',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{!unstyled && <DialogHeader>{title && <DialogTitle>{title}</DialogTitle>}</DialogHeader>}
|
||||
<DialogContentComponent
|
||||
{...contentProps}
|
||||
hide={() => onClose(id)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ManagedDialog.displayName = 'ManagedDialog';
|
||||
|
||||
export default ManagedDialog;
|
||||
Reference in new issue
Block a user