From 30afea9708210a0e9e0dcdece5aa73d098c72593 Mon Sep 17 00:00:00 2001 From: heswell Date: Fri, 13 Jan 2023 20:59:27 +0000 Subject: [PATCH 1/4] Typescript uplift layout package (#425) --- README.md | 2 +- vuu-ui/packages/vuu-layout/src/Component.css | 2 - .../vuu-layout/src/DraggableLayout.tsx | 9 +- .../vuu-layout/src/action-buttons/index.ts | 1 - .../src/chest-of-drawers/Drawer.tsx | 4 +- .../vuu-layout/src/drag-drop/BoxModel.ts | 23 +- .../vuu-layout/src/drag-drop/DragState.ts | 39 +- .../vuu-layout/src/drag-drop/Draggable.ts | 28 +- .../vuu-layout/src/drag-drop/DropTarget.ts | 17 +- .../src/drag-drop/DropTargetRenderer.tsx | 2 - .../vuu-layout/src/drag-drop/dragDropTypes.ts | 2 - .../vuu-layout/src/drag-drop/index.ts | 3 +- .../src/editable-label/EditableLabel.tsx | 2 +- .../vuu-layout/src/flexbox/FlexboxLayout.jsx | 26 - .../vuu-layout/src/flexbox/FlexboxLayout.tsx | 28 + .../vuu-layout/src/flexbox/FluidGrid.tsx | 2 - .../src/flexbox/FluidGridLayout.tsx | 3 +- .../vuu-layout/src/flexbox/Splitter.tsx | 10 +- .../vuu-layout/src/flexbox/flexbox-utils.ts | 10 +- .../vuu-layout/src/flexbox/flexboxTypes.ts | 17 +- .../packages/vuu-layout/src/flexbox/index.ts | 1 + .../src/flexbox/useResponsiveSizing.ts | 9 +- .../src/flexbox/useSplitterResizing.ts | 12 +- vuu-ui/packages/vuu-layout/src/index.ts | 9 +- .../packages/vuu-layout/src/layout-action.ts | 2 +- .../src/layout-header/ActionButton.tsx | 2 +- .../vuu-layout/src/layout-header/Header.tsx | 12 +- .../src/layout-provider/LayoutProvider.tsx | 35 +- .../vuu-layout/src/layout-provider/index.ts | 1 + .../src/layout-provider/useLayoutDragDrop.ts | 37 +- .../src/layout-reducer/flexUtils.ts | 63 +- .../vuu-layout/src/layout-reducer/index.ts | 3 +- .../layout-reducer/insert-layout-element.ts | 26 +- .../src/layout-reducer/layout-reducer.ts | 148 ++--- .../src/layout-reducer/layoutTypes.ts | 12 +- .../src/layout-reducer/layoutUtils.ts | 68 +- .../layout-reducer/remove-layout-element.ts | 36 +- .../layout-reducer/replace-layout-element.ts | 43 +- .../layout-reducer/resize-flex-children.ts | 13 +- .../src/layout-reducer/wrap-layout-element.ts | 44 +- .../vuu-layout/src/layout-view/View.tsx | 20 +- .../vuu-layout/src/layout-view/ViewContext.ts | 2 +- .../vuu-layout/src/layout-view/index.ts | 5 +- .../vuu-layout/src/layout-view/useView.tsx | 2 +- .../layout-view/useViewActionDispatcher.ts | 16 +- .../src/layout-view/useViewResize.ts | 2 +- .../vuu-layout/src/layout-view/viewTypes.ts | 10 +- .../vuu-layout/src/palette/Palette.css | 6 - .../vuu-layout/src/palette/Palette.tsx | 4 +- .../vuu-layout/src/palette/PaletteSalt.tsx | 2 +- .../packages/vuu-layout/src/palette/index.ts | 1 + .../src/placeholder/Placeholder.tsx | 3 +- .../src/registry/ComponentRegistry.ts | 4 +- .../src/responsive/OverflowMenu.css | 31 - .../src/responsive/OverflowMenu.jsx | 56 -- .../vuu-layout/src/responsive/index.ts | 1 - .../src/responsive/use-breakpoints.ts | 27 +- .../src/responsive/useOverflowObserver.ts | 606 ------------------ .../packages/vuu-layout/src/stack/Stack.css | 6 - .../packages/vuu-layout/src/stack/Stack.tsx | 27 +- .../vuu-layout/src/stack/StackLayout.tsx | 26 +- vuu-ui/packages/vuu-layout/src/stack/index.ts | 1 + .../packages/vuu-layout/src/tabs/TabPanel.tsx | 2 +- .../{ConfigWrapper.jsx => ConfigWrapper.tsx} | 28 +- .../config-wrapper/{index.js => index.ts} | 0 .../src/tools/{index.js => index.ts} | 1 + .../vuu-layout/src/use-persistent-state.ts | 13 +- .../src/utils/componentFromLayout.tsx | 4 +- vuu-ui/packages/vuu-layout/src/utils/index.ts | 7 +- .../vuu-layout/src/utils/pathUtils.ts | 59 +- .../vuu-layout/src/utils/propUtils.ts | 1 - .../vuu-layout/src/utils/styleUtils.ts | 13 +- .../packages/vuu-layout/src/utils/typeOf.ts | 9 +- .../vuu-layout/tsconfig-emit-types.json | 12 +- .../packages/vuu-popups/src/dialog/Dialog.tsx | 13 +- .../vuu-popups/src/portal/portal-utils.ts | 2 +- vuu-ui/scripts/build-all-type-defs.mjs | 2 +- vuu-ui/yarn.lock | 17 - 78 files changed, 472 insertions(+), 1375 deletions(-) delete mode 100644 vuu-ui/packages/vuu-layout/src/action-buttons/index.ts delete mode 100644 vuu-ui/packages/vuu-layout/src/flexbox/FlexboxLayout.jsx create mode 100644 vuu-ui/packages/vuu-layout/src/flexbox/FlexboxLayout.tsx delete mode 100644 vuu-ui/packages/vuu-layout/src/responsive/OverflowMenu.css delete mode 100644 vuu-ui/packages/vuu-layout/src/responsive/OverflowMenu.jsx delete mode 100644 vuu-ui/packages/vuu-layout/src/responsive/useOverflowObserver.ts rename vuu-ui/packages/vuu-layout/src/tools/config-wrapper/{ConfigWrapper.jsx => ConfigWrapper.tsx} (58%) rename vuu-ui/packages/vuu-layout/src/tools/config-wrapper/{index.js => index.ts} (100%) rename vuu-ui/packages/vuu-layout/src/tools/{index.js => index.ts} (99%) diff --git a/README.md b/README.md index ca7ae0df2..089ff7c70 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ The UI scripts all run from the vuu/vuu-ui directory. cd vuu-ui yarn yarn build -yarn buid:app +yarn build:app cd packages/electron #this should open an electron window pointing at https://localhost:8443/index.html yarn start diff --git a/vuu-ui/packages/vuu-layout/src/Component.css b/vuu-ui/packages/vuu-layout/src/Component.css index 7c91cb63a..e69de29bb 100644 --- a/vuu-ui/packages/vuu-layout/src/Component.css +++ b/vuu-ui/packages/vuu-layout/src/Component.css @@ -1,2 +0,0 @@ -.Component { -} diff --git a/vuu-ui/packages/vuu-layout/src/DraggableLayout.tsx b/vuu-ui/packages/vuu-layout/src/DraggableLayout.tsx index 6c74dee5c..a3225aa45 100644 --- a/vuu-ui/packages/vuu-layout/src/DraggableLayout.tsx +++ b/vuu-ui/packages/vuu-layout/src/DraggableLayout.tsx @@ -1,14 +1,11 @@ import classnames from 'classnames'; -import React, { useRef } from 'react'; +import { useRef } from 'react'; import { registerComponent } from './registry/ComponentRegistry'; import './DraggableLayout.css'; -// We need to add props to restrict drag behaviour to, for example, popups only -export const DraggableLayout = function DraggableLayout(props) { - // We shouldn't need this but somewhere the customDispatcher/handleDragStart callback is not - // being updated and preserves stale ref to props.children, so DragDrop from within a nested - // LatoutContext (Stack or DraggableLayout) fails. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const DraggableLayout = function DraggableLayout(props: any) { const sourceRef = useRef(); sourceRef.current = props; diff --git a/vuu-ui/packages/vuu-layout/src/action-buttons/index.ts b/vuu-ui/packages/vuu-layout/src/action-buttons/index.ts deleted file mode 100644 index b7793861b..000000000 --- a/vuu-ui/packages/vuu-layout/src/action-buttons/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './action-buttons'; diff --git a/vuu-ui/packages/vuu-layout/src/chest-of-drawers/Drawer.tsx b/vuu-ui/packages/vuu-layout/src/chest-of-drawers/Drawer.tsx index f34431b3b..4625b7783 100644 --- a/vuu-ui/packages/vuu-layout/src/chest-of-drawers/Drawer.tsx +++ b/vuu-ui/packages/vuu-layout/src/chest-of-drawers/Drawer.tsx @@ -1,6 +1,6 @@ -import React, { CSSProperties, HTMLAttributes, useCallback } from "react"; -import cx from "classnames"; import { Button, useControlled } from "@salt-ds/core"; +import cx from "classnames"; +import { CSSProperties, HTMLAttributes, useCallback } from "react"; import "./Drawer.css"; diff --git a/vuu-ui/packages/vuu-layout/src/drag-drop/BoxModel.ts b/vuu-ui/packages/vuu-layout/src/drag-drop/BoxModel.ts index f604bfc78..36aafcaeb 100644 --- a/vuu-ui/packages/vuu-layout/src/drag-drop/BoxModel.ts +++ b/vuu-ui/packages/vuu-layout/src/drag-drop/BoxModel.ts @@ -1,11 +1,11 @@ import { ReactElement } from "react"; -import { getProps, typeOf } from "../utils"; -import { isContainer } from "../registry/ComponentRegistry"; +import { rect } from "../common-types"; import { LayoutModel } from "../layout-reducer"; +import { isContainer } from "../registry/ComponentRegistry"; +import { getProps, typeOf } from "../utils"; import { DragDropRect, DropPos, RelativePosition } from "./dragDropTypes"; -import { rect } from "../common-types"; -export var positionValues = { +export const positionValues = { north: 1, east: 2, south: 4, @@ -20,7 +20,7 @@ export const RelativeDropPosition = { BEFORE: "before" as RelativePosition, }; -export var Position = Object.freeze({ +export const Position = Object.freeze({ North: _position("north"), East: _position("east"), South: _position("south"), @@ -58,7 +58,7 @@ function _position(str: keyof typeof positionValues) { }); } -var NORTH = Position.North, +const NORTH = Position.North, SOUTH = Position.South, EAST = Position.East, WEST = Position.West, @@ -77,7 +77,7 @@ export class BoxModel { model: ReactElement, dropTargetPaths: string[] = [] ): Measurements { - var measurements: Measurements = {}; + const measurements: Measurements = {}; measureRootComponent(model, measurements, dropTargetPaths); return measurements; } @@ -188,7 +188,6 @@ export function getPosition( } else { position = getPositionWithinBox(x, y, rect, pctX, pctY); } - return { position: position!, x, y, closeToTheEdge, tab }; } @@ -234,7 +233,7 @@ function getCenteredBox( function measureRootComponent( rootComponent: ReactElement, measurements: Measurements, - dropTargets: any[] + dropTargets: string[] ) { const { id, @@ -429,7 +428,7 @@ function measureComponentDomElement( } // Note: height and width are not required for dropTarget identification, but // are used in sizing calculations on drop - let { top, left, right, bottom, height, width } = el.getBoundingClientRect(); + const { top, left, right, bottom, height, width } = el.getBoundingClientRect(); let scrolling = undefined; if (isContainer(type)) { const scrollHeight = el.scrollHeight; @@ -467,7 +466,7 @@ function allBoxesContainingPoint( } = getProps(component); const type = typeOf(component) as string; - var rect = measurements[path]; + const rect = measurements[path]; if (!containsPoint(rect, x, y)) return boxes; if (dropTargets && dropTargets.length) { @@ -496,7 +495,7 @@ function allBoxesContainingPoint( scrollIntoViewIfNeccesary(rect, x, y); } - for (var i = 0; i < children.length; i++) { + for (let i = 0; i < children.length; i++) { if (type === "Stack" && component.props.active !== i) { continue; } diff --git a/vuu-ui/packages/vuu-layout/src/drag-drop/DragState.ts b/vuu-ui/packages/vuu-layout/src/drag-drop/DragState.ts index aed5d0966..bd64025e9 100644 --- a/vuu-ui/packages/vuu-layout/src/drag-drop/DragState.ts +++ b/vuu-ui/packages/vuu-layout/src/drag-drop/DragState.ts @@ -1,8 +1,7 @@ -import { rect } from '../common-types'; -import { Measurements, pointPositionWithinRect } from './BoxModel'; +import { pointPositionWithinRect } from './BoxModel'; import { DragDropRect } from './dragDropTypes'; -var SCALE_FACTOR = 0.4; +const SCALE_FACTOR = 0.4; export type IntrinsicSizes = { height?: number; @@ -59,31 +58,31 @@ export class DragState { rect: DragDropRect, intrinsicSize?: IntrinsicSizes ) { - var { left: x, top: y } = rect; + const { left: x, top: y } = rect; - var { pctX, pctY } = pointPositionWithinRect(mouseX, mouseY, rect); + const { pctX, pctY } = pointPositionWithinRect(mouseX, mouseY, rect); // We are applying a scale factor of 0.4 to the draggee. This is purely a visual // effect - the actual box size remains the original size. The 'leading' values // represent the difference between the visual scaled down box and the actual box. - var scaleFactor = SCALE_FACTOR; + const scaleFactor = SCALE_FACTOR; - var leadX = pctX * rect.width; - var trailX = rect.width - leadX; - var leadY = pctY * rect.height; - var trailY = rect.height - leadY; + const leadX = pctX * rect.width; + const trailX = rect.width - leadX; + const leadY = pctY * rect.height; + const trailY = rect.height - leadY; // When we assign position to rect using css. positioning units are applied to the // unscaled shape, so we have to adjust values to take scaling into account. - var scaledWidth = rect.width * scaleFactor, + const scaledWidth = rect.width * scaleFactor, scaledHeight = rect.height * scaleFactor; - var scaleDiff = 1 - scaleFactor; - var leadXScaleDiff = leadX * scaleDiff; - var leadYScaleDiff = leadY * scaleDiff; - var trailXScaleDiff = trailX * scaleDiff; - var trailYScaleDiff = trailY * scaleDiff; + const scaleDiff = 1 - scaleFactor; + const leadXScaleDiff = leadX * scaleDiff; + const leadYScaleDiff = leadY * scaleDiff; + const trailXScaleDiff = trailX * scaleDiff; + const trailYScaleDiff = trailY * scaleDiff; this.intrinsicSize = intrinsicSize; @@ -121,8 +120,6 @@ export class DragState { } }; - // onsole.log(`DragState: constraint ${JSON.stringify(this.constraint, null, 2)}`); - this.x = { pos: x, lo: false, @@ -165,12 +162,12 @@ export class DragState { */ //todo, diff can be calculated in here update(xy: 'x' | 'y', mousePos: number) { - var state = this[xy], + const state = this[xy], mouseConstraint = this.constraint.mouse[xy], posConstraint = this.constraint.pos[xy], previousPos = state.pos; - var diff = mousePos - state.mousePos; + const diff = mousePos - state.mousePos; //xy==='x' && console.log(`update: state.lo=${state.lo}, mPos=${mousePos}, mC.lo=${mouseConstraint.lo}, prevPos=${previousPos}, diff=${diff} ` ); @@ -210,7 +207,7 @@ export class DragState { } private dropXY(this: DragState, dir: 'x' | 'y') { - var pos = this[dir], + const pos = this[dir], rect = this.constraint.zone[dir]; // why not do the rounding +/- 1 on the rect initially - this is all it is usef for return pos.lo diff --git a/vuu-ui/packages/vuu-layout/src/drag-drop/Draggable.ts b/vuu-ui/packages/vuu-layout/src/drag-drop/Draggable.ts index 4d9f6f8a2..a771fefc5 100644 --- a/vuu-ui/packages/vuu-layout/src/drag-drop/Draggable.ts +++ b/vuu-ui/packages/vuu-layout/src/drag-drop/Draggable.ts @@ -27,10 +27,10 @@ let _dragEndCallback: DragEndCallback | null; let _dragStartX: number; let _dragStartY: number; -let _dragContainer: ReactElement | null; +let _dragContainer: ReactElement | undefined; let _dragState: DragState; let _dropTarget: DropTarget | null = null; -let _validDropTargetPaths: string[] | null; +let _validDropTargetPaths: string[] | undefined; let _dragInstructions: DragInstructions; let _measurements: Measurements; let _simpleDrag: boolean; @@ -116,12 +116,12 @@ export const Draggable = { }; function preDragMousemoveHandler(e: MouseEvent) { - var x = true; - var y = true; + const x = true; + const y = true; - let x_diff = x ? e.clientX - _dragStartX : 0; - let y_diff = y ? e.clientY - _dragStartY : 0; - let mouseMoveDistance = Math.max(Math.abs(x_diff), Math.abs(y_diff)); + const x_diff = x ? e.clientX - _dragStartX : 0; + const y_diff = y ? e.clientY - _dragStartY : 0; + const mouseMoveDistance = Math.max(Math.abs(x_diff), Math.abs(y_diff)); // when we do finally move the draggee, we are going to 'jump' by the amount of the drag threshold, should we // attempt to animate this ? @@ -216,7 +216,7 @@ function dragMousemoveHandler(evt: MouseEvent) { _dragMoveCallback?.(newX, newY); } - if (_simpleDrag) { + if (_simpleDrag || _dragContainer === undefined) { return; } @@ -224,16 +224,16 @@ function dragMousemoveHandler(evt: MouseEvent) { dropTarget = identifyDropTarget( x, y, - _dragContainer!, + _dragContainer, _measurements, dragState.hasIntrinsicSize(), - _validDropTargetPaths! + _validDropTargetPaths ); } else { dropTarget = identifyDropTarget( dragState.dropX(), dragState.dropY(), - _dragContainer!, + _dragContainer, _measurements ); } @@ -266,7 +266,7 @@ function onDragEnd() { _dropTarget = null; } else { _dragEndCallback?.({ - component: _dragContainer!, + component: _dragContainer, pos: { position: Position.Absolute } as any, }); } @@ -274,9 +274,9 @@ function onDragEnd() { _dragMoveCallback = null; _dragEndCallback = null; - _dragContainer = null; + _dragContainer = undefined; _dropTargetRenderer.clear(); - _validDropTargetPaths = null; + _validDropTargetPaths = undefined; window.removeEventListener("mousemove", dragMousemoveHandler, false); window.removeEventListener("mouseup", dragMouseupHandler, false); } diff --git a/vuu-ui/packages/vuu-layout/src/drag-drop/DropTarget.ts b/vuu-ui/packages/vuu-layout/src/drag-drop/DropTarget.ts index b4ff71b9a..e0734ce9e 100644 --- a/vuu-ui/packages/vuu-layout/src/drag-drop/DropTarget.ts +++ b/vuu-ui/packages/vuu-layout/src/drag-drop/DropTarget.ts @@ -1,16 +1,16 @@ +import { rect, rectTuple } from "../common-types"; +import { LayoutModel } from "../layout-reducer"; +import { getProps, typeOf } from "../utils"; import { BoxModel, - positionValues, getPosition, + Measurements, Position, + positionValues, RelativeDropPosition, - Measurements, } from "./BoxModel"; -import { getProps, typeOf } from "../utils"; import { DragDropRect, DropPos, DropPosTab } from "./dragDropTypes"; -import { LayoutModel } from "../layout-reducer"; import { DragState } from "./DragState"; -import { rect, rectTuple } from "../common-types"; export const isTabstrip = (dropTarget: DropTarget) => dropTarget.pos.tab && @@ -52,7 +52,7 @@ export interface TargetDropOutline { export class DropTarget { private active: boolean; - public box: any; + public box: unknown; public clientRect: DragDropRect; public component: LayoutModel; public dropRect: rectTuple | undefined; @@ -118,14 +118,14 @@ export class DropTarget { const b = Math.round(bottom - gap); const tabLeft = this.targetTabPos(tab); const tabWidth = 60; // should really measure text - const tabHeight = header!.bottom - header!.top; + const tabHeight = (header?.bottom ?? 0) - (header?.top ?? 0); return { l, t, r, b, tabLeft, tabWidth, tabHeight }; } getIntrinsicDropRect(dragState: DragState): TargetDropOutline { const { pos, clientRect: rect } = this; - let { x, y } = dragState; + const { x, y } = dragState; let height = dragState.intrinsicSize?.height ?? 0; let width = dragState.intrinsicSize?.height ?? 0; @@ -233,6 +233,7 @@ export class DropTarget { } toArray(this: DropTarget) { + // eslint-disable-next-line @typescript-eslint/no-this-alias let dropTarget: DropTarget | null = this; const dropTargets = [dropTarget]; // eslint-disable-next-line no-cond-assign diff --git a/vuu-ui/packages/vuu-layout/src/drag-drop/DropTargetRenderer.tsx b/vuu-ui/packages/vuu-layout/src/drag-drop/DropTargetRenderer.tsx index 691f8c9f0..d84ebc704 100644 --- a/vuu-ui/packages/vuu-layout/src/drag-drop/DropTargetRenderer.tsx +++ b/vuu-ui/packages/vuu-layout/src/drag-drop/DropTargetRenderer.tsx @@ -181,8 +181,6 @@ export default class DropTargetCanvas { top, component, }); - } else { - PopupService.movePopupTo(left, top); } } else { PopupService.hidePopup(); diff --git a/vuu-ui/packages/vuu-layout/src/drag-drop/dragDropTypes.ts b/vuu-ui/packages/vuu-layout/src/drag-drop/dragDropTypes.ts index 442454516..cba6fd2d3 100644 --- a/vuu-ui/packages/vuu-layout/src/drag-drop/dragDropTypes.ts +++ b/vuu-ui/packages/vuu-layout/src/drag-drop/dragDropTypes.ts @@ -1,6 +1,4 @@ -import type { ReactElement } from 'react'; import type { rect } from '../common-types'; -import { DropTarget } from './DropTarget'; export interface DragDropRect extends rect { children?: DragDropRect[]; header?: { diff --git a/vuu-ui/packages/vuu-layout/src/drag-drop/index.ts b/vuu-ui/packages/vuu-layout/src/drag-drop/index.ts index f9a816c49..fe7211cb3 100644 --- a/vuu-ui/packages/vuu-layout/src/drag-drop/index.ts +++ b/vuu-ui/packages/vuu-layout/src/drag-drop/index.ts @@ -1,4 +1,5 @@ +export * from "./dragDropTypes"; export * from "./Draggable"; export * from "./DropMenu"; export * from "./DropTarget"; -export * from "./dragDropTypes"; + diff --git a/vuu-ui/packages/vuu-layout/src/editable-label/EditableLabel.tsx b/vuu-ui/packages/vuu-layout/src/editable-label/EditableLabel.tsx index 91a546f49..724e10e8c 100644 --- a/vuu-ui/packages/vuu-layout/src/editable-label/EditableLabel.tsx +++ b/vuu-ui/packages/vuu-layout/src/editable-label/EditableLabel.tsx @@ -1,5 +1,5 @@ -import React, { ChangeEvent, FocusEvent, HTMLAttributes, KeyboardEvent, MouseEvent, useLayoutEffect, useRef, useState } from 'react'; import cx from 'classnames'; +import { ChangeEvent, FocusEvent, HTMLAttributes, KeyboardEvent, MouseEvent, useLayoutEffect, useRef, useState } from 'react'; import './EditableLabel.css'; diff --git a/vuu-ui/packages/vuu-layout/src/flexbox/FlexboxLayout.jsx b/vuu-ui/packages/vuu-layout/src/flexbox/FlexboxLayout.jsx deleted file mode 100644 index b8426dfda..000000000 --- a/vuu-ui/packages/vuu-layout/src/flexbox/FlexboxLayout.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { useCallback } from 'react'; -import Flexbox from './Flexbox'; -import { Action } from '../layout-action'; -import { registerComponent } from '../registry/ComponentRegistry'; -import { useLayoutProviderDispatch } from '../layout-provider'; - -export const FlexboxLayout = function FlexboxLayout(props) { - const { path } = props; - const dispatch = useLayoutProviderDispatch(); - - const handleSplitterMoved = useCallback( - (sizes) => { - dispatch({ - type: Action.SPLITTER_RESIZE, - path, - sizes - }); - }, - [dispatch, path] - ); - - return ; -}; -FlexboxLayout.displayName = 'Flexbox'; - -registerComponent('Flexbox', FlexboxLayout, 'container'); diff --git a/vuu-ui/packages/vuu-layout/src/flexbox/FlexboxLayout.tsx b/vuu-ui/packages/vuu-layout/src/flexbox/FlexboxLayout.tsx new file mode 100644 index 000000000..4a6731d5b --- /dev/null +++ b/vuu-ui/packages/vuu-layout/src/flexbox/FlexboxLayout.tsx @@ -0,0 +1,28 @@ +import { useCallback } from "react"; +import { Action } from "../layout-action"; +import { useLayoutProviderDispatch } from "../layout-provider"; +import { SplitterResizeAction } from "../layout-reducer"; +import { registerComponent } from "../registry/ComponentRegistry"; +import Flexbox from "./Flexbox"; +import { FlexboxLayoutProps } from "./flexboxTypes"; + +export const FlexboxLayout = function FlexboxLayout(props: FlexboxLayoutProps) { + const { path } = props; + const dispatch = useLayoutProviderDispatch(); + + const handleSplitterMoved = useCallback( + (sizes) => { + dispatch({ + type: Action.SPLITTER_RESIZE, + path, + sizes, + } as SplitterResizeAction); + }, + [dispatch, path] + ); + + return ; +}; +FlexboxLayout.displayName = "Flexbox"; + +registerComponent("Flexbox", FlexboxLayout, "container"); diff --git a/vuu-ui/packages/vuu-layout/src/flexbox/FluidGrid.tsx b/vuu-ui/packages/vuu-layout/src/flexbox/FluidGrid.tsx index 5b8966f8e..dc4dc3243 100644 --- a/vuu-ui/packages/vuu-layout/src/flexbox/FluidGrid.tsx +++ b/vuu-ui/packages/vuu-layout/src/flexbox/FluidGrid.tsx @@ -39,7 +39,6 @@ export const FluidGrid = forwardRef(function FluidGrid( const { cols, content, rootRef } = useResponsiveSizing({ children, cols: colsProp, - // onSplitterMoved, style: styleProp, }); @@ -61,7 +60,6 @@ export const FluidGrid = forwardRef(function FluidGrid( const style = { ...styleProp, "--spacing": spacing, - // only needed to display the cols "--grid-col-count": cols, "--grid-gap": gap, }; diff --git a/vuu-ui/packages/vuu-layout/src/flexbox/FluidGridLayout.tsx b/vuu-ui/packages/vuu-layout/src/flexbox/FluidGridLayout.tsx index 246a6b664..247d3200e 100644 --- a/vuu-ui/packages/vuu-layout/src/flexbox/FluidGridLayout.tsx +++ b/vuu-ui/packages/vuu-layout/src/flexbox/FluidGridLayout.tsx @@ -1,6 +1,5 @@ -import React from 'react'; -import {FluidGrid, FluidGridProps} from './FluidGrid'; import { registerComponent } from '../registry/ComponentRegistry'; +import { FluidGrid, FluidGridProps } from './FluidGrid'; export const FluidGridLayout = function FluidGridLayout(props: FluidGridProps) { return ; diff --git a/vuu-ui/packages/vuu-layout/src/flexbox/Splitter.tsx b/vuu-ui/packages/vuu-layout/src/flexbox/Splitter.tsx index 27edf9a1d..cd8a271ff 100644 --- a/vuu-ui/packages/vuu-layout/src/flexbox/Splitter.tsx +++ b/vuu-ui/packages/vuu-layout/src/flexbox/Splitter.tsx @@ -1,5 +1,5 @@ -import React, { HTMLAttributes, KeyboardEvent, useCallback, useRef, useState } from 'react'; import cx from 'classnames'; +import React, { HTMLAttributes, KeyboardEvent, useCallback, useRef, useState } from 'react'; import './Splitter.css'; @@ -32,7 +32,6 @@ export const Splitter = React.memo(function Splitter({ const handleKeyDownDrag = useCallback( ({ key, shiftKey }) => { - // TODO calc max distance const distance = shiftKey ? 10 : 1; if (column && key === 'ArrowDown') { onDrag(index, distance); @@ -69,7 +68,6 @@ export const Splitter = React.memo(function Splitter({ ignoreClick.current = true; const pos = e[column ? 'clientY' : 'clientX']; const diff = pos - lastPos.current; - // we seem to get a final value of zero if (pos && pos !== lastPos.current) { onDrag(index, diff); } @@ -98,10 +96,6 @@ export const Splitter = React.memo(function Splitter({ [column, handleMouseMove, handleMouseUp, index, onDragStart, setActive] ); - const handleFocus = () => { - // TODO - }; - const handleClick = () => { if (ignoreClick.current) { ignoreClick.current = false; @@ -111,7 +105,6 @@ export const Splitter = React.memo(function Splitter({ }; const handleBlur = () => { - // TODO keyDownHandlerRef.current = handleKeyDownInitDrag; }; @@ -125,7 +118,6 @@ export const Splitter = React.memo(function Splitter({ style={style} onBlur={handleBlur} onClick={handleClick} - onFocus={handleFocus} onKeyDown={handleKeyDown} onMouseDown={handleMouseDown} tabIndex={0}> diff --git a/vuu-ui/packages/vuu-layout/src/flexbox/flexbox-utils.ts b/vuu-ui/packages/vuu-layout/src/flexbox/flexbox-utils.ts index d7842c8cc..0d5fbd2d6 100644 --- a/vuu-ui/packages/vuu-layout/src/flexbox/flexbox-utils.ts +++ b/vuu-ui/packages/vuu-layout/src/flexbox/flexbox-utils.ts @@ -1,6 +1,6 @@ -import { getProp } from '../utils'; -import { getIntrinsicSize, hasUnboundedFlexStyle } from '../layout-reducer/flexUtils'; import { ReactElement } from 'react'; +import { getIntrinsicSize, hasUnboundedFlexStyle } from '../layout-reducer/flexUtils'; +import { getProp } from '../utils'; import type { BreakPoint, ContentMeta } from './flexboxTypes'; const NO_INTRINSIC_SIZE: { @@ -91,7 +91,7 @@ export const identifyResizeParties = (contentMeta: ContentMeta[], idx: number) = function identifyResizeBystanders(contentMeta: ContentMeta[], participants?: number[]) { if (participants) { - let bystanders = []; + const bystanders = []; for (let i = 0; i < contentMeta.length; i++) { if (contentMeta[i].flexOpen && !participants.includes(i)) { bystanders.push(i); @@ -113,8 +113,8 @@ function getLeadingResizeablePos(contentMeta: ContentMeta[], idx: number) { function getTrailingResizeablePos(contentMeta: ContentMeta[], idx: number) { let pos = idx, - resizeable = false, - count = contentMeta.length; + resizeable = false; + const count = contentMeta.length; while (pos < count && !resizeable) { pos = pos + 1; resizeable = isResizeable(contentMeta, pos); diff --git a/vuu-ui/packages/vuu-layout/src/flexbox/flexboxTypes.ts b/vuu-ui/packages/vuu-layout/src/flexbox/flexboxTypes.ts index 30aff6fae..41c5cbd76 100644 --- a/vuu-ui/packages/vuu-layout/src/flexbox/flexboxTypes.ts +++ b/vuu-ui/packages/vuu-layout/src/flexbox/flexboxTypes.ts @@ -4,15 +4,16 @@ import { MutableRefObject, ReactElement, ReactNode, - RefObject -} from 'react'; -import { SplitterProps } from './Splitter'; +} from "react"; +import { SplitterProps } from "./Splitter"; export interface LayoutContainerProps { resizeable?: boolean; } -export interface FlexboxProps extends LayoutContainerProps, HTMLAttributes { +export interface FlexboxProps + extends LayoutContainerProps, + HTMLAttributes { breakPoints?: BreakPointsProp; children: ReactElement[]; cols?: number; @@ -26,6 +27,10 @@ export interface FlexboxProps extends LayoutContainerProps, HTMLAttributes void; @@ -34,7 +39,7 @@ export interface SplitterHookProps { export interface SplitterHookResult { content: ReactElement[]; - rootRef: MutableRefObject; + rootRef: MutableRefObject; } export type SplitterFactory = (index: number) => ReactElement; @@ -56,7 +61,7 @@ export type FlexSize = { minSize: number; }; -export type BreakPoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; +export type BreakPoint = "xs" | "sm" | "md" | "lg" | "xl"; export type BreakPoints = BreakPoint[]; export type BreakPointsProp = { [keys in BreakPoint]?: number; diff --git a/vuu-ui/packages/vuu-layout/src/flexbox/index.ts b/vuu-ui/packages/vuu-layout/src/flexbox/index.ts index 343eab58f..8d8053711 100644 --- a/vuu-ui/packages/vuu-layout/src/flexbox/index.ts +++ b/vuu-ui/packages/vuu-layout/src/flexbox/index.ts @@ -2,3 +2,4 @@ export { default as Flexbox } from './Flexbox'; export * from './FlexboxLayout'; export * from './FluidGrid'; export * from './FluidGridLayout'; + diff --git a/vuu-ui/packages/vuu-layout/src/flexbox/useResponsiveSizing.ts b/vuu-ui/packages/vuu-layout/src/flexbox/useResponsiveSizing.ts index d0beb9d60..6e06ad757 100644 --- a/vuu-ui/packages/vuu-layout/src/flexbox/useResponsiveSizing.ts +++ b/vuu-ui/packages/vuu-layout/src/flexbox/useResponsiveSizing.ts @@ -1,3 +1,4 @@ +import { getUniqueId } from "@finos/vuu-utils"; import { cloneElement, CSSProperties, @@ -5,9 +6,8 @@ import { ReactElement, useCallback, useMemo, - useRef, + useRef } from "react"; -import { getUniqueId } from "@finos/vuu-utils"; import { gatherChildMeta } from "./flexbox-utils"; import { BreakPoint } from "./flexboxTypes"; @@ -52,11 +52,9 @@ export const useResponsiveSizing = ({ const { style: { flex, ...rest }, } = child.props; - // TODO do we always need to clone ? - // TODO emit the --col-span based on media query content.push( cloneElement(child, { - key: getUniqueId(), // need to store these + key: getUniqueId(), style: { ...rest, "--parent-col-count": cols, @@ -71,7 +69,6 @@ export const useResponsiveSizing = ({ ); useMemo(() => { - // console.log(`useMemo`, children) const [content, meta] = buildContent(children, dimension); metaRef.current = meta; contentRef.current = content; diff --git a/vuu-ui/packages/vuu-layout/src/flexbox/useSplitterResizing.ts b/vuu-ui/packages/vuu-layout/src/flexbox/useSplitterResizing.ts index b46eee3d1..4f4624eab 100644 --- a/vuu-ui/packages/vuu-layout/src/flexbox/useSplitterResizing.ts +++ b/vuu-ui/packages/vuu-layout/src/flexbox/useSplitterResizing.ts @@ -1,3 +1,4 @@ +import { getUniqueId } from "@finos/vuu-utils"; import React, { ReactElement, useCallback, @@ -5,9 +6,8 @@ import React, { useRef, useState, } from "react"; -import { getUniqueId } from "@finos/vuu-utils"; -import { Splitter } from "./Splitter"; import { Placeholder } from "../placeholder"; +import { Splitter } from "./Splitter"; import { findSplitterAndPlaceholderPositions, @@ -32,7 +32,7 @@ export const useSplitterResizing = ({ onSplitterMoved, style, }: SplitterHookProps): SplitterHookResult => { - const rootRef = useRef(); + const rootRef = useRef(null); const metaRef = useRef(); const contentRef = useRef(); const assignedKeys = useRef([]); @@ -130,7 +130,6 @@ export const useSplitterResizing = ({ ); useMemo(() => { - // This will always fire when Flexbox has rendered, but nor during splitter resize const [content, meta] = buildContent( children, dimension, @@ -161,7 +160,6 @@ function buildContent( for (let i = 0; i < children.length; i++) { const child = children[i]; if (i === 0 && splitterAndPlaceholderPositions[i] & PLACEHOLDER) { - //TODO need to assign an id to placeholder content.push(createPlaceholder(i)); meta.push({ placeholder: true, shim: true }); } @@ -197,7 +195,7 @@ function resizeContent( return content.map((child, idx) => { const meta = contentMeta[idx]; - let { currentSize, flexOpen, flexBasis } = meta; + const { currentSize, flexOpen, flexBasis } = meta; const hasCurrentSize = currentSize !== undefined; if (hasCurrentSize || flexOpen) { const { flexBasis: actualFlexBasis } = child.props.style || {}; @@ -230,7 +228,7 @@ function updateMeta(contentMeta: ContentMeta[], distance: number) { }); // we want the target being reduced first, this may limit the distance we can apply - let target1 = distance < 0 ? resizeTargets[0] : resizeTargets[1]; + const target1 = distance < 0 ? resizeTargets[0] : resizeTargets[1]; const { currentSize = 0, minSize = 0 } = contentMeta[target1]; if (currentSize === minSize) { diff --git a/vuu-ui/packages/vuu-layout/src/index.ts b/vuu-ui/packages/vuu-layout/src/index.ts index fba5b2a0f..c074d0b1a 100644 --- a/vuu-ui/packages/vuu-layout/src/index.ts +++ b/vuu-ui/packages/vuu-layout/src/index.ts @@ -1,13 +1,14 @@ +export * from "@finos/vuu-popups/src/dialog"; export * from "./chest-of-drawers"; -export { default as Component } from "./Component"; export * from "./common-types"; -export * from "@finos/vuu-popups/src/dialog"; -export * from "./DraggableLayout"; +export { default as Component } from "./Component"; export * from "./drag-drop"; +export * from "./DraggableLayout"; export * from "./flexbox"; export { Action } from "./layout-action"; export * from "./layout-header"; export * from "./layout-provider"; +export * from "./layout-view"; export * from "./palette"; export * from "./placeholder"; export * from "./registry"; @@ -16,4 +17,4 @@ export * from "./stack"; export * from "./tools"; export * from "./use-persistent-state"; export * from "./utils"; -export * from "./layout-view"; + diff --git a/vuu-ui/packages/vuu-layout/src/layout-action.ts b/vuu-ui/packages/vuu-layout/src/layout-action.ts index 3d2cc9ad3..6286c8591 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-action.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-action.ts @@ -18,4 +18,4 @@ export const Action = { SPLITTER_RESIZE: 'splitter-resize', SWITCH_TAB: 'switch-tab', TEAR_OUT: 'tear-out' -}; +}; \ No newline at end of file diff --git a/vuu-ui/packages/vuu-layout/src/layout-header/ActionButton.tsx b/vuu-ui/packages/vuu-layout/src/layout-header/ActionButton.tsx index 25169de43..bd1dc385d 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-header/ActionButton.tsx +++ b/vuu-ui/packages/vuu-layout/src/layout-header/ActionButton.tsx @@ -1,5 +1,5 @@ -import React, { HTMLAttributes, MouseEvent } from 'react'; import classnames from 'classnames'; +import { HTMLAttributes, MouseEvent } from 'react'; export interface ActionButtonProps extends Omit, 'onClick'> { actionId: 'maximize' | 'minimize' | 'restore' | 'tearout'; diff --git a/vuu-ui/packages/vuu-layout/src/layout-header/Header.tsx b/vuu-ui/packages/vuu-layout/src/layout-header/Header.tsx index 06a972660..0285570a9 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-header/Header.tsx +++ b/vuu-ui/packages/vuu-layout/src/layout-header/Header.tsx @@ -5,7 +5,7 @@ import React, { MouseEvent, ReactElement, useRef, - useState, + useState } from "react"; import { Contribution, useViewDispatch } from "../layout-view"; @@ -14,7 +14,7 @@ import { Toolbar, ToolbarButton, ToolbarField, - Tooltray, + Tooltray } from "@heswell/salt-lab"; import { CloseIcon } from "@salt-ds/icons"; @@ -34,12 +34,10 @@ export const Header = ({ className: classNameProp, contributions, collapsed, - expanded, closeable, onEditTitle, orientation: orientationProp = "horizontal", style, - tearOut, title = "Untitled", }: HeaderProps) => { const labelFieldRef = useRef(null); @@ -47,15 +45,11 @@ export const Header = ({ const [editing, setEditing] = useState(false); const viewDispatch = useViewDispatch(); - const handleAction = ( - evt: MouseEvent, - actionId: "maximize" | "restore" | "minimize" | "tearout" - ) => viewDispatch?.({ type: actionId }, evt); const handleClose = (evt: MouseEvent) => viewDispatch?.({ type: "remove" }, evt); const classBase = "vuuHeader"; - const handleTitleMouseDown = (e: MouseEvent) => { + const handleTitleMouseDown = () => { labelFieldRef.current?.focus(); }; diff --git a/vuu-ui/packages/vuu-layout/src/layout-provider/LayoutProvider.tsx b/vuu-ui/packages/vuu-layout/src/layout-provider/LayoutProvider.tsx index 46023560e..246117031 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-provider/LayoutProvider.tsx +++ b/vuu-ui/packages/vuu-layout/src/layout-provider/LayoutProvider.tsx @@ -1,11 +1,11 @@ -import React, { +import { MutableRefObject, ReactElement, useCallback, useContext, useEffect, useRef, - useState, + useState } from "react"; import { LayoutActionType, @@ -14,15 +14,16 @@ import { layoutReducer, LayoutReducerAction, layoutToJSON, - processLayoutElement, + processLayoutElement } from "../layout-reducer"; import { findTarget, getChildProp, getProps, typeOf } from "../utils"; import { LayoutProviderContext, - LayoutProviderDispatch, + LayoutProviderDispatch } from "./LayoutProviderContext"; import { useLayoutDragDrop } from "./useLayoutDragDrop"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any const withDropTarget = (props: any) => props.dropTarget; const shouldSave = (action: LayoutReducerAction) => [ @@ -51,7 +52,7 @@ export const LayoutProvider = (props: LayoutProviderProps): ReactElement => { const state = useRef(undefined); const childrenRef = useRef(children); - const [, forceRefresh] = useState(null); + const [, forceRefresh] = useState(null); const serializeState = useCallback( (source) => { @@ -85,19 +86,19 @@ export const LayoutProvider = (props: LayoutProviderProps): ReactElement => { const layoutActionDispatcher: LayoutProviderDispatch = useCallback( (action) => { - // onsole.log( - // `%cdispatchLayoutProviderAction ${action.type}`, - // "color: blue; font-weight: bold;" - // ); - - if (action.type === "drag-start") { - prepareToDragLayout(action); - } else if (action.type === "save") { - serializeState(state.current); - } else if (state.current) { - dispatchLayoutAction(action); + switch(action.type) { + case "drag-start": { + prepareToDragLayout(action); break + } + case "save": { + serializeState(state.current); break + } + default: { + dispatchLayoutAction(action); break + } } }, + // eslint-disable-next-line react-hooks/exhaustive-deps [dispatchLayoutAction, serializeState] ); @@ -109,7 +110,7 @@ export const LayoutProvider = (props: LayoutProviderProps): ReactElement => { useEffect(() => { if (layout) { const targetContainer = findTarget( - state.current as any, + state.current as never, withDropTarget ) as ReactElement; const target = getChildProp(targetContainer); diff --git a/vuu-ui/packages/vuu-layout/src/layout-provider/index.ts b/vuu-ui/packages/vuu-layout/src/layout-provider/index.ts index c1c5d1330..5fac89ec7 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-provider/index.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-provider/index.ts @@ -1,2 +1,3 @@ export * from './LayoutProvider'; export * from './LayoutProviderContext'; + diff --git a/vuu-ui/packages/vuu-layout/src/layout-provider/useLayoutDragDrop.ts b/vuu-ui/packages/vuu-layout/src/layout-provider/useLayoutDragDrop.ts index 196494b72..a14627881 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-provider/useLayoutDragDrop.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-provider/useLayoutDragDrop.ts @@ -3,7 +3,7 @@ import { DragDropRect, DragEndCallback, Draggable, - DragInstructions, + DragInstructions } from "../drag-drop"; import { DragStartAction } from "../layout-reducer"; import { getIntrinsicSize } from "../layout-reducer/flexUtils"; @@ -20,14 +20,12 @@ interface CurrentDragAction extends Omit { interface DragOperation { payload: ReactElement; originalCSS: string; - dragRect: any; + dragRect: unknown; dragInstructions: DragInstructions; dragOffsets: [number, number]; targetPosition: { left: number; top: number }; } -// Create a temporary object for dragging, where we don not have an existing object -// e.g dragging a non-selected tab from a Stack or an item from Palette const getDragElement = ( rect: DragDropRect, id: string, @@ -35,7 +33,6 @@ const getDragElement = ( ): [HTMLElement, string, number, number] => { const wrapper = document.createElement("div"); wrapper.className = "vuuSimpleDraggableWrapper"; - // TODO caller needs to supply the salt classes wrapper.classList.add( "vuuSimpleDraggableWrapper", "salt-theme", @@ -44,7 +41,6 @@ const getDragElement = ( wrapper.dataset.dragging = "true"; const div = dragElement ?? document.createElement("div"); - // this seems wrong the id is the payload id div.id = id; wrapper.appendChild(div); @@ -121,14 +117,8 @@ export const useLayoutDragDrop = ( dragOperationRef.current = undefined; draggableHTMLElementRef.current = undefined; } - }, []); + }, [dispatch]); - /** - * This will be called when Draggable has established that a drag operation is - * underway. There may be a delay between the initial mousedown and the call to - * this function - while we wait for either a drag timeout to fire or a minumum - * mouse move threshold to be reached. - */ const handleDragStart = useCallback( (evt: MouseEvent) => { if (dragActionRef.current) { @@ -137,11 +127,8 @@ export const useLayoutDragDrop = ( dragContainerPath, dragElement, dragRect, - // dropTargets, instructions = NO_INSTRUCTIONS, path, - // preDragActivity, - // resolveDragStart // see View drag } = dragActionRef.current; const { current: rootLayout } = rootLayoutRef; const dragPos = { x: evt.clientX, y: evt.clientY }; @@ -151,27 +138,19 @@ export const useLayoutDragDrop = ( let originalCSS = "", dragCSS = "", dragTransform = ""; - let dragInstructions = instructions; let dragStartLeft = -1; let dragStartTop = -1; let dragOffsets: [number, number] = NO_OFFSETS; - // TODO this has a bearing on offsets we apply to (absolutely positioned) dragged element. - // If we are creating the element here, offset parent will be document body. let element = document.getElementById(dragPayloadId); if (element === null) { - // This may bew the case where, for example, we drag a Tab (non selected) from a Tabstrip. [element, dragCSS, dragStartLeft, dragStartTop] = getDragElement( dragRect, dragPayloadId, dragElement ); - dragInstructions = { - ...dragInstructions, - RemoveDraggableOnDragEnd: true, - }; } else { dragOffsets = determineDragOffsets(element); const [offsetLeft, offsetTop] = dragOffsets; @@ -179,16 +158,7 @@ export const useLayoutDragDrop = ( dragStartLeft = left - offsetLeft; dragStartTop = top - offsetTop; dragCSS = `width:${width}px;height:${height}px;left:${dragStartLeft}px;top:${dragStartTop}px;z-index: 100;background-color:#ccc;opacity: 0.6;`; - // Important that this is set before we call initDrag - // this just enables position: absolute element.dataset.dragging = "true"; - - // resolveDragStart && resolveDragStart(true); - - // if (preDragActivity) { - // await preDragActivity(); - // } - originalCSS = element.style.cssText; } @@ -230,7 +200,6 @@ export const useLayoutDragDrop = ( dragActionRef.current = { ...options, dragContainerPath: "", - // dragContainerPath: '0.0.1.1' }; Draggable.handleMousedown(evt, handleDragStart, options.instructions); }, diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/flexUtils.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/flexUtils.ts index 49794e477..ea553c6ae 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/flexUtils.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/flexUtils.ts @@ -1,10 +1,9 @@ -import React from "react"; -import { CSSProperties, ReactElement, ReactNode } from "react"; import { uuid } from "@finos/vuu-utils"; -import { ComponentRegistry } from "../registry/ComponentRegistry"; -import { getProps, resetPath } from "../utils"; +import React, { CSSProperties, ReactElement, ReactNode } from "react"; import { dimension, rect, rectTuple } from "../common-types"; import { DropPos } from "../drag-drop/dragDropTypes"; +import { ComponentRegistry } from "../registry/ComponentRegistry"; +import { getProps, resetPath } from "../utils"; const placeHolderProps = { "data-placeholder": true, "data-resizeable": true }; const NO_STYLE = {}; @@ -47,7 +46,6 @@ export const getIntrinsicSize = ( ): { height?: number; width?: number } | undefined => { const { style: { width = auto, height = auto } = NO_STYLE } = component.props; - // Eliminate 'auto' and percentage sizes const numHeight = typeof height === "number"; const numWidth = typeof width === "number"; @@ -92,20 +90,18 @@ export function getFlexStyle( } } -//TODO this is not comprehensive export function hasUnboundedFlexStyle(component: ReactElement) { - const { style: { flex, flexGrow, flexShrink, flexBasis } = NO_STYLE } = - component.props; - // console.log(`flex ${flex}, flexBasis ${flexBasis}, flexShrink ${flexShrink}, flexGrow ${flexGrow}`) + const { style: { flex, flexGrow, flexShrink, flexBasis } = NO_STYLE } = component.props; if (typeof flex === "number") { return true; - } else if (flexBasis === 0 && flexGrow === 1 && flexShrink === 1) { + } + if (flexBasis === 0 && flexGrow === 1 && flexShrink === 1) { return true; - } else if (typeof flexBasis === "number") { + } + if (typeof flexBasis === "number") { return false; - } else { - return true; } + return true; } export function getFlexOrIntrinsicStyle( @@ -125,38 +121,34 @@ export function getFlexOrIntrinsicStyle( if (intrinsicSize !== auto) { if (isPercentageSize(intrinsicSize)) { return { - // Is this right? discrad the percenbtage size ? flexBasis: 0, flexGrow: 1, flexShrink: 1, [dimension]: undefined, [crossDimension]: intrinsicCrossSize, }; - } else { - return { - // or should we leave this as auto until user resizes ? - flexBasis: intrinsicSize, - flexGrow: 0, - flexShrink: 0, - [dimension]: intrinsicSize, - [crossDimension]: intrinsicCrossSize, - }; } - } else if (pos && pos[dimension]) { return { - ...intrinsicStyles, - ...defaultFlexStyle, - flexBasis: pos[dimension], + flexBasis: intrinsicSize, flexGrow: 0, flexShrink: 0, + [dimension]: intrinsicSize, + [crossDimension]: intrinsicCrossSize, }; - } else { + } + if (pos && pos[dimension]) { return { ...intrinsicStyles, - // ...defaultFlexStyle, - [crossDimension]: intrinsicCrossSize, + ...defaultFlexStyle, + flexBasis: pos[dimension], + flexGrow: 0, + flexShrink: 0, }; } + return { + ...intrinsicStyles, + [crossDimension]: intrinsicCrossSize, + }; } export function wrapIntrinsicSizeComponentWithFlexbox( @@ -187,7 +179,6 @@ export function wrapIntrinsicSizeComponentWithFlexbox( ); } } else { - // If we don't pass the rect values, we are wrapping an existing child, this is always a trailing placeholder endPlaceholder = true; } @@ -221,18 +212,16 @@ export function wrapIntrinsicSizeComponentWithFlexbox( ); } -const getFlexValue = (flexBasis: number, flexFill: boolean) => { +const getFlexValue = (flexBasis: number, flexFill: boolean): number | undefined => { if (flexFill) { return undefined; - } else if (flexBasis === 0) { - return 1; - } else { - return 0; } + return flexBasis === 0 ? 1 : 0 }; export function createFlexbox( flexDirection: flexDirection, + // eslint-disable-next-line @typescript-eslint/no-explicit-any props: any, children: ReactNode, path: string @@ -241,6 +230,7 @@ export function createFlexbox( const { flexFill, style, resizeable = true } = props; const { flexBasis = flexFill ? undefined : "auto" } = style; const flex = getFlexValue(flexBasis, flexFill); + // eslint-disable-next-line @typescript-eslint/no-explicit-any return React.createElement( ComponentRegistry.Flexbox, { @@ -267,6 +257,7 @@ export function createPlaceHolder( path: string, size: number, style?: CSSProperties, + // eslint-disable-next-line @typescript-eslint/no-explicit-any props?: any ) { const id = uuid(); diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/index.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/index.ts index a8eb919aa..28cf95f74 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/index.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/index.ts @@ -1,4 +1,5 @@ +export * from './flexUtils'; export * from './layout-reducer'; export * from './layoutTypes'; export * from './layoutUtils'; -export * from './flexUtils'; + diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/insert-layout-element.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/insert-layout-element.ts index 042f8ddca..792c76304 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/insert-layout-element.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/insert-layout-element.ts @@ -1,6 +1,9 @@ -import React, { ReactElement } from "react"; +/* eslint-disable @typescript-eslint/no-explicit-any */ import { uuid } from "@finos/vuu-utils"; -import { getManagedDimension, LayoutProps } from "./layoutUtils"; +import React, { ReactElement } from "react"; +import { rectTuple } from "../common-types"; +import { DropPos } from "../drag-drop"; +import { DropTarget } from "../drag-drop/DropTarget"; import { getProp, getProps, nextStep, resetPath, typeOf } from "../utils"; import { createPlaceHolder, @@ -8,12 +11,10 @@ import { getFlexDimensions, getFlexOrIntrinsicStyle, getIntrinsicSize, - wrapIntrinsicSizeComponentWithFlexbox, + wrapIntrinsicSizeComponentWithFlexbox } from "./flexUtils"; -import { LayoutModel, LayoutRoot } from "./layoutTypes"; -import { DropPos } from "../drag-drop"; -import { DropTarget } from "../drag-drop/DropTarget"; -import { rectTuple } from "../common-types"; +import { LayoutModel } from "./layoutTypes"; +import { getManagedDimension, LayoutProps } from "./layoutUtils"; type insertionPosition = "before" | "after"; @@ -39,6 +40,7 @@ export function insertIntoContainer( const existingComponentPath = getProp(targetContainer, "path"); const { idx, finalStep } = nextStep( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion containerPath!, existingComponentPath, true @@ -111,6 +113,7 @@ export function insertBesideChild( idx, newComponent, insertionPosition, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion pos!, clientRect, dropRect @@ -155,6 +158,7 @@ function updateChildren( newComponent, insertionPosition, clientRect, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion dropRect! ); } else { @@ -205,8 +209,6 @@ function insertIntrinsicSizedComponent( getIntrinsicSize(newComponent) as { height: number; width: number }; const path = getProp(containerChildren[idx], "path"); - // If we are introducing a new item into a row/column, but it is not flush against existing child, we will insert - // a leading placeholder ... const placeholderSize = getLeadingPlaceholderSize( flexDirection, insertionPosition, @@ -323,7 +325,8 @@ function getStyledComponents( newComponent: ReactElement, targetRect: DropTarget["clientRect"] ): [ReactElement, ReactElement] { - let { id = uuid(), version = 0 } = getProps(newComponent); + const id = uuid() + let { version = 0 } = getProps(newComponent); version += 1; if (typeOf(container) === "Flexbox") { const [dim] = getManagedDimension(container.props.style); @@ -354,9 +357,6 @@ function getStyledComponents( flex: undefined, }, } = getProps(newComponent); - // TODO why would we strip out width, height if resizeable - // we might need these if in a Stack, for example - // const dimensions = source.props.resizeable ? {} : { width, height }; return [ existingComponent, React.cloneElement(newComponent, { id, version, style }), diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/layout-reducer.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/layout-reducer.ts index d9b069a7f..1b9b2d79f 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/layout-reducer.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/layout-reducer.ts @@ -1,4 +1,6 @@ import React, { ReactElement } from "react"; +import { DropPos } from "../drag-drop/dragDropTypes"; +import { DropTarget } from "../drag-drop/DropTarget"; import { isContainer } from "../registry/ComponentRegistry"; import { findTarget, @@ -6,40 +8,28 @@ import { followPathToParent, getProp, getProps, - typeOf, + typeOf } from "../utils"; import { getIntrinsicSize } from "./flexUtils"; import { getInsertTabBeforeAfter, insertBesideChild, - insertIntoContainer, + insertIntoContainer } from "./insert-layout-element"; import { AddAction, - DragDropAction, - LayoutReducerAction, - LayoutActionType, - SetTitleAction, - SwitchTabAction, - MaximizeAction, + DragDropAction, LayoutActionType, LayoutReducerAction, MaximizeAction, SetTitleAction, + SwitchTabAction } from "./layoutTypes"; import { LayoutProps } from "./layoutUtils"; import { removeChild } from "./remove-layout-element"; import { replaceChild, swapChild, - _replaceChild, + _replaceChild } from "./replace-layout-element"; import { resizeFlexChildren } from "./resize-flex-children"; import { wrap } from "./wrap-layout-element"; -import { DropPos } from "../drag-drop/dragDropTypes"; -import { DropTarget } from "../drag-drop/DropTarget"; - -// const handlers: Handlers = { -// [Action.MAXIMIZE]: setChildProps, -// [Action.MINIMIZE]: setChildProps, -// [Action.RESTORE]: setChildProps, -// }; export const layoutReducer = ( state: ReactElement, @@ -63,45 +53,40 @@ export const layoutReducer = ( case LayoutActionType.SWITCH_TAB: return switchTab(state, action); default: - console.warn( - `layoutActionHandlers. No handler for action.type ${ - (action as any).type - }` - ); return state; } }; -function switchTab(state: ReactElement, { path, nextIdx }: SwitchTabAction) { - var target = followPath(state, path, true); +const switchTab = (state: ReactElement, { path, nextIdx }: SwitchTabAction) => { + const target = followPath(state, path, true); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const replacement = React.cloneElement(target, { active: nextIdx, }); return swapChild(state, target, replacement); } -function setTitle(state: ReactElement, { path, title }: SetTitleAction) { - var target = followPath(state, path, true); +const setTitle = (state: ReactElement, { path, title }: SetTitleAction) => { + const target = followPath(state, path, true); const replacement = React.cloneElement(target, { title, }); return swapChild(state, target, replacement); } -function setChildProps(state: ReactElement, { path, type }: MaximizeAction) { +const setChildProps = (state: ReactElement, { path, type }: MaximizeAction) => { if (path) { - // path will always be set here. Need to distinguisj ViewAction from LayoutAction - var target = followPath(state, path, true); + const target = followPath(state, path, true); return swapChild(state, target, target, type); } else { return state; } } -function dragDrop( +const dragDrop = ( layoutRoot: ReactElement, action: DragDropAction -): ReactElement { +): ReactElement => { console.log("drag drop"); const { draggedReactElement: newComponent, @@ -117,6 +102,7 @@ function dragDrop( let newLayoutRoot: ReactElement; if (destinationTabstrip) { const [targetTab, insertionPosition] = getInsertTabBeforeAfter( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion existingComponent!, pos ); @@ -148,24 +134,22 @@ function dragDrop( ); } - // return newLayoutRoot - if (dragInstructions.DoNotRemove) { return newLayoutRoot; - } else { - const finalTarget = findTarget( - newLayoutRoot, - (props: LayoutProps) => props.id === id && props.version === version - ) as ReactElement; - const finalPath = getProp(finalTarget, "path"); - return removeChild(newLayoutRoot, { path: finalPath, type: "remove" }); } + + const finalTarget = findTarget( + newLayoutRoot, + (props: LayoutProps) => props.id === id && props.version === version + ) as ReactElement; + const finalPath = getProp(finalTarget, "path"); + return removeChild(newLayoutRoot, { path: finalPath, type: "remove" }); } -function addChild( +const addChild = ( layoutRoot: ReactElement, { path: containerPath, component }: AddAction -) { +) => { return insertIntoContainer( layoutRoot, followPath(layoutRoot, containerPath) as ReactElement, @@ -173,11 +157,11 @@ function addChild( ); } -function dropLayoutIntoContainer( +const dropLayoutIntoContainer = ( layoutRoot: ReactElement, dropTarget: DropTarget, newComponent: ReactElement -): ReactElement { +): ReactElement => { const { component: existingComponent, pos, @@ -185,50 +169,48 @@ function dropLayoutIntoContainer( dropRect, } = dropTarget; const existingComponentPath = getProp(existingComponent, "path"); - // In a Draggable layout, 0.n is the top-level layout - if ( - /* existingComponent.path === '0.0' || */ existingComponentPath === "0.0" - ) { + + if (existingComponentPath === "0.0") { return wrap(layoutRoot, existingComponent, newComponent, pos); - } else { - var targetContainer = followPathToParent( + } + + const targetContainer = followPathToParent( + layoutRoot, + existingComponentPath + ) as ReactElement; + + if (withTheGrain(pos, targetContainer)) { + const insertionPosition = pos.position.SouthOrEast ? "after" : "before"; + return insertBesideChild( + layoutRoot, + existingComponent, + newComponent, + insertionPosition, + pos, + clientRect, + dropRect + ); + } + + if (!withTheGrain(pos, targetContainer)) { + return wrap( layoutRoot, - existingComponentPath - ) as ReactElement; + existingComponent, + newComponent, + pos, + clientRect, + dropRect + ); + } - if (withTheGrain(pos, targetContainer)) { - const insertionPosition = pos.position.SouthOrEast ? "after" : "before"; - return insertBesideChild( - layoutRoot, - existingComponent, - newComponent, - insertionPosition, - pos, - clientRect, - dropRect - ); - } else if (!withTheGrain(pos, targetContainer)) { - return wrap( - layoutRoot, - existingComponent, - newComponent, - pos, - clientRect, - dropRect - ); - } else if (isContainer(typeOf(targetContainer) as string)) { - return wrap(layoutRoot, existingComponent, newComponent, pos); - } else { - throw Error(`no support right now for position = ${pos.position}`); - } + if (isContainer(typeOf(targetContainer) as string)) { + return wrap(layoutRoot, existingComponent, newComponent, pos); } - return layoutRoot; + throw Error(`no support right now for position = ${pos.position}`); } -// Note: withTheGrain is not the negative of againstTheGrain - the difference lies in the -// handling of non-Flexible containers, the response for which is always false; -function withTheGrain(pos: DropPos, container: ReactElement) { +const withTheGrain = (pos: DropPos, container: ReactElement) => { if (pos.position.Centre) { return isTerrace(container) || isTower(container); } @@ -240,14 +222,14 @@ function withTheGrain(pos: DropPos, container: ReactElement) { : false; } -function isTower(container: ReactElement) { +const isTower = (container: ReactElement) => { return ( typeOf(container) === "Flexbox" && container.props.style.flexDirection === "column" ); } -function isTerrace(container: ReactElement) { +const isTerrace = (container: ReactElement) => { return ( typeOf(container) === "Flexbox" && container.props.style.flexDirection !== "column" diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutTypes.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutTypes.ts index ed3e14792..96663cc40 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutTypes.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutTypes.ts @@ -1,6 +1,7 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { ReactElement } from "react"; +import { DragDropRect, DragInstructions } from "../drag-drop"; import { DropTarget } from "../drag-drop/DropTarget"; -import { DragDropRect, DragInstructions, DropPos } from "../drag-drop"; export interface WithProps { props?: { [key: string]: any }; @@ -67,38 +68,46 @@ export type MaximizeAction = { path?: string; type: typeof LayoutActionType.MAXIMIZE; }; + export type MinimizeAction = { path?: string; type: typeof LayoutActionType.MINIMIZE; }; + export type RemoveAction = { path?: string; type: typeof LayoutActionType.REMOVE; }; + export type ReplaceAction = { replacement: any; target: any; type: typeof LayoutActionType.REPLACE; }; + export type RestoreAction = { path?: string; type: typeof LayoutActionType.RESTORE; }; + export type SetTitleAction = { path: string; title: string; type: typeof LayoutActionType.SET_TITLE; }; + export type SplitterResizeAction = { path: string; sizes: { currentSize: number; flexBasis: number }[]; type: typeof LayoutActionType.SPLITTER_RESIZE; }; + export type SwitchTabAction = { nextIdx: number; path: string; type: typeof LayoutActionType.SWITCH_TAB; }; + export type TearoutAction = { path?: string; type: typeof LayoutActionType.TEAROUT; @@ -137,7 +146,6 @@ export type MousedownViewAction = { type: "mousedown"; }; -// TODO split this out into separate actions for different drag scenarios export type DragStartAction = { payload?: ReactElement; dragContainerPath?: string; diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutUtils.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutUtils.ts index c92fcce0b..c8ece7871 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutUtils.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutUtils.ts @@ -1,16 +1,16 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { uuid } from "@finos/vuu-utils"; -import { CSSProperties, ReactElement } from "react"; -import React, { cloneElement } from "react"; +import React, { cloneElement, CSSProperties, ReactElement } from "react"; import { dimension } from "../common-types"; import { ComponentRegistry, isContainer, - isLayoutComponent, + isLayoutComponent } from "../registry/ComponentRegistry"; import { getPersistentState, hasPersistentState, - setPersistentState, + setPersistentState } from "../use-persistent-state"; import { expandFlex, getProps, typeOf } from "../utils"; import { LayoutJSON, LayoutModel, layoutType } from "./layoutTypes"; @@ -67,7 +67,6 @@ export const applyLayout = ( props: LayoutProps, previousLayout?: LayoutModel ): LayoutModel => { - // This works if the root layout is itself loaded from JSON const [layoutProps, children] = getChildLayoutProps( type, props, @@ -99,14 +98,11 @@ function getLayoutProps( } = getProps(previousLayout); const prevMatch = typeOf(previousLayout) === type && path === prevPath; - // TODO is there anything else we can re-use from previousType ? const id = prevMatch ? prevId : props.id ?? uuid(); const active = type === "Stack" ? props.active ?? prevActive : undefined; const key = id; - //TODO this might be wrong if client has updated style ? const style = prevMatch ? prevStyle : getStyle(type, props, parentType); - // TODO need two interfaces to cover these two scenarios return isLayoutComponent(type) ? { id, key, path, style, type, active } : { id, key, style, "data-path": path }; @@ -128,8 +124,6 @@ function getChildLayoutProps( ); if (props.layout && !previousLayout) { - // reconstitute children from layout. Will always be a single child, - // but return as array to make subsequent processing more consistent return [layoutProps, [layoutFromJson(props.layout, `${path}.0`)]]; } @@ -148,34 +142,28 @@ function getLayoutChildren( path = "0", previousChildren?: ReactElement[] ) { - // Avoid React.Children.map here, it messes with the keys. const kids = Array.isArray(children) ? children : React.isValidElement(children) ? [children] : []; - return isContainer(type) /*|| isView(type)*/ - ? kids.map((child, i) => { - const childType = typeOf(child) as string; - const previousType = typeOf(previousChildren?.[i]); - if (!previousType || childType === previousType) { - const [layoutProps, children] = getChildLayoutProps( - childType, - child.props, - `${path}.${i}`, - type, - previousChildren?.[i] - ); - return React.cloneElement(child, layoutProps, children); - } else { - //TODO is this always correct ? - return previousChildren?.[i]; - } - }) - : // TODO should we check the types of children ? - // : previousChildren ?? children; - //TODO this is new - is it dangerous ? - children; + return isContainer(type) ? kids.map((child, i) => { + const childType = typeOf(child) as string; + const previousType = typeOf(previousChildren?.[i]); + + if (!previousType || childType === previousType) { + const [layoutProps, children] = getChildLayoutProps( + childType, + child.props, + `${path}.${i}`, + type, + previousChildren?.[i] + ); + return React.cloneElement(child, layoutProps, children); + } + + return previousChildren?.[i]; + }) : children; } const getStyle = ( @@ -196,7 +184,7 @@ const getStyle = ( const { flex, ...otherStyles } = style; style = { ...otherStyles, - ...expandFlex(flex), + ...expandFlex(typeof flex === "number" ? flex : 0), }; } else if (parentType === "Stack") { style = { @@ -208,7 +196,6 @@ const getStyle = ( (style.width || style.height) && style.flexBasis === undefined ) { - // strictly, this should depend on flexDirection style = { ...style, flexBasis: "auto", @@ -220,15 +207,10 @@ const getStyle = ( return style; }; -//TODO we don't need id beyond view export function layoutFromJson( { id = uuid(), type, children, props, state }: LayoutJSON, path: string ): ReactElement { - // if (type === "DraggableLayout") { - // return layoutFromJson(children[0], "0"); - // } - const componentType = type.match(/^[a-z]/) ? type : ComponentRegistry[type]; if (componentType === undefined) { @@ -243,9 +225,7 @@ export function layoutFromJson( componentType, { ...props, - id, key: id, - path, }, children ? children.map((child, i) => layoutFromJson(child, `${path}.${i}`)) @@ -276,7 +256,7 @@ export function serializeProps(props?: LayoutProps) { if (props) { const { path, ...otherProps } = props; const result: { [key: string]: any } = {}; - for (let [key, value] of Object.entries(otherProps)) { + for (const [key, value] of Object.entries(otherProps)) { result[key] = serializeValue(value); } return result; @@ -294,7 +274,7 @@ function serializeValue(value: unknown): any { return value.map(serializeValue); } else if (typeof value === "object" && value !== null) { const result: { [key: string]: any } = {}; - for (let [k, v] of Object.entries(value)) { + for (const [k, v] of Object.entries(value)) { result[k] = serializeValue(v); } return result; diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/remove-layout-element.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/remove-layout-element.ts index 4d56c4a03..b305d3e65 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/remove-layout-element.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/remove-layout-element.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import React, { ReactElement } from 'react'; import { createPlaceHolder } from './flexUtils'; import { swapChild } from './replace-layout-element'; @@ -21,7 +22,6 @@ export function removeChild(layoutRoot: ReactElement, { path }: RemoveAction) { } const { children } = getProps(targetParent); if (children.length > 1 && allOtherChildrenArePlaceholders(children, path)) { - // eslint-disable-next-line no-unused-vars const { style: { flexBasis, display, flexDirection, ...style } } = getProps(targetParent); @@ -31,7 +31,6 @@ export function removeChild(layoutRoot: ReactElement, { path }: RemoveAction) { targetParent, createPlaceHolder(containerPath, flexBasis, style) ); - // eslint-disable-next-line no-cond-assign while ((targetParent = followPathToParent(newLayout, containerPath))) { if (getProp(targetParent, 'path') === '0') { break; @@ -39,7 +38,6 @@ export function removeChild(layoutRoot: ReactElement, { path }: RemoveAction) { const { children } = getProps(targetParent); if (allOtherChildrenArePlaceholders(children)) { containerPath = getProp(targetParent, 'path'); - // eslint-disable-next-line no-unused-vars const { style: { flexBasis, display, flexDirection, ...style } } = getProps(targetParent); @@ -50,29 +48,23 @@ export function removeChild(layoutRoot: ReactElement, { path }: RemoveAction) { ); } else if (hasAdjacentPlaceholders(children)) { newLayout = collapsePlaceholders(layoutRoot, targetParent as ReactElement); - // } else if (hasRedundantPlaceholders(children)){ - /* - We may have redundany placeholders for example where we have a tower containing a Terrace and a placeholder - If all the components bordering on the lower placeholder are themselves placeholders, the lower placeholder - is redundant - */ } else { break; } } return newLayout; - // return removeChild(rootProps, {path: targetParent.props.path}); - // return removeChildAndPlaceholder(rootProps, {path: targetParent.props.path}); - } else { - return _removeChild(layoutRoot, target); } + return _removeChild(layoutRoot, target); } function _removeChild(container: ReactElement, child: ReactElement): ReactElement { - let { active, children: componentChildren, path, preserve } = getProps(container); + const props = getProps(container) + const { children: componentChildren, path, preserve } = props + let { active } = props const { idx, finalStep } = nextStep(path, getProp(child, 'path')); const type = typeOf(container) as string; let children = componentChildren.slice() as ReactElement[]; + if (finalStep) { children.splice(idx, 1); if (active !== undefined && active >= idx) { @@ -83,7 +75,6 @@ function _removeChild(container: ReactElement, child: ReactElement): ReactElemen return unwrap(container, children[0]); } - // Not 100% sure we should do this, unless configured to if (!children.some(isFlexible) && children.some(canBeMadeFlexible)) { children = makeFlexible(children); } @@ -114,17 +105,12 @@ function unwrap(container: ReactElement, child: ReactElement) { } else if (type === 'Flexbox') { const dim = container.props.style.flexDirection === 'column' ? 'height' : 'width'; const { - // eslint-disable-next-line no-unused-vars style: { [dim]: size, ...style } } = unwrappedChild.props; - // Need to overwrite key unwrappedChild = React.cloneElement(unwrappedChild, { - // Need to assign key flexFill: undefined, style: { ...style, - // flexFill, if present described the childs relationship to the doomed flexbox, - // must not be applied to new parent flexGrow, flexShrink, flexBasis, @@ -136,16 +122,16 @@ function unwrap(container: ReactElement, child: ReactElement) { return unwrappedChild; } -function isFlexible(element: ReactElement) { +const isFlexible = (element: ReactElement) => { return element.props.style.flexGrow > 0; } -function canBeMadeFlexible(element: ReactElement) { +const canBeMadeFlexible = (element: ReactElement) => { const { width, height, flexGrow } = element.props.style; return flexGrow === 0 && typeof width !== 'number' && typeof height !== 'number'; } -function makeFlexible(children: ReactElement[]) { +const makeFlexible = (children: ReactElement[]) => { return children.map((child) => canBeMadeFlexible(child) ? React.cloneElement(child, { @@ -173,7 +159,7 @@ const hasAdjacentPlaceholders = (children: ReactElement[]) => { }; const collapsePlaceholders = (container: ReactElement, target: ReactElement) => { - let { children: componentChildren, path } = getProps(container); + const { children: componentChildren, path } = getProps(container); const { idx, finalStep } = nextStep(path, getProp(target, 'path')); let children = componentChildren.slice() as ReactElement[]; if (finalStep) { @@ -222,7 +208,7 @@ const _collapsePlaceHolders = (container: ReactElement) => { const mergePlaceholders = ([placeholder, ...placeholders]: ReactElement[]) => { const targetStyle = getProp(placeholder, 'style'); let { flexBasis, flexGrow, flexShrink } = targetStyle; - for (let { + for (const { props: { style } } of placeholders) { flexBasis += style.flexBasis; diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/replace-layout-element.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/replace-layout-element.ts index 0d1a59f76..0c04360e9 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/replace-layout-element.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/replace-layout-element.ts @@ -1,8 +1,8 @@ import React, { ReactElement } from 'react'; -import { getProp, getProps, nextStep } from '../utils'; import { Action } from '../layout-action'; -import { applyLayoutProps, LayoutProps } from './layoutUtils'; +import { getProp, getProps, nextStep } from '../utils'; import { ReplaceAction } from './layoutTypes'; +import { applyLayoutProps, LayoutProps } from './layoutUtils'; export function replaceChild(model: ReactElement, { target, replacement }: ReplaceAction) { return _replaceChild(model, target, replacement); @@ -17,9 +17,6 @@ export function _replaceChild( const resizeable = getProp(child, 'resizeable'); const { style } = getProps(child); const newChild = - // applyLayoutProps is a bit heavy here - it supports the scenario - // where we drop/replace a template. Might want to make it somehow - // an opt-in option applyLayoutProps( React.cloneElement(replacement, { resizeable, @@ -41,27 +38,27 @@ export function swapChild( op?: 'maximize' | 'minimize' | 'restore' ): ReactElement { if (model === child) { - return replacement as any; - } else { - const { idx, finalStep } = nextStep(getProp(model, 'path'), getProp(child, 'path')); - const children = model.props.children.slice(); - if (finalStep) { - if (!op) { - children[idx] = replacement; - } else if (op === Action.MINIMIZE) { - children[idx] = minimize(model, children[idx]); - } else if (op === Action.RESTORE) { - children[idx] = restore(children[idx]); - } - } else { - children[idx] = swapChild(children[idx], child, replacement, op); + return replacement; + } + + const { idx, finalStep } = nextStep(getProp(model, 'path'), getProp(child, 'path')); + const children = model.props.children.slice(); + + if (finalStep) { + if (!op) { + children[idx] = replacement; + } else if (op === Action.MINIMIZE) { + children[idx] = minimize(model, children[idx]); + } else if (op === Action.RESTORE) { + children[idx] = restore(children[idx]); } - return React.cloneElement(model, undefined, children); + } else { + children[idx] = swapChild(children[idx], child, replacement, op); } + return React.cloneElement(model, undefined, children); } function minimize(parent: ReactElement, child: ReactElement) { - // Right now, parent is always going to be a FLexbox, but might not always be the case const { style: parentStyle } = getProps(parent); const { style: childStyle } = getProps(child); @@ -94,13 +91,11 @@ function minimize(parent: ReactElement, child: ReactElement) { restoreStyle, style }); - } else { - return child; } + return child; } function restore(child: ReactElement) { - // Right now, parent is always going to be a FLexbox, but might not always be the case const { style: childStyle, restoreStyle } = getProps(child); const { flexBasis, flexShrink, flexGrow, ...rest } = childStyle; diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/resize-flex-children.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/resize-flex-children.ts index 5c01211c5..7b41bc7c7 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/resize-flex-children.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/resize-flex-children.ts @@ -1,8 +1,8 @@ import React, { CSSProperties, ReactElement } from 'react'; +import { dimension } from '../common-types'; import { followPath, getProps } from '../utils'; -import { swapChild } from './replace-layout-element'; import { SplitterResizeAction } from './layoutTypes'; -import { dimension } from '../common-types'; +import { swapChild } from './replace-layout-element'; export function resizeFlexChildren( layoutRoot: ReactElement, @@ -29,17 +29,16 @@ function applySizesToChildren( style: { [dimension]: size, flexBasis: actualFlexBasis } } = getProps(child); const meta = sizes[i]; - let { currentSize, flexBasis } = meta; + const { currentSize, flexBasis } = meta; const hasCurrentSize = currentSize !== undefined; const newSize = hasCurrentSize ? meta.currentSize : flexBasis; if (newSize === undefined || size === newSize || actualFlexBasis === newSize) { return child; - } else { - return React.cloneElement(child, { - style: applySizeToChild(child.props.style, dimension, newSize) - }); } + return React.cloneElement(child, { + style: applySizeToChild(child.props.style, dimension, newSize) + }); }); } diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/wrap-layout-element.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/wrap-layout-element.ts index b44b96095..f98d0aa03 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/wrap-layout-element.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/wrap-layout-element.ts @@ -1,20 +1,21 @@ -import React, { ReactElement } from "react"; +/* eslint-disable @typescript-eslint/no-explicit-any */ import { uuid } from "@finos/vuu-utils"; -import { getProp, getProps, nextStep, resetPath, typeOf } from "../utils"; +import React, { ReactElement } from "react"; +import { rectTuple } from "../common-types"; +import { DropPos } from "../drag-drop/dragDropTypes"; +import { DropTarget } from "../drag-drop/DropTarget"; import { ComponentRegistry } from "../registry/ComponentRegistry"; +import { getProp, getProps, nextStep, resetPath, typeOf } from "../utils"; import { createFlexbox, createPlaceHolder, flexDirection, getFlexStyle, getIntrinsicSize, - wrapIntrinsicSizeComponentWithFlexbox, + wrapIntrinsicSizeComponentWithFlexbox } from "./flexUtils"; -import { applyLayoutProps, LayoutProps } from "./layoutUtils"; import { LayoutModel } from "./layoutTypes"; -import { DropPos } from "../drag-drop/dragDropTypes"; -import { rectTuple } from "../common-types"; -import { DropTarget } from "../drag-drop/DropTarget"; +import { applyLayoutProps, LayoutProps } from "./layoutUtils"; export interface LayoutSpec { type: "Stack" | "Flexbox"; @@ -27,11 +28,6 @@ const isHtmlElement = (component: LayoutModel) => { return firstLetter === firstLetter.toLowerCase(); }; -// newComponent has been dropped onto an existingComponent. A wrapper container will be inserted -// into the layout tree, wrapping the existingComponent. newComponent will be injected into the -// new wrapper, so existingComponent and newComponent will be siblings. Putting it another way, -// wrapper will replace existingComponent in the layout tree and it will contain existingComponent -// and newComponent. export function wrap( container: ReactElement, existingComponent: any, @@ -96,15 +92,14 @@ function updateChildren( clientRect, dropRect ); - } else { - return wrapFlexComponent( - container, - containerChildren, - existingComponent, - newComponent, - pos - ); } + return wrapFlexComponent( + container, + containerChildren, + existingComponent, + newComponent, + pos + ); } function wrapFlexComponent( @@ -130,9 +125,8 @@ function wrapFlexComponent( pos ); const targetFirst = isTargetFirst(pos); - const active = targetFirst ? 1 : 0; // double check this + const active = targetFirst ? 1 : 0; - // TODO how do we decide whether children should be resizable ? const newComponentProps = { resizeable: true, style: newComponentStyle, @@ -157,7 +151,7 @@ function wrapFlexComponent( : undefined; const id = uuid(); - var wrapper = React.createElement( + const wrapper = React.createElement( ComponentRegistry[type], { active, @@ -165,7 +159,6 @@ function wrapFlexComponent( key: id, path: getProp(existingComponent, "path"), flexFill: getProp(existingComponent, "flexFill"), - // TODO we should be able to configure this in setDefaultLayoutProps ...splitterSize, ...showTabs, style, @@ -178,7 +171,6 @@ function wrapFlexComponent( `${existingComponentPath}.0`, existingComponentProps ), - // resetPath(newComponent, `${existingComponentPath}.1`, newComponentProps), applyLayoutProps( React.cloneElement(newComponent, newComponentProps), `${existingComponentPath}.1` @@ -189,7 +181,6 @@ function wrapFlexComponent( React.cloneElement(newComponent, newComponentProps), `${existingComponentPath}.0` ), - // resetPath(newComponent, `${existingComponentPath}.0`, newComponentProps), resetPath( existingComponent, `${existingComponentPath}.1`, @@ -271,7 +262,6 @@ function wrapIntrinsicSizedComponent( ); } -//TODO we need to respect styles on the source, full-on flex might not be appropriate function getWrappedFlexStyles( type: string, existingComponent: ReactElement, diff --git a/vuu-ui/packages/vuu-layout/src/layout-view/View.tsx b/vuu-ui/packages/vuu-layout/src/layout-view/View.tsx index 83e96d506..d7a844596 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-view/View.tsx +++ b/vuu-ui/packages/vuu-layout/src/layout-view/View.tsx @@ -3,10 +3,10 @@ import cx from "classnames"; import React, { ForwardedRef, forwardRef, useMemo, useRef } from "react"; import { Header } from "../layout-header/Header"; import { registerComponent } from "../registry/ComponentRegistry"; -import { ViewContext } from "./ViewContext"; -import { ViewProps } from "./viewTypes"; import { useView } from "./useView"; import { useViewResize } from "./useViewResize"; +import { ViewContext } from "./ViewContext"; +import { ViewProps } from "./viewTypes"; import "./View.css"; @@ -17,17 +17,17 @@ const View = forwardRef(function View( const { children, className, - collapsed, // "vertical" | "horizontal" | false | undefined + collapsed, closeable, "data-resizeable": dataResizeable, dropTargets, expanded, - flexFill, // use data-flexfill instead + flexFill, id: idProp, header, orientation = "horizontal", path, - resize = "responsive", // maybe throttle or debounce ? + resize = "responsive", resizeable = dataResizeable, tearOut, style = {}, @@ -35,7 +35,6 @@ const View = forwardRef(function View( ...restProps } = props; - // A View within a managed layout will always be passed an id const id = useId(idProp); const rootRef = useRef(null); const mainRef = useRef(null); @@ -65,14 +64,10 @@ const View = forwardRef(function View( const classBase = "vuuView"; const getContent = () => { - // We only inject restored state as props if child is a single element. Maybe we - // should take this further and only do it if the component has opted into this - // behaviour. if (React.isValidElement(children) && restoredState) { return React.cloneElement(children, restoredState); - } else { - return children; } + return children; }; const viewContextValue = useMemo( @@ -127,9 +122,8 @@ const View = forwardRef(function View( expanded={expanded} closeable={closeable} onEditTitle={onEditTitle} - orientation={/*collapsed || */ orientation} + orientation={orientation} tearOut={tearOut} - // title={`${title} v${version} #${id}`} title={title} /> ) : null} diff --git a/vuu-ui/packages/vuu-layout/src/layout-view/ViewContext.ts b/vuu-ui/packages/vuu-layout/src/layout-view/ViewContext.ts index 0186c604e..e5001dc3f 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-view/ViewContext.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-view/ViewContext.ts @@ -1,4 +1,4 @@ -import path from "path"; +/* eslint-disable @typescript-eslint/no-explicit-any */ import React, { SyntheticEvent, useContext } from "react"; import { ViewAction } from "./viewTypes"; diff --git a/vuu-ui/packages/vuu-layout/src/layout-view/index.ts b/vuu-ui/packages/vuu-layout/src/layout-view/index.ts index 41855f529..fbf7afca9 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-view/index.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-view/index.ts @@ -1,4 +1,5 @@ -export * from './ViewContext'; -export * from './View'; export * from './useViewActionDispatcher'; +export * from './View'; +export * from './ViewContext'; export * from './viewTypes'; + diff --git a/vuu-ui/packages/vuu-layout/src/layout-view/useView.tsx b/vuu-ui/packages/vuu-layout/src/layout-view/useView.tsx index ba0080445..81e0c9618 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-view/useView.tsx +++ b/vuu-ui/packages/vuu-layout/src/layout-view/useView.tsx @@ -61,7 +61,7 @@ export const useView = ({ purgeState(id, key); layoutDispatch({ type: "save" }); }, - [id, purgeState] + [id, layoutDispatch, purgeState] ); const save = useCallback( diff --git a/vuu-ui/packages/vuu-layout/src/layout-view/useViewActionDispatcher.ts b/vuu-ui/packages/vuu-layout/src/layout-view/useViewActionDispatcher.ts index 5c505652f..ff7e56d29 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-view/useViewActionDispatcher.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-view/useViewActionDispatcher.ts @@ -1,16 +1,16 @@ +import { DataSource } from "@finos/vuu-data"; import { ReactElement, RefObject, SyntheticEvent, useCallback, - useState, + useState } from "react"; import { useLayoutProviderDispatch } from "../layout-provider"; import { DragStartAction } from "../layout-reducer"; +import { usePersistentState } from "../use-persistent-state"; import { ViewDispatch } from "./ViewContext"; import { ViewAction } from "./viewTypes"; -import { usePersistentState } from "../use-persistent-state"; -import { DataSource } from "@finos/vuu-data"; export type Contribution = { index?: number; @@ -48,9 +48,6 @@ export const useViewActionDispatcher = ( }, [id, purgeSessionState]); const handleRemove = useCallback(() => { - // TODO this requires a bit more thought. I works BECAUSE filteredGrid has - // stored its datasource in sessionState. It is highly pretty much a - // requirement for features to do so - how do we enforce it. const ds = loadSessionState(id, "data-source") as DataSource; if (ds) { ds.unsubscribe(); @@ -72,7 +69,6 @@ export const useViewActionDispatcher = ( evt.stopPropagation(); const dragRect = root.current?.getBoundingClientRect(); return new Promise((resolve, reject) => { - // TODO should we check if we are allowed to drag ? dispatchLayoutAction({ type: "drag-start", evt, @@ -88,8 +84,6 @@ export const useViewActionDispatcher = ( [root, dispatchLayoutAction, viewPath, dropTargets] ); - // TODO should be event, action, then this method can bea assigned directly to a html element - // as an event hander const dispatchAction = useCallback( async ( action: A, @@ -100,7 +94,6 @@ export const useViewActionDispatcher = ( case "maximize": case "minimize": case "restore": - // case Action.TEAR_OUT: return dispatchLayoutAction({ type, path: action.path ?? viewPath }); case "remove": return handleRemove(); @@ -112,9 +105,6 @@ export const useViewActionDispatcher = ( case "remove-toolbar-contribution": return clearContributions(); default: { - // if (Object.values(Action).includes(type)) { - // dispatch(action); - // } return undefined; } } diff --git a/vuu-ui/packages/vuu-layout/src/layout-view/useViewResize.ts b/vuu-ui/packages/vuu-layout/src/layout-view/useViewResize.ts index ca48442cc..f2384777c 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-view/useViewResize.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-view/useViewResize.ts @@ -30,7 +30,7 @@ export const useViewResize = ({ mainRef.current.style.width = mainSize.current.width + "px"; } resizeHandle.current = undefined; - }, []); + }, [mainRef]); const onResize = useCallback( ({ height, width }) => { diff --git a/vuu-ui/packages/vuu-layout/src/layout-view/viewTypes.ts b/vuu-ui/packages/vuu-layout/src/layout-view/viewTypes.ts index 853aa5b19..cd81f19d6 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-view/viewTypes.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-view/viewTypes.ts @@ -1,14 +1,11 @@ import { HTMLAttributes } from "react"; import { HeaderProps } from "../layout-header"; import { - MaximizeAction, + AddToolbarContributionViewAction, MaximizeAction, MinimizeAction, MousedownViewAction, - RemoveAction, - RestoreAction, - TearoutAction, - AddToolbarContributionViewAction, - RemoveToolbarContributionViewAction, + RemoveAction, RemoveToolbarContributionViewAction, RestoreAction, + TearoutAction } from "../layout-reducer"; export type ViewAction = @@ -27,6 +24,7 @@ export interface ViewProps extends HTMLAttributes { "data-resizeable"?: boolean; dropTargets?: string[]; expanded?: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any flexFill?: any; header?: boolean | Partial; orientation?: "vertical" | "horizontal"; diff --git a/vuu-ui/packages/vuu-layout/src/palette/Palette.css b/vuu-ui/packages/vuu-layout/src/palette/Palette.css index a7356eaad..bfbeb4795 100644 --- a/vuu-ui/packages/vuu-layout/src/palette/Palette.css +++ b/vuu-ui/packages/vuu-layout/src/palette/Palette.css @@ -1,14 +1,8 @@ -.vuuPalette { -} - .vuuPalette-horizontal { align-items: center; display: flex; } -.vuuPalette .vuuComponentIcon { -} - .vuuPaletteItem { --vuu-icon-color: var(--salt-separable-primary-background); --vuu-icon-inset: calc(50% - 12px) auto auto -3px; diff --git a/vuu-ui/packages/vuu-layout/src/palette/Palette.tsx b/vuu-ui/packages/vuu-layout/src/palette/Palette.tsx index b48779e98..be71c037e 100644 --- a/vuu-ui/packages/vuu-layout/src/palette/Palette.tsx +++ b/vuu-ui/packages/vuu-layout/src/palette/Palette.tsx @@ -1,12 +1,12 @@ -import { List, ListItem, ListItemProps } from "@heswell/salt-lab"; import { uuid } from "@finos/vuu-utils"; +import { List, ListItem, ListItemProps } from "@heswell/salt-lab"; import cx from "classnames"; import { cloneElement, HTMLAttributes, memo, MouseEvent, - ReactElement, + ReactElement } from "react"; import { useLayoutProviderDispatch } from "../layout-provider"; import { View } from "../layout-view"; diff --git a/vuu-ui/packages/vuu-layout/src/palette/PaletteSalt.tsx b/vuu-ui/packages/vuu-layout/src/palette/PaletteSalt.tsx index cbf4b7497..8dd050d95 100644 --- a/vuu-ui/packages/vuu-layout/src/palette/PaletteSalt.tsx +++ b/vuu-ui/packages/vuu-layout/src/palette/PaletteSalt.tsx @@ -1,5 +1,5 @@ -import { List, ListItem, ListItemProps, ListProps } from "@heswell/salt-lab"; import { uuid } from "@finos/vuu-utils"; +import { List, ListItem, ListItemProps, ListProps } from "@heswell/salt-lab"; import cx from "classnames"; import { MouseEvent, ReactElement } from "react"; import { useLayoutProviderDispatch } from "../layout-provider"; diff --git a/vuu-ui/packages/vuu-layout/src/palette/index.ts b/vuu-ui/packages/vuu-layout/src/palette/index.ts index a0e35f97d..d4923f0dc 100644 --- a/vuu-ui/packages/vuu-layout/src/palette/index.ts +++ b/vuu-ui/packages/vuu-layout/src/palette/index.ts @@ -1,2 +1,3 @@ export * from "./Palette"; export * from "./PaletteSalt"; + diff --git a/vuu-ui/packages/vuu-layout/src/placeholder/Placeholder.tsx b/vuu-ui/packages/vuu-layout/src/placeholder/Placeholder.tsx index 41df38e35..6e2457fe4 100644 --- a/vuu-ui/packages/vuu-layout/src/placeholder/Placeholder.tsx +++ b/vuu-ui/packages/vuu-layout/src/placeholder/Placeholder.tsx @@ -1,5 +1,5 @@ -import React, { HTMLAttributes } from "react"; import cx from "classnames"; +import { HTMLAttributes } from "react"; import { registerComponent } from "../registry/ComponentRegistry"; import "./Placeholder.css"; @@ -30,7 +30,6 @@ export const Placeholder = ({ data-placeholder data-resizeable > - {/* */} ); }; diff --git a/vuu-ui/packages/vuu-layout/src/registry/ComponentRegistry.ts b/vuu-ui/packages/vuu-layout/src/registry/ComponentRegistry.ts index 9178907b1..624c5fcb3 100644 --- a/vuu-ui/packages/vuu-layout/src/registry/ComponentRegistry.ts +++ b/vuu-ui/packages/vuu-layout/src/registry/ComponentRegistry.ts @@ -1,4 +1,4 @@ -import React, { FunctionComponent } from 'react'; +import { FunctionComponent } from 'react'; const _containers: { [key: string]: boolean } = {}; const _views: { [key: string]: boolean } = {}; @@ -19,9 +19,9 @@ export const isLayoutComponent = (type: string) => isContainer(type) || isView(t export const isRegistered = (className: string) => !!ComponentRegistry[className]; -// We could check and set displayName in here export function registerComponent( componentName: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any component: FunctionComponent, type: layoutComponentType = 'component' ) { diff --git a/vuu-ui/packages/vuu-layout/src/responsive/OverflowMenu.css b/vuu-ui/packages/vuu-layout/src/responsive/OverflowMenu.css deleted file mode 100644 index 5d21c26f7..000000000 --- a/vuu-ui/packages/vuu-layout/src/responsive/OverflowMenu.css +++ /dev/null @@ -1,31 +0,0 @@ -.OverflowMenu { - /* width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - margin: 6px 0px 6px 0; - align-self: flex-end; - border: none; - border-radius: 0; - background-color: inherit; - padding: 0; */ -} - -/* .OverflowMenu-open { - backgroundcolor: active.overflow.background; -} */ - -/* .OverflowMenu-open .OverflowMenu-icon { - color: active.overflow.color; -} */ - -/* .Toolbar .Toolbar-overflow:hover { - background-color: lightgrey; -} */ - -[data-overflowed] { - order: 99; - /* visibility: hidden; */ -} diff --git a/vuu-ui/packages/vuu-layout/src/responsive/OverflowMenu.jsx b/vuu-ui/packages/vuu-layout/src/responsive/OverflowMenu.jsx deleted file mode 100644 index 1bd78fdf3..000000000 --- a/vuu-ui/packages/vuu-layout/src/responsive/OverflowMenu.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { forwardRef } from 'react'; -import { MoreSmallListVertButton } from '../action-buttons'; - -import './OverflowMenu.css'; - -const OverflowMenu = forwardRef(function OverflowMenu( - // eslint-disable-next-line no-unused-vars - { iconName = 'more', overflowOffsetLeft: left, source = [], ...rest }, - // eslint-disable-next-line no-unused-vars - ref -) { - return source.length > 0 ? ( - - ) : // - // {({ DropdownButtonProps, isOpen }) => { - // const { style, ...restButtonProps } = DropdownButtonProps; - - // const { - // onClick, - // onKeyDown, - // onFocus, - // onBlur, - // 'aria-expanded': menuOpen, // do we use this or isOpen ? - // } = DropdownButtonProps; - // const defaultProps = { - // 'data-jpmui-test': 'dropdown-button', - // 'aria-label': 'toggle overflow', - // 'aria-haspopup': true, - // className: cx('OverflowMenu-dropdown', { - // 'OverflowMenu-open': isOpen, - // }), - // onBlur, - // onPress: onClick, - // onFocus, - // onKeyDown, - // title: 'Overflow Menu', - // type: 'button', - // variant: 'secondary', - // }; - - // return ( - // - // ); - // }} - // - null; -}); - -export default OverflowMenu; diff --git a/vuu-ui/packages/vuu-layout/src/responsive/index.ts b/vuu-ui/packages/vuu-layout/src/responsive/index.ts index 1d89ce7cb..7b9449761 100644 --- a/vuu-ui/packages/vuu-layout/src/responsive/index.ts +++ b/vuu-ui/packages/vuu-layout/src/responsive/index.ts @@ -1,4 +1,3 @@ export * from "./use-breakpoints"; -export * from "./useOverflowObserver"; export * from "./useResizeObserver"; export * from "./utils"; diff --git a/vuu-ui/packages/vuu-layout/src/responsive/use-breakpoints.ts b/vuu-ui/packages/vuu-layout/src/responsive/use-breakpoints.ts index 444e6d724..b1d1e49ac 100644 --- a/vuu-ui/packages/vuu-layout/src/responsive/use-breakpoints.ts +++ b/vuu-ui/packages/vuu-layout/src/responsive/use-breakpoints.ts @@ -1,12 +1,11 @@ -import { RefObject, useCallback, useEffect, useRef, useState } from 'react'; -import { useResizeObserver } from './useResizeObserver'; +import { RefObject, useCallback, useEffect, useRef, useState } from "react"; +import { useResizeObserver } from "./useResizeObserver"; import { BreakPointRamp, breakpointRamp, - getBreakPoints as getDocumentBreakpoints -} from './breakpoints'; -import { BreakPoint, BreakPointsProp } from '../flexbox/flexboxTypes'; -import { ExecFileOptionsWithStringEncoding } from 'child_process'; + getBreakPoints as getDocumentBreakpoints, +} from "./breakpoints"; +import { BreakPoint, BreakPointsProp } from "../flexbox/flexboxTypes"; const EMPTY_ARRAY: BreakPoint[] = []; @@ -18,21 +17,23 @@ export interface BreakpointsHookProps { // TODO how do we cater for smallerThan/greaterThan breakpoints export const useBreakpoints = ( { breakPoints: breakPointsProp, smallerThan }: BreakpointsHookProps, - ref: RefObject + ref: RefObject ) => { - const [breakpointMatch, setBreakpointmatch] = useState(smallerThan ? false : 'lg'); + const [breakpointMatch, setBreakpointmatch] = useState( + smallerThan ? false : "lg" + ); const bodyRef = useRef(document.body); - const breakPointsRef = useRef( + const breakPointsRef = useRef( breakPointsProp ? breakpointRamp(breakPointsProp) : getDocumentBreakpoints() ); // TODO how do we identify the default - const sizeRef = useRef('lg'); + const sizeRef = useRef("lg"); const stopFromMinWidth = useCallback( (w) => { if (breakPointsRef.current) { - for (let [name, size] of breakPointsRef.current) { + for (const [name, size] of breakPointsRef.current) { if (w >= size) { return name; } @@ -64,8 +65,8 @@ export const useBreakpoints = ( // TODO need to make the dimension a config useResizeObserver( ref || bodyRef, - breakPointsRef.current ? ['width'] : EMPTY_ARRAY, - ({ width: measuredWidth }: { width: number }) => { + breakPointsRef.current ? ["width"] : EMPTY_ARRAY, + ({ width: measuredWidth }: { width?: number }) => { const result = matchSizeAgainstBreakpoints(measuredWidth); if (result !== sizeRef.current) { sizeRef.current = result; diff --git a/vuu-ui/packages/vuu-layout/src/responsive/useOverflowObserver.ts b/vuu-ui/packages/vuu-layout/src/responsive/useOverflowObserver.ts deleted file mode 100644 index 887e68c59..000000000 --- a/vuu-ui/packages/vuu-layout/src/responsive/useOverflowObserver.ts +++ /dev/null @@ -1,606 +0,0 @@ -import { useCallback, useLayoutEffect, useRef, useState } from "react"; -import { useResizeObserver } from "./useResizeObserver"; -import { measureMinimumNodeSize } from "./measureMinimumNodeSize"; - -const MONITORED_DIMENSIONS = { - horizontal: ["width", "scrollHeight"], - vertical: ["height", "scrollWidth"], - none: [], -}; -const NO_OVERFLOW_INDICATOR = {}; -const NO_DATA = {}; - -const UNCOLLAPSED_DYNAMIC_ITEMS = - '[data-collapsible="dynamic"]:not([data-collapsed="true"]):not([data-collapsing="true"])'; - -const addAll = (sum: number, m: any) => sum + m.size; -const addAllExceptOverflowIndicator = (sum: number, m: any) => - sum + (m.isOverflowIndicator ? 0 : m.size); - -// There should be no collapsible items here that are not already collapsed -// otherwise we would be collapsing, not overflowing -const lastOverflowableItem = (arr) => { - for (let i = arr.length - 1; i >= 0; i--) { - const item = arr[i]; - // TODO should we support a no-overflow attribute (maybe a priority 0) - // to prevent an item from overflowing ? - // TODO when all collapsible items are collapsed and we start overflowing, - // should we leave collapsed items to last in the overflow priority ? - if (!item.isOverflowIndicator) { - return item; - } - } - return null; -}; -const OVERFLOWING = 1000; -const collapsedOnly = (status) => status > 0 && status < 1000; -const includesOverflow = (status) => status >= OVERFLOWING; -const lastListItem = (listRef) => listRef.current[listRef.current.length - 1]; - -const newlyCollapsed = (visibleItems) => - visibleItems.some((item) => item.collapsed && item.fullWidth === null); - -const hasUncollapsedDynamicItems = (containerRef) => - containerRef.current.querySelector(UNCOLLAPSED_DYNAMIC_ITEMS) !== null; - -const moveOverflowItem = (fromStack, toStack) => { - const item = lastOverflowableItem(fromStack.current); - if (item) { - fromStack.current = fromStack.current.filter((i) => i !== item); - toStack.current = toStack.current.concat(item); - return item; - } else { - return null; - } -}; - -const byDescendingPriority = (m1, m2) => { - let result = m1.priority - m2.priority; - if (result === 0) { - result = m1.index - m2.index; - } - return result; -}; - -const getOverflowIndicator = (visibleRef) => - visibleRef.current.find((item) => item.isOverflowIndicator); - -const Dimensions = { - horizontal: { - size: "clientWidth", - depth: "clientHeight", - scrollDepth: "scrollHeight", - }, - vertical: { - size: "clientHeight", - depth: "clientWidth", - scrollDepth: "scrollWidth", - }, -}; - -const measureContainerOverflow = ( - { current: innerEl }, - orientation = "horizontal" -) => { - const dim = Dimensions[orientation]; - const { [dim.depth]: containerDepth } = innerEl.parentNode; - const { [dim.scrollDepth]: scrollDepth, [dim.size]: contentSize } = innerEl; - const isOverflowing = containerDepth < scrollDepth; - return [isOverflowing, contentSize, containerDepth]; -}; - -const useOverflowStatus = () => { - const [, forceUpdate] = useState(null); - // TODO make this easier to understand by storing the overflow and - // collapse status as separate reference count fields - const [overflowing, _setOverflowing] = useState(0); - const overflowingRef = useRef(0); - const setOverflowing = useCallback( - (value) => { - _setOverflowing((overflowingRef.current = value)); - }, - [_setOverflowing] - ); - - const updateOverflowStatus = useCallback( - (value, force) => { - if (Math.abs(value) === OVERFLOWING) { - if (value > 0 && !includesOverflow(overflowingRef.current)) { - setOverflowing(overflowingRef.current + value); - } else if (value < 0 && includesOverflow(overflowingRef.current)) { - setOverflowing(overflowingRef.current + value); - } else { - forceUpdate({}); - } - } else if (value !== 0) { - setOverflowing(overflowingRef.current + value); - } else if (force) { - forceUpdate({}); - } - }, - [forceUpdate, overflowingRef, setOverflowing] - ); - - return [overflowingRef, overflowing, updateOverflowStatus]; -}; - -const measureChildNodes = ({ current: innerEl }, dimension) => { - const measurements = Array.from(innerEl.childNodes).reduce( - (list, node: Node) => { - const { - collapsible, - collapsed, - collapsing, - index, - priority = "1", - overflowIndicator, - overflowed, - } = node?.dataset ?? NO_DATA; - if (index) { - const size = measureMinimumNodeSize(node, dimension); - if (overflowed) { - delete node.dataset.overflowed; - } - list.push({ - collapsible, - collapsed: collapsible ? collapsed === "true" : undefined, - collapsing, - // only to be populated in case of collapse - // TODO check the role of this - especially the way we check it in useEffect - // to detect collapse - fullSize: null, - index: parseInt(index, 10), - isOverflowIndicator: overflowIndicator, - label: node.title || node.innerText, - priority: parseInt(priority, 10), - size, - }); - } - return list; - }, - [] - ); - - return measurements.sort(byDescendingPriority); -}; - -const getElementForItem = (ref, item) => - ref.current.querySelector(`:scope > [data-idx='${item.index}']`); - -// value could be anything which might require a re-evaluation. In the case of tabs -// we might have selected an overflowed tab. Can we make this more efficient, only -// needs action if an overflowed item re-enters the visible section -export function useOverflowObserver(orientation = "horizontal", label = "") { - const ref = useRef(null); - const [overflowingRef, overflowing, updateOverflowStatus] = - useOverflowStatus(); - // const [, forceUpdate] = useState(); - const visibleRef = useRef([]); - const overflowedRef = useRef([]); - const collapsedRef = useRef([]); - const collapsingRef = useRef(false); - const rootDepthRef = useRef(null); - const containerSizeRef = useRef(null); - const horizontalRef = useRef(orientation === "horizontal"); - const overflowIndicatorSizeRef = useRef(36); // should default by density - const minSizeRef = useRef(0); - - const setContainerMinSize = useCallback( - (size) => { - const isHorizontal = horizontalRef.current; - if (size === undefined) { - const dimension = isHorizontal ? "width" : "height"; - ({ [dimension]: size } = ref.current.getBoundingClientRect()); - } - minSizeRef.current = size; - const styleDimension = isHorizontal ? "minWidth" : "minHeight"; - ref.current.style[styleDimension] = size + "px"; - }, - [ref] - ); - - const markOverflowingItems = useCallback( - (visibleContentSize, containerSize) => { - let result = 0; - // First pass, see if there is a collapsible item we can collapse. We won't - // know how much space this frees up until the thing has re-rendered, so this - // may kick off a chain of renders and remeasures if there are multiple collapsible - // items and each yields only a part of the shrinkage we need to apply. - // That's the worst case scenario. - if ( - visibleRef.current.some((item) => item.collapsible && !item.collapsed) - ) { - for (let i = visibleRef.current.length - 1; i >= 0; i--) { - const item = visibleRef.current[i]; - if (item.collapsible === "instant" && !item.collapsed) { - item.collapsed = true; - const target = getElementForItem(ref, item); - target.dataset.collapsed = true; - collapsedRef.current.push(item); - // We only ever collapse 1 item at a time. We now need to wait for - // it to render, so we can re-measure and determine how much space - // this has saved. - return 1; - } else if ( - item.collapsible === "dynamic" && - !item.collapsed && - !item.collapsing - ) { - item.collapsing = true; - const target = getElementForItem(ref, item); - target.dataset.collapsing = true; - collapsedRef.current.push(item); - ref.current.dataset.collapsing = true; - // We only ever collapse 1 item at a time. We now need to wait for - // it to render, so we can re-measure and determine how much space - // this has saved. - return 1; - } - } - } - - // If no collapsible items, movin items from visible to overflowed queues - while (visibleContentSize > containerSize) { - const overflowedItem = moveOverflowItem(visibleRef, overflowedRef); - if (overflowedItem === null) { - // unable to overflow, all items are collapsed, this is our minimum width, - // enforce it ... - // TODO what if density changes - //TODO probably not right, now we overflow even collapsed items, min width should be - // overflow indicator width plus width of any non-overflowable items - setContainerMinSize(visibleContentSize); - break; - } - visibleContentSize -= overflowedItem.size; - const target = getElementForItem(ref, overflowedItem); - target.dataset.overflowed = true; - result = OVERFLOWING; - } - return result; - }, - [setContainerMinSize] - ); - - const removeOverflowIfSpaceAllows = useCallback( - (containerSize) => { - let result = 0; - // TODO calculate this without using fullWidth if we have OVERFLOW - // Need a loop here where we first remove OVERFLOW, then potentially remove - // COLLAPSE too - // We want to re-introduce overflowed items before we start to restore collapsed items - // When we are dealing with overflowed items, we just use the current width of collapsed items. - let visibleContentSize = visibleRef.current.reduce( - addAllExceptOverflowIndicator, - 0 - ); - let diff = containerSize - visibleContentSize; - - if (collapsedOnly(overflowingRef.current)) { - // find the next collapsed item, see how much extra space it would - // occupy if restored. If we have enough space, restore it. - while (collapsedRef.current.length) { - const item = lastListItem(collapsedRef); - const itemDiff = item.fullSize - item.size; - if (diff >= itemDiff) { - item.collapsed = false; - item.size = item.fullSize; - // Be careful before setting this to null, check the code in useEffect - delete item.fullSize; - const target = getElementForItem(ref, item); - collapsedRef.current.pop(); - delete target.dataset.collapsed; - diff = diff - itemDiff; - result += 1; - } else { - break; - } - } - return result; - } else { - while (overflowedRef.current.length > 0) { - const { size: nextSize } = lastListItem(overflowedRef); - - if (diff >= nextSize) { - const { size: overflowSize = 0 } = - getOverflowIndicator(visibleRef) || NO_OVERFLOW_INDICATOR; - // we can only ignore the width of overflow Indicator if either there is only one remaining - // overflow item (so overflowIndicator will be removed) or diff is big enough to accommodate - // the overflow Ind. - if ( - overflowedRef.current.length === 1 || - diff >= nextSize + overflowSize - ) { - const overflowedItem = moveOverflowItem( - overflowedRef, - visibleRef - ); - visibleContentSize += overflowedItem.size; - const target = getElementForItem(ref, overflowedItem); - delete target.dataset.overflowed; - diff = diff - overflowedItem.size; - result = OVERFLOWING; - } else { - break; - } - } else { - break; - } - } - } - // DOn't return OVERFLOWING unless there is no more overflow - return result; - }, - [overflowingRef] - ); - - const initializeDynamicContent = useCallback(() => { - let renderedSize = visibleRef.current.reduce(addAll, 0); - let diff = renderedSize - containerSizeRef.current; - for (let i = visibleRef.current.length - 1; i >= 0; i--) { - const item = visibleRef.current[i]; - if (item.collapsible && !item.collapsed) { - const target = getElementForItem(ref, item); - // TODO where do we derive min width 28 + 8 - if (diff > item.size - 36) { - // We really want to know if it has reached min-width, but we will have to - // wait for it to render - target.dataset.collapsed = item.collapsed = true; - diff -= item.size; - } else { - target.dataset.collapsing = item.collapsing = true; - break; - } - } - } - }, [containerSizeRef, ref, visibleRef]); - - const collapseCollapsingItem = useCallback( - (item, target) => { - target.dataset.collapsing = item.collapsing = false; - target.dataset.collapsed = item.collapsed = true; - - const rest = visibleRef.current.filter( - ({ collapsible, collapsed }) => collapsible === "dynamic" && !collapsed - ); - const last = rest.pop(); - if (last) { - const lastTarget = getElementForItem(ref, last); - lastTarget.dataset.collapsing = last.collapsing = true; - } else { - // Set minSize to current measured size - // TODO check that this makes sense...suspect it doesn't - setContainerMinSize(); - } - }, - [setContainerMinSize] - ); - - const restoreCollapsingItem = useCallback((item, target) => { - target.dataset.collapsing = item.collapsing = false; - // we might have an opportunity to switch the next collapsed item to - // collapsing here. If we don't do this, it will ge handled in the next resize - }, []); - - const checkDynamicContent = useCallback( - (containerHasGrown) => { - // The order must matter here - const collapsingItem = visibleRef.current.find( - ({ collapsible, collapsing }) => collapsible === "dynamic" && collapsing - ); - const collapsedItem = visibleRef.current.find( - ({ collapsible, collapsed }) => collapsible === "dynamic" && collapsed - ); - - if (collapsingItem === undefined && collapsedItem === undefined) { - return; - } - - if (collapsingItem === undefined) { - const target = getElementForItem(ref, collapsedItem); - target.dataset.collapsed = collapsedItem.collapsed = false; - target.dataset.collapsing = collapsedItem.collapsing = true; - return; - } - - const target = getElementForItem(ref, collapsingItem); - const dimension = horizontalRef.current ? "width" : "height"; - - if (containerHasGrown && collapsedItem) { - const size = measureMinimumNodeSize(target, dimension); - // We don't restore a collapsing item unless there is at least one collapsed item - if (collapsedItem && size === collapsingItem.size) { - restoreCollapsingItem(collapsingItem, target); - } - } else { - // Note we are going to compare width with minWidth. Margin is ignored here, so we - // use getBoundingClientRect rather than measureNode - const { [dimension]: size } = target.getBoundingClientRect(); - const style = getComputedStyle(target); - const minSize = parseInt(style.getPropertyValue(`min-${dimension}`)); - if (size === minSize) { - collapseCollapsingItem(collapsingItem, target); - } - } - }, - [collapseCollapsingItem, restoreCollapsingItem] - ); - - const resetMeasurements = useCallback(() => { - const [isOverflowing, innerContainerSize, rootContainerDepth] = - measureContainerOverflow(ref, orientation); - - containerSizeRef.current = innerContainerSize; - rootDepthRef.current = rootContainerDepth; - - const hasDynamicItems = hasUncollapsedDynamicItems(ref); - - if (hasDynamicItems || isOverflowing) { - const dimension = horizontalRef.current ? "width" : "height"; - const measurements = measureChildNodes(ref, dimension); - visibleRef.current = measurements; - overflowedRef.current = []; - } - - if (hasDynamicItems) { - // if we don't have overflow, but we do have dynamic collapse items, we need to monitor resize events - // to determine when the collapsing item reaches min-width. At which point it becomes collapsed, and - // the next dynanic collapse item assumes collapsing status - collapsingRef.current = true; - ref.current.dataset.collapsing = true; - - if (isOverflowing) { - // We will only encounter this scenario first-time in. Once we initialize for dynamic content, - // there will be no more overflow (unless we decide to re-enable overflow once all dynamic - // items are collapsed ). - initializeDynamicContent(); - } else { - const collapsingItem = lastListItem(visibleRef); - const element = getElementForItem(ref, collapsingItem); - element.dataset.collapsing = collapsingItem.collapsing = true; - } - } else if (isOverflowing) { - // We may already have an overflowIndicator here, if caller is Tabstrip - let renderedSize = visibleRef.current.reduce( - addAllExceptOverflowIndicator, - 0 - ); - const result = markOverflowingItems( - renderedSize, - innerContainerSize - overflowIndicatorSizeRef.current - ); - updateOverflowStatus(+result); - } - }, [ - initializeDynamicContent, - markOverflowingItems, - orientation, - updateOverflowStatus, - ]); - - const resizeHandler = useCallback( - ({ - scrollHeight, - height = scrollHeight, - scrollWidth, - width = scrollWidth, - }) => { - const [size, depth] = horizontalRef.current - ? [width, height] - : [height, width]; - - const wasFullSize = overflowingRef.current === 0; - const overflowDetected = depth > rootDepthRef.current; - const containerHasGrown = size > containerSizeRef.current; - - containerSizeRef.current = size; - - if (containerHasGrown && size === minSizeRef.current) { - // ignore - } else if (collapsingRef.current) { - checkDynamicContent(containerHasGrown); - } else if (!wasFullSize && containerHasGrown) { - const result = removeOverflowIfSpaceAllows(size); - // Don't remove the overflowing status if there are remaining overflowed item(s). - // Unlike collapsed items, overflowed is not a reference count. - if (result !== OVERFLOWING || overflowedRef.current.length === 0) { - updateOverflowStatus(-result); - } else if (result === OVERFLOWING) { - updateOverflowStatus(0, true); - } - } else if (wasFullSize && overflowDetected) { - // TODO if client is not using an overflow indicator, there is nothing to do here, - // just let nature take its course. How do we know this ? - // This is when we need to add width to measurements we are tracking - resetMeasurements(); - } else if (!wasFullSize && overflowDetected) { - // we're still overflowing - let renderedSize = visibleRef.current.reduce(addAll, 0); - if (size < renderedSize) { - const result = markOverflowingItems(renderedSize, size); - updateOverflowStatus(+result); - } - } - }, - [ - checkDynamicContent, - removeOverflowIfSpaceAllows, - resetMeasurements, - markOverflowingItems, - overflowingRef, - updateOverflowStatus, - ] - ); - - useLayoutEffect(() => { - const dimension = horizontalRef.current ? "width" : "height"; - if (newlyCollapsed(visibleRef.current)) { - // These are in reverse priority order, so last collapsed will always be first - const [collapsedItem] = visibleRef.current.filter( - (item) => item.collapsed - ); - if (collapsedItem.fullSize === null) { - const target = getElementForItem(ref, collapsedItem); - if (target) { - const collapsedSize = measureMinimumNodeSize(target, dimension); - collapsedItem.fullSize = collapsedItem.size; - collapsedItem.size = collapsedSize; - // is the difference between collapsed size and original size enough ? - // TODO we repeat this code a lot, factoer it out - const renderedSize = visibleRef.current.reduce(addAll, 0); - if (renderedSize > containerSizeRef.current) { - const strategy = markOverflowingItems( - renderedSize, - containerSizeRef.current - overflowIndicatorSizeRef.current - ); - updateOverflowStatus(+strategy); - } - } - } - } else if (includesOverflow(overflowing)) { - const target = ref.current.querySelector( - `:scope > [data-overflow-indicator='true']` - ); - if (target) { - const { index, priority = "1" } = target?.dataset ?? NO_DATA; - const item = { - index: parseInt(index, 10), - isOverflowIndicator: true, - priority: parseInt(priority, 10), - label: target.innerText, - size: measureMinimumNodeSize(target, dimension), - }; - overflowIndicatorSizeRef.current = item.size; - visibleRef.current = visibleRef.current - .concat(item) - .sort(byDescendingPriority); - } - } else if (getOverflowIndicator(visibleRef)) { - visibleRef.current = visibleRef.current.filter( - (item) => !item.isOverflowIndicator - ); - } - }, [ - markOverflowingItems, - overflowing, - ref, - updateOverflowStatus, - visibleRef, - ]); - - // Measurement occurs post-render, by necessity, need to trigger a render - useLayoutEffect(() => { - async function measure() { - await document.fonts.ready; - if (ref.current !== null) { - resetMeasurements(); - } - } - if (orientation !== "none") { - measure(); - } - }, [label, orientation, resetMeasurements]); - - useResizeObserver(ref, MONITORED_DIMENSIONS[orientation], resizeHandler); - - return [ref, overflowedRef.current, collapsedRef.current, resetMeasurements]; -} diff --git a/vuu-ui/packages/vuu-layout/src/stack/Stack.css b/vuu-ui/packages/vuu-layout/src/stack/Stack.css index f7fb92a9f..f2d1e7549 100644 --- a/vuu-ui/packages/vuu-layout/src/stack/Stack.css +++ b/vuu-ui/packages/vuu-layout/src/stack/Stack.css @@ -8,8 +8,6 @@ flex-direction: row; } - - .Tabs .Toolbar:before { left: 0; width: 100%; @@ -36,10 +34,6 @@ overflow: hidden; } -/* .Splitter.column + .Flexbox > .Tabs:first-child { - border-top: solid 1px lightgrey; -} */ - .Layout-svg-button { --spacing-medium: 5px; } diff --git a/vuu-ui/packages/vuu-layout/src/stack/Stack.tsx b/vuu-ui/packages/vuu-layout/src/stack/Stack.tsx index 51b819faa..d1ed2f7ab 100644 --- a/vuu-ui/packages/vuu-layout/src/stack/Stack.tsx +++ b/vuu-ui/packages/vuu-layout/src/stack/Stack.tsx @@ -1,13 +1,13 @@ +import { Tab, Tabstrip, Toolbar, ToolbarField } from "@heswell/salt-lab"; import { useIdMemo as useId } from "@salt-ds/core"; import cx from "classnames"; -import { Tab, Tabstrip, Toolbar, ToolbarField } from "@heswell/salt-lab"; import React, { ForwardedRef, forwardRef, MouseEvent, ReactElement, ReactNode, - useCallback, + useCallback } from "react"; import { StackProps } from "./stackTypes"; @@ -15,7 +15,7 @@ import "./Stack.css"; const classBase = "Tabs"; -const getDefaultTabIcon = (component: ReactElement, tabIndex: number) => +const getDefaultTabIcon = () => undefined; const getDefaultTabLabel = (component: ReactElement, tabIndex: number) => @@ -60,34 +60,27 @@ export const Stack = forwardRef(function Stack( const id = useId(idProp); const handleTabSelection = (nextIdx: number) => { - // if uncontrolled, handle it internally onTabSelectionChanged?.(nextIdx); }; const handleTabClose = (tabIndex: number) => { - // if uncontrolled, handle it internally onTabClose?.(tabIndex); }; const handleAddTab = () => { - // if uncontrolled, handle it internally onTabAdd?.(React.Children.count(children)); }; const handleMouseDown = (e: MouseEvent) => { - // if uncontrolled, handle it internally const target = e.target as HTMLElement; const tabElement = target.closest('[role^="tab"]') as HTMLDivElement; const role = tabElement?.getAttribute("role"); if (role === "tab") { const tabIndex = parseInt(tabElement.dataset.idx ?? "-1"); - if (tabIndex !== -1) { - onMouseDown?.(e, tabIndex); - } else { + if (tabIndex === -1) { throw Error("Stack: mousedown on tab with unknown index"); } - } else if (role === "tablist") { - console.log(`Stack mousedown on tabstrip`); + onMouseDown?.(e, tabIndex); } }; @@ -106,11 +99,11 @@ export const Stack = forwardRef(function Stack( const activeChild = () => { if (React.isValidElement(children)) { return children; - } else if (Array.isArray(children)) { + } + if (Array.isArray(children)) { return children[active] ?? null; - } else { - return null; } + return null; }; const renderTabs = () => @@ -122,12 +115,11 @@ export const Stack = forwardRef(function Stack( ariaControls={`${rootId}-tab`} data-icon={getTabIcon(child, idx)} draggable - key={childId ?? idx} // Important that we key by child identifier, not using index + key={childId ?? idx} id={rootId} label={getTabLabel(child, idx)} closeable={closeable} editable={TabstripProps?.enableRenameTab !== false} - // onEdit={handleTabEdit} /> ); }); @@ -147,7 +139,6 @@ export const Stack = forwardRef(function Stack( ( - // Note make this width 100% and height 100% and we get a weird error where view continually resizes - growing ( export const StackLayout = (props: StackProps) => { const ref = useRef(null); const dispatch = useLayoutProviderDispatch(); - const { loadState, saveState } = usePersistentState(); + const { loadState } = usePersistentState(); const { createNewChild = defaultCreateNewChild, @@ -61,7 +58,7 @@ export const StackLayout = (props: StackProps) => { } }; - const handleTabAdd = (e: any, tabIndex = React.Children.count(children)) => { + const handleTabAdd = (e: unknown, tabIndex = React.Children.count(children)) => { if (path) { console.log(`[StackLayout] handleTabAdd`); const component = createNewChild(tabIndex); @@ -74,14 +71,10 @@ export const StackLayout = (props: StackProps) => { } }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const handleMouseDown = async (e: any, index: number) => { - // If user drags the selected Tab, we need to select another Tab and re-render. - // This needs to be co-ordinated with drag Tab within Tabstrip, whcih can - // be handles within The Tabstrip until final release - much like Splitter - // dragging in Flexbox. let readyToDrag: undefined | ((value: unknown) => void); - // Experimental const preDragActivity = async () => new Promise((resolve) => { console.log("preDragActivity: Ok, gonna release the drag"); @@ -99,10 +92,6 @@ export const StackLayout = (props: StackProps) => { }; const handleTabEdit = (tabIndex: number, text: string) => { - // Save into state on behalf of the associated View - // Do we need a mechanism to get this into the JSPOMN when we serialize ? - // const { id } = children[tabIndex].props; - // saveState(id, 'view-title', text); dispatch({ type: "set-title", path: `${path}.${tabIndex}`, title: text }); }; @@ -122,13 +111,6 @@ export const StackLayout = (props: StackProps) => { onTabEdit={handleTabEdit} onTabSelectionChanged={handleTabSelection} ref={ref} - // toolbarContent={ - // - // - // - // - // - // } /> ); }; diff --git a/vuu-ui/packages/vuu-layout/src/stack/index.ts b/vuu-ui/packages/vuu-layout/src/stack/index.ts index c577cb35d..49b21cc76 100644 --- a/vuu-ui/packages/vuu-layout/src/stack/index.ts +++ b/vuu-ui/packages/vuu-layout/src/stack/index.ts @@ -1,3 +1,4 @@ export * from "./Stack"; export * from "./StackLayout"; export * from "./stackTypes"; + diff --git a/vuu-ui/packages/vuu-layout/src/tabs/TabPanel.tsx b/vuu-ui/packages/vuu-layout/src/tabs/TabPanel.tsx index 14d1f02e2..ad65d4b60 100644 --- a/vuu-ui/packages/vuu-layout/src/tabs/TabPanel.tsx +++ b/vuu-ui/packages/vuu-layout/src/tabs/TabPanel.tsx @@ -1,4 +1,4 @@ -import React, { HTMLAttributes } from 'react'; +import { HTMLAttributes } from 'react'; import './TabPanel.css'; diff --git a/vuu-ui/packages/vuu-layout/src/tools/config-wrapper/ConfigWrapper.jsx b/vuu-ui/packages/vuu-layout/src/tools/config-wrapper/ConfigWrapper.tsx similarity index 58% rename from vuu-ui/packages/vuu-layout/src/tools/config-wrapper/ConfigWrapper.jsx rename to vuu-ui/packages/vuu-layout/src/tools/config-wrapper/ConfigWrapper.tsx index a0d6af79b..793c603a0 100644 --- a/vuu-ui/packages/vuu-layout/src/tools/config-wrapper/ConfigWrapper.jsx +++ b/vuu-ui/packages/vuu-layout/src/tools/config-wrapper/ConfigWrapper.tsx @@ -1,48 +1,50 @@ -import React, { useState } from 'react'; +import React, { useState } from "react"; -import { LayoutConfigurator, LayoutTreeViewer } from '..'; -import { followPathToComponent } from '../..'; +import { LayoutConfigurator, LayoutTreeViewer } from ".."; +import { followPathToComponent } from "../.."; -export const ConfigWrapper = ({ children }) => { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const ConfigWrapper = ({ children }: any) => { const designMode = false; - // const [designMode, setDesignMode] = useState(false); const [layout, setLayout] = useState(children); const [selectedComponent, setSelectedComponent] = useState(children); - const handleSelection = (selectedPath) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleSelection = (selectedPath: any) => { const targetComponent = followPathToComponent(layout, selectedPath); setSelectedComponent(targetComponent); }; - const handleChange = (property, value) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleChange = (property: any, value: any) => { console.log(`change ${property} -> ${value}`); - // 2) replace selectedComponent and set layout const newComponent = React.cloneElement(selectedComponent, { style: { ...selectedComponent.props.style, - [property]: value - } + [property]: value, + }, }); setSelectedComponent(newComponent); - setLayout(React.cloneElement(layout, null, newComponent)); + setLayout(React.cloneElement(layout, {}, newComponent)); }; return (
{layout}
-
+
{/* (); const sessionState = new Map(); -// These is not exported by package, only available within -// layout. Used by LayoutProvider for layout serialization. export const getPersistentState = (id: string) => persistentState.get(id); export const hasPersistentState = (id: string) => persistentState.has(id); export const setPersistentState = (id: string, value: any) => persistentState.set(id, value); export const usePersistentState = () => { - //TODO create single set of methods that operate on either session or state const loadSessionState = useCallback((id, key) => { const state = sessionState.get(id); if (state) { if (key !== undefined && state[key] !== undefined) { return state[key]; - } else if (key !== undefined) { + } + if (key !== undefined) { return undefined; - } else { - return state; } + return state; } }, []); @@ -63,9 +61,8 @@ export const usePersistentState = () => { if (state) { if (key !== undefined) { return state[key]; - } else { - return state; } + return state; } }, []); diff --git a/vuu-ui/packages/vuu-layout/src/utils/componentFromLayout.tsx b/vuu-ui/packages/vuu-layout/src/utils/componentFromLayout.tsx index fab5782c0..0602267a7 100644 --- a/vuu-ui/packages/vuu-layout/src/utils/componentFromLayout.tsx +++ b/vuu-ui/packages/vuu-layout/src/utils/componentFromLayout.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { LayoutJSON } from '../layout-reducer'; import { ComponentRegistry } from '../registry/ComponentRegistry'; @@ -6,7 +5,7 @@ export function componentFromLayout(layoutModel: LayoutJSON) { const { id, type, props, children: layoutChildren } = layoutModel; const ReactType = getComponentType(type); - let children = + const children = !layoutChildren || layoutChildren.length === 0 ? null : layoutChildren.length === 1 @@ -20,7 +19,6 @@ export function componentFromLayout(layoutModel: LayoutJSON) { ); } -// support for built-in react ttpes (div etc) removed here function getComponentType(type: string) { const reactType = ComponentRegistry[type]; if (reactType === undefined){ diff --git a/vuu-ui/packages/vuu-layout/src/utils/index.ts b/vuu-ui/packages/vuu-layout/src/utils/index.ts index 7f3826e23..17fd48e55 100644 --- a/vuu-ui/packages/vuu-layout/src/utils/index.ts +++ b/vuu-ui/packages/vuu-layout/src/utils/index.ts @@ -1,6 +1,7 @@ -export * from "./styleUtils"; +export * from "./componentFromLayout"; export * from "./pathUtils"; export * from "./propUtils"; -export * from "./typeOf"; -export * from "./componentFromLayout"; export * from "./refUtils"; +export * from "./styleUtils"; +export * from "./typeOf"; + diff --git a/vuu-ui/packages/vuu-layout/src/utils/pathUtils.ts b/vuu-ui/packages/vuu-layout/src/utils/pathUtils.ts index bdaecf3fe..7bd623ed1 100644 --- a/vuu-ui/packages/vuu-layout/src/utils/pathUtils.ts +++ b/vuu-ui/packages/vuu-layout/src/utils/pathUtils.ts @@ -13,7 +13,6 @@ const removeFinalPathSegment = (path: string) => { } }; -// TODO isn't this equivalent to containerOf ? export function followPathToParent( source: ReactElement, path: string @@ -29,14 +28,16 @@ export function followPathToParent( export function findTarget( source: LayoutModel, + // eslint-disable-next-line @typescript-eslint/no-explicit-any test: (props: any) => boolean ): LayoutModel | undefined { const { children, ...props } = getProps(source); if (test(props)) { return source; - } else if (React.Children.count(children) > 0) { + } + if (React.Children.count(children) > 0) { const array = React.isValidElement(children) ? [children] : children; - for (let child of array) { + for (const child of array) { const target = findTarget(child, test); if (target) { return target; @@ -51,37 +52,32 @@ export function containerOf( ): LayoutModel | null { if (target === source) { return null; - } else { - const { path: sourcePath, children } = getProps(source); - - let { idx, finalStep } = nextStep(sourcePath, getProp(target, "path")); - if (finalStep) { - return source; - } else if (children === undefined || children[idx] === undefined) { - return null; - } else { - return containerOf(children[idx], target); - } } + const { path: sourcePath, children } = getProps(source); + const { idx, finalStep } = nextStep(sourcePath, getProp(target, "path")); + if (finalStep) { + return source; + } + if (children === undefined || children[idx] === undefined) { + return null; + } + return containerOf(children[idx], target); } -// Do not use React.Children.toArray, -// it does not preserve keys export const getChild = ( children: ReactElement[], idx: number ): ReactElement | undefined => { - // idx may be a nu,mber or string if (React.isValidElement(children) && idx == 0) { return children; - } else if (Array.isArray(children)) { + } + if (Array.isArray(children)) { return children[idx]; } }; -// Use a path only to identify a component export function followPathToComponent(component: ReactElement, path: string) { - var paths = path.split("."); + const paths = path.split("."); let children = [component]; const getChildren = (c: ReactElement) => @@ -94,9 +90,8 @@ export function followPathToComponent(component: ReactElement, path: string) { const child = children[idx]; if (i === paths.length - 1) { return child; - } else { - children = getChildren(child); } + children = getChildren(child); } } @@ -109,6 +104,7 @@ export function followPath( path: string, throwIfNotFound: true ): ReactElement; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function followPath(source: any, path: any, throwIfNotFound = false) { const { "data-path": dataPath, path: sourcePath = dataPath } = getProps(source); @@ -125,7 +121,7 @@ export function followPath(source: any, path: any, throwIfNotFound = false) { let target = source; const paths = route.split("."); - for (var i = 0; i < paths.length; i++) { + for (let i = 0; i < paths.length; i++) { if (React.Children.count(target.props.children) === 0) { const message = `element at 0.${paths .slice(0, i) @@ -191,7 +187,7 @@ export function nextLeaf(root: ReactElement, path: string) { } export function previousLeaf(root: ReactElement, path: string) { - let pathIndices = path.split(".").map((idx) => parseInt(idx, 10)); + const pathIndices = path.split(".").map((idx) => parseInt(idx, 10)); let lastIdx = pathIndices.pop(); let parent = followPathToParent(root, path); if (parent != null && typeof lastIdx === "number") { @@ -205,16 +201,12 @@ export function previousLeaf(root: ReactElement, path: string) { root, getProp(parent, "path") ) as ReactElement; - // pathIndices = nextParent.props.path - // .split(".") - // .map((idx) => parseInt(idx, 10)); if (lastIdx > 0) { const nextStep = parent.props.children[lastIdx - 1]; if (isContainer(typeOf(nextStep) as string)) { return lastLeaf(nextStep); - } else { - return nextStep; } + return nextStep; } } } @@ -226,18 +218,16 @@ function firstLeaf(layoutRoot: ReactElement): ReactElement { if (isContainer(typeOf(layoutRoot) as string)) { const { children } = layoutRoot.props || layoutRoot; return firstLeaf(children[0]); - } else { - return layoutRoot; } + return layoutRoot; } function lastLeaf(root: ReactElement): ReactElement { if (isContainer(typeOf(root) as string)) { const { children } = root.props || root; return lastLeaf(children[children.length - 1]); - } else { - return root; } + return root; } type NextStepResult = { @@ -260,7 +250,6 @@ export function nextStep( } const endOfTheLine = followPathToEnd ? 0 : 1; - // check that pathSoFar startsWith targetPath and if not, throw const paths = targetPath .replace(pathVisited, "") .split(".") @@ -271,13 +260,13 @@ export function nextStep( export function resetPath( model: ReactElement, path: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any additionalProps?: any ): ReactElement { if (getProp(model, "path") === path) { return model; } const children: ReactElement[] = []; - // React.Children.map rewrites keys, forEach does not React.Children.forEach(model.props.children, (child, i) => { if (!getProp(child, "path")) { children.push(child); diff --git a/vuu-ui/packages/vuu-layout/src/utils/propUtils.ts b/vuu-ui/packages/vuu-layout/src/utils/propUtils.ts index 11049691b..5da06f5ac 100644 --- a/vuu-ui/packages/vuu-layout/src/utils/propUtils.ts +++ b/vuu-ui/packages/vuu-layout/src/utils/propUtils.ts @@ -9,7 +9,6 @@ export const getProp = (component: LayoutModel, propName: string) => { export const getProps = (component?: LayoutModel) => component?.props || component || NO_PROPS; -// Used when a container is expected to have a single child export const getChildProp = (container: LayoutModel) => { const props = getProps(container); if (props.children) { diff --git a/vuu-ui/packages/vuu-layout/src/utils/styleUtils.ts b/vuu-ui/packages/vuu-layout/src/utils/styleUtils.ts index 4f6293136..b87d8cc66 100644 --- a/vuu-ui/packages/vuu-layout/src/utils/styleUtils.ts +++ b/vuu-ui/packages/vuu-layout/src/utils/styleUtils.ts @@ -2,13 +2,12 @@ import { CSSProperties } from 'react'; export type CSSFlexProperties = Pick; export const expandFlex = (flex: number | CSSFlexProperties): CSSFlexProperties => { - if (typeof flex === 'number') { - return { - flexBasis: 0, - flexGrow: 1, - flexShrink: 1 - }; - } else { + if (typeof flex !== 'number') { throw Error(`"no support yet for flex value ${flex}`); } + return { + flexBasis: 0, + flexGrow: 1, + flexShrink: 1 + }; }; diff --git a/vuu-ui/packages/vuu-layout/src/utils/typeOf.ts b/vuu-ui/packages/vuu-layout/src/utils/typeOf.ts index fb1ebc142..99bc4b367 100644 --- a/vuu-ui/packages/vuu-layout/src/utils/typeOf.ts +++ b/vuu-ui/packages/vuu-layout/src/utils/typeOf.ts @@ -1,18 +1,21 @@ import { ReactElement } from 'react'; import { LayoutModel, WithType } from '../layout-reducer'; -//TODO this should throw if we cannot identify a type export function typeOf(element?: LayoutModel | WithType): string | undefined { if (element) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const type = element.type as any; if (typeof type === 'function' || typeof type === 'object') { const elementName = type.displayName || type.name || type.type?.name; if (typeof elementName === 'string') { return elementName; } - } else if (typeof element.type === 'string') { + } + if (typeof element.type === 'string') { return element.type; - } else if (element.constructor) { + } + if (element.constructor) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any return (element.constructor as any).displayName as string; } throw Error(`typeOf unable to determine type of element`); diff --git a/vuu-ui/packages/vuu-layout/tsconfig-emit-types.json b/vuu-ui/packages/vuu-layout/tsconfig-emit-types.json index f26a6030c..79cd539ec 100644 --- a/vuu-ui/packages/vuu-layout/tsconfig-emit-types.json +++ b/vuu-ui/packages/vuu-layout/tsconfig-emit-types.json @@ -1,13 +1,11 @@ -{ - "extends": "../../tsconfig.json", + { + "extends": "../../tsconfig-emit-types.json", "compilerOptions": { - "declaration": true, - "emitDeclarationOnly": true, - "noEmit": false, - "outDir": "types" + "outDir": "../../dist/vuu-layout/types" }, "include": [ - "src", + "src" ] } + \ No newline at end of file diff --git a/vuu-ui/packages/vuu-popups/src/dialog/Dialog.tsx b/vuu-ui/packages/vuu-popups/src/dialog/Dialog.tsx index 0f62bbe24..a57a01210 100644 --- a/vuu-ui/packages/vuu-popups/src/dialog/Dialog.tsx +++ b/vuu-ui/packages/vuu-popups/src/dialog/Dialog.tsx @@ -1,10 +1,8 @@ -import React, { HTMLAttributes, useCallback, useRef, useState } from "react"; +import { Scrim, Toolbar, ToolbarButton } from "@heswell/salt-lab"; +import { Text } from "@salt-ds/core"; import cx from "classnames"; +import { HTMLAttributes, useCallback, useRef, useState } from "react"; import { Portal } from "../portal"; -import { Scrim } from "@heswell/salt-lab"; - -import { Text } from "@salt-ds/core"; -import { Toolbar, ToolbarButton } from "@heswell/salt-lab"; import "./Dialog.css"; @@ -24,11 +22,10 @@ export const Dialog = ({ ...props }: DialogProps) => { const root = useRef(null); - const [posX, setPosX] = useState(0); - const [posY, setPosY] = useState(0); + const [posX] = useState(0); + const [posY] = useState(0); const close = useCallback(() => { - // TODO onClose?.(); }, [onClose]); diff --git a/vuu-ui/packages/vuu-popups/src/portal/portal-utils.ts b/vuu-ui/packages/vuu-popups/src/portal/portal-utils.ts index 8f7c82775..eae63e58b 100644 --- a/vuu-ui/packages/vuu-popups/src/portal/portal-utils.ts +++ b/vuu-ui/packages/vuu-popups/src/portal/portal-utils.ts @@ -1,4 +1,4 @@ -export const installTheme = (themeId) => { +export const installTheme = (themeId: string) => { const installedThemes = getComputedStyle(document.body).getPropertyValue( "--installed-themes" ); diff --git a/vuu-ui/scripts/build-all-type-defs.mjs b/vuu-ui/scripts/build-all-type-defs.mjs index 5e052b584..8b39f02f8 100644 --- a/vuu-ui/scripts/build-all-type-defs.mjs +++ b/vuu-ui/scripts/build-all-type-defs.mjs @@ -13,7 +13,7 @@ const packages = [ // 'vuu-datagrid', "vuu-datatable", // "vuu-datagrid-extras", - // 'vuu-layout', + "vuu-layout", // 'vuu-shell' ]; diff --git a/vuu-ui/yarn.lock b/vuu-ui/yarn.lock index 555f18cac..ae1b258d8 100644 --- a/vuu-ui/yarn.lock +++ b/vuu-ui/yarn.lock @@ -479,23 +479,6 @@ acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== -ag-grid-community@^28.2.1: - version "28.2.1" - resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-28.2.1.tgz#50778cb2254ee79497222781909d8364007dd91e" - integrity sha512-DMZh/xD/FqYP17qJ1M92PolTYe+hrKuEaf+A4h13O6qn2x/xZQrTRGW5DgnQLR/uLMe1XXZQPKR3UKgAlKo69A== - -ag-grid-enterprise@^28.2.1: - version "28.2.1" - resolved "https://registry.yarnpkg.com/ag-grid-enterprise/-/ag-grid-enterprise-28.2.1.tgz#e5685c879b2a80ee9a4e111d5b8f0774fcc682b0" - integrity sha512-FIqIiaMMO9m8eqfu64rZ7dOK8vQFqD+Dp9CTpSfjMtyUp4IoRE36c+mUpUGLX/bIix9LltpaUmr185nFM1Alrw== - -ag-grid-react@^28.2.1: - version "28.2.1" - resolved "https://registry.yarnpkg.com/ag-grid-react/-/ag-grid-react-28.2.1.tgz#c38585b8f165a5cf9343eab6994c06855f5b2caf" - integrity sha512-3vbw+B77uWwAyiOJxQA5U+PQFRCCccUx7L5PIwrnA4Y7c1yAu8sB65hAZdBc9kuW26iltwv7asq0UzP7UAQUpg== - dependencies: - prop-types "^15.8.1" - ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" From 43bdc3052e0a6df9419f5acf239a09483e6bd490 Mon Sep 17 00:00:00 2001 From: heswell Date: Sat, 14 Jan 2023 23:07:03 +0000 Subject: [PATCH 2/4] Issue 406 typescript popups (#426) * typescript uplift for vuu-popups * additional tidy up --- .../build-context-menu-descriptors.ts | 7 +- .../src/context-menu/useContextMenu.ts | 14 +- .../vuu-popups/src/menu/ContextMenu.css | 22 -- .../menu/{ContextMenu.jsx => ContextMenu.tsx} | 33 +-- .../packages/vuu-popups/src/menu/MenuList.css | 2 +- .../src/menu/{MenuList.jsx => MenuList.tsx} | 112 ++++++---- .../packages/vuu-popups/src/menu/aim/aim.js | 92 -------- .../packages/vuu-popups/src/menu/aim/aim.ts | 153 +++++++++++++ .../vuu-popups/src/menu/aim/corners.js | 114 ---------- .../vuu-popups/src/menu/aim/corners.ts | 157 ++++++++++++++ .../src/menu/aim/point-in-polygon.js | 25 --- .../src/menu/aim/{utils.js => utils.ts} | 2 +- .../vuu-popups/src/menu/apply-handlers.js | 15 -- .../src/menu/context-menu-provider.jsx | 145 ------------- .../src/menu/context-menu-provider.tsx | 103 +++++++++ .../src/menu/{index.js => index.ts} | 1 + .../packages/vuu-popups/src/menu/key-code.js | 61 ------ .../packages/vuu-popups/src/menu/key-code.ts | 71 ++++++ .../vuu-popups/src/menu/list-dom-utils.js | 22 -- .../vuu-popups/src/menu/list-dom-utils.ts | 25 +++ .../menu/{use-cascade.js => use-cascade.ts} | 125 +++++++---- .../vuu-popups/src/menu/use-click-away.js | 22 -- .../vuu-popups/src/menu/use-click-away.ts | 34 +++ ...tems-with-ids.js => use-items-with-ids.ts} | 44 ++-- .../src/menu/use-keyboard-navigation.js | 162 -------------- .../src/menu/use-keyboard-navigation.ts | 203 ++++++++++++++++++ .../vuu-popups/src/menu/useContextMenu.tsx | 91 ++++++++ vuu-ui/packages/vuu-popups/src/menu/utils.js | 5 - vuu-ui/packages/vuu-popups/src/menu/utils.ts | 7 + .../src/popup/{index.js => index.ts} | 0 .../{popup-service.js => popup-service.ts} | 126 ++++++----- .../vuu-popups/tsconfig-emit-types.json | 10 + vuu-ui/scripts/build-all-type-defs.mjs | 2 +- .../{Menu.stories.jsx => Menu.examples.tsx} | 87 +++++--- vuu-ui/showcase/src/examples/Layout/index.ts | 2 +- 35 files changed, 1215 insertions(+), 881 deletions(-) delete mode 100644 vuu-ui/packages/vuu-popups/src/menu/ContextMenu.css rename vuu-ui/packages/vuu-popups/src/menu/{ContextMenu.jsx => ContextMenu.tsx} (80%) rename vuu-ui/packages/vuu-popups/src/menu/{MenuList.jsx => MenuList.tsx} (60%) delete mode 100644 vuu-ui/packages/vuu-popups/src/menu/aim/aim.js create mode 100644 vuu-ui/packages/vuu-popups/src/menu/aim/aim.ts delete mode 100644 vuu-ui/packages/vuu-popups/src/menu/aim/corners.js create mode 100644 vuu-ui/packages/vuu-popups/src/menu/aim/corners.ts delete mode 100644 vuu-ui/packages/vuu-popups/src/menu/aim/point-in-polygon.js rename vuu-ui/packages/vuu-popups/src/menu/aim/{utils.js => utils.ts} (91%) delete mode 100644 vuu-ui/packages/vuu-popups/src/menu/apply-handlers.js delete mode 100644 vuu-ui/packages/vuu-popups/src/menu/context-menu-provider.jsx create mode 100644 vuu-ui/packages/vuu-popups/src/menu/context-menu-provider.tsx rename vuu-ui/packages/vuu-popups/src/menu/{index.js => index.ts} (74%) delete mode 100644 vuu-ui/packages/vuu-popups/src/menu/key-code.js create mode 100644 vuu-ui/packages/vuu-popups/src/menu/key-code.ts delete mode 100644 vuu-ui/packages/vuu-popups/src/menu/list-dom-utils.js create mode 100644 vuu-ui/packages/vuu-popups/src/menu/list-dom-utils.ts rename vuu-ui/packages/vuu-popups/src/menu/{use-cascade.js => use-cascade.ts} (70%) delete mode 100644 vuu-ui/packages/vuu-popups/src/menu/use-click-away.js create mode 100644 vuu-ui/packages/vuu-popups/src/menu/use-click-away.ts rename vuu-ui/packages/vuu-popups/src/menu/{use-items-with-ids.js => use-items-with-ids.ts} (61%) delete mode 100644 vuu-ui/packages/vuu-popups/src/menu/use-keyboard-navigation.js create mode 100644 vuu-ui/packages/vuu-popups/src/menu/use-keyboard-navigation.ts create mode 100644 vuu-ui/packages/vuu-popups/src/menu/useContextMenu.tsx delete mode 100644 vuu-ui/packages/vuu-popups/src/menu/utils.js create mode 100644 vuu-ui/packages/vuu-popups/src/menu/utils.ts rename vuu-ui/packages/vuu-popups/src/popup/{index.js => index.ts} (100%) rename vuu-ui/packages/vuu-popups/src/popup/{popup-service.js => popup-service.ts} (66%) create mode 100644 vuu-ui/packages/vuu-popups/tsconfig-emit-types.json rename vuu-ui/showcase/src/examples/Layout/{Menu.stories.jsx => Menu.examples.tsx} (82%) diff --git a/vuu-ui/packages/vuu-datagrid/src/context-menu/build-context-menu-descriptors.ts b/vuu-ui/packages/vuu-datagrid/src/context-menu/build-context-menu-descriptors.ts index 5254a2185..bcb3a36f3 100644 --- a/vuu-ui/packages/vuu-datagrid/src/context-menu/build-context-menu-descriptors.ts +++ b/vuu-ui/packages/vuu-datagrid/src/context-menu/build-context-menu-descriptors.ts @@ -1,3 +1,4 @@ +import { MenuBuilder } from "@finos/vuu-popups"; import { VuuAggregation, VuuGroupBy, @@ -35,8 +36,10 @@ interface GridContextMenuDescriptor { } export const buildContextMenuDescriptors = - (gridModel: GridModelType) => - (location: ContextMenuLocation, options: ContextMenuOptions) => { + ( + gridModel: GridModelType + ): MenuBuilder => + (location, options) => { const descriptors = []; if (location === "header") { descriptors.push(...buildSortMenuItems(gridModel.sort, options)); diff --git a/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts b/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts index a7ab8c5ae..98f71cb4d 100644 --- a/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts +++ b/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts @@ -1,6 +1,7 @@ /* eslint-disable no-sequences */ import { DataSource } from "@finos/vuu-data"; import { removeColumnFromFilter } from "@finos/vuu-filters"; +import { MenuActionHandler } from "@finos/vuu-popups"; import { AggregationType } from "../constants"; import { GridModelDispatch } from "../grid-context"; import { GridModelType } from "../grid-model/gridModelTypes"; @@ -36,12 +37,13 @@ export const useContextMenu = ({ dispatchGridModelAction, }: ContextMenuHookProps) => { /** return {boolean} used by caller to determine whether to forward to additional installed context menu handlers */ - const handleContextMenuAction = ( - type: string, - options: ContextMenuOptions + const handleContextMenuAction: MenuActionHandler = ( + type, + options ): boolean => { - if (options.column) { - const { column } = options; + const gridOptions = options as ContextMenuOptions; + if (gridOptions.column) { + const { column } = gridOptions; // prettier-ignore switch(type){ case "sort-asc": return dataSource.sort(GridModel.setSortColumn(gridModel, column, "A")), true; @@ -51,7 +53,7 @@ export const useContextMenu = ({ case "group": return dataSource.group(GridModel.addGroupColumn({}, column)), true; case "group-add": return dataSource.group(GridModel.addGroupColumn(gridModel, column)), true; case "column-hide": return dispatchGridModelAction({type, column}),true; - case "filter-remove-column": return handleRemoveColumnFromFilter(options, dataSource), true; + case "filter-remove-column": return handleRemoveColumnFromFilter(gridOptions, dataSource), true; case "remove-filters": return dataSource.filter(undefined, ""), true; case "agg-avg": return dataSource.aggregate(GridModel.setAggregation(gridModel, column, Average)), true; case "agg-high": return dataSource.aggregate(GridModel.setAggregation(gridModel, column, High)), true; diff --git a/vuu-ui/packages/vuu-popups/src/menu/ContextMenu.css b/vuu-ui/packages/vuu-popups/src/menu/ContextMenu.css deleted file mode 100644 index 4534a9f22..000000000 --- a/vuu-ui/packages/vuu-popups/src/menu/ContextMenu.css +++ /dev/null @@ -1,22 +0,0 @@ -.hwPopupContainer.top-bottom-right-right .popup-menu { - left: auto; - right: 0; -} - -.popup-menu .menu-item.showing > button, -.popup-menu .menu-item > button:focus, -.popup-menu .menu-item > button:hover { - text-decoration: none; - color: rgb(0, 0, 0); - background-color: rgb(220, 220, 220); -} - -.popup-menu .menu-item.disabled > button { - clear: both; - font-weight: normal; - line-height: 1.5; - color: rgb(120, 120, 120); - white-space: nowrap; - text-decoration: none; - cursor: default; -} diff --git a/vuu-ui/packages/vuu-popups/src/menu/ContextMenu.jsx b/vuu-ui/packages/vuu-popups/src/menu/ContextMenu.tsx similarity index 80% rename from vuu-ui/packages/vuu-popups/src/menu/ContextMenu.jsx rename to vuu-ui/packages/vuu-popups/src/menu/ContextMenu.tsx index 4335c79f9..6956533ee 100644 --- a/vuu-ui/packages/vuu-popups/src/menu/ContextMenu.jsx +++ b/vuu-ui/packages/vuu-popups/src/menu/ContextMenu.tsx @@ -1,33 +1,39 @@ import { useIdMemo as useId } from "@salt-ds/core"; import { useCallback, useRef } from "react"; import { Portal } from "../portal"; -import MenuList from "./MenuList"; -import { useItemsWithIds } from "./use-items-with-ids"; +import MenuList, { MenuListProps } from "./MenuList"; import { getItemId, getMenuId, useCascade } from "./use-cascade"; - -import "./ContextMenu.css"; import { useClickAway } from "./use-click-away"; +import { useItemsWithIds } from "./use-items-with-ids"; + +export interface ContextMenuProps extends Omit { + onClose?: (menuId?: string, options?: unknown) => void; + position?: { x: number; y: number }; + withPortal?: boolean; +} + +const noop = () => undefined; export const ContextMenu = ({ - activatedWithKeyboard = false, + activatedByKeyboard, children: childrenProp, className, id: idProp, onClose = () => undefined, position = { x: 0, y: 0 }, - source: sourceProp, style, -}) => { + ...menuListProps +}: ContextMenuProps) => { const id = useId(idProp); - const closeMenuRef = useRef(null); - const [menus, actions] = useItemsWithIds(sourceProp, childrenProp); - const navigatingWithKeyboard = useRef(activatedWithKeyboard); + const closeMenuRef = useRef<(location?: string) => void>(noop); + const [menus, actions] = useItemsWithIds(childrenProp); + const navigatingWithKeyboard = useRef(activatedByKeyboard); const handleMouseEnterItem = useCallback(() => { navigatingWithKeyboard.current = false; }, []); const handleActivate = useCallback( - (menuId) => { + (menuId: string) => { const { action, options } = actions[menuId]; closeMenuRef.current("root"); onClose(action, options); @@ -57,7 +63,7 @@ export const ContextMenu = ({ isOpen: openMenus.length > 0, }); - const handleOpenMenu = (id) => { + const handleOpenMenu = (id: string) => { const itemId = getItemId(id); const menuId = getMenuId(itemId); navigatingWithKeyboard.current = true; @@ -74,7 +80,7 @@ export const ContextMenu = ({ const lastMenu = openMenus.length - 1; - const getChildMenuIndex = (i) => { + const getChildMenuIndex = (i: number) => { if (i >= lastMenu) { return -1; } else { @@ -94,6 +100,7 @@ export const ContextMenu = ({ return (
  • ; +export interface MenuItemGroupProps { + children: ReactElement[]; + label: string; +} + +export interface MenuItemProps extends HTMLAttributes { + action?: string; + idx?: number; + options?: unknown; +} + // Purely used as markers, props will be extracted -export const MenuItemGroup = () => null; +export const MenuItemGroup: FC = () => null; // eslint-disable-next-line no-unused-vars -export const MenuItem = ({ children, idx, ...props }) => { +export const MenuItem = ({ children, idx, ...props }: MenuItemProps) => { return
    {children}
    ; }; -const hasIcon = (child) => child.props["data-icon"]; +const hasIcon = (child: ReactElement) => child.props["data-icon"]; + +export interface MenuListProps extends HTMLAttributes { + activatedByKeyboard?: boolean; + children: ReactElement[]; + childMenuShowing?: number; + highlightedIdx?: number; + isRoot?: boolean; + listItemProps?: Partial; + menuId?: string; + onActivate?: (menuId: string) => void; + onCloseMenu: (idx: number) => void; + onOpenMenu?: (menuId: string) => void; + onHighlightMenuItem?: (idx: number) => void; +} const MenuList = ({ activatedByKeyboard, @@ -34,45 +66,44 @@ const MenuList = ({ onCloseMenu, onOpenMenu, ...props -}) => { +}: MenuListProps) => { const id = useId(idProp); - const root = useRef(null); + const root = useRef(null); // The id generation be,ongs in useIttemsWithIds const mapIdxToId = useMemo(() => new Map(), []); - const handleOpenMenu = (idx) => { - const el = root.current.querySelector(`:scope > [data-idx='${idx}']`); - onOpenMenu(el.id); + const handleOpenMenu = (idx: number) => { + const el = root.current?.querySelector(`:scope > [data-idx='${idx}']`); + el?.id && onOpenMenu?.(el.id); }; - const handleActivate = (idx) => { - const el = root.current.querySelector(`:scope > [data-idx='${idx}']`); - onActivate(el.id); + const handleActivate = (idx: number) => { + const el = root.current?.querySelector(`:scope > [data-idx='${idx}']`); + el?.id && onActivate?.(el.id); }; - const { focusVisible, highlightedIdx, listProps } = useKeyboardNavigation({ - count: children.length, - highlightedIdx: highlightedIdxProp, + const { focusVisible, highlightedIndex, listProps } = useKeyboardNavigation({ + count: React.Children.count(children), + highlightedIndex: highlightedIdxProp, onActivate: handleActivate, onHighlight: onHighlightMenuItem, onOpenMenu: handleOpenMenu, onCloseMenu, - id, }); const appliedFocusVisible = childMenuShowing == -1 ? focusVisible : -1; useLayoutEffect(() => { if (childMenuShowing === -1 && activatedByKeyboard) { - root.current.focus(); + root.current?.focus(); } }, [activatedByKeyboard, childMenuShowing]); const getActiveDescendant = () => - highlightedIdx === undefined || highlightedIdx === -1 + highlightedIndex === undefined || highlightedIndex === -1 ? undefined - : mapIdxToId.get(highlightedIdx); + : mapIdxToId.get(highlightedIndex); return (
    + const maybeIcon = ( + childElement: ReactElement, + withIcon: boolean, + iconName?: string + ) => withIcon ? [ , - ].concat(children) - : children; - - function addClonedChild(list, child, idx, withIcon) { + ].concat(childElement) + : childElement; + + function addClonedChild( + list: ReactElement[], + child: ReactElement, + idx: number, + withIcon: boolean + ) { const { children, className, @@ -131,8 +171,8 @@ const MenuList = ({ `${id}-${menuId}`, itemId, idx, - child.key, - highlightedIdx, + child.key ?? itemId, + highlightedIndex, appliedFocusVisible, className, hasSeparator @@ -149,9 +189,9 @@ const MenuList = ({ // mapIdxToId.set(idx, itemId); } - const listItems = []; + const listItems: ReactElement[] = []; - if (children && children.length > 0) { + if (children.length > 0) { const withIcon = children.some(hasIcon); children.forEach((child, idx) => { @@ -164,14 +204,14 @@ const MenuList = ({ }; const getMenuItemProps = ( - baseId, - itemId, - idx, - key, - highlightedIdx, - focusVisible, - className, - hasSeparator + baseId: string, + itemId: string, + idx: number, + key: string, + highlightedIdx: number, + focusVisible: number, + className: string, + hasSeparator: boolean ) => ({ id: `${baseId}-${itemId}`, key: key ?? idx, diff --git a/vuu-ui/packages/vuu-popups/src/menu/aim/aim.js b/vuu-ui/packages/vuu-popups/src/menu/aim/aim.js deleted file mode 100644 index 5cf1a7640..000000000 --- a/vuu-ui/packages/vuu-popups/src/menu/aim/aim.js +++ /dev/null @@ -1,92 +0,0 @@ -import findCorners, { boundaries } from './corners'; -import pointInPolygon from './point-in-polygon'; - -export function distance(source, target) { - const a = source.x - target.x; - const b = source.y - target.y; - return Math.sqrt(a * a + b * b); -} - -export function side(corners) { - if (corners[0] === 'top-right' && corners[1] === 'bottom-right') return 'right'; - else if (corners[0] === 'top-left' && corners[1] === 'bottom-right') return 'top-right'; - else if (corners[0] === 'top-left' && corners[1] === 'top-right') return 'top'; - else if (corners[0] === 'bottom-left' && corners[1] === 'top-right') return 'top-left'; - else if (corners[0] === 'bottom-left' && corners[1] === 'top-left') return 'left'; - else if (corners[0] === 'bottom-right' && corners[1] === 'top-left') return 'bottom-left'; - else if (corners[0] === 'bottom-right' && corners[1] === 'bottom-left') return 'bottom'; - else if (corners[0] === 'top-right' && corners[1] === 'bottom-left') return 'bottom-right'; -} - -export function bullseye(corners, boundaries, mousePosition) { - switch (side(corners)) { - case 'right': - return { - x: boundaries[0].x, - y: mousePosition.y - }; - case 'top-right': - return { - x: boundaries[1].x, - y: boundaries[0].y - }; - case 'top': - return { - x: mousePosition.x, - y: boundaries[0].y - }; - case 'top-left': - return { - x: boundaries[0].x, - y: boundaries[1].y - }; - case 'left': - return { - x: boundaries[0].x, - y: mousePosition.y - }; - case 'bottom-left': - return { - x: boundaries[1].x, - y: boundaries[0].y - }; - case 'bottom': - return { - x: mousePosition.x, - y: boundaries[0].y - }; - case 'bottom-right': - return { - x: boundaries[0].x, - y: boundaries[1].y - }; - } -} - -function formatPoints(points) { - const finalPoints = []; - for (let i = 0, len = points.length; i < len; ++i) { - finalPoints.push([points[i].x, points[i].y]); - } - return finalPoints; -} - -export function aiming(e, mousePosition, prevMousePosition, target, alreadyAiming) { - if (!prevMousePosition) return false; - else if ( - !alreadyAiming && - mousePosition.x === prevMousePosition.x && - mousePosition.y === prevMousePosition.y - ) { - return false; - } - - const corners = findCorners(e, target); - const bound = boundaries(corners, prevMousePosition, target); - - if (pointInPolygon([mousePosition.x, mousePosition.y], formatPoints(bound))) { - const dist = Math.round(distance(mousePosition, bullseye(corners, bound, mousePosition))); - return Math.max(dist, 1); - } - return false; -} diff --git a/vuu-ui/packages/vuu-popups/src/menu/aim/aim.ts b/vuu-ui/packages/vuu-popups/src/menu/aim/aim.ts new file mode 100644 index 000000000..3c33a31f3 --- /dev/null +++ b/vuu-ui/packages/vuu-popups/src/menu/aim/aim.ts @@ -0,0 +1,153 @@ +import { findCorners, boundaries, Corner } from "./corners"; + +export type Point = [number, number]; + +export type Position = { + x: number; + y: number; +}; + +export type Positions = [Position, Position]; + +export function distance(source: Position, target: Position) { + const a = source.x - target.x; + const b = source.y - target.y; + return Math.sqrt(a * a + b * b); +} + +export function pointInPolygon(point: Point, vs: Point[]) { + // ray-casting algorithm based on + // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html + + const [x, y] = point; + + let inside = false; + for (let i = 0, j = vs.length - 1; i < vs.length; j = i++) { + const xi = vs[i][0], + yi = vs[i][1]; + const xj = vs[j][0], + yj = vs[j][1]; + + const intersect = + yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; + if (intersect) inside = !inside; + } + + return inside; +} + +export type Side = + | "right" + | "top-right" + | "top" + | "top-left" + | "left" + | "bottom-left" + | "bottom" + | "bottom-right"; + +export function side(corners: [Corner, Corner] | []): Side { + if (corners[0] === "top-right" && corners[1] === "bottom-right") + return "right"; + else if (corners[0] === "top-left" && corners[1] === "bottom-right") + return "top-right"; + else if (corners[0] === "top-left" && corners[1] === "top-right") + return "top"; + else if (corners[0] === "bottom-left" && corners[1] === "top-right") + return "top-left"; + else if (corners[0] === "bottom-left" && corners[1] === "top-left") + return "left"; + else if (corners[0] === "bottom-right" && corners[1] === "top-left") + return "bottom-left"; + else if (corners[0] === "bottom-right" && corners[1] === "bottom-left") + return "bottom"; + else if (corners[0] === "top-right" && corners[1] === "bottom-left") + return "bottom-right"; + + throw Error("will never happen, typescript"); +} + +export function bullseye( + corners: [Corner, Corner] | [], + boundaries: [Position, Position], + mousePosition: Position +): Position { + switch (side(corners)) { + case "right": + return { + x: boundaries[0].x, + y: mousePosition.y, + }; + case "top-right": + return { + x: boundaries[1].x, + y: boundaries[0].y, + }; + case "top": + return { + x: mousePosition.x, + y: boundaries[0].y, + }; + case "top-left": + return { + x: boundaries[0].x, + y: boundaries[1].y, + }; + case "left": + return { + x: boundaries[0].x, + y: mousePosition.y, + }; + case "bottom-left": + return { + x: boundaries[1].x, + y: boundaries[0].y, + }; + case "bottom": + return { + x: mousePosition.x, + y: boundaries[0].y, + }; + case "bottom-right": + return { + x: boundaries[0].x, + y: boundaries[1].y, + }; + } +} + +function formatPoints(points: Position[]) { + const finalPoints: Point[] = []; + for (let i = 0, len = points.length; i < len; ++i) { + finalPoints.push([points[i].x, points[i].y]); + } + return finalPoints; +} + +export function aiming( + e: MouseEvent, + mousePosition: Position, + prevMousePosition: Position, + target: HTMLElement, + alreadyAiming: boolean +) { + if (!prevMousePosition) return false; + else if ( + !alreadyAiming && + mousePosition.x === prevMousePosition.x && + mousePosition.y === prevMousePosition.y + ) { + return false; + } + + const corners = findCorners(e, target); + const bound = boundaries(corners, prevMousePosition, target); + + if (pointInPolygon([mousePosition.x, mousePosition.y], formatPoints(bound))) { + const dist = Math.round( + distance(mousePosition, bullseye(corners, bound, mousePosition)) + ); + return Math.max(dist, 1); + } + return false; +} diff --git a/vuu-ui/packages/vuu-popups/src/menu/aim/corners.js b/vuu-ui/packages/vuu-popups/src/menu/aim/corners.js deleted file mode 100644 index 829d35481..000000000 --- a/vuu-ui/packages/vuu-popups/src/menu/aim/corners.js +++ /dev/null @@ -1,114 +0,0 @@ -import { distance, bullseye } from './aim'; - -function inside(source, targetMin, targetMax) { - if (source >= targetMin && source <= targetMax) return 0; - else if (source > targetMin) return -1; - else return 1; -} - -export function corners(source, target) { - source = { left: source.pageX, top: source.pageY }; - target = target.getBoundingClientRect(); - - let ver, hor; - - hor = inside(source.left, target.left, target.left + target.width); - ver = inside(source.top, target.top, source.top + target.height); - - if (hor === -1 && ver === -1) return ['top-right', 'bottom-left']; - if (hor === -1 && ver === 0) return ['top-right', 'bottom-right']; - if (hor === -1 && ver === 1) return ['top-left', 'bottom-right']; - - if (hor === 0 && ver === -1) return ['bottom-right', 'bottom-left']; - if (hor === 0 && ver === 0) return []; - if (hor === 0 && ver === 1) return ['top-left', 'top-right']; - - if (hor === 1 && ver === -1) return ['bottom-right', 'top-left']; - if (hor === 1 && ver === 0) return ['bottom-left', 'top-left']; - if (hor === 1 && ver === 1) return ['bottom-left', 'top-right']; -} - -export function boundaries(corners, source, target, adjustment = false) { - if (target instanceof HTMLElement || target instanceof SVGElement) { - target = target.getBoundingClientRect(); - } - - if (!source) return []; - else if (source instanceof Event) { - source = { - left: source.pageX, - top: source.pageY - }; - } else if (source.x) { - source = { - left: source.x, - top: source.y - }; - } - - let tolerance = adjustment !== false ? Math.round(adjustment / 10) * 1.5 : 0; - const position = { - left: target.left - tolerance, - top: target.top - tolerance, - width: target.width + tolerance * 2, - height: target.height + tolerance * 2 - }; - - var doc = document.documentElement; - var left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); - var top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); - - let first = true; - let positions = []; - corners.forEach((corner) => { - switch (corner) { - case 'top-right': - if (first) positions.push({ x: target.left + target.width + left, y: target.top + top }); - positions.push({ x: position.left + position.width + left, y: position.top + top }); - if (!first) positions.push({ x: target.left + target.width + left, y: target.top + top }); - break; - case 'top-left': - if (first) positions.push({ x: target.left + left, y: target.top + top }); - positions.push({ x: position.left + left, y: position.top + top }); - if (!first) positions.push({ x: target.left + left, y: target.top + top }); - break; - case 'bottom-right': - if (first) - positions.push({ - x: target.left + target.width + left, - y: target.top + target.height + top - }); - positions.push({ - x: position.left + position.width + left, - y: position.top + position.height + top - }); - if (!first) - positions.push({ - x: target.left + target.width + left, - y: target.top + target.height + top - }); - break; - case 'bottom-left': - if (first) positions.push({ x: target.left + left, y: target.top + target.height + top }); - positions.push({ x: position.left + left, y: position.top + position.height + top }); - if (!first) positions.push({ x: target.left + left, y: target.top + target.height + top }); - break; - } - if (first) { - positions.push({ x: source.left, y: source.top }); - } - first = false; - }); - - if (adjustment === false) { - const be = bullseye(corners, positions, { x: source.left, y: source.top }); - if (be) { - const dist = Math.round(distance({ x: source.left, y: source.top }, be)); - return boundaries(corners, source, target, dist); - } - } - - return positions; -} - -export default corners; diff --git a/vuu-ui/packages/vuu-popups/src/menu/aim/corners.ts b/vuu-ui/packages/vuu-popups/src/menu/aim/corners.ts new file mode 100644 index 000000000..638472bc1 --- /dev/null +++ b/vuu-ui/packages/vuu-popups/src/menu/aim/corners.ts @@ -0,0 +1,157 @@ +import { distance, bullseye, Position, Positions } from "./aim"; + +export type Corner = "top-right" | "top-left" | "bottom-right" | "bottom-left"; + +function inside(source: number, targetMin: number, targetMax: number) { + if (source >= targetMin && source <= targetMax) return 0; + else if (source > targetMin) return -1; + else return 1; +} + +export function findCorners( + sourceEvt: MouseEvent, + targetElement: HTMLElement +): [Corner, Corner] | [] { + const source = { left: sourceEvt.pageX, top: sourceEvt.pageY }; + const target = targetElement.getBoundingClientRect(); + + const hor = inside(source.left, target.left, target.left + target.width); + const ver = inside(source.top, target.top, source.top + target.height); + + if (hor === -1 && ver === -1) return ["top-right", "bottom-left"]; + if (hor === -1 && ver === 0) return ["top-right", "bottom-right"]; + if (hor === -1 && ver === 1) return ["top-left", "bottom-right"]; + + if (hor === 0 && ver === -1) return ["bottom-right", "bottom-left"]; + if (hor === 0 && ver === 0) return []; + if (hor === 0 && ver === 1) return ["top-left", "top-right"]; + + if (hor === 1 && ver === -1) return ["bottom-right", "top-left"]; + if (hor === 1 && ver === 0) return ["bottom-left", "top-left"]; + if (hor === 1 && ver === 1) return ["bottom-left", "top-right"]; + + throw Error("cannot happen"); +} + +type Source = { + left: number; + top: number; +}; + +const isSource = (source: Source | Position | MouseEvent): source is Source => + typeof (source as Source).left === "number"; +const isEvent = ( + source: Source | Position | MouseEvent +): source is MouseEvent => typeof (source as MouseEvent).pageX === "number"; + +export function boundaries( + corners: [Corner, Corner] | [], + sourcePos: Position | MouseEvent | Source, + targetElement: HTMLElement | SVGElement, + adjustment: false | number = false +): Positions { + const target = targetElement.getBoundingClientRect(); + const source = isSource(sourcePos) + ? sourcePos + : isEvent(sourcePos) + ? { + left: sourcePos.pageX, + top: sourcePos.pageY, + } + : { + left: sourcePos.x, + top: sourcePos.y, + }; + + const tolerance = + adjustment !== false ? Math.round(adjustment / 10) * 1.5 : 0; + const position = { + left: target.left - tolerance, + top: target.top - tolerance, + width: target.width + tolerance * 2, + height: target.height + tolerance * 2, + }; + + const doc = document.documentElement; + const left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); + const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); + + let first = true; + const positions: Position[] = []; + corners.forEach((corner) => { + switch (corner) { + case "top-right": + if (first) + positions.push({ + x: target.left + target.width + left, + y: target.top + top, + }); + positions.push({ + x: position.left + position.width + left, + y: position.top + top, + }); + if (!first) + positions.push({ + x: target.left + target.width + left, + y: target.top + top, + }); + break; + case "top-left": + if (first) + positions.push({ x: target.left + left, y: target.top + top }); + positions.push({ x: position.left + left, y: position.top + top }); + if (!first) + positions.push({ x: target.left + left, y: target.top + top }); + break; + case "bottom-right": + if (first) + positions.push({ + x: target.left + target.width + left, + y: target.top + target.height + top, + }); + positions.push({ + x: position.left + position.width + left, + y: position.top + position.height + top, + }); + if (!first) + positions.push({ + x: target.left + target.width + left, + y: target.top + target.height + top, + }); + break; + case "bottom-left": + if (first) + positions.push({ + x: target.left + left, + y: target.top + target.height + top, + }); + positions.push({ + x: position.left + left, + y: position.top + position.height + top, + }); + if (!first) + positions.push({ + x: target.left + left, + y: target.top + target.height + top, + }); + break; + } + if (first) { + positions.push({ x: source.left, y: source.top }); + } + first = false; + }); + + if (adjustment === false) { + const be = bullseye(corners, positions as Positions, { + x: source.left, + y: source.top, + }); + if (be) { + const dist = Math.round(distance({ x: source.left, y: source.top }, be)); + return boundaries(corners, source, targetElement, dist); + } + } + + return positions as Positions; +} diff --git a/vuu-ui/packages/vuu-popups/src/menu/aim/point-in-polygon.js b/vuu-ui/packages/vuu-popups/src/menu/aim/point-in-polygon.js deleted file mode 100644 index 211448662..000000000 --- a/vuu-ui/packages/vuu-popups/src/menu/aim/point-in-polygon.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @license MIT - * @url https://github.com/substack/point-in-polygon - */ - -export default function (point, vs) { - // ray-casting algorithm based on - // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html - - var x = point[0], - y = point[1]; - - var inside = false; - for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) { - var xi = vs[i][0], - yi = vs[i][1]; - var xj = vs[j][0], - yj = vs[j][1]; - - var intersect = yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; - if (intersect) inside = !inside; - } - - return inside; -} diff --git a/vuu-ui/packages/vuu-popups/src/menu/aim/utils.js b/vuu-ui/packages/vuu-popups/src/menu/aim/utils.ts similarity index 91% rename from vuu-ui/packages/vuu-popups/src/menu/aim/utils.js rename to vuu-ui/packages/vuu-popups/src/menu/aim/utils.ts index 72b5f333b..492483023 100644 --- a/vuu-ui/packages/vuu-popups/src/menu/aim/utils.js +++ b/vuu-ui/packages/vuu-popups/src/menu/aim/utils.ts @@ -9,7 +9,7 @@ function scrollPosition() { return { scrollTop, scrollLeft }; } -export function mousePosition(event) { +export function mousePosition(event: MouseEvent) { const sPos = scrollPosition(); const x = document.all ? event.clientX + sPos.scrollLeft : event.pageX; diff --git a/vuu-ui/packages/vuu-popups/src/menu/apply-handlers.js b/vuu-ui/packages/vuu-popups/src/menu/apply-handlers.js deleted file mode 100644 index 6ee441da8..000000000 --- a/vuu-ui/packages/vuu-popups/src/menu/apply-handlers.js +++ /dev/null @@ -1,15 +0,0 @@ -export const applyHandlers = (props, evtName, ...params) => { - const isPropagationStopped = () => params?.[0].isPropagationStopped?.(); - - if (props.length > 0 && !isPropagationStopped()) { - const additionalHandlers = props - .filter((props) => props[evtName]) - .map((props) => props[evtName]); - for (let handleEvent of additionalHandlers) { - if (isPropagationStopped()) { - break; - } - handleEvent(...params); - } - } -}; diff --git a/vuu-ui/packages/vuu-popups/src/menu/context-menu-provider.jsx b/vuu-ui/packages/vuu-popups/src/menu/context-menu-provider.jsx deleted file mode 100644 index e291e77d2..000000000 --- a/vuu-ui/packages/vuu-popups/src/menu/context-menu-provider.jsx +++ /dev/null @@ -1,145 +0,0 @@ -import React, { useCallback, useContext, useMemo } from "react"; -import { PopupService } from "../popup"; -import { ContextMenu } from "./ContextMenu"; -import { MenuItem, MenuItemGroup } from "./MenuList"; - -const showContextMenu = (e, menuDescriptors, handleContextMenuAction) => { - const { clientX: left, clientY: top } = e; - const menuItems = (menuDescriptors) => { - const fromDescriptor = ({ children, label, icon, action, options }, i) => - children ? ( - - {children.map(fromDescriptor)} - - ) : ( - - {label} - - ); - - return menuDescriptors.map(fromDescriptor); - }; - - const handleClose = (menuId, options) => { - if (menuId) { - handleContextMenuAction(menuId, options); - PopupService.hidePopup(); - } - }; - - const component = ( - - {menuItems(menuDescriptors)} - - ); - PopupService.showPopup({ left: 0, top: 0, component }); -}; - -export const ContextMenuContext = React.createContext(null); - -const NO_INHERITED_CONTEXT = { - menuItemDescriptors: [], -}; - -// The menuBuilder will always be supplied by the code that will display the local -// context menu. It will be passed all configured menu descriptors. It is free to -// augment, replace or ignore the existing menu descriptors. -export const useContextMenu = () => { - const { menuActionHandler, menuBuilders } = useContext(ContextMenuContext); - - const buildMenuOptions = useCallback((menuBuilders, location, options) => { - let results = []; - for (const menuBuilder of menuBuilders) { - // Maybe we should leave the concatenation to the menuBuilder, then it can control menuItem order - results = results.concat(menuBuilder(location, options)); - } - return results; - }, []); - - const handleShowContextMenu = (e, location, options) => { - e.stopPropagation(); - e.preventDefault(); - const menuItemDescriptors = buildMenuOptions( - menuBuilders, - location, - options - ); - if (menuItemDescriptors.length) { - showContextMenu(e, menuItemDescriptors, menuActionHandler); - } - }; - - return handleShowContextMenu; -}; - -const Provider = ({ - children, - context: { - menuBuilders: inheritedMenuBuilders, - menuActionHandler: inheritedMenuActionHandler, - }, - menuActionHandler, - menuBuilder, -}) => { - const menuBuilders = useMemo(() => { - if (inheritedMenuBuilders && menuBuilder) { - return inheritedMenuBuilders.concat(menuBuilder); - } else if (menuBuilder) { - return [menuBuilder]; - } else { - return inheritedMenuBuilders || []; - } - }, [inheritedMenuBuilders, menuBuilder]); - - const handleMenuAction = useCallback( - (type, options) => { - if (menuActionHandler && menuActionHandler(type, options)) { - return true; - } - - if ( - inheritedMenuActionHandler && - inheritedMenuActionHandler(type, options) - ) { - return true; - } - }, - [inheritedMenuActionHandler, menuActionHandler] - ); - - return ( - - {children} - - ); -}; - -// Need an option for local menu to override higher-level menu, rather than extend -export const ContextMenuProvider = ({ - children, - menuActionHandler, - menuBuilder, - menuItemDescriptors, - label, -}) => { - return ( - - {(parentContext) => ( - - {children} - - )} - - ); -}; diff --git a/vuu-ui/packages/vuu-popups/src/menu/context-menu-provider.tsx b/vuu-ui/packages/vuu-popups/src/menu/context-menu-provider.tsx new file mode 100644 index 000000000..488cb2f99 --- /dev/null +++ b/vuu-ui/packages/vuu-popups/src/menu/context-menu-provider.tsx @@ -0,0 +1,103 @@ +import { createContext, ReactNode, useCallback, useMemo } from "react"; + +export type MenuActionHandler = ( + type: string, + options: unknown +) => boolean | undefined; +export type MenuBuilder = ( + location: L, + options: O +) => ContextMenuItemDescriptor[]; + +export interface ContextMenuContext { + menuBuilders: MenuBuilder[]; + menuActionHandler: MenuActionHandler; +} + +export const ContextMenuContext = createContext( + null +); + +export type ContextMenuItemDescriptor = { + action: string; + children?: ContextMenuItemDescriptor[]; + icon?: string; + label: string; + location?: string; + options?: unknown; +}; + +export interface ContextMenuProviderProps { + children: ReactNode; + label?: string; + menuActionHandler?: MenuActionHandler; + menuBuilder: MenuBuilder; +} + +interface ProviderProps extends ContextMenuProviderProps { + context: ContextMenuContext | null; +} + +const Provider = ({ + children, + context, + menuActionHandler, + menuBuilder, +}: ProviderProps) => { + const menuBuilders = useMemo(() => { + if (context?.menuBuilders && menuBuilder) { + return context.menuBuilders.concat(menuBuilder); + } else if (menuBuilder) { + return [menuBuilder]; + } else { + return context?.menuBuilders || []; + } + }, [context, menuBuilder]); + + const handleMenuAction = useCallback( + (type, options) => { + if (menuActionHandler?.(type, options)) { + return true; + } + + if (context?.menuActionHandler?.(type, options)) { + return true; + } + }, + [context, menuActionHandler] + ); + + return ( + + {children} + + ); +}; + +// Need an option for local menu to override higher-level menu, rather than extend +export const ContextMenuProvider = ({ + children, + label, + menuActionHandler, + menuBuilder, +}: ContextMenuProviderProps) => { + return ( + + {(parentContext) => ( + + {children} + + )} + + ); +}; diff --git a/vuu-ui/packages/vuu-popups/src/menu/index.js b/vuu-ui/packages/vuu-popups/src/menu/index.ts similarity index 74% rename from vuu-ui/packages/vuu-popups/src/menu/index.js rename to vuu-ui/packages/vuu-popups/src/menu/index.ts index 5f3dac331..c2c3876a1 100644 --- a/vuu-ui/packages/vuu-popups/src/menu/index.js +++ b/vuu-ui/packages/vuu-popups/src/menu/index.ts @@ -1,3 +1,4 @@ export * from "./ContextMenu"; export * from "./MenuList"; export * from "./context-menu-provider"; +export * from "./useContextMenu"; diff --git a/vuu-ui/packages/vuu-popups/src/menu/key-code.js b/vuu-ui/packages/vuu-popups/src/menu/key-code.js deleted file mode 100644 index 25d426042..000000000 --- a/vuu-ui/packages/vuu-popups/src/menu/key-code.js +++ /dev/null @@ -1,61 +0,0 @@ -function union(set1, ...sets) { - const result = new Set(set1); - for (let set of sets) { - for (let element of set) { - result.add(element); - } - } - return result; -} - -export const ArrowUp = 'ArrowUp'; -export const ArrowDown = 'ArrowDown'; -export const ArrowLeft = 'ArrowLeft'; -export const Backspace = 'Backspace'; -export const ArrowRight = 'ArrowRight'; -export const Enter = 'Enter'; -export const Escape = 'Escape'; -export const Delete = 'Delete'; - -const actionKeys = new Set([Enter, Delete]); -const focusKeys = new Set(['Tab']); -// const navigationKeys = new Set(["Home", "End", "ArrowRight", "ArrowLeft","ArrowDown", "ArrowUp"]); -const arrowLeftRightKeys = new Set(['ArrowRight', 'ArrowLeft']); -const verticalNavigationKeys = new Set(['Home', 'End', 'ArrowDown', 'ArrowUp']); -const horizontalNavigationKeys = new Set(['Home', 'End', 'ArrowRight', 'ArrowLeft']); -const functionKeys = new Set([ - 'F1', - 'F2', - 'F3', - 'F4', - 'F5', - 'F6', - 'F7', - 'F8', - 'F9', - 'F10', - 'F11', - 'F12' -]); -const specialKeys = union( - actionKeys, - horizontalNavigationKeys, - verticalNavigationKeys, - arrowLeftRightKeys, - functionKeys, - focusKeys -); -export const isCharacterKey = (evt) => { - if (specialKeys.has(evt.key)) { - return false; - } - if (typeof evt.which === 'number' && evt.which > 0) { - return !evt.ctrlKey && !evt.metaKey && !evt.altKey && evt.which !== 8; - } -}; - -export const isNavigationKey = ({ key }, orientation = 'vertical') => { - const navigationKeys = - orientation === 'vertical' ? verticalNavigationKeys : horizontalNavigationKeys; - return navigationKeys.has(key); -}; diff --git a/vuu-ui/packages/vuu-popups/src/menu/key-code.ts b/vuu-ui/packages/vuu-popups/src/menu/key-code.ts new file mode 100644 index 000000000..188b725f3 --- /dev/null +++ b/vuu-ui/packages/vuu-popups/src/menu/key-code.ts @@ -0,0 +1,71 @@ +function union(set1: Set, ...sets: Set[]) { + const result = new Set(set1); + for (const set of sets) { + for (const element of set) { + result.add(element); + } + } + return result; +} + +export const ArrowUp = "ArrowUp"; +export const ArrowDown = "ArrowDown"; +export const ArrowLeft = "ArrowLeft"; +export const Backspace = "Backspace"; +export const ArrowRight = "ArrowRight"; +export const Enter = "Enter"; +export const Escape = "Escape"; +export const Delete = "Delete"; + +const actionKeys = new Set([Enter, Delete]); +const focusKeys = new Set(["Tab"]); +// const navigationKeys = new Set(["Home", "End", "ArrowRight", "ArrowLeft","ArrowDown", "ArrowUp"]); +const arrowLeftRightKeys = new Set(["ArrowRight", "ArrowLeft"]); +const verticalNavigationKeys = new Set(["Home", "End", "ArrowDown", "ArrowUp"]); +const horizontalNavigationKeys = new Set([ + "Home", + "End", + "ArrowRight", + "ArrowLeft", +]); +const functionKeys = new Set([ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12", +]); +const specialKeys = union( + actionKeys, + horizontalNavigationKeys, + verticalNavigationKeys, + arrowLeftRightKeys, + functionKeys, + focusKeys +); +export const isCharacterKey = (evt: KeyboardEvent) => { + if (specialKeys.has(evt.key)) { + return false; + } + if (typeof evt.which === "number" && evt.which > 0) { + return !evt.ctrlKey && !evt.metaKey && !evt.altKey && evt.which !== 8; + } +}; + +export const isNavigationKey = ( + { key }: { key: string }, + orientation = "vertical" +) => { + const navigationKeys = + orientation === "vertical" + ? verticalNavigationKeys + : horizontalNavigationKeys; + return navigationKeys.has(key); +}; diff --git a/vuu-ui/packages/vuu-popups/src/menu/list-dom-utils.js b/vuu-ui/packages/vuu-popups/src/menu/list-dom-utils.js deleted file mode 100644 index fd9621e5a..000000000 --- a/vuu-ui/packages/vuu-popups/src/menu/list-dom-utils.js +++ /dev/null @@ -1,22 +0,0 @@ -export const listItemElement = (listEl, listItemIdx) => - listEl.querySelector(`:scope > [data-idx="${listItemIdx}"]`); - -export function listItemIndex(listItemEl) { - if (listItemEl) { - let idx = listItemEl.dataset.idx; - if (idx) { - return parseInt(idx, 10); - // eslint-disable-next-line no-cond-assign - } else if ((idx = listItemEl.ariaPosInSet)) { - return parseInt(idx, 10) - 1; - } - } -} - -export const listItemId = (el) => el?.id; - -export const closestListItem = (el) => el.closest('[data-idx],[aria-posinset]'); - -export const closestListItemId = (el) => listItemId(closestListItem(el)); - -export const closestListItemIndex = (el) => listItemIndex(closestListItem(el)); diff --git a/vuu-ui/packages/vuu-popups/src/menu/list-dom-utils.ts b/vuu-ui/packages/vuu-popups/src/menu/list-dom-utils.ts new file mode 100644 index 000000000..e34cd4157 --- /dev/null +++ b/vuu-ui/packages/vuu-popups/src/menu/list-dom-utils.ts @@ -0,0 +1,25 @@ +// const listItemElement = (listEl: HTMLElement, listItemIdx: number) => +// listEl.querySelector(`:scope > [data-idx="${listItemIdx}"]`); + +export function listItemIndex(listItemEl: HTMLElement) { + if (listItemEl) { + const idx = listItemEl.dataset.idx; + if (idx) { + return parseInt(idx, 10); + // eslint-disable-next-line no-cond-assign + } else if (listItemEl.ariaPosInSet) { + return parseInt(listItemEl.ariaPosInSet, 10) - 1; + } + } +} + +const listItemId = (el: HTMLElement | null | undefined) => el?.id; + +export const closestListItem = (el: HTMLElement | null | undefined) => + el?.closest("[data-idx],[aria-posinset]") as HTMLElement; + +export const closestListItemId = (el: HTMLElement) => + listItemId(closestListItem(el)); + +export const closestListItemIndex = (el: HTMLElement) => + listItemIndex(closestListItem(el)); diff --git a/vuu-ui/packages/vuu-popups/src/menu/use-cascade.js b/vuu-ui/packages/vuu-popups/src/menu/use-cascade.ts similarity index 70% rename from vuu-ui/packages/vuu-popups/src/menu/use-cascade.js rename to vuu-ui/packages/vuu-popups/src/menu/use-cascade.ts index 39c6355ea..36e921c28 100644 --- a/vuu-ui/packages/vuu-popups/src/menu/use-cascade.js +++ b/vuu-ui/packages/vuu-popups/src/menu/use-cascade.ts @@ -1,10 +1,22 @@ -import { useCallback, useMemo, useRef, useState } from "react"; +import { + MouseEvent, + SyntheticEvent, + useCallback, + useMemo, + useRef, + useState, +} from "react"; import { closestListItem, listItemIndex } from "./list-dom-utils"; +import { MenuItemProps } from "./MenuList"; // import {mousePosition} from './aim/utils'; // import {aiming} from './aim/aim'; -const nudge = (menus, distance, pos) => { +const nudge = ( + menus: RuntimeMenuDescriptor[], + distance: number, + pos: "left" | "top" +) => { return menus.map((m, i) => i === menus.length - 1 ? { @@ -14,12 +26,17 @@ const nudge = (menus, distance, pos) => { : m ); }; -const nudgeLeft = (menus, distance) => nudge(menus, distance, "left"); -const nudgeUp = (menus, distance) => nudge(menus, distance, "top"); +const nudgeLeft = (menus: RuntimeMenuDescriptor[], distance: number) => + nudge(menus, distance, "left"); +const nudgeUp = (menus: RuntimeMenuDescriptor[], distance: number) => + nudge(menus, distance, "top"); -const flipSides = (id, menus) => { +const flipSides = (id: string, menus: RuntimeMenuDescriptor[]) => { const [parentMenu, menu] = menus.slice(-2); const el = document.getElementById(`${id}-${menu.id}`); + if (el === null) { + throw Error(`useCascade.flipSides element with id ${menu.id} not found`); + } const { width } = el.getBoundingClientRect(); return menus.map((m) => m === menu @@ -31,9 +48,9 @@ const flipSides = (id, menus) => { ); }; -const closedNode = (el) => - el.ariaHasPopup === "true" && el.ariaExpanded !== "true"; -const getPosition = (el, openMenus) => { +// const closedNode = (el: HTMLElement) => +// el.ariaHasPopup === "true" && el.ariaExpanded !== "true"; +const getPosition = (el: HTMLElement, openMenus: RuntimeMenuDescriptor[]) => { const [{ left, top: menuTop }] = openMenus.slice(-1); // const {top, right, bottom, left} = el.getBoundingClientRect(); // this will not work for MenuList within window, we need the @@ -42,18 +59,24 @@ const getPosition = (el, openMenus) => { return { left: left + width, top: top + menuTop }; }; -export const getItemId = (id) => { - let pos = id.lastIndexOf("-"); +export type RuntimeMenuDescriptor = { + id: string; + left: number; + top: number; +}; + +export const getItemId = (id: string) => { + const pos = id.lastIndexOf("-"); return pos === -1 ? id : id.slice(pos + 1); }; -export const getMenuId = (id) => { +export const getMenuId = (id: string) => { const itemId = getItemId(id); const pos = itemId.lastIndexOf("."); return pos > -1 ? itemId.slice(0, pos) : "root"; }; -const getMenuDepth = (id) => { +const getMenuDepth = (id: string) => { let count = 0, pos = id.indexOf(".", 0); while (pos !== -1) { @@ -62,31 +85,52 @@ const getMenuDepth = (id) => { } return count; }; -const identifyItem = (el) => [ - getMenuId(el.id), - getItemId(el.id), - el.ariaHasPopup === "true", - el.ariaExpanded === "true", - getMenuDepth(el.id), -]; + +const identifyItem = (el: HTMLElement) => ({ + menuId: getMenuId(el.id), + itemId: getItemId(el.id), + isGroup: el.ariaHasPopup === "true", + isOpen: el.ariaExpanded === "true", + level: getMenuDepth(el.id), +}); + +export interface CascadeHookProps { + id: string; + onActivate: (menuId: string) => void; + onMouseEnterItem: (evt: MouseEvent, itemId: string) => void; + position: { x: number; y: number }; +} + +export interface CascadeHooksResult { + closeMenu: () => void; + handleRender: () => void; + listItemProps: Partial; + openMenu: (menuId?: string, itemId?: string) => void; + openMenus: RuntimeMenuDescriptor[]; +} + +type MenuStatus = "no-popup" | "popup-open" | "pending-close" | "popup-pending"; +type MenuState = { [key: string]: MenuStatus }; export const useCascade = ({ id, onActivate, onMouseEnterItem, position: { x: posX, y: posY }, -}) => { +}: CascadeHookProps): CascadeHooksResult => { const [, forceRefresh] = useState({}); - const openMenus = useRef([{ id: "root", left: posX, top: posY }]); + const openMenus = useRef([ + { id: "root", left: posX, top: posY }, + ]); - const setOpenMenus = useCallback((menus) => { + const setOpenMenus = useCallback((menus: RuntimeMenuDescriptor[]) => { openMenus.current = menus; forceRefresh({}); }, []); - const menuOpenPendingTimeout = useRef(null); - const menuClosePendingTimeout = useRef(null); - const menuState = useRef({ root: "no-popup" }); + const menuOpenPendingTimeout = useRef(); + const menuClosePendingTimeout = useRef(); + const menuState = useRef({ root: "no-popup" }); const prevLevel = useRef(0); // const prevAim = useRef({mousePos: null, distance: true}); @@ -107,7 +151,7 @@ export const useCascade = ({ ); const closeMenu = useCallback( - (menuId) => { + (menuId?: string) => { if (menuId === "root") { setOpenMenus([]); } else { @@ -140,7 +184,7 @@ export const useCascade = ({ if (menuOpenPendingTimeout.current) { clearTimeout(menuOpenPendingTimeout.current); } - menuOpenPendingTimeout.current = setTimeout(() => { + menuOpenPendingTimeout.current = window.setTimeout(() => { console.log(`scheduleOpen timed out opening ${itemId}`); closeMenus(menuId, itemId); menuState.current[menuId] = "popup-open"; @@ -157,7 +201,7 @@ export const useCascade = ({ `scheduleClose openMenuId ${openMenuId} menuId ${menuId} itemId ${itemId}` ); menuState.current[openMenuId] = "pending-close"; - menuClosePendingTimeout.current = setTimeout(() => { + menuClosePendingTimeout.current = window.setTimeout(() => { closeMenus(menuId, itemId); }, 400); }, @@ -184,11 +228,11 @@ export const useCascade = ({ } }, [id, setOpenMenus]); - const listItemProps = useMemo( + const listItemProps: Partial = useMemo( () => ({ - onMouseEnter: (evt) => { - const listItemEl = closestListItem(evt.target); - const [menuId, itemId, isGroup, isOpen, level] = + onMouseEnter: (evt: MouseEvent) => { + const listItemEl = closestListItem(evt.target as HTMLElement); + const { menuId, itemId, isGroup, isOpen, level } = identifyItem(listItemEl); const sameLevel = prevLevel.current === level; const { @@ -211,7 +255,7 @@ export const useCascade = ({ } else if (state === "popup-pending" && !isGroup) { menuState.current[menuId] = "no-popup"; clearTimeout(menuOpenPendingTimeout.current); - menuOpenPendingTimeout.current = null; + menuOpenPendingTimeout.current = undefined; } else if (state === "popup-pending" && isGroup) { clearTimeout(menuOpenPendingTimeout.current); scheduleOpen(menuId, itemId, listItemEl); @@ -248,20 +292,24 @@ export const useCascade = ({ if (state === "pending-close") { if (menuOpenPendingTimeout.current) { clearTimeout(menuOpenPendingTimeout.current); - menuOpenPendingTimeout.current = null; + menuOpenPendingTimeout.current = undefined; } clearTimeout(menuClosePendingTimeout.current); - menuClosePendingTimeout.current = null; + menuClosePendingTimeout.current = undefined; menuState.current[menuId] = "popup-open"; } onMouseEnterItem(evt, itemId); }, - onClick: (evt) => { - const listItemEl = closestListItem(evt.target); + onClick: (evt: SyntheticEvent) => { + const targetElement = evt.target as HTMLElement; + const listItemEl = closestListItem(targetElement); const idx = listItemIndex(listItemEl); - if (closedNode(listItemEl).ariaHasPopup === "true") { + console.log( + `list item click [${idx}] hasPopup ${listItemEl.ariaHasPopup}` + ); + if (listItemEl.ariaHasPopup === "true") { if (listItemEl.ariaExpanded !== "true") { openMenu(idx); } else { @@ -275,6 +323,7 @@ export const useCascade = ({ [ closeMenus, onActivate, + onMouseEnterItem, openMenu, scheduleClose, diff --git a/vuu-ui/packages/vuu-popups/src/menu/use-click-away.js b/vuu-ui/packages/vuu-popups/src/menu/use-click-away.js deleted file mode 100644 index 5002d6e65..000000000 --- a/vuu-ui/packages/vuu-popups/src/menu/use-click-away.js +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffect } from 'react'; - -export const useClickAway = ({ containerClassName, isOpen, onClose }) => { - useEffect(() => { - const clickHandler = isOpen - ? (evt) => { - const container = evt.target.closest(`.${containerClassName}`); - if (container === null) { - onClose('root'); - } - } - : null; - - document.body.addEventListener('click', clickHandler, true); - - return () => { - if (clickHandler) { - document.body.removeEventListener('click', clickHandler, true); - } - }; - }, [containerClassName, isOpen, onClose]); -}; diff --git a/vuu-ui/packages/vuu-popups/src/menu/use-click-away.ts b/vuu-ui/packages/vuu-popups/src/menu/use-click-away.ts new file mode 100644 index 000000000..6c1d67f4a --- /dev/null +++ b/vuu-ui/packages/vuu-popups/src/menu/use-click-away.ts @@ -0,0 +1,34 @@ +import { useEffect } from "react"; + +export interface ClickAwayHookProps { + containerClassName: string; + isOpen: boolean; + onClose?: (target: string) => void; +} + +export const useClickAway = ({ + containerClassName, + isOpen, + onClose, +}: ClickAwayHookProps) => { + useEffect(() => { + let clickHandler: (evt: MouseEvent) => void; + if (isOpen) { + clickHandler = (evt) => { + const target = evt.target as HTMLElement; + const container = target.closest(`.${containerClassName}`); + if (container === null) { + onClose?.("root"); + } + }; + + document.body.addEventListener("click", clickHandler, true); + } + + return () => { + if (clickHandler) { + document.body.removeEventListener("click", clickHandler, true); + } + }; + }, [containerClassName, isOpen, onClose]); +}; diff --git a/vuu-ui/packages/vuu-popups/src/menu/use-items-with-ids.js b/vuu-ui/packages/vuu-popups/src/menu/use-items-with-ids.ts similarity index 61% rename from vuu-ui/packages/vuu-popups/src/menu/use-items-with-ids.js rename to vuu-ui/packages/vuu-popups/src/menu/use-items-with-ids.ts index dcdec77a6..7567955af 100644 --- a/vuu-ui/packages/vuu-popups/src/menu/use-items-with-ids.js +++ b/vuu-ui/packages/vuu-popups/src/menu/use-items-with-ids.ts @@ -1,22 +1,23 @@ -import React, { useCallback, useMemo } from "react"; +import React, { ReactElement, useCallback, useMemo } from "react"; import { MenuItemGroup, Separator } from "./MenuList"; -export const isMenuItemGroup = (child) => +export const isMenuItemGroup = (child: ReactElement) => child.type === MenuItemGroup || !!child.props["data-group"]; -export const useItemsWithIds = (sourceProp, childrenProp) => { - const normalizeChildren = useCallback(() => { - if (childrenProp === undefined) { - return; - } +type Menus = { [key: string]: ReactElement[] }; +type Actions = { [key: string]: { action: string; options?: unknown } }; +export const useItemsWithIds = ( + childrenProp: ReactElement[] +): [Menus, Actions] => { + const normalizeChildren = useCallback(() => { const collectChildren = ( - children, + children: ReactElement[], path = "root", - menus = {}, - actions = {} + menus: Menus = {}, + actions: Actions = {} ) => { - const list = (menus[path] = []); + const list: ReactElement[] = (menus[path] = []); let idx = 0; let hasSeparator = false; @@ -29,7 +30,7 @@ export const useItemsWithIds = (sourceProp, childrenProp) => { const { props: { action, options }, } = child; - const [childWithId, grandChildren] = assignId( + const { childWithId, grandChildren } = assignId( child, childPath, group, @@ -48,28 +49,33 @@ export const useItemsWithIds = (sourceProp, childrenProp) => { return [menus, actions]; }; - const assignId = (child, path, group, hasSeparator = false) => { + const assignId = ( + child: ReactElement, + path: string, + group: boolean, + hasSeparator = false + ) => { const { props: { children }, } = child; - return [ - React.cloneElement(child, { + return { + childWithId: React.cloneElement(child, { hasSeparator, id: `${path}`, key: path, children: group ? undefined : children, }), - group ? children : undefined, - ]; + grandChildren: group ? children : undefined, + }; }; return collectChildren(childrenProp); }, [childrenProp]); - const [children, actions] = useMemo( + const [menus, actions] = useMemo( () => normalizeChildren(), [normalizeChildren] ); - return [children, actions]; + return [menus, actions] as [Menus, Actions]; }; diff --git a/vuu-ui/packages/vuu-popups/src/menu/use-keyboard-navigation.js b/vuu-ui/packages/vuu-popups/src/menu/use-keyboard-navigation.js deleted file mode 100644 index 660be0424..000000000 --- a/vuu-ui/packages/vuu-popups/src/menu/use-keyboard-navigation.js +++ /dev/null @@ -1,162 +0,0 @@ -import { useCallback, useRef, useState } from "react"; -import { hasPopup, isRoot } from "./utils"; -import { applyHandlers } from "./apply-handlers"; -import { isNavigationKey } from "./key-code"; - -// we need a way to set highlightedIdx when selection changes -export const useKeyboardNavigation = ( - { - autoHighlightFirstItem = false, - count, - highlightedIdx: highlightedIdxProp, - onActivate, - onHighlight, - onKeyDown, - onCloseMenu, - onOpenMenu, - }, - ...additionalHandlers -) => { - // const prevCount = useRef(count); - const highlightedIndexRef = useRef( - highlightedIdxProp ?? autoHighlightFirstItem ? 0 : -1 - ); - const [, forceRefresh] = useState(null); - const controlledHighlighting = highlightedIdxProp !== undefined; - - // count will not work for this, as it will change when we expand collapse groups - // if (count !== prevCount.current) { - // prevCount.current = count; - // if (highlightedIndexRef.current !== -1){ - // highlightedIndexRef.current = autoHighlightFirstItem ? 0 : -1; - // } - // } - - const setHighlightedIndex = useCallback( - (idx) => { - highlightedIndexRef.current = idx; - onHighlight && onHighlight(idx); - applyHandlers(additionalHandlers, "onHighlight", idx); - forceRefresh({}); - }, - [additionalHandlers, onHighlight] - ); - - // does this belong here or should it be a method passed in? - const keyBoardNavigation = useRef(true); - const ignoreFocus = useRef(false); - const setIgnoreFocus = (value) => (ignoreFocus.current = value); - - const hiliteItemAtIndex = useCallback( - (idx) => { - if (idx !== highlightedIndexRef.current) { - if (!controlledHighlighting) { - setHighlightedIndex(idx); - } - } - }, - [controlledHighlighting, setHighlightedIndex] - ); - - const highlightedIdx = controlledHighlighting - ? highlightedIdxProp - : highlightedIndexRef.current; - - const listProps = { - onFocus: () => { - if (highlightedIdx === -1) { - setHighlightedIndex(0); - } - }, - onKeyDown: (e) => { - if (isNavigationKey(e)) { - e.preventDefault(); - e.stopPropagation(); - keyBoardNavigation.current = true; - navigateChildldItems(e); - } else if ( - (e.key === "ArrowRight" || e.key === "Enter") && - hasPopup(e.target, highlightedIdx) - ) { - onOpenMenu(highlightedIdx); - } else if (e.key === "ArrowLeft" && !isRoot(e.target)) { - onCloseMenu(highlightedIdx); - } else if (e.key === "Enter") { - onActivate && onActivate(highlightedIdx); - } - // Is there any harm in allowing other keyDown Handlers to fire ? - // TODO this is out of date - use additionalHandlers - if (Array.isArray(onKeyDown)) { - for (let handleEvent of onKeyDown) { - if (e.isPropagationStopped()) { - break; - } - handleEvent(e); - } - } else if (onKeyDown && !e.isPropagationStopped()) { - onKeyDown(e); - } - - applyHandlers(additionalHandlers, "onKeyDown", e); - }, - onMouseDownCapture: () => { - keyBoardNavigation.current = false; - setIgnoreFocus(true); - }, - - // onMouseEnter would seem less expensive but it misses some cases - onMouseMove: () => { - if (keyBoardNavigation.current) { - keyBoardNavigation.current = false; - } - }, - onMouseLeave: () => { - // label === 'ParsedInput' && console.log(`%c[useKeyboardNavigationHook]<${label}> onMouseLeave`,'color:brown') - keyBoardNavigation.current = true; - setIgnoreFocus(false); - hiliteItemAtIndex(-1); - }, - }; - - const navigateChildldItems = (e) => { - const nextIdx = nextItemIdx(count, e.key, highlightedIndexRef.current); - if (nextIdx !== highlightedIndexRef.current) { - hiliteItemAtIndex(nextIdx); - applyHandlers(additionalHandlers, "onKeyboardNavigation", e, nextIdx); - } - }; - - // label === 'ParsedInput' && console.log(`%cuseNavigationHook<${label}> - // highlightedIdxProp= ${highlightedIdxProp}, - // highlightedIndexRef= ${highlightedIndexRef.current}, - // %chighlightedIdx= ${highlightedIdx}`, 'color: brown','color: brown;font-weight: bold;') - - return { - focusVisible: keyBoardNavigation.current ? highlightedIdx : -1, - controlledHighlighting, - highlightedIdx, - hiliteItemAtIndex, - keyBoardNavigation, - listProps, - setIgnoreFocus, - }; -}; - -// need to be able to accommodate disabled items -function nextItemIdx(count, key, idx) { - if (key === "Up") { - if (idx > 0) { - return idx - 1; - } else { - return idx; - } - } else { - if (idx === null) { - return 0; - } else if (idx === count - 1) { - return idx; - } else { - return idx + 1; - } - } -} diff --git a/vuu-ui/packages/vuu-popups/src/menu/use-keyboard-navigation.ts b/vuu-ui/packages/vuu-popups/src/menu/use-keyboard-navigation.ts new file mode 100644 index 000000000..68f56c59a --- /dev/null +++ b/vuu-ui/packages/vuu-popups/src/menu/use-keyboard-navigation.ts @@ -0,0 +1,203 @@ +import { + FocusEvent, + KeyboardEvent, + useCallback, + useMemo, + useRef, + useState, +} from "react"; +import { hasPopup, isRoot } from "./utils"; +import { isNavigationKey } from "./key-code"; + +export interface KeyboardNavigationProps { + autoHighlightFirstItem?: boolean; + count: number; + highlightedIndex?: number; + onActivate: (idx: number) => void; + onHighlight?: (idx: number) => void; + onCloseMenu: (idx: number) => void; + onOpenMenu: (idx: number) => void; +} + +export interface KeyboardHookListProps { + // onBlur: (evt: FocusEvent) => void; + onFocus: (evt: FocusEvent) => void; + onKeyDown: (evt: KeyboardEvent) => void; + onMouseDownCapture: () => void; + onMouseMove: () => void; + onMouseLeave: () => void; +} + +export interface NavigationHookResult { + focusVisible: number; + controlledHighlighting: boolean; + highlightedIndex: number; + setHighlightedIndex: (idx: number) => void; + // keyboardNavigation: RefObject; + listProps: KeyboardHookListProps; + setIgnoreFocus: (ignoreFocus: boolean) => void; +} + +// we need a way to set highlightedIdx when selection changes +export const useKeyboardNavigation = ({ + autoHighlightFirstItem = false, + count, + highlightedIndex: highlightedIndexProp, + onActivate, + onHighlight, + // onKeyDown, + onCloseMenu, + onOpenMenu, +}: KeyboardNavigationProps): NavigationHookResult => { + // const prevCount = useRef(count); + const highlightedIndexRef = useRef( + highlightedIndexProp ?? autoHighlightFirstItem ? 0 : -1 + ); + const [, forceRender] = useState(null); + const controlledHighlighting = highlightedIndexProp !== undefined; + + // count will not work for this, as it will change when we expand collapse groups + // if (count !== prevCount.current) { + // prevCount.current = count; + // if (highlightedIndexRef.current !== -1){ + // highlightedIndexRef.current = autoHighlightFirstItem ? 0 : -1; + // } + // } + + const setHighlightedIdx = useCallback( + (idx) => { + highlightedIndexRef.current = idx; + onHighlight?.(idx); + forceRender({}); + }, + [onHighlight] + ); + + const setHighlightedIndex = useCallback( + (idx) => { + if (idx !== highlightedIndexRef.current) { + if (!controlledHighlighting) { + setHighlightedIdx(idx); + } + } + }, + [controlledHighlighting, setHighlightedIdx] + ); + + // does this belong here or should it be a method passed in? + const keyBoardNavigation = useRef(true); + const ignoreFocus = useRef(false); + const setIgnoreFocus = (value: boolean) => (ignoreFocus.current = value); + + const highlightedIndex = controlledHighlighting + ? highlightedIndexProp + : highlightedIndexRef.current; + + const navigateChildldItems = useCallback( + (e: KeyboardEvent) => { + const nextIdx = nextItemIdx(count, e.key, highlightedIndexRef.current); + if (nextIdx !== highlightedIndexRef.current) { + setHighlightedIndex(nextIdx); + } + }, + [count, setHighlightedIndex] + ); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (isNavigationKey(e)) { + e.preventDefault(); + e.stopPropagation(); + keyBoardNavigation.current = true; + navigateChildldItems(e); + } else if ( + (e.key === "ArrowRight" || e.key === "Enter") && + hasPopup(e.target as HTMLElement, highlightedIndex) + ) { + onOpenMenu(highlightedIndex); + } else if (e.key === "ArrowLeft" && !isRoot(e.target as HTMLElement)) { + onCloseMenu(highlightedIndex); + } else if (e.key === "Enter") { + onActivate && onActivate(highlightedIndex); + } + }, + [ + highlightedIndex, + navigateChildldItems, + onActivate, + onCloseMenu, + onOpenMenu, + ] + ); + + const listProps: KeyboardHookListProps = useMemo( + () => ({ + onFocus: () => { + if (highlightedIndex === -1) { + setHighlightedIdx(0); + } + }, + onKeyDown: handleKeyDown, + onMouseDownCapture: () => { + keyBoardNavigation.current = false; + setIgnoreFocus(true); + }, + + // onMouseEnter would seem less expensive but it misses some cases + onMouseMove: () => { + if (keyBoardNavigation.current) { + keyBoardNavigation.current = false; + } + }, + onMouseLeave: () => { + // label === 'ParsedInput' && console.log(`%c[useKeyboardNavigationHook]<${label}> onMouseLeave`,'color:brown') + keyBoardNavigation.current = true; + setIgnoreFocus(false); + setHighlightedIndex(-1); + }, + }), + [ + highlightedIndex, + setHighlightedIndex, + navigateChildldItems, + onActivate, + onCloseMenu, + onOpenMenu, + setHighlightedIdx, + ] + ); + + // label === 'ParsedInput' && console.log(`%cuseNavigationHook<${label}> + // highlightedIdxProp= ${highlightedIdxProp}, + // highlightedIndexRef= ${highlightedIndexRef.current}, + // %chighlightedIdx= ${highlightedIdx}`, 'color: brown','color: brown;font-weight: bold;') + + return { + focusVisible: keyBoardNavigation.current ? highlightedIndex : -1, + controlledHighlighting, + highlightedIndex, + setHighlightedIndex: setHighlightedIndex, + // keyBoardNavigation, + listProps, + setIgnoreFocus, + }; +}; + +// need to be able to accommodate disabled items +function nextItemIdx(count: number, key: string, idx: number) { + if (key === "ArrowUp") { + if (idx > 0) { + return idx - 1; + } else { + return idx; + } + } else { + if (idx === null) { + return 0; + } else if (idx === count - 1) { + return idx; + } else { + return idx + 1; + } + } +} diff --git a/vuu-ui/packages/vuu-popups/src/menu/useContextMenu.tsx b/vuu-ui/packages/vuu-popups/src/menu/useContextMenu.tsx new file mode 100644 index 000000000..811c0f283 --- /dev/null +++ b/vuu-ui/packages/vuu-popups/src/menu/useContextMenu.tsx @@ -0,0 +1,91 @@ +// The menuBuilder will always be supplied by the code that will display the local +// context menu. It will be passed all configured menu descriptors. It is free to + +import { MouseEvent, useCallback, useContext } from "react"; +import { PopupService } from "../popup"; +import { + ContextMenuContext, + ContextMenuItemDescriptor, + MenuActionHandler, +} from "./context-menu-provider"; +import { ContextMenu } from "./ContextMenu"; +import { MenuItem, MenuItemGroup } from "./MenuList"; + +// augment, replace or ignore the existing menu descriptors. +export const useContextMenu = () => { + const ctx = useContext(ContextMenuContext); + // const { menuActionHandler, menuBuilders } = useContext(ContextMenuContext); + + const buildMenuOptions = useCallback((menuBuilders, location, options) => { + let results: ContextMenuItemDescriptor[] = []; + for (const menuBuilder of menuBuilders) { + // Maybe we should leave the concatenation to the menuBuilder, then it can control menuItem order + results = results.concat(menuBuilder(location, options)); + } + return results; + }, []); + + const handleShowContextMenu = useCallback( + (e: MouseEvent, location: string, options: unknown) => { + e.stopPropagation(); + e.preventDefault(); + const menuBuilders = ctx?.menuBuilders ?? []; + const menuItemDescriptors = buildMenuOptions( + menuBuilders, + location, + options + ); + console.log({ + menuItemDescriptors, + }); + if (menuItemDescriptors.length && ctx?.menuActionHandler) { + console.log(`showContextMenu ${location}`, { + options, + }); + showContextMenu(e, menuItemDescriptors, ctx.menuActionHandler); + } + }, + [buildMenuOptions, ctx] + ); + + return handleShowContextMenu; +}; + +const showContextMenu = ( + e: MouseEvent, + menuDescriptors: ContextMenuItemDescriptor[], + handleContextMenuAction: MenuActionHandler +) => { + const { clientX: left, clientY: top } = e; + const menuItems = (menuDescriptors: ContextMenuItemDescriptor[]) => { + const fromDescriptor = ( + { children, label, icon, action, options }: ContextMenuItemDescriptor, + i: number + ) => + children ? ( + + {children.map(fromDescriptor)} + + ) : ( + + {label} + + ); + + return menuDescriptors.map(fromDescriptor); + }; + + const handleClose = (menuId?: string, options?: unknown) => { + if (menuId) { + handleContextMenuAction(menuId, options); + PopupService.hidePopup(); + } + }; + + const component = ( + + {menuItems(menuDescriptors)} + + ); + PopupService.showPopup({ left: 0, top: 0, component }); +}; diff --git a/vuu-ui/packages/vuu-popups/src/menu/utils.js b/vuu-ui/packages/vuu-popups/src/menu/utils.js deleted file mode 100644 index 50be9ebfe..000000000 --- a/vuu-ui/packages/vuu-popups/src/menu/utils.js +++ /dev/null @@ -1,5 +0,0 @@ -export const isRoot = (el) => el.closest(`[data-root='true']`) !== null; - -export const hasPopup = (el, idx) => - (el.ariaHasPopup === 'true' && el.dataset?.idx === `${idx}`) || - el.querySelector(`:scope > [data-idx='${idx}'][aria-haspopup='true']`) !== null; diff --git a/vuu-ui/packages/vuu-popups/src/menu/utils.ts b/vuu-ui/packages/vuu-popups/src/menu/utils.ts new file mode 100644 index 000000000..ee8b35a24 --- /dev/null +++ b/vuu-ui/packages/vuu-popups/src/menu/utils.ts @@ -0,0 +1,7 @@ +export const isRoot = (el: HTMLElement) => + el.closest(`[data-root='true']`) !== null; + +export const hasPopup = (el: HTMLElement, idx: number) => + (el.ariaHasPopup === "true" && el.dataset?.idx === `${idx}`) || + el.querySelector(`:scope > [data-idx='${idx}'][aria-haspopup='true']`) !== + null; diff --git a/vuu-ui/packages/vuu-popups/src/popup/index.js b/vuu-ui/packages/vuu-popups/src/popup/index.ts similarity index 100% rename from vuu-ui/packages/vuu-popups/src/popup/index.js rename to vuu-ui/packages/vuu-popups/src/popup/index.ts diff --git a/vuu-ui/packages/vuu-popups/src/popup/popup-service.js b/vuu-ui/packages/vuu-popups/src/popup/popup-service.ts similarity index 66% rename from vuu-ui/packages/vuu-popups/src/popup/popup-service.js rename to vuu-ui/packages/vuu-popups/src/popup/popup-service.ts index fcc84c80c..a24f0a865 100644 --- a/vuu-ui/packages/vuu-popups/src/popup/popup-service.js +++ b/vuu-ui/packages/vuu-popups/src/popup/popup-service.ts @@ -1,33 +1,39 @@ import cx from "classnames"; -import React, { createElement, useEffect, useRef } from "react"; +import React, { + createElement, + CSSProperties, + HTMLAttributes, + ReactElement, + useEffect, + useRef, +} from "react"; import ReactDOM from "react-dom"; import { renderPortal } from "../portal"; import "./popup-service.css"; -// TODO what ! -window.popupReact = React; let _dialogOpen = false; -const _popups = []; +const _popups: string[] = []; -function specialKeyHandler(e) { - if (e.keyCode === 27 /* ESC */) { +function specialKeyHandler(e: KeyboardEvent) { + if (e.key === "Esc") { if (_popups.length) { closeAllPopups(); } else if (_dialogOpen) { - ReactDOM.unmountComponentAtNode( - document.body.querySelector(".vuuDialog") - ); + const dialogRoot = document.body.querySelector(".vuuDialog"); + if (dialogRoot) { + ReactDOM.unmountComponentAtNode(dialogRoot); + } } } } -function outsideClickHandler(e) { +function outsideClickHandler(e: MouseEvent) { if (_popups.length) { // onsole.log(`Popup.outsideClickHandler`); const popupContainers = document.body.querySelectorAll(".vuuPopup"); for (let i = 0; i < popupContainers.length; i++) { - if (popupContainers[i].contains(e.target)) { + if (popupContainers[i].contains(e.target as HTMLElement)) { return; } } @@ -60,7 +66,7 @@ function dialogClosed() { } } -function popupOpened(name /*, group*/) { +function popupOpened(name: string) { if (_popups.indexOf(name) === -1) { _popups.push(name); //onsole.log('PopupService, popup opened ' + name + ' popups : ' + _popups); @@ -71,7 +77,7 @@ function popupOpened(name /*, group*/) { } } -function popupClosed(name /*, group=null*/) { +function popupClosed(name: string /*, group=null*/) { if (_popups.length) { if (name === "*") { _popups.length = 0; @@ -89,7 +95,14 @@ function popupClosed(name /*, group=null*/) { } } -const PopupComponent = ({ children, position, style }) => { +const PopupComponent = ({ + children, + position, + style, +}: HTMLAttributes & { + position?: "above" | "below" | ""; + style?: CSSProperties; +}) => { const className = cx("hwPopup", "hwPopupContainer", position); return createElement("div", { className, style }, children); }; @@ -106,14 +119,24 @@ export class PopupService { top = 0, width = "auto", component, + }: { + depth?: number; + name?: string; + group?: string; + position?: "above" | "below" | ""; + left?: number; + right?: "auto" | number; + top?: number; + component: ReactElement; + width?: number | "auto"; }) { if (!component) { throw Error(`PopupService showPopup, no component supplied`); } - popupOpened(name, group); - let el = document.body.querySelector(".vuuPopup." + group); + popupOpened(name); + let el = document.body.querySelector(".vuuPopup." + group) as HTMLElement; if (el === null) { - el = document.createElement("div"); + el = document.createElement("div") as HTMLElement; el.className = "vuuPopup " + group; document.body.appendChild(el); } @@ -139,15 +162,16 @@ export class PopupService { //onsole.log('PopupService.hidePopup name=' + name + ', group=' + group) if (_popups.indexOf(name) !== -1) { - popupClosed(name, group); - ReactDOM.unmountComponentAtNode( - document.body.querySelector(`.vuuPopup.${group}`) - ); + popupClosed(name); + const popupRoot = document.body.querySelector(`.vuuPopup.${group}`); + if (popupRoot) { + ReactDOM.unmountComponentAtNode(popupRoot); + } } } - static keepWithinThePage(el, right = "auto") { - const target = el.querySelector(".vuuPopupContainer > *"); + static keepWithinThePage(el: HTMLElement, right: number | "auto" = "auto") { + const target = el.querySelector(".vuuPopupContainer > *") as HTMLElement; if (target) { const { top, @@ -162,12 +186,12 @@ export class PopupService { const overflowH = h - (top + height); if (overflowH < 0) { - target.style.top = parseInt(top, 10) + overflowH + "px"; + target.style.top = Math.round(top) + overflowH + "px"; } const overflowW = w - (left + width); if (overflowW < 0) { - target.style.left = parseInt(left, 10) + overflowW + "px"; + target.style.left = Math.round(left) + overflowW + "px"; } if (typeof right === "number" && right !== currentRight) { @@ -179,7 +203,7 @@ export class PopupService { } export class DialogService { - static showDialog(dialog) { + static showDialog(dialog: ReactElement) { const containerEl = ".vuuDialog"; const onClose = dialog.props.onClose; @@ -201,21 +225,35 @@ export class DialogService { static closeDialog() { dialogClosed(); - ReactDOM.unmountComponentAtNode(document.body.querySelector(".vuuDialog")); + const dialogRoot = document.body.querySelector(".vuuDialog"); + if (dialogRoot) { + ReactDOM.unmountComponentAtNode(dialogRoot); + } } } -export const Popup = (props) => { - const pendingTask = useRef(null); - const ref = useRef(null); +export interface PopupProps { + children: ReactElement; + close?: boolean; + depth: number; + group?: string; + name: string; + position?: "above" | "below" | ""; + width: number; +} + +export const Popup = (props: PopupProps) => { + const pendingTask = useRef(); + const ref = useRef(null); - const show = (props, boundingClientRect) => { + const show = (props: PopupProps, boundingClientRect: DOMRect) => { const { name, group, depth, width } = props; - let left, top; + let left: number | undefined; + let top: number | undefined; if (pendingTask.current) { - clearTimeout(pendingTask.current); - pendingTask.current = null; + window.clearTimeout(pendingTask.current); + pendingTask.current = undefined; } if (props.close === true) { @@ -237,7 +275,7 @@ export const Popup = (props) => { top = targetTop; } - pendingTask.current = setTimeout(() => { + pendingTask.current = window.setTimeout(() => { PopupService.showPopup({ name, group, @@ -255,9 +293,10 @@ export const Popup = (props) => { useEffect(() => { if (ref.current) { const el = ref.current.parentElement; - const boundingClientRect = el.getBoundingClientRect(); - //onsole.log(`%cPopup.componentDidMount about to call show`,'color:green'); - show(props, boundingClientRect); + const boundingClientRect = el?.getBoundingClientRect(); + if (boundingClientRect) { + show(props, boundingClientRect); + } } return () => { @@ -266,15 +305,4 @@ export const Popup = (props) => { }, [props]); return React.createElement("div", { className: "popup-proxy", ref }); - - // componentWillReceiveProps(nextProps) { - - // const domNode = ReactDOM.findDOMNode(this); - // if (domNode) { - // const el = domNode.parentElement; - // const boundingClientRect = el.getBoundingClientRect(); - // //onsole.log(`%cPopup.componentWillReceiveProps about to call show`,'color:green'); - // this.show(nextProps, boundingClientRect); - // } - // } }; diff --git a/vuu-ui/packages/vuu-popups/tsconfig-emit-types.json b/vuu-ui/packages/vuu-popups/tsconfig-emit-types.json new file mode 100644 index 000000000..755997611 --- /dev/null +++ b/vuu-ui/packages/vuu-popups/tsconfig-emit-types.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig-emit-types.json", + "compilerOptions": { + "outDir": "../../dist/vuu-popup/types" + }, + "include": [ + "src" + ] + } + \ No newline at end of file diff --git a/vuu-ui/scripts/build-all-type-defs.mjs b/vuu-ui/scripts/build-all-type-defs.mjs index 8b39f02f8..e368068fa 100644 --- a/vuu-ui/scripts/build-all-type-defs.mjs +++ b/vuu-ui/scripts/build-all-type-defs.mjs @@ -9,7 +9,7 @@ const packages = [ // 'react-utils', // 'vuu-theme', "vuu-data", - // 'ui-controls', + "vuu-popups", // 'vuu-datagrid', "vuu-datatable", // "vuu-datagrid-extras", diff --git a/vuu-ui/showcase/src/examples/Layout/Menu.stories.jsx b/vuu-ui/showcase/src/examples/Layout/Menu.examples.tsx similarity index 82% rename from vuu-ui/showcase/src/examples/Layout/Menu.stories.jsx rename to vuu-ui/showcase/src/examples/Layout/Menu.examples.tsx index 59474b60b..7be494832 100644 --- a/vuu-ui/showcase/src/examples/Layout/Menu.stories.jsx +++ b/vuu-ui/showcase/src/examples/Layout/Menu.examples.tsx @@ -1,7 +1,11 @@ import { Flexbox } from "@finos/vuu-layout"; import { ContextMenu, + ContextMenuItemDescriptor, + ContextMenuProps, ContextMenuProvider, + MenuActionHandler, + MenuBuilder, MenuItem, MenuItemGroup, Separator, @@ -10,14 +14,22 @@ import { import { Button } from "@salt-ds/core"; -import { ComponentAnatomy as RenderVisualiser } from "@heswell/component-anatomy"; -import { useLayoutEffect, useRef, useState } from "react"; +import { + HTMLAttributes, + MouseEventHandler, + useLayoutEffect, + useRef, + useState, +} from "react"; let displaySequence = 1; const usePosition = () => { - const ref = useRef(null); - const [position, setPosition] = useState(undefined); + const ref = useRef(null); + const [position, setPosition] = useState<{ x: number; y: number }>({ + x: -1, + y: -1, + }); useLayoutEffect(() => { if (ref.current) { const { left: x, top: y } = ref.current.getBoundingClientRect(); @@ -25,10 +37,10 @@ const usePosition = () => { } }, []); - return [ref, position]; + return { ref, position }; }; -const SampleContextMenu = (props) => ( +const SampleContextMenu = (props: Partial) => ( Item 1.1 @@ -56,25 +68,24 @@ const SampleContextMenu = (props) => ( ); export const DefaultContextMenu = () => { - const handleClose = (/*action, options*/) => { + const handleClose: ContextMenuProps["onClose"] = (/*action, options*/) => { console.log(`clicked menu action`); }; - const [ref, position] = usePosition(); + const { ref, position } = usePosition(); + + console.log({ position }); return (
    - {position ? ( + {position.x !== -1 && position.y !== -1 ? ( ) : null}
    @@ -88,14 +99,14 @@ const Id = ({ children }) => ( ); export const AdditionalNesting = () => { - const [ref, position] = usePosition(); + const { ref, position } = usePosition(); return (
    - {position ? ( + {position.x !== -1 && position.y !== -1 ? ( @@ -165,7 +176,10 @@ export const AdditionalNesting = () => { AdditionalNesting.displaySequence = displaySequence++; export const ContextMenuPopup = () => { - const [position, setPosition] = useState(null); + const [position, setPosition] = useState<{ x: number; y: number }>({ + x: 0, + y: 0, + }); const ref = useRef(null); const keyboardNav = useRef(false); @@ -181,19 +195,19 @@ export const ContextMenuPopup = () => { const handleClose = (/* menuId */) => { console.log(`closed with menuId`); - setPosition(null); + setPosition({ x: 0, y: 0 }); }; const getContextMenu = () => { return position ? ( - Item 1.1 + Item 1.1 Item 1.2 Item 1.3 @@ -276,10 +290,13 @@ export const ContextMenuPopup = () => { ContextMenuPopup.displaySequence = displaySequence++; -const ComponentWithMenu = ({ location, ...props }) => { +const ComponentWithMenu = ({ + location, + ...props +}: HTMLAttributes & { location: "left" | "right" }) => { const showContextMenu = useContextMenu(); - const handleContextMenu = (e) => { - console.log("handleContextMenu"); + const handleContextMenu: MouseEventHandler = (e) => { + console.log(`ComponentWithMenu<${location}> handleContextMenu`); showContextMenu(e, location, { type: "outer" }); }; return
    ; @@ -292,8 +309,9 @@ export const SimpleContextMenuProvider = () => { { label: "Group", action: "group" }, ]; - const handleMenuAction = (/*type, options*/) => { + const handleMenuAction: MenuActionHandler = (/*type, options*/) => { console.log(`handleContextMenu`); + return true; }; const menuBuilder = () => menuDescriptors; @@ -314,7 +332,7 @@ export const SimpleContextMenuProvider = () => { SimpleContextMenuProvider.displaySequence = displaySequence++; export const ContextMenuProviderWithLocationAwareMenuBuilder = () => { - const menuDescriptors = [ + const menuDescriptors: ContextMenuItemDescriptor[] = [ { label: "Sort", action: "sort", icon: "sort-up" }, { label: "Filter", action: "filter", icon: "filter" }, { label: "Group", action: "group" }, @@ -331,20 +349,31 @@ export const ContextMenuProviderWithLocationAwareMenuBuilder = () => { }, ]; - const handleMenuAction = (/* type, options */) => { - console.log(`handleContextMenu`); + const handleMenuAction: MenuActionHandler = ( + action: string, + options: unknown + ) => { + console.log(`handleContextMenu ${action}`, { + options, + }); + return true; }; - const menuBuilder = (location, options) => + const menuBuilder: MenuBuilder = (location: string) => menuDescriptors.filter( (descriptor) => descriptor.location === undefined || descriptor.location === location ); - const localMenuBuilder = (location, options) => { + const localMenuBuilder: MenuBuilder = (/* location: string */) => { + // localMenuBuilder isn't using location, as we just return hardcoded options return [ - { label: "Red 1", action: "left1", location: "left" }, - { label: "Red 2", action: "left2", location: "left" }, + { + label: "Local 1", + action: "local1", + options: { LookAtMeMa: "no-hands" }, + }, + { label: "Local 2", action: "local2" }, ]; }; diff --git a/vuu-ui/showcase/src/examples/Layout/index.ts b/vuu-ui/showcase/src/examples/Layout/index.ts index 03a8dd73a..2a9be837c 100644 --- a/vuu-ui/showcase/src/examples/Layout/index.ts +++ b/vuu-ui/showcase/src/examples/Layout/index.ts @@ -4,7 +4,7 @@ export * as Dialog from "./Dialog.stories"; export * as Flexbox from "./Flexbox.stories"; export * as DraggableLayout from "./DraggableLayout.stories"; export * as FluidGrid from "./FluidGrid.stories"; -export * as Menu from "./Menu.stories"; +export * as Menu from "./Menu.examples"; export * as Palette from "./Palette.stories"; export * as Stack from "./Stack.stories"; export * as StackLayout from "./StackLayout.stories"; From 06d5c0fe89815e409765713cf44c219aea688a3c Mon Sep 17 00:00:00 2001 From: heswell Date: Sun, 15 Jan 2023 10:27:28 +0000 Subject: [PATCH 3/4] move default column config from vuu-data to vuu-blotter, it is specific to SIMUL module (#427) --- .../vuu-data/src/hooks/useVuuTables.ts | 15 ++---- vuu-ui/packages/vuu-utils/package.json | 5 +- .../app-vuu-example/src}/columnMetaData.ts | 47 ++++++++-------- .../sample-apps/app-vuu-example/tsconfig.json | 6 +++ .../feature-vuu-blotter/src/VuuBlotter.tsx | 54 ++++++++++++++----- 5 files changed, 80 insertions(+), 47 deletions(-) rename vuu-ui/{packages/vuu-data/src/hooks => sample-apps/app-vuu-example/src}/columnMetaData.ts (89%) create mode 100644 vuu-ui/sample-apps/app-vuu-example/tsconfig.json diff --git a/vuu-ui/packages/vuu-data/src/hooks/useVuuTables.ts b/vuu-ui/packages/vuu-data/src/hooks/useVuuTables.ts index 8a5095adb..a17a012bd 100644 --- a/vuu-ui/packages/vuu-data/src/hooks/useVuuTables.ts +++ b/vuu-ui/packages/vuu-data/src/hooks/useVuuTables.ts @@ -4,16 +4,11 @@ import { VuuTableMeta, } from "@finos/vuu-protocol-types"; import { useCallback, useEffect, useState } from "react"; -// TODO remove getColumnConfig here, accept as a parameter -import { getColumnConfig } from "./columnMetaData"; import { useServerConnection } from "./useServerConnection"; export type SchemaColumn = { name: string; serverDataType: VuuColumnDataType; - label?: string; - type?: { name: string }; - width?: number; }; export type TableSchema = { @@ -28,12 +23,10 @@ const createSchemaFromTableMetadata = ({ }: VuuTableMeta): TableSchema => { return { table, - columns: columns.map((col, idx) => { - const columnConfig = getColumnConfig(table.table, col); - return columnConfig - ? { ...columnConfig, serverDataType: dataTypes[idx] } - : { name: col, serverDataType: dataTypes[idx] }; - }), + columns: columns.map((col, idx) => ({ + name: col, + serverDataType: dataTypes[idx], + })), }; }; diff --git a/vuu-ui/packages/vuu-utils/package.json b/vuu-ui/packages/vuu-utils/package.json index c397b9870..0d9ef145f 100644 --- a/vuu-ui/packages/vuu-utils/package.json +++ b/vuu-ui/packages/vuu-utils/package.json @@ -10,7 +10,10 @@ "test": "vitest run", "type-defs": "node ../../scripts/build-type-defs.mjs" }, - "devDependencies": {}, + "devDependencies": { + "@finos/vuu-datagrid-types": "0.0.26", + "@finos/vuu-protocol-types": "0.0.26" + }, "peerDependencies": { "react": "^17.0.2", "react-dom": "^17.0.2" diff --git a/vuu-ui/packages/vuu-data/src/hooks/columnMetaData.ts b/vuu-ui/sample-apps/app-vuu-example/src/columnMetaData.ts similarity index 89% rename from vuu-ui/packages/vuu-data/src/hooks/columnMetaData.ts rename to vuu-ui/sample-apps/app-vuu-example/src/columnMetaData.ts index f0e378955..6ab79a595 100644 --- a/vuu-ui/packages/vuu-data/src/hooks/columnMetaData.ts +++ b/vuu-ui/sample-apps/app-vuu-example/src/columnMetaData.ts @@ -1,12 +1,8 @@ -type ColumnConfig = { - aggregate?: "avg"; - label?: string; - name: string; - type?: unknown; - width?: number; -}; +import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; + +const Average = 2; -const columnMetaData: { [key: string]: ColumnConfig } = { +const columnMetaData: { [key: string]: Partial } = { account: { label: "Account", name: "account", @@ -29,7 +25,7 @@ const columnMetaData: { [key: string]: ColumnConfig } = { renderer: { name: "background", flashStyle: "arrow-bg" }, formatting: { decimals: 2, zeroPad: true }, }, - aggregate: "avg", + aggregate: Average, }, askSize: { name: "askSize", @@ -37,7 +33,7 @@ const columnMetaData: { [key: string]: ColumnConfig } = { type: { name: "number", }, - aggregate: "avg", + aggregate: Average, }, averagePrice: { label: "Average Price", @@ -45,7 +41,7 @@ const columnMetaData: { [key: string]: ColumnConfig } = { type: { name: "number", }, - aggregate: "avg", + aggregate: Average, }, bbg: { name: "bbg", @@ -62,7 +58,7 @@ const columnMetaData: { [key: string]: ColumnConfig } = { renderer: { name: "background", flashStyle: "arrow-bg" }, formatting: { decimals: 2, zeroPad: true }, }, - aggregate: "avg", + aggregate: Average, }, bidSize: { label: "Bid Size", @@ -70,7 +66,7 @@ const columnMetaData: { [key: string]: ColumnConfig } = { type: { name: "number", }, - aggregate: "avg", + aggregate: Average, }, ccy: { name: "ccy", @@ -83,7 +79,7 @@ const columnMetaData: { [key: string]: ColumnConfig } = { type: { name: "number", }, - aggregate: "avg", + aggregate: Average, }, close: { @@ -93,7 +89,7 @@ const columnMetaData: { [key: string]: ColumnConfig } = { name: "number", formatting: { decimals: 2, zeroPad: true }, }, - aggregate: "avg", + aggregate: Average, }, clOrderId: { label: "Child Order ID", @@ -167,7 +163,7 @@ const columnMetaData: { [key: string]: ColumnConfig } = { name: "number", formatting: { decimals: 2, zeroPad: true }, }, - aggregate: "avg", + aggregate: Average, }, lastUpdate: { label: "Last Update", @@ -207,7 +203,7 @@ const columnMetaData: { [key: string]: ColumnConfig } = { name: "number", formatting: { decimals: 2, zeroPad: true }, }, - aggregate: "avg", + aggregate: Average, }, openQty: { label: "Open Qty", @@ -215,7 +211,7 @@ const columnMetaData: { [key: string]: ColumnConfig } = { width: 80, type: { name: "number", - format: { decimals: 0 }, + formatting: { decimals: 0 }, }, }, orderId: { @@ -253,7 +249,7 @@ const columnMetaData: { [key: string]: ColumnConfig } = { name: "number", formatting: { decimals: 2, zeroPad: true }, }, - aggregate: "avg", + aggregate: Average, }, priceLevel: { label: "Price Level", @@ -367,7 +363,9 @@ const columnMetaData: { [key: string]: ColumnConfig } = { }, }; -const tables: { [key: string]: any } = { +type TableColDefs = { [key: string]: Partial }; + +const tables: { [key: string]: TableColDefs } = { orders: { filledQuantity: { ...columnMetaData.filledQuantity, @@ -375,7 +373,7 @@ const tables: { [key: string]: any } = { type: { name: "number", renderer: { name: "progress", associatedField: "quantity" }, - format: { decimals: 0 }, + formatting: { decimals: 0 }, }, }, }, @@ -386,12 +384,15 @@ const tables: { [key: string]: any } = { type: { name: "number", renderer: { name: "progress", associatedField: "quantity" }, - format: { decimals: 0 }, + formatting: { decimals: 0 }, }, }, }, }; -export const getColumnConfig = (tableName: string, columnName: string) => { +export const getDefaultColumnConfig = ( + tableName: string, + columnName: string +) => { return tables[tableName]?.[columnName] ?? columnMetaData[columnName]; }; diff --git a/vuu-ui/sample-apps/app-vuu-example/tsconfig.json b/vuu-ui/sample-apps/app-vuu-example/tsconfig.json new file mode 100644 index 000000000..409f9488e --- /dev/null +++ b/vuu-ui/sample-apps/app-vuu-example/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "esnext", + } +} diff --git a/vuu-ui/sample-apps/feature-vuu-blotter/src/VuuBlotter.tsx b/vuu-ui/sample-apps/feature-vuu-blotter/src/VuuBlotter.tsx index 17729921b..ee380e2d0 100644 --- a/vuu-ui/sample-apps/feature-vuu-blotter/src/VuuBlotter.tsx +++ b/vuu-ui/sample-apps/feature-vuu-blotter/src/VuuBlotter.tsx @@ -1,12 +1,11 @@ +import { Filter } from "@finos/vuu-filter-types"; +import { filterAsQuery, FilterInput, updateFilter } from "@finos/vuu-filters"; import { useViewContext } from "@finos/vuu-layout"; import { ContextMenuProvider } from "@finos/vuu-popups"; -import { useShellContext } from "@finos/vuu-shell"; +import { ShellContextProps, useShellContext } from "@finos/vuu-shell"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useSuggestionProvider } from "./useSuggestionProvider"; -import { Filter } from "@finos/vuu-filter-types"; -import { filterAsQuery, FilterInput, updateFilter } from "@finos/vuu-filters"; - import { ConfigChangeMessage, DataSourceMenusMessage, @@ -16,8 +15,9 @@ import { useVuuMenuActions, } from "@finos/vuu-data"; import { Grid, GridProvider } from "@finos/vuu-datagrid"; -import { LinkedIcon } from "@salt-ds/icons"; +import { VuuGroupBy, VuuSort } from "@finos/vuu-protocol-types"; import { ToolbarButton } from "@heswell/salt-lab"; +import { LinkedIcon } from "@salt-ds/icons"; import { FeatureProps } from "@finos/vuu-shell"; @@ -25,15 +25,43 @@ import "./VuuBlotter.css"; const classBase = "vuuBlotter"; +const CONFIG_KEYS = ["filter", "filterQuery", "groupBy", "sort"]; + +type BlotterConfig = { + groupBy?: VuuGroupBy; + sort?: VuuSort; +}; export interface FilteredGridProps extends FeatureProps { schema: TableSchema; } +const applyDefaults = ( + { columns, table }: TableSchema, + getDefaultColumnConfig?: ShellContextProps["getDefaultColumnConfig"] +) => { + if (typeof getDefaultColumnConfig === "function") { + return columns.map((column) => { + const config = getDefaultColumnConfig(table.table, column.name); + if (config) { + return { + ...column, + ...config, + }; + } else { + return column; + } + }); + } else { + return columns; + } +}; + const VuuBlotter = ({ schema, ...props }: FilteredGridProps) => { const { id, dispatch, load, purge, save, loadSession, saveSession } = useViewContext(); - const config = useMemo(() => load?.(), [load]); - const { handleRpcResponse } = useShellContext(); + const config = useMemo(() => load?.() as BlotterConfig | undefined, [load]); + console.log({ config }); + const { getDefaultColumnConfig, handleRpcResponse } = useShellContext(); const [currentFilter, setCurrentFilter] = useState(); const suggestionProvider = useSuggestionProvider({ @@ -119,8 +147,10 @@ const VuuBlotter = ({ schema, ...props }: FilteredGridProps) => { break; default: - for (let [key, state] of Object.entries(update)) { - save(state, key); + for (const [key, state] of Object.entries(update)) { + if (CONFIG_KEYS.includes(key)) { + save?.(state, key); + } } } }, @@ -175,13 +205,13 @@ const VuuBlotter = ({ schema, ...props }: FilteredGridProps) => { columnSizing="fill" dataSource={dataSource} aggregations={config?.aggregations} - columns={config?.columns || schema.columns} - groupBy={config?.group} + columns={ + config?.columns || applyDefaults(schema, getDefaultColumnConfig) + } onConfigChange={handleConfigChange} renderBufferSize={80} rowHeight={18} selectionModel="extended" - sort={config?.sort} showLineNumbers />
    From 5021c4d13cb3f0ba6a0a16d0d5410b67ca59062d Mon Sep 17 00:00:00 2001 From: heswell Date: Sun, 15 Jan 2023 18:44:59 +0000 Subject: [PATCH 4/4] fix layout drag drop issues (#429) --- .../vuu-layout/src/drag-drop/BoxModel.ts | 14 ++++-- .../src/layout-reducer/layoutUtils.ts | 46 +++++++++++-------- .../vuu-layout/src/palette/Palette.css | 28 +++++------ .../vuu-layout/src/palette/Palette.tsx | 4 +- .../src/registry/ComponentRegistry.ts | 25 ++++++---- .../vuu-layout/src/utils/propUtils.ts | 13 ++++-- vuu-ui/packages/vuu-theme/css/icons/icons.css | 9 +++- 7 files changed, 85 insertions(+), 54 deletions(-) diff --git a/vuu-ui/packages/vuu-layout/src/drag-drop/BoxModel.ts b/vuu-ui/packages/vuu-layout/src/drag-drop/BoxModel.ts index 36aafcaeb..a2fc8fac8 100644 --- a/vuu-ui/packages/vuu-layout/src/drag-drop/BoxModel.ts +++ b/vuu-ui/packages/vuu-layout/src/drag-drop/BoxModel.ts @@ -420,16 +420,22 @@ function omitDragging(component: ReactElement) { function measureComponentDomElement( component: LayoutModel ): [DragDropRect, HTMLElement, LayoutModel] { - const { id } = getProps(component); - const type = typeOf(component) as string; + const { id } = getProps(component) as { id: string }; + if (id === undefined) { + throw Error("`BoxModel.measureComponentElement, component has no id"); + } const el = document.getElementById(id); if (!el) { - throw Error(`No DOM for ${type} ${id}`); + throw Error( + "BoxModel.measureComponentElement, no DOM element found for component" + ); } // Note: height and width are not required for dropTarget identification, but // are used in sizing calculations on drop - const { top, left, right, bottom, height, width } = el.getBoundingClientRect(); + const { top, left, right, bottom, height, width } = + el.getBoundingClientRect(); let scrolling = undefined; + const type = typeOf(component) as string; if (isContainer(type)) { const scrollHeight = el.scrollHeight; if (scrollHeight > height) { diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutUtils.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutUtils.ts index c8ece7871..db875d081 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutUtils.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutUtils.ts @@ -5,12 +5,12 @@ import { dimension } from "../common-types"; import { ComponentRegistry, isContainer, - isLayoutComponent + isLayoutComponent, } from "../registry/ComponentRegistry"; import { getPersistentState, hasPersistentState, - setPersistentState + setPersistentState, } from "../use-persistent-state"; import { expandFlex, getProps, typeOf } from "../utils"; import { LayoutJSON, LayoutModel, layoutType } from "./layoutTypes"; @@ -147,23 +147,25 @@ function getLayoutChildren( : React.isValidElement(children) ? [children] : []; - return isContainer(type) ? kids.map((child, i) => { - const childType = typeOf(child) as string; - const previousType = typeOf(previousChildren?.[i]); - - if (!previousType || childType === previousType) { - const [layoutProps, children] = getChildLayoutProps( - childType, - child.props, - `${path}.${i}`, - type, - previousChildren?.[i] - ); - return React.cloneElement(child, layoutProps, children); - } - - return previousChildren?.[i]; - }) : children; + return isContainer(type) + ? kids.map((child, i) => { + const childType = typeOf(child) as string; + const previousType = typeOf(previousChildren?.[i]); + + if (!previousType || childType === previousType) { + const [layoutProps, children] = getChildLayoutProps( + childType, + child.props, + `${path}.${i}`, + type, + previousChildren?.[i] + ); + return React.cloneElement(child, layoutProps, children); + } + + return previousChildren?.[i]; + }) + : children; } const getStyle = ( @@ -214,7 +216,9 @@ export function layoutFromJson( const componentType = type.match(/^[a-z]/) ? type : ComponentRegistry[type]; if (componentType === undefined) { - throw Error(`Unable to create component from JSON, unknown type ${type}`); + throw Error( + `layoutUtils unable to create component from JSON, unknown type ${type}` + ); } if (state) { @@ -224,8 +228,10 @@ export function layoutFromJson( return React.createElement( componentType, { + id, ...props, key: id, + path, }, children ? children.map((child, i) => layoutFromJson(child, `${path}.${i}`)) diff --git a/vuu-ui/packages/vuu-layout/src/palette/Palette.css b/vuu-ui/packages/vuu-layout/src/palette/Palette.css index bfbeb4795..b99f8e7bf 100644 --- a/vuu-ui/packages/vuu-layout/src/palette/Palette.css +++ b/vuu-ui/packages/vuu-layout/src/palette/Palette.css @@ -4,28 +4,30 @@ } .vuuPaletteItem { - --vuu-icon-color: var(--salt-separable-primary-background); + --vuu-icon-color: var(--salt-separable-primary-borderColor); --vuu-icon-inset: calc(50% - 12px) auto auto -3px; - --vuu-icon-svg: var(--svg-grab-handle); --vuu-icon-height: 24px; --vuu-icon-width: 24px; - padding-left: 20px; + --list-item-text-padding: 0 0 0 calc(var(--salt-size-unit) * 3); } -.vuuPaletteItem[data-icon]:after { - --height: var(--vuu-icon-height, var(--vuu-icon-size, 12px)); - --width: var(--vuu-icon-width, var(--vuu-icon-size, 12px)); - +.vuuPaletteItem[data-draggable]:after { + height: 22px; + width: 6px; content: ""; - background-color: var(--vuu-icon-color, black); - height: var(--height); - inset: var(--vuu-icon-inset,0 auto 0 0); - mask: var(--vuu-icon-svg) center center/var(--width) var(--height) no-repeat; - -webkit-mask: var(--vuu-icon-svg) center center/var(--width) var(--height) no-repeat; position: absolute; - width: var(--width); + + background-image: + linear-gradient(45deg, rgb(180, 183, 190) 25%, transparent 25%), + linear-gradient(-45deg, rgb(180, 183, 190) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, rgb(180, 183, 190) 25%), + linear-gradient(-45deg, transparent 75%, rgb(180, 183, 190) 25%); + background-size: 4px 4px; + background-position: 0 0, 2px 0, 2px -2px, 0 2px; + } .vuuSimpleDraggableWrapper > .vuuPaletteItem { --vuu-icon-color: var(--salt-selectable-foreground); + } \ No newline at end of file diff --git a/vuu-ui/packages/vuu-layout/src/palette/Palette.tsx b/vuu-ui/packages/vuu-layout/src/palette/Palette.tsx index be71c037e..48c49d29e 100644 --- a/vuu-ui/packages/vuu-layout/src/palette/Palette.tsx +++ b/vuu-ui/packages/vuu-layout/src/palette/Palette.tsx @@ -6,7 +6,7 @@ import { HTMLAttributes, memo, MouseEvent, - ReactElement + ReactElement, } from "react"; import { useLayoutProviderDispatch } from "../layout-provider"; import { View } from "../layout-view"; @@ -43,7 +43,7 @@ export const PaletteItem = memo( return ( ); diff --git a/vuu-ui/packages/vuu-layout/src/registry/ComponentRegistry.ts b/vuu-ui/packages/vuu-layout/src/registry/ComponentRegistry.ts index 624c5fcb3..d93f1c9fb 100644 --- a/vuu-ui/packages/vuu-layout/src/registry/ComponentRegistry.ts +++ b/vuu-ui/packages/vuu-layout/src/registry/ComponentRegistry.ts @@ -1,11 +1,18 @@ -import { FunctionComponent } from 'react'; +import { FunctionComponent } from "react"; const _containers: { [key: string]: boolean } = {}; const _views: { [key: string]: boolean } = {}; -export type layoutComponentType = 'component' | 'container' | 'view'; +export type layoutComponentType = "component" | "container" | "view"; -export const ComponentRegistry: { [key: string]: FunctionComponent } = {}; +export interface ComponentWithId { + id: string; + [key: string]: unknown; +} + +export const ComponentRegistry: { + [key: string]: FunctionComponent; +} = {}; export function isContainer(componentType: string) { return _containers[componentType] === true; @@ -15,21 +22,23 @@ export function isView(componentType: string) { return _views[componentType] === true; } -export const isLayoutComponent = (type: string) => isContainer(type) || isView(type); +export const isLayoutComponent = (type: string) => + isContainer(type) || isView(type); -export const isRegistered = (className: string) => !!ComponentRegistry[className]; +export const isRegistered = (className: string) => + !!ComponentRegistry[className]; export function registerComponent( componentName: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any component: FunctionComponent, - type: layoutComponentType = 'component' + type: layoutComponentType = "component" ) { ComponentRegistry[componentName] = component; - if (type === 'container') { + if (type === "container") { _containers[componentName] = true; - } else if (type === 'view') { + } else if (type === "view") { _views[componentName] = true; } } diff --git a/vuu-ui/packages/vuu-layout/src/utils/propUtils.ts b/vuu-ui/packages/vuu-layout/src/utils/propUtils.ts index 5da06f5ac..dd9835f8a 100644 --- a/vuu-ui/packages/vuu-layout/src/utils/propUtils.ts +++ b/vuu-ui/packages/vuu-layout/src/utils/propUtils.ts @@ -1,5 +1,5 @@ -import { ReactElement } from 'react'; -import { LayoutModel } from '../layout-reducer'; +import { ReactElement } from "react"; +import { LayoutModel } from "../layout-reducer"; const NO_PROPS = {}; export const getProp = (component: LayoutModel, propName: string) => { @@ -7,16 +7,19 @@ export const getProp = (component: LayoutModel, propName: string) => { return props[propName] ?? props[`data-${propName}`]; }; -export const getProps = (component?: LayoutModel) => component?.props || component || NO_PROPS; +export const getProps = (component?: LayoutModel) => + component?.props || component || NO_PROPS; export const getChildProp = (container: LayoutModel) => { const props = getProps(container); if (props.children) { const { - children: [target, ...rest] + children: [target, ...rest], } = props; if (rest.length > 0) { - console.warn(`getChild expected a single child, found ${rest.length + 1}`); + console.warn( + `getChild expected a single child, found ${rest.length + 1}` + ); } return target as ReactElement; } diff --git a/vuu-ui/packages/vuu-theme/css/icons/icons.css b/vuu-ui/packages/vuu-theme/css/icons/icons.css index 2474a3fe8..faff7da84 100644 --- a/vuu-ui/packages/vuu-theme/css/icons/icons.css +++ b/vuu-ui/packages/vuu-theme/css/icons/icons.css @@ -43,7 +43,7 @@ span[data-icon]{ width: var(--vuu-icon-width, var(--vuu-icon-size, 18px)); } -[data-icon]:after { +/* [data-icon]:after { content: ""; background-color: var(--vuu-icon-color, black); left: var(--vuu-icon-left, auto); @@ -55,7 +55,7 @@ span[data-icon]{ position: absolute; top: var(--vuu-icon-top, auto); width: var(--vuu-icon-width, var(--vuu-icon-size, 12px)); -} +} */ [data-icon='filter'] { --vuu-icon-svg: var(--svg-filter); @@ -87,6 +87,11 @@ span[data-icon]{ --vuu-icon-svg: var(--svg-chevron-double-right); } +[data-icon='grab-handle'] { + /* --vuu-icon-svg: var(--svg-grab-handle); */ + --vuu-icon-svg: linear-gradient(to bottom, blue, red); +} + [data-icon='settings'] { --vuu-icon-svg: var(--svg-settings); }