Skip to content

Commit

Permalink
Merge pull request #1565 from TouK/NU-1881-fix-manual-window-size-change
Browse files Browse the repository at this point in the history
Nu 1881 fix manual window size change
  • Loading branch information
JulianWielga authored Nov 25, 2024
2 parents 817f724 + 9af367b commit 8b82d6e
Show file tree
Hide file tree
Showing 12 changed files with 203 additions and 171 deletions.
45 changes: 15 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@
"react-focus-lock": "2.12.1",
"react-hotkeys-hook": "4.5.0",
"react-inspector": "6.0.2",
"react-rnd": "10.4.10",
"react-transition-group": "4.4.5",
"react-rnd": "10.4.13",
"react-transition-state": "2.2.0",
"reselect": "5.1.0",
"rooks": "7.14.1"
},
Expand Down
12 changes: 7 additions & 5 deletions src/components/ModalMask.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { css, cx } from "@emotion/css";
import React, { forwardRef, RefObject } from "react";
import React from "react";
import { rgba } from "../rgba";
import { useModalMaskTheme } from "../themeHooks";
import { WindowId } from "../types";
import { useTransition } from "./TransitionProvider";

export const ModalMask = forwardRef(({ zIndex }: { zIndex?: number }, ref: RefObject<HTMLDivElement>): JSX.Element => {
export const ModalMask = ({ zIndex, id }: { zIndex?: number; id: WindowId }): JSX.Element => {
const modalMaskTheme = useModalMaskTheme();
const modalMaskClass = css({
top: 0,
Expand All @@ -13,7 +15,7 @@ export const ModalMask = forwardRef(({ zIndex }: { zIndex?: number }, ref: RefOb
position: "fixed",
background: rgba("black", 0.6),
});
return <div ref={ref} className={cx(modalMaskClass, modalMaskTheme)} style={{ zIndex }} />;
});
const { getTransitionStyle } = useTransition();

ModalMask.displayName = "ModalMask";
return <div className={cx(modalMaskClass, modalMaskTheme, ...getTransitionStyle(id))} style={{ zIndex }} />;
};
77 changes: 77 additions & 0 deletions src/components/TransitionProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { createContext, PropsWithChildren, useCallback, useContext } from "react";
import { useTransitionMap } from "react-transition-state";
import { defaultFadeAnimation, FadeInAnimation } from "./getFadeInAnimation";
import { WindowId } from "../types";

const TRANSITION_TIMEOUT = 100;
const transitionsAnimationTime = new Map<string, number>();
const TransitionContext = createContext<{
startTransition: (id: string) => Promise<boolean>;
finishTransition: (id: string) => Promise<boolean>;
getTransitionStyle: (id: string, fadeInAnimation?: FadeInAnimation) => string[];
}>(null);

export const TransitionProvider = ({ children }: PropsWithChildren) => {
const transition = useTransitionMap({
timeout: TRANSITION_TIMEOUT,
exit: true,
preExit: false,
allowMultiple: true,
});

const getTransitionStyle = useCallback(
(id: WindowId, fadeInAnimation: FadeInAnimation = defaultFadeAnimation) => {
transitionsAnimationTime.set(id, fadeInAnimation.animationTime * 1000);
const transitionItem = transition.stateMap.get(id);
const entered = transitionItem.status === "entered" && fadeInAnimation.entered;
const exited = transitionItem.status === "exited" && fadeInAnimation.exited;

return [fadeInAnimation.mounted, entered, exited];
},
[transition.stateMap],
);

const startTransition = useCallback(
(id: WindowId) => {
return new Promise<boolean>((resolve) => {
transition.setItem(id);
transition.toggle(id, true);
const transitionAnimationTime = transitionsAnimationTime.get(id);

setTimeout(() => {
resolve(true);
}, TRANSITION_TIMEOUT + transitionAnimationTime);
});
},
[transition],
);

const finishTransition = useCallback(
(id: WindowId) => {
return new Promise<boolean>((resolve) => {
transition.toggle(id, false);
const transitionAnimationTime = transitionsAnimationTime.get(id);
setTimeout(() => {
transition.deleteItem(id);
transitionsAnimationTime.delete(id);
resolve(true);
}, TRANSITION_TIMEOUT + transitionAnimationTime);
});
},
[transition],
);

return (
<TransitionContext.Provider value={{ startTransition, finishTransition, getTransitionStyle }}>{children}</TransitionContext.Provider>
);
};

export const useTransition = () => {
const context = useContext(TransitionContext);

if (!context) {
throw new Error();
}

return context;
};
19 changes: 11 additions & 8 deletions src/components/WindowManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AppTheme } from "../AppTheme";
import { WindowManagerContextProvider } from "../context";
import { ContentGetter } from "./window/WindowContent";
import { WindowsContainer } from "./WindowsContainer";
import { TransitionProvider } from "./TransitionProvider";

const defaultTheme = {
backgroundOpacity: 0.9,
Expand Down Expand Up @@ -34,13 +35,15 @@ export function WindowManager<K extends number | string = any>({
...props
}: WindowManagerProps<K>): JSX.Element {
return (
<WindowManagerContextProvider>
<div {...props}>
{children}
<ThemeProvider theme={(outerTheme = {}) => defaultsDeep(theme, outerTheme, defaultTheme)}>
<WindowsContainer contentGetter={contentGetter} />
</ThemeProvider>
</div>
</WindowManagerContextProvider>
<TransitionProvider>
<WindowManagerContextProvider>
<div {...props}>
{children}
<ThemeProvider theme={(outerTheme = {}) => defaultsDeep(theme, outerTheme, defaultTheme)}>
<WindowsContainer contentGetter={contentGetter} />
</ThemeProvider>
</div>
</WindowManagerContextProvider>
</TransitionProvider>
);
}
18 changes: 4 additions & 14 deletions src/components/WindowsContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { flatMap } from "lodash";
import React, { useRef } from "react";
import { createPortal } from "react-dom";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import { useWindowManager } from "../hooks";
import { defaultFadeAnimation } from "./getFadeInAnimation";
import { ModalMask } from "./ModalMask";
import { Window } from "./window/Window";
import { ContentGetter } from "./window/WindowContent";
Expand All @@ -16,22 +14,14 @@ interface WindowsContainerProps {

export function WindowsContainer({ container = document.body, contentGetter }: WindowsContainerProps): JSX.Element {
const { windows } = useWindowManager();
const modalMaskRef = useRef<HTMLDivElement>();
const windowRef = useRef<HTMLDivElement>();

return createPortal(
<TransitionGroup component={WindowsViewport}>
<WindowsViewport>
{flatMap(windows, (d, index) => [
d.isModal && (
<CSSTransition nodeRef={modalMaskRef} key={`${d.id}/mask`} timeout={250} classNames={defaultFadeAnimation}>
<ModalMask ref={modalMaskRef} key={`${d.id}/mask`} zIndex={index} />
</CSSTransition>
),
<CSSTransition nodeRef={windowRef} key={d.id} timeout={250} classNames={defaultFadeAnimation}>
<Window ref={windowRef} data={d} contentGetter={contentGetter} />
</CSSTransition>,
d.isModal && <ModalMask zIndex={index} key={`${d.id}/mask`} id={d.id} />,
<Window key={d.id} data={d} contentGetter={contentGetter} />,
]).filter(Boolean)}
</TransitionGroup>,
</WindowsViewport>,
container,
);
}
14 changes: 5 additions & 9 deletions src/components/getFadeInAnimation.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import { css } from "@emotion/css";

export const getFadeInAnimation = (t = 0.25) => ({
enter: css({
animationTime: t,
mounted: css({
opacity: 0,
}),
enterActive: css({
opacity: 1,
transition: `opacity ${t}s ease-in-out`,
pointerEvents: "none",
}),
exit: css({
entered: css({
opacity: 1,
}),
exitActive: css({
exited: css({
opacity: 0,
transition: `opacity ${t}s ease-in-out`,
pointerEvents: "none",
}),
});

export type FadeInAnimation = ReturnType<typeof getFadeInAnimation>;
export const defaultFadeAnimation = getFadeInAnimation();
export const fastFadeAnimation = getFadeInAnimation(0.15);
40 changes: 17 additions & 23 deletions src/components/window/SnapMask.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,32 @@
import { useTheme } from "@emotion/react";
import React, { useRef } from "react";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import React from "react";
import { rgba } from "../../rgba";
import { fastFadeAnimation } from "../getFadeInAnimation";
import { Box } from "./useSnapAreas";

export const SnapMask = ({ previewBox }: { previewBox: Box }) => {
const nodeRef = useRef<HTMLDivElement>(null);
const {
colors,
spacing: { baseUnit },
} = useTheme();

return (
<TransitionGroup>
<>
{previewBox && (
<CSSTransition nodeRef={nodeRef} timeout={250} classNames={fastFadeAnimation}>
<div
ref={nodeRef}
style={{
boxSizing: "border-box",
position: "fixed",
zIndex: 10,
background: rgba(colors.focusColor, 0.25),
border: `${Math.round(baseUnit / 3)}px solid ${colors?.focusColor}`,
top: previewBox.y,
left: previewBox.x,
width: previewBox.width,
height: previewBox.height,
transition: "all .15s ease-in-out",
}}
/>
</CSSTransition>
<div
style={{
boxSizing: "border-box",
position: "fixed",
zIndex: 10,
background: rgba(colors.focusColor, 0.25),
border: `${Math.round(baseUnit / 3)}px solid ${colors?.focusColor}`,
top: previewBox.y,
left: previewBox.x,
width: previewBox.width,
height: previewBox.height,
transition: "all .15s ease-in-out",
}}
/>
)}
</TransitionGroup>
</>
);
};
8 changes: 4 additions & 4 deletions src/components/window/Window.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { forwardRef, RefObject, useCallback } from "react";
import React, { useCallback } from "react";
import { useWindowManager, useWindowZoom } from "../../hooks";
import { WindowWithOrder } from "../../types";
import { ContentGetter, WindowContent } from "./WindowContent";
Expand All @@ -9,7 +9,7 @@ export interface WindowProps {
contentGetter: ContentGetter;
}

export const Window = forwardRef(({ data, contentGetter }: WindowProps, ref: RefObject<HTMLDivElement>): JSX.Element => {
export const Window = ({ data, contentGetter }: WindowProps): JSX.Element => {
const { isResizable, isStatic, focusParent, id, order, shouldCloseOnEsc } = data;

const { focus: onFocus, close: onClose } = useWindowManager(id);
Expand All @@ -31,7 +31,7 @@ export const Window = forwardRef(({ data, contentGetter }: WindowProps, ref: Ref
height={data.height}
minWidth={data.minWidth}
minHeight={data.minHeight}
ref={ref}
id={data.id}
layoutData={{
width: data.width,
height: data.height,
Expand All @@ -43,6 +43,6 @@ export const Window = forwardRef(({ data, contentGetter }: WindowProps, ref: Ref
<WindowContent contentGetter={contentGetter} data={data} close={onClose} zoom={onToggleZoom} isMaximized={zoom} />
</WindowFrame>
);
});
};

Window.displayName = "Window";
Loading

0 comments on commit 8b82d6e

Please sign in to comment.