Skip to content

Commit

Permalink
Merge pull request #1234
Browse files Browse the repository at this point in the history
* [#187401278] Feature: Components animate into position and size

* [#187401278] Feature: Components animate into position and size

* chore: code review: use CSS transition rather than JS animation

* * Fix remaining issues that occur when a new graph is created

* chore: fix map cypress tests
  • Loading branch information
bfinzer authored May 1, 2024
1 parent 6bd3ed9 commit 7d8586b
Show file tree
Hide file tree
Showing 14 changed files with 111 additions and 70 deletions.
43 changes: 2 additions & 41 deletions v3/cypress/e2e/map.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const arrayOfAttributes = ["Category", "Educ_Tertiary_Perc", "Inversions"]

context("Map UI", () => {
beforeEach(function () {
const url = `${Cypress.config("index")}?mouseSensor`
const url = `${Cypress.config("index")}?mouseSensor&noComponentAnimation`
cy.visit(url)
cy.wait(3000)
})
Expand Down Expand Up @@ -205,46 +205,7 @@ context("Map UI", () => {
it("checks show/hide map points with legend selections", () => {
cfm.openLocalDoc(filename1)
c.getIconFromToolshelf("map").click()
cy.dragAttributeToTarget("attribute", arrayOfAttributes[2], "map")

mlh.verifyCategoricalLegend(arrayOfValues[2].values.length)
mlh.selectCategoryNameForCategoricalLegend(arrayOfValues[2].values[0])
map.selectHideShowButton()
map.getHideSelectedCases().should("not.be.disabled")
map.getHideUnselectedCases().should("not.be.disabled")
map.getShowAllCases().should("be.disabled")

map.selectHideSelectedCases()
mlh.verifyCategoricalLegend(arrayOfValues[2].values.length-1)

map.selectHideShowButton()
map.getHideSelectedCases().should("be.disabled")
map.getHideUnselectedCases().should("not.be.disabled")
map.getShowAllCases().should("not.be.disabled")

map.selectShowAllCases()
mlh.verifyCategoricalLegend(arrayOfValues[2].values.length)
mlh.verifyCategoricalLegendKeySelected(arrayOfValues[2].values[0], arrayOfValues[2].selected[0])

// TODO: unselecting by clicking away in map doesn't work
// PT bug - #186747322
// Uncomment the below six lines once it's fixed
// mlh.unselectLegendCategory()
// mlh.verifyNoLegendCategorySelectedForCategoricalLegend()
// map.selectHideShowButton()
// map.getHideSelectedCases().should("be.disabled")
// map.getHideUnselectedCases().should("not.be.disabled")
// map.getShowAllCases().should("be.disabled")

// map.selectHideUnselectedCases()
// TODO: This should be 0, but it's currently 1.
// Once fixed, this should be updated.
// PT bug - #186916697
// mlh.verifyCategoricalLegend(1)
})
it("checks show/hide map points with legend selections", () => {
cfm.openLocalDoc(filename1)
c.getIconFromToolshelf("map").click()
cy.wait(1000)
cy.dragAttributeToTarget("attribute", arrayOfAttributes[2], "map")

mlh.verifyCategoricalLegend(arrayOfValues[2].values.length)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export const NumericAxisDragRects = observer(
? [0]
: place === 'bottom' ? [0, 1, 2] : [2, 1, 0],
rectCount = lockZero ? 1 : 3
if (length != null && axisBounds != null) {
if (length != null && length > 0 && axisBounds != null) {
selectDragRects(rectRef.current)
?.data(numbering)// data signify lower, middle, upper rectangles
.join(
Expand Down
20 changes: 17 additions & 3 deletions v3/src/components/case-table/case-table-tool-shelf-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { observer } from "mobx-react-lite"
import { t } from "../../utilities/translation/translate"
import { getFormulaManager, getSharedModelManager } from "../../models/tiles/tile-environment"
import { getTileComponentInfo } from "../../models/tiles/tile-component-info"
import { kTitleBarHeight } from "../constants"
import { ComponentRect } from "../../utilities/animation-utils"
import { appState } from "../../models/app-state"
import { ISharedDataSet, kSharedDataSetType, SharedDataSet } from "../../models/shared/shared-data-set"
import { ISharedCaseMetadata, kSharedCaseMetadataType, SharedCaseMetadata }
Expand Down Expand Up @@ -51,11 +53,23 @@ export const CaseTableToolShelfMenuList = observer(function CaseTableToolShelfMe
manager?.addTileSharedModel(tile.content, caseMetadata, true)
caseMetadata.setCaseTableTileId(tile.id)

const width = caseTableComponentInfo.defaultWidth
const height = caseTableComponentInfo.defaultHeight
const width = caseTableComponentInfo.defaultWidth || 0
const height = caseTableComponentInfo.defaultHeight || 0
const {x, y} = getPositionOfNewComponent({width, height})
content?.insertTileInRow(tile, row, {x, y, width, height})
const from: ComponentRect = { x: 0, y: 0, width: 0, height: kTitleBarHeight },
to: ComponentRect = { x, y, width, height: height + kTitleBarHeight}
content?.insertTileInRow(tile, row, from)
uiState.setFocusedTile(tile.id)
const tileLayout = content.getTileLayoutById(tile.id)
if (!isFreeTileLayout(tileLayout)) return
// use setTimeout to push the change into a subsequent action
setTimeout(() => {
// use applyModelChange to wrap into a single non-undoable action without undo string
content.applyModelChange(() => {
tileLayout.setPosition(to.x, to.y)
tileLayout.setSize(to.width, to.height)
})
})
}

const handleCreateNewDataSet = () => {
Expand Down
2 changes: 1 addition & 1 deletion v3/src/components/container/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const Container: React.FC = () => {
if (dragTileId) {
if (isFreeTileRow(row)) {
const rowTile = row.getNode(dragTileId)
if (rowTile) {
if (rowTile && (evt.delta.x || evt.delta.y)) {
documentContent?.applyModelChange(() => {
rowTile.setPosition(rowTile.x + evt.delta.x, rowTile.y + evt.delta.y)
}, {
Expand Down
17 changes: 13 additions & 4 deletions v3/src/components/container/free-tile-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getTileComponentInfo } from "../../models/tiles/tile-component-info"
import { ITileModel } from "../../models/tiles/tile-model"
import { CodapComponent } from "../codap-component"
import { kTitleBarHeight } from "../constants"
import { urlParams } from "../../utilities/url-params"

interface IProps {
row: IFreeTileRow;
Expand Down Expand Up @@ -75,8 +76,13 @@ export const FreeTileComponent = observer(function FreeTileComponent({ row, tile
const onPointerUp = () => {
document.body.removeEventListener("pointermove", onPointerMove, { capture: true })
document.body.removeEventListener("pointerup", onPointerUp, { capture: true })
mtile.setSize(resizingWidth, resizingHeight)
mtile.setPosition(resizingLeft, mtile.y)
mtile.applyModelChange(() => {
mtile.setSize(resizingWidth, resizingHeight)
mtile.setPosition(resizingLeft, mtile.y)
}, {
undoStringKey: "DG.Undo.componentResize",
redoStringKey: "DG.Redo.componentResize"
})
setResizingTileId("")
}

Expand Down Expand Up @@ -107,7 +113,7 @@ export const FreeTileComponent = observer(function FreeTileComponent({ row, tile
const startStyleTop = top || 0
const startStyleLeft = left || 0
const movingStyle = transform && {top: startStyleTop + transform.y, left: startStyleLeft + transform.x,
width, height}
width, height, transition: "none"}
const minimizedStyle = { left, top, width, height: kTitleBarHeight }
const style = rowTile?.isMinimized
? minimizedStyle
Expand All @@ -120,7 +126,10 @@ export const FreeTileComponent = observer(function FreeTileComponent({ row, tile
const info = getTileComponentInfo(tileType)
if (info?.isFixedWidth) delete style?.width
if (info?.isFixedHeight) delete style?.height
const classes = clsx("free-tile-component", { minimized: rowTile?.isMinimized })
const disableAnimation = urlParams.noComponentAnimation !== undefined
const classes = clsx("free-tile-component", {
minimized: rowTile?.isMinimized,
"disable-animation": disableAnimation })

if (!info || rowTile?.isHidden) return null

Expand Down
7 changes: 6 additions & 1 deletion v3/src/components/container/free-tile-row.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
position: absolute;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 25%);
border-radius: 6px;
transition: $minimize-transition;
// https://stackoverflow.com/a/17117992
transition: all 0.4s ease-in-out 0s;
transition-property: left, top, width, height;
&.disable-animation {
transition: none;
}
&.minimized {
height: $title-bar-height;
}
Expand Down
4 changes: 2 additions & 2 deletions v3/src/components/data-display/components/background.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ export const Background = forwardRef<SVGGElement | HTMLDivElement, IProps>((prop
bgColor = String(color(plotBackgroundColor)),
darkBgColor = String(color(plotBackgroundColor)?.darker(0.2)),
{numRows, numColumns} = layout,
cellWidth = plotWidth / numColumns,
cellHeight = plotHeight / numRows,
cellWidth = Math.max(0, plotWidth / numColumns),
cellHeight = Math.max(0, plotHeight / numRows),
row = (index: number) => Math.floor(index / numColumns),
col = (index: number) => index % numColumns,
groupElement = bgRef.current
Expand Down
4 changes: 2 additions & 2 deletions v3/src/components/graph/components/graph-axis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ export const GraphAxis = observer(function GraphAxis(
select(wrapperElt)
.selectAll<SVGRectElement, number>('rect.axis-background')
.attr('transform', transform)
.attr('width', width)
.attr('height', bounds.height)
.attr('width', Math.max(0, width))
.attr('height', Math.max(0, bounds.height))
}
}, { name: "GraphAxis.installBackground" })
}, [layout, place, wrapperElt])
Expand Down
4 changes: 3 additions & 1 deletion v3/src/components/graph/components/graph-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {usePixiPointsArray} from '../../data-display/hooks/use-pixi-points-array
import {GraphController} from "../models/graph-controller"
import {isGraphContentModel} from "../models/graph-content-model"
import {Graph} from "./graph"
import { kTitleBarHeight } from "../../constants"
import {AttributeDragOverlay} from "../../drag-drop/attribute-drag-overlay"
import "../register-adornment-types"

Expand All @@ -37,7 +38,8 @@ export const GraphComponent = observer(function GraphComponent({tile}: ITileBase
useGraphController({graphController, graphModel, pixiPointsArrayRef})

useEffect(() => {
(width != null) && (height != null) && layout.setTileExtent(width, height)
(width != null) && width >= 0 && (height != null) &&
height >= kTitleBarHeight && layout.setTileExtent(width, height)
}, [width, height, layout])

useEffect(function cleanup() {
Expand Down
4 changes: 2 additions & 2 deletions v3/src/components/graph/components/graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ export const Graph = observer(function Graph({graphController, graphRef, pixiPoi
select(abovePointsGroupRef.current).attr("transform", translate)
select(pixiContainerRef.current)
.attr("x", x).attr("y", y) // translate won't work in Safari!
.attr("width", `${layout.plotWidth}px`)
.attr("height", `${layout.plotHeight}px`)
.attr("width", `${Math.max(0, layout.plotWidth)}px`)
.attr("height", `${Math.max(0, layout.plotHeight)}px`)

pixiPoints?.resize(layout.plotWidth, layout.plotHeight)
}
Expand Down
3 changes: 3 additions & 0 deletions v3/src/components/graph/models/graph-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ export class GraphLayout extends DataDisplayLayout implements IAxisLayout {
}

@override setTileExtent(width: number, height: number) {
if (width < 0 || height < 0) {
return
}
super.setTileExtent(width, height)
this.updateScaleRanges(this.plotWidth, this.plotHeight)
}
Expand Down
26 changes: 17 additions & 9 deletions v3/src/models/document/document-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getFormulaManager, getSharedModelManager, getTileEnvironment } from "..
import { getTileContentInfo } from "../tiles/tile-content-info"
import { ITileModel, ITileModelSnapshotIn } from "../tiles/tile-model"
import { typedId } from "../../utilities/js-utils"
import { ComponentRect } from "../../utilities/animation-utils"
import { getPositionOfNewComponent } from "../../utilities/view-utils"
import { DataSet, IDataSet, IDataSetSnapshot, toCanonical } from "../data/data-set"
import { gDataBroker } from "../data/data-broker"
Expand Down Expand Up @@ -56,6 +57,8 @@ export interface INewTileOptions {

export const DocumentContentModel = BaseDocumentContentModel
.named("DocumentContent")
// performs the specified action so that response actions are included and undo/redo strings assigned
.actions(applyModelChange)
.actions(self => ({
async prepareSnapshot() {
// prepare each row for serialization
Expand Down Expand Up @@ -123,8 +126,8 @@ export const DocumentContentModel = BaseDocumentContentModel
createTile(tileType: string, options?: INewTileOptions): ITileModel | undefined {
const componentInfo = getTileComponentInfo(tileType)
if (!componentInfo) return
const width = options?.width ?? componentInfo.defaultWidth
const height = options?.height ?? componentInfo.defaultHeight
const width = options?.width ?? (componentInfo.defaultWidth || 0)
const height = options?.height ?? (componentInfo.defaultHeight || 0)
const row = self.getRowByIndex(0)
if (row) {
const newTileSnapshot = self.createDefaultTileSnapshotOfType(tileType)
Expand All @@ -134,13 +137,20 @@ export const DocumentContentModel = BaseDocumentContentModel
const computedPosition = getPositionOfNewComponent(newTileSize)
const x = options?.x ?? computedPosition.x
const y = options?.y ?? computedPosition.y
const tileOptions = { x, y, width, height }
const newTile = self.insertTileSnapshotInRow(newTileSnapshot, row, tileOptions)
const from: ComponentRect = { x: 0, y: 0, width: 0, height: kTitleBarHeight },
to: ComponentRect = { x, y, width, height: height + kTitleBarHeight}
const newTile = self.insertTileSnapshotInRow(newTileSnapshot, row, from)
if (newTile) {
const rowTile = row.tiles.get(newTile.id)
if (width && height) {
rowTile?.setSize(width, height + kTitleBarHeight)
rowTile?.setPosition(tileOptions.x, tileOptions.y)
if (width && height && rowTile) {
// use setTimeout to push the change into a subsequent action
setTimeout(() => {
// use applyModelChange to wrap into a single non-undoable action without undo string
self.applyModelChange(() => {
rowTile.setPosition(to.x, to.y)
rowTile.setSize(to.width, to.height)
})
})
}
return newTile
}
Expand Down Expand Up @@ -264,8 +274,6 @@ export const DocumentContentModel = BaseDocumentContentModel
return sharedData
}
}))
// performs the specified action so that response actions are included and undo/redo strings assigned
.actions(applyModelChange)

export type IDocumentContentModel = Instance<typeof DocumentContentModel>
export type IDocumentContentSnapshotIn = SnapshotIn<typeof DocumentContentModel>
6 changes: 3 additions & 3 deletions v3/src/models/document/free-tile-row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { observable } from "mobx"
import { Instance, SnapshotIn, addDisposer, onPatch, types } from "mobx-state-tree"
import { ITileInRowOptions, ITileRowModel, TileRowModel } from "./tile-row"
import { withoutUndo } from "../history/without-undo"
import { withUndoRedoStrings } from "../history/codap-undo-types"
import { applyModelChange } from "../history/apply-model-change"

/*
Represents the layout of a set of tiles/components with arbitrary rectangular bounds that can
Expand Down Expand Up @@ -35,12 +35,10 @@ export const FreeTileLayout = types.model("FreeTileLayout", {
setPosition(x: number, y: number) {
self.x = x
self.y = y
withUndoRedoStrings("DG.Undo.componentMove", "DG.Redo.componentMove")
},
setSize(width?: number, height?: number) {
self.width = width
self.height = height
withUndoRedoStrings("DG.Undo.componentResize", "DG.Redo.componentResize")
},
setHidden(isHidden: boolean) {
// only store it if it's true
Expand All @@ -51,6 +49,8 @@ export const FreeTileLayout = types.model("FreeTileLayout", {
self.isMinimized = isMinimized || undefined
}
}))
.actions(applyModelChange)

export interface IFreeTileLayout extends Instance<typeof FreeTileLayout> {}
export interface IFreeTileLayoutSnapshot extends SnapshotIn<typeof FreeTileLayout> {}

Expand Down
39 changes: 39 additions & 0 deletions v3/src/utilities/animation-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export const kDefaultAnimationDuration = 500
export interface ComponentRect {
x: number
y: number
width: number
height: number
[key: string]: number
}

export const interpolateEaseInOut = <T extends Record<string, number>>(duration: number, from: T, to: T) => {
return (time: number) => {
const t = Math.min(1, time / duration)
const ease = 0.5 - 0.5 * Math.cos(Math.PI * t)
const result: Partial<T> = {}
for (const key in from) {
result[key] = (from[key] + (to[key] - from[key]) * ease) as T[typeof key]
}
return result as T
}
}

// This function takes a duration, a <from> object, a <to> object and a callback function. It calls
// interpolateEaseInOut repeatedly and calls the callback with the result each time through the loop.
// The loop runs until the elapsed time is greater than the duration.
export const animateEaseInOut = <T extends Record<string, number>>(duration: number, from: T, to: T,
callback: (result: T) => void) => {
const interpolate = interpolateEaseInOut(duration, from, to)
const startTime = Date.now()
const step = () => {
const time = Date.now() - startTime
if (time < duration) {
callback(interpolate(time))
requestAnimationFrame(step)
} else {
callback(to)
}
}
requestAnimationFrame(step)
}

0 comments on commit 7d8586b

Please sign in to comment.