@linzjs/floating-windows v1.0.18
Floating Windows
A component that provides a windowing interface for browser content. We provide the windows, you provide the content.
Screenshots
In Browser
Popped out
Interaction
Features
- Can open multiple windows at a time.
- Can drag windows around inside the browser window by dragging on title bar.
- Re-size windows inside the browser window by dragging window edges.
- Pop out window, so the content is inside a separate browser window.
- Pop in window, so the popped out window returns to the original browser window.
- Click on a window to bring to front.
- Has a title bar, with title and action buttons.
- Has a close button and pop out button in title bar.
- Windows can start "popped out".
- Event listener, so you can hook your own code into UI events.
- Example code.
- Storybook examples.
- Not tied to LUI for styles/components (you can provide styles and/or components)
- Provision for other controls inside the header.
- Additional content in title area.
Planned Features
- Some way of allowing this control to work in a mobile setting (either disabled, or mobile aware?)
- Style agnostic - style how you want.
- Ability to set the base Z value.
- Able to get/set entire windows state, so application can, say, store to local storage.
- Can define initial defaults to the context.
- Beefing up the event system (more events, multiple listeners.)
- Listeners can cancel events.
- Ability to open the window as a draggable window, a panel attached to the side, or a panel attached to the bottom.
- Keyboard controls.
Storybook
The link to the latest storybook is here. This is updated automatically when changes are pushed to master.
Installing
npm install --save @linzjs/floating-windows
Styles
You'll also need to include the styles somewhere (like main.ts
or similar) to ensure that the components are rendered as they should.
import "node_modules/@linzjs/floating-windows/dist/style.css";
Quick Example
Surround your app content with the window context provider.
import { FloatingWindowContextProvider } from "@linzjs/floating-windows";
import "@linzjs/floating-windows/dist/style.css";
export const App = () => {
return (
<FloatingWindowContextProvider>
<AppPage />
</FloatingWindowContextProvider>
);
};
// Name of window (we use this below)
const windowAName = "Win A";
Create a floating window component with your content as children
import { FloatingWindow } from "@linzjs/floating-windows";
export const FloatingWindowExample = () => {
return (
<FloatingWindow
name={windowAName}
title={"This is window A"}
initialSize={{ width: 400, height: 400 }}
initialPosition={{ x: 100, y: 100 }}
>
<div>Lorem ipsum dolor sit amet, consectetur...</div>
</FloatingWindow>
);
};
In this example the content is initially not displayed. To display it, you'll need to show the window,
which can be done by specifying the prop startDisplayed
or programmatically.
E.g. Here's a button that does just that. It uses the context to show and hide the window, and to find out if the window is currently open or not.
A button to show/hide the window
import { useFloatingWindow } from "@linzjs/floating-windows";
const TestButton = (props: { windowName: string }): JSX.Element => {
const { showWindow, hideWindow, isWindowOpen } = useFloatingWindow();
const isOpen = isWindowOpen(props.windowName);
return (
<button
style={{ color: isOpen ? "red" : "green" }}
onClick={() => {
if (isOpen) {
hideWindow(props.windowName);
} else {
showWindow(props.windowName);
}
}}
>
{props.windowName}
</button>
);
};
API
Notes:
- Windows are identified by a unique internal name, which can be different to the displayed title.
Window Context
export type FloatingWindowContextType = {
setWindowDefaults: (windowName: string, windowProperties: FloatingWindowProperties) => void;
setWindowProperties: (windowName: string, windowProperties: FloatingWindowProperties) => void;
getWindowProperties: (windowName: string) => FloatingWindowProperties;
getOpenWindowNames: () => string[];
isWindowOpen: (windowName: string) => boolean;
isWindowPoppedOut: (windowName: string) => boolean;
showWindow: (windowName: string, showPoppedOut?: boolean) => void;
hideWindow: (windowName: string | string[]) => void;
bringWindowToTop: (windowName: string) => void;
getTopWindowName: () => string | null;
getWindowPosition: (windowName: string) => FloatingWindowPosition;
setWindowPosition: (windowName: string, position: FloatingWindowPosition) => void;
getPositionalStyle: (windowName: string) => CSS.Properties;
getWindowSize: (windowName: string) => FloatingWindowSize;
setWindowSize: (windowName: string, dimensions: FloatingWindowSize) => void;
setWindowBounds: (windowName: string, windowBounds: string | undefined) => void;
externalWindowsDOM: Record<string, Document>;
setExternalWindowsDOM: React.Dispatch<React.SetStateAction<Record<string, Document>>>;
popWindowOut: (windowName: string) => void;
popWindowIn: (windowName: string) => void;
getZ: (windowName: string) => number;
addEventListener: (
listenerId: string,
eventType: FloatingWindowEventType,
windowName: string,
eventCallback: FloatingWindowCallback,
) => void;
removeEventListener: (
listenerId: string | null,
eventType: FloatingWindowEventType | null,
windowName: string | null,
) => void;
};
Method | Description |
---|---|
setWindowDefaults: (windowName: string, windowProperties: FloatingWindowProperties) => void | Set initial defaults for windows. |
setWindowProperties: (windowName: string, windowProperties: FloatingWindowProperties) => void | Set window properties windowProperties for window windowName |
getWindowProperties: (windowName: string) => FloatingWindowProperties | Return the current properties for window windowName |
getOpenWindowNames: () => string[] | Return a list of the open window names. |
isWindowOpen: (windowName: string) => boolean | Returns true if the window named windowName is open. False otherwise |
isWindowPoppedOut: (windowName: string) => boolean | Returns true if the window named windowName is popped out. False otherwise. |
showWindow: (windowName: string, showPoppedOut?: boolean) => void | Show window named windowName , bringing it to the front. If showPoppedOut is true then the window is started popped out |
hideWindow: (windowName: string) => void | Hide window named windowName , closing it. |
bringWindowToTop: (windowName: string) => void | Bring the window named windowName to the front of the display |
getTopWindowName: () => string \| null | Return the name of the top-most window, or null if there is none. |
getWindowPosition: (windowName: string) => FloatingWindowPosition | Return the position of the window named windowName |
setWindowPosition: (windowName: string, position: FloatingWindowPosition) => void | Set the position of the window windowName to position |
getPositionalStyle: (windowName: string) => CSS.Properties | Return a CSS style that includes the window position for window windowName |
getWindowSize: (windowName: string) => FloatingWindowSize | Return the size of the window named windowName |
setWindowSize: (windowName: string, dimensions: FloatingWindowSize) => void | Set the size of the window called windowName to dimensions |
setWindowBounds: (windowName: string, windowBounds: string \| undefined) => void | Set the window bounds of window windowName to windowBounds . See below |
externalWindowsDOM: Record<string, Document>; | |
setExternalWindowsDOM: React.Dispatch<React.SetStateAction<Record<string, Document>>> | |
popWindowOut: (windowName: string) => void | Pop the window named windowName out into it's own window. |
popWindowIn: (windowName: string) => void | Pop the window named windowName back into the browser. |
getZ: (windowName: string) => number | Return the Z index of the window named windowName |
addEventListener: ( listenerId: string, eventType: FloatingWindowEventType, windowName: string, eventCallback: FloatingWindowCallback) => void | See below |
removeEventListener: ( listenerId: string \| null, eventType: FloatingWindowEventType \| null, windowName: string \| null) => void | See below |
Components
FloatingWindow
Props
interface FloatingWindowProps {
name: string;
title?: string;
children?: JSX.Element;
header?: ReactNode;
extraHeaderButtons?: ReactNode;
initialPosition: FloatingWindowPosition;
initialSize: FloatingWindowSize;
startPoppedOut?: boolean;
startDisplayed?: boolean;
bounds?: string;
standardButtons?: boolean;
headerProperties?: FloatingWindowHeaderProps;
render?: FloatingWindowRenderCallback;
headerRender?: FloatingWindowRenderCallback;
}
Prop | Description |
---|---|
name: string | Unique (internal) name of this window |
title?: string | The displayed title in the window. No title bar is displayed if the title is not given. |
children?: JSX.Element | The window content. |
header?: ReactNode | Add extra header content. For compatibility. |
extraHeaderButtons?: ReactNode | Any extra buttons in the header. |
initialPosition: FloatingWindowPosition | Initial position (X,Y coordinates.) |
initialSize: FloatingWindowSize | Initial size (width and height.) |
startPoppedOut?: boolean | Set to true to start the window popped out. False to keep in in the browser, and undefined to use the last state the window was in. |
startDisplayed? boolean | Set to true to start the window as displayed (shown.) False to start the window as hidden. |
bounds?: string | Define the bounds of the window - what limits the window has on movement. (See below) |
standardButtons?: boolean | Set to true to render the 'standard' buttons. Close/pop in/out. |
headerProperties?: FloatingWindowHeaderProps | Set object used to customise header rendering |
render?: FloatingWindowRenderCallback | For future use |
headerRender?: FloatingWindowRenderCallback | Defines a custom header renderer. (See below) |
Other Components Used
This module uses the windowing capabilities of this component https://github.com/bokuweb/react-rnd In particular, https://github.com/bokuweb/react-rnd#bounds-string describing the bounds options that can be set for a window.
Customising the Header
By default, the window renders with a title bar at the top, with close and popout buttons on the right. The title bar is used to grab and drag the window inside the browser.
There's several ways to customise this (work in progress):
- The property
standardButtons
. Setting this to false will prevent render of the standard close and pop in/out buttons. - Pass properties in the
headerProperties
. This allows you to set a heading text and redefine the buttons. - Provide a custom rendering callback in the property
headerRender
. In this case you are totally responsible for the panel HTML.
There's some components included that make the custom renderer easier to handle:
Component | Description |
---|---|
FloatingWindowPopinButton | The pop in button |
FloatingWindowPopoutButton | The pop out button |
FloatingWindowCloseButton | The close button |
FloatingWindowButtonDivider | A vertical line dividing buttons |
FloatingWindowHeader | A component used to render the default header. Can be used to wrap your own HTML |
The render callback provides these props to help you render. See also the example code.
export interface FloatingWindowRenderProps {
context: FloatingWindowContextType;
windowProps: FloatingWindowProps;
isPoppedOut: boolean;
standardButtons: JSX.Element[];
onClose: () => void;
onPopout: () => void;
onPopin: () => void;
}
Prop | Description |
---|---|
context: FloatingWindowContextType | The current window context |
windowProps: FloatingWindowProps | Props passed to this window |
isPoppedOut: boolean | true if popped out, false otherwise |
standardButtons: JSX.Element[] | An array of standard buttons for you to render if you want. |
onClose: () => void | The standard function called when the close button is clicked |
onPopout: () => void | The standard function called when the popout button is clicked |
onPopin: () => void | The standard function called when the popin button is clicked |
Events
The window context has the ability to call user-supplied callbacks when internal events happen, such as windows being popped out. A listener listens for an event type for a particular window, you specify both when adding the listener.
The addEventListener
method allows you to add a listener to an event, and removeEventListener
removes it. You can add multiple listeners to a single event/window combination.
Each listener you add is identified by a listenerId:string
, allowing you to selectively remove listeners from a window/event.
// Add a listener to showWindow event for window named "windowA"
addEventListener("myShow", FloatingWindowEventType.showWindow, "windowA", (event) => {
console.log(`${event.windowName} shown`);
});
// Remove just the listener added above
removeEventListener("myShow", FloatingWindowEventType.showWindow, "windowA");
// Remove all showWindow listeners for "windowA"
removeEventListener(null, FloatingWindowEventType.showWindow, "windowA");
The removeEventListener
allows you to pass null
for any/all of the window/event/listenerId to act as a don't care value. E.g.
// Remove all listeners from the show window event, for all windows
windowContext.removeEventListener(null, FloatingWindowEventType.showWindow, null);
// Remove all listeners for :windowA"
windowContext.removeEventListener(null, null, "windowA");
// Remove all listeners (all windows)
windowContext.removeEventListener(null, null, null);
Event Types
enum FloatingWindowEventType {
showWindow,
hideWindow,
popoutWindow,
}
Event | Description |
---|---|
showWindow | When a window is shown |
hideWindow | When a window is hidden |
popoutWindow | When a window is popped out |
Development
Build Locally
To build a package locally (creates linzjs-floating-windows-0.0.0.tgz
)
npm run pack
it uses vite to build the javascript and css files, and typescript compiler to create the type definitions.
You can include this directly into your package.json
file like so,
"dependencies": {
"@linzjs/floating-windows": "file:linzjs-floating-windows-0.0.0.tgz"
},
The code in the example folder does just that.
Example Code
Inside examples/basic
(included in the package itself) is a working example of using the package. Note it needs the package already created (as above.)
cd examples/basic
npm i
npm run dev
This gives you the basic vite/react application in your browser.
- Navigate to http://localhost:3000/ to display the example app with a locally generated package.
- Navigate to http://localhost:3000/local/index.html to see a version of the app which uses the package source included directly. This allows you to edit the package source and see results immediately (Without having to build a package.)
By displaying the second page, any changes to the package code will immediately be seen in the dev environment. The first page will remain unchanged as it uses the generated package until you regenerate it with npm run pack-update-example
. This re-builds the package locally, and updates the example code without having to publish a new version to the world. (You don't have to worry about changing the version number each time.)
Testing
Run the tests with
npm run test
Publishing
Publishing is taken care of by the github action (.github/workflows/push.yml
) This runs the tests, builds a package, and releases it if there's any changes to the master branch that fit the semantic versioning (See here) - it also handles updating the version numbers.
Storybook
Storybook is included as a development dependency. To build the storybook site locally
npm run build-storybook
And to display it
npm run storybook
There's a github action (.github/workflows/publish-to-chromatic.yml
) that automates the process of publishing our storybook to chromatic, and running the UI tests.
2 days ago
3 months ago
4 months ago
10 months ago
10 months ago
10 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
9 months ago
10 months ago
8 months ago
8 months ago
6 months ago
7 months ago
8 months ago
8 months ago
11 months ago
1 year ago
1 year ago
12 months ago
12 months ago
12 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago