@linzjs/windows 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.storybook/main.ts +26 -6
  2. package/README.md +32 -6
  3. package/package.json +34 -12
  4. package/src/modal/Modal.tsx +9 -5
  5. package/src/modal/ModalContextProvider.tsx +35 -36
  6. package/src/modal/PreModal.tsx +45 -0
  7. package/src/panel/OpenPanelButton.tsx +18 -0
  8. package/src/panel/OpenPanelIcon.scss +73 -0
  9. package/src/panel/OpenPanelIcon.tsx +50 -0
  10. package/src/panel/Panel.scss +34 -0
  11. package/src/panel/Panel.tsx +150 -0
  12. package/src/panel/PanelContext.ts +17 -0
  13. package/src/panel/PanelInstanceContext.ts +41 -0
  14. package/src/panel/PanelInstanceContextProvider.tsx +47 -0
  15. package/src/panel/PanelsContext.tsx +36 -0
  16. package/src/panel/PanelsContextProvider.tsx +140 -0
  17. package/src/panel/PopoutWindow.tsx +183 -0
  18. package/src/panel/generateId.ts +23 -0
  19. package/src/panel/handleStyleSheetsChanges.ts +71 -0
  20. package/src/stories/Introduction.mdx +18 -0
  21. package/src/stories/Introduction.stories.tsx +8 -0
  22. package/src/stories/modal/Modal.mdx +9 -3
  23. package/src/stories/modal/Modal.stories.tsx +1 -1
  24. package/src/stories/modal/PreModal.mdx +26 -0
  25. package/src/stories/modal/PreModal.stories.tsx +27 -0
  26. package/src/stories/modal/PreModal.tsx +79 -0
  27. package/src/stories/modal/TestModal.scss +21 -0
  28. package/src/stories/panel/PanelButtons/ShowPanel.mdx +21 -0
  29. package/src/stories/panel/PanelButtons/ShowPanel.stories.tsx +27 -0
  30. package/src/stories/panel/PanelButtons/ShowPanel.tsx +86 -0
  31. package/src/stories/panel/ShowPanel/ShowPanel.mdx +20 -0
  32. package/src/stories/panel/ShowPanel/ShowPanel.stories.tsx +27 -0
  33. package/src/stories/panel/ShowPanel/ShowPanel.tsx +70 -0
  34. package/src/stories/panel/ShowPanelResizingAgGrid/ShowPanelResizingAgGrid.mdx +21 -0
  35. package/src/stories/panel/ShowPanelResizingAgGrid/ShowPanelResizingAgGrid.stories.tsx +27 -0
  36. package/src/stories/panel/ShowPanelResizingAgGrid/ShowPanelResizingStepAgGrid.tsx +164 -0
  37. package/src/stories/support.js +16 -0
  38. package/src/util/useInterval.ts +11 -19
@@ -0,0 +1,17 @@
1
+ import { PanelSize } from "./PanelInstanceContext";
2
+ import { createContext } from "react";
3
+
4
+ export interface PanelContextType {
5
+ resizePanel: (size: Partial<PanelSize>) => void;
6
+ }
7
+
8
+ const NoContextError = () => {
9
+ console.error("Missing PanelContext Provider");
10
+ };
11
+
12
+ /**
13
+ * Provides access to closing/popping panels
14
+ */
15
+ export const PanelContext = createContext<PanelContextType>({
16
+ resizePanel: NoContextError,
17
+ });
@@ -0,0 +1,41 @@
1
+ import { createContext } from "react";
2
+
3
+ export interface PanelSize {
4
+ width: number;
5
+ height: number;
6
+ }
7
+
8
+ export interface PanelInstanceContextType {
9
+ title: string;
10
+ setTitle: (title: string) => void;
11
+ uniqueId: string;
12
+ panelName: string;
13
+ bounds: string | Element | undefined;
14
+ panelTogglePopout: () => void;
15
+ panelClose: () => void;
16
+ setPanelWindow: (w: Window) => void;
17
+ bringPanelToFront: () => void;
18
+ panelPoppedOut: boolean;
19
+ zIndex: number;
20
+ }
21
+
22
+ const NoContextError = () => {
23
+ console.error("Missing PanelInstanceContext Provider");
24
+ };
25
+
26
+ /**
27
+ * Provides access to closing/popping panels
28
+ */
29
+ export const PanelInstanceContext = createContext<PanelInstanceContextType>({
30
+ title: "Missing PanelInstanceContext Provider: title",
31
+ uniqueId: "Missing PanelInstanceContext Provider: uniqueId",
32
+ panelName: "Missing PanelInstanceContext Provider: panelName",
33
+ bounds: document.body,
34
+ setTitle: NoContextError,
35
+ panelTogglePopout: NoContextError,
36
+ panelClose: NoContextError,
37
+ setPanelWindow: NoContextError,
38
+ bringPanelToFront: NoContextError,
39
+ panelPoppedOut: false,
40
+ zIndex: 0,
41
+ });
@@ -0,0 +1,47 @@
1
+ import { PanelInstanceContext } from "./PanelInstanceContext";
2
+ import { PanelInstance, PanelsContext } from "./PanelsContext";
3
+ import { ReactElement, ReactNode, useContext, useState } from "react";
4
+
5
+ /**
6
+ * Provides access to closing/popping panels
7
+ */
8
+ export const PanelInstanceContextProvider = ({
9
+ panelInstance,
10
+ bounds,
11
+ children,
12
+ }: {
13
+ bounds: string | Element | undefined;
14
+ panelInstance: PanelInstance;
15
+ children: ReactNode;
16
+ }): ReactElement => {
17
+ const { closePanel, togglePopOut, bringPanelToFront } = useContext(PanelsContext);
18
+ const [title, setTitle] = useState("");
19
+
20
+ return (
21
+ <PanelInstanceContext.Provider
22
+ value={{
23
+ title,
24
+ setTitle,
25
+ uniqueId: panelInstance.uniqueId,
26
+ bounds,
27
+ panelName: panelInstance.uniqueId,
28
+ panelClose: () => {
29
+ panelInstance.window?.close();
30
+ panelInstance.window = null;
31
+ closePanel(panelInstance);
32
+ },
33
+ panelTogglePopout: () => {
34
+ togglePopOut(panelInstance);
35
+ },
36
+ panelPoppedOut: panelInstance.poppedOut,
37
+ setPanelWindow: (w: Window) => {
38
+ panelInstance.window = w;
39
+ },
40
+ bringPanelToFront: () => bringPanelToFront(panelInstance),
41
+ zIndex: panelInstance.zIndex,
42
+ }}
43
+ >
44
+ {children}
45
+ </PanelInstanceContext.Provider>
46
+ );
47
+ };
@@ -0,0 +1,36 @@
1
+ import { ReactElement, createContext } from "react";
2
+
3
+ export interface PanelPosition {
4
+ x: number;
5
+ y: number;
6
+ }
7
+
8
+ export interface PanelInstance {
9
+ uniqueId: string;
10
+ componentInstance: ReactElement;
11
+ zIndex: number;
12
+ poppedOut: boolean;
13
+ window: Window | null;
14
+ }
15
+
16
+ export interface PanelsContextType {
17
+ openPanels: Set<string>;
18
+ openPanel: (uniqueId: string, component: () => ReactElement) => void;
19
+ closePanel: (panelInstance: PanelInstance) => void;
20
+ togglePopOut: (panelInstance: PanelInstance) => void;
21
+ bringPanelToFront: (panelInstance: PanelInstance) => void;
22
+ nextStackPosition: () => PanelPosition;
23
+ }
24
+
25
+ const NoContext = () => {
26
+ console.error("Missing PanelContext Provider");
27
+ };
28
+
29
+ export const PanelsContext = createContext<PanelsContextType>({
30
+ openPanels: new Set(),
31
+ openPanel: NoContext,
32
+ closePanel: NoContext,
33
+ togglePopOut: NoContext,
34
+ bringPanelToFront: NoContext,
35
+ nextStackPosition: () => ({ x: 0, y: 0 }),
36
+ });
@@ -0,0 +1,140 @@
1
+ import { useInterval } from "../util/useInterval";
2
+ import { PanelInstanceContextProvider } from "./PanelInstanceContextProvider";
3
+ import { PanelInstance, PanelPosition, PanelsContext } from "./PanelsContext";
4
+ import { castArray, maxBy, sortBy } from "lodash-es";
5
+ import { Fragment, PropsWithChildren, ReactElement, useCallback, useMemo, useRef, useState } from "react";
6
+
7
+ export interface PanelsContextProviderProps {
8
+ baseZIndex?: number;
9
+ bounds?: string | Element;
10
+ tilingStart?: PanelPosition;
11
+ tilingOffset?: PanelPosition;
12
+ tilingMax?: PanelPosition;
13
+ }
14
+
15
+ export const PanelsContextProvider = ({
16
+ bounds,
17
+ baseZIndex = 500,
18
+ children,
19
+ tilingStart = { x: 30, y: 30 },
20
+ tilingOffset = { x: 30, y: 50 },
21
+ tilingMax = { x: 200, y: 300 },
22
+ }: PropsWithChildren<PanelsContextProviderProps>): ReactElement => {
23
+ const stackPositionRef = useRef(tilingStart);
24
+
25
+ // Can't use a map here as we need to retain render order for performance
26
+ const [panelInstances, setPanelInstances] = useState<PanelInstance[]>([]);
27
+
28
+ const bringPanelToFront = useCallback(
29
+ (panelInstance: PanelInstance) => {
30
+ if (panelInstance.window) {
31
+ panelInstance.window.focus();
32
+ } else {
33
+ const maxZIndexPanelInstance = maxBy(panelInstances, "zIndex");
34
+ // Prevent unnecessary state updates
35
+ if (maxZIndexPanelInstance === panelInstance) return;
36
+ sortBy(panelInstances, (pi) => (panelInstance === pi ? 32768 : pi.zIndex)).forEach(
37
+ (pi, i) => (pi.zIndex = baseZIndex + i),
38
+ );
39
+ setPanelInstances([...panelInstances]);
40
+ }
41
+ },
42
+ [baseZIndex, panelInstances],
43
+ );
44
+
45
+ const openPanel = useCallback(
46
+ (uniqueId: string, componentFn: () => ReactElement): void => {
47
+ try {
48
+ const existingPanelInstance = panelInstances.find((pi) => pi.uniqueId === uniqueId);
49
+ if (existingPanelInstance) {
50
+ if (existingPanelInstance.window) {
51
+ existingPanelInstance.window?.focus();
52
+ } else {
53
+ bringPanelToFront(existingPanelInstance);
54
+ }
55
+ return;
56
+ }
57
+
58
+ // If there are any exceptions the modal won't show
59
+ setPanelInstances([
60
+ ...panelInstances,
61
+ {
62
+ uniqueId,
63
+ componentInstance: componentFn(),
64
+ zIndex: baseZIndex + panelInstances.length,
65
+ poppedOut: false,
66
+ window: null,
67
+ },
68
+ ]);
69
+ } catch (e) {
70
+ console.error(e);
71
+ }
72
+ },
73
+ [baseZIndex, bringPanelToFront, panelInstances],
74
+ );
75
+
76
+ const closePanel = useCallback(
77
+ (panelInstance: PanelInstance | PanelInstance[]) => {
78
+ const panelNames = castArray(panelInstance).map((pi) => pi.uniqueId);
79
+ const newPanelInstances = panelInstances.filter((pi) => !panelNames.includes(pi.uniqueId));
80
+ if (panelInstances.length !== newPanelInstances.length) setPanelInstances(newPanelInstances);
81
+ },
82
+ [panelInstances],
83
+ );
84
+
85
+ const togglePopOut = useCallback(
86
+ (panelInstance: PanelInstance) => {
87
+ panelInstance.poppedOut = !panelInstance.poppedOut;
88
+ if (!panelInstance.poppedOut) {
89
+ panelInstance.window = null;
90
+ }
91
+ setPanelInstances([...panelInstances]);
92
+ },
93
+ [panelInstances],
94
+ );
95
+
96
+ /**
97
+ * It's not easy to tell the difference between a window closing and a window popping in via events,
98
+ * so we're periodically checking instead.
99
+ */
100
+ useInterval(() => {
101
+ // close any panels that have a window that is closed
102
+ closePanel(panelInstances.filter((pi) => pi.window?.closed));
103
+ }, 500);
104
+
105
+ /**
106
+ * Tile the panel position relative to previous panel
107
+ */
108
+ const nextStackPosition = useCallback(() => {
109
+ const p = { ...stackPositionRef.current };
110
+ const result = { ...p };
111
+ p.x += tilingOffset.x;
112
+ p.y += tilingOffset.y;
113
+ stackPositionRef.current = p.x > tilingMax.x || p.y > tilingMax.y ? tilingStart : p;
114
+ return result;
115
+ }, [tilingMax.x, tilingMax.y, tilingOffset.x, tilingOffset.y, tilingStart]);
116
+
117
+ const openPanels = useMemo(() => new Set(panelInstances.map((pi) => pi.uniqueId)), [panelInstances]);
118
+
119
+ return (
120
+ <PanelsContext.Provider
121
+ value={{
122
+ openPanels,
123
+ openPanel,
124
+ closePanel,
125
+ togglePopOut,
126
+ bringPanelToFront,
127
+ nextStackPosition,
128
+ }}
129
+ >
130
+ <Fragment key={"panels"}>
131
+ {panelInstances.map((panelInstance) => (
132
+ <PanelInstanceContextProvider key={panelInstance.uniqueId} panelInstance={panelInstance} bounds={bounds}>
133
+ {panelInstance.componentInstance}
134
+ </PanelInstanceContextProvider>
135
+ ))}
136
+ </Fragment>
137
+ <Fragment key={"children"}>{children}</Fragment>
138
+ </PanelsContext.Provider>
139
+ );
140
+ };
@@ -0,0 +1,183 @@
1
+ import { PanelInstanceContext } from "./PanelInstanceContext";
2
+ import { makePopoutId, popoutWindowDivId } from "./generateId";
3
+ import { copyStyleSheets, observeStyleSheetChanges } from "./handleStyleSheetsChanges";
4
+ import createCache from "@emotion/cache";
5
+ import { CacheProvider } from "@emotion/react";
6
+ import { Dispatch, ReactElement, ReactNode, SetStateAction, useContext, useEffect, useRef, useState } from "react";
7
+ import ReactDOM from "react-dom";
8
+
9
+ export type FloatingWindowSize = { height: number; width: number };
10
+
11
+ interface PopoutWindowProps {
12
+ name: string;
13
+ title?: string; // The title of the popout window
14
+ children: ReactNode; // what to render inside the window
15
+ size: FloatingWindowSize;
16
+
17
+ //says if we want to watch for any dynamic style changes
18
+ //only needed when using UI libraries like MUI
19
+ observeStyleChanges?: boolean;
20
+
21
+ /** Testing seam to avoid extensively mocking the window object. */
22
+ openExternalWindowFn?: () => OpenExternalWindowResponse;
23
+ className?: string;
24
+ }
25
+
26
+ type OpenExternalWindowResponse = {
27
+ externalWindow: Window | null;
28
+ onCloseExternalWindow: (() => void) | null;
29
+ };
30
+
31
+ /**
32
+ * Open and scaffold the new external window.
33
+ *
34
+ * The main motivation for extracting these procedures into a function is for testing ease. JSDOM does not mock out the
35
+ * entire window object, making the PopoutWindow component difficult to unit test without extensive mocking. Instead, we
36
+ * can avoid mocking most of the window methods entirely by conditionally calling an alternative to `openExternalWindow`
37
+ * during unit testing by providing a truthy `openExternalWindowFn` prop.
38
+ */
39
+ const openExternalWindow = ({
40
+ title,
41
+ size,
42
+ setContainerElement,
43
+ setHeadElement,
44
+ observeStyleChanges = false,
45
+ className,
46
+ }: {
47
+ title: string;
48
+ size: { width: number; height: number };
49
+ setContainerElement: Dispatch<SetStateAction<HTMLElement | undefined>>;
50
+ setHeadElement: Dispatch<SetStateAction<HTMLElement | undefined>>;
51
+ observeStyleChanges?: boolean;
52
+ className?: string;
53
+ }): OpenExternalWindowResponse => {
54
+ const response: OpenExternalWindowResponse = { externalWindow: null, onCloseExternalWindow: null };
55
+
56
+ // NOTE:
57
+ // 1/ Edge ignores the left and top settings
58
+ // 2/ Chrome has a bug that screws up the window size by a factor dependent on your windows scaling settings
59
+ // if you open the browser in a window with a scaling and drag it to a window with a different scaling
60
+ const features =
61
+ `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,` +
62
+ `width=${size.width},height=${size.height},left=300,top=200`;
63
+ response.externalWindow = window.open("", makePopoutId(title), features);
64
+
65
+ if (response.externalWindow) {
66
+ const { externalWindow } = response;
67
+
68
+ // Reset the default body style applied by the browser
69
+ externalWindow.document.body.style.margin = "0";
70
+ externalWindow.document.body.style.padding = "0";
71
+
72
+ // Remove any existing body HTML from this window
73
+ const existingBodyMountElement = externalWindow.document.body?.firstChild;
74
+ if (existingBodyMountElement) {
75
+ externalWindow.document.body.removeChild(existingBodyMountElement);
76
+ }
77
+ // Create a new HTML element to hang our rendering off
78
+ const newMountElement = externalWindow.document.createElement("div");
79
+ newMountElement.className = `PopoutWindowContainer ${className}`;
80
+ setContainerElement(newMountElement);
81
+ externalWindow.document.body.appendChild(newMountElement);
82
+
83
+ // Do the same for the head node (where all the styles are added)
84
+ const existingHeadMountElement = externalWindow.document.head?.firstChild;
85
+ if (existingHeadMountElement) {
86
+ externalWindow.document.head.removeChild(existingHeadMountElement);
87
+ }
88
+ // and an HTML element to hang our copied styles off
89
+ const newHeadMountElement = externalWindow.document.createElement("div");
90
+ externalWindow.document.head.appendChild(newHeadMountElement);
91
+ setHeadElement(newHeadMountElement);
92
+
93
+ // Set External window title
94
+ externalWindow.document.title = title;
95
+
96
+ // Set an id to the external window div to be queried and used by react-modal as parentSelector
97
+ // so the modal shows up in the popped out window instead the main window
98
+ newMountElement.id = popoutWindowDivId(title);
99
+
100
+ copyStyleSheets(newHeadMountElement);
101
+
102
+ let observer: MutationObserver | null = null;
103
+ if (observeStyleChanges) {
104
+ observer = observeStyleSheetChanges(newHeadMountElement);
105
+ }
106
+
107
+ externalWindow.onbeforeunload = () => observer?.disconnect();
108
+
109
+ response.onCloseExternalWindow = () => {
110
+ // Make sure the window closes when the component unmounts
111
+ externalWindow.close();
112
+ };
113
+ }
114
+
115
+ return response;
116
+ };
117
+
118
+ export const PopoutWindow = ({
119
+ name,
120
+ title = name,
121
+ size,
122
+ observeStyleChanges,
123
+ className,
124
+ openExternalWindowFn,
125
+ children,
126
+ }: PopoutWindowProps): ReactElement | null => {
127
+ const { setPanelWindow } = useContext(PanelInstanceContext);
128
+
129
+ const extWindow = useRef<Window | null>(null);
130
+
131
+ const [containerElement, setContainerElement] = useState<HTMLElement>();
132
+ const [headElement, setHeadElement] = useState<HTMLElement>();
133
+
134
+ // We only want to update the title when it changes
135
+ useEffect(() => {
136
+ if (extWindow.current) {
137
+ extWindow.current.document.title = title;
138
+ }
139
+ }, [title]);
140
+
141
+ // When we create this component, open a new window
142
+ useEffect(
143
+ () => {
144
+ const oew = openExternalWindowFn ?? openExternalWindow;
145
+ const { externalWindow, onCloseExternalWindow } = oew({
146
+ title,
147
+ size,
148
+ setContainerElement,
149
+ setHeadElement,
150
+ observeStyleChanges,
151
+ className,
152
+ });
153
+
154
+ if (!externalWindow) return;
155
+
156
+ setPanelWindow(externalWindow);
157
+
158
+ const closeExternalWindow = () => {
159
+ externalWindow.close();
160
+ };
161
+
162
+ // When the main window closes or reloads, close the popout
163
+ window.addEventListener("beforeunload", closeExternalWindow, { once: true });
164
+
165
+ return () => {
166
+ onCloseExternalWindow?.();
167
+ };
168
+ },
169
+ // These deps MUST be empty
170
+ // eslint-disable-next-line react-hooks/exhaustive-deps
171
+ [],
172
+ );
173
+
174
+ // Render this component's children into the root element of the popout window
175
+ // The `@emotion/react/CacheProvider`provides a cache derived from createCache for inline styles
176
+ // these will be generated into the `container` and prefixed with the `key` below, ensuring
177
+ // the styles are in the popout rather than the root window.
178
+ const myCache = createCache({ key: makePopoutId(name), container: headElement });
179
+
180
+ const wrappedChildren = <CacheProvider value={myCache}>{children}</CacheProvider>;
181
+
182
+ return containerElement ? ReactDOM.createPortal(wrappedChildren, containerElement) : null;
183
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The only valid characters for an @emotion key are lower case chars or '-'.
3
+ * We need to replace any char that is not valid with a substitute.
4
+ */
5
+ export function makePopoutId(str: string): string {
6
+ return str
7
+ .replace(/[/\s_]*/g, "-")
8
+ .replaceAll("0", "zero")
9
+ .replaceAll("1", "one")
10
+ .replaceAll("2", "two")
11
+ .replaceAll("3", "three")
12
+ .replaceAll("4", "four")
13
+ .replaceAll("5", "five")
14
+ .replaceAll("6", "six")
15
+ .replaceAll("7", "seven")
16
+ .replaceAll("8", "eight")
17
+ .replaceAll("9", "nine")
18
+ .toLowerCase();
19
+ }
20
+
21
+ export function popoutWindowDivId(windowTitle: string): string {
22
+ return `popoutWindow-${makePopoutId(windowTitle)}`;
23
+ }
@@ -0,0 +1,71 @@
1
+ import { throttle } from "lodash-es";
2
+
3
+ // Copy the app's styles into the new window
4
+ export const copyStyleSheets = (newHeadMountElement: HTMLDivElement) => {
5
+ const stylesheets = Array.from(document.styleSheets);
6
+
7
+ stylesheets.forEach((stylesheet) => {
8
+ if (stylesheet.href) {
9
+ const newStyleElement = document.createElement("link");
10
+ newStyleElement.rel = "stylesheet";
11
+ newStyleElement.href = stylesheet.href;
12
+ newHeadMountElement.appendChild(newStyleElement);
13
+ } else {
14
+ const css = stylesheet as CSSStyleSheet;
15
+ if (css && css.cssRules && css.cssRules.length > 0) {
16
+ const newStyleElement = document.createElement("style");
17
+ Array.from(css.cssRules).forEach((rule) => {
18
+ newStyleElement.appendChild(document.createTextNode(rule.cssText));
19
+ });
20
+ newHeadMountElement.appendChild(newStyleElement);
21
+ }
22
+ }
23
+ });
24
+ };
25
+ /**
26
+ * Add listener for dynamic style changes
27
+ * This works by listening to any changes to the head element
28
+ * If there are any changes it replaces all the styles in the floating window
29
+ * at most every 100ms
30
+ * @param newHeadMountElement
31
+ */
32
+ export const observeStyleSheetChanges = (newHeadMountElement: HTMLDivElement) => {
33
+ const targetNode = document.querySelector("head");
34
+
35
+ if (!targetNode) {
36
+ return null;
37
+ }
38
+ // Options for the observer (which mutations to observe)
39
+ const config = { /*attributes: true,*/ childList: true /*, subtree: true */ };
40
+
41
+ //throttle the copying of the all the styles at most 100ms
42
+ const throttleCopyStyleSheets = throttle(() => {
43
+ copyStyleSheets(newHeadMountElement);
44
+ }, 100);
45
+
46
+ // Callback function to execute when mutations are observed
47
+ const callback: MutationCallback = function (mutationList) {
48
+ for (const mutation of mutationList) {
49
+ if (mutation.type === "childList") {
50
+ // console.log("A child node has been added or removed.", mutation);
51
+ //naive way up updating style sheets
52
+ //the observer tells us what has been added and removed, but syncing the changes with the
53
+ //floating window is complicated, so the easiest way is to just wait for modifications for finish
54
+ //updating, then replace them all
55
+ //we could probably just copy all style on every modification, by that might have a performance hit
56
+ throttleCopyStyleSheets();
57
+ } /*else if (mutation.type === "attributes") {
58
+ //Do we need to handle attribute modifications?
59
+ // console.log("The " + mutation.attributeName + " attribute was modified.");
60
+ }*/
61
+ }
62
+ };
63
+
64
+ // Create an observer instance linked to the callback function
65
+ const observer = new MutationObserver(callback);
66
+
67
+ // Start observing the target node for configured mutations
68
+ observer.observe(targetNode, config);
69
+
70
+ return observer;
71
+ };
@@ -0,0 +1,18 @@
1
+ # @linzjs/windows
2
+
3
+ [![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release)
4
+
5
+ > Reusable promise based windowing component for LINZ / Toitū te whenua.
6
+
7
+ Rect state based modals/windows are painful because they require:
8
+ - shared states for open/closed.
9
+ - callbacks/states for return values.
10
+ - inline modal/window includes, which prevent you from closing the invoking component before the modal/window has
11
+ completed.
12
+
13
+ This module gives you promise based modals/windows which don't require all the state
14
+ based boiler-plate / inline-components.
15
+
16
+ ## Features
17
+ - Async HTML dialog based Modals.
18
+ - Draggable and resizeable, pop-in/out Windows.
@@ -0,0 +1,8 @@
1
+ import { Meta } from "@storybook/react";
2
+
3
+ // eslint-disable-next-line storybook/story-exports
4
+ const meta: Meta<typeof Object> = {
5
+ title: "Introduction",
6
+ };
7
+
8
+ export default meta;
@@ -1,9 +1,10 @@
1
+ import {getBlocks, blockToString} from "../support.js";
1
2
  import {Meta, Source, Story} from "@storybook/blocks";
2
3
  import * as ModalStories from "./Modal.stories"
3
4
 
4
5
  import myModule from './TestModal?raw';
5
6
 
6
- <Meta title="Modal/Primary" of={ModalStories}/>
7
+ <Meta name="Show" of={ModalStories}/>
7
8
  # Show Modal
8
9
  ## Example
9
10
  Click to show the modal:
@@ -11,10 +12,15 @@ Click to show the modal:
11
12
 
12
13
  ## Code
13
14
  <br/>
14
- {myModule.toString().split("// #").splice(1).map(block => (
15
+ {getBlocks(myModule).map((block) => (
15
16
  <>
16
17
  <h3>{block.split("\n")[0]}</h3>
17
- <Source code={block.split("\n").splice(1).join("\n")} language='typescript'/>
18
+ <Source code={blockToString(block)} language="typescript" />
18
19
  </>
19
20
  ))}
20
21
 
22
+ ### Example: Mocking Modal response
23
+
24
+ <Source code={"{ /* Responds with the value 1 */ }\n<ModalContext.Provider value={showModal: async() => 1}>\n" +
25
+ "... Component under test ...\n" +
26
+ "</ModalContext.Provider>\n"} language="typescript"/>
@@ -3,7 +3,7 @@ import { TestModalUsage } from "./TestModal";
3
3
  import type { Meta, StoryObj } from "@storybook/react";
4
4
 
5
5
  const meta: Meta<typeof TestModalUsage> = {
6
- title: "Modal",
6
+ title: "Components/Modal",
7
7
  component: TestModalUsage,
8
8
  argTypes: {
9
9
  backgroundColor: { control: "color" },
@@ -0,0 +1,26 @@
1
+ import {getBlocks, blockToString} from "../support";
2
+ import {Meta, Source, Story} from "@storybook/blocks";
3
+ import * as PreModalStories from "./PreModal.stories"
4
+
5
+ import myModule from './PreModal?raw';
6
+
7
+ <Meta name="Show Preset Modal" of={PreModalStories}/>
8
+ # Show Preset Modal
9
+ ## Example
10
+ Click to show the modal:
11
+ <Story of={PreModalStories.ShowPreModal}/>
12
+
13
+ ## Code
14
+ <br/>
15
+ {getBlocks(myModule).map((block) => (
16
+ <>
17
+ <h3>{block.split("\n")[0]}</h3>
18
+ <Source code={blockToString(block)} language="typescript" />
19
+ </>
20
+ ))}
21
+
22
+ ### Example: Mocking Modal response
23
+
24
+ <Source code={"{ /* Responds with the value 1 */ }\n<ModalContext.Provider value={showModal: async() => 1}>\n" +
25
+ "... Component under test ...\n" +
26
+ "</ModalContext.Provider>\n"} language="typescript"/>
@@ -0,0 +1,27 @@
1
+ import { ModalContextProvider } from "../../modal/ModalContextProvider";
2
+ import { PreModalUsage } from "./PreModal";
3
+ import type { Meta, StoryObj } from "@storybook/react";
4
+
5
+ const meta: Meta<typeof PreModalUsage> = {
6
+ title: "Components/Modal",
7
+ component: PreModalUsage,
8
+ argTypes: {
9
+ backgroundColor: { control: "color" },
10
+ },
11
+ decorators: [
12
+ (Story: any) => (
13
+ <div>
14
+ <ModalContextProvider>
15
+ <Story />
16
+ </ModalContextProvider>
17
+ </div>
18
+ ),
19
+ ],
20
+ };
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ export const ShowPreModal: Story = {
26
+ args: {},
27
+ };