Repository navigation
Improvements to interactive modal component #289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c2a5b56
e36dfbf
e392151
4e2e80a
ef794cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,40 @@ import { | |
| DialogBackdrop, | ||
| } from "@headlessui/react"; | ||
| import gwMerge from "../../gw-merge"; | ||
| import { WIDTH_OPTIONS } from "../../utils/sizes"; | ||
| import { useRef, useState, useEffect } from "react"; | ||
|
|
||
| const WIDTH_OPTIONS = { | ||
| xs: 300, | ||
| sm: 400, | ||
| md: 600, | ||
| lg: 800, | ||
| xl: 1140, | ||
| "2xl": 1600, | ||
| "4xl": 2000, | ||
| }; | ||
|
|
||
| // Set initial window size and track window resize to handle responsive formatting. | ||
| function useWindowSize() { | ||
| const [windowSize, setWindowSize] = useState({ | ||
| windowWidth: undefined, | ||
| windowHeight: undefined, | ||
| }); | ||
|
|
||
| useEffect(() => { | ||
| function handleResize() { | ||
| setWindowSize({ | ||
| windowWidth: window.innerWidth, | ||
| windowHeight: window.innerHeight, | ||
| }); | ||
| } | ||
|
|
||
| window.addEventListener("resize", handleResize); | ||
| handleResize(); // set size when window changes | ||
| return () => window.removeEventListener("resize", handleResize); | ||
| }, []); | ||
|
|
||
| return windowSize; | ||
| } | ||
|
|
||
| const roleOptions = ["dialog", "alertdialog"]; | ||
|
|
||
|
|
@@ -15,28 +48,23 @@ function Modal({ | |
| onClose, | ||
| dialogTitle, | ||
| dialogDescription, | ||
| footer, | ||
| isStatic = false, | ||
| autoFocus = false, | ||
| dialogTransition = false, | ||
| dialogPanelTransition = false, | ||
| unmount = true, | ||
| role = "dialog", | ||
| buttons, | ||
| size = "2xl", | ||
| size, | ||
| className, | ||
| background, | ||
| children, | ||
| }) { | ||
| const widthClass = WIDTH_OPTIONS[size] ?? WIDTH_OPTIONS["md"]; | ||
| // Check if the size exists | ||
| if (!WIDTH_OPTIONS[size]) { | ||
| console.error( | ||
| `Invalid size prop: ${size}. Must be one of: 'sx', 'sm', 'md', 'lg', 'xl', '2xl', '4xl', 'full'`, | ||
| ); | ||
| console.warn( | ||
| `Defaulting to '2xl' for size of <Modal modalTitle="${dialogTitle}" .../>`, | ||
| ); | ||
| size = "2xl"; | ||
| console.warn(`Modal: invalid size "${size}" passed. Falling back to "md".`); | ||
| } | ||
|
|
||
| // Check if Role exists | ||
| if (!roleOptions.includes(role)) { | ||
| console.error( | ||
|
|
@@ -48,42 +76,186 @@ function Modal({ | |
| role = "dialog"; | ||
| } | ||
|
|
||
| const panelRef = useRef(null); | ||
| // Modal Defaults | ||
| const [dimensions, setDimensions] = useState({ | ||
| width: widthClass, | ||
| height: null, | ||
| }); | ||
| const [position, setPosition] = useState({ x: 0, y: 0 }); //May not be needed | ||
| const [offset, setOffset] = useState({ x: 0, y: 0 }); | ||
| const [resizing, setResizing] = useState(false); | ||
| const { windowWidth, windowHeight } = useWindowSize(); | ||
|
|
||
| //set the plot size dynamically based on the initial screen size | ||
| useEffect(() => { | ||
| if (!windowHeight || !windowWidth) { | ||
| return; | ||
| } | ||
| //Set the dimensions to the smaller of 90% of the window width, or the user defined input. Set the height to 75% of window height | ||
| setDimensions({ | ||
| width: Math.min(windowWidth * 0.9, widthClass), | ||
| height: windowHeight * 0.75, | ||
| }); | ||
| }, [windowWidth, windowHeight, widthClass]); | ||
|
|
||
| const handlePointerDown = (e) => { | ||
| if (!panelRef.current) return; | ||
| const rect = panelRef?.current?.getBoundingClientRect(); | ||
| setOffset({ x: e.clientX - rect.left, y: e.clientY - rect.top }); | ||
| }; | ||
|
|
||
| const handleMove = (e) => { | ||
| if (!panelRef.current) return; | ||
| if (resizing) { | ||
| const rect = panelRef?.current?.getBoundingClientRect(); | ||
| //check if the resized modal window exceeds the height or width of the window | ||
| if ( | ||
| e.clientX - rect.left > windowWidth || | ||
| e.clientY - rect.top > windowHeight | ||
| ) { | ||
| //if yet, set the modal window at the smallest of either the window height/width or user input width/height | ||
| setDimensions({ | ||
| width: Math.min(windowWidth, e.clientX - rect.left), | ||
| height: Math.min(windowHeight, e.clientY - rect.top), | ||
| }); | ||
| } else { | ||
| //Set the minimum modal size (300 width, 200 height) | ||
| setDimensions({ | ||
| width: Math.max(300, e.clientX - rect.left), | ||
| height: Math.max(200, e.clientY - rect.top), | ||
| }); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| const handleRelease = () => { | ||
| setResizing(false); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| //Converted from mouse based events to pointer events. Pointer events handle mouse/stylus/trackpad/finger inputs. | ||
| const onPointerMove = (event) => { | ||
| handleMove(event); | ||
| }; | ||
|
|
||
| if (resizing) { | ||
| // handle resizing inputs | ||
| window.addEventListener("pointermove", onPointerMove); | ||
| window.addEventListener("pointerup", handleRelease); | ||
| window.addEventListener("pointercancel", handleRelease); // Fired if the OS interrupts the gesture | ||
| } | ||
|
|
||
| return () => { | ||
| // remove event inputs when done | ||
| window.removeEventListener("pointermove", onPointerMove); | ||
| window.removeEventListener("pointerup", handleRelease); | ||
| window.removeEventListener("pointercancel", handleRelease); | ||
| }; | ||
| // Ensure all values read by handleMove are in the dependency array to prevent stale closures | ||
| }, [resizing, offset, windowWidth, windowHeight]); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| return ( | ||
| <Dialog | ||
| open={opened} | ||
| onClose={onClose} | ||
| onPointerDown={handlePointerDown} | ||
| static={isStatic} | ||
| autoFocus={autoFocus} | ||
| transition={dialogTransition} | ||
| unmount={unmount} | ||
| role={role} | ||
| className={gwMerge("gw-relative", "gw-z-[200]", className)} | ||
| style={{ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new |
||
| position: "fixed", | ||
| top: 0, | ||
| left: 0, | ||
| right: 0, | ||
| bottom: 0, | ||
| backgroundColor: background, | ||
| display: "flex", | ||
| justifyContent: "center", | ||
| alignItems: "center", | ||
| overflow: "auto", | ||
| zIndex: 1000 /* Ensure it's on top of other content */, | ||
| }} | ||
| > | ||
| <DialogBackdrop className="gw-fixed gw-inset-0 gw-bg-black/30" /> | ||
| <div className="gw-fixed gw-inset-0 gw-w-screen gw-overflow-auto gw-p-4"> | ||
| <div className="gw-flex gw-min-h-full gw-items-center gw-justify-center"> | ||
| <div | ||
| style={ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or should use expansion on the objects so that the results of the conditional are merged with the initial object |
||
| ({ | ||
| background: "white", | ||
| padding: "20px", | ||
| overflow: "hidden", | ||
| borderRadius: "5px", | ||
| zIndex: 1001, | ||
| }, | ||
| dimensions.width && dimensions.height | ||
| ? { | ||
| width: dimensions.width, | ||
| height: dimensions.height, | ||
| maxWidth: windowWidth, | ||
| maxHeight: "95%", | ||
| //minWidth: "50%", | ||
| } | ||
| : undefined) | ||
| } | ||
| > | ||
| <div className=""> | ||
| {" "} | ||
| {/*gw-flex gw-min-h-full gw-items-center gw-justify-center*/} | ||
| <div | ||
| className="gw-relative" | ||
| style={{ top: position.y, left: position.x }} | ||
| ></div> | ||
| <DialogPanel | ||
| ref={panelRef} | ||
| className={gwMerge( | ||
| WIDTH_OPTIONS[size], | ||
| "gw-space-y-4", | ||
| widthClass, | ||
| "gw-border", | ||
| "gw-rounded-lg", | ||
| "gw-shadow-lg", | ||
| "gw-bg-white", | ||
| "gw-p-12", | ||
| "gw-relative", | ||
| )} | ||
| transition={dialogPanelTransition} | ||
| > | ||
| {dialogTitle && ( | ||
| <DialogTitle className="gw-font-bold gw-text-center"> | ||
| {dialogTitle} | ||
| </DialogTitle> | ||
| <div className="gw-cursor-move gw-p-4 gw-bg-gray-100 gw-rounded"> | ||
| <DialogTitle className="gw-font-bold gw-text-center gw-select-none"> | ||
| {dialogTitle} | ||
| </DialogTitle> | ||
| </div> | ||
| )} | ||
| {dialogDescription && ( | ||
| <Description>{dialogDescription}</Description> | ||
| <Description className="gw-px-5 gw-my-2"> | ||
| {dialogDescription} | ||
| </Description> | ||
| )} | ||
| {children} | ||
| {buttons} | ||
| <div className="gw-overflow-auto gw-h-full gw-px-5 gw-bg-white dark:gw-bg-slate-700 dark:gw-text-white"> | ||
| {children} | ||
| </div> | ||
| <div className="gw-flex gw-items-center gw-bg-gray-100 gw-rounded-b"> | ||
| <div className="gw-px-4 gw-py-4">{footer}</div> | ||
| <div | ||
| onPointerDown={(e) => { | ||
| e.preventDefault(); | ||
| e.stopPropagation(); | ||
| setResizing(true); | ||
| }} | ||
| className="gw-w-6 gw-h-6 gw-cursor-se-resize gw-ml-auto gw-mt-8" | ||
| style={{ | ||
| backgroundImage: | ||
| "linear-gradient(135deg, transparent 45%, #4b5563 45%, #4b5563 55%, transparent 55%)," + | ||
| "linear-gradient(135deg, transparent 65%, #4b5563 65%, #4b5563 75%, transparent 75%)", | ||
| backgroundRepeat: "no-repeat", | ||
| backgroundPosition: "bottom right", | ||
| backgroundSize: "100% 100%", | ||
| touchAction: "none", | ||
| }} | ||
| title="Resize" | ||
| /> | ||
| </div> | ||
| </DialogPanel> | ||
| </div> | ||
| </div> | ||
|
|
||
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be removed, looks to be unused