1.0.18 • Published 2 days ago

@linzjs/floating-windows v1.0.18

Weekly downloads
-
License
MIT
Repository
github
Last release
2 days ago

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;
};
MethodDescription
setWindowDefaults: (windowName: string, windowProperties: FloatingWindowProperties) => voidSet initial defaults for windows.
setWindowProperties: (windowName: string, windowProperties: FloatingWindowProperties) => voidSet window properties windowProperties for window windowName
getWindowProperties: (windowName: string) => FloatingWindowPropertiesReturn the current properties for window windowName
getOpenWindowNames: () => string[]Return a list of the open window names.
isWindowOpen: (windowName: string) => booleanReturns true if the window named windowName is open. False otherwise
isWindowPoppedOut: (windowName: string) => booleanReturns true if the window named windowName is popped out. False otherwise.
showWindow: (windowName: string, showPoppedOut?: boolean) => voidShow window named windowName, bringing it to the front. If showPoppedOut is true then the window is started popped out
hideWindow: (windowName: string) => voidHide window named windowName, closing it.
bringWindowToTop: (windowName: string) => voidBring the window named windowName to the front of the display
getTopWindowName: () => string \| nullReturn the name of the top-most window, or null if there is none.
getWindowPosition: (windowName: string) => FloatingWindowPositionReturn the position of the window named windowName
setWindowPosition: (windowName: string, position: FloatingWindowPosition) => voidSet the position of the window windowName to position
getPositionalStyle: (windowName: string) => CSS.PropertiesReturn a CSS style that includes the window position for window windowName
getWindowSize: (windowName: string) => FloatingWindowSizeReturn the size of the window named windowName
setWindowSize: (windowName: string, dimensions: FloatingWindowSize) => voidSet the size of the window called windowName to dimensions
setWindowBounds: (windowName: string, windowBounds: string \| undefined) => voidSet 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) => voidPop the window named windowName out into it's own window.
popWindowIn: (windowName: string) => voidPop the window named windowName back into the browser.
getZ: (windowName: string) => numberReturn the Z index of the window named windowName
addEventListener: ( listenerId: string, eventType: FloatingWindowEventType, windowName: string, eventCallback: FloatingWindowCallback) => voidSee below
removeEventListener: ( listenerId: string \| null, eventType: FloatingWindowEventType \| null, windowName: string \| null) => voidSee 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;
}
PropDescription
name: stringUnique (internal) name of this window
title?: stringThe displayed title in the window. No title bar is displayed if the title is not given.
children?: JSX.ElementThe window content.
header?: ReactNodeAdd extra header content. For compatibility.
extraHeaderButtons?: ReactNodeAny extra buttons in the header.
initialPosition: FloatingWindowPositionInitial position (X,Y coordinates.)
initialSize: FloatingWindowSizeInitial size (width and height.)
startPoppedOut?: booleanSet 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? booleanSet to true to start the window as displayed (shown.) False to start the window as hidden.
bounds?: stringDefine the bounds of the window - what limits the window has on movement. (See below)
standardButtons?: booleanSet to true to render the 'standard' buttons. Close/pop in/out.
headerProperties?: FloatingWindowHeaderPropsSet object used to customise header rendering
render?: FloatingWindowRenderCallbackFor future use
headerRender?: FloatingWindowRenderCallbackDefines 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):

  1. The property standardButtons. Setting this to false will prevent render of the standard close and pop in/out buttons.
  2. Pass properties in the headerProperties. This allows you to set a heading text and redefine the buttons.
  3. 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:

ComponentDescription
FloatingWindowPopinButtonThe pop in button
FloatingWindowPopoutButtonThe pop out button
FloatingWindowCloseButtonThe close button
FloatingWindowButtonDividerA vertical line dividing buttons
FloatingWindowHeaderA 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;
}
PropDescription
context: FloatingWindowContextTypeThe current window context
windowProps: FloatingWindowPropsProps passed to this window
isPoppedOut: booleantrue if popped out, false otherwise
standardButtons: JSX.Element[]An array of standard buttons for you to render if you want.
onClose: () => voidThe standard function called when the close button is clicked
onPopout: () => voidThe standard function called when the popout button is clicked
onPopin: () => voidThe 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,
}
EventDescription
showWindowWhen a window is shown
hideWindowWhen a window is hidden
popoutWindowWhen 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.

1.0.18

2 days ago

1.0.17

3 months ago

1.0.16

4 months ago

1.0.2

10 months ago

1.0.1

10 months ago

1.0.0

10 months ago

1.0.9

8 months ago

1.0.8

8 months ago

1.0.7

8 months ago

1.0.6

8 months ago

1.0.5

8 months ago

1.0.4

9 months ago

1.0.3

10 months ago

1.0.11

8 months ago

1.0.10

8 months ago

1.0.15

6 months ago

1.0.14

7 months ago

1.0.13

8 months ago

1.0.12

8 months ago

0.9.4

11 months ago

0.8.4

1 year ago

0.9.0

1 year ago

0.9.2

12 months ago

0.9.1

12 months ago

0.9.3

12 months ago

0.7.2

1 year ago

0.7.1

1 year ago

0.5.0

1 year ago

0.7.0

1 year ago

0.5.1

1 year ago

0.4.13

1 year ago

0.8.1

1 year ago

0.8.0

1 year ago

0.8.3

1 year ago

0.8.2

1 year ago

0.6.0

1 year ago

0.4.12

1 year ago

0.4.9

1 year ago

0.4.8

1 year ago

0.4.10

1 year ago

0.4.11

1 year ago

0.4.7

1 year ago

0.4.6

1 year ago

0.4.5

2 years ago

0.4.4

2 years ago

0.4.1

2 years ago

0.4.0

2 years ago

0.4.3

2 years ago

0.4.2

2 years ago

0.0.31

2 years ago

0.0.32

2 years ago

0.1.0

2 years ago

0.3.0

2 years ago

0.2.1

2 years ago

0.2.0

2 years ago

0.1.1

2 years ago

0.1.4

2 years ago

0.2.2

2 years ago

0.1.3

2 years ago

0.0.30

2 years ago

0.0.29

2 years ago

0.0.28

2 years ago

0.0.27

2 years ago

0.0.26

2 years ago

0.0.25

2 years ago

0.0.24

2 years ago

0.0.23

2 years ago

0.0.22

2 years ago

0.0.21

2 years ago

0.0.20

2 years ago

0.0.19

2 years ago

0.0.18

2 years ago