diff --git a/.gitignore b/.gitignore index f3b3edda78..fb3835ba66 100644 --- a/.gitignore +++ b/.gitignore @@ -33,8 +33,7 @@ dist-demo website # Tests Report & Coverage -.jest-cache -jest-coverage +.vitest **/coverage **/vitest-coverage **/cypress-report diff --git a/.rtk/filters.toml b/.rtk/filters.toml new file mode 100644 index 0000000000..d9bd43fee1 --- /dev/null +++ b/.rtk/filters.toml @@ -0,0 +1,13 @@ +# Project-local RTK filters — commit this file with your repo. +# Filters here override user-global and built-in filters. +# Docs: https://github.com/rtk-ai/rtk#custom-filters +schema_version = 1 + +# Example: suppress build noise from a custom tool +# [filters.my-tool] +# description = "Compact my-tool output" +# match_command = "^my-tool\\s+build" +# strip_ansi = true +# strip_lines_matching = ["^\\s*$", "^Downloading", "^Installing"] +# max_lines = 30 +# on_empty = "my-tool: ok" diff --git a/AGENTS.md b/AGENTS.md index 0c8ea23fa6..fdb03924b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,6 +56,35 @@ pnpm build - Use `pnpm lint:fix` and `pnpm prettier:write` only when autofix or formatting changes are intended. - Check the applicable `.oxlintrc.json` when working in Angular or framework-plugin code. The repository has three configurations: root `.oxlintrc.json`, `frameworks/angular-slickgrid/.oxlintrc.json`, and `frameworks-plugins/angular-row-detail-plugin/.oxlintrc.json`. +## VEXP context tools + +When VEXP context tools are available, `run_pipeline` is the primary tool and must be called first for repository tasks. VEXP returns pre-indexed, graph-ranked context in a single call. + +### Workflow + +1. Call `run_pipeline` with the task description before other repository searches. +2. Make targeted changes from the returned context. +3. Call `run_pipeline` again only when more context is needed. + +### Available MCP tools + +- `run_pipeline` - primary tool; runs capsule, impact, and memory in one call. +- `get_skeleton` - compact file structure. +- `index_status` - indexing status. +- `expand_vexp_ref` - expand V-REF placeholders in VEXP output. + +### Agentic search + +When VEXP is available, use `run_pipeline` before built-in file search, grep, or codebase indexing. If spawning sub-agents or background tasks, pass them the context from `run_pipeline` rather than letting them search independently. + +### Multi-repo + +`run_pipeline` can query all indexed repositories. Use `repos: ["alias"]` to scope it, and use `index_status` to see aliases. If VEXP is unavailable, use the normal repository tools. + +### Smart features + +VEXP can auto-detect intent, combine hybrid ranking, use session memory, and expand its context budget as needed. + # RTK - Token-Optimized CLI @@ -163,10 +192,6 @@ When VEXP is available, use `run_pipeline` before built-in file search, grep, or `run_pipeline` can query all indexed repositories. Use `repos: ["alias"]` to scope it, and use `index_status` to see aliases. If VEXP is unavailable, use the normal repository tools. -### Smart features - -VEXP can auto-detect intent, combine hybrid ranking, use session memory, and expand its context budget as needed. - ## Documentation Update corresponding framework documentation under `frameworks/*/docs/` when applicable. @@ -199,3 +224,5 @@ VEXP can auto-detect intent, combine hybrid ranking, use session memory, and exp - Review the diff for unrelated changes and accidental generated files. - Verify affected tests, lint, and formatting. - Mention any checks that could not be run and why. + +@RTK.md diff --git a/PINNING-POC-PROGRESS.md b/PINNING-POC-PROGRESS.md new file mode 100644 index 0000000000..209309392d --- /dev/null +++ b/PINNING-POC-PROGRESS.md @@ -0,0 +1,749 @@ +# Single-viewport pinning/stickiness POC — progress handoff + +Last updated: 2026-09-09 (framework cell-menu parity and migration documentation reviewed; legacy runtime removal and structural alias audit remain) + +## Goal + +Replace SlickGrid's multi-pane column/row architecture with an AG Grid-style docking model: + +- exactly one live body viewport with one native vertical scrollbar; ordinary grids use the + viewport for horizontal scrolling, while pinning/sticky grids use one dedicated docking + horizontal scrollbar; +- one virtualized DOM row per data row; +- each rendered row contains stable sibling left, center, and right cell regions; +- permanent pinning and scroll-activated stickiness use the same internal docking resolver; +- vertical and horizontal virtualization must remain viable for large datasets; +- this is intentionally a major-version breaking change; compatibility with the old pane renderer is not a design goal. + +The POC supports per-column pinning and the canonical nested `pinning` option. +`pinning.columns.left` accepts an inclusive edge-boundary number for contiguous +left pinning, while `pinning.columns.right` accepts a count from the trailing +edge. Either side also accepts arrays of stable column ids/indexes for +non-contiguous pinning. An inclusive v11-and-lower boundary is written as +`pinning.columns.left: 2`; users do not need to expand it into an index array. +Legacy option names are documented only in the v11 migration guide. +There is no separate `pinnedColumn` or `pinnedRows` grid option; those temporary +aliases were removed after the canonical shape was wired through core and state. +The POC does not target compatibility with the old pane-based UX. + +The canonical grid-state shape is now a single nested `GridOption.pinning` object: +`{ columns: { left, right }, rows: { top, bottom } }`. `Column.pinned` remains the +per-column representation. `GridService.setPinning()` and `GridStateService` now read +and write the unified shape; sticky configuration remains separate because it has +different scroll-activated semantics. `CurrentColumn.pinning` also carries the +per-column side in column layouts, providing a hybrid preset representation for +consumers that do not want to persist a separate aggregate pinning object. +`Column.lockPinned` prevents Header Menu pinning changes for protected columns while leaving +programmatic pinning available. + +Vanilla Example 11 serializes the new nested `CurrentPinning` shape in its +saved views and intentionally opts into both pinning header commands to +exercise the new behavior. The single-column menu action calls +`SlickGrid.setColumnPinning` and updates `Column.pinned`; the bulk “Pin +Columns” menu action updates +`pinning.columns.left`, which applies the same left pins through the unified +pinning resolver. Neither action uses removed legacy options or validation. The current +`hidePinningColumnsCommand` +setting controls the bulk command, while `hidePinColumnCommand` controls the +single-column command. + +The Header Menu now exposes a `pin-column` root command displayed as `Column Pinning`. Its +sub-menu always contains `pin-left`, `pin-right`, and `pin-columns`, followed by a separator, +then `unpin-column` and `unpin-columns`. The first two commands set the selected column's +`Column.pinned` side, `pin-columns` writes the aggregate `pinning.columns.left` boundary, and the +two unpin commands clear the selected column or all aggregate column edges. All five commands +remain visible regardless of the current pin state. +The old bulk command ids and translation keys are documented only in the v11 migration guide; +none recreates the old two-pane layout. + +The POC has gone through visual hardening, selected Cypress migration, framework demo parity, +and removal of the legacy pane options/interfaces and runtime branches. The common unit suite +and focused coverage checks pass; framework browser validation remains follow-up work. The +remaining cleanup is limited to internal pane-shaped aliases, historical CSS variable names, +and intentional migration-facing terminology/documentation. + +## Refactoring status and immediate follow-up + +The legacy option/interface/state/service branches have been removed from the runtime +implementation. The current code no longer defines or reads the former flat pinning +configuration or its legacy state fields. + +A structural cleanup remains for the internal `_pane*`, `_viewport*`, and `_canvas*` aliases, +which now point to the single live nodes and do not create additional panes or scrollbars. The +Migration documentation retains the old theme variable names as v11-and-lower +references, while the active stylesheet now uses `--slick-pinned-*`. Old command +IDs, locale keys, and demo selectors are removed from active examples/runtime and remain only in +migration docs where needed. Do not reintroduce legacy runtime branches. + +The previous working-tree LOC estimate is stale after the deletion pass. Recalculate production +LOC after the remaining alias/style audit rather than using the earlier planning estimate. + +## Maintainability acceptance gate + +The original PR 1238 motivation must be reviewed before this work is accepted: multi-pane layouts +made `slickGrid.ts` harder to maintain because ordinary operations had to know about left/right +headers, footers, viewports, and canvases. The single-viewport rewrite is successful only if it +removes that model; it is **not** sufficient to create the old panes and force their options +off at runtime. + +The final implementation must satisfy all of the following: + +- construct one live header, header-row, footer-row, viewport, and canvas; the remaining pane- + shaped fields must be aliases only and must not become separate DOM/scroll containers; +- make ordinary header/footer creation and column-element lookup direct single-container + operations, without legacy pane target selection; +- keep the old option fields, state/menu/service plumbing, synchronized-scroll branches, + and resize branches deleted; intentionally retained migration-facing command IDs, locale + wording, and theme variable names must not turn into compatibility code. The obsolete + `-1000px` header-container offset is also deleted; +- keep pinning-specific behavior in the DOM-free `DockingController` plus a small docking DOM + layer that applies per-row left/center/right regions and pinned chrome offsets; +- remove the obsolete `HEADER_WIDTH_SLACK`/`1000px` header-coordinate workaround as part of the + rewrite; header titles, grouped headers, and header regions now use ordinary coordinates; +- rename the remaining pane-shaped internal fields to neutral names (`_header`, `_headerRow`, + `_footerRow`, `_viewport`, `_canvas`) so the old `L`/`R` pane model cannot leak back into normal + code; +- complete sticky docking or remove any temporary feature-flag path; a dormant sticky API is + not an acceptable final design. + +Do not revive a `ViewportMgr` merely to conceal the old multi-pane renderer. In this design, +deleting the multi-pane renderer is simpler and better aligned with the major-version breaking +change. This gate remains a required review item before final acceptance; focused tests and +documentation work may continue while the follow-up deletion audit is underway. + +## leftover TODOs identified by user +- [x] Unified grid options support pinning (left, right, top, bottom) +- [x] Header Menu exposes a `Column Pinning` sub-menu with always-visible `Pin Left`, `Pin Right`, and `Pin Through Here` commands, a separator, and `Unpin Column`/`Unpin All Columns` commands +- [x] `CurrentColumn.pinning` provides a per-column Grid State/Preset representation alongside aggregate `GridState.pinning` +- [x] Row/body/header/footer docking regions have a predictable left/center/right DOM shape. Row + regions use the compatibility-oriented names `.slick-pinned-left-cells`, + `.slick-scrolling-cells`, and `.slick-pinned-right-cells`; they are per-row regions, not old + full-height panes or independent scroll containers. Header, header-row, and footer regions use + `.slick-*-columns-left/center/right` wrappers. +- [x] The optional `.slick-docking-overlay` is not created for a grid without row pinning or + sticky-row configuration. Once row docking is configured, the overlay remains a stable row layer + even when no row is currently active. +- rowspan z-index is problematic with new docking overlay, need further investigation +- [x] Restored the original `.slick-viewport` horizontal scroll element for ordinary grids. The + active horizontal scroll element always receives the generic `.slick-horizontal-scroller` + class: ordinary grids apply it to `.slick-viewport`, while grids with pinning/sticky docking + apply it to `.slick-docking-horizontal-scroller`. The docking-specific class remains available + for code that needs to identify the docking scrollbar. +- [x] Added `.slick-vertical-scroller` as the stable selector for the native vertical scroll + element. It currently points to the single `.slick-viewport` in all grid configurations. +- [x] Pinning validation now uses the canonical `invalidColumnPinning*` and + `skipPinningValidation` options. Requests that pin every visible column or whose permanent + left/right bands consume the viewport are rejected and preserve the previous state. +- [x] Header regions expose `.slick-header-columns-left/center/right` (and equivalent header-row/ + footer-row classes), so consumers can identify each region without relying on removed pane roots. +- a11y issues with sticky example when using arrows to navigate, it jumps from a center column to a sticky column, is that expected? I would think that it should rather move the scroll instead and show next data cell instead (that is what the legacy pane implementation did, also need to verify new pinning feature). +- probably need more pinning Cypress test in vanilla example04.cy.ts to cover the new feature. +- why is context menu opened outside of the grid viewport in vanilla example04.cy.ts? The context menu position is unexpected (seemed to be the tests in describe "accessibility sub-menus tests"). +- [x] Audited legacy configuration names. The former flat pinning options are removed from + runtime code; historical references remain only in the + migration guide and documented theme-variable compatibility notes. +- address comment brought up in SlickGrid for new pinning/sticky features: + > I think the major version would indicate this well enough. yeah its a bit more than a break, its a feature deprecation sort of, but the replacement is subjectively better for me. + > what the latest push in AI development made me think of though is that we might should start thinking about shipping curated skills along with the library. that would serve two purposes. first, LLMs would know better how to apply specific features from slickgrid on the consumer end. but secondly, the skills could also act as a verification of the docs and thus overall improve the development of new features as LLMs could check up on skills when touching existing features +- identify and document breaking changes (docs folder) +- **COMPLETED MAJOR CLEANUP:** removed the legacy grid options, public interfaces, + runtime validation names, state/service plumbing, and old multi-pane behavior across + `SlickGrid`, GridState/GridService, header grouping, resizer, extensions, and framework + integrations. Remaining work is the neutralization/removal of internal pane aliases and the + audit of intentionally historical CSS/demo terminology; do not add compatibility branches. + +## Starting point + +- Branch: `master` +- Base commit: `e757539c2` +- Worktree was clean before this POC. +- The earlier `feat/viewport-mgr`/PR 1238 approach was inspected but not reused because it extends the old full-height pane architecture. +- GitHub Discussion 1237 was reviewed for arbitrary sticky rows/columns, pixel budgets, overflow policies, variable row heights, and hierarchical sticky-row semantics. +- AG Grid v36's single-scroll DOM change was used as the structural reference. + +## Implemented architecture + +### One live scroll viewport + +`SlickGrid.activateSingleViewportLayout()` configures the public/internal active collections to +one live viewport and one live canvas: + +- `_viewport = [_viewportTopL]` +- `_canvas = [_canvasTopL]` +- the active header/header-row/top-panel/footer collections likewise contain only their left/single instance. + +Pane-shaped fields still exist temporarily as internal aliases to those same live nodes. They do +not represent separate DOM panes or own additional scrollbars and are the remaining structural +cleanup item. + +### Per-row left/center/right regions + +In the single-viewport docking renderer, every rendered row has this shape, including grids +with no active pinned columns (the side regions are then empty and have no active separator): + +```html +
+ + + +
+``` + +There is no left/right row clone and no second body canvas. `renderRows()` now appends exactly one row node to `_canvasTopL`. + +The same stable-region principle applies to chrome. The single header, header-row, and footer +roots each contain persistent `left`, `center`, and `right` semantic wrappers. They use +`display: contents`, so the wrappers do not introduce another layout or scrolling layer. + +The center region retains horizontal cell virtualization. Pinned and active sticky cells are always materialized, while ordinary center cells continue to be created/cleaned according to the rendered pixel range. + +### Horizontal positioning + +The one native viewport scrolls the full-width canvas. The small number of rendered left/right row regions receive `translateX()` updates derived from that single `scrollLeft`: + +- left region translation: `scrollLeft`; +- right region translation: `scrollLeft + viewportClientWidth - contentWidth`. + +This is necessary for right-pinned cells to be visible immediately. Pure `position: sticky; right: 0` does not pull an element whose natural position starts beyond the right side of a wide canvas into the initial viewport. + +The header, header-row, footer, and optional panel content receive whole-layer `translate3d(-scrollLeft, 0, 0)` transforms. Pinned chrome receives the inverse docking offset so it remains fixed at the edge. Horizontal virtual-cell rendering is queued behind the scroll task, and ordinary rows with only leading pinned columns avoid redundant per-scroll style writes because their left region already uses CSS sticky. + +The old paired header coordinate trick (`-1000px` on the header root plus `+1000px` on +header-column rules) has been removed from the docking renderer. Header widths no longer include +the `HEADER_WIDTH_SLACK` value, and grouped/pre-header titles use the same normal coordinate +system. This is intentional cleanup for the major-version rewrite; the offset was layout +technical debt from the old pane renderer, not a pinning or virtualization requirement. + +An attempted shared sticky-canvas coordinate system was **reverted** because it broke the far-right scroll geometry (visible blank space and header/body misalignment). The replacement preserves the canvas as a normal full-width element and moves horizontal scrolling to one dedicated scrollbar overlay aligned with the body viewport. The body viewport is now vertical-only; its canvas, the pinned-row overlay, headers, header row, footer, and optional panels all receive the same `translate3d(-scrollLeft, 0, 0)` from the dedicated scrollbar's scroll event. A pair of scoped CSS variables applies the inverse offset to left/right pinned regions and pinned chrome, so regular rows no longer receive individual JavaScript positioning writes during horizontal scrolling. This is the POC's current attempt to remove native-body/header visual tearing while retaining a single horizontal scrollbar and normal canvas scroll extent; it still needs visual confirmation. + +The always-created right-region wrapper is now marked active only when right pinning has a non-zero width. This prevents a zero-width `slick-pinned-right-cells` region (present for the stable left/center/right row shape) from drawing a spurious pinned-border line in left-only pinning scenarios. + +The pinned-row overlay now uses the full canvas/docking content width rather than the visible viewport width and is refreshed after every canvas-width update. Since the dedicated horizontal scrollbar translates the overlay by `-scrollLeft`, a viewport-sized (or stale) overlay would clip itself and expose a trailing blank square as soon as it scrolled right. The top pane remains the clipping boundary. + +Example 04 keeps `enableAutoSizeColumns: true` to preserve the existing option +contract. The shared resizer service invokes `autosizeColumns()` after +browser/container resize, and the pinning layout must continue updating its +canvas, proxy scrollbar, and docked regions correctly when those widths change. + +Example 04 now exercises both docking edges by default: the first three columns are pinned left and the final `Action` column is pinned right. A separate `Pinned Right` count control updates the right band dynamically; setting it to zero removes right pinning, and the existing remove button clears both edges. + +### Unified docking resolver + +New DOM-free `DockingController` resolves both axes: + +- permanent left/right columns; +- center columns and their natural offsets; +- sticky left/right columns activated only after they have been fully seen and then clipped; +- permanent top/bottom rows; +- sticky rows activated after they have been fully seen and scroll past the top edge; +- viewport-percentage pixel budgets; +- `conveyor`, `clamp`, and `priority` sticky overflow policies; +- revision counters so DOM membership changes happen only when a docking boundary is crossed, not on every scroll pixel. + +Scroll-activated sticky docking is now enabled through the same resolver as permanent pins. +Example 47 uses it for Q1–Q4 and its three summary rows. The transition path +still needs visual validation before this can be considered production-ready. + +`sticky: true` means the leading edge (`left` in LTR and `right` in RTL). Explicit `'left'` and `'right'` remain physical edges. + +### Sticky feasibility and quarterly-style groups + +The current pinning implementation is a viable base for sticky columns/rows. Permanent +pins and scroll-activated sticky items already resolve into the same left/center/right and +top/bottom bands, and both kinds of item can be represented by the same per-row regions. +Completing the existing sticky path should therefore be an incremental change rather than a +second rendering architecture: + +- keep the single native horizontal and vertical scrollbars; +- let the controller activate/deactivate sticky candidates only when a visibility boundary is + crossed (not on every scroll pixel); +- keep center cells horizontally and vertically virtualized; only configured pinned/sticky + cells and rows are materialized outside the normal range; +- use the same width/height budgets, hysteresis, resize invalidation, and overlay stacking + already needed for permanent pins. + +For a basic sticky-column/sticky-row feature (including coexistence with permanent pins), a +rough **+150 to +300 library LOC** is expected after the current POC is cleaned up. Most of +that work is transition edge cases: large scroll jumps, RTL, resize/reorder, variable row +heights, and deciding how sticky rows are pushed off when several candidates compete. + +The user's quarterly example is also feasible, but there are two different scopes: + +1. If Q1/Q2/etc. are ordinary columns with `sticky: 'left'` (or a runtime sticky callback), + the basic estimate applies. +2. If Q1 is a group header spanning January–March and the group itself must remain visible, + grouped-header metadata and a separate sticky group-header layout are required. That is + approximately **+150 to +300 additional library LOC**, with explicit rules for groups that + cross a pinned/center boundary and for push-off/replacement as the next quarter enters. + +This is still compatible with permanent pinning: a permanent pin always wins its edge budget, +while sticky candidates use the remaining center viewport. The performance model remains +O(configured sticky candidates) per scroll event and O(1) DOM work between boundary crossings; +large datasets continue to render only the normal virtual range plus the small docked set. +Sticky activation is enabled for Example 47's quarterly columns. Permanent pinning and sticky +transition visuals still require the same acceptance review before the API is finalized. + +### Pinned/sticky rows and virtual scrolling + +Pinned row references are resolved to row indexes and cached. With a plain array, resolving a string ID may scan the dataset once; subsequent vertical scroll events are O(number of configured docked rows). With a SlickDataView, `getRowById()` is used when available. + +The normal virtual rendered range is unchanged. Only configured top/bottom rows are additionally rendered, so a million-row dataset does not produce a million-row DOM. + +Pinned and active sticky rows reuse their normal cached row element, but are reparented into a small overlay outside the scrolling canvas. Their vertical `top`/`bottom` coordinates are constant during scrolling; only the center cell region follows horizontal scroll. No additional tall top/bottom canvas is created. + +Pinned rows now live outside the scrolling canvas, so their vertical coordinate does not +change as `scrollTop` changes. Transforms remain available for ordinary rows because a +growing transform on a row inside the scrolling canvas produced visible jumps during +virtual-page recycling. Virtual-page changes update row positions only after the physical +scroll position and page offset have both been committed, avoiding a transient mixed-coordinate +frame. Pinned region boundaries use the current `--slick-pinned-border-right` and +`--slick-pinned-border-bottom` theme variables for body rows and column chrome. The former +Legacy theme variable names are migration-guide references only. The horizontal +row boundary is emitted only on the last top-pinned row (or first bottom-pinned row), rather +than repeating across every pinned row. Per-scroll vertical updates no longer rewrite pinned rows; normal +virtual rows are repositioned only when a page offset actually changes. The overlay now +inherits the normal grid-cell typography, borders, alternating backgrounds, and selection +styles, and is stacked above hovered scrolling rows so the pinned content cannot show through. +Header-row and footer cells in pinned bands now receive explicit border-box widths, so filter +controls track left- and right-pinned column resizing equally. +Their width calculation now preserves content-box semantics and subtracts each element's +horizontal padding/border from the rendered header width, preventing fractional header/body +boundary offsets. +Pinned body-region borders no longer consume the measured content width (`content-box`), keeping +body cell widths consistent with the fractional header/filter widths. +Column-resize auto-scroll is now limited to center columns; resizing a permanently pinned +right column no longer forces the native horizontal viewport to jump to its maximum position. +Pinned body regions and header/filter/footer chrome now use opaque theme backgrounds and a +dedicated stacking layer, preventing center cells or hovered rows from painting over pinned +content during width updates. +For ordinary scrolling rows, the permanently left-pinned region uses native CSS sticky +positioning at the leading edge. The canvas and pinned-row overlay now share the same +CSS-variable horizontal transform as headers and filters, while scroll-activated sticky docking +is resolved through the shared controller. + +## New POC APIs + +### Column definition + +```ts +interface Column { + pinned?: 'left' | 'right' | null; + sticky?: 'left' | 'right' | 'both' | boolean; +} +``` + +Examples: + +```ts +{ id: 'title', field: 'title', pinned: 'left' } +{ id: 'total', field: 'total', pinned: 'right' } +{ id: 'country', field: 'country', sticky: true } +{ id: 'quarter1', field: 'quarter1', sticky: 'both' } +``` + +### Grid options + +```ts +interface GridOption { + pinning?: { + columns?: { + left?: number | Array; + right?: number | Array; + }; + rows?: { + top?: Array; + bottom?: Array; + }; + }; + stickyRows?: { + top?: Array; + bottom?: Array; + both?: Array; + }; + docking?: { + maxColumnViewportWidthPercent?: number; // default 60 + maxRowViewportHeightPercent?: number; // default 60 + overflowStrategy?: 'conveyor' | 'clamp' | 'priority'; + stickyHysteresis?: number; // default 2px + }; +} +``` + +Row references can currently be row indexes or values from `datasetIdPropertyName` (default `id`). Numeric references prefer row-index semantics when they are within the current data range. + +The unified `GridOption.pinning` shape now owns both column bands and row bands +(`pinning.columns.left/right` and `pinning.rows.top/bottom`). `Column.pinned` +remains the per-column representation for explicit/non-contiguous pinning. +Runtime updates and state serialization use the nested shape so initial options, +dynamic updates, and grid-state persistence cannot drift apart. + +### Runtime grid methods + +```ts +grid.getPinnedColumns(side?); +grid.setColumnPinning(columnId, 'left' | 'right' | null); +grid.setColumnStickiness(columnId, true | false | 'left' | 'right' | 'both'); +``` + +Rows are changed through `grid.setOptions({ pinning: { rows }, stickyRows })`. +`stickyRows.top` docks a seen row after scrolling below it, while `stickyRows.bottom` +docks a seen row after scrolling back above it. `stickyRows.both` chooses the closest vertical +edge after normal scrolling would clip the row. + +## Example 04 conversion + +Vanilla Example 04 now loads the equivalent of its previous configuration through the new APIs: + +- the previous column boundary becomes `pinning.columns.left: 2`; core expands that + inclusive boundary to the first three final visible columns (checkbox, title, + percent complete). Explicit arrays remain available for non-contiguous pins; +- the previous top-row count becomes `pinning.rows.top: [0, 1, 2]`; +- the existing column-count, row-count, remove, set-three, top/bottom, and large-width controls now mutate the nested `pinning` option; +- Grid Menu/Header Menu pin commands now write the canonical `pinning` state without recreating + a second pane; +- the page title identifies it as the single-viewport POC. + +Example variable/function names and Cypress `data-test` attributes now use pinning terminology. +The old names remain only in the v11 migration guide where they are needed as migration inputs. + +## Example 47 — sticky financial-report fixture + +Vanilla Example 47 reproduces the report shape from Discussion 1237's animated mockup: + +- the example intentionally has **no permanent pins**; +- `Account`, Q1–Q4, and YTD retain their natural locations and declare `sticky: 'both'`, so + each docks to the nearest edge only after scrolling would clip it; +- the three report totals (`Total Revenue`, `Total Expenses`, and `Net Profit`) declare + `stickyRows.both`, so they dock to whichever vertical edge is closest after they have been + seen; they retain the dark summary band from the mockup; +- follow-on Capex, headcount, R&D, grants, FX, and provisions rows remain after Net Profit, + allowing the statement totals to be crossed in both vertical scroll directions; +- normal manual grid scrolling is used to inspect the sticky transitions. + +Example 47 is the primary fixture for validating sticky columns, future sticky summary rows, +and later group-header behavior without conflating those semantics with Example 04's +permanent-pinning controls. + +## User-observed status + +- Initial load first failed in `getHeaderChildren()` because column resize assumed `_headers[1]` existed. +- That was fixed by flattening the connected header collection. +- SortableJS no longer assumes or creates a connected second header instance. +- The user subsequently reported no more console errors before the Example 04 API conversion. +- Visual correctness after the final Example 04 conversion and right-edge transform change still needs user confirmation. +- Sticky columns are enabled in Example 47 for Q1–Q4; their large-scroll, resize, RTL, and + multi-sticky visual behavior still needs manual confirmation. +- The stable docking-region DOM is now implemented and the old 1000px header offset has been + removed. Header/row/footer regions and per-row body regions should now be selected by their + explicit left/center/right classes rather than by legacy pane roots. +- Horizontal scrolling is conditional: the active horizontal scroll element is always + exposed as `.slick-horizontal-scroller`. Ordinary grids apply that class to the legacy + `.slick-viewport.slick-viewport-top.slick-viewport-left`, while grids with permanent pinning + or sticky docking apply it to `.slick-docking-horizontal-scroller`. The docking scroller is + materialized lazily if pinning/sticky state is enabled after initialization. +- The native vertical scroll element is always exposed as `.slick-vertical-scroller` and remains + separate from the docking horizontal scroller when pinning or sticky docking is active. + +## Files changed + +Core implementation: + +- `packages/common/src/core/dockingController.ts` — new shared docking resolver. +- `packages/common/src/core/slickGrid.ts` — single live viewport, stable header/body regions, + per-row pin/sticky routing, scrolling, row caching, runtime API, validation, and hit-testing fixes. +- `packages/common/src/services/headerGrouping.service.ts` — grouped/pre-header titles now use + normal coordinates without the legacy 1000px offset. +- `packages/common/src/styles/slick-grid.scss` — three-region row layout and docked stacking styles. +- `packages/common/src/core/index.ts` — public `DockingController` class export; its data types + are exported from the interfaces barrel. + +Public types: + +- `packages/common/src/interfaces/docking.interface.ts` +- `packages/common/src/interfaces/column.interface.ts` +- `packages/common/src/interfaces/gridOption.interface.ts` +- `packages/common/src/interfaces/index.ts` + +POC demonstration: + +- `demos/vanilla/src/examples/example04.ts` +- `demos/vanilla/src/examples/example04.html` +- `demos/vanilla/src/examples/example47.ts` +- `demos/vanilla/src/examples/example47.html` +- `demos/vanilla/src/examples/example47.scss` + +## Validation completed + +The following implementation-only checks passed: + +```bash +pnpm --filter @slickgrid-universal/common exec tsc --noEmit --pretty false +pnpm --dir demos/vanilla type-check +pnpm exec oxlint packages/common/src/core/slickGrid.ts +pnpm exec prettier --write +git diff --check +``` + +Static validation for the recent cleanup passed: common-package TypeScript, Oxlint, +Prettier, and `git diff --check`. The focused DockingController/pinning unit suite passes +(34 tests), and the common SlickGrid coverage run reports 100% statements, functions, and lines +for `slickGrid.ts`. The framework Cypress TypeScript configs also pass after +the custom-command typing fix. The root Cypress config still reports unrelated existing +errors in `test/cypress/support/common.ts` and `test/cypress/support/index.ts`; a Cypress +run in the current agent environment exits with code 132 before browser startup. + +The Cypress custom-command return-type fix was applied consistently to the root, Angular, +Aurelia, React, and Vue support copies: `getCell`/`getNthCell` now return +`Chainable>`, and `convertPosition` has its concrete chainable shape. +Do not undo this narrowing when revisiting Cypress typings. The remaining root support errors +are separate pre-existing typing issues and should be handled independently. + +## Current production LOC delta and cleanup estimate + +These are rough **library-only** figures for `packages/common` (including SCSS and public +interfaces, excluding Example 04, tests, generated output, and framework-wrapper changes). +They are calculated from the current diff, counting the two new files as additions: + +- current working-tree library diff: approximately `+2,886 / -387`, or **+2,499 net LOC** + relative to `HEAD` (packages, excluding tests/demos); +- this includes the new `DockingController` and docking types, single-viewport/per-row + routing, sticky/pinning hardening, and the pinning/docking stylesheet changes; +- this remains a provisional figure because internal pane-shaped aliases and historical style + names still need to be assessed separately from the removed runtime pane implementation. + +The earlier 800–1,200-line removal estimate is retained only as a planning range. It is not a +final forecast: the exact permanent-pinning LOC must be measured after the remaining internal +alias cleanup and the decision on whether historical theme variable names are retained. + +Hardening basic sticky columns/rows would add roughly **+150–300 LOC**, giving an estimated +**+120 to +670 net LOC** after cleanup. Supporting grouped quarterly sticky headers would add +another **+150–300 LOC**, for an estimated **+270 to +970 net LOC** in the broader design. +These ranges are planning numbers, not a final count; the deletion pass and grouped-header +requirements are the two largest sources of variance. + +## Known limitations and likely breakage + +### Framework parity and recent Cypress regressions (2026-09-09) + +- Angular and React Example 20 no longer install the obsolete hover-selection handlers that + selected a row and called `preventDefault()` on mouse enter/leave. Those handlers were tied to + the old split-pane renderer and could interfere with opening a Cell Menu from a pinned Action + cell. Their behavior now matches Vue and Aurelia. +- The Example 20 cell-menu option callback uses each framework's grid service to update the + selected item. Angular no longer calls the removed SlickGrid `updateItem()` method directly. +- Angular Example 25's grid-menu regression was caused by a stale Cypress double-click pattern; + its menu-opening step now uses one click, matching Vue. The subsequent French metrics failure + was a cascade from the filters not being cleared. +- These framework/demo fixes preserve the single horizontal scroll-owner contract: use + `.slick-horizontal-scroller` for horizontal scrolling and `.slick-vertical-scroller` for + vertical scrolling. The more specific `.slick-docking-horizontal-scroller` remains available + for docking grids. + +### Latest visual fixes (2026-09-03) + +- The dedicated horizontal scrollbar now reserves its measured height from the live body viewport. Unlike the native scrollbar it replaces, the proxy is absolutely positioned and otherwise covered the last fully scrolled row. +- Financial-report summary rows now force their dark foreground/background palette on individual cells, including when a sticky row is moved to the docking overlay; this prevents an inherited canvas background from making Total Expenses unreadable. +- Bottom sticky-row activation now tests the row's bottom edge rather than its top edge. Bottom candidates are resolved upward from the viewport edge, reserving the height of each already-docked row; a preceding summary therefore docks against Net Profit rather than one full row-height late. +- Sticky-row transitions use the exact top/bottom boundary instead of the configurable 2px column hysteresis, preventing an otherwise visible 1–2px snap into the docking overlay. +- Example 47's sticky candidate and active sticky cells/headers now consistently use the exact `#e4edf7` report blue with higher CSS priority than odd-row striping; docking no longer darkens the cells. +- Horizontal sticky-column thresholds and cell coordinates now use the visible body width (excluding the vertical scrollbar gutter), preventing right stickies from activating 10–15px late. Scroll transitions commit the current header transform before measuring right chrome, avoiding stale left/right header offsets when the right sticky set changes. +- The initial leftmost sticky-column pass now seeds configured candidates as eligible, allowing offscreen-right Q3/Q4/YTD columns to dock immediately at load instead of requiring a right-and-back scroll first. +- The initial top sticky-row pass likewise seeds configured rows as eligible, allowing two-sided report summary rows to dock at their nearest vertical edge immediately at load. +- Example 47's YTD definition now retains the shared sticky-candidate classes when adding its YTD-specific classes, so it keeps the sticky blue background even when it reaches its natural right edge and Q4 takes over the separator. +- Added a higher-specificity right-edge separator rule for header, header-row, and footer chrome so the first right-sticky column title/filter receives the same `--slick-pinned-border-right` separator as the body region. +- Removed the non-user-facing Example 47 auto-scroll control and timer; the fixture now uses only normal manual grid scrolling. +- Draggable Grouping now tolerates the single-viewport layout: it creates a Sortable instance only for header containers that actually exist, instead of passing a removed right header (`null`) to SortableJS. +- Pinned left/right edge header-row and footer cells now use border-box sizing with the full measured header outer width. This keeps an empty edge filter cell aligned with its data cells when the pinning separator contributes a border (for example, 100px data cell versus 98px header-row cell). +- The single horizontal scrollbar proxy now has an opaque canvas background, themed `scrollbar-color`, pointer events, and an isolated stacking context. Its z-index remains above grid rows but below application overlays such as Bulma navbar menus, and its track is aligned to the pane content edge. +- The docking scrollbar now uses `overflow-x: auto` and sizes its spacer from the natural docking content width. When all columns fit the viewport, the proxy has zero height and no horizontal track is shown; when overflow exists, its height still comes from the measured native scrollbar dimensions. +- The full-width docked-row overlay now uses a scroll-aware clip window equal to the viewport's content width. It can still retain enough translated width for right-pinned cells, while excluding the native vertical scrollbar strip from overlay painting. +- The docked-row overlay stacking layer is now `z-index: 5`, matching the normal pinned-row layer. This keeps pinned rows above scrolling cells but below application overlays such as Bulma navbar dropdowns (`z-index: 20`). +- In single-viewport mode the header-row scroller now gets an opaque header-row background. Its unused trailing gutter (the body viewport's scrollbar space) no longer reveals translated center columns when widths change; logical right-pinned column widths remain unchanged. +- The right-edge pinned header-row cell now extends by the measured vertical scrollbar width when that scrollbar is present. This fills the header-only gutter directly while leaving body cells and right-pinning offsets at their normal widths. +- The right-edge pinned header-row cell is positioned by the computed physical separator border width (with RTL-aware direction), rather than a hard-coded 1px, to match customized or hidden borders. +- Example04 now clears the opposite `pinning.rows` side when toggling top/bottom. This is required because `setOptions()` deep-merges nested option objects; supplying only `{ bottom }` previously left the old top references active. +- Example04 bottom mode now pins the last configured rows instead of reusing indexes `0..N`. This matches the former bottom-pinning behavior and prevents the first rows' natural slots from becoming blank when they move to the bottom overlay. +- Docked left/right row regions now mirror odd-row striping and hover backgrounds. Their opaque pinning backgrounds no longer hide the configured gray odd-row color. +- Docked rows no longer receive the legacy active-row padding, preventing every cell in a clicked row from shrinking. Active-cell coordinate resolution now handles rows rendered in the docking overlay, allowing editors to open on top-pinned cells. +- Docked rows now receive an explicit resolved `rowHeight` inline, including the default value. This prevents active/editor box-model styles from reducing a configured 45px row to its 35px content height. +- Cell interaction handlers are bound to the docking overlay as well as the canvas, enabling click/auto-edit and double-click editing for top- and bottom-pinned rows. +- Example04 includes a Toggle Right Pinning button that switches the right-pinned Action column on/off while preserving the configured left pins. +- `internal_setOptions()` now renders after `setColumns()` invalidation. This fixes dynamic row-pinning count changes, which were previously rendered and then cleared when the column refresh removed cached rows. +- `setOptions()` now replaces `pinning.rows.top`/`bottom` arrays atomically instead of deep-merging them. This removes stale row references when the configured pin count decreases. +- Browser grow-after-shrink handling now separates the natural column-content width from the rendered docked-row width. The canvas and row center region grow to at least the body viewport, preventing a white gap before a right pin; right-pinned body regions use the rendered-width offset while header chrome retains natural scroll coordinates. Right-pinned header cells are also taken out of flex flow and explicitly positioned, so their titles remain at the visible right edge. The inner header/header-row/footer column containers now allow this docked chrome to overflow to their existing outer viewport clip; the old inner `overflow: hidden` was clipping every right-pinned header title and filter. This needs visual confirmation in Example 04 after shrinking the browser and restoring it to full width. +- Right-pinned header chrome no longer uses the shared natural-content transform used by row regions. Each right-pinned header/header-row/footer cell is positioned at its direct viewport coordinate (`scrollLeft + viewportWidth - rightBandWidth + columnOffset`) inside the already translated chrome layer. This fixes titles landing beside a center column and supports multiple right-pinned columns; visual confirmation remains pending. +- The viewport width used for right-pinned chrome is now read from the header scroller itself, rather than from the horizontal-scroll proxy. The proxy can retain a stale narrow width during resize (for example, yielding `left: 1537px` from a 1637px proxy for a 100px column), while the header scroller is the actual visible clip boundary. Visual confirmation is pending. +- The legacy `-1000px` header-container / `+1000px` header-column coordinate pair has now been + removed from the single-viewport renderer and grouped-header service. Header widths no longer + include the corresponding 1000px slack. If any remaining legacy pane path is temporarily + exercised during migration, it must not be mixed with the new docking coordinate system. +- Bulk pin/unpin state is keyed by stable column IDs rather than + column object identity. This preserves the generated-pin bookkeeping across + `updateColumnProps()` cloning and makes the existing `Unpin All Columns` command + reliably restore the pre-pinning state. +- Sticky-column horizontal scrolling now keeps the scrollbar/compositor path + immediate while coalescing sticky-band resolution to one animation-frame pass. + On a band transition, rendered cells are moved between their existing + left/center/right row regions instead of being discarded and reformatted; the + same frame updates only those row-wrapper dimensions and lets the regular + deferred virtualizer fill missing cells. A full row rebuild remains only as a + safe fallback when a layout gains its first docking region. Permanent-pinning- + only grids retain the synchronous fast path; visual validation of Example 47 + is still required. +- Empty left docking regions no longer paint the left separator: the pinned + border is now enabled only while the left region contains an active docked + column, including when sticky membership changes during scrolling. +- Right-edge sticky/pinned header, header-row, and footer chrome now offsets + its physical start coordinate by the configured separator border width, so + the title/filter border aligns with the body separator for any border size. +- Vanilla Example 11 view presets now retain and restore the complete pinning + state again. Creating/updating a view serializes `GridState.pinning`, reset + clears permanent pinning, and selecting a view reapplies pinning after its + column layout (the required order for hidden/reordered columns). +- Removed the duplicate `pinnedColumn` and `pinnedRows` grid options. Header-menu + bulk pin/unpin and row docking now write/read the canonical `pinning` + object directly; `Column.pinned` remains available for explicit per-column pins. +- Reworked the Vanilla Example 04 Cypress spec for the persistent docking DOM: + header assertions now query `.slick-header-column` descendants, row assertions + target `data-row` plus cell index, and no-pinning checks expect stable empty + left/right regions rather than removed panes. Header-menu, accessibility, large-scroll, + and reorder cases are enabled again; the two autocomplete-editor cases remain skipped + pending browser/component follow-up. +- Restored the invalid-hide alert contract for pinning. The canonical pinning validation + now checks the prospective visible set against the docking layout, so hiding the last + available center column is rejected without mutating the grid. +- Column reorder now creates Sortable instances for the persistent left, center, + and right docking wrappers and combines their order on drop. This keeps drag + auto-scroll and reorder functional after the old right pane is removed. +- Sticky transitions now rehome header titles, header-row filters, and footer + cells into the same persistent left/center/right wrappers as their body cells, + then restore column order within each wrapper. This fixes the intermittent + Example 47/48 state where body Q2/Q3 had moved bands but their chrome remained + in the old wrapper and appeared misaligned while scrolling. Left-band chrome + now uses the grouped wrapper flow rather than subtracting its old natural + center-column offset, keeping Q1/Q2 visible at the far-right scroll position. +- Docking chrome now keeps the region bands as direct `.slick-header-columns` + (and equivalent header-row/footer) children of a separate `*-columns-root`. + This preserves the legacy selector contract where `.slick-header-columns` + `.children()` are actual cells, while still exposing stable left/center/right + region classes for pinning/sticky grids. Empty explicit pinning keeps the row + bands stable; grids with no pinning configuration remain flat. +- Tightened docking activation so an empty `pinning.columns` state does not + create nested header wrappers, while an explicitly pinning-configured grid + retains predictable row-region DOM after clearing pinning. Fixed the related + TypeScript narrowing error in `hasConfiguredRowDocking()`. +- Example 04 vertical-scroll coverage now uses rows that exist in its 40-item + fixture, resets both scroll owners between suites, and identifies reordered + columns by stable IDs. With the dev server running, Firefox headless Cypress + passes all 42 Example 04 tests with retries disabled. Electron cannot start in + this environment because its bundled binary exits with SIGILL. +- Example 04's large-column action now applies its explicit layout before + applying pinning. This avoids validating stale, previously resized widths and + accidentally clearing pinning (which removed the center docking region before + the final drag/reorder test). +- Added dedicated right-pinning coverage to Example 04: multiple right columns, + header/header-row regions, horizontal-scroll retention, numeric disable/re-enable, + removing the first right-pinned column, and restoring the hidden edge column. + The focused Firefox spec now passes all 46 tests. +- Example 04 now explicitly enables `showHeaderRow`. The persistent docking + header-row root also receives `headerRowHeight`; without that root height, + `display: contents` band wrappers collapsed the visible filter bar to the + 1px spacer height. +- Example 17 Cypress migration is in progress. Its demo now uses canonical + `pinning` instead of inert legacy options, and the shared drag helper reads the + docking horizontal scrollbar for single-viewport grids. Active canvas/viewport + fallback now also supports drag selection from pinned-row overlays. A related + `scrollRowIntoView()` fix accounts for top/bottom docked-row height when + determining the usable center viewport. Firefox currently reports 12 passing, + 1 pending (the intentionally skipped grouping auto-scroll case), and one + remaining bottom-edge assertion that still needs follow-up; do not spend more + migration time here until the other spec failures are triaged. + +1. **Legacy runtime removal is complete.** The old options, interfaces, state/service + fields, validation names, and pane behavior have been removed. Internal pane-shaped aliases + and historical CSS variable names remain as separate cleanup/documentation decisions; they + must not become compatibility branches. +2. **Old options intentionally no longer work.** The former flat options are not valid ways to + configure this POC. The old names and command ids are migration-guide + references only; active menus use `Pin Through Here`/`Unpin All Columns` and write the canonical + `pinning` option. `GridService.setPinning()` accepts the unified nested shape. +3. **Visual/browser validation is incomplete.** Left/right pinning, bottom rows, sticky transitions, resize, reorder, RTL, variable row height, row/column spans, editors, selection, and all four framework wrappers need manual follow-up. +4. **Colspans crossing docking bands are not defined.** A colspan beginning in one band and ending in another can produce incorrect geometry. The final design should reject, split, or explicitly define this case. +5. **Grouped/pre-header chrome is not dock-aware yet.** Standard column headers, header-row cells, footer cells, and body cells are wired. Multi-level/group header layout needs a dedicated band implementation. +6. **Sticky activation can be skipped by a very large scroll jump.** Candidates currently must first be fully visible. A final implementation should detect that the scroll path crossed a candidate even if no intermediate frame showed it fully. +7. **Numeric row reference ambiguity.** An in-range number is treated as a row index before it is treated as a dataset ID. A tagged `{ id } | { index }` row reference would remove this ambiguity. +8. **Pinned rows remain part of the normal dataset height.** They reuse/move the real row node and their natural dataset slot remains represented in scroll geometry. Confirm this product semantic against the desired AG Grid behavior. +9. **Permanent pin over-allocation is rejected at the API boundary.** Pinning every visible + column or consuming the whole viewport invokes the configured canonical pinning validation + callback and leaves the prior pinning state intact. +10. **Column reorder policy is undecided.** The visual order groups permanent pins at the edges, but dragging between center and pinned regions does not yet automatically change `pinned` state. +11. **Header Menu terminology is now pinning-based.** The `Column Pinning` root opens an + always-visible sub-menu containing `Pin Left`, `Pin Right`, `Pin Through Here`, a separator, + `Unpin Column`, and `Unpin All Columns`. The directional commands write `Column.pinned`, the + through-here command writes `pinning.columns.left`, and the unpin commands clear the selected + column or all aggregate column edges. The removed v10 names remain documented in the migration + guide only. +12. **Public controller surface is provisional.** `DockingController` is currently exported for the POC; it may be better kept internal in the final API. +13. **Migration references are intentionally narrow.** Historical option names, command ids, + translation keys, and labels belong in the v11 migration guide. Active runtime code and + examples use pinning terminology; only the `--slick-pinned-*` theme variables remain as + the current styling API. + +## Documentation scope + +The v11 migration guide is currently maintained for the vanilla/root documentation only: +`docs/migrations/migration-to-11.x.md`. Framework-specific migration guides are intentionally +deferred until the vanilla guide and API cleanup are settled; do not add framework v11 guides yet. + +## Resume checklist + +1. Reload Vanilla Example 04 and confirm: + - only one horizontal and one vertical scrollbar are visible; + - checkbox/title/percent-complete stay pinned left; + - the first three rows stay pinned top; + - toggling rows to the bottom works; + - widening the first three columns preserves the right/center layout; + - cell clicking, editing, filtering, resizing, and horizontal scrolling still work. +2. Add at least one temporary right-pinned column in Example 04 and confirm it is visible before horizontal scrolling and remains aligned vertically/horizontally. +3. Use Example 47's Account/Q1–Q4/YTD sticky columns and bottom sticky totals to confirm activation/deactivation/hysteresis. Add a top sticky-row counterpart only after deciding its report hierarchy/push-off behavior. +4. Fix visual/interaction problems before adding tests. +5. Review the implemented unified `GridOption.pinning` shape and `CurrentColumn.pinning` precedence before freezing the public API. The former `pinnedColumn`/`pinnedRows` shorthands have been removed. +6. Review the pinning-based Header Menu: `Column Pinning` must keep `Pin Left`, `Pin Right`, and + `Pin Through Here`, a separator, and `Unpin Column`/`Unpin All Columns` visible in the same + sub-menu. Keep the old references documented for v11-and-lower users without adding runtime + aliases. +7. Complete the remaining structural audit: + - rename/remove internal `_pane*`, `_viewport*`, and `_canvas*` aliases where practical; + - document the current `--slick-pinned-*` theme variables and their v11-and-lower names; + - update remaining demo labels/selectors only where it does not conflict with migration coverage. +8. Recalculate production LOC after the structural audit. +9. Keep unit, coverage, Cypress, framework, and documentation work aligned with the cleaned API; + do not reintroduce the removed runtime options or pane renderer. + +## New-context handoff checklist + +- Treat this file and the current working tree as the source of truth; do not restart the POC + from the old PR 1238 multi-pane branch. +- The legacy runtime options/interfaces and pane behavior are removed. Continue with the + structural alias/style audit, but do not restore compatibility branches for old configuration. +- Before changing layout code, preserve the current invariants: one native horizontal scroll, + one native vertical scroll, one rendered row with left/center/right regions, stable header / + header-row / footer region wrappers, and one shared `DockingController`. +- Re-run the focused checks after edits. A browser/Cypress failure in the current agent + environment may be infrastructure-related when the process exits with code 132 before + browser startup; distinguish that from a real spec failure. +- During the structural audit, distinguish intentional migration references (docs, command IDs, + locale text, demo selectors, and theme variables) from runtime configuration. Audit pane aliases, + synchronized scroll branches, header-width slack, GridState/GridService, resizer, header menus, + extensions, and all four framework wrappers, then recalculate LOC from `git diff HEAD` and update + this file again. + +## Suggested resume prompt + +> Read `PINNING-POC-PROGRESS.md` and inspect the current diff. This is a major-breaking +> single-native-scroll pinning/stickiness rewrite, not an extension of the old pane renderer. +> The legacy runtime options/interfaces and pane behavior have already been removed. +> Preserve one live viewport, one row node with left/center/right cell regions, and the shared +> `DockingController`; do not add legacy compatibility branches. Finish the structural alias/style +> audit across `SlickGrid`, framework wrappers, menus, and theme variables, then run focused +> regressions and recalculate the production-library LOC. diff --git a/demos/aurelia/src/assets/i18n/en/aurelia-slickgrid.json b/demos/aurelia/src/assets/i18n/en/aurelia-slickgrid.json index b412eea6d6..600e68cdc1 100644 --- a/demos/aurelia/src/assets/i18n/en/aurelia-slickgrid.json +++ b/demos/aurelia/src/assets/i18n/en/aurelia-slickgrid.json @@ -7,7 +7,7 @@ "CLEAR_ALL_FILTERS": "Clear all Filters", "CLEAR_ALL_GROUPING": "Clear all Grouping", "CLEAR_ALL_SORTING": "Clear all Sorting", - "CLEAR_PINNING": "Unfreeze Columns/Rows", + "CLEAR_PINNING": "Unpin Columns/Rows", "CLONE": "Clone", "COLLAPSE_ALL_GROUPS": "Collapse all Groups", "COLUMNS": "Columns", @@ -28,7 +28,10 @@ "FILTER_SHORTCUTS": "Filter Shortcuts", "FROM_TO_OF_TOTAL_ITEMS": "{{from}}-{{to}} of {{totalItems}} items", "FORCE_FIT_COLUMNS": "Force fit columns", - "FREEZE_COLUMNS": "Freeze Columns", + "PIN_COLUMN": "Column Pinning", + "PIN_COLUMNS": "Pin Through Here", + "PIN_LEFT": "Pin Left", + "PIN_RIGHT": "Pin Right", "INVALID_FLOAT": "The number must be valid and have a maximum of {{maxDecimal}} decimals.", "GREATER_THAN": "Greater than", "GREATER_THAN_OR_EQUAL_TO": "Greater than or equal to", @@ -63,7 +66,8 @@ "SYNCHRONOUS_RESIZE": "Synchronous resize", "TOGGLE_FILTER_ROW": "Toggle Filter Row", "TOGGLE_PRE_HEADER_ROW": "Toggle Pre-Header Row", - "UNFREEZE_COLUMNS": "Unfreeze Columns", + "UNPIN_COLUMN": "Unpin Column", + "UNPIN_COLUMNS": "Unpin All Columns", "X_OF_Y_SELECTED": "# of % selected", "X_OF_Y_MASS_SELECTED": "{{x}} of {{y}} selected", "BILLING": { diff --git a/demos/aurelia/src/assets/i18n/fr/aurelia-slickgrid.json b/demos/aurelia/src/assets/i18n/fr/aurelia-slickgrid.json index c957bb46b8..832f0207b7 100644 --- a/demos/aurelia/src/assets/i18n/fr/aurelia-slickgrid.json +++ b/demos/aurelia/src/assets/i18n/fr/aurelia-slickgrid.json @@ -7,7 +7,7 @@ "CLEAR_ALL_FILTERS": "Supprimer tous les filtres", "CLEAR_ALL_GROUPING": "Supprimer tous les groupes", "CLEAR_ALL_SORTING": "Supprimer tous les tris", - "CLEAR_PINNING": "Dégeler les colonnes/rangées", + "CLEAR_PINNING": "Désépingler les colonnes/rangées", "CLONE": "Cloner", "COLLAPSE_ALL_GROUPS": "Réduire tous les groupes", "COLUMNS": "Colonnes", @@ -28,7 +28,10 @@ "FILTER_SHORTCUTS": "Raccourcis de filtre", "FROM_TO_OF_TOTAL_ITEMS": "{{from}}-{{to}} de {{totalItems}} éléments", "FORCE_FIT_COLUMNS": "Ajustement forcé des colonnes", - "FREEZE_COLUMNS": "Geler les colonnes", + "PIN_COLUMN": "Épinglage de colonnes", + "PIN_COLUMNS": "Épingler jusqu'ici", + "PIN_LEFT": "Épingler à gauche", + "PIN_RIGHT": "Épingler à droite", "GREATER_THAN": "Plus grand que", "GREATER_THAN_OR_EQUAL_TO": "Plus grand ou égal à", "GROUP_BY": "Grouper par", @@ -63,7 +66,8 @@ "SYNCHRONOUS_RESIZE": "Redimension synchrone", "TOGGLE_FILTER_ROW": "Basculer la ligne des filtres", "TOGGLE_PRE_HEADER_ROW": "Basculer la ligne de pré-en-tête", - "UNFREEZE_COLUMNS": "Dégeler les colonnes", + "UNPIN_COLUMN": "Désépingler la colonne", + "UNPIN_COLUMNS": "Désépingler toutes les colonnes", "X_OF_Y_SELECTED": "# de % sélectionnés", "X_OF_Y_MASS_SELECTED": "{{x}} de {{y}} sélectionnés", "BILLING": { diff --git a/demos/aurelia/src/examples/locales/fr.ts b/demos/aurelia/src/examples/locales/fr.ts index 134f9c5793..f6ec4bb391 100644 --- a/demos/aurelia/src/examples/locales/fr.ts +++ b/demos/aurelia/src/examples/locales/fr.ts @@ -19,6 +19,10 @@ export const localeFrench = { TEXT_EXPORT_TO_TEXT_FORMAT: 'Exporter en format texte', TEXT_FILTER_SHORTCUTS: 'Raccourcis de filtre', TEXT_FORCE_FIT_COLUMNS: 'Ajustement forcé des colonnes', + TEXT_PIN_COLUMN: 'Épinglage de colonnes', + TEXT_PIN_COLUMNS: "Épingler jusqu'ici", + TEXT_PIN_LEFT: 'Épingler à gauche', + TEXT_PIN_RIGHT: 'Épingler à droite', TEXT_GROUP_BY: 'Grouper par', TEXT_HIDE_COLUMN: 'Cacher la colonne', TEXT_LAST_UPDATE: 'Dernière mise à jour', @@ -32,10 +36,11 @@ export const localeFrench = { TEXT_SORT_ASCENDING: 'Trier par ordre croissant', TEXT_SORT_DESCENDING: 'Trier par ordre décroissant', TEXT_STARTS_WITH: 'Commence par', - TEXT_UNFREEZE_COLUMNS: 'Dégeler les colonnes', + TEXT_UNPIN_COLUMN: 'Désépingler la colonne', + TEXT_UNPIN_COLUMNS: 'Désépingler toutes les colonnes', TEXT_X_OF_Y_SELECTED: '# de % sélectionné', TEXT_X_OF_Y_MASS_SELECTED: '{{x}} de {{y}} sélectionnés', - TEXT_CLEAR_PINNING: 'Dégeler les colonnes/rangées', + TEXT_CLEAR_PINNING: 'Désépingler les colonnes/rangées', TEXT_EQUAL_TO: 'Égal à', TEXT_GREATER_THAN: 'Plus grand que', TEXT_GREATER_THAN_OR_EQUAL_TO: 'Plus grand ou égal à', diff --git a/demos/aurelia/src/examples/slickgrid/example07.ts b/demos/aurelia/src/examples/slickgrid/example07.ts index e5cc17a3e6..5803422afe 100644 --- a/demos/aurelia/src/examples/slickgrid/example07.ts +++ b/demos/aurelia/src/examples/slickgrid/example07.ts @@ -68,8 +68,6 @@ export class Example07 { ...this.gridOptions1, enableHeaderMenu: true, enableFiltering: true, - // frozenColumn: 2, - // frozenRow: 2, headerButton: { onCommand: (_e: any, args) => this.handleOnCommand(_e, args, 2), }, diff --git a/demos/aurelia/src/examples/slickgrid/example14.html b/demos/aurelia/src/examples/slickgrid/example14.html index d88969481c..612e4a4783 100644 --- a/demos/aurelia/src/examples/slickgrid/example14.html +++ b/demos/aurelia/src/examples/slickgrid/example14.html @@ -47,14 +47,14 @@


-

Grid 2 (with Header Grouping & Frozen/Pinned Columns)

+

Grid 2 (with Header Grouping & Pinned Columns)

- -
diff --git a/demos/aurelia/src/examples/slickgrid/example14.scss b/demos/aurelia/src/examples/slickgrid/example14.scss index 6e31054384..e41265a0e7 100644 --- a/demos/aurelia/src/examples/slickgrid/example14.scss +++ b/demos/aurelia/src/examples/slickgrid/example14.scss @@ -1,8 +1,8 @@ -/** You can change the pinned/frozen border styling through this css override */ +/** You can change the pinned/pinned border styling through this css override */ -.slick-row .slick-cell.frozen:last-child, -.slick-headerrow-column.frozen:last-child, -.slick-footerrow-column.frozen:last-child { +.slick-row .slick-cell.pinned:last-child, +.slick-headerrow-column.pinned:last-child, +.slick-footerrow-column.pinned:last-child { border-right: 1px solid #969696 !important; } diff --git a/demos/aurelia/src/examples/slickgrid/example14.ts b/demos/aurelia/src/examples/slickgrid/example14.ts index 746d24938f..1c536de1e2 100644 --- a/demos/aurelia/src/examples/slickgrid/example14.ts +++ b/demos/aurelia/src/examples/slickgrid/example14.ts @@ -56,6 +56,7 @@ export class Example14 { preHeaderPanelHeight: 28, gridHeight: 275, gridWidth: 800, + rowHeight: 33, enablePdfExport: true, enableExcelExport: true, excelExportOptions: { @@ -104,15 +105,16 @@ export class Example14 { explicitInitialization: true, gridHeight: 275, gridWidth: 800, - frozenColumn: 2, + rowHeight: 33, + pinning: { columns: { left: 2 } }, enablePdfExport: true, enableExcelExport: true, excelExportOptions: { exportWithFormatter: false, }, externalResources: [new ExcelExportService(), new PdfExportService()], - gridMenu: { hideClearFrozenColumnsCommand: false }, - headerMenu: { hideFreezeColumnsCommand: false }, + gridMenu: { hideClearPinningCommand: false }, + headerMenu: { hidePinColumnCommand: false, hidePinningColumnsCommand: false }, }; } @@ -134,8 +136,8 @@ export class Example14 { return mockDataset; } - setFrozenColumns2(frozenCols: number) { - this.gridObj2.setOptions({ frozenColumn: frozenCols }); + setPinnedColumns2(pinnedCols: number) { + this.gridObj2.setOptions({ pinning: { columns: { left: pinnedCols } } }); this.gridOptions2 = this.gridObj2.getOptions(); } diff --git a/demos/aurelia/src/examples/slickgrid/example15.ts b/demos/aurelia/src/examples/slickgrid/example15.ts index 5d4bf60e7d..2b3f7f42ab 100644 --- a/demos/aurelia/src/examples/slickgrid/example15.ts +++ b/demos/aurelia/src/examples/slickgrid/example15.ts @@ -193,10 +193,11 @@ export class Example15 { }, gridMenu: { hideForceFitButton: true, - hideClearFrozenColumnsCommand: false, + hideClearPinningCommand: false, }, headerMenu: { - hideFreezeColumnsCommand: false, + hidePinColumnCommand: false, + hidePinningColumnsCommand: false, }, enablePagination: true, pagination: { diff --git a/demos/aurelia/src/examples/slickgrid/example20.html b/demos/aurelia/src/examples/slickgrid/example20.html index 1eecb2421b..3ba0e7f903 100644 --- a/demos/aurelia/src/examples/slickgrid/example20.html +++ b/demos/aurelia/src/examples/slickgrid/example20.html @@ -1,77 +1,81 @@ -

- Example 20: Pinned (frozen) Columns/Rows - - +

+ Example 20: Pinned Columns/Rows + +

+

- -
- This example demonstrates the use of Pinned (aka frozen) Columns and/or Rows (Wiki docs) -
    -
  • Option to pin any number of columns (left only) or rows
  • -
  • Option to pin the rows at the bottom instead of the top (default)
  • -
  • You can also dynamically any of these options, through SlickGrid "setOptions()"
  • -
  • Possibility to change the styling of the line border between pinned columns/rows
  • -
-
- -
- -
-
- - - - - - - - - - + >Wiki docs)
-
- -
-
- - - -
+
+
+
+ +
+
+ - : ${ isFrozenBottom ? 'Bottom' : 'Top' } - +
+
+
+
+ + + + + + +
- - -

+
- - - diff --git a/demos/aurelia/src/examples/slickgrid/example20.scss b/demos/aurelia/src/examples/slickgrid/example20.scss index 6e31054384..e41265a0e7 100644 --- a/demos/aurelia/src/examples/slickgrid/example20.scss +++ b/demos/aurelia/src/examples/slickgrid/example20.scss @@ -1,8 +1,8 @@ -/** You can change the pinned/frozen border styling through this css override */ +/** You can change the pinned/pinned border styling through this css override */ -.slick-row .slick-cell.frozen:last-child, -.slick-headerrow-column.frozen:last-child, -.slick-footerrow-column.frozen:last-child { +.slick-row .slick-cell.pinned:last-child, +.slick-headerrow-column.pinned:last-child, +.slick-footerrow-column.pinned:last-child { border-right: 1px solid #969696 !important; } diff --git a/demos/aurelia/src/examples/slickgrid/example20.ts b/demos/aurelia/src/examples/slickgrid/example20.ts index d655caebce..6215b4e6db 100644 --- a/demos/aurelia/src/examples/slickgrid/example20.ts +++ b/demos/aurelia/src/examples/slickgrid/example20.ts @@ -1,9 +1,9 @@ +import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { Editors, Filters, formatNumber, Formatters, - SlickEventHandler, type AureliaGridInstance, type Column, type ColumnEditorDualInput, @@ -11,337 +11,305 @@ import { type SlickGrid, } from 'aurelia-slickgrid'; import { showToast } from './utilities.js'; -import './example20.scss'; // provide custom CSS/SASS styling +import './example20.scss'; export class Example20 { aureliaGrid!: AureliaGridInstance; - columns: Column[] = []; gridObj!: SlickGrid; + columns: Column[] = []; gridOptions!: GridOption; - frozenColumnCount = 2; - frozenRowCount = 3; - hideSubTitle = false; - isFrozenBottom = false; dataset: any[] = []; - slickEventHandler: any; - + pinnedColumnCount = 2; + pinnedRightColumnCount = 1; + pinnedRowCount = 3; + hideSubTitle = false; + isPinnedBottom = false; + isSelectAllShownAsColumnTitle = false; + checkboxSelectorInstance: any; constructor() { this.defineGrid(); - this.slickEventHandler = new SlickEventHandler(); } - - aureliaGridReady(aureliaGrid: AureliaGridInstance) { - this.aureliaGrid = aureliaGrid; - this.gridObj = aureliaGrid && aureliaGrid.slickGrid; - - // with frozen (pinned) grid, in order to see the entire row being highlighted when hovering - // we need to do some extra tricks (that is because frozen grids use 2 separate div containers) - // the trick is to use row selection to highlight when hovering current row and remove selection once we're not - this.slickEventHandler.subscribe(this.gridObj.onMouseEnter, (event: Event) => this.colorizeHoveringRow(event, true)); - this.slickEventHandler.subscribe(this.gridObj.onMouseLeave, (event: Event) => this.colorizeHoveringRow(event, false)); - } - - colorizeHoveringRow(event: Event, isMouseEnter: boolean) { - const cell = this.gridObj.getCellFromEvent(event); - const rows = isMouseEnter ? [cell?.row ?? 0] : []; - this.gridObj.setSelectedRows(rows); // highlight current row - event.preventDefault(); - } - attached() { - // populate the dataset once the grid is ready - this.getData(); + this.dataset = Array.from({ length: 500 }, (_v, i) => ({ + id: i, + title: `Task ${i}`, + duration: `${Math.round(Math.random() * 100)}`, + percentComplete: Math.round(Math.random() * 100), + start: new Date(2009, 0, 1), + finish: new Date(2009, 4, 5), + completed: i % 5 === 0, + cost: i % 33 === 0 ? null : Math.random() * 10000, + cityOfOrigin: i % 2 ? 'Vancouver, BC, Canada' : 'Boston, MA, United States', + })); } - - detaching() { - // unsubscribe every SlickGrid subscribed event (or use the Slick.EventHandler) - this.slickEventHandler.unsubscribeAll(); + aureliaGridReady(g: AureliaGridInstance) { + this.aureliaGrid = g; + this.gridObj = g.slickGrid; } - - /* Define grid Options and Columns */ defineGrid() { + const dual = { + leftInput: { field: 'cost', type: 'float', decimal: 2, minValue: 0, maxValue: 50000 }, + rightInput: { field: 'duration', type: 'float', minValue: 0, maxValue: 100 }, + } as ColumnEditorDualInput; this.columns = [ - { - id: 'sel', - name: '#', - field: 'id', - minWidth: 40, - width: 40, - maxWidth: 40, - cannotTriggerInsert: true, - resizable: false, - unselectable: true, - }, - { - id: 'title', - name: 'Title', - field: 'title', - minWidth: 100, - width: 120, - filterable: true, - sortable: true, - }, + { id: 'title', name: 'Title', field: 'title', width: 120, minWidth: 100, sortable: true, filterable: true }, { id: 'percentComplete', name: '% Complete', field: 'percentComplete', - resizable: false, - minWidth: 130, width: 140, - formatter: Formatters.percentCompleteBar, + minWidth: 130, type: 'number', + sortable: true, filterable: true, filter: { model: Filters.slider, operator: '>=' }, - sortable: true, + editor: { model: Editors.singleSelect, collection: Array.from({ length: 101 }, (_v, i) => ({ value: i, label: i })) }, }, { id: 'start', name: 'Start', field: 'start', - minWidth: 100, - width: 120, - filterable: true, + type: 'dateIso', sortable: true, + filterable: true, formatter: Formatters.dateIso, + filter: { model: Filters.compoundDate }, }, { id: 'finish', name: 'Finish', field: 'finish', - minWidth: 100, - width: 120, - filterable: true, + type: 'dateIso', sortable: true, + filterable: true, formatter: Formatters.dateIso, + filter: { model: Filters.compoundDate }, }, { - id: 'cost', - name: 'Cost | Duration', - field: 'cost', - formatter: this.costDurationFormatter.bind(this), - minWidth: 150, - width: 170, + id: 'completed', + name: 'Completed', + field: 'completed', sortable: true, - // filterable: true, - filter: { - model: Filters.compoundSlider, - }, - editor: { - model: Editors.dualInput, - // the DualInputEditor is of Type ColumnEditorDualInput and MUST include (leftInput/rightInput) in its params object - // in each of these 2 properties, you can pass any regular properties of a column editor - // and they will be executed following the options defined in each - params: { - leftInput: { - field: 'cost', - type: 'float', - decimal: 2, - minValue: 0, - maxValue: 50000, - placeholder: '< 50K', - errorMessage: 'Cost must be positive and below $50K.', - }, - rightInput: { - field: 'duration', - type: 'float', // you could have 2 different input type as well - minValue: 0, - maxValue: 100, - title: 'make sure Duration is withing its range of 0 to 100', - errorMessage: 'Duration must be between 0 and 100.', - - // Validator Option #1 - // You could also optionally define a custom validator in 1 or both inputs - /* - validator: (value, args) => { - let isValid = true; - let errorMsg = ''; - if (value < 0 || value > 120) { - isValid = false; - errorMsg = 'Duration MUST be between 0 and 120.'; - } - return { valid: isValid, msg: errorMsg }; - } - */ - }, - } as ColumnEditorDualInput, - - // Validator Option #2 (shared Validator) - this is the last alternative, option #1 (independent Validators) is still the recommended way - // You can also optionally use a common Validator (if you do then you cannot use the leftInput/rightInput validators at same time) - // to compare both values at the same time. - /* - validator: (values, args) => { - let isValid = true; - let errorMsg = ''; - if (values.cost < 0 || values.cost > 50000) { - isValid = false; - errorMsg = 'Cost MUST be between 0 and 50k.'; - } - if (values.duration < 0 || values.duration > 120) { - isValid = false; - errorMsg = 'Duration MUST be between 0 and 120.'; - } - if (values.cost < values.duration) { - isValid = false; - errorMsg = 'Cost can never be lower than its Duration.'; - } - return { valid: isValid, msg: errorMsg }; - } - */ - }, - }, - { - id: 'effortDriven', - name: 'Effort Driven', - field: 'effortDriven', - minWidth: 100, - width: 120, - formatter: Formatters.checkmarkMaterial, filterable: true, + formatter: Formatters.checkmarkMaterial, + editor: { model: Editors.checkbox }, filter: { + model: Filters.singleSelect, collection: [ { value: '', label: '' }, { value: true, label: 'True' }, { value: false, label: 'False' }, ], - model: Filters.singleSelect, }, - sortable: true, - }, - { - id: 'title1', - name: 'Title 1', - field: 'title1', - minWidth: 100, - width: 120, - filterable: true, - sortable: true, }, { - id: 'title2', - name: 'Title 2', - field: 'title2', - minWidth: 100, - width: 120, - filterable: true, - sortable: true, - }, - { - id: 'title3', - name: 'Title 3', - field: 'title3', - minWidth: 100, - width: 120, - filterable: true, + id: 'cost', + name: 'Cost | Duration', + field: 'cost', + formatter: this.costDurationFormatter.bind(this), sortable: true, + filter: { model: Filters.compoundSlider }, + editor: { model: Editors.dualInput, params: dual }, }, + { id: 'cityOfOrigin', name: 'City of Origin', field: 'cityOfOrigin', minWidth: 100, sortable: true, filterable: true }, { - id: 'title4', - name: 'Title 4', - field: 'title4', - minWidth: 100, - width: 120, - filterable: true, - sortable: true, + id: 'action', + name: 'Action', + field: 'action', + width: 100, + maxWidth: 100, + excludeFromExport: true, + formatter: () => '
Action
', + cellMenu: { + commandTitle: 'Commands', + commandItems: [ + { command: 'command1', title: 'Command 1' }, + { command: 'command2', title: 'Command 2', itemUsabilityOverride: (args: any) => !args.dataContext.completed }, + { command: 'delete-row', title: 'Delete Row', itemVisibilityOverride: (args: any) => !args.dataContext.completed }, + { divider: true, command: '' }, + { command: 'help', title: 'Help' }, + { command: 'something', title: 'Disabled Command', disabled: true }, + ], + optionTitle: 'Change Complete Flag', + optionItems: [ + { option: true, title: 'True' }, + { option: false, title: 'False' }, + ], + }, }, ]; - this.gridOptions = { - autoResize: { - container: '#demo-container', - rightPadding: 10, - }, - gridWidth: 920, + autoResize: { container: '#demo-container', rightPadding: 10 }, + // Keep the left-pinned columns compact so two right-pinned columns fit + // beside the framework demo's route sidebar. + autoFitColumnsOnFirstLoad: false, enableCellNavigation: true, editable: true, autoEdit: true, + enableFiltering: true, enableExcelCopyBuffer: true, - frozenColumn: this.frozenColumnCount, - frozenRow: this.frozenRowCount, - // frozenBottom: true, // if you want to freeze the bottom instead of the top, you can enable this property - - // show both Frozen Columns in HeaderMenu & GridMenu, these are opt-in commands so they're disabled by default - gridMenu: { hideClearFrozenColumnsCommand: false }, - headerMenu: { hideFreezeColumnsCommand: false }, + enableExcelExport: true, + externalResources: [new ExcelExportService()], + enableSelection: true, + enableCheckboxSelector: true, + selectionOptions: { selectActiveRow: false }, + checkboxSelector: { + hideInColumnTitleRow: true, + hideInFilterHeaderRow: false, + name: 'Sel', + onExtensionRegistered: (i: any) => (this.checkboxSelectorInstance = i), + }, + pinning: { columns: { left: ['_checkbox_selector', 'title', 'percentComplete'], right: ['action'] }, rows: { top: [0, 1, 2] } }, + enableCellMenu: true, + cellMenu: { + onCommand: (_e: any, a: any) => this.executeCommand(a), + onOptionSelected: (_e: any, a: any) => { + if (a?.dataContext) { + a.dataContext.completed = a.item.option; + this.aureliaGrid?.gridService?.updateItem(a.dataContext); + } + }, + }, + enableContextMenu: true, + contextMenu: this.getContextMenuOptions(), + gridMenu: { hideClearPinningCommand: false }, + headerMenu: { hidePinColumnCommand: false, hidePinningColumnsCommand: false }, }; } - - getData() { - // Set up some test columns. - const mockDataset: any[] = []; - for (let i = 0; i < 500; i++) { - mockDataset[i] = { - id: i, - title: 'Task ' + i, - cost: i % 33 === 0 ? null : Math.random() * 10000, - duration: i % 8 ? Math.round(Math.random() * 100) + '' : null, - percentComplete: Math.round(Math.random() * 100), - start: new Date(2009, 0, 1), - finish: new Date(2009, 4, 5), - effortDriven: i % 5 === 0, - title1: `Some Text ${Math.round(Math.random() * 25)}`, - title2: `Some Text ${Math.round(Math.random() * 25)}`, - title3: `Some Text ${Math.round(Math.random() * 25)}`, - title4: `Some Text ${Math.round(Math.random() * 25)}`, - }; - } - this.dataset = mockDataset; + getContextMenuOptions(): any { + const p = [ + { option: 0, title: 'Not Started (0%)' }, + { option: 50, title: 'Half Completed (50%)' }, + { option: 100, title: 'Completed (100%)' }, + ]; + return { + optionShownOverColumnIds: ['percentComplete'], + hideCloseButton: true, + dropSide: 'right', + optionTitle: 'Change Percent Complete', + optionItems: [...p, 'divider', { option: null, title: 'Sub-Options (demo)', subMenuTitle: 'Set Percent Complete', optionItems: p }], + commandItems: [ + { command: '', divider: true, positionOrder: 98 }, + { + command: 'export', + title: 'Exports', + positionOrder: 99, + commandItems: [ + { command: 'exports-txt', title: 'Text (tab delimited)' }, + { + command: 'sub-menu', + title: 'Excel', + subMenuTitle: 'available formats', + commandItems: [ + { command: 'exports-csv', title: 'Excel (csv)' }, + { command: 'exports-xlsx', title: 'Excel (xlsx)' }, + ], + }, + ], + }, + { + command: 'feedback', + title: 'Feedback', + positionOrder: 100, + commandItems: [ + { command: 'request-update', title: 'Request update from supplier' }, + 'divider', + { + command: 'sub-menu', + title: 'Contact Us', + subMenuTitle: 'contact us...', + commandItems: [ + { command: 'contact-email', title: 'Email us' }, + { command: 'contact-chat', title: 'Chat with us' }, + { command: 'contact-meeting', title: 'Book an appointment' }, + ], + }, + ], + }, + ], + onOptionSelected: (_e: any, a: any) => { + if (a?.dataContext) { + a.dataContext.percentComplete = a.item.option; + this.gridObj?.updateRow(a.row || 0); + } + }, + onCommand: (_e: any, a: any) => this.executeCommand(a), + }; } - - /** change dynamically, through slickgrid "setOptions()" the number of pinned columns */ - changeFrozenColumnCount() { - if (this.gridObj && this.gridObj.setOptions) { - this.gridObj.setOptions({ - frozenColumn: this.frozenColumnCount, - }); + executeCommand(a: any) { + if (a.command === 'delete-row' && confirm(`Do you really want to delete row (${a.row + 1}) with "${a.dataContext.title}"?`)) { + this.aureliaGrid?.gridService?.deleteItemById(a.dataContext.id); + } else if (['command1', 'command2', 'help'].includes(a.command)) { + alert(a.item.title); + } else if (['exports-csv', 'exports-txt', 'exports-xlsx'].includes(a.command)) { + alert(`Exporting as ${a.item.title}`); + } else { + alert(`Command: ${a.command}`); } } - - /** change dynamically, through slickgrid "setOptions()" the number of pinned rows */ - changeFrozenRowCount() { - if (this.gridObj && this.gridObj.setOptions) { - this.gridObj.setOptions({ - frozenRow: this.frozenRowCount, - }); - } + setPinnedColumns(left: number, right = this.pinnedRightColumnCount) { + const n = Math.max(0, Number(right) || 0); + this.gridObj?.setOptions({ + pinning: { + columns: { + left: left >= 0 ? ['_checkbox_selector', 'title', 'percentComplete'].slice(0, left + 1) : [], + right: ['cityOfOrigin', 'action'].slice(Math.max(0, 2 - n)), + }, + }, + }); + this.pinnedColumnCount = left; + this.pinnedRightColumnCount = n; } - - costDurationFormatter(_row: number, _cell: number, _value: any, _columnDef: Column, dataContext: any) { - const costText = this.isNullUndefinedOrEmpty(dataContext.cost) ? 'n/a' : formatNumber(dataContext.cost, 0, 2, false, '$', '', '.', ','); - let durationText = 'n/a'; - if (!this.isNullUndefinedOrEmpty(dataContext.duration) && dataContext.duration >= 0) { - durationText = `${dataContext.duration} ${dataContext.duration > 1 ? 'days' : 'day'}`; - } - return `${costText} | ${durationText}`; + changePinnedColumnCount() { + this.setPinnedColumns(this.pinnedColumnCount); } - - isNullUndefinedOrEmpty(data: any) { - return data === '' || data === null || data === undefined; + changePinnedRowCount() { + const rows = Array.from({ length: Math.max(0, this.pinnedRowCount) }, (_v, i) => i); + this.gridObj?.setOptions({ pinning: { rows: this.isPinnedBottom ? { top: [], bottom: rows } : { top: rows, bottom: [] } } }); + } + toggleRightPinning() { + this.setPinnedColumns(this.pinnedColumnCount, this.pinnedRightColumnCount > 0 ? 0 : 1); + } + togglePinnedBottomRows() { + this.isPinnedBottom = !this.isPinnedBottom; + this.changePinnedRowCount(); + } + toggleSelectAllRow() { + this.isSelectAllShownAsColumnTitle = !this.isSelectAllShownAsColumnTitle; + this.checkboxSelectorInstance?.setOptions({ + hideInColumnTitleRow: !this.isSelectAllShownAsColumnTitle, + hideInFilterHeaderRow: this.isSelectAllShownAsColumnTitle, + }); + } + setLargePinnedColumns() { + this.aureliaGrid?.gridStateService?.applyColumnLayout?.( + [ + { columnId: '_checkbox_selector', cssClass: 'slick-cell-checkboxsel', headerCssClass: '', width: 40 }, + { columnId: 'title', cssClass: '', headerCssClass: '', width: 220 }, + { columnId: 'percentComplete', cssClass: '', headerCssClass: '', width: 280 }, + { columnId: 'start', cssClass: '', headerCssClass: '', width: 150 }, + { columnId: 'finish', cssClass: '', headerCssClass: '', width: 280 }, + { columnId: 'completed', cssClass: '', headerCssClass: '', width: 180 }, + { columnId: 'cost', cssClass: '', headerCssClass: '', width: 220 }, + { columnId: 'cityOfOrigin', cssClass: '', headerCssClass: '', width: 180 }, + { columnId: 'action', cssClass: '', headerCssClass: '', width: 110 }, + ], + false, + false + ); + this.setPinnedColumns(2, this.pinnedRightColumnCount); } - onCellValidationError(_e: Event, args: any) { showToast(args.validationResults.msg, 'danger'); } - - setFrozenColumns(frozenCols: number) { - this.gridObj.setOptions({ frozenColumn: frozenCols }); - this.gridOptions = this.gridObj.getOptions(); - this.frozenColumnCount = frozenCols; - } - - /** toggle dynamically, through slickgrid "setOptions()" the top/bottom pinned location */ - toggleFrozenBottomRows() { - if (this.gridObj && this.gridObj.setOptions) { - this.gridObj.setOptions({ - frozenBottom: !this.isFrozenBottom, - }); - this.isFrozenBottom = !this.isFrozenBottom; // toggle the variable - } + costDurationFormatter(_r: number, _c: number, _v: any, _d: Column, x: any) { + const cost = x.cost == null ? 'n/a' : formatNumber(x.cost, 0, 2, false, '$', '', '.', ','); + const duration = x.duration != null && x.duration >= 0 ? `${x.duration} ${x.duration > 1 ? 'days' : 'day'}` : 'n/a'; + return `${cost} | ${duration}`; } - toggleSubTitle() { this.hideSubTitle = !this.hideSubTitle; - const action = this.hideSubTitle ? 'add' : 'remove'; - document.querySelector('.subtitle')?.classList[action]('hidden'); - this.aureliaGrid.resizerService.resizeGrid(0); + document.querySelector('.subtitle')?.classList[this.hideSubTitle ? 'add' : 'remove']('hidden'); + this.aureliaGrid?.resizerService?.resizeGrid(0); } } diff --git a/demos/aurelia/src/examples/slickgrid/example38.ts b/demos/aurelia/src/examples/slickgrid/example38.ts index 26a733fa97..68197fb7a6 100644 --- a/demos/aurelia/src/examples/slickgrid/example38.ts +++ b/demos/aurelia/src/examples/slickgrid/example38.ts @@ -96,7 +96,8 @@ export class Example38 { enableSelection: true, enableGrouping: true, headerMenu: { - hideFreezeColumnsCommand: false, + hidePinColumnCommand: false, + hidePinningColumnsCommand: false, }, presets: { // NOTE: pagination preset is NOT supported with infinite scroll diff --git a/demos/aurelia/src/examples/slickgrid/example43.ts b/demos/aurelia/src/examples/slickgrid/example43.ts index 9307b653d3..5c4c673c01 100644 --- a/demos/aurelia/src/examples/slickgrid/example43.ts +++ b/demos/aurelia/src/examples/slickgrid/example43.ts @@ -144,7 +144,6 @@ export class Example43 { autoResize: { container: '#demo-container', bottomPadding: 30, - rightPadding: 50, }, enableCellNavigation: true, enableColumnReorder: true, @@ -156,7 +155,7 @@ export class Example43 { autoEdit: true, editable: false, datasetIdPropertyName: 'employeeID', - frozenColumn: 0, + pinning: { columns: { left: 0 } }, gridHeight: 348, rowHeight: 30, dataView: { @@ -451,7 +450,7 @@ export class Example43 { newMetadata[row].columns[Number(col) + colDirIdx] = (this.metadata as any)[row].columns[col]; } } - this.aureliaGrid.slickGrid?.setOptions({ frozenColumn: newShowEmployeeId ? 0 : 1 }); + this.aureliaGrid.slickGrid?.setOptions({ pinning: { columns: { left: newShowEmployeeId ? 0 : 1 } } }); this.aureliaGrid.slickGrid?.updateColumnById('employeeID', { hidden: !newShowEmployeeId }); this.aureliaGrid.slickGrid?.updateColumns(); */ diff --git a/demos/aurelia/src/examples/slickgrid/example55.ts b/demos/aurelia/src/examples/slickgrid/example55.ts index 866e0ab26b..27a506af5c 100644 --- a/demos/aurelia/src/examples/slickgrid/example55.ts +++ b/demos/aurelia/src/examples/slickgrid/example55.ts @@ -83,7 +83,7 @@ export class Example55 { const owners = ['Alex', 'Priya', 'Mia', 'Sam', 'Chris']; const fragments = [ 'Refactor keyboard shortcut handling for better readability.', - 'Adjust frozen rows when view-model updates after grouping.', + 'Adjust pinned rows when view-model updates after grouping.', 'Improve screen-reader labels on grid menu actions.', 'Align batch editor validation with backend constraints.', 'Capture edge-case around hidden columns and row-span.', diff --git a/demos/aurelia/src/examples/slickgrid/example56.ts b/demos/aurelia/src/examples/slickgrid/example56.ts index 2d47583e09..e9bd506f48 100644 --- a/demos/aurelia/src/examples/slickgrid/example56.ts +++ b/demos/aurelia/src/examples/slickgrid/example56.ts @@ -80,7 +80,7 @@ export class Example56 { includeColumnWidth: true, }, rowHeight: 40, - frozenRow: 2, + pinning: { rows: { top: [0, 1] } }, gridHeight: 560, gridWidth: 1080, dataView: { @@ -111,7 +111,7 @@ export class Example56 { const statuses: Array = ['Todo', 'In Progress', 'Done']; const notesPool = [ 'Short note.', - 'Need to validate keyboard navigation and ensure screen reader output remains stable across frozen panes.', + 'Need to validate keyboard navigation and ensure screen reader output remains stable across pinned panes.', 'Review row height invalidation path when data changes quickly due to live updates from backend polling.', 'Longer QA note: validate scrolling behavior at top and bottom boundaries, compare rendered range against expected rows, and confirm no visual clipping for wrapped cells.', ]; diff --git a/demos/aurelia/test/cypress/e2e/example03.cy.ts b/demos/aurelia/test/cypress/e2e/example03.cy.ts index f488efd517..a08337a57c 100644 --- a/demos/aurelia/test/cypress/e2e/example03.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example03.cy.ts @@ -61,8 +61,8 @@ describe('Example 3 - Grid with Editors', () => { .click(); // change Title & Custom Title - cy.get('.editor-title > textarea').type('Task 2222'); - cy.get('.editor-title .btn-save').click(); + cy.get('.editor-title:visible > textarea').type('Task 2222'); + cy.get('.editor-title:visible .btn-save').click(); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(2)`).should('contain', 'Task 2222'); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(3)`).should('contain', 'Task 2222'); @@ -98,7 +98,7 @@ describe('Example 3 - Grid with Editors', () => { `[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(11) > input.editor-checkbox.editor-effort-driven` ).check(); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); }); it('should dynamically add 2x new "Title" columns', () => { @@ -147,8 +147,8 @@ describe('Example 3 - Grid with Editors', () => { .click(); // change Title & Custom Title - cy.get('.editor-title > textarea').type('Task 0000'); - cy.get('.editor-title .btn-save').click(); + cy.get('.editor-title:visible > textarea').type('Task 0000'); + cy.get('.editor-title:visible .btn-save').click(); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(2)`).should('contain', 'Task 0000'); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(3)`).should('contain', 'Task 0000'); @@ -180,7 +180,7 @@ describe('Example 3 - Grid with Editors', () => { .blur(); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(10)`).click(); // the blur seems to not always work, so just click on another cell cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(11)`).find('.mdi-check.checkmark-icon'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); }); it('should be able to filter and search "Task 2222" in the new column and expect only 1 row showing in the grid', () => { @@ -258,7 +258,7 @@ describe('Example 3 - Grid with Editors', () => { }); it('should open the "Prerequisites" Filter and expect to have Task 500 & 101 in the Filter', () => { - cy.get('div.ms-filter.filter-prerequisites').trigger('click', { force: true }); + cy.get('div.ms-filter.filter-prerequisites').trigger('click'); cy.get('.ms-drop').find('span:nth(1)').contains('Task 101'); @@ -270,7 +270,7 @@ describe('Example 3 - Grid with Editors', () => { it('should open the "Prerequisites" Editor and expect to have Task 100 & 101 in the Editor', () => { cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(12)`) .should('contain', '') - .click({ force: true }); + .click(); cy.get('.ms-drop').find('span:nth(1)').contains('Task 101'); @@ -284,11 +284,11 @@ describe('Example 3 - Grid with Editors', () => { }); it('should delete the last item "Task 101" and expect it to be removed from the Filter', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('right').wait(50); + cy.get('.slick-horizontal-scroller').scrollTo('right').wait(50); cy.get('[data-test="delete-item-btn"]').click(); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo(0, 0).wait(50); + cy.get('.slick-horizontal-scroller').scrollTo(0, 0).wait(50); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(2)`).should('contain', 'Task 100'); cy.get('div.ms-filter.filter-prerequisites').trigger('click'); diff --git a/demos/aurelia/test/cypress/e2e/example07.cy.ts b/demos/aurelia/test/cypress/e2e/example07.cy.ts index 048c260d9b..446c389b24 100644 --- a/demos/aurelia/test/cypress/e2e/example07.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example07.cy.ts @@ -75,7 +75,7 @@ describe('Example 7 - Header Button Plugin', () => { }); it('should go over the last "Column J" and expect to find the button to have the disabled class and clicking it should not turn the negative numbers to red neither expect console log after clicking the disabled button', () => { - cy.get('#grid7-1 .slick-viewport-top.slick-viewport-left').scrollTo('right').wait(50); + cy.get('#grid7-1 .slick-horizontal-scroller').scrollTo('right').wait(50); cy.get('#grid7-1 .slick-header-columns') .children('.slick-header-column:nth(9)') @@ -109,7 +109,7 @@ describe('Example 7 - Header Button Plugin', () => { }); it('should resize 1st column and make it wider', () => { - cy.get('#grid7-1 .slick-viewport-top.slick-viewport-left').scrollTo('left').wait(50); + cy.get('#grid7-1 .slick-horizontal-scroller').scrollTo('left').wait(50); cy.get('#grid7-1 .slick-header-columns').children('.slick-header-column:nth(0)').should('contain', 'Resize me!'); @@ -220,7 +220,7 @@ describe('Example 7 - Header Button Plugin', () => { }); it('should go over the last "Column J" and expect to find the button to have the disabled class and clicking it should not turn the negative numbers to red neither expect console log after clicking the disabled button', () => { - cy.get('#grid7-2 .slick-viewport-top.slick-viewport-left').scrollTo('right').wait(50); + cy.get('#grid7-2 .slick-horizontal-scroller').scrollTo('right').wait(50); cy.get('#grid7-2 .slick-header-columns') .children('.slick-header-column:nth(9)') @@ -254,7 +254,7 @@ describe('Example 7 - Header Button Plugin', () => { }); it('should resize 1st column and make it wider', () => { - cy.get('#grid7-2 .slick-viewport-top.slick-viewport-left').scrollTo('left').wait(50); + cy.get('#grid7-2 .slick-horizontal-scroller').scrollTo('left').wait(50); cy.get('#grid7-2 .slick-header-columns').children('.slick-header-column:nth(0)').should('contain', 'Resize me!'); @@ -364,7 +364,7 @@ describe('Example 7 - Header Button Plugin', () => { }); it('should expect first few items of "Column C" to be negative numbers and be red', () => { - cy.get('#grid7-2 .slick-viewport-top.slick-viewport-left').scrollTo('top').wait(50); + cy.get('#grid7-2 .slick-vertical-scroller').scrollTo('top').wait(50); cy.get('#grid7-2 .slick-row').each(($row, index) => { if (index > 10) { diff --git a/demos/aurelia/test/cypress/e2e/example10.cy.ts b/demos/aurelia/test/cypress/e2e/example10.cy.ts index f65cc30eeb..fb67e74664 100644 --- a/demos/aurelia/test/cypress/e2e/example10.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example10.cy.ts @@ -389,14 +389,14 @@ describe('Example 10 - Multiple Grids with Row Selection', () => { it('should scroll to the bottom of 2nd Grid and still have 5 rows (Task 1,Task 3,Task 12,Task 13,Task 522) selected and find 2 row selected because we now have 2 rows that got rendered (first and last)', () => { cy.get('#slickGridContainer-grid2').as('grid2'); cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522'); - cy.get('@grid2').find('.slick-viewport-top.slick-viewport-left').scrollTo('bottom').wait(10); + cy.get('@grid2').find('.slick-vertical-scroller').scrollTo('bottom').wait(10); cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2); }); it('should have 2 rows (Task 3,Task 13) selected in 2nd grid after typing in a search filter (3)', () => { cy.get('#slickGridContainer-grid2').as('grid2'); cy.get('@grid2').find('.filter-title').type('3'); - cy.get('@grid2').find('.slick-viewport-top.slick-viewport-left').scrollTo('top').wait(10); + cy.get('@grid2').find('.slick-vertical-scroller').scrollTo('top').wait(10); cy.get('@grid2').find('.slick-row').should('not.have.length', 0); cy.wait(50); diff --git a/demos/aurelia/test/cypress/e2e/example12.cy.ts b/demos/aurelia/test/cypress/e2e/example12.cy.ts index ec11ba1405..7ed1cab4e7 100644 --- a/demos/aurelia/test/cypress/e2e/example12.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example12.cy.ts @@ -229,7 +229,7 @@ describe('Example 12: Localization (i18n)', () => { it('should scroll to bottom of the grid then select "Task 4"', () => { cy.get('#slickGridContainer-grid12').as('grid12'); - cy.get('@grid12').find('.slick-viewport-top.slick-viewport-left').scrollTo('bottom').wait(10); + cy.get('@grid12').find('.slick-vertical-scroller').scrollTo('bottom').wait(10); cy.get('#grid12').contains('Task 4').parent().children('.slick-cell-checkboxsel').find('input[type=checkbox]').click({ force: true }); @@ -259,7 +259,7 @@ describe('Example 12: Localization (i18n)', () => { cy.get('.grid-canvas').find('.slick-row').should('be.visible'); - cy.get('@grid12').find('.slick-viewport-top.slick-viewport-left').scrollTo('top').wait(10); + cy.get('@grid12').find('.slick-vertical-scroller').scrollTo('top').wait(10); cy.get('@grid12').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 1); diff --git a/demos/aurelia/test/cypress/e2e/example14.cy.ts b/demos/aurelia/test/cypress/e2e/example14.cy.ts index 91f7db34b6..32b529fd3b 100644 --- a/demos/aurelia/test/cypress/e2e/example14.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example14.cy.ts @@ -1,5 +1,4 @@ describe('Example 14 - Column Span & Header Grouping', () => { - // NOTE: everywhere there's a * 2 is because we have a top+bottom (frozen rows) containers even after Unfreeze Columns/Rows const fullPreTitles = ['', 'Common Factor', 'Period', 'Analysis']; const fullTitles = ['#', 'Title', 'Duration', 'Start', 'Finish', '% Complete', 'Effort Driven']; @@ -20,17 +19,18 @@ describe('Example 14 - Column Span & Header Grouping', () => { .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); }); - it('should have a frozen grid on page load with 3 columns on the left and 4 columns on the right', () => { - cy.get('#grid2').find('[data-row=0]').should('have.length', 2); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 3); - cy.get('#grid2').find('.grid-canvas-right > [data-row=0]').children().should('have.length', 4); + it('should have a pinned grid on page load with 3 pinned columns and 4 scrolling columns', () => { + const firstRow = '#grid2 .slick-row[data-row="0"]'; + cy.get(firstRow).should('have.length', 1); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell`).should('have.length', 3); + cy.get(`${firstRow} .slick-scrolling-cells .slick-cell`).should('have.length', 4); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0]> .slick-cell:nth(0)').should('contain', '0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0]> .slick-cell:nth(1)').should('contain', 'Task 0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0]> .slick-cell:nth(2)').should('contain', '5 days'); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell:nth(0)`).should('contain', '0'); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell:nth(1)`).should('contain', 'Task 0'); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell:nth(2)`).should('contain', '5 days'); - cy.get('#grid2').find('.grid-canvas-right > [data-row=0]> .slick-cell:nth(0)').should('contain', '01/01/2009'); - cy.get('#grid2').find('.grid-canvas-right > [data-row=0]> .slick-cell:nth(1)').should('contain', '01/05/2009'); + cy.get(`${firstRow} .slick-scrolling-cells .slick-cell:nth(0)`).should('contain', '01/01/2009'); + cy.get(`${firstRow} .slick-scrolling-cells .slick-cell:nth(1)`).should('contain', '01/05/2009'); }); it('should have exact Column Pre-Header & Column Header Titles in the grid again', () => { @@ -45,17 +45,18 @@ describe('Example 14 - Column Span & Header Grouping', () => { .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); }); - it('should click on the "Remove Frozen Columns" button to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => { - cy.get('[data-test="remove-frozen-column-button"]').click(); + it('should click on the "Remove Pinned Columns" button to switch to a regular grid without pinned columns', () => { + cy.get('[data-test="remove-pinned-column-button"]').click(); - cy.get('#grid2').find('[data-row=0]').should('have.length', 1); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 7); + const firstRow = '#grid2 .slick-row[data-row="0"]'; + cy.get(firstRow).should('have.length', 1); + cy.get(`${firstRow} .slick-cell`).should('have.length', 7); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)').should('contain', '0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)').should('contain', 'Task 0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)').should('contain', '5 days'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(3)').should('contain', '01/01/2009'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(4)').should('contain', '01/05/2009'); + cy.get(`${firstRow} .slick-cell:nth(0)`).should('contain', '0'); + cy.get(`${firstRow} .slick-cell:nth(1)`).should('contain', 'Task 0'); + cy.get(`${firstRow} .slick-cell:nth(2)`).should('contain', '5 days'); + cy.get(`${firstRow} .slick-cell:nth(3)`).should('contain', '01/01/2009'); + cy.get(`${firstRow} .slick-cell:nth(4)`).should('contain', '01/05/2009'); }); it('should have exact Column Pre-Header & Column Header Titles in the grid once again', () => { @@ -70,19 +71,20 @@ describe('Example 14 - Column Span & Header Grouping', () => { .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); }); - it('should click on the "Set 3 Frozen Columns" button to switch frozen columns grid and expect 3 frozen columns on the left and 4 columns on the right', () => { - cy.contains('Set 3 Frozen Columns').click({ force: true }); + it('should click on the "Set 3 Pinned Columns" button to pin 3 columns and leave 4 scrolling columns', () => { + cy.contains('Set 3 Pinned Columns').click({ force: true }); - cy.get('#grid2').find('[data-row=0]').should('have.length', 2); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 3); - cy.get('#grid2').find('.grid-canvas-right > [data-row=0]').children().should('have.length', 4); + const firstRow = '#grid2 .slick-row[data-row="0"]'; + cy.get(firstRow).should('have.length', 1); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell`).should('have.length', 3); + cy.get(`${firstRow} .slick-scrolling-cells .slick-cell`).should('have.length', 4); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)').should('contain', '0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)').should('contain', 'Task 0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)').should('contain', '5 days'); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell:nth(0)`).should('contain', '0'); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell:nth(1)`).should('contain', 'Task 0'); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell:nth(2)`).should('contain', '5 days'); - cy.get('#grid2').find('.grid-canvas-right > [data-row=0] > .slick-cell:nth(0)').should('contain', '01/01/2009'); - cy.get('#grid2').find('.grid-canvas-right > [data-row=0] > .slick-cell:nth(1)').should('contain', '01/05/2009'); + cy.get(`${firstRow} .slick-scrolling-cells .slick-cell:nth(0)`).should('contain', '01/01/2009'); + cy.get(`${firstRow} .slick-scrolling-cells .slick-cell:nth(1)`).should('contain', '01/05/2009'); }); it('should have still exact Column Pre-Header & Column Header Titles in the grid', () => { @@ -97,56 +99,49 @@ describe('Example 14 - Column Span & Header Grouping', () => { .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); }); - it('should click on the Grid Menu command "Unfreeze Columns/Rows" to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => { + it('should click on the Grid Menu command "Unpin Columns/Rows" to switch to a regular grid without pinned columns', () => { cy.get('#grid2').find('button.slick-grid-menu-button').click({ force: true }); - cy.contains('Unfreeze Columns/Rows').click({ force: true }); + cy.contains('Unpin Columns/Rows').click({ force: true }); - cy.get('#grid2').find('[data-row=0]').should('have.length', 1); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 7); + const firstRow = '#grid2 .slick-row[data-row="0"]'; + cy.get(firstRow).should('have.length', 1); + cy.get(`${firstRow} .slick-cell`).should('have.length', 7); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)').should('contain', '0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)').should('contain', 'Task 0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)').should('contain', '5 days'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(3)').should('contain', '01/01/2009'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(4)').should('contain', '01/05/2009'); + cy.get(`${firstRow} .slick-cell:nth(0)`).should('contain', '0'); + cy.get(`${firstRow} .slick-cell:nth(1)`).should('contain', 'Task 0'); + cy.get(`${firstRow} .slick-cell:nth(2)`).should('contain', '5 days'); + cy.get(`${firstRow} .slick-cell:nth(3)`).should('contain', '01/01/2009'); + cy.get(`${firstRow} .slick-cell:nth(4)`).should('contain', '01/05/2009'); }); - it('should reapply 3 frozen columns on 2nd grid', () => { - cy.contains('Set 3 Frozen Columns').click({ force: true }); + it('should reapply 3 pinned columns on 2nd grid', () => { + cy.contains('Set 3 Pinned Columns').click({ force: true }); - cy.get('#grid2') - .find('.slick-pane-left .slick-header.slick-header-left .slick-header-columns .slick-header-column') - .should('have.length', 3); - - cy.get('#grid2') - .find('.slick-pane-right .slick-header.slick-header-right .slick-header-columns .slick-header-column') - .should('have.length', 4); + cy.get('#grid2 .slick-header.slick-header-left .slick-header-columns .slick-column-pinned-left').should('have.length', 3); + cy.get('#grid2 .slick-header.slick-header-left .slick-header-columns .slick-header-column:not(.slick-column-pinned-left)').should( + 'have.length', + 4 + ); }); - it('should be able to "Unfreeze Columns" from header menu', () => { + it('should be able to "Unpin All Columns" from header menu', () => { cy.get('#grid2') - .find('.slick-pane-left .slick-header-columns .slick-header-column[role="columnheader"]:nth(2)') + .find('.slick-header.slick-header-left .slick-header-columns .slick-header-column[role="columnheader"]:nth(2)') .trigger('mouseover') .children('.slick-header-menu-button') .invoke('show') .click(); - cy.get('.slick-header-menu .slick-menu-command-list') - .should('be.visible') - .children('.slick-menu-item:nth-of-type(1)') - .children('.slick-menu-content') - .should('contain', 'Unfreeze Columns') - .click(); + cy.get('.slick-header-menu .slick-menu-command-list').should('be.visible').find('[data-command="pin-column"]').click(); + cy.get('.slick-submenu [data-command="unpin-columns"]').should('contain', 'Unpin All Columns').click(); - cy.get('#grid2') - .find('.slick-pane-left .slick-header.slick-header-left .slick-header-columns .slick-header-column') - .should('have.length', 7); + cy.get('#grid2 .slick-header.slick-header-left .slick-header-columns .slick-header-column').should('have.length', 7); }); - it('should be able to "Freeze Columns" back from header menu', () => { + it('should be able to "Pin Through Here" back from header menu', () => { cy.get('#grid2') - .find('.slick-pane-left .slick-header-columns .slick-header-column[role="columnheader"]:nth(2)') + .find('.slick-header.slick-header-left .slick-header-columns .slick-header-column[role="columnheader"]:nth(2)') .trigger('mouseover') .children('.slick-header-menu-button') .invoke('show') @@ -154,57 +149,72 @@ describe('Example 14 - Column Span & Header Grouping', () => { cy.get('.slick-header-menu .slick-menu-command-list') .should('be.visible') - .children('.slick-menu-item:nth-of-type(1)') - .children('.slick-menu-content') - .should('contain', 'Freeze Columns') + .find('[data-command="pin-column"]') + .should('contain', 'Column Pinning') .click(); - cy.get('#grid2') - .find('.slick-pane-left .slick-header.slick-header-left .slick-header-columns .slick-header-column') - .should('have.length', 3); + cy.get('.slick-submenu [data-command="pin-columns"]').should('contain', 'Pin Through Here').click(); - cy.get('#grid2') - .find('.slick-pane-right .slick-header.slick-header-right .slick-header-columns .slick-header-column') - .should('have.length', 4); + cy.get('#grid2 .slick-header.slick-header-left .slick-header-columns .slick-column-pinned-left').should('have.length', 3); + cy.get('#grid2 .slick-header.slick-header-left .slick-header-columns .slick-header-column:not(.slick-column-pinned-left)').should( + 'have.length', + 4 + ); }); describe('Basic Key Navigations', () => { it('should remove any freezing', () => { - cy.get('[data-test="remove-frozen-column-button"]').click(); + cy.get('[data-test="remove-pinned-column-button"]').click(); + + cy.get('#grid2 .slick-header.slick-header-left .slick-header-columns .slick-header-column').should('have.length', 7); + }); + + it('should start at Task 1 on Duration colspan 5 days and type "PageDown" key once and land on a full colspan', () => { + cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('@active_cell').type('{pagedown}'); + cy.get('#grid1 .slick-cell.l0.r5.active').should('have.length', 1); + }); + + it('should start at Task 1 on Duration colspan 5 days and type "PageDown" key 2x times and land on a colspan of 3', () => { + cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('@active_cell').type('{pagedown}{pagedown}'); + cy.get('#grid1 .slick-cell.l1.r3.active').should('have.length', 1); + }); - cy.get('#grid2') - .find('.slick-pane-left .slick-header.slick-header-left .slick-header-columns .slick-header-column') - .should('have.length', 7); + it('should navigate PageUp twice from a colspan of 3 back to the starting colspan of 3', () => { + cy.get('#grid1 [data-row=15] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('@active_cell').type('{pageup}{pageup}'); + cy.get('#grid1 .slick-cell.l1.r3.active').should('have.length', 1); }); it('should start at Task 2 on Duration colspan 5 days and type "PageDown" key 2x times and "PageUp" twice and be back to Task 1 with colspan of 3', () => { - cy.get('[data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); cy.get('@active_cell').type('{pagedown}{pagedown}{pageup}{pageup}'); - cy.get('[data-row=1] > .slick-cell.l1.r3.active').should('have.length', 1); + cy.get('#grid1 .slick-cell.l1.r3.active').should('have.length', 1); }); it('should start at Task 2 on Duration colspan 5 days and type "PageDown" key 2x times and "PageUp" 3x times and be on Task 0 with full colspan', () => { - cy.get('[data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); cy.get('@active_cell').type('{pagedown}{pagedown}{pageup}{pageup}{pageup}'); - cy.get('[data-row=0] > .slick-cell.l0.r5.active').should('have.length', 1); + cy.get('#grid1 .slick-cell.l0.r5.active').should('have.length', 1); }); it('should start at Task 1 on Duration colspan 5 days and type "ArrowDown" key once and be on Task 2 with full colspan', () => { - cy.get('[data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); cy.get('@active_cell').type('{downarrow}'); - cy.get('[data-row=2] > .slick-cell.l0.r5.active').should('have.length', 1); + cy.get('#grid1 .slick-cell.l0.r5.active').should('have.length', 1); }); it('should start at Task 1 on Duration colspan 5 days and type "ArrowDown" key 2x times and be on Task 1 with colspan of 3', () => { - cy.get('[data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); cy.get('@active_cell').type('{downarrow}{downarrow}'); - cy.get('[data-row=3] > .slick-cell.l1.r3.active').should('have.length', 1); + cy.get('#grid1 .slick-cell.l1.r3.active').should('have.length', 1); }); it('should start at Task 1 on Duration colspan 5 days and type "ArrowDown" key 2x times, then "ArrowUp" key 2x times and be back on Task 1 with colspan of 3', () => { - cy.get('[data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); cy.get('@active_cell').type('{downarrow}{downarrow}{uparrow}{uparrow}'); - cy.get('[data-row=1] > .slick-cell.l1.r3.active').should('have.length', 1); + cy.get('#grid1 .slick-cell.l1.r3.active').should('have.length', 1); }); }); @@ -219,7 +229,7 @@ describe('Example 14 - Column Span & Header Grouping', () => { .contains(/(true|false)+$/); cy.get('#grid1') - .find('.slick-pane-left .slick-header-columns .slick-header-column[role="columnheader"]:nth(3)') + .find('.slick-header-columns .slick-header-column[role="columnheader"]:nth(3)') .trigger('mouseover') .children('.slick-header-menu-button') .invoke('show') @@ -232,6 +242,11 @@ describe('Example 14 - Column Span & Header Grouping', () => { .should('contain', 'Hide Column') .click(); + // The colspan still spans the logical Duration/Start/Finish range, but + // the hidden Finish track is zero-width. The host cell must remain + // rendered and visibly cover the two remaining columns. + cy.get('#grid1 [data-row=1] .slick-cell.l1.r3').should('contain', '5 days').and('be.visible'); + // goto right cy.get('#grid1').find('[data-row=1] .slick-cell.l0.r0').click(); cy.get('#grid1').find('[data-row=1] .slick-cell.l0.r0.active').should('contain', 'Task 1').type('{rightArrow}'); @@ -272,7 +287,7 @@ describe('Example 14 - Column Span & Header Grouping', () => { .contains(/(true|false)+$/); cy.get('#grid1') - .find('.slick-pane-left .slick-header-columns .slick-header-column[role="columnheader"]:nth(3)') + .find('.slick-header-columns .slick-header-column[role="columnheader"]:nth(3)') .trigger('mouseover') .children('.slick-header-menu-button') .invoke('show') diff --git a/demos/aurelia/test/cypress/e2e/example15.cy.ts b/demos/aurelia/test/cypress/e2e/example15.cy.ts index 2792dbeac6..5a7e09da9d 100644 --- a/demos/aurelia/test/cypress/e2e/example15.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example15.cy.ts @@ -1,7 +1,6 @@ import { format } from '@formkit/tempo'; describe('Example 15: Grid State & Presets using Local Storage', () => { - const GRID_ROW_HEIGHT = 35; const fullEnglishTitles = ['', 'Title', 'Description', 'Duration', '% Complete', 'Start', 'Completed']; beforeEach(() => { @@ -12,16 +11,11 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { cy.saveLocalStorage(); }); - it('should display Example title', () => { - cy.visit(`${Cypress.config('baseUrl')}/example15`); + it('should display Example title from a clean local-storage state', () => { + cy.visit(`${Cypress.config('baseUrl')}/example15`, { + onBeforeLoad: (window) => window.localStorage.clear(), + }); cy.get('h2').should('contain', 'Example 15: Grid State & Presets using Local Storage'); - - cy.clearLocalStorage(); - cy.get('[data-test=reset-button]').click(); - }); - - it('should reload the page', () => { - cy.reload().wait(50); }); it('should have exact Column Titles in the grid', () => { @@ -348,7 +342,7 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { .each(($child, index) => expect($child.find('.slick-column-name').text()).to.eq(expectedTitles[index])); }); - it('should be able to freeze "Description" column', () => { + it('should be able to pin "Description" column', () => { cy.get('.slick-header-columns') .children('.slick-header-column:nth(1)') .trigger('mouseover') @@ -359,10 +353,11 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { cy.get('.slick-header-menu .slick-menu-command-list') .should('be.visible') - .children('.slick-menu-item:nth-of-type(1)') - .children('.slick-menu-content') - .should('contain', 'Geler les colonnes') - .click(); + .find('[data-command="pin-column"]') + .should('contain', 'Épinglage de colonnes') + .trigger('mouseover'); + + cy.get('.slick-submenu:visible [data-command="pin-columns"]').should('contain', "Épingler jusqu'ici").click(); }); it('should reload the page', () => { @@ -423,10 +418,11 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { }); }); - it('should have a persisted frozen column after "Description" and a grid with 4 containers on page load with 2 columns on the left and 3 columns on the right', () => { - cy.get('[style="transform: translateY(0px);"]').should('have.length', 2); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]').children().should('have.length', 2); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]').children().should('have.length', 3); + it('should have a persisted pinned column after "Description" with 2 pinned and 3 scrolling columns', () => { + const firstRow = '#grid15 .slick-row[data-row="0"]'; + cy.get(firstRow).should('have.length', 1); + cy.get(`${firstRow} .slick-pinned-left-cells > .slick-cell`).should('have.length', 2); + cy.get(`${firstRow} .slick-scrolling-cells > .slick-cell`).should('have.length', 3); }); it('should click on the reset button and have exact Column Titles position as in beginning', () => { @@ -453,7 +449,7 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); }); - it('should be able to freeze "Description" 3rd column', () => { + it('should be able to pin "Description" 3rd column', () => { cy.get('.slick-header-columns') .children('.slick-header-column:nth(2)') .trigger('mouseover') @@ -464,10 +460,11 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { cy.get('.slick-header-menu .slick-menu-command-list') .should('be.visible') - .children('.slick-menu-item:nth-of-type(1)') - .children('.slick-menu-content') - .should('contain', 'Freeze Columns') - .click(); + .find('[data-command="pin-column"]') + .should('contain', 'Column Pinning') + .trigger('mouseover'); + + cy.get('.slick-submenu:visible [data-command="pin-columns"]').should('contain', 'Pin Through Here').click(); }); it('should swap "Duration" and "% Complete" columns', () => { @@ -481,7 +478,7 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); }); - it('should be able to freeze "% Complete" and expect 4th column to be freezed', () => { + it('should be able to pin "% Complete" and expect 4th column to be pinned', () => { cy.get('.slick-header-columns') .children('.slick-header-column:nth(3)') .trigger('mouseover') @@ -492,16 +489,18 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { cy.get('.slick-header-menu .slick-menu-command-list') .should('be.visible') - .children('.slick-menu-item:nth-of-type(1)') - .children('.slick-menu-content') - .should('contain', 'Freeze Columns') - .click(); + .find('[data-command="pin-column"]') + .should('contain', 'Column Pinning') + .trigger('mouseover'); + + cy.get('.slick-submenu:visible [data-command="pin-columns"]').should('contain', 'Pin Through Here').click(); }); - it('should have a persisted frozen column after "Description" and a grid with 4 containers on page load with 2 columns on the left and 3 columns on the right', () => { - cy.get('[style="transform: translateY(0px);"]').should('have.length', 2); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]').children().should('have.length', 4); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]').children().should('have.length', 3); + it('should have a persisted pinned column after "Description" with 4 pinned and 3 scrolling columns', () => { + const firstRow = '#grid15 .slick-row[data-row="0"]'; + cy.get(firstRow).should('have.length', 1); + cy.get(`${firstRow} .slick-pinned-left-cells > .slick-cell`).should('have.length', 4); + cy.get(`${firstRow} .slick-scrolling-cells > .slick-cell`).should('have.length', 3); }); describe('Filter Shortcuts', () => { @@ -599,9 +598,9 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { expect(Number($span.text())).to.gt(80); }); - cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(2)`).contains('desc'); - cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 1}px);"] > .slick-cell:nth(2)`).contains('desc'); - cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(2)`).contains('desc'); + cy.get('#grid15 .slick-row[data-row="0"] .slick-cell.l2').contains('desc'); + cy.get('#grid15 .slick-row[data-row="1"] .slick-cell.l2').contains('desc'); + cy.get('#grid15 .slick-row[data-row="2"] .slick-cell.l2').contains('desc'); }); }); }); diff --git a/demos/aurelia/test/cypress/e2e/example16.cy.ts b/demos/aurelia/test/cypress/e2e/example16.cy.ts index d9eb35e061..87d69f920c 100644 --- a/demos/aurelia/test/cypress/e2e/example16.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example16.cy.ts @@ -39,7 +39,7 @@ describe('Example 16 - Row Move & Checkbox Selector Selector Plugins', () => { }); it('should expect the row to have moved to another row index', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(2)`).should('contain', 'Task 0'); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 1}px);"] > .slick-cell:nth(2)`).should('contain', 'Task 1'); @@ -76,7 +76,7 @@ describe('Example 16 - Row Move & Checkbox Selector Selector Plugins', () => { cy.get('@moveIconTask5').trigger('mousemove', 'bottomRight').trigger('mouseup', 'bottomRight', { which: 1, force: true }); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(2)`).should('contain', 'Task 0'); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 1}px);"] > .slick-cell:nth(2)`).should('contain', 'Task 1'); diff --git a/demos/aurelia/test/cypress/e2e/example19.cy.ts b/demos/aurelia/test/cypress/e2e/example19.cy.ts index babe74c51a..d17f2c9055 100644 --- a/demos/aurelia/test/cypress/e2e/example19.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example19.cy.ts @@ -80,7 +80,7 @@ describe('Example 19 - Row Detail View', () => { cy.get('@detailContainer').find('[data-test=delete-btn]').click(); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('#grid19') .find('.slick-row') @@ -111,7 +111,7 @@ describe('Example 19 - Row Detail View', () => { cy.get('[data-test=collapse-all-btn]').click(); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('#grid19').find('.dynamic-cell-detail .innerDetailView_0 .container_0').should('not.exist'); @@ -175,7 +175,7 @@ describe('Example 19 - Row Detail View', () => { cy.get('#grid19').find('.slick-header-column:nth(1)').find('.slick-sort-indicator-asc').should('have.length', 1); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('#grid19').find('.dynamic-cell-detail .innerDetailView_0 .container_0').should('not.exist'); diff --git a/demos/aurelia/test/cypress/e2e/example20.cy.ts b/demos/aurelia/test/cypress/e2e/example20.cy.ts index 11481d0d77..91c8f4b076 100644 --- a/demos/aurelia/test/cypress/e2e/example20.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example20.cy.ts @@ -1,102 +1,172 @@ -describe('Example 20 - Frozen Grid', () => { - // NOTE: everywhere there's a * 2 is because we have a top+bottom (frozen rows) containers even after Unfreeze Columns/Rows - - const fullTitles = [ - '#', - 'Title', - '% Complete', - 'Start', - 'Finish', - 'Cost | Duration', - 'Effort Driven', - 'Title 1', - 'Title 2', - 'Title 3', - 'Title 4', - ]; +describe('Example 20 - Pinned Grid', () => { + before(() => { + // The framework demos include a 250px route sidebar. Use enough width for + // the two-column pinning scenario to remain valid with that sidebar. + cy.viewport(1440, 900); + }); + + const withTitleRowTitles = ['Sel', 'Title', '% Complete', 'Start', 'Finish', 'Completed', 'Cost | Duration', 'City of Origin', 'Action']; + const withoutTitleRowTitles = ['', 'Title', '% Complete', 'Start', 'Finish', 'Completed', 'Cost | Duration', 'City of Origin', 'Action']; + const getCell = (rowIndex: number, columnIndex: number) => + cy.get(`#grid20 .slick-row[data-row="${rowIndex}"] .slick-cell.l${columnIndex}`); + const setRightPinning = (count: number) => { + cy.get('.pinned-right-column-count').clear().type(`${count}`); + cy.get('[data-test="set-pinned-right-column"]').click(); + }; it('should display Example title', () => { cy.visit(`${Cypress.config('baseUrl')}/example20`); - cy.get('h2').should('contain', 'Example 20: Pinned (frozen) Columns/Rows'); + cy.get('h2').should('contain', 'Example 20: Pinned Columns/Rows'); }); it('should have exact column titles on 1st grid', () => { cy.get('#grid20') - .find('.slick-header-columns') - .children() - .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + .find('.slick-header-columns .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); + }); + + it('should hide sub-title to provide more space for the grid', () => { + cy.get('[data-test="toggle-subtitle"]').click(); }); it('should have exact Column Header Titles in the grid', () => { cy.get('#grid20') - .find('.slick-header-columns:nth(0)') - .children() - .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + .find('.slick-header-columns:nth(0) .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); }); - it('should have a frozen grid with 4 containers on page load with 3 columns on the left and 4 columns on the right', () => { - cy.get('[style="transform: translateY(0px);"]').should('have.length', 2 * 2); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 3 * 2); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 8 * 2); + it('should have three top-pinned rows and left/center/right cells on page load', () => { + const row0 = '#grid20 .slick-row[data-row="0"]'; + + // Pinning uses one row node split into regions and a single docking overlay; + // it no longer duplicates rows into legacy left/right pinned canvases. + cy.get('#grid20 .slick-docking-overlay > .slick-row.slick-row-pinned-top').should('have.length', 3); + cy.get(`${row0} .slick-pinned-left-cells > .slick-cell`).should('have.length', 3); + cy.get(`${row0} .slick-scrolling-cells > .slick-cell`).should('have.length', 5); + cy.get(`${row0} .slick-pinned-right-cells > .slick-cell`).should('have.length', 1); + + cy.get(`${row0} .slick-pinned-left-cells > .slick-cell:nth(0)`).should('contain', ''); + cy.get(`${row0} .slick-pinned-left-cells > .slick-cell:nth(1)`).should('contain', 'Task 0'); + cy.get(`${row0} .slick-scrolling-cells > .slick-cell:nth(0)`).should('contain', '2009-01-01'); + cy.get(`${row0} .slick-scrolling-cells > .slick-cell:nth(1)`).should('contain', '2009-05-05'); + + cy.get(`${row0} .slick-pinned-right-cells > .slick-cell:nth(0) .cell-menu-dropdown`).should('contain', 'Action'); + }); + + it('should pin multiple columns on the right and render matching header and filter regions', () => { + setRightPinning(2); + + const row0 = '#grid20 .slick-row[data-row="0"]'; + cy.get(`${row0} .slick-pinned-right-cells > .slick-cell`).should('have.length', 2); + cy.get(`${row0} .slick-pinned-right-cells > .slick-cell.l7`).should('contain', 'Boston'); + cy.get(`${row0} .slick-pinned-right-cells > .slick-cell.l8 .cell-menu-dropdown`).should('contain', 'Action'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '0'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + cy.get('#grid20 .slick-header-columns-right .slick-header-column').should('have.length', 2); + cy.get('#grid20 .slick-header-columns-right [data-id="cityOfOrigin"]').should('contain', 'City of Origin'); + cy.get('#grid20 .slick-header-columns-right [data-id="action"]').should('contain', 'Action'); + cy.get('#grid20 .slick-headerrow-columns-right .slick-headerrow-column').should('have.length', 2); + cy.get('#grid20 .slick-headerrow-columns-right .slick-headerrow-column.l7 input').should('exist'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + setRightPinning(1); + }); + + it('should keep multiple right-pinned columns fixed while scrolling', () => { + setRightPinning(2); + // The default fixture fits in the viewport, so use the demo's wide layout + // to exercise an actual horizontal scroll rather than a no-op scroll. + cy.get('[data-test="set-large-pinned-columns"]').click(); + const actionCell = '#grid20 .slick-row[data-row="10"] .slick-pinned-right-cells .slick-cell.l8'; + + cy.get(actionCell).then(($cell) => { + const rightEdge = $cell[0].getBoundingClientRect().right; + cy.get('#grid20 .slick-horizontal-scroller').scrollTo('right'); + cy.get(actionCell).should(($scrolledCell) => { + expect(Math.abs($scrolledCell[0].getBoundingClientRect().right - rightEdge)).to.be.lessThan(2); + }); + }); + + cy.get('#grid20 .slick-horizontal-scroller').scrollTo(0, 0, { ensureScrollable: false }); + // Restore the initial fixture after exercising the wide layout so the + // following serial tests do not inherit resized columns. + cy.visit(`${Cypress.config('baseUrl')}/example20`); + }); + + it('should resize columns while left and right pinning are active', () => { + cy.visit(`${Cypress.config('baseUrl')}/example20`); + + const resizeColumn = (columnSelector: string, pinClass: string) => { + cy.get(columnSelector).should('have.class', pinClass); + cy.get(`${columnSelector} .slick-resizable-handle`) + .should('exist') + .then(($handle) => { + const header = $handle.closest('.slick-header-column')[0] as HTMLElement; + const initialWidth = header.getBoundingClientRect().width; + + cy.wrap($handle).trigger('mousedown', { which: 1, pageX: 100, clientX: 100, force: true }); + cy.get('body').trigger('mousemove', { which: 1, pageX: 125, clientX: 125, force: true }); + cy.get('body').trigger('mousemove', { which: 1, pageX: 150, clientX: 150, force: true }); + cy.get('body').trigger('mouseup', { which: 1, pageX: 150, clientX: 150, force: true }); + + cy.get(columnSelector).should(($updatedHeader) => { + expect($updatedHeader[0].getBoundingClientRect().width).to.be.greaterThan(initialWidth); + }); + }); + }; + + resizeColumn('#grid20 .slick-header-columns-left [data-id="title"]', 'slick-column-pinned-left'); + + // Use City of Origin because Action has a maxWidth of 100px and would refuse a wider resize. + setRightPinning(2); + resizeColumn('#grid20 .slick-header-columns-right [data-id="cityOfOrigin"]', 'slick-column-pinned-right'); + + // Keep the following serial tests on the demo's default configuration. + cy.visit(`${Cypress.config('baseUrl')}/example20`); }); - it('should hide "Title" column from Grid Menu and expect last frozen column to be "% Complete"', () => { - const newColumnList = [ - '#', - '% Complete', - 'Start', - 'Finish', - 'Cost | Duration', - 'Effort Driven', - 'Title 1', - 'Title 2', - 'Title 3', - 'Title 4', - ]; + it('should disable and re-enable right pinning through the numeric grid control', () => { + setRightPinning(0); + cy.get('#grid20 .slick-row[data-row="0"] .slick-pinned-right-cells').should('exist'); + cy.get('#grid20 .slick-row[data-row="0"] .slick-pinned-right-cells > .slick-cell').should('not.exist'); + cy.get('#grid20 .slick-header-columns-right .slick-header-column').should('not.exist'); + + setRightPinning(1); + cy.get('#grid20 .slick-row[data-row="0"] .slick-pinned-right-cells > .slick-cell.l8').should('contain', 'Action'); + cy.get('#grid20 .slick-header-columns-right [data-id="action"]').should('contain', 'Action'); + }); + + it('should hide "Title" column from Grid Menu and expect last pinned column to be "% Complete"', () => { + const newColumnList = ['Sel', '% Complete', 'Start', 'Finish', 'Completed', 'Cost | Duration', 'City of Origin', 'Action']; + const row0 = '#grid20 .slick-row[data-row="0"]'; cy.get('#grid20').find('button.slick-grid-menu-button').click({ force: true }); cy.get('#grid20') .get('.slick-grid-menu:visible') .find('.slick-column-picker-list') - .children('li:visible:nth(1)') + .children('li:visible:nth(0)') .children('label') .should('contain', 'Title') .click({ force: true }); cy.get('#grid20') - .find('.slick-header-columns') - .children() + .find('.slick-header-columns .slick-header-column') .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 2 * 2); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 8 * 2); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').children().should('have.length', 2); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells').children().should('have.length', 5); + cy.get('.slick-row[data-row="0"] .slick-pinned-right-cells').children().should('have.length', 1); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', ''); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(0)').should('contain', ''); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + cy.get(`${row0} .slick-scrolling-cells > .slick-cell:nth(0)`).should('contain', '2009-01-01'); + cy.get(`${row0} .slick-scrolling-cells > .slick-cell:nth(1)`).should('contain', '2009-05-05'); }); - it('should show again "Title" column from Grid Menu and expect last frozen column to still be "% Complete"', () => { + it('should show again "Title" column from Grid Menu and expect last pinned column to still be "% Complete"', () => { cy.get('#grid20') .get('.slick-grid-menu:visible') .find('.slick-column-picker-list') - .children('li:visible:nth(1)') + .children('li:visible:nth(0)') .children('label') .should('contain', 'Title') .click({ force: true }); @@ -104,77 +174,60 @@ describe('Example 20 - Frozen Grid', () => { cy.get('#grid20').get('.slick-grid-menu:visible').find('.close').click({ force: true }); cy.get('#grid20') - .find('.slick-header-columns') - .children() - .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + .find('.slick-header-columns .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 3 * 2); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 8 * 2); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').children().should('have.length', 3); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells').children().should('have.length', 5); + cy.get('.slick-row[data-row="0"] .slick-pinned-right-cells').children().should('have.length', 1); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', ''); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(0)').should('contain', ''); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(1)').should('contain', 'Task 0'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + cy.get('.slick-scrolling-cells .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.slick-scrolling-cells .slick-cell:nth(1)').should('contain', '2009-05-05'); }); - it('should hide "Title" column from Header Menu and expect last frozen column to be "% Complete"', () => { - const newColumnList = [ - '#', - '% Complete', - 'Start', - 'Finish', - 'Cost | Duration', - 'Effort Driven', - 'Title 1', - 'Title 2', - 'Title 3', - 'Title 4', - ]; + it('should hide "Title" column from Header Menu and expect last pinned column to be "% Complete"', () => { + const newColumnList = ['Sel', '% Complete', 'Start', 'Finish', 'Completed', 'Cost | Duration', 'City of Origin', 'Action']; - cy.get('#grid20') - .find('.slick-header-column:nth(1)') - .trigger('mouseover') - .children('.slick-header-menu-button') - .should('be.hidden') - .invoke('show') - .click(); + cy.get('#grid20').find('.slick-header-column:nth(1)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click(); cy.get('.slick-header-menu .slick-menu-command-list') .should('be.visible') - .children('.slick-menu-item:nth-of-type(8)') + .children('.slick-menu-item:nth-of-type(9)') .children('.slick-menu-content') .should('contain', 'Hide Column') .click(); cy.get('#grid20') - .find('.slick-header-columns') - .children() + .find('.slick-header-columns .slick-header-column') .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 2 * 2); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 8 * 2); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').children().should('have.length', 2); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells').children().should('have.length', 5); + cy.get('.slick-row[data-row="0"] .slick-pinned-right-cells').children().should('have.length', 1); + + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(0)').should('contain', ''); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', ''); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell:nth(1)').should('contain', '2009-05-05'); + }); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + it('should toggle right pinned column and expect only 2 left/center containers to be visible', () => { + cy.get('[data-test="toggle-pinned-right"]').click(); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').children().should('have.length', 2); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells').children().should('have.length', 6); + cy.get('.slick-row[data-row="0"] .slick-pinned-right-cells').should('exist').and('not.have.class', 'slick-pinned-right-cells-active'); + cy.get('.slick-row[data-row="0"] .slick-pinned-right-cells > .slick-cell').should('not.exist'); }); - it('should show again "Title" column from Column Picker and expect last frozen column to still be "% Complete"', () => { - cy.get('#grid20').find('.slick-header-column:nth(5)').trigger('mouseover').trigger('contextmenu').invoke('show'); + it('should show again "Title" column from Column Picker and expect last pinned column to still be "% Complete"', () => { + cy.get('#grid20').find('.slick-header-column:nth(4)').trigger('mouseover').trigger('contextmenu').invoke('show'); cy.get('.slick-column-picker') .find('.slick-column-picker-list') - .children('li:nth-child(2)') + .children('li:nth-of-type(2)') .children('label') .should('contain', 'Title') .click(); @@ -182,83 +235,769 @@ describe('Example 20 - Frozen Grid', () => { cy.get('.slick-column-picker:visible').find('.close').trigger('click').click(); cy.get('#grid20') - .find('.slick-header-columns') - .children() - .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + .find('.slick-header-columns .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 3 * 2); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 8 * 2); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').children().should('have.length', 3); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells').children().should('have.length', 6); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', ''); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(0)').should('contain', ''); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(1)').should('contain', 'Task 0'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell:nth(1)').should('contain', '2009-05-05'); }); - it('should click on the "Remove Frozen Columns" button to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => { - cy.get('[data-test=remove-frozen-column-button]').click({ force: true }); + it('should click on the "Remove Pinned Columns" button to switch to a regular grid view without pinned columns and expect 7 columns on the left container', () => { + cy.get('[data-test=remove-pinned-column-button]').click({ force: true }); - cy.get('[style="transform: translateY(0px);"]').should('have.length', 1 * 2); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 11 * 2); + cy.get('#grid20 .slick-row[data-row="0"]').should('have.length.at.least', 1); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').should('exist').and('not.have.class', 'slick-pinned-left-cells-active'); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell').should('have.length', 9); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '0'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + getCell(0, 0).should('contain', ''); + getCell(0, 1).should('contain', 'Task 0'); + getCell(0, 3).should('contain', '2009-01-01'); + getCell(0, 4).should('contain', '2009-05-05'); + }); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(3)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(4)').should('contain', '2009-05-05'); + it('should expect to have exact Column Header Titles in the grid', () => { + cy.get('#grid20') + .find('.slick-header-columns:nth(0) .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); }); - it('should have exact Column Header Titles in the grid', () => { + it('should click on the "Set 3 Pinned Columns" button to switch pinned columns grid and expect 3 pinned columns on the left and 4 columns on the right', () => { + cy.get('[data-test=set-3pinned-columns]').click({ force: true }); + + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').children().should('have.length', 3); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells').children().should('have.length', 6); + + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(0)').should('contain', ''); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(1)').should('contain', 'Task 0'); + + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell:nth(1)').should('contain', '2009-05-05'); + }); + + it('should recheck again and still have exact Column Header Titles in the grid', () => { cy.get('#grid20') - .find('.slick-header-columns:nth(0)') - .children() - .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + .find('.slick-header-columns:nth(0) .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); }); - it('should click on the "Set 3 Frozen Columns" button to switch frozen columns grid and expect 3 frozen columns on the left and 4 columns on the right', () => { - cy.get('[data-test=set-3frozen-columns]').click({ force: true }); + it('should click on the Grid Menu command "Unpin Columns/Rows" to switch to a regular grid without pinned columns/rows', () => { + cy.get('#grid20').find('button.slick-grid-menu-button').click({ force: true }); - cy.get('[style="transform: translateY(0px);"]').should('have.length', 2 * 2); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 3 * 2); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 8 * 2); + cy.contains('Unpin Columns/Rows').click({ force: true }); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '0'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + cy.get('#grid20 .slick-row[data-row="0"]').should('have.length.at.least', 1); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').should('exist').and('not.have.class', 'slick-pinned-left-cells-active'); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell').should('have.length', 9); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + getCell(0, 0).should('contain', ''); + getCell(0, 1).should('contain', 'Task 0'); + getCell(0, 3).should('contain', '2009-01-01'); + getCell(0, 4).should('contain', '2009-05-05'); }); - it('should have exact Column Header Titles in the grid', () => { + it('should open the Cell Menu on 2nd and 3rd row and change the Effort-Driven to "True" and expect the cell to be updated and have checkmark icon', () => { + getCell(1, 1).should('contain', 'Task 1'); + getCell(1, 8).find('.checkmark-icon').should('have.length', 0); + getCell(2, 1).should('contain', 'Task 2'); + getCell(2, 8).find('.checkmark-icon').should('have.length', 0); + + getCell(1, 8).contains('Action').click({ force: true }); + cy.get('.slick-cell-menu .slick-menu-option-list .slick-menu-item').contains('True').click(); + getCell(2, 8).contains('Action').click({ force: true }); + cy.get('.slick-cell-menu .slick-menu-option-list .slick-menu-item').contains('True').click(); + + getCell(1, 5).find('.checkmark-icon').should('have.length', 1); + getCell(2, 5).find('.checkmark-icon').should('have.length', 1); + }); + + it('should open the Cell Menu on 2nd and 3rd row and change the Effort-Driven to "False" and expect the cell to be updated and no longer have checkmark', () => { + getCell(1, 5).find('.checkmark-icon').should('have.length', 1); + getCell(2, 5).find('.checkmark-icon').should('have.length', 1); + + getCell(1, 8).contains('Action').click({ force: true }); + cy.get('.slick-cell-menu .slick-menu-option-list .slick-menu-item').contains('False').click(); + getCell(2, 8).contains('Action').click({ force: true }); + cy.get('.slick-cell-menu .slick-menu-option-list .slick-menu-item').contains('False').click(); + + getCell(1, 5).find('.checkmark-icon').should('have.length', 0); + getCell(2, 5).find('.checkmark-icon').should('have.length', 0); + }); + + it('should open the Cell Menu and delete Row 3 and 4 from the Cell Menu', () => { + cy.window().then((win) => { + const stub = cy.stub(win, 'confirm').returns(true); + cy.wrap(stub).as('confirmStub'); + }); + + getCell(3, 1).should('contain', 'Task 3'); + getCell(4, 1).should('contain', 'Task 4'); + + getCell(3, 8).contains('Action').click({ force: true }); + + cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item').contains('Delete Row').click(); + cy.get('@confirmStub').should('have.been.calledWith', 'Do you really want to delete row (4) with "Task 3"?'); + getCell(3, 1).should('contain', 'Task 4'); + }); + + it.skip('should filter autocomplete by typing Vancouver in the "City of Origin" and expect only filtered rows to show up', () => { + cy.get('.search-filter.filter-cityOfOrigin').type('Vancouver'); + + cy.get('.slick-autocomplete').should('be.visible'); + cy.get('.slick-autocomplete div').should('have.length', 2); + cy.get('.slick-autocomplete').find('div:nth(0)').click(); + + getCell(0, 1).should('contain', 'Task 1'); + getCell(1, 1).should('contain', 'Task 5'); + getCell(2, 1).should('contain', 'Task 7'); + getCell(3, 1).should('contain', 'Task 9'); + getCell(4, 1).should('contain', 'Task 11'); + }); + + it('should Clear all Filters', () => { + cy.get('#grid20').find('button.slick-grid-menu-button').trigger('click').click({ force: true }); + + cy.get(`.slick-grid-menu:visible`).find('.slick-menu-item').first().find('span').contains('Clear all Filters').click(); + }); + + it.skip('should edit first row (Task 1) and change its city by choosing it inside the autocomplete editor list', () => { + getCell(0, 7).click(); + cy.get('input.autocomplete.editor-cityOfOrigin').type('Sydney'); + + cy.get('.slick-autocomplete').should('be.visible'); + cy.get('.slick-autocomplete div').should('have.length', 3); + cy.get('.slick-autocomplete').find('div:nth(1)').click(); + + getCell(0, 1).should('contain', 'Task 0'); + getCell(0, 7).should('contain', 'Sydney, NS, Australia'); + }); + + it('should open Context Menu hover "% Complete" column then select "Not Started (0%)" option and expect Task to be at 0', () => { + getCell(0, 2).rightclick(); + + cy.get('.slick-context-menu .slick-menu-option-list').should('exist').contains('Not Started (0%)').click(); + + getCell(0, 2).should('contain', '0'); + }); + + it('should reopen Context Menu hover "% Complete" column then open options sub-menu & select "Half Completed (50%)" option and expect Task to be at 50', () => { + const subOptions = ['Not Started (0%)', 'Half Completed (50%)', 'Completed (100%)']; + + getCell(0, 2).should('contain', '0'); + getCell(0, 2).rightclick(); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-option-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Sub-Options (demo)') + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-option-list').as('subMenuList'); + cy.get('@subMenuList').find('.slick-menu-title').contains('Set Percent Complete'); + cy.get('@subMenuList') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.eq(subOptions[index])); + + cy.get('@subMenuList').find('.slick-menu-item .slick-menu-content').contains('Half Completed (50%)').click(); + + getCell(0, 2).should('contain', '50'); + }); + + it('should be able to open Context Menu and click on Export->Text and expect alert triggered with Text Export', () => { + const subCommands1 = ['Text', 'Excel']; + cy.window().then((win) => { + cy.stub(win, 'alert').as('alertStub'); + }); + + getCell(0, 2).should('contain', '0'); + getCell(0, 2).rightclick(); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains(/^Exports$/) + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list').find('.slick-menu-item').contains('Text').click(); + cy.get('@alertStub').should('have.been.calledWith', 'Exporting as Text (tab delimited)'); + }); + + it('should be able to open Context Menu and click on Export->Excel-> sub-commands to see 1 context menu + 1 sub-menu then clicking on Text should call alert action', () => { + const subCommands1 = ['Text', 'Excel']; + const subCommands2 = ['Excel (csv)', 'Excel (xlsx)']; + cy.window().then((win) => { + cy.stub(win, 'alert').as('alertStub'); + }); + + getCell(0, 2).should('contain', '0'); + getCell(0, 2).rightclick(); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains(/^Exports$/) + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Excel') + .click(); + + cy.get('.slick-context-menu.slick-menu-level-2 .slick-menu-command-list').as('subMenuList2'); + + cy.get('@subMenuList2').find('.slick-menu-title').contains('available formats'); + + cy.get('@subMenuList2') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands2[index])); + + cy.get('.slick-context-menu.slick-menu-level-2 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Excel (xlsx)') + .click(); + cy.get('@alertStub').should('have.been.calledWith', 'Exporting as Excel (xlsx)'); + }); + + it('should open Export->Excel sub-menu & open again Sub-Options on top and expect sub-menu to be recreated with that Sub-Options list instead of the Export->Excel list', () => { + const subCommands1 = ['Text', 'Excel']; + const subCommands2 = ['Excel (csv)', 'Excel (xlsx)']; + const subOptions = ['Not Started (0%)', 'Half Completed (50%)', 'Completed (100%)']; + + getCell(0, 2).should('contain', '0'); + getCell(0, 2).rightclick(); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains(/^Exports$/) + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Excel') + .click(); + + cy.get('.slick-context-menu.slick-menu-level-2 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands2[index])); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-option-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Sub-Options') + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-option-list').as('optionSubList2'); + + cy.get('@optionSubList2').find('.slick-menu-title').contains('Set Percent Complete'); + + cy.get('@optionSubList2') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($option, index) => expect($option.text()).to.contain(subOptions[index])); + }); + + it('should open Export->Excel context sub-menu then open Feedback->ContactUs sub-menus and expect previous Export menu to no longer exists', () => { + const subCommands1 = ['Text', 'Excel']; + const subCommands2 = ['Request update from supplier', '', 'Contact Us']; + const subCommands2_1 = ['Email us', 'Chat with us', 'Book an appointment']; + + cy.window().then((win) => { + cy.stub(win, 'alert').as('alertStub'); + }); + + getCell(0, 2).should('contain', '0'); + getCell(0, 2).rightclick({ force: true }); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains(/^Exports$/) + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + // click different sub-menu + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Feedback') + .should('exist') + .click(); + + cy.get('.slick-submenu').should('have.length', 1); + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands2[index])); + + // click on Feedback->ContactUs + cy.get('.slick-context-menu.slick-menu-level-1.dropright') // right align + .find('.slick-menu-item .slick-menu-content') + .contains('Contact Us') + .should('exist') + .trigger('mouseover'); // mouseover or click should work + + cy.get('.slick-submenu').should('have.length', 2); + cy.get('.slick-context-menu.slick-menu-level-2.dropright') // right align + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.eq(subCommands2_1[index])); + + cy.get('.slick-context-menu.slick-menu-level-2'); + + cy.get('.slick-context-menu.slick-menu-level-2 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Chat with us') + .click(); + cy.get('@alertStub').should('have.been.calledWith', 'Command: contact-chat'); + + cy.get('.slick-submenu').should('have.length', 0); + }); + + it('should toggle Select All checkbox and expect back "Sel" column title to show when Select All checkbox is shown in the header row', () => { + cy.get('.slick-header-column:nth(0)').find('.slick-column-name').should('contain', 'Sel'); + cy.get('.slick-header-columns .slick-header-column').each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); + + cy.get('[data-test="toggle-select-all-row"]').click(); + + cy.get('.slick-header-column:nth(0)').find('.slick-column-name').should('not.contain', 'Sel'); + + cy.get('.slick-header-columns .slick-header-column').each(($child, index) => expect($child.text()).to.eq(withoutTitleRowTitles[index])); + }); + + it('should toggle back Select All checkbox and expect back "Sel" column title to show when Select All checkbox is shown in the header row', () => { + cy.get('[data-test="toggle-select-all-row"]').click(); + + cy.get('.slick-header-column:nth(0)').find('.slick-column-name').should('contain', 'Sel'); + cy.get('.slick-header-columns .slick-header-column').each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); + + cy.get('[data-test="toggle-select-all-row"]').click(); + + cy.get('.slick-header-column:nth(0)').find('.slick-column-name').should('not.contain', 'Sel'); + + cy.get('.slick-header-columns .slick-header-column').each(($child, index) => expect($child.text()).to.eq(withoutTitleRowTitles[index])); + }); + + it('should open Column Picker and try unchecked all the columns on the right of the column pinning and expect an error and abort of the execution', () => { + cy.window().then((win) => { + cy.stub(win, 'alert').as('alertStub'); + }); + cy.get('[data-test=set-3pinned-columns]').click({ force: true }); + + const leftColumns = ['', 'Title', '% Complete']; + const rightColumns = ['Start', 'Finish', 'Completed', 'Cost | Duration', 'City of Origin', 'Action']; + cy.get('#grid20').find('.slick-header-column').first().trigger('mouseover').trigger('contextmenu').invoke('show'); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children() + .each(($child, index) => { + if (index >= leftColumns.length) { + if ($child.text() === rightColumns[index - leftColumns.length]) { + expect($child.text()).to.eq(rightColumns[index - leftColumns.length]); + if (index <= rightColumns.length + 1) { + cy.wrap($child).children('label').click(); + } else { + cy.wrap($child) + .children('label') + .click() + .then(() => { + cy.get('@alertStub').should( + 'have.been.calledWith', + '[SlickGrid] Action not allowed and aborted, you need to have at least one or more column in the center section of the grid. ' + + 'You could alternatively unpin columns before trying again.' + ); + }); + } + } + } + }); + + cy.get('button[data-dismiss="slick-column-picker"]').click(); + }); + + it('should also not be able to "Hide Column" via the Header Menu', () => { + cy.window().then((win) => { + cy.stub(win, 'alert').as('alertStub'); + }); + const newColumnList = ['', 'Title', '% Complete', 'Action']; + + cy.get('#grid20').find('.slick-header-column:nth(3)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item') + .contains('Hide Column') + .click() + .then(() => { + cy.get('@alertStub').should( + 'have.been.calledWith', + '[SlickGrid] Action not allowed and aborted, you need to have at least one or more column in the center section of the grid. ' + + 'You could alternatively unpin columns before trying again.' + ); + }); + cy.get('#grid20') - .find('.slick-header-columns:nth(0)') + .find('.slick-header-columns .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); + }); + + it('should be able to uncheck "Title" column without any alert', () => { + cy.get('#grid20').find('.slick-header-column').first().trigger('mouseover').trigger('contextmenu').invoke('show'); + const updatedColumns = ['', '% Complete', 'Action']; + cy.get('.slick-column-picker-list li:not(.hidden) .checkbox-picker-label').first().click(); + cy.get('.slick-column-picker:visible').find('.close').trigger('click').click(); + cy.get('.slick-header-columns:nth(0) .slick-header-column').each(($child, index) => expect($child.text()).to.eq(updatedColumns[index])); + }); + + it('should be able to add back hidden "Title" column without any alert', () => { + const updatedColumns = ['', 'Title', '% Complete', 'Action']; + cy.get('.slick-header-column').first().trigger('mouseover').trigger('contextmenu').invoke('show'); + cy.get('.slick-column-picker-list li:not(.hidden) .checkbox-picker-label').first().click(); + cy.get('.slick-column-picker:visible').find('.close').trigger('click').click(); + cy.get('.slick-header-columns:nth(0) .slick-header-column').each(($child, index) => expect($child.text()).to.eq(updatedColumns[index])); + }); + + it('should reset hidden column from the Column Picker and expect all columns to be back', () => { + const leftColumns = ['', 'Title', '% Complete']; + const rightColumns = ['Start', 'Finish', 'Completed', 'Cost | Duration', 'City of Origin', 'Action']; + + cy.get('.slick-header-column').first().trigger('mouseover').trigger('contextmenu').invoke('show'); + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') .children() - .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + .each(($child, index) => { + if (index >= leftColumns.length) { + if ($child.text() === rightColumns[index - leftColumns.length]) { + expect($child.text()).to.eq(rightColumns[index - leftColumns.length]); + if (index <= rightColumns.length + 1) { + cy.wrap($child).children('label').click(); + } + } + } + }); + + cy.get('.slick-column-picker:visible').find('.close').trigger('click').click(); + + cy.get('#grid20') + .find('.slick-header-columns .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(withoutTitleRowTitles[index])); }); - it('should click on the Grid Menu command "Unfreeze Columns/Rows" to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => { - cy.get('#grid20').find('button.slick-grid-menu-button').click({ force: true }); + describe('Test UI rendering after Scrolling with large columns', () => { + it('should unpin all columns/rows', () => { + cy.get('#grid20').find('button.slick-grid-menu-button').click({ force: true }); + + cy.contains('Unpin Columns/Rows').click({ force: true }); + }); + + it('should resize all columns and make them wider', () => { + // resize CityOfOrigin column + cy.get('.slick-header-columns .slick-header-column:nth(7)').should('contain', 'City of Origin'); + + cy.get('.slick-resizable-handle:nth(7)').trigger('mousedown', { which: 1, force: true }).trigger('mousemove', 'bottomRight'); + + cy.get('.slick-header-column:nth(8)') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); - cy.contains('Unfreeze Columns/Rows').click({ force: true }); + // resize Cost|Duration column + cy.get('.slick-header-columns .slick-header-column:nth(6)').should('contain', 'Cost | Duration'); - cy.get('[style="transform: translateY(0px);"]').should('have.length', 1); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]').children().should('have.length', 11); + cy.get('.slick-resizable-handle:nth(6)').trigger('mousedown', { which: 1, force: true }).trigger('mousemove', 'bottomRight'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '0'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + cy.get('.slick-header-column:nth(8)') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + + // resize Completed column + cy.get('.slick-header-columns .slick-header-column:nth(5)').should('contain', 'Completed'); + + cy.get('.slick-resizable-handle:nth(5)').trigger('mousedown', { which: 1, force: true }).trigger('mousemove', 'bottomRight'); + + cy.get('.slick-header-column:nth(7)') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + + // resize Finish column + cy.get('.slick-header-columns .slick-header-column:nth(4)').should('contain', 'Finish'); + + cy.get('.slick-resizable-handle:nth(4)').trigger('mousedown', { which: 1, force: true }).trigger('mousemove', 'bottomRight'); + + cy.get('.slick-header-column:nth(6)') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + + // resize Start column + cy.get('.slick-header-columns .slick-header-column:nth(3)').should('contain', 'Start'); + + cy.get('.slick-resizable-handle:nth(3)').trigger('mousedown', { which: 1, force: true }).trigger('mousemove', 'bottomRight'); + + cy.get('.slick-header-column:nth(6)') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + + // resize %Complete column + cy.get('.slick-header-columns .slick-header-column:nth(2)').should('contain', '% Complete'); + + cy.get('.slick-resizable-handle:nth(2)').trigger('mousedown', { which: 1, force: true }).trigger('mousemove', 'bottomRight'); + + cy.get('.slick-header-column:nth(3)') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + + // resize Title column + cy.get('.slick-header-columns .slick-header-column:nth(1)').should('contain', 'Title'); + + cy.get('.slick-resizable-handle:nth(1)').trigger('mousedown', { which: 1, force: true }).trigger('mousemove', 'bottomRight'); + + cy.get('.slick-header-column:nth(3)') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + }); + + it('should scroll horizontally completely to the right and expect all cell to be rendered', () => { + getCell(2, 1).contains(/Task [0-9]*/); + getCell(2, 2).contains(/[0-9]*/); + + getCell(15, 1).contains(/Task [0-9]*/); + getCell(15, 2).contains(/[0-9]*/); + + // horizontal scroll to right + // Pinning has one real horizontal scroll owner. Scrolling the old body + // viewport only exercises the compatibility bridge; target the proxy + // here to verify the user-facing scrollbar and all chrome move together. + cy.get('#grid20 .slick-horizontal-scroller').scrollTo('100%', '0%', { duration: 1500 }); + getCell(2, 3).should('contain', '2009-01-01'); + getCell(2, 4).should('contain', '2009-05-05'); + getCell(2, 7).contains(/[United State|Canada]*/); + getCell(2, 8).should('contain', 'Action'); + + getCell(15, 3).should('contain', '2009-01-01'); + getCell(15, 4).should('contain', '2009-05-05'); + getCell(15, 7).contains(/[United State|Canada]*/); + getCell(15, 8).should('contain', 'Action'); + }); + + it('should scroll vertically to the middle of the grid and expect all cell to be rendered', () => { + // vertical scroll to middle + cy.get('.slick-vertical-scroller').scrollTo('0%', '40%', { duration: 1500 }); + + getCell(200, 3).should('contain', '2009-01-01'); + getCell(200, 4).should('contain', '2009-05-05'); + getCell(200, 7).contains(/[United State|Canada]*/); + getCell(200, 8).should('contain', 'Action'); + + getCell(205, 3).should('contain', '2009-01-01'); + getCell(205, 4).should('contain', '2009-05-05'); + getCell(205, 7).contains(/[United State|Canada]*/); + getCell(205, 8).should('contain', 'Action'); + + // reset scroll + cy.get('.slick-vertical-scroller').scrollTo(0, 0, { ensureScrollable: false }); + cy.get('.slick-horizontal-scroller').scrollTo(0, 0, { ensureScrollable: false }); + }); + }); + + describe('accessibility sub-menus tests', () => { + beforeEach(() => { + // Open the context menu on a cell to start each test + cy.get('.slick-vertical-scroller').scrollTo(0, 0, { ensureScrollable: false }); + cy.get('.slick-horizontal-scroller').scrollTo(0, 0, { ensureScrollable: false }); + cy.get('[data-row="0"] .slick-cell.l3.r3').rightclick({ force: true }); + cy.get('.slick-context-menu.slick-menu-level-0').should('be.visible'); + }); + + it('should open Exports sub-menu with ArrowRight, then Excel sub-menu with ArrowRight, and close with ArrowLeft', () => { + // Move down to "Exports" (4th item) + cy.focused(); + cy.press(Cypress.Keyboard.Keys.DOWN); + cy.press(Cypress.Keyboard.Keys.DOWN); + cy.get('.slick-context-menu.slick-menu-level-0 .slick-submenu-item[data-command="export"]').should('have.focus'); + + // Open "Exports" sub-menu with ArrowRight + cy.focused().type('{rightarrow}'); + cy.get('.slick-context-menu.slick-menu-level-1[data-sub-menu-parent="export"]').should('be.visible'); + + // Move down to "Excel" (2nd item in sub-menu) + cy.focused().type('{downarrow}'); + cy.get('.slick-context-menu.slick-menu-level-1 .slick-submenu-item[data-command="sub-menu"]').should('have.focus'); + + // Open "Excel" sub-menu with ArrowRight + cy.focused().type('{rightarrow}'); + cy.get('.slick-context-menu.slick-menu-level-2[data-sub-menu-parent="sub-menu"]').should('be.visible'); + + // Move down to "Excel (xlsx)" (2nd item in Excel sub-menu) + cy.focused().type('{downarrow}'); + cy.get('.slick-context-menu.slick-menu-level-2 .slick-menu-item[data-command="exports-xlsx"]').should('have.focus'); + + // Close Excel sub-menu with ArrowLeft + cy.focused().type('{leftarrow}'); + cy.get('.slick-context-menu.slick-menu-level-2').should('not.exist'); + cy.get('.slick-context-menu.slick-menu-level-1 .slick-submenu-item[data-command="sub-menu"]').should('have.focus'); + + // close all context menus + cy.get('[data-row="0"] .slick-cell.l0.r0').click(); + }); + + it('should open sub-menus using Enter as well as ArrowRight', () => { + // Move down to "Exports" + cy.focused(); + cy.press(Cypress.Keyboard.Keys.DOWN); + cy.press(Cypress.Keyboard.Keys.DOWN); + cy.get('.slick-context-menu.slick-menu-level-0 .slick-submenu-item[data-command="export"]').should('have.focus'); + + // Open "Exports" sub-menu with Enter + cy.focused().type('{enter}'); + cy.get('.slick-context-menu.slick-menu-level-1[data-sub-menu-parent="export"]').should('be.visible'); + + // Move down to "Excel" + cy.focused().type('{downarrow}'); + cy.get('.slick-context-menu.slick-menu-level-1 .slick-submenu-item[data-command="sub-menu"]').should('have.focus'); + + // Open "Excel" sub-menu with Enter + cy.focused().type('{enter}'); + cy.get('.slick-context-menu.slick-menu-level-2[data-sub-menu-parent="sub-menu"]').should('be.visible'); + + // close all context menus + cy.get('[data-row="0"] .slick-cell.l0.r0').click(); + }); + + it('should activate a sub-menu leaf item with Enter', () => { + // Move down to "Exports" + cy.window().then((win) => { + cy.stub(win, 'alert').as('alertStub'); + }); + cy.focused(); + cy.press(Cypress.Keyboard.Keys.DOWN); + cy.press(Cypress.Keyboard.Keys.DOWN); + cy.press(Cypress.Keyboard.Keys.ENTER); + cy.get('.slick-context-menu.slick-menu-level-1[data-sub-menu-parent="export"]').should('be.visible'); + + // "Text (tab delimited)" is first item, should have focus + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-item[data-command="exports-txt"]').should('have.focus'); + // Activate with Enter (add your assertion for the result) + cy.focused().type('{enter}'); + }); + + it('should reapply 3 Pinned Columns and expect to be able to focus on first filter and go left/right between both viewports without problems', () => { + cy.get('[data-test="set-3pinned-columns"]').click(); + cy.get('.slick-headerrow-column.l1 input').focus(); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.get('.slick-headerrow-column.l2 input').should('have.focus'); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.get('.slick-headerrow-column.l3 select').should('have.focus'); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.get('.slick-headerrow-column.l3 input').should('have.focus'); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.get('.slick-headerrow-column.l4 select').should('have.focus'); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.get('.slick-headerrow-column.l4 input').should('have.focus'); + + // Shift+Tab dosn't work in Cypress, so we can't go further with tests + }); + }); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(3)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(4)').should('contain', '2009-05-05'); + describe('drag & drop column reordering with auto-scroll', () => { + it('should auto-scroll right viewport and reorder columns when "Start" is dragged well past the right edge (ending up after "Finish")', () => { + // Close the context menu opened by beforeEach + cy.get('body').type('{esc}'); + + // Control app timers to make drag auto-scroll deterministic in CI. + cy.clock(); + + // Normalize right viewport scroll so this test is isolated from previous test state. + cy.get('.slick-horizontal-scroller').then(($viewport) => { + $viewport[0].scrollLeft = 0; + $viewport[0].dispatchEvent(new Event('scroll', { bubbles: true })); + }); + cy.get('.slick-horizontal-scroller').its('0.scrollLeft').should('equal', 0); + cy.get('[data-test="set-large-pinned-columns"]').click(); + + // Step 1: call SortableJS onStart for the "Start" column (1st center-section column). + // This binds the document 'drag' auto-scroll listener for the shared proxy scrollbar. + cy.get('.slick-header-columns-center').then(($rightHeader) => { + let sortInstance: any; + Object.keys($rightHeader[0]).forEach((prop) => { + if (prop.startsWith('Sortable')) { + sortInstance = ($rightHeader[0] as any)[prop]; + } + }); + expect(sortInstance).to.exist; + const startColumnEl = $rightHeader[0].querySelectorAll('.slick-header-column')[0] as HTMLElement; + sortInstance.options.onStart({ item: startColumnEl }); + }); + + // Step 2: fire a document drag event well past the right edge (viewport-relative) + // to avoid CI flakiness caused by environment-dependent viewport widths. + cy.window().then((win) => { + const dragX = win.innerWidth + 1200; + cy.document().trigger('drag', { pageX: dragX, clientX: dragX, clientY: 50 }); + }); + + // Step 3: advance mocked time so the 30ms scroll interval ticks several times. + cy.tick(350); + + // Auto-scroll should have moved the right viewport to the right + cy.get('.slick-horizontal-scroller').its('0.scrollLeft').should('be.greaterThan', 0); + + // Step 4: simulate the drag result — "Start" was moved to the right, past "Finish". + // SortableJS reads the DOM order via toArray() inside onEnd, so physically reorder the children first. + cy.get('.slick-header-columns-center').then(($rightHeader) => { + let sortInstance: any; + Object.keys($rightHeader[0]).forEach((prop) => { + if (prop.startsWith('Sortable')) { + sortInstance = ($rightHeader[0] as any)[prop]; + } + }); + expect(sortInstance).to.exist; + const startColumnEl = $rightHeader[0].querySelector('[data-id="start"]') as HTMLElement; + const finishColumnEl = $rightHeader[0].querySelector('[data-id="finish"]') as HTMLElement; + expect(startColumnEl).to.exist; + expect(finishColumnEl).to.exist; + + // Move "Finish" before "Start" → mirrors dragging Start past Finish + $rightHeader[0].insertBefore(finishColumnEl, startColumnEl); + + // onEnd reads the new DOM order via toArray() and calls setColumns() if the order changed + sortInstance.options.onEnd({ item: startColumnEl, stopPropagation: () => {} }); + }); + + // The center region should now place Finish before Start. The exact + // region membership can vary when a large requested pin band does not + // fit the current viewport, so assert semantic order by column id. + cy.get('.slick-header-column').then(($headers) => { + const ids = [...$headers].map((header) => header.dataset.id); + expect(ids.indexOf('finish')).to.be.lessThan(ids.indexOf('start')); + }); + + // When a left band is active, its order must remain unchanged. + cy.get('.slick-header-columns-left').then(($leftRegion) => { + const left = $leftRegion.find('.slick-header-column'); + if (left.length) { + expect([...left].map((header) => header.dataset.id)).to.deep.equal(['_checkbox_selector', 'title', 'percentComplete']); + } + }); + }); }); }); diff --git a/demos/aurelia/test/cypress/e2e/example24.cy.ts b/demos/aurelia/test/cypress/e2e/example24.cy.ts index c2ea74764b..b73ab056b1 100644 --- a/demos/aurelia/test/cypress/e2e/example24.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example24.cy.ts @@ -232,7 +232,7 @@ describe('Example 24 - Cell Menu & Context Menu Plugins', () => { }); it('should check Context Menu "menuUsabilityOverride" condition and expect to not be able to open Context Menu from rows than are >= to Task 21', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('bottom').wait(25); + cy.get('.slick-vertical-scroller').scrollTo('bottom').wait(25); cy.get('#grid24').find('.slick-row:nth(3) .slick-cell:nth(1)').rightclick({ force: true }); @@ -240,7 +240,7 @@ describe('Example 24 - Cell Menu & Context Menu Plugins', () => { }); it('should scroll back to top row and be able to open Context Menu', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top').wait(25); + cy.get('.slick-vertical-scroller').scrollTo('top').wait(25); cy.get('#grid24').find('.slick-row:nth(1) .slick-cell:nth(1)').rightclick({ force: true }); diff --git a/demos/aurelia/test/cypress/e2e/example27.cy.ts b/demos/aurelia/test/cypress/e2e/example27.cy.ts index 8b76749726..28873fbf4b 100644 --- a/demos/aurelia/test/cypress/e2e/example27.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example27.cy.ts @@ -160,7 +160,7 @@ describe('Example 27 - Tree Data (from a flat dataset with parentId references)' it('should be able to update the 1st row item (Task 0)', () => { cy.get('[data-test=update-item-btn]').contains('Update 1st Row Item').click(); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); const now = new Date(); const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; @@ -201,7 +201,7 @@ describe('Example 27 - Tree Data (from a flat dataset with parentId references)' cy.get(`.slick-grid-menu:visible`).find('.slick-menu-item').first().find('span').contains('Clear all Filters').click(); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); }); it('should be able to open "Task 1" and "Task 3" parents', () => { diff --git a/demos/aurelia/test/cypress/e2e/example28.cy.ts b/demos/aurelia/test/cypress/e2e/example28.cy.ts index 8f8f5e2228..123830953d 100644 --- a/demos/aurelia/test/cypress/e2e/example28.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example28.cy.ts @@ -84,7 +84,7 @@ describe('Example 28 - Tree Data (from a Hierarchical Dataset)', () => { it('should expand "pdf" folder and expect all folders to be expanded', () => { cy.get('[data-row="4"] > .slick-cell:nth(0) .slick-group-toggle.collapsed').click(); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top', { force: true } as any); + cy.get('.slick-vertical-scroller').scrollTo('top', { force: true } as any); }); it('should have default Files list', () => { @@ -97,7 +97,7 @@ describe('Example 28 - Tree Data (from a Hierarchical Dataset)', () => { }); it('should have pop songs folder with aggregations of Sum(53.3MB) / Avg(26.65MB)', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('center', { force: true } as any); + cy.get('.slick-vertical-scroller').scrollTo('center', { force: true } as any); cy.get('[data-row="16"] > .slick-cell:nth(0)').should('contain', 'music'); cy.get('[data-row="16"] > .slick-cell:nth(3)').should('contain', 'sum: 151.3 MB / avg: 50.43 MB'); @@ -118,7 +118,7 @@ describe('Example 28 - Tree Data (from a Hierarchical Dataset)', () => { }); it('should have pop songs folder with updated aggregations including new pop songs of Sum(218.3MB) / Avg(54.58MB)', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('bottom', { force: true } as any); + cy.get('.slick-vertical-scroller').scrollTo('bottom', { force: true } as any); cy.get('[data-row="16"] > .slick-cell:nth(0)').should('contain', 'music'); cy.get('[data-row="16"] > .slick-cell:nth(3)').should('contain', 'sum: 316.3 MB / avg: 63.26 MB'); @@ -225,7 +225,7 @@ describe('Example 28 - Tree Data (from a Hierarchical Dataset)', () => { }); it('should have pop songs folder with updated aggregations including 4 pop songs of Sum(400.3MB) / Avg(66.72MB)', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('bottom', { force: true } as any); + cy.get('.slick-vertical-scroller').scrollTo('bottom', { force: true } as any); cy.get('[data-row="16"] > .slick-cell:nth(0)').should('contain', 'music'); cy.get('[data-row="16"] > .slick-cell:nth(3)').should('contain', 'sum: 400.3 MB / avg: 66.72 MB'); @@ -308,7 +308,7 @@ describe('Example 28 - Tree Data (from a Hierarchical Dataset)', () => { }); it('should have again the pop songs folder with updated aggregations including 4 pop songs of Sum(400.3MB) / Avg(66.72MB)', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('center', { force: true } as any); + cy.get('.slick-vertical-scroller').scrollTo('center', { force: true } as any); cy.get('[data-row="16"] > .slick-cell:nth(0)').should('contain', 'music'); cy.get('[data-row="16"] > .slick-cell:nth(3)').should('contain', 'sum: 400.3 MB / avg: 66.72 MB'); @@ -336,7 +336,7 @@ describe('Example 28 - Tree Data (from a Hierarchical Dataset)', () => { }); it('should have pop songs folder with aggregation reflecting what is displayed, Sum(316.3MB) / Avg(63.26MB)', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('center', { force: true } as any); + cy.get('.slick-vertical-scroller').scrollTo('center', { force: true } as any); cy.get('[data-row="16"] > .slick-cell:nth(0)').should('contain', 'music'); cy.get('[data-row="16"] > .slick-cell:nth(3)').should('contain', 'sum: 316.3 MB / avg: 63.26 MB'); @@ -347,7 +347,7 @@ describe('Example 28 - Tree Data (from a Hierarchical Dataset)', () => { }); it('should have documents with same Sum as the beginning since auto-recalc is disabled, aggregation should be Sum(14.46MB) / Avg(1.45MB)', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top', { force: true } as any); + cy.get('.slick-vertical-scroller').scrollTo('top', { force: true } as any); cy.get('[data-row="1"] > .slick-cell:nth(0)').should('contain', 'documents'); cy.get('[data-row="1"] > .slick-cell:nth(3)').should('contain', 'sum: 14.46 MB / avg: 1.45 MB (total)'); diff --git a/demos/aurelia/test/cypress/e2e/example38.cy.ts b/demos/aurelia/test/cypress/e2e/example38.cy.ts index a179cdc5cb..02b700180d 100644 --- a/demos/aurelia/test/cypress/e2e/example38.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example38.cy.ts @@ -20,7 +20,7 @@ describe('Example 38 - Infinite Scroll with OData', () => { it('should scroll to bottom of the grid and expect next batch of 30 items appended to current dataset for a total of 60 items', () => { cy.get('[data-test="itemCount"]').should('have.text', '30'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '60'); @@ -32,7 +32,7 @@ describe('Example 38 - Infinite Scroll with OData', () => { it('should scroll to bottom of the grid and expect next batch of 30 items appended to current dataset for a new total of 90 items', () => { cy.get('[data-test="itemCount"]').should('have.text', '60'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '90'); @@ -48,7 +48,7 @@ describe('Example 38 - Infinite Scroll with OData', () => { cy.get('[data-test="data-loaded-tag"]').should('not.have.class', 'fully-loaded'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '100'); @@ -78,7 +78,7 @@ describe('Example 38 - Infinite Scroll with OData', () => { it('should scroll to bottom again and expect next batch of 30 items appended to current dataset for a total of 60 items', () => { cy.get('[data-test="itemCount"]').should('have.text', '30'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '60'); @@ -104,7 +104,7 @@ describe('Example 38 - Infinite Scroll with OData', () => { it('should scroll to bottom again and expect next batch to be only 20 females appended to current dataset for a total of 50 items found in DB', () => { cy.get('[data-test="itemCount"]').should('have.text', '30'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '50'); @@ -119,7 +119,7 @@ describe('Example 38 - Infinite Scroll with OData', () => { cy.get('[data-test="itemCount"]').should('have.text', '30'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('[data-test=odata-query-result]').should(($span) => { expect($span.text()).to.eq(`$count=true&$top=30`); @@ -130,11 +130,11 @@ describe('Example 38 - Infinite Scroll with OData', () => { }); it('should scroll to the bottom "Group by Gender" and expect 30 more items for a total of 60 items grouped', () => { - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '60'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('[data-test=odata-query-result]').should(($span) => { expect($span.text()).to.eq(`$count=true&$top=30&$skip=30`); diff --git a/demos/aurelia/test/cypress/e2e/example39.cy.ts b/demos/aurelia/test/cypress/e2e/example39.cy.ts index d7091726f6..a87b8ef667 100644 --- a/demos/aurelia/test/cypress/e2e/example39.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example39.cy.ts @@ -29,7 +29,7 @@ describe('Example 39 - Infinite Scroll with GraphQL', () => { it('should scroll to bottom of the grid and expect next batch of 30 items appended to current dataset for a total of 60 items', () => { cy.get('[data-test="itemCount"]').should('have.text', '30'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '60'); @@ -44,7 +44,7 @@ describe('Example 39 - Infinite Scroll with GraphQL', () => { it('should scroll to bottom of the grid and expect next batch of 30 items appended to current dataset for a new total of 90 items', () => { cy.get('[data-test="itemCount"]').should('have.text', '60'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '90'); @@ -63,7 +63,7 @@ describe('Example 39 - Infinite Scroll with GraphQL', () => { cy.get('[data-test="data-loaded-tag"]').should('not.have.class', 'fully-loaded'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '100'); @@ -100,7 +100,7 @@ describe('Example 39 - Infinite Scroll with GraphQL', () => { it('should scroll to bottom again and expect next batch of 30 items appended to current dataset for a total of 60 items', () => { cy.get('[data-test="itemCount"]').should('have.text', '30'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '60'); @@ -135,7 +135,7 @@ describe('Example 39 - Infinite Scroll with GraphQL', () => { it('should scroll to bottom again and expect next batch to be only 20 females appended to current dataset for a total of 50 items found in DB', () => { cy.get('[data-test="itemCount"]').should('have.text', '30'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '50'); diff --git a/demos/aurelia/test/cypress/e2e/example40.cy.ts b/demos/aurelia/test/cypress/e2e/example40.cy.ts index 4f8eeb96e2..4ef56ccdec 100644 --- a/demos/aurelia/test/cypress/e2e/example40.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example40.cy.ts @@ -25,14 +25,14 @@ describe('Example 40 - Infinite Scroll from JSON data', () => { it('should scroll to bottom of the grid and expect next batch of 50 items appended to current dataset for a total of 100 items', () => { cy.get('[data-test="totalItemCount"]').should('have.text', '50'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom', { timeout: 100 }); + cy.get('.slick-vertical-scroller').scrollTo('bottom', { timeout: 100 }); cy.get('[data-test="totalItemCount"]').should('have.text', '100'); }); it('should scroll to bottom of the grid again and expect 50 more items for a total of now 150 items', () => { cy.get('[data-test="totalItemCount"]').should('have.text', '100'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom', { timeout: 100 }); + cy.get('.slick-vertical-scroller').scrollTo('bottom', { timeout: 100 }); cy.get('[data-test="totalItemCount"]').should('have.text', '150'); }); @@ -42,7 +42,7 @@ describe('Example 40 - Infinite Scroll from JSON data', () => { cy.get('[data-id="title"]').click(); cy.get('[data-test="totalItemCount"]').should('have.text', '150'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('[data-row="0"] > .slick-cell:nth(0)').should('contain', 'Task 0'); cy.get('[data-row="1"] > .slick-cell:nth(0)').should('contain', 'Task 1'); @@ -55,7 +55,7 @@ describe('Example 40 - Infinite Scroll from JSON data', () => { cy.get('[data-id="title"]').click(); cy.get('[data-test="totalItemCount"]').should('have.text', '50'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('[data-row="0"] > .slick-cell:nth(0)').should('contain', 'Task 9'); cy.get('[data-row="1"] > .slick-cell:nth(0)').should('contain', 'Task 8'); @@ -68,17 +68,17 @@ describe('Example 40 - Infinite Scroll from JSON data', () => { cy.get('[data-test="totalItemCount"]').should('have.text', '50'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('[data-row="0"] > .slick-cell:nth(0) .slick-group-toggle.expanded').should('have.length', 1); cy.get('[data-row="0"] > .slick-cell:nth(0) .slick-group-title').contains(/Duration: [0-9]/); }); it('should scroll to the bottom "Group by Duration" and expect 50 more items for a total of 100 items grouped', () => { - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom', { timeout: 100 }); + cy.get('.slick-vertical-scroller').scrollTo('bottom', { timeout: 100 }); cy.get('[data-test="totalItemCount"]').should('have.text', '100'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('[data-row="0"] > .slick-cell:nth(0) .slick-group-toggle.expanded').should('have.length', 1); cy.get('[data-row="0"] > .slick-cell:nth(0) .slick-group-title').contains(/Duration: [0-9]/); }); @@ -103,12 +103,12 @@ describe('Example 40 - Infinite Scroll from JSON data', () => { it('should load 200 items and filter "Start" column with <=2020-08-25', () => { cy.get('[data-test="set-dynamic-filter"]').click(); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom', { timeout: 100 }); + cy.get('.slick-vertical-scroller').scrollTo('bottom', { timeout: 100 }); cy.get('[data-test="totalItemCount"]').should('have.text', '150'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom', { timeout: 100 }); + cy.get('.slick-vertical-scroller').scrollTo('bottom', { timeout: 100 }); cy.get('[data-test="totalItemCount"]').should('have.text', '200'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get(`[data-row=0] > .slick-cell:nth(3)`).contains(/^Aug [0-9]{2}, 2020$/); cy.get(`[data-row=1] > .slick-cell:nth(3)`).contains(/^Aug [0-9]{2}, 2020$/); diff --git a/demos/aurelia/test/cypress/e2e/example43.cy.ts b/demos/aurelia/test/cypress/e2e/example43.cy.ts index bfda594730..ca0c3b17cb 100644 --- a/demos/aurelia/test/cypress/e2e/example43.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example43.cy.ts @@ -27,15 +27,23 @@ describe('Example 43 - colspan/rowspan - Employees Timesheets', { retries: 0 }, cy.get('h2').should('contain', 'Example 43: colspan/rowspan - Employees Timesheets'); }); + it('should hide sub-title', () => { + cy.get('[data-test=toggle-subtitle]').click(); + }); + it('should have exact column titles', () => { cy.get('.slick-header-columns') .children() .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); }); - it('should expect 1st column to be frozen (frozen)', () => { - cy.get('.grid-canvas-left .slick-cell.frozen').should('have.length', 10); - cy.get('.grid-canvas-right .slick-cell:not(.frozen)').should('have.length.above', 50); + it('should expect 1st column to be pinned', () => { + // Pinning uses one live canvas and splits each row into left/center/right + // regions; the old grid-canvas-left/right pinned panes no longer exist. + cy.get('.slick-pinned-left-cells .slick-cell.slick-cell-pinned-left').should('have.length', 10); + // The shared Vue Cypress viewport is 1200px wide, but its route sidebar + // leaves less room for center-column virtualization than the Vanilla app. + cy.get('.slick-scrolling-cells .slick-cell').should('have.length.above', 50); }); it('should not display any Column Picker in the Grid Menu', () => { @@ -47,32 +55,32 @@ describe('Example 43 - colspan/rowspan - Employees Timesheets', { retries: 0 }, describe('Spanning', () => { it('should expect "Davolio", "Check Mail", and "Development" to all have rowspan of 2 in morning hours', () => { - cy.get(`[data-row=0] > .slick-cell.l1.r1.rowspan`).should('contain', 'Davolio'); - cy.get(`[data-row=0] > .slick-cell.l1.r1.rowspan`).should(($el) => + cy.get(`[data-row=0] .slick-cell.l1.r1.rowspan`).should('contain', 'Davolio'); + cy.get(`[data-row=0] .slick-cell.l1.r1.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); - cy.get(`[data-row=2] > .slick-cell.l2.r4.rowspan`).should('contain', 'Check Mail'); - cy.get(`[data-row=2] > .slick-cell.l2.r4.rowspan`).should(($el) => + cy.get(`[data-row=2] .slick-cell.l2.r4.rowspan`).should('contain', 'Check Mail'); + cy.get(`[data-row=2] .slick-cell.l2.r4.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); - cy.get(`[data-row=8] > .slick-cell.l7.r9.rowspan`).should('contain', 'Development'); - cy.get(`[data-row=8] > .slick-cell.l7.r9.rowspan`).should(($el) => + cy.get(`[data-row=8] .slick-cell.l7.r9.rowspan`).should('contain', 'Development'); + cy.get(`[data-row=8] .slick-cell.l7.r9.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); }); it('should expect "Lunch Break" to span over 3 columns and over all rows', () => { - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should(($el) => + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 10) ); }); it('should expect a large "Development" section that spans over multiple columns & rows in the afternoon', () => { - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should(($el) => + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5) ); }); @@ -80,334 +88,340 @@ describe('Example 43 - colspan/rowspan - Employees Timesheets', { retries: 0 }, describe('Basic Key Navigations', () => { it('should start at Employee 10001, then type "End" key and expect to be in "Team Meeting" between 4:30-5:00pm', () => { - cy.get('[data-row=0] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l0.r0.active').should('contain', '10001'); + cy.get('[data-row=0] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l0.r0.active').should('contain', '10001'); cy.get('@active_cell').type('{end}'); - cy.get('[data-row=0] > .slick-cell.l17.r18.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l17.r18.active').should('contain', 'Team Meeting'); }); it('should start at Employee 10002, then type "End" key and also expect to be in "Team Meeting" between 4:30-5:00pm', () => { - cy.get('[data-row=1] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=1] > .slick-cell.l0.r0.active').should('contain', '10002'); + cy.get('[data-row=1] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=1] .slick-cell.l0.r0.active').should('contain', '10002'); cy.get('@active_cell').type('{end}'); - cy.get('[data-row=0] > .slick-cell.l17.r18.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l17.r18.active').should('contain', 'Team Meeting'); }); it('should start at Employee 10004, then type "ArrowRight" key twice and expect to be in "Check Mail" between 9:00-10:30am', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{rightarrow}{rightarrow}'); - cy.get('[data-row=2] > .slick-cell.l2.r4.active').should('contain', 'Check Mail'); + cy.get('[data-row=2] .slick-cell.l2.r4.active').should('contain', 'Check Mail'); }); it('should start at Employee 10004, then type "ArrowRight" key 4x times and expect to be in "Testing" between 11:00-1:00pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}'); - cy.get('[data-row=3] > .slick-cell.l6.r9.active').should('contain', 'Testing'); + cy.get('[data-row=3] .slick-cell.l6.r9.active').should('contain', 'Testing'); }); it('should start at Employee 10004, then type "ArrowRight" key 5x times and expect to be in "Lunch Break"', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}'); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); }); it('should start at Employee 10004, then type "ArrowRight" key 6x times and expect to be in "Development" between 2:30-3:30pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}'); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); }); // then rollback by going backward it('should be on Employee 10004 row at previous "Development" cell, then type "ArrowLeft" key once and expect to be in "Lunch Break"', () => { - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).as('active_cell').click(); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); + // The proxy scrollbar is a sibling of the canvas, so native + // scrollIntoView() cannot reveal a horizontally virtualized cell. + cy.get('.slick-horizontal-scroller').scrollTo('right', { ensureScrollable: false }); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).as('active_cell').scrollIntoView().click(); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); cy.get('@active_cell').type('{leftarrow}'); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key once and expect to be in "Conference" between 4:00-5:00pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).type('{leftarrow}'); - cy.get(`[data-row=3] > .slick-cell.l16.r17.active`).should('contain', 'Conference'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).type('{leftarrow}'); + cy.get(`[data-row=3] .slick-cell.l16.r17.active`).should('contain', 'Conference'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key 3x times and expect to be back to "Development" between 2:30-3:30pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).type('{leftarrow}{leftarrow}{leftarrow}'); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).type('{leftarrow}{leftarrow}{leftarrow}'); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key 4x times and expect to be back to "Lunch Break"', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{leftarrow}{leftarrow}{leftarrow}{leftarrow}'); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key 5x times and expect to be back to "Testing" between 11:00-1:00pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{leftarrow}{leftarrow}{leftarrow}{leftarrow}{leftarrow}'); - cy.get(`[data-row=3] > .slick-cell.l6.r9.active`).should('contain', 'Testing'); + cy.get(`[data-row=3] .slick-cell.l6.r9.active`).should('contain', 'Testing'); }); // going down it('should start at 10am "Team Meeting, then type "ArrowDown" key once and expect to be in "Support" between 9:30-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('.slick-horizontal-scroller').scrollTo(0, 0, { ensureScrollable: false }); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}'); - cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support'); + cy.get(`[data-row=1] .slick-cell.l3.r5.active`).should('contain', 'Support'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key twice and expect to be in "Check Email" between 9:00-10:30am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}{downarrow}'); - cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); + cy.get(`[data-row=2] .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 3x times and expect to be in "Task Assign" between 9:00-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}'); - cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); + cy.get(`[data-row=4] .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times and expect to be in "Support" between 10:00-11:30am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}'); - cy.get(`[data-row=5] > .slick-cell.l4.r6.active`).should('contain', 'Support'); + cy.get(`[data-row=5] .slick-cell.l4.r6.active`).should('contain', 'Support'); }); // going up from inverse it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" once and expect to be in "Task Assign" between 9:00-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}'); - cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); + cy.get(`[data-row=4] .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 2x times and expect to be in "Task Assign" between 9:00-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}{uparrow}'); - cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); + cy.get(`[data-row=2] .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 3x times and expect to be in "Support" between 10:00-11:30am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}{uparrow}{uparrow}'); - cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support'); + cy.get(`[data-row=1] .slick-cell.l3.r5.active`).should('contain', 'Support'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 4x times and expect to be back to same "Team Meeting"', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}{uparrow}{uparrow}{uparrow}'); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); }); }); describe('Grid Navigate Functions', () => { it('should start at Employee 10004, then type "Navigate Right" twice and expect to be in "Check Mail" between 9:00-10:30am', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); cy.get('[data-test="goto-next"]').click().click(); - cy.get('[data-row=2] > .slick-cell.l2.r4.active').should('contain', 'Check Mail'); + cy.get('[data-row=2] .slick-cell.l2.r4.active').should('contain', 'Check Mail'); }); it('should start at Employee 10004, then type "Navigate Right" 4x times and expect to be in "Testing" between 11:00-1:00pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('[data-test="goto-next"]').click().click().click().click(); - cy.get('[data-row=3] > .slick-cell.l6.r9.active').should('contain', 'Testing'); + cy.get('[data-row=3] .slick-cell.l6.r9.active').should('contain', 'Testing'); }); it('should start at Employee 10004, then type "Navigate Right" 5x times and expect to be in "Lunch Break"', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('[data-test="goto-next"]').click().click().click().click().click(); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); }); it('should start at Employee 10004, then type "Navigate Right" 6x times and expect to be in "Development" between 2:30-3:30pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('[data-test="goto-next"]').click().click().click().click().click().click(); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); }); // then rollback by going backward it('should be on Employee 10004 row at previous "Development" cell, then type "Navigate Left" once and expect to be in "Lunch Break"', () => { - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).as('active_cell').click(); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); + cy.get('.slick-horizontal-scroller').scrollTo('right', { ensureScrollable: false }); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).as('active_cell').scrollIntoView().click(); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); cy.get('[data-test="goto-prev"]').click(); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" once and expect to be in "Conference" between 4:00-5:00pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); cy.get('[data-test="goto-prev"]').click(); - cy.get(`[data-row=3] > .slick-cell.l16.r17.active`).should('contain', 'Conference'); + cy.get(`[data-row=3] .slick-cell.l16.r17.active`).should('contain', 'Conference'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" 3x times and expect to be back to "Development" between 2:30-3:30pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); cy.get('[data-test="goto-prev"]').click().click().click(); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" 4x times and expect to be back to "Lunch Break"', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); cy.get('[data-test="goto-prev"]').click().click().click().click(); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" 5x times and expect to be back to "Testing" between 11:00-1:00pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); cy.get('[data-test="goto-prev"]').click().click().click().click().click(); - cy.get(`[data-row=3] > .slick-cell.l6.r9.active`).should('contain', 'Testing'); + cy.get(`[data-row=3] .slick-cell.l6.r9.active`).should('contain', 'Testing'); }); // going down it('should start at 10am "Team Meeting, then type "ArrowDown" key once and expect to be in "Support" between 9:30-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('.slick-horizontal-scroller').scrollTo(0, 0, { ensureScrollable: false }); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click(); - cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support'); + cy.get(`[data-row=1] .slick-cell.l3.r5.active`).should('contain', 'Support'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key twice and expect to be in "Check Email" between 9:00-10:30am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click().click(); - cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); + cy.get(`[data-row=2] .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 3x times and expect to be in "Task Assign" between 9:00-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click().click().click(); - cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); + cy.get(`[data-row=4] .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times and expect to be in "Support" between 10:00-11:30am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click().click().click().click(); - cy.get(`[data-row=5] > .slick-cell.l4.r6.active`).should('contain', 'Support'); + cy.get(`[data-row=5] .slick-cell.l4.r6.active`).should('contain', 'Support'); }); // going up from inverse it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" once and expect to be in "Task Assign" between 9:00-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click().click().click().click(); cy.get('[data-test="goto-up"]').click(); - cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); + cy.get(`[data-row=4] .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 2x times and expect to be in "Task Assign" between 9:00-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click().click().click().click(); cy.get('[data-test="goto-up"]').click().click(); - cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); + cy.get(`[data-row=2] .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 3x times and expect to be in "Support" between 10:00-11:30am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click().click().click().click(); cy.get('[data-test="goto-up"]').click().click().click(); - cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support'); + cy.get(`[data-row=1] .slick-cell.l3.r5.active`).should('contain', 'Support'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 4x times and expect to be back to same "Team Meeting"', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click().click().click().click(); cy.get('[data-test="goto-up"]').click().click().click().click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); }); }); describe('Grid Editing', () => { it('should toggle editing', () => { cy.get('#isEditable').contains('false'); - cy.get('[data-row=0] > .slick-cell.l4.r4').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active .editor-text').should('not.exist'); + cy.get('[data-row=0] .slick-cell.l4.r4').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active .editor-text').should('not.exist'); cy.get('[data-test=toggle-editing]').click(); cy.get('#isEditable').contains('true'); - cy.get('[data-row=0] > .slick-cell.l4.r4').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active.editable .editor-text').should('exist'); - cy.get('[data-row=0] > .slick-cell.l4.r4.active.editable .editor-text').type('Team Meeting.xyz{enter}'); + cy.get('[data-row=0] .slick-cell.l4.r4').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active.editable .editor-text').should('exist'); + cy.get('[data-row=0] .slick-cell.l4.r4.active.editable .editor-text').type('Team Meeting.xyz{enter}'); }); // going down it('should have changed active cell to "Support" between 9:30-11:00am', () => { - cy.get('[data-row=1] > .slick-cell.l3.r5.active.editable .editor-text') + cy.get('[data-row=1] .slick-cell.l3.r5.active.editable .editor-text') .invoke('val') .then((text) => expect(text).to.eq('Support')); - cy.get('[data-row=1] > .slick-cell.l3.r5.active.editable .editor-text').type('Support.xyz{enter}'); + cy.get('[data-row=1] .slick-cell.l3.r5.active.editable .editor-text').type('Support.xyz{enter}'); }); it('should have changed active cell to "Check Email" between 9:00-10:30am', () => { - cy.get('[data-row=2] > .slick-cell.l2.r4.active.editable .editor-text') + cy.get('[data-row=2] .slick-cell.l2.r4.active.editable .editor-text') .invoke('val') .then((text) => expect(text).to.eq('Check Mail')); - cy.get('[data-row=2] > .slick-cell.l2.r4.active.editable .editor-text').type('Check Mail.xyz{enter}'); + cy.get('[data-row=2] .slick-cell.l2.r4.active.editable .editor-text').type('Check Mail.xyz{enter}'); }); it('should have changed active cell to "Task Assign" between 9:00-11:00am', () => { - cy.get('[data-row=4] > .slick-cell.l2.r5.active.editable .editor-text') + cy.get('[data-row=4] .slick-cell.l2.r5.active.editable .editor-text') .invoke('val') .then((text) => expect(text).to.eq('Task Assign')); - cy.get('[data-row=4] > .slick-cell.l2.r5.active.editable .editor-text').type('Task Assign.xyz{enter}'); + cy.get('[data-row=4] .slick-cell.l2.r5.active.editable .editor-text').type('Task Assign.xyz{enter}'); }); it('should have changed active cell to "Support" between 10:00-11:30am', () => { - cy.get('[data-row=5] > .slick-cell.l4.r6.active.editable .editor-text') + cy.get('[data-row=5] .slick-cell.l4.r6.active.editable .editor-text') .invoke('val') .then((text) => expect(text).to.eq('Support')); - cy.get('[data-row=5] > .slick-cell.l4.r6.active.editable .editor-text').type('Support.xyz{enter}'); + cy.get('[data-row=5] .slick-cell.l4.r6.active.editable .editor-text').type('Support.xyz{enter}'); }); it('should have changed active cell to "Testing" and cancel editing when typing "Escape" key', () => { - cy.get('[data-row=6] > .slick-cell.l4.r4.active.editable .editor-text') + cy.get('[data-row=6] .slick-cell.l4.r4.active.editable .editor-text') .invoke('val') .then((text) => expect(text).to.eq('Testing')); - cy.get('[data-row=6] > .slick-cell.l4.r4.active.editable .editor-text').type('{esc}'); - cy.get('[data-row=6] > .slick-cell.l4.r4.active.editable .editor-text').should('not.exist'); + cy.get('[data-row=6] .slick-cell.l4.r4.active.editable .editor-text').type('{esc}'); + cy.get('[data-row=6] .slick-cell.l4.r4.active.editable .editor-text').should('not.exist'); }); }); @@ -418,32 +432,32 @@ describe('Example 43 - colspan/rowspan - Employees Timesheets', { retries: 0 }, }); it('should expect EmployeeID to follow columns at index 0 column index', () => { - cy.get(`[data-row=0] > .slick-cell.l0.r0.rowspan`).should('contain', 'Davolio'); - cy.get(`[data-row=0] > .slick-cell.l0.r0.rowspan`).should(($el) => + cy.get(`[data-row=0] .slick-cell.l0.r0.rowspan`).should('contain', 'Davolio'); + cy.get(`[data-row=0] .slick-cell.l0.r0.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); - cy.get(`[data-row=2] > .slick-cell.l1.r3.rowspan`).should('contain', 'Check Mail'); - cy.get(`[data-row=2] > .slick-cell.l1.r3.rowspan`).should(($el) => + cy.get(`[data-row=2] .slick-cell.l1.r3.rowspan`).should('contain', 'Check Mail'); + cy.get(`[data-row=2] .slick-cell.l1.r3.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); - cy.get(`[data-row=8] > .slick-cell.l6.r8.rowspan`).should('contain', 'Development'); - cy.get(`[data-row=8] > .slick-cell.l6.r8.rowspan`).should(($el) => + cy.get(`[data-row=8] .slick-cell.l6.r8.rowspan`).should('contain', 'Development'); + cy.get(`[data-row=8] .slick-cell.l6.r8.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); }); it('should expect "Lunch Break" to be moved to the left by 1 index less', () => { - cy.get(`[data-row=0] > .slick-cell.l9.r11.rowspan`).should('contain', 'Lunch Break'); - cy.get(`[data-row=0] > .slick-cell.l9.r11.rowspan`).should(($el) => + cy.get(`[data-row=0] .slick-cell.l9.r11.rowspan`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l9.r11.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 10) ); }); it('should expect "Development" to be moved to the left by 1 index less', () => { - cy.get(`[data-row=1] > .slick-cell.l12.r13.rowspan`).should('contain', 'Development'); - cy.get(`[data-row=1] > .slick-cell.l12.r13.rowspan`).should(($el) => + cy.get(`[data-row=1] .slick-cell.l12.r13.rowspan`).should('contain', 'Development'); + cy.get(`[data-row=1] .slick-cell.l12.r13.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5) ); }); @@ -455,32 +469,32 @@ describe('Example 43 - colspan/rowspan - Employees Timesheets', { retries: 0 }, }); it('should expect EmployeeID to follow columns at index 1 column index', () => { - cy.get(`[data-row=0] > .slick-cell.l1.r1.rowspan`).should('contain', 'Davolio'); - cy.get(`[data-row=0] > .slick-cell.l1.r1.rowspan`).should(($el) => + cy.get(`[data-row=0] .slick-cell.l1.r1.rowspan`).should('contain', 'Davolio'); + cy.get(`[data-row=0] .slick-cell.l1.r1.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); - cy.get(`[data-row=2] > .slick-cell.l2.r4.rowspan`).should('contain', 'Check Mail'); - cy.get(`[data-row=2] > .slick-cell.l2.r4.rowspan`).should(($el) => + cy.get(`[data-row=2] .slick-cell.l2.r4.rowspan`).should('contain', 'Check Mail'); + cy.get(`[data-row=2] .slick-cell.l2.r4.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); - cy.get(`[data-row=8] > .slick-cell.l7.r9.rowspan`).should('contain', 'Development'); - cy.get(`[data-row=8] > .slick-cell.l7.r9.rowspan`).should(($el) => + cy.get(`[data-row=8] .slick-cell.l7.r9.rowspan`).should('contain', 'Development'); + cy.get(`[data-row=8] .slick-cell.l7.r9.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); }); it('should expect "Lunch Break" to be moved to the right by 1 index less', () => { - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should(($el) => + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 10) ); }); it('should expect "Development" to be moved to the right by 1 index less and a large "Development" section that spans over multiple columns & rows in the afternoon', () => { - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should(($el) => + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5) ); }); diff --git a/demos/aurelia/test/cypress/e2e/example44.cy.ts b/demos/aurelia/test/cypress/e2e/example44.cy.ts index 5d5bf6f477..451c07efd7 100644 --- a/demos/aurelia/test/cypress/e2e/example44.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example44.cy.ts @@ -30,8 +30,14 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { cy.get('h2').should('contain', 'Example 44: colspan/rowspan with large dataset'); }); + it('should hide sub-title', () => { + cy.get('[data-test=toggle-subtitle]').click(); + }); + it('should calculate a height that fits the wrapped Revenue Growth header', () => { - cy.get('.slick-header-auto-height').should('have.length', 2); + // The pinning POC uses one live header instead of separate left/right + // header panes, so auto-height is applied to a single header element. + cy.get('.slick-header-auto-height').should('have.length', 1); cy.get('.slick-header-auto-height') .first() .should(($header) => { @@ -58,7 +64,7 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { let draggedColumn: HTMLElement; cy.clock(); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo(0, 0).its('0.scrollLeft').should('equal', 0); + cy.get('.slick-horizontal-scroller').scrollTo(0, 0).its('0.scrollLeft').should('equal', 0); cy.get('.slick-header-columns-left').then(($header) => { const sortableProperty = Object.keys($header[0]).find((property) => property.startsWith('Sortable')); @@ -75,12 +81,12 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { }); cy.tick(300); - cy.get('.slick-viewport-top.slick-viewport-left').its('0.scrollLeft').should('be.greaterThan', 0); + cy.get('.slick-horizontal-scroller').its('0.scrollLeft').should('be.greaterThan', 0); cy.then(() => sortInstance.options.onEnd({ item: draggedColumn, stopPropagation: () => {} })); cy.get('.slick-header-column:nth(0)').should('contain', 'Title'); cy.get('.slick-header-column:nth(1)').should('contain', 'Revenue Growth'); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo(0, 0); + cy.get('.slick-vertical-scroller').scrollTo(0, 0); }); it('should drag Title column to swap with 2nd column "Revenue Growth" in the grid and expect rowspan to stay at same position with Task 0 to spread instead', () => { @@ -208,7 +214,7 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { }); it('should scroll to the right and still expect spans without any extra texts', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo(400, 0).wait(10); + cy.get('.slick-horizontal-scroller').scrollTo(400, 0).wait(10); cy.get(`[data-row=3] > .slick-cell:nth(1)`).contains(/\d+$/); cy.get(`[data-row=3] > .slick-cell.l1.r2.rowspan`).should('exist'); @@ -229,7 +235,7 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { }); it('should scroll back to left and expect Task 8 to have 2 different spans (Revenue Grow: rowspan=80, Policy Index: rowspan=2000,colspan=2)', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo(0, 0).wait(10); + cy.get('.slick-vertical-scroller').scrollTo(0, 0).wait(10); cy.get(`[data-row=8] > .slick-cell.l0.r0`).should('contain', 'Task 8'); cy.get(`[data-row=8] > .slick-cell.l1.r1.rowspan`).should(($el) => { @@ -350,6 +356,10 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { }); it('should start at RevenueGrowth column on first dashed cell, then type "Ctrl+End" then "Ctrl+Home" keys and expect active cell to go to bottom/top of grid on same column', () => { + // The preceding span/scroll cases share state. Reset both axes through + // the POC's actual scroll owners before interacting with the first row. + cy.get('.slick-horizontal-scroller').scrollTo(0, 0); + cy.get('.slick-vertical-scroller').scrollTo(0, 0); cy.get('[data-row=0] > .slick-cell.l2.r2').as('active_cell').click(); cy.get('[data-row=0] > .slick-cell.l2.r2.active').should('have.length', 1); cy.get('@active_cell').type('{ctrl}{end}', { release: false }); diff --git a/demos/aurelia/test/cypress/e2e/example45.cy.ts b/demos/aurelia/test/cypress/e2e/example45.cy.ts index 6f56e1a753..b4485b498b 100644 --- a/demos/aurelia/test/cypress/e2e/example45.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example45.cy.ts @@ -224,9 +224,9 @@ describe('Example 45 - Row Detail with inner Grid', () => { }); it('should scroll down when the row detail is just barely visible and then scroll back up and still expect same filters/sorting', () => { - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 350); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 350); cy.wait(50); - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 0); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 0); cy.get(`#innergrid-1 .slick-row[style*="translateY(${GRID_ROW_HEIGHT * 0}px)"] > .slick-cell:nth(0)`).should('contain', '10281'); cy.get(`#innergrid-1 .slick-row[style*="translateY(${GRID_ROW_HEIGHT * 0}px)"] > .slick-cell:nth(1)`).should('contain', 'Madrid'); @@ -236,9 +236,9 @@ describe('Example 45 - Row Detail with inner Grid', () => { it('should scroll down by 2 pages down and then scroll back up and no longer the same filters/sorting', () => { cy.get('#grid45 [data-row="0"] .slick-cell.r2.l2').first().click().type('{pagedown}'); - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 2000); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 2000); cy.wait(50); - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 0); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 0); cy.get(`#innergrid-1 .slick-row[style*="translateY(${GRID_ROW_HEIGHT * 0}px)"] > .slick-cell:nth(0)`).should( 'not.contain', @@ -419,7 +419,7 @@ describe('Example 45 - Row Detail with inner Grid', () => { cy.get('#grid45').type('{pageDown}{pageDown}', { release: false }); cy.wait(50); - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 350); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 350); // expect same grid details for both grids // 2nd row detail @@ -520,9 +520,9 @@ describe('Example 45 - Row Detail with inner Grid', () => { }); it('should scroll far down (out of viewport) and back up and expect inner grid sort/filter state is PRESERVED (keepComponentAlive)', () => { - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 800); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 800); cy.wait(50); - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 0); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 0); // state should be PRESERVED because keepComponentAlive is enabled cy.get(`#innergrid-1 .slick-row[style*="translateY(${GRID_ROW_HEIGHT * 0}px)"] > .slick-cell:nth(0)`).should('contain', '10281'); @@ -532,9 +532,9 @@ describe('Example 45 - Row Detail with inner Grid', () => { }); it('should scroll out of viewport a second time and back up and still expect inner grid sort/filter state is PRESERVED', () => { - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 800); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 800); cy.wait(50); - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 0); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 0); cy.get(`#innergrid-1 .slick-row[style*="translateY(${GRID_ROW_HEIGHT * 0}px)"] > .slick-cell:nth(0)`).should('contain', '10281'); cy.get(`#innergrid-1 .slick-row[style*="translateY(${GRID_ROW_HEIGHT * 0}px)"] > .slick-cell:nth(1)`).should('contain', 'Madrid'); diff --git a/demos/aurelia/test/cypress/e2e/example47.cy.ts b/demos/aurelia/test/cypress/e2e/example47.cy.ts index 936cb3002e..3e5480a883 100644 --- a/demos/aurelia/test/cypress/e2e/example47.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example47.cy.ts @@ -101,7 +101,7 @@ describe('Example 47 - Row Detail View + Grouping', () => { cy.get('.detail-label label').should('contain', 'Assignee:'); cy.get('.detail-label input').should('exist'); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('.dynamic-cell-detail').find('[data-test=delete-btn]').click(); cy.get('.toast.text-bg-danger').contains(/Deleted row with Task [0-9]*/); cy.get('.dynamic-cell-detail').should('have.length', 0); @@ -112,7 +112,7 @@ describe('Example 47 - Row Detail View + Grouping', () => { cy.on('window:alert', stub); let assigneeName = ''; - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('[data-row="1"] > .slick-cell.l1').contains(/Task [0-9]*/); cy.get('[data-row="1"] > .slick-cell.l0').click().wait(40); diff --git a/demos/aurelia/test/cypress/e2e/example48.cy.ts b/demos/aurelia/test/cypress/e2e/example48.cy.ts index 5e7f317681..329aac187e 100644 --- a/demos/aurelia/test/cypress/e2e/example48.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example48.cy.ts @@ -153,7 +153,7 @@ describe('Example 48 - Hybrid Selection Model', () => { }); it('should auto scroll take effect to display the selecting element when dragging', { scrollBehavior: false }, () => { - cy.get('#grid48-1 .slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('#grid48-1 .slick-vertical-scroller').scrollTo('top'); testScroll('#grid48-1', '#grid48-1', 0, 1).then((scrollDistance: { cell: any; row: any }) => { expect(scrollDistance.cell.scrollBefore).to.be.lte(scrollDistance.cell.scrollAfter); @@ -161,11 +161,11 @@ describe('Example 48 - Hybrid Selection Model', () => { }); cy.get('#selectionRange1').contains(/"fromRow":0,"fromCell":1,"toRow":1[45],"toCell":3/); - cy.get('#grid48-1 .slick-viewport-top.slick-viewport-left').scrollTo(0, 13 * 35); + cy.get('#grid48-1 .slick-vertical-scroller').scrollTo(0, 13 * 35); }); it('should toggle multiple cell selection ranges with the checkbox', () => { - cy.get('#grid48-1 .slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('#grid48-1 .slick-vertical-scroller').scrollTo('top'); cy.get('[data-test="enable-multi-selection"]').check(); cy.get('#grid48-1 .slick-row[data-row="1"] .slick-cell.l1.r1').click(); @@ -201,7 +201,7 @@ describe('Example 48 - Hybrid Selection Model', () => { }); it('should preserve row and column offsets when copying multiple cell ranges', () => { - cy.get('#grid48-1 .slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('#grid48-1 .slick-vertical-scroller').scrollTo('top'); cy.get('[data-test="enable-multi-selection"]').should('be.checked'); cy.window().then((win) => { cy.stub(win.navigator.clipboard, 'writeText').as('clipboardWriteText'); @@ -264,7 +264,7 @@ describe('Example 48 - Hybrid Selection Model', () => { }); cy.get('#selectionRange2').contains(/"fromRow":0,"fromCell":0,"toRow":1[0-9],"toCell":7/); - cy.get('#grid48-2 .slick-viewport-top.slick-viewport-left').scrollTo(0, 12 * 35); + cy.get('#grid48-2 .slick-vertical-scroller').scrollTo(0, 12 * 35); }); it('should click on a cell outside of the selected range and expect previous selection to remain', () => { @@ -272,7 +272,7 @@ describe('Example 48 - Hybrid Selection Model', () => { cy.get('@task1x') .contains(/Task 1[0-9]/) .click(); - cy.get('#grid48-2 .slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('#grid48-2 .slick-vertical-scroller').scrollTo('top'); cy.get('#grid48-2 .slick-cell.selected').should('have.length.gte', 60); cy.get('#selectionRange2').contains(/"fromRow":0,"fromCell":0,"toRow":1[0-9],"toCell":7/); }); @@ -284,20 +284,20 @@ describe('Example 48 - Hybrid Selection Model', () => { it('should click on row 4 and 5 row checkbox and expect 5 full rows to be selected', () => { cy.get('#grid48-2 .slick-row[data-row="4"] .slick-cell.l1.r1').should('contain', '4'); cy.get('#grid48-2 .slick-row[data-row="4"] input[type=checkbox]').click({ force: true }); - cy.get('#grid48-2 .slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('#grid48-2 .slick-vertical-scroller').scrollTo('top'); cy.get('#grid48-2 .slick-row[data-row="4"] .slick-cell.l0.r0').should('have.class', 'selected'); cy.get('#grid48-2 .slick-cell.selected').should('have.length', 8 * 1); // select another row cy.get('#grid48-2 .slick-row[data-row="5"] .slick-cell.l1.r1').should('contain', '5'); cy.get('#grid48-2 .slick-row[data-row="5"] input[type=checkbox]').click({ force: true }); - cy.get('#grid48-2 .slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('#grid48-2 .slick-vertical-scroller').scrollTo('top'); cy.get('#grid48-2 .slick-row[data-row="5"] .slick-cell.l0.r0').should('have.class', 'selected'); cy.get('#grid48-2 .slick-cell.selected').should('have.length', 8 * 2); }); it('should toggle multiple row selection ranges with the checkbox', () => { - cy.get('#grid48-2 .slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('#grid48-2 .slick-vertical-scroller').scrollTo('top'); cy.get('#grid48-2 .slick-row[data-row="4"] input[type=checkbox]').uncheck({ force: true }); cy.get('#grid48-2 .slick-row[data-row="5"] input[type=checkbox]').uncheck({ force: true }); cy.get('[data-test="enable-multi-selection"]').should('be.checked'); @@ -321,7 +321,7 @@ describe('Example 48 - Hybrid Selection Model', () => { const secondRange = '{"fromRow":3,"fromCell":0,"toRow":4,"toCell":7}'; const combinedRanges = `${firstRange}${secondRange}`; - cy.get(`${gridSelector} .slick-viewport-top.slick-viewport-left`).scrollTo('top'); + cy.get(`${gridSelector} .slick-vertical-scroller`).scrollTo('top'); cy.get(`${gridSelector} .slick-row[data-row="1"] input[type=checkbox]`).uncheck({ force: true }); cy.get(`${gridSelector} .slick-row[data-row="2"] input[type=checkbox]`).uncheck({ force: true }); cy.get(`${gridSelector} .slick-row[data-row="4"] input[type=checkbox]`).uncheck({ force: true }); diff --git a/demos/aurelia/test/cypress/e2e/example55.cy.ts b/demos/aurelia/test/cypress/e2e/example55.cy.ts index 37ecd02a59..3a8477ecf3 100644 --- a/demos/aurelia/test/cypress/e2e/example55.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example55.cy.ts @@ -38,7 +38,7 @@ describe('Example 55 - Variable Row Height (provider)', { retries: 1 }, () => { it('should keep row 90 aligned at top after clicking scroll button', () => { cy.get('[data-test="scroll-row-90-example55"]').click(); - cy.get('.slick-viewport-top.slick-viewport-left') + cy.get('.slick-vertical-scroller') .invoke('scrollTop') .then((scrollTop) => { expect(Number(scrollTop)).to.be.closeTo(topOf(90), 2); diff --git a/demos/aurelia/test/cypress/e2e/example56.cy.ts b/demos/aurelia/test/cypress/e2e/example56.cy.ts index c02518e284..e30d1b7f00 100644 --- a/demos/aurelia/test/cypress/e2e/example56.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example56.cy.ts @@ -1,6 +1,6 @@ describe('Example 56 - Variable Row Height (item metadata)', { retries: 1 }, () => { const BASE_ROW_HEIGHT = 40; - const FROZEN_ROW_COUNT = 2; + const PINNED_ROW_COUNT = 2; const hDefault = (r: number) => { const cycle = [33, 44, 44, 80]; @@ -18,36 +18,41 @@ describe('Example 56 - Variable Row Height (item metadata)', { retries: 1 }, () return t; }; - const frozenTopHeight = (hOf: (row: number) => number) => topOf(FROZEN_ROW_COUNT, hOf); + const pinnedTopHeight = (hOf: (row: number) => number) => topOf(PINNED_ROW_COUNT, hOf); const relativeTopInCanvas = (r: number, hOf: (row: number) => number) => { - if (r < FROZEN_ROW_COUNT) { - return topOf(r, hOf); - } - return topOf(r, hOf) - frozenTopHeight(hOf); + // Pinned rows are moved into the overlay, but center rows retain their + // natural document coordinates behind that overlay. + return topOf(r, hOf); }; - const canvasSelector = (r: number) => (r < FROZEN_ROW_COUNT ? '.grid-canvas-top' : '.grid-canvas-bottom'); + const rowHostSelector = (r: number) => (r < PINNED_ROW_COUNT ? '.slick-docking-overlay' : '.grid-canvas-top'); const assertRowStyle = (row: number, hOf: (row: number) => number) => { const expectedHeight = hOf(row); const expectedTop = relativeTopInCanvas(row, hOf); - cy.get(`${canvasSelector(row)} .slick-row[data-row=${row}]`) + cy.get(`${rowHostSelector(row)} .slick-row[data-row=${row}]`) .should('have.attr', 'style') .and('contain', `transform: translateY(${expectedTop}px)`) .then((style) => { if (expectedHeight !== BASE_ROW_HEIGHT) { expect(style).to.contain(`height: ${expectedHeight}px`); } else { - expect(style).not.to.contain('height:'); + // Docked rows carry their resolved height inline so editor/content + // styles cannot collapse the pinned row. The base-height case is + // therefore valid with either the stylesheet fallback or an + // explicit `height: 40px` declaration. + expect(style).to.match(/(?:height: 40px;|^(?!.*height:))/); } }); - cy.get(`[data-row="${row}"] > .slick-cell:nth(3)`).should('contain', `${expectedHeight}px`); + // Rows are split into left/center/right docking regions, so cells are + // nested under their region wrapper rather than being direct row children. + cy.get(`[data-row="${row}"] .slick-cell:nth(3)`).should('contain', `${expectedHeight}px`); }; const ensureDefaultDensity = () => { - cy.get('.grid-canvas-top .slick-row[data-row=1]') + cy.get('.slick-docking-overlay .slick-row[data-row=1]') .invoke('attr', 'style') .then((style) => { if ((style ?? '').includes('height: 50px')) { @@ -55,7 +60,7 @@ describe('Example 56 - Variable Row Height (item metadata)', { retries: 1 }, () } }); - cy.get('.grid-canvas-top .slick-row[data-row=1]').should('have.attr', 'style').and('contain', 'height: 44px'); + cy.get('.slick-docking-overlay .slick-row[data-row=1]').should('have.attr', 'style').and('contain', 'height: 44px'); }; beforeEach(() => { @@ -67,7 +72,7 @@ describe('Example 56 - Variable Row Height (item metadata)', { retries: 1 }, () cy.get('h2').should('contain', 'Example 56: Variable Row Height (item metadata)'); }); - it('should render frozen and scrollable rows with expected transform and row heights from metadata fallback', () => { + it('should render pinned and scrollable rows with expected transform and row heights from metadata fallback', () => { for (const r of [0, 1, 2, 3, 4, 5, 6]) { assertRowStyle(r, hDefault); } @@ -85,12 +90,12 @@ describe('Example 56 - Variable Row Height (item metadata)', { retries: 1 }, () } }); - it('should scroll row 90 to top of scrollable pane with frozen top rows', () => { - const expectedScrollTop = topOf(90, hDefault) - frozenTopHeight(hDefault); + it('should scroll row 90 to top of scrollable pane with pinned top rows', () => { + const expectedScrollTop = topOf(90, hDefault) - pinnedTopHeight(hDefault); cy.get('[data-test="scroll-row-90-example56"]').click(); - cy.get('.slick-viewport-bottom.slick-viewport-left').should(($viewport) => { + cy.get('.slick-vertical-scroller').should(($viewport) => { expect($viewport.scrollTop()).to.be.closeTo(expectedScrollTop, 2); }); diff --git a/demos/aurelia/test/cypress/e2e/example57.cy.ts b/demos/aurelia/test/cypress/e2e/example57.cy.ts index df73fe98f8..7055d540b1 100644 --- a/demos/aurelia/test/cypress/e2e/example57.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example57.cy.ts @@ -54,14 +54,14 @@ describe('Example 57 - RTL (Right-to-Left)', () => { describe('Scrolling Behavior', () => { it('should have horizontal scroll enabled', () => { - cy.get('#grid57 .slick-viewport').then(($viewport) => { + cy.get('#grid57 .slick-horizontal-scroller').then(($viewport) => { const viewport = $viewport[0] as HTMLElement; expect(viewport.scrollWidth).to.be.greaterThan(viewport.clientWidth); }); }); it('should update visible header columns when scrolling', () => { - cy.get('#grid57 .slick-viewport').then(($viewport) => { + cy.get('#grid57 .slick-horizontal-scroller').then(($viewport) => { const viewport = $viewport[0] as HTMLElement; const maxScroll = viewport.scrollWidth - viewport.clientWidth; viewport.scrollLeft = maxScroll; @@ -72,7 +72,7 @@ describe('Example 57 - RTL (Right-to-Left)', () => { cy.wait(150); - cy.get('#grid57 .slick-viewport').then(($viewport) => { + cy.get('#grid57 .slick-horizontal-scroller').then(($viewport) => { const viewport = $viewport[0] as HTMLElement; expect(Math.abs(viewport.scrollLeft)).to.be.greaterThan(0); }); diff --git a/demos/aurelia/test/cypress/support/commands.ts b/demos/aurelia/test/cypress/support/commands.ts index 3be5a3685b..1f58e699f1 100644 --- a/demos/aurelia/test/cypress/support/commands.ts +++ b/demos/aurelia/test/cypress/support/commands.ts @@ -32,19 +32,19 @@ declare global { namespace Cypress { interface Chainable { // triggerHover: (elements: NodeListOf) => void; - convertPosition(viewport: string): Chainable | { x: string; y: string }>; + convertPosition(viewport: string): Chainable<{ x: string; y: string }>; getCell( row: number, col: number, viewport?: string, options?: { parentSelector?: string; rowHeight?: number } - ): Chainable>; + ): Chainable>; getNthCell( row: number, nthCol: number, viewport?: string, options?: { parentSelector?: string; rowHeight?: number } - ): Chainable>; + ): Chainable>; saveLocalStorage: () => void; restoreLocalStorage: () => void; getTransformValue(cssTransformMatrix: string, absoluteValue: boolean, transformType?: 'rotate' | 'scale'): Chainable; diff --git a/demos/aurelia/test/cypress/support/drag.ts b/demos/aurelia/test/cypress/support/drag.ts index 8228176a5a..b26012a988 100644 --- a/demos/aurelia/test/cypress/support/drag.ts +++ b/demos/aurelia/test/cypress/support/drag.ts @@ -82,15 +82,22 @@ export function getScrollDistanceWhenDragOutsideGrid( return (cy as any).convertPosition(viewport).then((_viewportPosition: { x: number; y: number }) => { const viewportSelector = `${selector} .slick-viewport-${_viewportPosition.x}.slick-viewport-${_viewportPosition.y}`; (cy as any).getNthCell(fromRow, fromCol, viewport, { parentSelector: selector }).dragStart(); - return cy.get(viewportSelector).then(($viewport) => { - const scrollTopBefore = $viewport.scrollTop(); - const scrollLeftBefore = $viewport.scrollLeft(); + return cy.get(selector).then(($grid) => { + const viewport = ($grid.find(viewportSelector)[0] || $grid.find('.slick-vertical-scroller')[0]) as HTMLElement; + const horizontalScroller = $grid.find('.slick-horizontal-scroller')[0] as HTMLElement | undefined; + const horizontalOwner = horizontalScroller || viewport; + const scrollTopBefore = viewport.scrollTop; + const scrollLeftBefore = horizontalOwner.scrollLeft; cy.dragOutside(dragDirection, 300, px, { parentSelector: selector }); - return cy.get(viewportSelector).then(($viewportAfter) => { + return cy.get(selector).then(($gridAfter) => { + const viewportAfter = ($gridAfter.find(viewportSelector)[0] || $gridAfter.find('.slick-vertical-scroller')[0]) as HTMLElement; + const horizontalScrollerAfter = $gridAfter.find('.slick-horizontal-scroller')[0] as HTMLElement | undefined; + const horizontalOwnerAfter = horizontalScrollerAfter || viewportAfter; cy.dragEnd(selector); - const scrollTopAfter = $viewportAfter.scrollTop(); - const scrollLeftAfter = $viewportAfter.scrollLeft(); - cy.get(viewportSelector).scrollTo(0, 0, { ensureScrollable: false }); + const scrollTopAfter = viewportAfter.scrollTop; + const scrollLeftAfter = horizontalOwnerAfter.scrollLeft; + horizontalOwnerAfter.scrollLeft = 0; + viewportAfter.scrollTop = 0; return cy.wrap({ scrollTopBefore, scrollLeftBefore, diff --git a/demos/react-fluent/src/examples/slickgrid/Example05.tsx b/demos/react-fluent/src/examples/slickgrid/Example05.tsx index e5c9206e1d..127472e9c3 100644 --- a/demos/react-fluent/src/examples/slickgrid/Example05.tsx +++ b/demos/react-fluent/src/examples/slickgrid/Example05.tsx @@ -22,9 +22,9 @@ const Example05: React.FC = () => { const [dataset] = useState(getData()); const [gridOptions, setGridOptions] = useState(undefined); const [darkModeGrid, setDarkModeGrid] = useState(false); - const [frozenColumnCount, setFrozenColumnCount] = useState(2); - const [frozenRowCount, setFrozenRowCount] = useState(3); - const [isFrozenBottom, setIsFrozenBottom] = useState(false); + const [pinnedColumnCount, setPinnedColumnCount] = useState(2); + const [pinnedRowCount, setPinnedRowCount] = useState(3); + const [isPinnedBottom, setIsPinnedBottom] = useState(false); const reactGridRef = useRef(null); const slickEventHandler = new SlickEventHandler(); @@ -41,8 +41,7 @@ const Example05: React.FC = () => { function reactGridReady(reactGrid: SlickgridReactInstance) { reactGridRef.current = reactGrid; - // with frozen (pinned) grid, in order to see the entire row being highlighted when hovering - // we need to do some extra tricks (that is because frozen grids use 2 separate div containers) + // With a pinned grid, the entire row is highlighted through its shared row regions. // the trick is to use row selection to highlight when hovering current row and remove selection once we're not slickEventHandler.subscribe(reactGridRef.current?.slickGrid.onMouseEnter, (event) => highlightRow(event, true)); slickEventHandler.subscribe(reactGridRef.current?.slickGrid.onMouseLeave, (event) => highlightRow(event, false)); @@ -253,13 +252,11 @@ const Example05: React.FC = () => { editable: true, autoEdit: true, enableExcelCopyBuffer: true, - frozenColumn: 2, - frozenRow: 3, - // frozenBottom: true, // if you want to freeze the bottom instead of the top, you can enable this property + pinning: { columns: { left: 2 }, rows: { top: [0, 1, 2] } }, - // show both Frozen Columns in HeaderMenu & GridMenu, these are opt-in commands so they're disabled by default - gridMenu: { hideClearFrozenColumnsCommand: false }, - headerMenu: { hideFreezeColumnsCommand: false }, + // show both single-column and bulk pinning commands in HeaderMenu & GridMenu; these are opt-in commands + gridMenu: { hideClearPinningCommand: false }, + headerMenu: { hidePinColumnCommand: false, hidePinningColumnsCommand: false }, ...baseFluentGridOption, }; @@ -290,26 +287,26 @@ const Example05: React.FC = () => { } /** change dynamically, through slickgrid "setOptions()" the number of pinned columns */ - function changeFrozenColumnCount(e: React.FormEvent) { - const frozenColumn = +((e.target as HTMLInputElement)?.value ?? 0); - setFrozenColumnCount(frozenColumn); + function changePinnedColumnCount(e: React.FormEvent) { + const pinnedColumn = +((e.target as HTMLInputElement)?.value ?? 0); + setPinnedColumnCount(pinnedColumn); } - function updateFrozenColumnCount() { + function updatePinnedColumnCount() { reactGridRef.current?.slickGrid?.setOptions({ - frozenColumn: frozenColumnCount, + pinning: { columns: { left: pinnedColumnCount } }, }); } /** change dynamically, through slickgrid "setOptions()" the number of pinned rows */ - function changeFrozenRowCount(e: React.FormEvent) { - const frozenRow = +((e.target as HTMLInputElement)?.value ?? 0); - setFrozenRowCount(frozenRow); + function changePinnedRowCount(e: React.FormEvent) { + const pinnedRow = +((e.target as HTMLInputElement)?.value ?? 0); + setPinnedRowCount(pinnedRow); } - function updateFrozenRowCount() { + function updatePinnedRowCount() { reactGridRef.current?.slickGrid?.setOptions({ - frozenRow: frozenRowCount, + pinning: { rows: { top: Array.from({ length: Math.max(0, pinnedRowCount) }, (_value, index) => index) } }, }); } @@ -330,8 +327,8 @@ const Example05: React.FC = () => { showToast(args.validationResults.msg, 'danger'); } - function setFrozenColumns(frozenCols: number) { - reactGridRef.current?.slickGrid.setOptions({ frozenColumn: frozenCols }); + function setPinnedColumns(pinnedCols: number) { + reactGridRef.current?.slickGrid.setOptions({ pinning: { columns: { left: pinnedCols } } }); const updatedGridOptions = reactGridRef.current?.slickGrid.getOptions(); setGridOptions(updatedGridOptions); } @@ -348,20 +345,20 @@ const Example05: React.FC = () => { } /** toggle dynamically, through slickgrid "setOptions()" the top/bottom pinned location */ - function toggleFrozenBottomRows() { + function togglePinnedBottomRows() { reactGridRef.current?.slickGrid.setOptions({ - frozenBottom: !isFrozenBottom, + pinning: { rows: { top: isPinnedBottom ? [0, 1, 2] : [], bottom: isPinnedBottom ? [] : [0, 1, 2] } }, }); - const newIsFrozenBottom = !isFrozenBottom; - setIsFrozenBottom(newIsFrozenBottom); + const newIsPinnedBottom = !isPinnedBottom; + setIsPinnedBottom(newIsPinnedBottom); } return !gridOptions ? ( '' ) : (
-

Example 5: Pinned (frozen) Columns/Rows

+

Example 5: Pinned Columns/Rows

@@ -370,10 +367,10 @@ const Example05: React.FC = () => { changeFrozenRowCount($event)} + defaultValue={pinnedRowCount} + onInput={($event) => changePinnedRowCount($event)} /> - @@ -382,10 +379,10 @@ const Example05: React.FC = () => { changeFrozenColumnCount($event)} + defaultValue={pinnedColumnCount} + onInput={($event) => changePinnedColumnCount($event)} /> - @@ -398,17 +395,17 @@ const Example05: React.FC = () => { Toggle Dark Mode - - - - : {isFrozenBottom ? 'Bottom' : 'Top'} + : {isPinnedBottom ? 'Bottom' : 'Top'}
diff --git a/demos/react-fluent/src/examples/slickgrid/base-fluent-grid-options.ts b/demos/react-fluent/src/examples/slickgrid/base-fluent-grid-options.ts index b6503efedb..e521dcf8de 100644 --- a/demos/react-fluent/src/examples/slickgrid/base-fluent-grid-options.ts +++ b/demos/react-fluent/src/examples/slickgrid/base-fluent-grid-options.ts @@ -16,7 +16,7 @@ export const baseFluentGridOption: GridOption = { iconCssClass: 'fi fi-navigation', iconClearAllFiltersCommand: 'fi fi-filter-dismiss', iconClearAllSortingCommand: 'fi fi-arrow-sort', - iconClearFrozenColumnsCommand: 'fi fi-pin-off', + iconClearPinningCommand: 'fi fi-pin-off', iconExportCsvCommand: 'fi fi-arrow-download', iconExportExcelCommand: 'fi fi-arrow-download', iconExportPdfCommand: 'fi fi-arrow-download', @@ -31,8 +31,8 @@ export const baseFluentGridOption: GridOption = { iconClearFilterCommand: 'fi fi-filter-dismiss', iconClearSortCommand: 'fi fi-arrow-sort', iconFilterShortcutSubMenu: 'fi fi-filter', - iconFreezeColumns: 'fi fi-pin', - iconUnfreezeColumns: 'fi fi-pin-off', + iconPinningColumns: 'fi fi-pin', + iconUnpinningColumns: 'fi fi-pin-off', iconSortAscCommand: 'fi fi-sort-arrow-up', iconSortDescCommand: 'fi fi-sort-arrow-down', iconColumnHideCommand: 'fi fi-dismiss', diff --git a/demos/react-fluent/src/examples/slickgrid/example05.scss b/demos/react-fluent/src/examples/slickgrid/example05.scss index 6e31054384..e41265a0e7 100644 --- a/demos/react-fluent/src/examples/slickgrid/example05.scss +++ b/demos/react-fluent/src/examples/slickgrid/example05.scss @@ -1,8 +1,8 @@ -/** You can change the pinned/frozen border styling through this css override */ +/** You can change the pinned/pinned border styling through this css override */ -.slick-row .slick-cell.frozen:last-child, -.slick-headerrow-column.frozen:last-child, -.slick-footerrow-column.frozen:last-child { +.slick-row .slick-cell.pinned:last-child, +.slick-headerrow-column.pinned:last-child, +.slick-footerrow-column.pinned:last-child { border-right: 1px solid #969696 !important; } diff --git a/demos/react/src/assets/locales/en/translation.json b/demos/react/src/assets/locales/en/translation.json index b412eea6d6..600e68cdc1 100644 --- a/demos/react/src/assets/locales/en/translation.json +++ b/demos/react/src/assets/locales/en/translation.json @@ -7,7 +7,7 @@ "CLEAR_ALL_FILTERS": "Clear all Filters", "CLEAR_ALL_GROUPING": "Clear all Grouping", "CLEAR_ALL_SORTING": "Clear all Sorting", - "CLEAR_PINNING": "Unfreeze Columns/Rows", + "CLEAR_PINNING": "Unpin Columns/Rows", "CLONE": "Clone", "COLLAPSE_ALL_GROUPS": "Collapse all Groups", "COLUMNS": "Columns", @@ -28,7 +28,10 @@ "FILTER_SHORTCUTS": "Filter Shortcuts", "FROM_TO_OF_TOTAL_ITEMS": "{{from}}-{{to}} of {{totalItems}} items", "FORCE_FIT_COLUMNS": "Force fit columns", - "FREEZE_COLUMNS": "Freeze Columns", + "PIN_COLUMN": "Column Pinning", + "PIN_COLUMNS": "Pin Through Here", + "PIN_LEFT": "Pin Left", + "PIN_RIGHT": "Pin Right", "INVALID_FLOAT": "The number must be valid and have a maximum of {{maxDecimal}} decimals.", "GREATER_THAN": "Greater than", "GREATER_THAN_OR_EQUAL_TO": "Greater than or equal to", @@ -63,7 +66,8 @@ "SYNCHRONOUS_RESIZE": "Synchronous resize", "TOGGLE_FILTER_ROW": "Toggle Filter Row", "TOGGLE_PRE_HEADER_ROW": "Toggle Pre-Header Row", - "UNFREEZE_COLUMNS": "Unfreeze Columns", + "UNPIN_COLUMN": "Unpin Column", + "UNPIN_COLUMNS": "Unpin All Columns", "X_OF_Y_SELECTED": "# of % selected", "X_OF_Y_MASS_SELECTED": "{{x}} of {{y}} selected", "BILLING": { diff --git a/demos/react/src/assets/locales/fr/translation.json b/demos/react/src/assets/locales/fr/translation.json index c957bb46b8..832f0207b7 100644 --- a/demos/react/src/assets/locales/fr/translation.json +++ b/demos/react/src/assets/locales/fr/translation.json @@ -7,7 +7,7 @@ "CLEAR_ALL_FILTERS": "Supprimer tous les filtres", "CLEAR_ALL_GROUPING": "Supprimer tous les groupes", "CLEAR_ALL_SORTING": "Supprimer tous les tris", - "CLEAR_PINNING": "Dégeler les colonnes/rangées", + "CLEAR_PINNING": "Désépingler les colonnes/rangées", "CLONE": "Cloner", "COLLAPSE_ALL_GROUPS": "Réduire tous les groupes", "COLUMNS": "Colonnes", @@ -28,7 +28,10 @@ "FILTER_SHORTCUTS": "Raccourcis de filtre", "FROM_TO_OF_TOTAL_ITEMS": "{{from}}-{{to}} de {{totalItems}} éléments", "FORCE_FIT_COLUMNS": "Ajustement forcé des colonnes", - "FREEZE_COLUMNS": "Geler les colonnes", + "PIN_COLUMN": "Épinglage de colonnes", + "PIN_COLUMNS": "Épingler jusqu'ici", + "PIN_LEFT": "Épingler à gauche", + "PIN_RIGHT": "Épingler à droite", "GREATER_THAN": "Plus grand que", "GREATER_THAN_OR_EQUAL_TO": "Plus grand ou égal à", "GROUP_BY": "Grouper par", @@ -63,7 +66,8 @@ "SYNCHRONOUS_RESIZE": "Redimension synchrone", "TOGGLE_FILTER_ROW": "Basculer la ligne des filtres", "TOGGLE_PRE_HEADER_ROW": "Basculer la ligne de pré-en-tête", - "UNFREEZE_COLUMNS": "Dégeler les colonnes", + "UNPIN_COLUMN": "Désépingler la colonne", + "UNPIN_COLUMNS": "Désépingler toutes les colonnes", "X_OF_Y_SELECTED": "# de % sélectionnés", "X_OF_Y_MASS_SELECTED": "{{x}} de {{y}} sélectionnés", "BILLING": { diff --git a/demos/react/src/examples/slickgrid/Example07.tsx b/demos/react/src/examples/slickgrid/Example07.tsx index 96db83d625..71a44a2f5d 100644 --- a/demos/react/src/examples/slickgrid/Example07.tsx +++ b/demos/react/src/examples/slickgrid/Example07.tsx @@ -58,8 +58,6 @@ const Example7: React.FC = () => { ...gridOptions1, enableHeaderMenu: true, enableFiltering: true, - // frozenColumn: 2, - // frozenRow: 2, headerButton: { onCommand: (_e, args) => handleOnCommand(_e, args, 2), }, diff --git a/demos/react/src/examples/slickgrid/Example14.tsx b/demos/react/src/examples/slickgrid/Example14.tsx index b0c88d91de..0b0ba6a73f 100644 --- a/demos/react/src/examples/slickgrid/Example14.tsx +++ b/demos/react/src/examples/slickgrid/Example14.tsx @@ -49,6 +49,7 @@ const Example14: React.FC = () => { createPreHeaderPanel: true, showPreHeaderPanel: true, preHeaderPanelHeight: 28, + rowHeight: 33, gridHeight: 275, gridWidth: 800, enableExcelExport: true, @@ -101,14 +102,15 @@ const Example14: React.FC = () => { explicitInitialization: true, gridHeight: 275, gridWidth: 800, - frozenColumn: 2, + rowHeight: 33, + pinning: { columns: { left: 2 } }, enableExcelExport: true, excelExportOptions: { exportWithFormatter: false, }, externalResources: [new ExcelExportService(), new PdfExportService()], - gridMenu: { hideClearFrozenColumnsCommand: false }, - headerMenu: { hideFreezeColumnsCommand: false }, + gridMenu: { hideClearPinningCommand: false }, + headerMenu: { hidePinColumnCommand: false, hidePinningColumnsCommand: false }, }; setColumns2(columns2); @@ -133,8 +135,8 @@ const Example14: React.FC = () => { return mockDataset; } - function setFrozenColumns2(frozenCols: number) { - reactGridRef2.current?.slickGrid.setOptions({ frozenColumn: frozenCols }); + function setPinnedColumns2(pinnedCols: number) { + reactGridRef2.current?.slickGrid.setOptions({ pinning: { columns: { left: pinnedCols } } }); const updatedGridOptions = reactGridRef2.current?.slickGrid.getOptions(); setGridOptions2(updatedGridOptions); } @@ -230,23 +232,23 @@ const Example14: React.FC = () => {

- Grid 2 (with Header Grouping & Frozen/Pinned Columns) + Grid 2 (with Header Grouping & Pinned Columns)

diff --git a/demos/react/src/examples/slickgrid/Example15.tsx b/demos/react/src/examples/slickgrid/Example15.tsx index 82c2889f62..c9264c6a16 100644 --- a/demos/react/src/examples/slickgrid/Example15.tsx +++ b/demos/react/src/examples/slickgrid/Example15.tsx @@ -72,10 +72,11 @@ const Example15: React.FC = () => { }, gridMenu: { hideForceFitButton: true, - hideClearFrozenColumnsCommand: false, + hideClearPinningCommand: false, }, headerMenu: { - hideFreezeColumnsCommand: false, + hidePinColumnCommand: false, + hidePinningColumnsCommand: false, }, enablePagination: true, pagination: { diff --git a/demos/react/src/examples/slickgrid/Example20.tsx b/demos/react/src/examples/slickgrid/Example20.tsx index b785c6bd6a..8ad3093fc8 100644 --- a/demos/react/src/examples/slickgrid/Example20.tsx +++ b/demos/react/src/examples/slickgrid/Example20.tsx @@ -1,10 +1,10 @@ +import { ExcelExportService } from '@slickgrid-universal/excel-export'; import React, { useEffect, useRef, useState } from 'react'; import { Editors, Filters, formatNumber, Formatters, - SlickEventHandler, SlickgridReact, type Column, type ColumnEditorDualInput, @@ -18,54 +18,31 @@ const Example20: React.FC = () => { const [columns, setColumns] = useState([]); const [dataset] = useState(getData()); const [gridOptions, setGridOptions] = useState(undefined); - const [frozenColumnCount, setFrozenColumnCount] = useState(2); - const [frozenRowCount, setFrozenRowCount] = useState(3); - const [isFrozenBottom, setIsFrozenBottom] = useState(false); + const [pinnedColumnCount, setPinnedColumnCount] = useState(2); + const [pinnedRowCount, setPinnedRowCount] = useState(3); + const [pinnedRightColumnCount, setPinnedRightColumnCount] = useState(1); + const [isPinnedBottom, setIsPinnedBottom] = useState(false); const [hideSubTitle, setHideSubTitle] = useState(false); + const [isSelectAllShownAsColumnTitle, setIsSelectAllShownAsColumnTitle] = useState(false); + const checkboxSelectorRef = useRef(null); + const pinnedColumnCountRef = useRef(2); + const pinnedRowCountInputRef = useRef(null); + const pinnedRightColumnInputRef = useRef(null); + const pinnedRightColumnCountRef = useRef(1); const reactGridRef = useRef(null); - const slickEventHandler = new SlickEventHandler(); useEffect(() => { defineGrid(); - - // when unmounting - return () => { - slickEventHandler.unsubscribeAll(); - }; }, []); function reactGridReady(reactGrid: SlickgridReactInstance) { reactGridRef.current = reactGrid; - - // with frozen (pinned) grid, in order to see the entire row being highlighted when hovering - // we need to do some extra tricks (that is because frozen grids use 2 separate div containers) - // the trick is to use row selection to highlight when hovering current row and remove selection once we're not - slickEventHandler.subscribe(reactGridRef.current?.slickGrid.onMouseEnter, (event) => highlightRow(event, true)); - slickEventHandler.subscribe(reactGridRef.current?.slickGrid.onMouseLeave, (event) => highlightRow(event, false)); - } - - function highlightRow(event: any, isMouseEnter: boolean) { - const cell = reactGridRef.current?.slickGrid.getCellFromEvent(event); - const rows = isMouseEnter ? [cell?.row ?? 0] : []; - reactGridRef.current?.slickGrid.setSelectedRows(rows); // highlight current row - event.preventDefault(); } /* Define grid Options and Columns */ function defineGrid() { const columns: Column[] = [ - { - id: 'sel', - name: '#', - field: 'id', - minWidth: 40, - width: 40, - maxWidth: 40, - cannotTriggerInsert: true, - resizable: false, - unselectable: true, - }, { id: 'title', name: 'Title', @@ -82,7 +59,6 @@ const Example20: React.FC = () => { resizable: false, minWidth: 130, width: 140, - formatter: Formatters.percentCompleteBar, type: 'number', filterable: true, filter: { model: Filters.slider, operator: '>=' }, @@ -92,29 +68,44 @@ const Example20: React.FC = () => { id: 'start', name: 'Start', field: 'start', - minWidth: 100, - width: 120, + type: 'dateIso', filterable: true, sortable: true, formatter: Formatters.dateIso, + filter: { model: Filters.compoundDate }, }, { id: 'finish', name: 'Finish', field: 'finish', - minWidth: 100, - width: 120, + type: 'dateIso', filterable: true, sortable: true, formatter: Formatters.dateIso, + filter: { model: Filters.compoundDate }, + }, + { + id: 'completed', + name: 'Completed', + field: 'completed', + sortable: true, + filterable: true, + formatter: Formatters.checkmarkMaterial, + editor: { model: Editors.checkbox }, + filter: { + model: Filters.singleSelect, + collection: [ + { value: '', label: '' }, + { value: true, label: 'True' }, + { value: false, label: 'False' }, + ], + }, }, { id: 'cost', name: 'Cost | Duration', field: 'cost', formatter: costDurationFormatter, - minWidth: 150, - width: 170, sortable: true, // filterable: true, filter: { @@ -184,58 +175,37 @@ const Example20: React.FC = () => { }, }, { - id: 'effortDriven', - name: 'Effort Driven', - field: 'effortDriven', + id: 'cityOfOrigin', + name: 'City of Origin', + field: 'cityOfOrigin', minWidth: 100, - width: 120, - formatter: Formatters.checkmarkMaterial, filterable: true, - filter: { - collection: [ - { value: '', label: '' }, - { value: true, label: 'True' }, - { value: false, label: 'False' }, - ], - model: Filters.singleSelect, - }, sortable: true, }, { - id: 'title1', - name: 'Title 1', - field: 'title1', - minWidth: 100, - width: 120, - filterable: true, - sortable: true, - }, - { - id: 'title2', - name: 'Title 2', - field: 'title2', - minWidth: 100, - width: 120, - filterable: true, - sortable: true, - }, - { - id: 'title3', - name: 'Title 3', - field: 'title3', - minWidth: 100, - width: 120, - filterable: true, - sortable: true, - }, - { - id: 'title4', - name: 'Title 4', - field: 'title4', - minWidth: 100, - width: 120, - filterable: true, - sortable: true, + id: 'action', + name: 'Action', + field: 'action', + width: 100, + maxWidth: 100, + excludeFromExport: true, + formatter: () => '
Action
', + cellMenu: { + commandTitle: 'Commands', + commandItems: [ + { command: 'command1', title: 'Command 1' }, + { command: 'command2', title: 'Command 2', itemUsabilityOverride: (args: any) => !args.dataContext.completed }, + { command: 'delete-row', title: 'Delete Row', itemVisibilityOverride: (args: any) => !args.dataContext.completed }, + { divider: true, command: '' }, + { command: 'help', title: 'Help' }, + { command: 'something', title: 'Disabled Command', disabled: true }, + ], + optionTitle: 'Change Complete Flag', + optionItems: [ + { option: true, title: 'True' }, + { option: false, title: 'False' }, + ], + }, }, ]; @@ -244,18 +214,49 @@ const Example20: React.FC = () => { container: '#demo-container', rightPadding: 10, }, - gridWidth: 920, + // Keep the left-pinned columns compact so two right-pinned columns fit + // beside the framework demo's route sidebar. + autoFitColumnsOnFirstLoad: false, enableCellNavigation: true, editable: true, autoEdit: true, enableExcelCopyBuffer: true, - frozenColumn: 2, - frozenRow: 3, - // frozenBottom: true, // if you want to freeze the bottom instead of the top, you can enable this property - - // show both Frozen Columns in HeaderMenu & GridMenu, these are opt-in commands so they're disabled by default - gridMenu: { hideClearFrozenColumnsCommand: false }, - headerMenu: { hideFreezeColumnsCommand: false }, + enableExcelExport: true, + externalResources: [new ExcelExportService()], + enableFiltering: true, + enableSelection: true, + enableCheckboxSelector: true, + selectionOptions: { selectActiveRow: false }, + checkboxSelector: { + hideInColumnTitleRow: !isSelectAllShownAsColumnTitle, + hideInFilterHeaderRow: isSelectAllShownAsColumnTitle, + name: 'Sel', + onExtensionRegistered: (instance: any) => (checkboxSelectorRef.current = instance), + }, + pinning: { columns: { left: ['_checkbox_selector', 'title', 'percentComplete'], right: ['action'] }, rows: { top: [0, 1, 2] } }, + // pinnedBottom: true, // if you want to pin the bottom instead of the top, you can enable this property + + // show both single-column and bulk pinning commands in HeaderMenu & GridMenu; these are opt-in commands + gridMenu: { + hideClearPinningCommand: false, + // Grid Menu visibility updates rebuild the column layout. Reassert the + // demo's current ID-based pinning after that rebuild so React's right + // pinning state cannot briefly fall back to the scrolling band. + onColumnsChanged: () => reapplyPinnedColumns(), + }, + headerMenu: { hidePinColumnCommand: false, hidePinningColumnsCommand: false }, + enableCellMenu: true, + cellMenu: { + onCommand: (_e: unknown, args: any) => executeCommand(args), + onOptionSelected: (_e: unknown, args: any) => { + if (args?.dataContext) { + args.dataContext.completed = args.item.option; + reactGridRef.current?.gridService?.updateItem(args.dataContext); + } + }, + }, + enableContextMenu: true, + contextMenu: getContextMenuOptions(), }; setColumns(columns); @@ -274,38 +275,114 @@ const Example20: React.FC = () => { percentComplete: Math.round(Math.random() * 100), start: new Date(2009, 0, 1), finish: new Date(2009, 4, 5), - effortDriven: i % 5 === 0, - title1: `Some Text ${Math.round(Math.random() * 25)}`, - title2: `Some Text ${Math.round(Math.random() * 25)}`, - title3: `Some Text ${Math.round(Math.random() * 25)}`, - title4: `Some Text ${Math.round(Math.random() * 25)}`, + completed: i % 5 === 0, + cityOfOrigin: i % 2 ? 'Vancouver, BC, Canada' : 'Boston, MA, United States', }; } return mockDataset; } /** change dynamically, through slickgrid "setOptions()" the number of pinned columns */ - function changeFrozenColumnCount(e: React.FormEvent) { - const frozenColumn = +((e.target as HTMLInputElement)?.value ?? 0); - setFrozenColumnCount(frozenColumn); + function changePinnedColumnCount(e: React.FormEvent) { + const value = +(e.currentTarget.value || 0); + setPinnedColumnCount(value); } - function updateFrozenColumnCount() { - reactGridRef.current?.slickGrid?.setOptions({ - frozenColumn: frozenColumnCount, - }); + function updatePinnedColumnCount() { + setPinnedColumns(pinnedColumnCount, pinnedRightColumnCount); } /** change dynamically, through slickgrid "setOptions()" the number of pinned rows */ - function changeFrozenRowCount(e: React.FormEvent) { - const frozenRow = +((e.target as HTMLInputElement)?.value ?? 0); - setFrozenRowCount(frozenRow); + function updatePinnedRowCount() { + const inputValue = pinnedRowCountInputRef.current?.value; + const nextPinnedRowCount = Math.max(0, Number(inputValue ?? pinnedRowCount) || 0); + const rows = Array.from({ length: nextPinnedRowCount }, (_v, i) => i); + + const slickGrid = reactGridRef.current?.slickGrid; + slickGrid?.setOptions({ + pinning: { rows: isPinnedBottom ? { top: [], bottom: rows } : { top: rows, bottom: [] } }, + }); + + setPinnedRowCount(nextPinnedRowCount); } - function updateFrozenRowCount() { - reactGridRef.current?.slickGrid?.setOptions({ - frozenRow: frozenRowCount, - }); + function getContextMenuOptions(): any { + const percentItems = [ + { option: 0, title: 'Not Started (0%)' }, + { option: 50, title: 'Half Completed (50%)' }, + { option: 100, title: 'Completed (100%)' }, + ]; + return { + optionShownOverColumnIds: ['percentComplete'], + hideCloseButton: true, + dropSide: 'right', + optionTitle: 'Change Percent Complete', + optionItems: [ + ...percentItems, + 'divider', + { option: null, title: 'Sub-Options (demo)', subMenuTitle: 'Set Percent Complete', optionItems: percentItems }, + ], + commandItems: [ + { command: '', divider: true, positionOrder: 98 }, + { + command: 'export', + title: 'Exports', + positionOrder: 99, + commandItems: [ + { command: 'exports-txt', title: 'Text (tab delimited)' }, + { + command: 'sub-menu', + title: 'Excel', + subMenuTitle: 'available formats', + commandItems: [ + { command: 'exports-csv', title: 'Excel (csv)' }, + { command: 'exports-xlsx', title: 'Excel (xlsx)' }, + ], + }, + ], + }, + { + command: 'feedback', + title: 'Feedback', + positionOrder: 100, + commandItems: [ + { command: 'request-update', title: 'Request update from supplier' }, + 'divider', + { + command: 'sub-menu', + title: 'Contact Us', + subMenuTitle: 'contact us...', + commandItems: [ + { command: 'contact-email', title: 'Email us' }, + { command: 'contact-chat', title: 'Chat with us' }, + { command: 'contact-meeting', title: 'Book an appointment' }, + ], + }, + ], + }, + ], + onOptionSelected: (_e: unknown, args: any) => { + if (args?.dataContext) { + args.dataContext.percentComplete = args.item.option; + reactGridRef.current?.slickGrid?.updateRow(args.row || 0); + } + }, + onCommand: (_e: unknown, args: any) => executeCommand(args), + }; + } + + function executeCommand(args: any) { + if (args.command === 'delete-row') { + if (confirm(`Do you really want to delete row (${args.row + 1}) with "${args.dataContext.title}"?`)) { + reactGridRef.current?.gridService?.deleteItemById(args.dataContext.id); + } + } else if (['command1', 'command2', 'help'].includes(args.command)) { + alert(args.item.title); + } else if (['exports-csv', 'exports-txt', 'exports-xlsx'].includes(args.command)) { + alert(`Exporting as ${args.item.title}`); + } else { + alert(`Command: ${args.command}`); + } } function costDurationFormatter(_row: number, _cell: number, _value: any, _columnDef: Column, dataContext: any) { @@ -325,21 +402,75 @@ const Example20: React.FC = () => { showToast(args.validationResults.msg, 'danger'); } - function setFrozenColumns(frozenCols: number) { - reactGridRef.current?.slickGrid.setOptions({ frozenColumn: frozenCols }); - const updatedGridOptions = reactGridRef.current?.slickGrid.getOptions(); - setGridOptions(updatedGridOptions); - setFrozenColumnCount(frozenCols); + function removePinnedColumns() { + setPinnedColumns(-1, 0); + setPinnedColumnCount(0); } - /** toggle dynamically, through slickgrid "setOptions()" the top/bottom pinned location */ - function toggleFrozenBottomRows() { + function setPinnedColumns(left: number, right = pinnedRightColumnCount) { + const nextRight = Math.max(0, Number(right) || 0); + const rightIds = ['cityOfOrigin', 'action'].slice(Math.max(0, 2 - nextRight)); + pinnedColumnCountRef.current = left; + pinnedRightColumnCountRef.current = nextRight; reactGridRef.current?.slickGrid.setOptions({ - frozenBottom: !isFrozenBottom, + pinning: { columns: { left: left >= 0 ? left : [], right: rightIds } }, }); + setPinnedColumnCount(left); + setPinnedRightColumnCount(nextRight); + if (pinnedRightColumnInputRef.current) { + pinnedRightColumnInputRef.current.value = `${nextRight}`; + } + } + + function reapplyPinnedColumns() { + const left = pinnedColumnCountRef.current; + const right = pinnedRightColumnCountRef.current; + const rightIds = ['cityOfOrigin', 'action'].slice(Math.max(0, 2 - right)); + reactGridRef.current?.slickGrid.setOptions({ pinning: { columns: { left: left >= 0 ? left : [], right: rightIds } } }); + } + + function updatePinnedRightColumnCount() { + const nextRight = Math.max(0, Number(pinnedRightColumnInputRef.current?.value ?? pinnedRightColumnCount) || 0); + setPinnedColumns(pinnedColumnCount, nextRight); + } + + function toggleRightPinning() { + setPinnedColumns(pinnedColumnCount, pinnedRightColumnCount > 0 ? 0 : 1); + } + + function toggleSelectAllRow() { + const next = !isSelectAllShownAsColumnTitle; + setIsSelectAllShownAsColumnTitle(next); + checkboxSelectorRef.current?.setOptions({ hideInColumnTitleRow: !next, hideInFilterHeaderRow: next }); + } - const newIsFrozenBottom = !isFrozenBottom; - setIsFrozenBottom(newIsFrozenBottom); + function setLargePinnedColumns() { + reactGridRef.current?.gridStateService?.applyColumnLayout?.( + [ + { columnId: '_checkbox_selector', cssClass: 'slick-cell-checkboxsel', headerCssClass: '', width: 40 }, + { columnId: 'title', cssClass: '', headerCssClass: '', width: 220 }, + { columnId: 'percentComplete', cssClass: '', headerCssClass: '', width: 280 }, + { columnId: 'start', cssClass: '', headerCssClass: '', width: 150 }, + { columnId: 'finish', cssClass: '', headerCssClass: '', width: 280 }, + { columnId: 'completed', cssClass: '', headerCssClass: '', width: 180 }, + { columnId: 'cost', cssClass: '', headerCssClass: '', width: 220 }, + { columnId: 'cityOfOrigin', cssClass: '', headerCssClass: '', width: 180 }, + { columnId: 'action', cssClass: '', headerCssClass: '', width: 110 }, + ], + false, + false + ); + setPinnedColumns(pinnedColumnCount, pinnedRightColumnCount); + } + + /** toggle dynamically, through slickgrid "setOptions()" the top/bottom pinned location */ + function togglePinnedBottomRows() { + const newIsPinnedBottom = !isPinnedBottom; + const rows = Array.from({ length: Math.max(0, pinnedRowCount) }, (_value, index) => index); + reactGridRef.current?.slickGrid.setOptions({ + pinning: { rows: newIsPinnedBottom ? { top: [], bottom: rows } : { top: rows, bottom: [] } }, + }); + setIsPinnedBottom(newIsPinnedBottom); } function toggleSubTitle() { @@ -355,7 +486,7 @@ const Example20: React.FC = () => { ) : (

- Example 20: Pinned (frozen) Columns/Rows + Example 20: Pinned Columns/Rows see  {

- This example demonstrates the use of Pinned (aka frozen) Columns and/or Rows ( - + This example demonstrates the use of Pinned (aka pinned) Columns and/or Rows ( + Docs ) @@ -395,15 +526,37 @@ const Example20: React.FC = () => {
- changeFrozenRowCount($event)} /> - - changeFrozenColumnCount($event)} /> - + + + + + @@ -414,20 +567,49 @@ const Example20: React.FC = () => {
- + - - : {isFrozenBottom ? 'Bottom' : 'Top'} + : {isPinnedBottom ? 'Bottom' : 'Top'} + +
diff --git a/demos/react/src/examples/slickgrid/Example43.tsx b/demos/react/src/examples/slickgrid/Example43.tsx index 2521e1e0ef..fbc350e3e1 100644 --- a/demos/react/src/examples/slickgrid/Example43.tsx +++ b/demos/react/src/examples/slickgrid/Example43.tsx @@ -130,7 +130,6 @@ export default function Example43() { autoResize: { container: '#demo-container', bottomPadding: 30, - rightPadding: 50, }, enableCellNavigation: true, enableColumnReorder: true, @@ -142,7 +141,7 @@ export default function Example43() { autoEdit: true, editable: false, datasetIdPropertyName: 'employeeID', - frozenColumn: 0, + pinning: { columns: { left: 0 } }, gridHeight: 348, rowHeight: 30, dataView: { @@ -443,7 +442,7 @@ export default function Example43() { newMetadata[row].columns[Number(col) + colDirIdx] = (this.metadata as any)[row].columns[col]; } } - reactGrid?.slickGrid?.setOptions({ frozenColumn: newShowEmployeeId ? 0 : 1 }); + reactGrid?.slickGrid?.setOptions({ pinning: { columns: { left: newShowEmployeeId ? 0 : 1 } } }); reactGrid?.slickGrid?.updateColumnById('employeeID', { hidden: !newShowEmployeeId }); reactGrid?.slickGrid?.updateColumns(); */ diff --git a/demos/react/src/examples/slickgrid/Example55.tsx b/demos/react/src/examples/slickgrid/Example55.tsx index 099865443b..553bc4eaca 100644 --- a/demos/react/src/examples/slickgrid/Example55.tsx +++ b/demos/react/src/examples/slickgrid/Example55.tsx @@ -84,7 +84,7 @@ const Example55: React.FC = () => { const owners = ['Alex', 'Priya', 'Mia', 'Sam', 'Chris']; const fragments = [ 'Refactor keyboard shortcut handling for better readability.', - 'Adjust frozen rows when view-model updates after grouping.', + 'Adjust pinned rows when view-model updates after grouping.', 'Improve screen-reader labels on grid menu actions.', 'Align batch editor validation with backend constraints.', 'Capture edge-case around hidden columns and row-span.', diff --git a/demos/react/src/examples/slickgrid/Example56.tsx b/demos/react/src/examples/slickgrid/Example56.tsx index 5af54c812a..b15f7c5cd9 100644 --- a/demos/react/src/examples/slickgrid/Example56.tsx +++ b/demos/react/src/examples/slickgrid/Example56.tsx @@ -79,7 +79,7 @@ const Example56: React.FC = () => { includeColumnWidth: true, }, rowHeight: 40, - frozenRow: 2, + pinning: { rows: { top: [0, 1] } }, gridHeight: 560, gridWidth: 1080, dataView: { @@ -113,7 +113,7 @@ const Example56: React.FC = () => { const statuses: Array = ['Todo', 'In Progress', 'Done']; const notesPool = [ 'Short note.', - 'Need to validate keyboard navigation and ensure screen reader output remains stable across frozen panes.', + 'Need to validate keyboard navigation and ensure screen reader output remains stable across pinned panes.', 'Review row height invalidation path when data changes quickly due to live updates from backend polling.', 'Longer QA note: validate scrolling behavior at top and bottom boundaries, compare rendered range against expected rows, and confirm no visual clipping for wrapped cells.', ]; diff --git a/demos/react/src/examples/slickgrid/example14.scss b/demos/react/src/examples/slickgrid/example14.scss index 6e31054384..e41265a0e7 100644 --- a/demos/react/src/examples/slickgrid/example14.scss +++ b/demos/react/src/examples/slickgrid/example14.scss @@ -1,8 +1,8 @@ -/** You can change the pinned/frozen border styling through this css override */ +/** You can change the pinned/pinned border styling through this css override */ -.slick-row .slick-cell.frozen:last-child, -.slick-headerrow-column.frozen:last-child, -.slick-footerrow-column.frozen:last-child { +.slick-row .slick-cell.pinned:last-child, +.slick-headerrow-column.pinned:last-child, +.slick-footerrow-column.pinned:last-child { border-right: 1px solid #969696 !important; } diff --git a/demos/react/src/examples/slickgrid/example20.scss b/demos/react/src/examples/slickgrid/example20.scss index 6e31054384..e41265a0e7 100644 --- a/demos/react/src/examples/slickgrid/example20.scss +++ b/demos/react/src/examples/slickgrid/example20.scss @@ -1,8 +1,8 @@ -/** You can change the pinned/frozen border styling through this css override */ +/** You can change the pinned/pinned border styling through this css override */ -.slick-row .slick-cell.frozen:last-child, -.slick-headerrow-column.frozen:last-child, -.slick-footerrow-column.frozen:last-child { +.slick-row .slick-cell.pinned:last-child, +.slick-headerrow-column.pinned:last-child, +.slick-footerrow-column.pinned:last-child { border-right: 1px solid #969696 !important; } diff --git a/demos/react/test/cypress/e2e/example03.cy.ts b/demos/react/test/cypress/e2e/example03.cy.ts index f488efd517..a08337a57c 100644 --- a/demos/react/test/cypress/e2e/example03.cy.ts +++ b/demos/react/test/cypress/e2e/example03.cy.ts @@ -61,8 +61,8 @@ describe('Example 3 - Grid with Editors', () => { .click(); // change Title & Custom Title - cy.get('.editor-title > textarea').type('Task 2222'); - cy.get('.editor-title .btn-save').click(); + cy.get('.editor-title:visible > textarea').type('Task 2222'); + cy.get('.editor-title:visible .btn-save').click(); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(2)`).should('contain', 'Task 2222'); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(3)`).should('contain', 'Task 2222'); @@ -98,7 +98,7 @@ describe('Example 3 - Grid with Editors', () => { `[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(11) > input.editor-checkbox.editor-effort-driven` ).check(); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); }); it('should dynamically add 2x new "Title" columns', () => { @@ -147,8 +147,8 @@ describe('Example 3 - Grid with Editors', () => { .click(); // change Title & Custom Title - cy.get('.editor-title > textarea').type('Task 0000'); - cy.get('.editor-title .btn-save').click(); + cy.get('.editor-title:visible > textarea').type('Task 0000'); + cy.get('.editor-title:visible .btn-save').click(); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(2)`).should('contain', 'Task 0000'); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(3)`).should('contain', 'Task 0000'); @@ -180,7 +180,7 @@ describe('Example 3 - Grid with Editors', () => { .blur(); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(10)`).click(); // the blur seems to not always work, so just click on another cell cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(11)`).find('.mdi-check.checkmark-icon'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); }); it('should be able to filter and search "Task 2222" in the new column and expect only 1 row showing in the grid', () => { @@ -258,7 +258,7 @@ describe('Example 3 - Grid with Editors', () => { }); it('should open the "Prerequisites" Filter and expect to have Task 500 & 101 in the Filter', () => { - cy.get('div.ms-filter.filter-prerequisites').trigger('click', { force: true }); + cy.get('div.ms-filter.filter-prerequisites').trigger('click'); cy.get('.ms-drop').find('span:nth(1)').contains('Task 101'); @@ -270,7 +270,7 @@ describe('Example 3 - Grid with Editors', () => { it('should open the "Prerequisites" Editor and expect to have Task 100 & 101 in the Editor', () => { cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(12)`) .should('contain', '') - .click({ force: true }); + .click(); cy.get('.ms-drop').find('span:nth(1)').contains('Task 101'); @@ -284,11 +284,11 @@ describe('Example 3 - Grid with Editors', () => { }); it('should delete the last item "Task 101" and expect it to be removed from the Filter', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('right').wait(50); + cy.get('.slick-horizontal-scroller').scrollTo('right').wait(50); cy.get('[data-test="delete-item-btn"]').click(); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo(0, 0).wait(50); + cy.get('.slick-horizontal-scroller').scrollTo(0, 0).wait(50); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(2)`).should('contain', 'Task 100'); cy.get('div.ms-filter.filter-prerequisites').trigger('click'); diff --git a/demos/react/test/cypress/e2e/example07.cy.ts b/demos/react/test/cypress/e2e/example07.cy.ts index 048c260d9b..446c389b24 100644 --- a/demos/react/test/cypress/e2e/example07.cy.ts +++ b/demos/react/test/cypress/e2e/example07.cy.ts @@ -75,7 +75,7 @@ describe('Example 7 - Header Button Plugin', () => { }); it('should go over the last "Column J" and expect to find the button to have the disabled class and clicking it should not turn the negative numbers to red neither expect console log after clicking the disabled button', () => { - cy.get('#grid7-1 .slick-viewport-top.slick-viewport-left').scrollTo('right').wait(50); + cy.get('#grid7-1 .slick-horizontal-scroller').scrollTo('right').wait(50); cy.get('#grid7-1 .slick-header-columns') .children('.slick-header-column:nth(9)') @@ -109,7 +109,7 @@ describe('Example 7 - Header Button Plugin', () => { }); it('should resize 1st column and make it wider', () => { - cy.get('#grid7-1 .slick-viewport-top.slick-viewport-left').scrollTo('left').wait(50); + cy.get('#grid7-1 .slick-horizontal-scroller').scrollTo('left').wait(50); cy.get('#grid7-1 .slick-header-columns').children('.slick-header-column:nth(0)').should('contain', 'Resize me!'); @@ -220,7 +220,7 @@ describe('Example 7 - Header Button Plugin', () => { }); it('should go over the last "Column J" and expect to find the button to have the disabled class and clicking it should not turn the negative numbers to red neither expect console log after clicking the disabled button', () => { - cy.get('#grid7-2 .slick-viewport-top.slick-viewport-left').scrollTo('right').wait(50); + cy.get('#grid7-2 .slick-horizontal-scroller').scrollTo('right').wait(50); cy.get('#grid7-2 .slick-header-columns') .children('.slick-header-column:nth(9)') @@ -254,7 +254,7 @@ describe('Example 7 - Header Button Plugin', () => { }); it('should resize 1st column and make it wider', () => { - cy.get('#grid7-2 .slick-viewport-top.slick-viewport-left').scrollTo('left').wait(50); + cy.get('#grid7-2 .slick-horizontal-scroller').scrollTo('left').wait(50); cy.get('#grid7-2 .slick-header-columns').children('.slick-header-column:nth(0)').should('contain', 'Resize me!'); @@ -364,7 +364,7 @@ describe('Example 7 - Header Button Plugin', () => { }); it('should expect first few items of "Column C" to be negative numbers and be red', () => { - cy.get('#grid7-2 .slick-viewport-top.slick-viewport-left').scrollTo('top').wait(50); + cy.get('#grid7-2 .slick-vertical-scroller').scrollTo('top').wait(50); cy.get('#grid7-2 .slick-row').each(($row, index) => { if (index > 10) { diff --git a/demos/react/test/cypress/e2e/example10.cy.ts b/demos/react/test/cypress/e2e/example10.cy.ts index f65cc30eeb..fb67e74664 100644 --- a/demos/react/test/cypress/e2e/example10.cy.ts +++ b/demos/react/test/cypress/e2e/example10.cy.ts @@ -389,14 +389,14 @@ describe('Example 10 - Multiple Grids with Row Selection', () => { it('should scroll to the bottom of 2nd Grid and still have 5 rows (Task 1,Task 3,Task 12,Task 13,Task 522) selected and find 2 row selected because we now have 2 rows that got rendered (first and last)', () => { cy.get('#slickGridContainer-grid2').as('grid2'); cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522'); - cy.get('@grid2').find('.slick-viewport-top.slick-viewport-left').scrollTo('bottom').wait(10); + cy.get('@grid2').find('.slick-vertical-scroller').scrollTo('bottom').wait(10); cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2); }); it('should have 2 rows (Task 3,Task 13) selected in 2nd grid after typing in a search filter (3)', () => { cy.get('#slickGridContainer-grid2').as('grid2'); cy.get('@grid2').find('.filter-title').type('3'); - cy.get('@grid2').find('.slick-viewport-top.slick-viewport-left').scrollTo('top').wait(10); + cy.get('@grid2').find('.slick-vertical-scroller').scrollTo('top').wait(10); cy.get('@grid2').find('.slick-row').should('not.have.length', 0); cy.wait(50); diff --git a/demos/react/test/cypress/e2e/example12.cy.ts b/demos/react/test/cypress/e2e/example12.cy.ts index ec11ba1405..7ed1cab4e7 100644 --- a/demos/react/test/cypress/e2e/example12.cy.ts +++ b/demos/react/test/cypress/e2e/example12.cy.ts @@ -229,7 +229,7 @@ describe('Example 12: Localization (i18n)', () => { it('should scroll to bottom of the grid then select "Task 4"', () => { cy.get('#slickGridContainer-grid12').as('grid12'); - cy.get('@grid12').find('.slick-viewport-top.slick-viewport-left').scrollTo('bottom').wait(10); + cy.get('@grid12').find('.slick-vertical-scroller').scrollTo('bottom').wait(10); cy.get('#grid12').contains('Task 4').parent().children('.slick-cell-checkboxsel').find('input[type=checkbox]').click({ force: true }); @@ -259,7 +259,7 @@ describe('Example 12: Localization (i18n)', () => { cy.get('.grid-canvas').find('.slick-row').should('be.visible'); - cy.get('@grid12').find('.slick-viewport-top.slick-viewport-left').scrollTo('top').wait(10); + cy.get('@grid12').find('.slick-vertical-scroller').scrollTo('top').wait(10); cy.get('@grid12').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 1); diff --git a/demos/react/test/cypress/e2e/example14.cy.ts b/demos/react/test/cypress/e2e/example14.cy.ts index 91f7db34b6..32b529fd3b 100644 --- a/demos/react/test/cypress/e2e/example14.cy.ts +++ b/demos/react/test/cypress/e2e/example14.cy.ts @@ -1,5 +1,4 @@ describe('Example 14 - Column Span & Header Grouping', () => { - // NOTE: everywhere there's a * 2 is because we have a top+bottom (frozen rows) containers even after Unfreeze Columns/Rows const fullPreTitles = ['', 'Common Factor', 'Period', 'Analysis']; const fullTitles = ['#', 'Title', 'Duration', 'Start', 'Finish', '% Complete', 'Effort Driven']; @@ -20,17 +19,18 @@ describe('Example 14 - Column Span & Header Grouping', () => { .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); }); - it('should have a frozen grid on page load with 3 columns on the left and 4 columns on the right', () => { - cy.get('#grid2').find('[data-row=0]').should('have.length', 2); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 3); - cy.get('#grid2').find('.grid-canvas-right > [data-row=0]').children().should('have.length', 4); + it('should have a pinned grid on page load with 3 pinned columns and 4 scrolling columns', () => { + const firstRow = '#grid2 .slick-row[data-row="0"]'; + cy.get(firstRow).should('have.length', 1); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell`).should('have.length', 3); + cy.get(`${firstRow} .slick-scrolling-cells .slick-cell`).should('have.length', 4); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0]> .slick-cell:nth(0)').should('contain', '0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0]> .slick-cell:nth(1)').should('contain', 'Task 0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0]> .slick-cell:nth(2)').should('contain', '5 days'); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell:nth(0)`).should('contain', '0'); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell:nth(1)`).should('contain', 'Task 0'); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell:nth(2)`).should('contain', '5 days'); - cy.get('#grid2').find('.grid-canvas-right > [data-row=0]> .slick-cell:nth(0)').should('contain', '01/01/2009'); - cy.get('#grid2').find('.grid-canvas-right > [data-row=0]> .slick-cell:nth(1)').should('contain', '01/05/2009'); + cy.get(`${firstRow} .slick-scrolling-cells .slick-cell:nth(0)`).should('contain', '01/01/2009'); + cy.get(`${firstRow} .slick-scrolling-cells .slick-cell:nth(1)`).should('contain', '01/05/2009'); }); it('should have exact Column Pre-Header & Column Header Titles in the grid again', () => { @@ -45,17 +45,18 @@ describe('Example 14 - Column Span & Header Grouping', () => { .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); }); - it('should click on the "Remove Frozen Columns" button to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => { - cy.get('[data-test="remove-frozen-column-button"]').click(); + it('should click on the "Remove Pinned Columns" button to switch to a regular grid without pinned columns', () => { + cy.get('[data-test="remove-pinned-column-button"]').click(); - cy.get('#grid2').find('[data-row=0]').should('have.length', 1); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 7); + const firstRow = '#grid2 .slick-row[data-row="0"]'; + cy.get(firstRow).should('have.length', 1); + cy.get(`${firstRow} .slick-cell`).should('have.length', 7); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)').should('contain', '0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)').should('contain', 'Task 0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)').should('contain', '5 days'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(3)').should('contain', '01/01/2009'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(4)').should('contain', '01/05/2009'); + cy.get(`${firstRow} .slick-cell:nth(0)`).should('contain', '0'); + cy.get(`${firstRow} .slick-cell:nth(1)`).should('contain', 'Task 0'); + cy.get(`${firstRow} .slick-cell:nth(2)`).should('contain', '5 days'); + cy.get(`${firstRow} .slick-cell:nth(3)`).should('contain', '01/01/2009'); + cy.get(`${firstRow} .slick-cell:nth(4)`).should('contain', '01/05/2009'); }); it('should have exact Column Pre-Header & Column Header Titles in the grid once again', () => { @@ -70,19 +71,20 @@ describe('Example 14 - Column Span & Header Grouping', () => { .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); }); - it('should click on the "Set 3 Frozen Columns" button to switch frozen columns grid and expect 3 frozen columns on the left and 4 columns on the right', () => { - cy.contains('Set 3 Frozen Columns').click({ force: true }); + it('should click on the "Set 3 Pinned Columns" button to pin 3 columns and leave 4 scrolling columns', () => { + cy.contains('Set 3 Pinned Columns').click({ force: true }); - cy.get('#grid2').find('[data-row=0]').should('have.length', 2); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 3); - cy.get('#grid2').find('.grid-canvas-right > [data-row=0]').children().should('have.length', 4); + const firstRow = '#grid2 .slick-row[data-row="0"]'; + cy.get(firstRow).should('have.length', 1); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell`).should('have.length', 3); + cy.get(`${firstRow} .slick-scrolling-cells .slick-cell`).should('have.length', 4); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)').should('contain', '0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)').should('contain', 'Task 0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)').should('contain', '5 days'); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell:nth(0)`).should('contain', '0'); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell:nth(1)`).should('contain', 'Task 0'); + cy.get(`${firstRow} .slick-pinned-left-cells .slick-cell:nth(2)`).should('contain', '5 days'); - cy.get('#grid2').find('.grid-canvas-right > [data-row=0] > .slick-cell:nth(0)').should('contain', '01/01/2009'); - cy.get('#grid2').find('.grid-canvas-right > [data-row=0] > .slick-cell:nth(1)').should('contain', '01/05/2009'); + cy.get(`${firstRow} .slick-scrolling-cells .slick-cell:nth(0)`).should('contain', '01/01/2009'); + cy.get(`${firstRow} .slick-scrolling-cells .slick-cell:nth(1)`).should('contain', '01/05/2009'); }); it('should have still exact Column Pre-Header & Column Header Titles in the grid', () => { @@ -97,56 +99,49 @@ describe('Example 14 - Column Span & Header Grouping', () => { .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); }); - it('should click on the Grid Menu command "Unfreeze Columns/Rows" to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => { + it('should click on the Grid Menu command "Unpin Columns/Rows" to switch to a regular grid without pinned columns', () => { cy.get('#grid2').find('button.slick-grid-menu-button').click({ force: true }); - cy.contains('Unfreeze Columns/Rows').click({ force: true }); + cy.contains('Unpin Columns/Rows').click({ force: true }); - cy.get('#grid2').find('[data-row=0]').should('have.length', 1); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 7); + const firstRow = '#grid2 .slick-row[data-row="0"]'; + cy.get(firstRow).should('have.length', 1); + cy.get(`${firstRow} .slick-cell`).should('have.length', 7); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)').should('contain', '0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)').should('contain', 'Task 0'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)').should('contain', '5 days'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(3)').should('contain', '01/01/2009'); - cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(4)').should('contain', '01/05/2009'); + cy.get(`${firstRow} .slick-cell:nth(0)`).should('contain', '0'); + cy.get(`${firstRow} .slick-cell:nth(1)`).should('contain', 'Task 0'); + cy.get(`${firstRow} .slick-cell:nth(2)`).should('contain', '5 days'); + cy.get(`${firstRow} .slick-cell:nth(3)`).should('contain', '01/01/2009'); + cy.get(`${firstRow} .slick-cell:nth(4)`).should('contain', '01/05/2009'); }); - it('should reapply 3 frozen columns on 2nd grid', () => { - cy.contains('Set 3 Frozen Columns').click({ force: true }); + it('should reapply 3 pinned columns on 2nd grid', () => { + cy.contains('Set 3 Pinned Columns').click({ force: true }); - cy.get('#grid2') - .find('.slick-pane-left .slick-header.slick-header-left .slick-header-columns .slick-header-column') - .should('have.length', 3); - - cy.get('#grid2') - .find('.slick-pane-right .slick-header.slick-header-right .slick-header-columns .slick-header-column') - .should('have.length', 4); + cy.get('#grid2 .slick-header.slick-header-left .slick-header-columns .slick-column-pinned-left').should('have.length', 3); + cy.get('#grid2 .slick-header.slick-header-left .slick-header-columns .slick-header-column:not(.slick-column-pinned-left)').should( + 'have.length', + 4 + ); }); - it('should be able to "Unfreeze Columns" from header menu', () => { + it('should be able to "Unpin All Columns" from header menu', () => { cy.get('#grid2') - .find('.slick-pane-left .slick-header-columns .slick-header-column[role="columnheader"]:nth(2)') + .find('.slick-header.slick-header-left .slick-header-columns .slick-header-column[role="columnheader"]:nth(2)') .trigger('mouseover') .children('.slick-header-menu-button') .invoke('show') .click(); - cy.get('.slick-header-menu .slick-menu-command-list') - .should('be.visible') - .children('.slick-menu-item:nth-of-type(1)') - .children('.slick-menu-content') - .should('contain', 'Unfreeze Columns') - .click(); + cy.get('.slick-header-menu .slick-menu-command-list').should('be.visible').find('[data-command="pin-column"]').click(); + cy.get('.slick-submenu [data-command="unpin-columns"]').should('contain', 'Unpin All Columns').click(); - cy.get('#grid2') - .find('.slick-pane-left .slick-header.slick-header-left .slick-header-columns .slick-header-column') - .should('have.length', 7); + cy.get('#grid2 .slick-header.slick-header-left .slick-header-columns .slick-header-column').should('have.length', 7); }); - it('should be able to "Freeze Columns" back from header menu', () => { + it('should be able to "Pin Through Here" back from header menu', () => { cy.get('#grid2') - .find('.slick-pane-left .slick-header-columns .slick-header-column[role="columnheader"]:nth(2)') + .find('.slick-header.slick-header-left .slick-header-columns .slick-header-column[role="columnheader"]:nth(2)') .trigger('mouseover') .children('.slick-header-menu-button') .invoke('show') @@ -154,57 +149,72 @@ describe('Example 14 - Column Span & Header Grouping', () => { cy.get('.slick-header-menu .slick-menu-command-list') .should('be.visible') - .children('.slick-menu-item:nth-of-type(1)') - .children('.slick-menu-content') - .should('contain', 'Freeze Columns') + .find('[data-command="pin-column"]') + .should('contain', 'Column Pinning') .click(); - cy.get('#grid2') - .find('.slick-pane-left .slick-header.slick-header-left .slick-header-columns .slick-header-column') - .should('have.length', 3); + cy.get('.slick-submenu [data-command="pin-columns"]').should('contain', 'Pin Through Here').click(); - cy.get('#grid2') - .find('.slick-pane-right .slick-header.slick-header-right .slick-header-columns .slick-header-column') - .should('have.length', 4); + cy.get('#grid2 .slick-header.slick-header-left .slick-header-columns .slick-column-pinned-left').should('have.length', 3); + cy.get('#grid2 .slick-header.slick-header-left .slick-header-columns .slick-header-column:not(.slick-column-pinned-left)').should( + 'have.length', + 4 + ); }); describe('Basic Key Navigations', () => { it('should remove any freezing', () => { - cy.get('[data-test="remove-frozen-column-button"]').click(); + cy.get('[data-test="remove-pinned-column-button"]').click(); + + cy.get('#grid2 .slick-header.slick-header-left .slick-header-columns .slick-header-column').should('have.length', 7); + }); + + it('should start at Task 1 on Duration colspan 5 days and type "PageDown" key once and land on a full colspan', () => { + cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('@active_cell').type('{pagedown}'); + cy.get('#grid1 .slick-cell.l0.r5.active').should('have.length', 1); + }); + + it('should start at Task 1 on Duration colspan 5 days and type "PageDown" key 2x times and land on a colspan of 3', () => { + cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('@active_cell').type('{pagedown}{pagedown}'); + cy.get('#grid1 .slick-cell.l1.r3.active').should('have.length', 1); + }); - cy.get('#grid2') - .find('.slick-pane-left .slick-header.slick-header-left .slick-header-columns .slick-header-column') - .should('have.length', 7); + it('should navigate PageUp twice from a colspan of 3 back to the starting colspan of 3', () => { + cy.get('#grid1 [data-row=15] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('@active_cell').type('{pageup}{pageup}'); + cy.get('#grid1 .slick-cell.l1.r3.active').should('have.length', 1); }); it('should start at Task 2 on Duration colspan 5 days and type "PageDown" key 2x times and "PageUp" twice and be back to Task 1 with colspan of 3', () => { - cy.get('[data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); cy.get('@active_cell').type('{pagedown}{pagedown}{pageup}{pageup}'); - cy.get('[data-row=1] > .slick-cell.l1.r3.active').should('have.length', 1); + cy.get('#grid1 .slick-cell.l1.r3.active').should('have.length', 1); }); it('should start at Task 2 on Duration colspan 5 days and type "PageDown" key 2x times and "PageUp" 3x times and be on Task 0 with full colspan', () => { - cy.get('[data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); cy.get('@active_cell').type('{pagedown}{pagedown}{pageup}{pageup}{pageup}'); - cy.get('[data-row=0] > .slick-cell.l0.r5.active').should('have.length', 1); + cy.get('#grid1 .slick-cell.l0.r5.active').should('have.length', 1); }); it('should start at Task 1 on Duration colspan 5 days and type "ArrowDown" key once and be on Task 2 with full colspan', () => { - cy.get('[data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); cy.get('@active_cell').type('{downarrow}'); - cy.get('[data-row=2] > .slick-cell.l0.r5.active').should('have.length', 1); + cy.get('#grid1 .slick-cell.l0.r5.active').should('have.length', 1); }); it('should start at Task 1 on Duration colspan 5 days and type "ArrowDown" key 2x times and be on Task 1 with colspan of 3', () => { - cy.get('[data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); cy.get('@active_cell').type('{downarrow}{downarrow}'); - cy.get('[data-row=3] > .slick-cell.l1.r3.active').should('have.length', 1); + cy.get('#grid1 .slick-cell.l1.r3.active').should('have.length', 1); }); it('should start at Task 1 on Duration colspan 5 days and type "ArrowDown" key 2x times, then "ArrowUp" key 2x times and be back on Task 1 with colspan of 3', () => { - cy.get('[data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); + cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').as('active_cell').click(); cy.get('@active_cell').type('{downarrow}{downarrow}{uparrow}{uparrow}'); - cy.get('[data-row=1] > .slick-cell.l1.r3.active').should('have.length', 1); + cy.get('#grid1 .slick-cell.l1.r3.active').should('have.length', 1); }); }); @@ -219,7 +229,7 @@ describe('Example 14 - Column Span & Header Grouping', () => { .contains(/(true|false)+$/); cy.get('#grid1') - .find('.slick-pane-left .slick-header-columns .slick-header-column[role="columnheader"]:nth(3)') + .find('.slick-header-columns .slick-header-column[role="columnheader"]:nth(3)') .trigger('mouseover') .children('.slick-header-menu-button') .invoke('show') @@ -232,6 +242,11 @@ describe('Example 14 - Column Span & Header Grouping', () => { .should('contain', 'Hide Column') .click(); + // The colspan still spans the logical Duration/Start/Finish range, but + // the hidden Finish track is zero-width. The host cell must remain + // rendered and visibly cover the two remaining columns. + cy.get('#grid1 [data-row=1] .slick-cell.l1.r3').should('contain', '5 days').and('be.visible'); + // goto right cy.get('#grid1').find('[data-row=1] .slick-cell.l0.r0').click(); cy.get('#grid1').find('[data-row=1] .slick-cell.l0.r0.active').should('contain', 'Task 1').type('{rightArrow}'); @@ -272,7 +287,7 @@ describe('Example 14 - Column Span & Header Grouping', () => { .contains(/(true|false)+$/); cy.get('#grid1') - .find('.slick-pane-left .slick-header-columns .slick-header-column[role="columnheader"]:nth(3)') + .find('.slick-header-columns .slick-header-column[role="columnheader"]:nth(3)') .trigger('mouseover') .children('.slick-header-menu-button') .invoke('show') diff --git a/demos/react/test/cypress/e2e/example15.cy.ts b/demos/react/test/cypress/e2e/example15.cy.ts index 2792dbeac6..5a7e09da9d 100644 --- a/demos/react/test/cypress/e2e/example15.cy.ts +++ b/demos/react/test/cypress/e2e/example15.cy.ts @@ -1,7 +1,6 @@ import { format } from '@formkit/tempo'; describe('Example 15: Grid State & Presets using Local Storage', () => { - const GRID_ROW_HEIGHT = 35; const fullEnglishTitles = ['', 'Title', 'Description', 'Duration', '% Complete', 'Start', 'Completed']; beforeEach(() => { @@ -12,16 +11,11 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { cy.saveLocalStorage(); }); - it('should display Example title', () => { - cy.visit(`${Cypress.config('baseUrl')}/example15`); + it('should display Example title from a clean local-storage state', () => { + cy.visit(`${Cypress.config('baseUrl')}/example15`, { + onBeforeLoad: (window) => window.localStorage.clear(), + }); cy.get('h2').should('contain', 'Example 15: Grid State & Presets using Local Storage'); - - cy.clearLocalStorage(); - cy.get('[data-test=reset-button]').click(); - }); - - it('should reload the page', () => { - cy.reload().wait(50); }); it('should have exact Column Titles in the grid', () => { @@ -348,7 +342,7 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { .each(($child, index) => expect($child.find('.slick-column-name').text()).to.eq(expectedTitles[index])); }); - it('should be able to freeze "Description" column', () => { + it('should be able to pin "Description" column', () => { cy.get('.slick-header-columns') .children('.slick-header-column:nth(1)') .trigger('mouseover') @@ -359,10 +353,11 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { cy.get('.slick-header-menu .slick-menu-command-list') .should('be.visible') - .children('.slick-menu-item:nth-of-type(1)') - .children('.slick-menu-content') - .should('contain', 'Geler les colonnes') - .click(); + .find('[data-command="pin-column"]') + .should('contain', 'Épinglage de colonnes') + .trigger('mouseover'); + + cy.get('.slick-submenu:visible [data-command="pin-columns"]').should('contain', "Épingler jusqu'ici").click(); }); it('should reload the page', () => { @@ -423,10 +418,11 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { }); }); - it('should have a persisted frozen column after "Description" and a grid with 4 containers on page load with 2 columns on the left and 3 columns on the right', () => { - cy.get('[style="transform: translateY(0px);"]').should('have.length', 2); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]').children().should('have.length', 2); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]').children().should('have.length', 3); + it('should have a persisted pinned column after "Description" with 2 pinned and 3 scrolling columns', () => { + const firstRow = '#grid15 .slick-row[data-row="0"]'; + cy.get(firstRow).should('have.length', 1); + cy.get(`${firstRow} .slick-pinned-left-cells > .slick-cell`).should('have.length', 2); + cy.get(`${firstRow} .slick-scrolling-cells > .slick-cell`).should('have.length', 3); }); it('should click on the reset button and have exact Column Titles position as in beginning', () => { @@ -453,7 +449,7 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); }); - it('should be able to freeze "Description" 3rd column', () => { + it('should be able to pin "Description" 3rd column', () => { cy.get('.slick-header-columns') .children('.slick-header-column:nth(2)') .trigger('mouseover') @@ -464,10 +460,11 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { cy.get('.slick-header-menu .slick-menu-command-list') .should('be.visible') - .children('.slick-menu-item:nth-of-type(1)') - .children('.slick-menu-content') - .should('contain', 'Freeze Columns') - .click(); + .find('[data-command="pin-column"]') + .should('contain', 'Column Pinning') + .trigger('mouseover'); + + cy.get('.slick-submenu:visible [data-command="pin-columns"]').should('contain', 'Pin Through Here').click(); }); it('should swap "Duration" and "% Complete" columns', () => { @@ -481,7 +478,7 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); }); - it('should be able to freeze "% Complete" and expect 4th column to be freezed', () => { + it('should be able to pin "% Complete" and expect 4th column to be pinned', () => { cy.get('.slick-header-columns') .children('.slick-header-column:nth(3)') .trigger('mouseover') @@ -492,16 +489,18 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { cy.get('.slick-header-menu .slick-menu-command-list') .should('be.visible') - .children('.slick-menu-item:nth-of-type(1)') - .children('.slick-menu-content') - .should('contain', 'Freeze Columns') - .click(); + .find('[data-command="pin-column"]') + .should('contain', 'Column Pinning') + .trigger('mouseover'); + + cy.get('.slick-submenu:visible [data-command="pin-columns"]').should('contain', 'Pin Through Here').click(); }); - it('should have a persisted frozen column after "Description" and a grid with 4 containers on page load with 2 columns on the left and 3 columns on the right', () => { - cy.get('[style="transform: translateY(0px);"]').should('have.length', 2); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]').children().should('have.length', 4); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]').children().should('have.length', 3); + it('should have a persisted pinned column after "Description" with 4 pinned and 3 scrolling columns', () => { + const firstRow = '#grid15 .slick-row[data-row="0"]'; + cy.get(firstRow).should('have.length', 1); + cy.get(`${firstRow} .slick-pinned-left-cells > .slick-cell`).should('have.length', 4); + cy.get(`${firstRow} .slick-scrolling-cells > .slick-cell`).should('have.length', 3); }); describe('Filter Shortcuts', () => { @@ -599,9 +598,9 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { expect(Number($span.text())).to.gt(80); }); - cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(2)`).contains('desc'); - cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 1}px);"] > .slick-cell:nth(2)`).contains('desc'); - cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(2)`).contains('desc'); + cy.get('#grid15 .slick-row[data-row="0"] .slick-cell.l2').contains('desc'); + cy.get('#grid15 .slick-row[data-row="1"] .slick-cell.l2').contains('desc'); + cy.get('#grid15 .slick-row[data-row="2"] .slick-cell.l2').contains('desc'); }); }); }); diff --git a/demos/react/test/cypress/e2e/example16.cy.ts b/demos/react/test/cypress/e2e/example16.cy.ts index d9eb35e061..87d69f920c 100644 --- a/demos/react/test/cypress/e2e/example16.cy.ts +++ b/demos/react/test/cypress/e2e/example16.cy.ts @@ -39,7 +39,7 @@ describe('Example 16 - Row Move & Checkbox Selector Selector Plugins', () => { }); it('should expect the row to have moved to another row index', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(2)`).should('contain', 'Task 0'); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 1}px);"] > .slick-cell:nth(2)`).should('contain', 'Task 1'); @@ -76,7 +76,7 @@ describe('Example 16 - Row Move & Checkbox Selector Selector Plugins', () => { cy.get('@moveIconTask5').trigger('mousemove', 'bottomRight').trigger('mouseup', 'bottomRight', { which: 1, force: true }); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(2)`).should('contain', 'Task 0'); cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 1}px);"] > .slick-cell:nth(2)`).should('contain', 'Task 1'); diff --git a/demos/react/test/cypress/e2e/example19.cy.ts b/demos/react/test/cypress/e2e/example19.cy.ts index babe74c51a..d17f2c9055 100644 --- a/demos/react/test/cypress/e2e/example19.cy.ts +++ b/demos/react/test/cypress/e2e/example19.cy.ts @@ -80,7 +80,7 @@ describe('Example 19 - Row Detail View', () => { cy.get('@detailContainer').find('[data-test=delete-btn]').click(); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('#grid19') .find('.slick-row') @@ -111,7 +111,7 @@ describe('Example 19 - Row Detail View', () => { cy.get('[data-test=collapse-all-btn]').click(); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('#grid19').find('.dynamic-cell-detail .innerDetailView_0 .container_0').should('not.exist'); @@ -175,7 +175,7 @@ describe('Example 19 - Row Detail View', () => { cy.get('#grid19').find('.slick-header-column:nth(1)').find('.slick-sort-indicator-asc').should('have.length', 1); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('#grid19').find('.dynamic-cell-detail .innerDetailView_0 .container_0').should('not.exist'); diff --git a/demos/react/test/cypress/e2e/example20.cy.ts b/demos/react/test/cypress/e2e/example20.cy.ts index 11481d0d77..91c8f4b076 100644 --- a/demos/react/test/cypress/e2e/example20.cy.ts +++ b/demos/react/test/cypress/e2e/example20.cy.ts @@ -1,102 +1,172 @@ -describe('Example 20 - Frozen Grid', () => { - // NOTE: everywhere there's a * 2 is because we have a top+bottom (frozen rows) containers even after Unfreeze Columns/Rows - - const fullTitles = [ - '#', - 'Title', - '% Complete', - 'Start', - 'Finish', - 'Cost | Duration', - 'Effort Driven', - 'Title 1', - 'Title 2', - 'Title 3', - 'Title 4', - ]; +describe('Example 20 - Pinned Grid', () => { + before(() => { + // The framework demos include a 250px route sidebar. Use enough width for + // the two-column pinning scenario to remain valid with that sidebar. + cy.viewport(1440, 900); + }); + + const withTitleRowTitles = ['Sel', 'Title', '% Complete', 'Start', 'Finish', 'Completed', 'Cost | Duration', 'City of Origin', 'Action']; + const withoutTitleRowTitles = ['', 'Title', '% Complete', 'Start', 'Finish', 'Completed', 'Cost | Duration', 'City of Origin', 'Action']; + const getCell = (rowIndex: number, columnIndex: number) => + cy.get(`#grid20 .slick-row[data-row="${rowIndex}"] .slick-cell.l${columnIndex}`); + const setRightPinning = (count: number) => { + cy.get('.pinned-right-column-count').clear().type(`${count}`); + cy.get('[data-test="set-pinned-right-column"]').click(); + }; it('should display Example title', () => { cy.visit(`${Cypress.config('baseUrl')}/example20`); - cy.get('h2').should('contain', 'Example 20: Pinned (frozen) Columns/Rows'); + cy.get('h2').should('contain', 'Example 20: Pinned Columns/Rows'); }); it('should have exact column titles on 1st grid', () => { cy.get('#grid20') - .find('.slick-header-columns') - .children() - .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + .find('.slick-header-columns .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); + }); + + it('should hide sub-title to provide more space for the grid', () => { + cy.get('[data-test="toggle-subtitle"]').click(); }); it('should have exact Column Header Titles in the grid', () => { cy.get('#grid20') - .find('.slick-header-columns:nth(0)') - .children() - .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + .find('.slick-header-columns:nth(0) .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); }); - it('should have a frozen grid with 4 containers on page load with 3 columns on the left and 4 columns on the right', () => { - cy.get('[style="transform: translateY(0px);"]').should('have.length', 2 * 2); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 3 * 2); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 8 * 2); + it('should have three top-pinned rows and left/center/right cells on page load', () => { + const row0 = '#grid20 .slick-row[data-row="0"]'; + + // Pinning uses one row node split into regions and a single docking overlay; + // it no longer duplicates rows into legacy left/right pinned canvases. + cy.get('#grid20 .slick-docking-overlay > .slick-row.slick-row-pinned-top').should('have.length', 3); + cy.get(`${row0} .slick-pinned-left-cells > .slick-cell`).should('have.length', 3); + cy.get(`${row0} .slick-scrolling-cells > .slick-cell`).should('have.length', 5); + cy.get(`${row0} .slick-pinned-right-cells > .slick-cell`).should('have.length', 1); + + cy.get(`${row0} .slick-pinned-left-cells > .slick-cell:nth(0)`).should('contain', ''); + cy.get(`${row0} .slick-pinned-left-cells > .slick-cell:nth(1)`).should('contain', 'Task 0'); + cy.get(`${row0} .slick-scrolling-cells > .slick-cell:nth(0)`).should('contain', '2009-01-01'); + cy.get(`${row0} .slick-scrolling-cells > .slick-cell:nth(1)`).should('contain', '2009-05-05'); + + cy.get(`${row0} .slick-pinned-right-cells > .slick-cell:nth(0) .cell-menu-dropdown`).should('contain', 'Action'); + }); + + it('should pin multiple columns on the right and render matching header and filter regions', () => { + setRightPinning(2); + + const row0 = '#grid20 .slick-row[data-row="0"]'; + cy.get(`${row0} .slick-pinned-right-cells > .slick-cell`).should('have.length', 2); + cy.get(`${row0} .slick-pinned-right-cells > .slick-cell.l7`).should('contain', 'Boston'); + cy.get(`${row0} .slick-pinned-right-cells > .slick-cell.l8 .cell-menu-dropdown`).should('contain', 'Action'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '0'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + cy.get('#grid20 .slick-header-columns-right .slick-header-column').should('have.length', 2); + cy.get('#grid20 .slick-header-columns-right [data-id="cityOfOrigin"]').should('contain', 'City of Origin'); + cy.get('#grid20 .slick-header-columns-right [data-id="action"]').should('contain', 'Action'); + cy.get('#grid20 .slick-headerrow-columns-right .slick-headerrow-column').should('have.length', 2); + cy.get('#grid20 .slick-headerrow-columns-right .slick-headerrow-column.l7 input').should('exist'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + setRightPinning(1); + }); + + it('should keep multiple right-pinned columns fixed while scrolling', () => { + setRightPinning(2); + // The default fixture fits in the viewport, so use the demo's wide layout + // to exercise an actual horizontal scroll rather than a no-op scroll. + cy.get('[data-test="set-large-pinned-columns"]').click(); + const actionCell = '#grid20 .slick-row[data-row="10"] .slick-pinned-right-cells .slick-cell.l8'; + + cy.get(actionCell).then(($cell) => { + const rightEdge = $cell[0].getBoundingClientRect().right; + cy.get('#grid20 .slick-horizontal-scroller').scrollTo('right'); + cy.get(actionCell).should(($scrolledCell) => { + expect(Math.abs($scrolledCell[0].getBoundingClientRect().right - rightEdge)).to.be.lessThan(2); + }); + }); + + cy.get('#grid20 .slick-horizontal-scroller').scrollTo(0, 0, { ensureScrollable: false }); + // Restore the initial fixture after exercising the wide layout so the + // following serial tests do not inherit resized columns. + cy.visit(`${Cypress.config('baseUrl')}/example20`); + }); + + it('should resize columns while left and right pinning are active', () => { + cy.visit(`${Cypress.config('baseUrl')}/example20`); + + const resizeColumn = (columnSelector: string, pinClass: string) => { + cy.get(columnSelector).should('have.class', pinClass); + cy.get(`${columnSelector} .slick-resizable-handle`) + .should('exist') + .then(($handle) => { + const header = $handle.closest('.slick-header-column')[0] as HTMLElement; + const initialWidth = header.getBoundingClientRect().width; + + cy.wrap($handle).trigger('mousedown', { which: 1, pageX: 100, clientX: 100, force: true }); + cy.get('body').trigger('mousemove', { which: 1, pageX: 125, clientX: 125, force: true }); + cy.get('body').trigger('mousemove', { which: 1, pageX: 150, clientX: 150, force: true }); + cy.get('body').trigger('mouseup', { which: 1, pageX: 150, clientX: 150, force: true }); + + cy.get(columnSelector).should(($updatedHeader) => { + expect($updatedHeader[0].getBoundingClientRect().width).to.be.greaterThan(initialWidth); + }); + }); + }; + + resizeColumn('#grid20 .slick-header-columns-left [data-id="title"]', 'slick-column-pinned-left'); + + // Use City of Origin because Action has a maxWidth of 100px and would refuse a wider resize. + setRightPinning(2); + resizeColumn('#grid20 .slick-header-columns-right [data-id="cityOfOrigin"]', 'slick-column-pinned-right'); + + // Keep the following serial tests on the demo's default configuration. + cy.visit(`${Cypress.config('baseUrl')}/example20`); }); - it('should hide "Title" column from Grid Menu and expect last frozen column to be "% Complete"', () => { - const newColumnList = [ - '#', - '% Complete', - 'Start', - 'Finish', - 'Cost | Duration', - 'Effort Driven', - 'Title 1', - 'Title 2', - 'Title 3', - 'Title 4', - ]; + it('should disable and re-enable right pinning through the numeric grid control', () => { + setRightPinning(0); + cy.get('#grid20 .slick-row[data-row="0"] .slick-pinned-right-cells').should('exist'); + cy.get('#grid20 .slick-row[data-row="0"] .slick-pinned-right-cells > .slick-cell').should('not.exist'); + cy.get('#grid20 .slick-header-columns-right .slick-header-column').should('not.exist'); + + setRightPinning(1); + cy.get('#grid20 .slick-row[data-row="0"] .slick-pinned-right-cells > .slick-cell.l8').should('contain', 'Action'); + cy.get('#grid20 .slick-header-columns-right [data-id="action"]').should('contain', 'Action'); + }); + + it('should hide "Title" column from Grid Menu and expect last pinned column to be "% Complete"', () => { + const newColumnList = ['Sel', '% Complete', 'Start', 'Finish', 'Completed', 'Cost | Duration', 'City of Origin', 'Action']; + const row0 = '#grid20 .slick-row[data-row="0"]'; cy.get('#grid20').find('button.slick-grid-menu-button').click({ force: true }); cy.get('#grid20') .get('.slick-grid-menu:visible') .find('.slick-column-picker-list') - .children('li:visible:nth(1)') + .children('li:visible:nth(0)') .children('label') .should('contain', 'Title') .click({ force: true }); cy.get('#grid20') - .find('.slick-header-columns') - .children() + .find('.slick-header-columns .slick-header-column') .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 2 * 2); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 8 * 2); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').children().should('have.length', 2); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells').children().should('have.length', 5); + cy.get('.slick-row[data-row="0"] .slick-pinned-right-cells').children().should('have.length', 1); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', ''); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(0)').should('contain', ''); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + cy.get(`${row0} .slick-scrolling-cells > .slick-cell:nth(0)`).should('contain', '2009-01-01'); + cy.get(`${row0} .slick-scrolling-cells > .slick-cell:nth(1)`).should('contain', '2009-05-05'); }); - it('should show again "Title" column from Grid Menu and expect last frozen column to still be "% Complete"', () => { + it('should show again "Title" column from Grid Menu and expect last pinned column to still be "% Complete"', () => { cy.get('#grid20') .get('.slick-grid-menu:visible') .find('.slick-column-picker-list') - .children('li:visible:nth(1)') + .children('li:visible:nth(0)') .children('label') .should('contain', 'Title') .click({ force: true }); @@ -104,77 +174,60 @@ describe('Example 20 - Frozen Grid', () => { cy.get('#grid20').get('.slick-grid-menu:visible').find('.close').click({ force: true }); cy.get('#grid20') - .find('.slick-header-columns') - .children() - .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + .find('.slick-header-columns .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 3 * 2); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 8 * 2); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').children().should('have.length', 3); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells').children().should('have.length', 5); + cy.get('.slick-row[data-row="0"] .slick-pinned-right-cells').children().should('have.length', 1); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', ''); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(0)').should('contain', ''); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(1)').should('contain', 'Task 0'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + cy.get('.slick-scrolling-cells .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.slick-scrolling-cells .slick-cell:nth(1)').should('contain', '2009-05-05'); }); - it('should hide "Title" column from Header Menu and expect last frozen column to be "% Complete"', () => { - const newColumnList = [ - '#', - '% Complete', - 'Start', - 'Finish', - 'Cost | Duration', - 'Effort Driven', - 'Title 1', - 'Title 2', - 'Title 3', - 'Title 4', - ]; + it('should hide "Title" column from Header Menu and expect last pinned column to be "% Complete"', () => { + const newColumnList = ['Sel', '% Complete', 'Start', 'Finish', 'Completed', 'Cost | Duration', 'City of Origin', 'Action']; - cy.get('#grid20') - .find('.slick-header-column:nth(1)') - .trigger('mouseover') - .children('.slick-header-menu-button') - .should('be.hidden') - .invoke('show') - .click(); + cy.get('#grid20').find('.slick-header-column:nth(1)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click(); cy.get('.slick-header-menu .slick-menu-command-list') .should('be.visible') - .children('.slick-menu-item:nth-of-type(8)') + .children('.slick-menu-item:nth-of-type(9)') .children('.slick-menu-content') .should('contain', 'Hide Column') .click(); cy.get('#grid20') - .find('.slick-header-columns') - .children() + .find('.slick-header-columns .slick-header-column') .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 2 * 2); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 8 * 2); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').children().should('have.length', 2); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells').children().should('have.length', 5); + cy.get('.slick-row[data-row="0"] .slick-pinned-right-cells').children().should('have.length', 1); + + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(0)').should('contain', ''); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', ''); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell:nth(1)').should('contain', '2009-05-05'); + }); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + it('should toggle right pinned column and expect only 2 left/center containers to be visible', () => { + cy.get('[data-test="toggle-pinned-right"]').click(); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').children().should('have.length', 2); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells').children().should('have.length', 6); + cy.get('.slick-row[data-row="0"] .slick-pinned-right-cells').should('exist').and('not.have.class', 'slick-pinned-right-cells-active'); + cy.get('.slick-row[data-row="0"] .slick-pinned-right-cells > .slick-cell').should('not.exist'); }); - it('should show again "Title" column from Column Picker and expect last frozen column to still be "% Complete"', () => { - cy.get('#grid20').find('.slick-header-column:nth(5)').trigger('mouseover').trigger('contextmenu').invoke('show'); + it('should show again "Title" column from Column Picker and expect last pinned column to still be "% Complete"', () => { + cy.get('#grid20').find('.slick-header-column:nth(4)').trigger('mouseover').trigger('contextmenu').invoke('show'); cy.get('.slick-column-picker') .find('.slick-column-picker-list') - .children('li:nth-child(2)') + .children('li:nth-of-type(2)') .children('label') .should('contain', 'Title') .click(); @@ -182,83 +235,769 @@ describe('Example 20 - Frozen Grid', () => { cy.get('.slick-column-picker:visible').find('.close').trigger('click').click(); cy.get('#grid20') - .find('.slick-header-columns') - .children() - .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + .find('.slick-header-columns .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 3 * 2); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 8 * 2); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').children().should('have.length', 3); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells').children().should('have.length', 6); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', ''); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(0)').should('contain', ''); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(1)').should('contain', 'Task 0'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell:nth(1)').should('contain', '2009-05-05'); }); - it('should click on the "Remove Frozen Columns" button to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => { - cy.get('[data-test=remove-frozen-column-button]').click({ force: true }); + it('should click on the "Remove Pinned Columns" button to switch to a regular grid view without pinned columns and expect 7 columns on the left container', () => { + cy.get('[data-test=remove-pinned-column-button]').click({ force: true }); - cy.get('[style="transform: translateY(0px);"]').should('have.length', 1 * 2); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 11 * 2); + cy.get('#grid20 .slick-row[data-row="0"]').should('have.length.at.least', 1); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').should('exist').and('not.have.class', 'slick-pinned-left-cells-active'); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell').should('have.length', 9); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '0'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + getCell(0, 0).should('contain', ''); + getCell(0, 1).should('contain', 'Task 0'); + getCell(0, 3).should('contain', '2009-01-01'); + getCell(0, 4).should('contain', '2009-05-05'); + }); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(3)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(4)').should('contain', '2009-05-05'); + it('should expect to have exact Column Header Titles in the grid', () => { + cy.get('#grid20') + .find('.slick-header-columns:nth(0) .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); }); - it('should have exact Column Header Titles in the grid', () => { + it('should click on the "Set 3 Pinned Columns" button to switch pinned columns grid and expect 3 pinned columns on the left and 4 columns on the right', () => { + cy.get('[data-test=set-3pinned-columns]').click({ force: true }); + + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').children().should('have.length', 3); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells').children().should('have.length', 6); + + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(0)').should('contain', ''); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells > .slick-cell:nth(1)').should('contain', 'Task 0'); + + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell:nth(1)').should('contain', '2009-05-05'); + }); + + it('should recheck again and still have exact Column Header Titles in the grid', () => { cy.get('#grid20') - .find('.slick-header-columns:nth(0)') - .children() - .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + .find('.slick-header-columns:nth(0) .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); }); - it('should click on the "Set 3 Frozen Columns" button to switch frozen columns grid and expect 3 frozen columns on the left and 4 columns on the right', () => { - cy.get('[data-test=set-3frozen-columns]').click({ force: true }); + it('should click on the Grid Menu command "Unpin Columns/Rows" to switch to a regular grid without pinned columns/rows', () => { + cy.get('#grid20').find('button.slick-grid-menu-button').click({ force: true }); - cy.get('[style="transform: translateY(0px);"]').should('have.length', 2 * 2); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 3 * 2); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"]') - .children() - .should('have.length', 8 * 2); + cy.contains('Unpin Columns/Rows').click({ force: true }); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '0'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + cy.get('#grid20 .slick-row[data-row="0"]').should('have.length.at.least', 1); + cy.get('.slick-row[data-row="0"] .slick-pinned-left-cells').should('exist').and('not.have.class', 'slick-pinned-left-cells-active'); + cy.get('.slick-row[data-row="0"] .slick-scrolling-cells > .slick-cell').should('have.length', 9); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-right > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + getCell(0, 0).should('contain', ''); + getCell(0, 1).should('contain', 'Task 0'); + getCell(0, 3).should('contain', '2009-01-01'); + getCell(0, 4).should('contain', '2009-05-05'); }); - it('should have exact Column Header Titles in the grid', () => { + it('should open the Cell Menu on 2nd and 3rd row and change the Effort-Driven to "True" and expect the cell to be updated and have checkmark icon', () => { + getCell(1, 1).should('contain', 'Task 1'); + getCell(1, 8).find('.checkmark-icon').should('have.length', 0); + getCell(2, 1).should('contain', 'Task 2'); + getCell(2, 8).find('.checkmark-icon').should('have.length', 0); + + getCell(1, 8).contains('Action').click({ force: true }); + cy.get('.slick-cell-menu .slick-menu-option-list .slick-menu-item').contains('True').click(); + getCell(2, 8).contains('Action').click({ force: true }); + cy.get('.slick-cell-menu .slick-menu-option-list .slick-menu-item').contains('True').click(); + + getCell(1, 5).find('.checkmark-icon').should('have.length', 1); + getCell(2, 5).find('.checkmark-icon').should('have.length', 1); + }); + + it('should open the Cell Menu on 2nd and 3rd row and change the Effort-Driven to "False" and expect the cell to be updated and no longer have checkmark', () => { + getCell(1, 5).find('.checkmark-icon').should('have.length', 1); + getCell(2, 5).find('.checkmark-icon').should('have.length', 1); + + getCell(1, 8).contains('Action').click({ force: true }); + cy.get('.slick-cell-menu .slick-menu-option-list .slick-menu-item').contains('False').click(); + getCell(2, 8).contains('Action').click({ force: true }); + cy.get('.slick-cell-menu .slick-menu-option-list .slick-menu-item').contains('False').click(); + + getCell(1, 5).find('.checkmark-icon').should('have.length', 0); + getCell(2, 5).find('.checkmark-icon').should('have.length', 0); + }); + + it('should open the Cell Menu and delete Row 3 and 4 from the Cell Menu', () => { + cy.window().then((win) => { + const stub = cy.stub(win, 'confirm').returns(true); + cy.wrap(stub).as('confirmStub'); + }); + + getCell(3, 1).should('contain', 'Task 3'); + getCell(4, 1).should('contain', 'Task 4'); + + getCell(3, 8).contains('Action').click({ force: true }); + + cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item').contains('Delete Row').click(); + cy.get('@confirmStub').should('have.been.calledWith', 'Do you really want to delete row (4) with "Task 3"?'); + getCell(3, 1).should('contain', 'Task 4'); + }); + + it.skip('should filter autocomplete by typing Vancouver in the "City of Origin" and expect only filtered rows to show up', () => { + cy.get('.search-filter.filter-cityOfOrigin').type('Vancouver'); + + cy.get('.slick-autocomplete').should('be.visible'); + cy.get('.slick-autocomplete div').should('have.length', 2); + cy.get('.slick-autocomplete').find('div:nth(0)').click(); + + getCell(0, 1).should('contain', 'Task 1'); + getCell(1, 1).should('contain', 'Task 5'); + getCell(2, 1).should('contain', 'Task 7'); + getCell(3, 1).should('contain', 'Task 9'); + getCell(4, 1).should('contain', 'Task 11'); + }); + + it('should Clear all Filters', () => { + cy.get('#grid20').find('button.slick-grid-menu-button').trigger('click').click({ force: true }); + + cy.get(`.slick-grid-menu:visible`).find('.slick-menu-item').first().find('span').contains('Clear all Filters').click(); + }); + + it.skip('should edit first row (Task 1) and change its city by choosing it inside the autocomplete editor list', () => { + getCell(0, 7).click(); + cy.get('input.autocomplete.editor-cityOfOrigin').type('Sydney'); + + cy.get('.slick-autocomplete').should('be.visible'); + cy.get('.slick-autocomplete div').should('have.length', 3); + cy.get('.slick-autocomplete').find('div:nth(1)').click(); + + getCell(0, 1).should('contain', 'Task 0'); + getCell(0, 7).should('contain', 'Sydney, NS, Australia'); + }); + + it('should open Context Menu hover "% Complete" column then select "Not Started (0%)" option and expect Task to be at 0', () => { + getCell(0, 2).rightclick(); + + cy.get('.slick-context-menu .slick-menu-option-list').should('exist').contains('Not Started (0%)').click(); + + getCell(0, 2).should('contain', '0'); + }); + + it('should reopen Context Menu hover "% Complete" column then open options sub-menu & select "Half Completed (50%)" option and expect Task to be at 50', () => { + const subOptions = ['Not Started (0%)', 'Half Completed (50%)', 'Completed (100%)']; + + getCell(0, 2).should('contain', '0'); + getCell(0, 2).rightclick(); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-option-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Sub-Options (demo)') + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-option-list').as('subMenuList'); + cy.get('@subMenuList').find('.slick-menu-title').contains('Set Percent Complete'); + cy.get('@subMenuList') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.eq(subOptions[index])); + + cy.get('@subMenuList').find('.slick-menu-item .slick-menu-content').contains('Half Completed (50%)').click(); + + getCell(0, 2).should('contain', '50'); + }); + + it('should be able to open Context Menu and click on Export->Text and expect alert triggered with Text Export', () => { + const subCommands1 = ['Text', 'Excel']; + cy.window().then((win) => { + cy.stub(win, 'alert').as('alertStub'); + }); + + getCell(0, 2).should('contain', '0'); + getCell(0, 2).rightclick(); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains(/^Exports$/) + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list').find('.slick-menu-item').contains('Text').click(); + cy.get('@alertStub').should('have.been.calledWith', 'Exporting as Text (tab delimited)'); + }); + + it('should be able to open Context Menu and click on Export->Excel-> sub-commands to see 1 context menu + 1 sub-menu then clicking on Text should call alert action', () => { + const subCommands1 = ['Text', 'Excel']; + const subCommands2 = ['Excel (csv)', 'Excel (xlsx)']; + cy.window().then((win) => { + cy.stub(win, 'alert').as('alertStub'); + }); + + getCell(0, 2).should('contain', '0'); + getCell(0, 2).rightclick(); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains(/^Exports$/) + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Excel') + .click(); + + cy.get('.slick-context-menu.slick-menu-level-2 .slick-menu-command-list').as('subMenuList2'); + + cy.get('@subMenuList2').find('.slick-menu-title').contains('available formats'); + + cy.get('@subMenuList2') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands2[index])); + + cy.get('.slick-context-menu.slick-menu-level-2 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Excel (xlsx)') + .click(); + cy.get('@alertStub').should('have.been.calledWith', 'Exporting as Excel (xlsx)'); + }); + + it('should open Export->Excel sub-menu & open again Sub-Options on top and expect sub-menu to be recreated with that Sub-Options list instead of the Export->Excel list', () => { + const subCommands1 = ['Text', 'Excel']; + const subCommands2 = ['Excel (csv)', 'Excel (xlsx)']; + const subOptions = ['Not Started (0%)', 'Half Completed (50%)', 'Completed (100%)']; + + getCell(0, 2).should('contain', '0'); + getCell(0, 2).rightclick(); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains(/^Exports$/) + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Excel') + .click(); + + cy.get('.slick-context-menu.slick-menu-level-2 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands2[index])); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-option-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Sub-Options') + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-option-list').as('optionSubList2'); + + cy.get('@optionSubList2').find('.slick-menu-title').contains('Set Percent Complete'); + + cy.get('@optionSubList2') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($option, index) => expect($option.text()).to.contain(subOptions[index])); + }); + + it('should open Export->Excel context sub-menu then open Feedback->ContactUs sub-menus and expect previous Export menu to no longer exists', () => { + const subCommands1 = ['Text', 'Excel']; + const subCommands2 = ['Request update from supplier', '', 'Contact Us']; + const subCommands2_1 = ['Email us', 'Chat with us', 'Book an appointment']; + + cy.window().then((win) => { + cy.stub(win, 'alert').as('alertStub'); + }); + + getCell(0, 2).should('contain', '0'); + getCell(0, 2).rightclick({ force: true }); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains(/^Exports$/) + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + // click different sub-menu + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Feedback') + .should('exist') + .click(); + + cy.get('.slick-submenu').should('have.length', 1); + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands2[index])); + + // click on Feedback->ContactUs + cy.get('.slick-context-menu.slick-menu-level-1.dropright') // right align + .find('.slick-menu-item .slick-menu-content') + .contains('Contact Us') + .should('exist') + .trigger('mouseover'); // mouseover or click should work + + cy.get('.slick-submenu').should('have.length', 2); + cy.get('.slick-context-menu.slick-menu-level-2.dropright') // right align + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.eq(subCommands2_1[index])); + + cy.get('.slick-context-menu.slick-menu-level-2'); + + cy.get('.slick-context-menu.slick-menu-level-2 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Chat with us') + .click(); + cy.get('@alertStub').should('have.been.calledWith', 'Command: contact-chat'); + + cy.get('.slick-submenu').should('have.length', 0); + }); + + it('should toggle Select All checkbox and expect back "Sel" column title to show when Select All checkbox is shown in the header row', () => { + cy.get('.slick-header-column:nth(0)').find('.slick-column-name').should('contain', 'Sel'); + cy.get('.slick-header-columns .slick-header-column').each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); + + cy.get('[data-test="toggle-select-all-row"]').click(); + + cy.get('.slick-header-column:nth(0)').find('.slick-column-name').should('not.contain', 'Sel'); + + cy.get('.slick-header-columns .slick-header-column').each(($child, index) => expect($child.text()).to.eq(withoutTitleRowTitles[index])); + }); + + it('should toggle back Select All checkbox and expect back "Sel" column title to show when Select All checkbox is shown in the header row', () => { + cy.get('[data-test="toggle-select-all-row"]').click(); + + cy.get('.slick-header-column:nth(0)').find('.slick-column-name').should('contain', 'Sel'); + cy.get('.slick-header-columns .slick-header-column').each(($child, index) => expect($child.text()).to.eq(withTitleRowTitles[index])); + + cy.get('[data-test="toggle-select-all-row"]').click(); + + cy.get('.slick-header-column:nth(0)').find('.slick-column-name').should('not.contain', 'Sel'); + + cy.get('.slick-header-columns .slick-header-column').each(($child, index) => expect($child.text()).to.eq(withoutTitleRowTitles[index])); + }); + + it('should open Column Picker and try unchecked all the columns on the right of the column pinning and expect an error and abort of the execution', () => { + cy.window().then((win) => { + cy.stub(win, 'alert').as('alertStub'); + }); + cy.get('[data-test=set-3pinned-columns]').click({ force: true }); + + const leftColumns = ['', 'Title', '% Complete']; + const rightColumns = ['Start', 'Finish', 'Completed', 'Cost | Duration', 'City of Origin', 'Action']; + cy.get('#grid20').find('.slick-header-column').first().trigger('mouseover').trigger('contextmenu').invoke('show'); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children() + .each(($child, index) => { + if (index >= leftColumns.length) { + if ($child.text() === rightColumns[index - leftColumns.length]) { + expect($child.text()).to.eq(rightColumns[index - leftColumns.length]); + if (index <= rightColumns.length + 1) { + cy.wrap($child).children('label').click(); + } else { + cy.wrap($child) + .children('label') + .click() + .then(() => { + cy.get('@alertStub').should( + 'have.been.calledWith', + '[SlickGrid] Action not allowed and aborted, you need to have at least one or more column in the center section of the grid. ' + + 'You could alternatively unpin columns before trying again.' + ); + }); + } + } + } + }); + + cy.get('button[data-dismiss="slick-column-picker"]').click(); + }); + + it('should also not be able to "Hide Column" via the Header Menu', () => { + cy.window().then((win) => { + cy.stub(win, 'alert').as('alertStub'); + }); + const newColumnList = ['', 'Title', '% Complete', 'Action']; + + cy.get('#grid20').find('.slick-header-column:nth(3)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item') + .contains('Hide Column') + .click() + .then(() => { + cy.get('@alertStub').should( + 'have.been.calledWith', + '[SlickGrid] Action not allowed and aborted, you need to have at least one or more column in the center section of the grid. ' + + 'You could alternatively unpin columns before trying again.' + ); + }); + cy.get('#grid20') - .find('.slick-header-columns:nth(0)') + .find('.slick-header-columns .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); + }); + + it('should be able to uncheck "Title" column without any alert', () => { + cy.get('#grid20').find('.slick-header-column').first().trigger('mouseover').trigger('contextmenu').invoke('show'); + const updatedColumns = ['', '% Complete', 'Action']; + cy.get('.slick-column-picker-list li:not(.hidden) .checkbox-picker-label').first().click(); + cy.get('.slick-column-picker:visible').find('.close').trigger('click').click(); + cy.get('.slick-header-columns:nth(0) .slick-header-column').each(($child, index) => expect($child.text()).to.eq(updatedColumns[index])); + }); + + it('should be able to add back hidden "Title" column without any alert', () => { + const updatedColumns = ['', 'Title', '% Complete', 'Action']; + cy.get('.slick-header-column').first().trigger('mouseover').trigger('contextmenu').invoke('show'); + cy.get('.slick-column-picker-list li:not(.hidden) .checkbox-picker-label').first().click(); + cy.get('.slick-column-picker:visible').find('.close').trigger('click').click(); + cy.get('.slick-header-columns:nth(0) .slick-header-column').each(($child, index) => expect($child.text()).to.eq(updatedColumns[index])); + }); + + it('should reset hidden column from the Column Picker and expect all columns to be back', () => { + const leftColumns = ['', 'Title', '% Complete']; + const rightColumns = ['Start', 'Finish', 'Completed', 'Cost | Duration', 'City of Origin', 'Action']; + + cy.get('.slick-header-column').first().trigger('mouseover').trigger('contextmenu').invoke('show'); + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') .children() - .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + .each(($child, index) => { + if (index >= leftColumns.length) { + if ($child.text() === rightColumns[index - leftColumns.length]) { + expect($child.text()).to.eq(rightColumns[index - leftColumns.length]); + if (index <= rightColumns.length + 1) { + cy.wrap($child).children('label').click(); + } + } + } + }); + + cy.get('.slick-column-picker:visible').find('.close').trigger('click').click(); + + cy.get('#grid20') + .find('.slick-header-columns .slick-header-column') + .each(($child, index) => expect($child.text()).to.eq(withoutTitleRowTitles[index])); }); - it('should click on the Grid Menu command "Unfreeze Columns/Rows" to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => { - cy.get('#grid20').find('button.slick-grid-menu-button').click({ force: true }); + describe('Test UI rendering after Scrolling with large columns', () => { + it('should unpin all columns/rows', () => { + cy.get('#grid20').find('button.slick-grid-menu-button').click({ force: true }); + + cy.contains('Unpin Columns/Rows').click({ force: true }); + }); + + it('should resize all columns and make them wider', () => { + // resize CityOfOrigin column + cy.get('.slick-header-columns .slick-header-column:nth(7)').should('contain', 'City of Origin'); + + cy.get('.slick-resizable-handle:nth(7)').trigger('mousedown', { which: 1, force: true }).trigger('mousemove', 'bottomRight'); + + cy.get('.slick-header-column:nth(8)') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); - cy.contains('Unfreeze Columns/Rows').click({ force: true }); + // resize Cost|Duration column + cy.get('.slick-header-columns .slick-header-column:nth(6)').should('contain', 'Cost | Duration'); - cy.get('[style="transform: translateY(0px);"]').should('have.length', 1); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"]').children().should('have.length', 11); + cy.get('.slick-resizable-handle:nth(6)').trigger('mousedown', { which: 1, force: true }).trigger('mousemove', 'bottomRight'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(0)').should('contain', '0'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + cy.get('.slick-header-column:nth(8)') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + + // resize Completed column + cy.get('.slick-header-columns .slick-header-column:nth(5)').should('contain', 'Completed'); + + cy.get('.slick-resizable-handle:nth(5)').trigger('mousedown', { which: 1, force: true }).trigger('mousemove', 'bottomRight'); + + cy.get('.slick-header-column:nth(7)') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + + // resize Finish column + cy.get('.slick-header-columns .slick-header-column:nth(4)').should('contain', 'Finish'); + + cy.get('.slick-resizable-handle:nth(4)').trigger('mousedown', { which: 1, force: true }).trigger('mousemove', 'bottomRight'); + + cy.get('.slick-header-column:nth(6)') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + + // resize Start column + cy.get('.slick-header-columns .slick-header-column:nth(3)').should('contain', 'Start'); + + cy.get('.slick-resizable-handle:nth(3)').trigger('mousedown', { which: 1, force: true }).trigger('mousemove', 'bottomRight'); + + cy.get('.slick-header-column:nth(6)') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + + // resize %Complete column + cy.get('.slick-header-columns .slick-header-column:nth(2)').should('contain', '% Complete'); + + cy.get('.slick-resizable-handle:nth(2)').trigger('mousedown', { which: 1, force: true }).trigger('mousemove', 'bottomRight'); + + cy.get('.slick-header-column:nth(3)') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + + // resize Title column + cy.get('.slick-header-columns .slick-header-column:nth(1)').should('contain', 'Title'); + + cy.get('.slick-resizable-handle:nth(1)').trigger('mousedown', { which: 1, force: true }).trigger('mousemove', 'bottomRight'); + + cy.get('.slick-header-column:nth(3)') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + }); + + it('should scroll horizontally completely to the right and expect all cell to be rendered', () => { + getCell(2, 1).contains(/Task [0-9]*/); + getCell(2, 2).contains(/[0-9]*/); + + getCell(15, 1).contains(/Task [0-9]*/); + getCell(15, 2).contains(/[0-9]*/); + + // horizontal scroll to right + // Pinning has one real horizontal scroll owner. Scrolling the old body + // viewport only exercises the compatibility bridge; target the proxy + // here to verify the user-facing scrollbar and all chrome move together. + cy.get('#grid20 .slick-horizontal-scroller').scrollTo('100%', '0%', { duration: 1500 }); + getCell(2, 3).should('contain', '2009-01-01'); + getCell(2, 4).should('contain', '2009-05-05'); + getCell(2, 7).contains(/[United State|Canada]*/); + getCell(2, 8).should('contain', 'Action'); + + getCell(15, 3).should('contain', '2009-01-01'); + getCell(15, 4).should('contain', '2009-05-05'); + getCell(15, 7).contains(/[United State|Canada]*/); + getCell(15, 8).should('contain', 'Action'); + }); + + it('should scroll vertically to the middle of the grid and expect all cell to be rendered', () => { + // vertical scroll to middle + cy.get('.slick-vertical-scroller').scrollTo('0%', '40%', { duration: 1500 }); + + getCell(200, 3).should('contain', '2009-01-01'); + getCell(200, 4).should('contain', '2009-05-05'); + getCell(200, 7).contains(/[United State|Canada]*/); + getCell(200, 8).should('contain', 'Action'); + + getCell(205, 3).should('contain', '2009-01-01'); + getCell(205, 4).should('contain', '2009-05-05'); + getCell(205, 7).contains(/[United State|Canada]*/); + getCell(205, 8).should('contain', 'Action'); + + // reset scroll + cy.get('.slick-vertical-scroller').scrollTo(0, 0, { ensureScrollable: false }); + cy.get('.slick-horizontal-scroller').scrollTo(0, 0, { ensureScrollable: false }); + }); + }); + + describe('accessibility sub-menus tests', () => { + beforeEach(() => { + // Open the context menu on a cell to start each test + cy.get('.slick-vertical-scroller').scrollTo(0, 0, { ensureScrollable: false }); + cy.get('.slick-horizontal-scroller').scrollTo(0, 0, { ensureScrollable: false }); + cy.get('[data-row="0"] .slick-cell.l3.r3').rightclick({ force: true }); + cy.get('.slick-context-menu.slick-menu-level-0').should('be.visible'); + }); + + it('should open Exports sub-menu with ArrowRight, then Excel sub-menu with ArrowRight, and close with ArrowLeft', () => { + // Move down to "Exports" (4th item) + cy.focused(); + cy.press(Cypress.Keyboard.Keys.DOWN); + cy.press(Cypress.Keyboard.Keys.DOWN); + cy.get('.slick-context-menu.slick-menu-level-0 .slick-submenu-item[data-command="export"]').should('have.focus'); + + // Open "Exports" sub-menu with ArrowRight + cy.focused().type('{rightarrow}'); + cy.get('.slick-context-menu.slick-menu-level-1[data-sub-menu-parent="export"]').should('be.visible'); + + // Move down to "Excel" (2nd item in sub-menu) + cy.focused().type('{downarrow}'); + cy.get('.slick-context-menu.slick-menu-level-1 .slick-submenu-item[data-command="sub-menu"]').should('have.focus'); + + // Open "Excel" sub-menu with ArrowRight + cy.focused().type('{rightarrow}'); + cy.get('.slick-context-menu.slick-menu-level-2[data-sub-menu-parent="sub-menu"]').should('be.visible'); + + // Move down to "Excel (xlsx)" (2nd item in Excel sub-menu) + cy.focused().type('{downarrow}'); + cy.get('.slick-context-menu.slick-menu-level-2 .slick-menu-item[data-command="exports-xlsx"]').should('have.focus'); + + // Close Excel sub-menu with ArrowLeft + cy.focused().type('{leftarrow}'); + cy.get('.slick-context-menu.slick-menu-level-2').should('not.exist'); + cy.get('.slick-context-menu.slick-menu-level-1 .slick-submenu-item[data-command="sub-menu"]').should('have.focus'); + + // close all context menus + cy.get('[data-row="0"] .slick-cell.l0.r0').click(); + }); + + it('should open sub-menus using Enter as well as ArrowRight', () => { + // Move down to "Exports" + cy.focused(); + cy.press(Cypress.Keyboard.Keys.DOWN); + cy.press(Cypress.Keyboard.Keys.DOWN); + cy.get('.slick-context-menu.slick-menu-level-0 .slick-submenu-item[data-command="export"]').should('have.focus'); + + // Open "Exports" sub-menu with Enter + cy.focused().type('{enter}'); + cy.get('.slick-context-menu.slick-menu-level-1[data-sub-menu-parent="export"]').should('be.visible'); + + // Move down to "Excel" + cy.focused().type('{downarrow}'); + cy.get('.slick-context-menu.slick-menu-level-1 .slick-submenu-item[data-command="sub-menu"]').should('have.focus'); + + // Open "Excel" sub-menu with Enter + cy.focused().type('{enter}'); + cy.get('.slick-context-menu.slick-menu-level-2[data-sub-menu-parent="sub-menu"]').should('be.visible'); + + // close all context menus + cy.get('[data-row="0"] .slick-cell.l0.r0').click(); + }); + + it('should activate a sub-menu leaf item with Enter', () => { + // Move down to "Exports" + cy.window().then((win) => { + cy.stub(win, 'alert').as('alertStub'); + }); + cy.focused(); + cy.press(Cypress.Keyboard.Keys.DOWN); + cy.press(Cypress.Keyboard.Keys.DOWN); + cy.press(Cypress.Keyboard.Keys.ENTER); + cy.get('.slick-context-menu.slick-menu-level-1[data-sub-menu-parent="export"]').should('be.visible'); + + // "Text (tab delimited)" is first item, should have focus + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-item[data-command="exports-txt"]').should('have.focus'); + // Activate with Enter (add your assertion for the result) + cy.focused().type('{enter}'); + }); + + it('should reapply 3 Pinned Columns and expect to be able to focus on first filter and go left/right between both viewports without problems', () => { + cy.get('[data-test="set-3pinned-columns"]').click(); + cy.get('.slick-headerrow-column.l1 input').focus(); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.get('.slick-headerrow-column.l2 input').should('have.focus'); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.get('.slick-headerrow-column.l3 select').should('have.focus'); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.get('.slick-headerrow-column.l3 input').should('have.focus'); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.get('.slick-headerrow-column.l4 select').should('have.focus'); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.get('.slick-headerrow-column.l4 input').should('have.focus'); + + // Shift+Tab dosn't work in Cypress, so we can't go further with tests + }); + }); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(3)').should('contain', '2009-01-01'); - cy.get('.grid-canvas-left > [style="transform: translateY(0px);"] > .slick-cell:nth(4)').should('contain', '2009-05-05'); + describe('drag & drop column reordering with auto-scroll', () => { + it('should auto-scroll right viewport and reorder columns when "Start" is dragged well past the right edge (ending up after "Finish")', () => { + // Close the context menu opened by beforeEach + cy.get('body').type('{esc}'); + + // Control app timers to make drag auto-scroll deterministic in CI. + cy.clock(); + + // Normalize right viewport scroll so this test is isolated from previous test state. + cy.get('.slick-horizontal-scroller').then(($viewport) => { + $viewport[0].scrollLeft = 0; + $viewport[0].dispatchEvent(new Event('scroll', { bubbles: true })); + }); + cy.get('.slick-horizontal-scroller').its('0.scrollLeft').should('equal', 0); + cy.get('[data-test="set-large-pinned-columns"]').click(); + + // Step 1: call SortableJS onStart for the "Start" column (1st center-section column). + // This binds the document 'drag' auto-scroll listener for the shared proxy scrollbar. + cy.get('.slick-header-columns-center').then(($rightHeader) => { + let sortInstance: any; + Object.keys($rightHeader[0]).forEach((prop) => { + if (prop.startsWith('Sortable')) { + sortInstance = ($rightHeader[0] as any)[prop]; + } + }); + expect(sortInstance).to.exist; + const startColumnEl = $rightHeader[0].querySelectorAll('.slick-header-column')[0] as HTMLElement; + sortInstance.options.onStart({ item: startColumnEl }); + }); + + // Step 2: fire a document drag event well past the right edge (viewport-relative) + // to avoid CI flakiness caused by environment-dependent viewport widths. + cy.window().then((win) => { + const dragX = win.innerWidth + 1200; + cy.document().trigger('drag', { pageX: dragX, clientX: dragX, clientY: 50 }); + }); + + // Step 3: advance mocked time so the 30ms scroll interval ticks several times. + cy.tick(350); + + // Auto-scroll should have moved the right viewport to the right + cy.get('.slick-horizontal-scroller').its('0.scrollLeft').should('be.greaterThan', 0); + + // Step 4: simulate the drag result — "Start" was moved to the right, past "Finish". + // SortableJS reads the DOM order via toArray() inside onEnd, so physically reorder the children first. + cy.get('.slick-header-columns-center').then(($rightHeader) => { + let sortInstance: any; + Object.keys($rightHeader[0]).forEach((prop) => { + if (prop.startsWith('Sortable')) { + sortInstance = ($rightHeader[0] as any)[prop]; + } + }); + expect(sortInstance).to.exist; + const startColumnEl = $rightHeader[0].querySelector('[data-id="start"]') as HTMLElement; + const finishColumnEl = $rightHeader[0].querySelector('[data-id="finish"]') as HTMLElement; + expect(startColumnEl).to.exist; + expect(finishColumnEl).to.exist; + + // Move "Finish" before "Start" → mirrors dragging Start past Finish + $rightHeader[0].insertBefore(finishColumnEl, startColumnEl); + + // onEnd reads the new DOM order via toArray() and calls setColumns() if the order changed + sortInstance.options.onEnd({ item: startColumnEl, stopPropagation: () => {} }); + }); + + // The center region should now place Finish before Start. The exact + // region membership can vary when a large requested pin band does not + // fit the current viewport, so assert semantic order by column id. + cy.get('.slick-header-column').then(($headers) => { + const ids = [...$headers].map((header) => header.dataset.id); + expect(ids.indexOf('finish')).to.be.lessThan(ids.indexOf('start')); + }); + + // When a left band is active, its order must remain unchanged. + cy.get('.slick-header-columns-left').then(($leftRegion) => { + const left = $leftRegion.find('.slick-header-column'); + if (left.length) { + expect([...left].map((header) => header.dataset.id)).to.deep.equal(['_checkbox_selector', 'title', 'percentComplete']); + } + }); + }); }); }); diff --git a/demos/react/test/cypress/e2e/example24.cy.ts b/demos/react/test/cypress/e2e/example24.cy.ts index c2ea74764b..b73ab056b1 100644 --- a/demos/react/test/cypress/e2e/example24.cy.ts +++ b/demos/react/test/cypress/e2e/example24.cy.ts @@ -232,7 +232,7 @@ describe('Example 24 - Cell Menu & Context Menu Plugins', () => { }); it('should check Context Menu "menuUsabilityOverride" condition and expect to not be able to open Context Menu from rows than are >= to Task 21', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('bottom').wait(25); + cy.get('.slick-vertical-scroller').scrollTo('bottom').wait(25); cy.get('#grid24').find('.slick-row:nth(3) .slick-cell:nth(1)').rightclick({ force: true }); @@ -240,7 +240,7 @@ describe('Example 24 - Cell Menu & Context Menu Plugins', () => { }); it('should scroll back to top row and be able to open Context Menu', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top').wait(25); + cy.get('.slick-vertical-scroller').scrollTo('top').wait(25); cy.get('#grid24').find('.slick-row:nth(1) .slick-cell:nth(1)').rightclick({ force: true }); diff --git a/demos/react/test/cypress/e2e/example27.cy.ts b/demos/react/test/cypress/e2e/example27.cy.ts index 74b4750874..a6a3c1b699 100644 --- a/demos/react/test/cypress/e2e/example27.cy.ts +++ b/demos/react/test/cypress/e2e/example27.cy.ts @@ -157,7 +157,7 @@ describe('Example 27 - Tree Data (from a flat dataset with parentId references)' it('should be able to update the 1st row item (Task 0)', () => { cy.get('[data-test=update-item-btn]').contains('Update 1st Row Item').click(); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); const now = new Date(); const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; @@ -198,7 +198,7 @@ describe('Example 27 - Tree Data (from a flat dataset with parentId references)' cy.get(`.slick-grid-menu:visible`).find('.slick-menu-item').first().find('span').contains('Clear all Filters').click(); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); }); it('should be able to open "Task 1" and "Task 3" parents', () => { diff --git a/demos/react/test/cypress/e2e/example28.cy.ts b/demos/react/test/cypress/e2e/example28.cy.ts index 8f8f5e2228..123830953d 100644 --- a/demos/react/test/cypress/e2e/example28.cy.ts +++ b/demos/react/test/cypress/e2e/example28.cy.ts @@ -84,7 +84,7 @@ describe('Example 28 - Tree Data (from a Hierarchical Dataset)', () => { it('should expand "pdf" folder and expect all folders to be expanded', () => { cy.get('[data-row="4"] > .slick-cell:nth(0) .slick-group-toggle.collapsed').click(); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top', { force: true } as any); + cy.get('.slick-vertical-scroller').scrollTo('top', { force: true } as any); }); it('should have default Files list', () => { @@ -97,7 +97,7 @@ describe('Example 28 - Tree Data (from a Hierarchical Dataset)', () => { }); it('should have pop songs folder with aggregations of Sum(53.3MB) / Avg(26.65MB)', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('center', { force: true } as any); + cy.get('.slick-vertical-scroller').scrollTo('center', { force: true } as any); cy.get('[data-row="16"] > .slick-cell:nth(0)').should('contain', 'music'); cy.get('[data-row="16"] > .slick-cell:nth(3)').should('contain', 'sum: 151.3 MB / avg: 50.43 MB'); @@ -118,7 +118,7 @@ describe('Example 28 - Tree Data (from a Hierarchical Dataset)', () => { }); it('should have pop songs folder with updated aggregations including new pop songs of Sum(218.3MB) / Avg(54.58MB)', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('bottom', { force: true } as any); + cy.get('.slick-vertical-scroller').scrollTo('bottom', { force: true } as any); cy.get('[data-row="16"] > .slick-cell:nth(0)').should('contain', 'music'); cy.get('[data-row="16"] > .slick-cell:nth(3)').should('contain', 'sum: 316.3 MB / avg: 63.26 MB'); @@ -225,7 +225,7 @@ describe('Example 28 - Tree Data (from a Hierarchical Dataset)', () => { }); it('should have pop songs folder with updated aggregations including 4 pop songs of Sum(400.3MB) / Avg(66.72MB)', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('bottom', { force: true } as any); + cy.get('.slick-vertical-scroller').scrollTo('bottom', { force: true } as any); cy.get('[data-row="16"] > .slick-cell:nth(0)').should('contain', 'music'); cy.get('[data-row="16"] > .slick-cell:nth(3)').should('contain', 'sum: 400.3 MB / avg: 66.72 MB'); @@ -308,7 +308,7 @@ describe('Example 28 - Tree Data (from a Hierarchical Dataset)', () => { }); it('should have again the pop songs folder with updated aggregations including 4 pop songs of Sum(400.3MB) / Avg(66.72MB)', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('center', { force: true } as any); + cy.get('.slick-vertical-scroller').scrollTo('center', { force: true } as any); cy.get('[data-row="16"] > .slick-cell:nth(0)').should('contain', 'music'); cy.get('[data-row="16"] > .slick-cell:nth(3)').should('contain', 'sum: 400.3 MB / avg: 66.72 MB'); @@ -336,7 +336,7 @@ describe('Example 28 - Tree Data (from a Hierarchical Dataset)', () => { }); it('should have pop songs folder with aggregation reflecting what is displayed, Sum(316.3MB) / Avg(63.26MB)', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('center', { force: true } as any); + cy.get('.slick-vertical-scroller').scrollTo('center', { force: true } as any); cy.get('[data-row="16"] > .slick-cell:nth(0)').should('contain', 'music'); cy.get('[data-row="16"] > .slick-cell:nth(3)').should('contain', 'sum: 316.3 MB / avg: 63.26 MB'); @@ -347,7 +347,7 @@ describe('Example 28 - Tree Data (from a Hierarchical Dataset)', () => { }); it('should have documents with same Sum as the beginning since auto-recalc is disabled, aggregation should be Sum(14.46MB) / Avg(1.45MB)', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top', { force: true } as any); + cy.get('.slick-vertical-scroller').scrollTo('top', { force: true } as any); cy.get('[data-row="1"] > .slick-cell:nth(0)').should('contain', 'documents'); cy.get('[data-row="1"] > .slick-cell:nth(3)').should('contain', 'sum: 14.46 MB / avg: 1.45 MB (total)'); diff --git a/demos/react/test/cypress/e2e/example38.cy.ts b/demos/react/test/cypress/e2e/example38.cy.ts index a179cdc5cb..02b700180d 100644 --- a/demos/react/test/cypress/e2e/example38.cy.ts +++ b/demos/react/test/cypress/e2e/example38.cy.ts @@ -20,7 +20,7 @@ describe('Example 38 - Infinite Scroll with OData', () => { it('should scroll to bottom of the grid and expect next batch of 30 items appended to current dataset for a total of 60 items', () => { cy.get('[data-test="itemCount"]').should('have.text', '30'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '60'); @@ -32,7 +32,7 @@ describe('Example 38 - Infinite Scroll with OData', () => { it('should scroll to bottom of the grid and expect next batch of 30 items appended to current dataset for a new total of 90 items', () => { cy.get('[data-test="itemCount"]').should('have.text', '60'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '90'); @@ -48,7 +48,7 @@ describe('Example 38 - Infinite Scroll with OData', () => { cy.get('[data-test="data-loaded-tag"]').should('not.have.class', 'fully-loaded'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '100'); @@ -78,7 +78,7 @@ describe('Example 38 - Infinite Scroll with OData', () => { it('should scroll to bottom again and expect next batch of 30 items appended to current dataset for a total of 60 items', () => { cy.get('[data-test="itemCount"]').should('have.text', '30'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '60'); @@ -104,7 +104,7 @@ describe('Example 38 - Infinite Scroll with OData', () => { it('should scroll to bottom again and expect next batch to be only 20 females appended to current dataset for a total of 50 items found in DB', () => { cy.get('[data-test="itemCount"]').should('have.text', '30'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '50'); @@ -119,7 +119,7 @@ describe('Example 38 - Infinite Scroll with OData', () => { cy.get('[data-test="itemCount"]').should('have.text', '30'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('[data-test=odata-query-result]').should(($span) => { expect($span.text()).to.eq(`$count=true&$top=30`); @@ -130,11 +130,11 @@ describe('Example 38 - Infinite Scroll with OData', () => { }); it('should scroll to the bottom "Group by Gender" and expect 30 more items for a total of 60 items grouped', () => { - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '60'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('[data-test=odata-query-result]').should(($span) => { expect($span.text()).to.eq(`$count=true&$top=30&$skip=30`); diff --git a/demos/react/test/cypress/e2e/example39.cy.ts b/demos/react/test/cypress/e2e/example39.cy.ts index d7091726f6..a87b8ef667 100644 --- a/demos/react/test/cypress/e2e/example39.cy.ts +++ b/demos/react/test/cypress/e2e/example39.cy.ts @@ -29,7 +29,7 @@ describe('Example 39 - Infinite Scroll with GraphQL', () => { it('should scroll to bottom of the grid and expect next batch of 30 items appended to current dataset for a total of 60 items', () => { cy.get('[data-test="itemCount"]').should('have.text', '30'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '60'); @@ -44,7 +44,7 @@ describe('Example 39 - Infinite Scroll with GraphQL', () => { it('should scroll to bottom of the grid and expect next batch of 30 items appended to current dataset for a new total of 90 items', () => { cy.get('[data-test="itemCount"]').should('have.text', '60'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '90'); @@ -63,7 +63,7 @@ describe('Example 39 - Infinite Scroll with GraphQL', () => { cy.get('[data-test="data-loaded-tag"]').should('not.have.class', 'fully-loaded'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '100'); @@ -100,7 +100,7 @@ describe('Example 39 - Infinite Scroll with GraphQL', () => { it('should scroll to bottom again and expect next batch of 30 items appended to current dataset for a total of 60 items', () => { cy.get('[data-test="itemCount"]').should('have.text', '30'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '60'); @@ -135,7 +135,7 @@ describe('Example 39 - Infinite Scroll with GraphQL', () => { it('should scroll to bottom again and expect next batch to be only 20 females appended to current dataset for a total of 50 items found in DB', () => { cy.get('[data-test="itemCount"]').should('have.text', '30'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom'); + cy.get('.slick-vertical-scroller').scrollTo('bottom'); cy.get('[data-test="itemCount"]').should('have.text', '50'); diff --git a/demos/react/test/cypress/e2e/example40.cy.ts b/demos/react/test/cypress/e2e/example40.cy.ts index 4f8eeb96e2..4ef56ccdec 100644 --- a/demos/react/test/cypress/e2e/example40.cy.ts +++ b/demos/react/test/cypress/e2e/example40.cy.ts @@ -25,14 +25,14 @@ describe('Example 40 - Infinite Scroll from JSON data', () => { it('should scroll to bottom of the grid and expect next batch of 50 items appended to current dataset for a total of 100 items', () => { cy.get('[data-test="totalItemCount"]').should('have.text', '50'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom', { timeout: 100 }); + cy.get('.slick-vertical-scroller').scrollTo('bottom', { timeout: 100 }); cy.get('[data-test="totalItemCount"]').should('have.text', '100'); }); it('should scroll to bottom of the grid again and expect 50 more items for a total of now 150 items', () => { cy.get('[data-test="totalItemCount"]').should('have.text', '100'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom', { timeout: 100 }); + cy.get('.slick-vertical-scroller').scrollTo('bottom', { timeout: 100 }); cy.get('[data-test="totalItemCount"]').should('have.text', '150'); }); @@ -42,7 +42,7 @@ describe('Example 40 - Infinite Scroll from JSON data', () => { cy.get('[data-id="title"]').click(); cy.get('[data-test="totalItemCount"]').should('have.text', '150'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('[data-row="0"] > .slick-cell:nth(0)').should('contain', 'Task 0'); cy.get('[data-row="1"] > .slick-cell:nth(0)').should('contain', 'Task 1'); @@ -55,7 +55,7 @@ describe('Example 40 - Infinite Scroll from JSON data', () => { cy.get('[data-id="title"]').click(); cy.get('[data-test="totalItemCount"]').should('have.text', '50'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('[data-row="0"] > .slick-cell:nth(0)').should('contain', 'Task 9'); cy.get('[data-row="1"] > .slick-cell:nth(0)').should('contain', 'Task 8'); @@ -68,17 +68,17 @@ describe('Example 40 - Infinite Scroll from JSON data', () => { cy.get('[data-test="totalItemCount"]').should('have.text', '50'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('[data-row="0"] > .slick-cell:nth(0) .slick-group-toggle.expanded').should('have.length', 1); cy.get('[data-row="0"] > .slick-cell:nth(0) .slick-group-title').contains(/Duration: [0-9]/); }); it('should scroll to the bottom "Group by Duration" and expect 50 more items for a total of 100 items grouped', () => { - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom', { timeout: 100 }); + cy.get('.slick-vertical-scroller').scrollTo('bottom', { timeout: 100 }); cy.get('[data-test="totalItemCount"]').should('have.text', '100'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('[data-row="0"] > .slick-cell:nth(0) .slick-group-toggle.expanded').should('have.length', 1); cy.get('[data-row="0"] > .slick-cell:nth(0) .slick-group-title').contains(/Duration: [0-9]/); }); @@ -103,12 +103,12 @@ describe('Example 40 - Infinite Scroll from JSON data', () => { it('should load 200 items and filter "Start" column with <=2020-08-25', () => { cy.get('[data-test="set-dynamic-filter"]').click(); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom', { timeout: 100 }); + cy.get('.slick-vertical-scroller').scrollTo('bottom', { timeout: 100 }); cy.get('[data-test="totalItemCount"]').should('have.text', '150'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('bottom', { timeout: 100 }); + cy.get('.slick-vertical-scroller').scrollTo('bottom', { timeout: 100 }); cy.get('[data-test="totalItemCount"]').should('have.text', '200'); - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get(`[data-row=0] > .slick-cell:nth(3)`).contains(/^Aug [0-9]{2}, 2020$/); cy.get(`[data-row=1] > .slick-cell:nth(3)`).contains(/^Aug [0-9]{2}, 2020$/); diff --git a/demos/react/test/cypress/e2e/example43.cy.ts b/demos/react/test/cypress/e2e/example43.cy.ts index c184db9802..ca0c3b17cb 100644 --- a/demos/react/test/cypress/e2e/example43.cy.ts +++ b/demos/react/test/cypress/e2e/example43.cy.ts @@ -1,4 +1,4 @@ -describe('Example 43 - colspan/rowspan - Employees Timesheets', { retries: 1 }, () => { +describe('Example 43 - colspan/rowspan - Employees Timesheets', { retries: 0 }, () => { const GRID_ROW_HEIGHT = 30; const fullTitles = [ 'Employee ID', @@ -27,15 +27,23 @@ describe('Example 43 - colspan/rowspan - Employees Timesheets', { retries: 1 }, cy.get('h2').should('contain', 'Example 43: colspan/rowspan - Employees Timesheets'); }); + it('should hide sub-title', () => { + cy.get('[data-test=toggle-subtitle]').click(); + }); + it('should have exact column titles', () => { cy.get('.slick-header-columns') .children() .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); }); - it('should expect 1st column to be frozen (frozen)', () => { - cy.get('.grid-canvas-left .slick-cell.frozen').should('have.length', 10); - cy.get('.grid-canvas-right .slick-cell:not(.frozen)').should('have.length.above', 50); + it('should expect 1st column to be pinned', () => { + // Pinning uses one live canvas and splits each row into left/center/right + // regions; the old grid-canvas-left/right pinned panes no longer exist. + cy.get('.slick-pinned-left-cells .slick-cell.slick-cell-pinned-left').should('have.length', 10); + // The shared Vue Cypress viewport is 1200px wide, but its route sidebar + // leaves less room for center-column virtualization than the Vanilla app. + cy.get('.slick-scrolling-cells .slick-cell').should('have.length.above', 50); }); it('should not display any Column Picker in the Grid Menu', () => { @@ -47,32 +55,32 @@ describe('Example 43 - colspan/rowspan - Employees Timesheets', { retries: 1 }, describe('Spanning', () => { it('should expect "Davolio", "Check Mail", and "Development" to all have rowspan of 2 in morning hours', () => { - cy.get(`[data-row=0] > .slick-cell.l1.r1.rowspan`).should('contain', 'Davolio'); - cy.get(`[data-row=0] > .slick-cell.l1.r1.rowspan`).should(($el) => + cy.get(`[data-row=0] .slick-cell.l1.r1.rowspan`).should('contain', 'Davolio'); + cy.get(`[data-row=0] .slick-cell.l1.r1.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); - cy.get(`[data-row=2] > .slick-cell.l2.r4.rowspan`).should('contain', 'Check Mail'); - cy.get(`[data-row=2] > .slick-cell.l2.r4.rowspan`).should(($el) => + cy.get(`[data-row=2] .slick-cell.l2.r4.rowspan`).should('contain', 'Check Mail'); + cy.get(`[data-row=2] .slick-cell.l2.r4.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); - cy.get(`[data-row=8] > .slick-cell.l7.r9.rowspan`).should('contain', 'Development'); - cy.get(`[data-row=8] > .slick-cell.l7.r9.rowspan`).should(($el) => + cy.get(`[data-row=8] .slick-cell.l7.r9.rowspan`).should('contain', 'Development'); + cy.get(`[data-row=8] .slick-cell.l7.r9.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); }); it('should expect "Lunch Break" to span over 3 columns and over all rows', () => { - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should(($el) => + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 10) ); }); it('should expect a large "Development" section that spans over multiple columns & rows in the afternoon', () => { - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should(($el) => + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5) ); }); @@ -80,334 +88,340 @@ describe('Example 43 - colspan/rowspan - Employees Timesheets', { retries: 1 }, describe('Basic Key Navigations', () => { it('should start at Employee 10001, then type "End" key and expect to be in "Team Meeting" between 4:30-5:00pm', () => { - cy.get('[data-row=0] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l0.r0.active').should('contain', '10001'); + cy.get('[data-row=0] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l0.r0.active').should('contain', '10001'); cy.get('@active_cell').type('{end}'); - cy.get('[data-row=0] > .slick-cell.l17.r18.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l17.r18.active').should('contain', 'Team Meeting'); }); it('should start at Employee 10002, then type "End" key and also expect to be in "Team Meeting" between 4:30-5:00pm', () => { - cy.get('[data-row=1] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=1] > .slick-cell.l0.r0.active').should('contain', '10002'); + cy.get('[data-row=1] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=1] .slick-cell.l0.r0.active').should('contain', '10002'); cy.get('@active_cell').type('{end}'); - cy.get('[data-row=0] > .slick-cell.l17.r18.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l17.r18.active').should('contain', 'Team Meeting'); }); it('should start at Employee 10004, then type "ArrowRight" key twice and expect to be in "Check Mail" between 9:00-10:30am', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{rightarrow}{rightarrow}'); - cy.get('[data-row=2] > .slick-cell.l2.r4.active').should('contain', 'Check Mail'); + cy.get('[data-row=2] .slick-cell.l2.r4.active').should('contain', 'Check Mail'); }); it('should start at Employee 10004, then type "ArrowRight" key 4x times and expect to be in "Testing" between 11:00-1:00pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}'); - cy.get('[data-row=3] > .slick-cell.l6.r9.active').should('contain', 'Testing'); + cy.get('[data-row=3] .slick-cell.l6.r9.active').should('contain', 'Testing'); }); it('should start at Employee 10004, then type "ArrowRight" key 5x times and expect to be in "Lunch Break"', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}'); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); }); it('should start at Employee 10004, then type "ArrowRight" key 6x times and expect to be in "Development" between 2:30-3:30pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}'); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); }); // then rollback by going backward it('should be on Employee 10004 row at previous "Development" cell, then type "ArrowLeft" key once and expect to be in "Lunch Break"', () => { - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).as('active_cell').click(); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); + // The proxy scrollbar is a sibling of the canvas, so native + // scrollIntoView() cannot reveal a horizontally virtualized cell. + cy.get('.slick-horizontal-scroller').scrollTo('right', { ensureScrollable: false }); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).as('active_cell').scrollIntoView().click(); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); cy.get('@active_cell').type('{leftarrow}'); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key once and expect to be in "Conference" between 4:00-5:00pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).type('{leftarrow}'); - cy.get(`[data-row=3] > .slick-cell.l16.r17.active`).should('contain', 'Conference'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).type('{leftarrow}'); + cy.get(`[data-row=3] .slick-cell.l16.r17.active`).should('contain', 'Conference'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key 3x times and expect to be back to "Development" between 2:30-3:30pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).type('{leftarrow}{leftarrow}{leftarrow}'); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).type('{leftarrow}{leftarrow}{leftarrow}'); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key 4x times and expect to be back to "Lunch Break"', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{leftarrow}{leftarrow}{leftarrow}{leftarrow}'); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key 5x times and expect to be back to "Testing" between 11:00-1:00pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{leftarrow}{leftarrow}{leftarrow}{leftarrow}{leftarrow}'); - cy.get(`[data-row=3] > .slick-cell.l6.r9.active`).should('contain', 'Testing'); + cy.get(`[data-row=3] .slick-cell.l6.r9.active`).should('contain', 'Testing'); }); // going down it('should start at 10am "Team Meeting, then type "ArrowDown" key once and expect to be in "Support" between 9:30-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('.slick-horizontal-scroller').scrollTo(0, 0, { ensureScrollable: false }); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}'); - cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support'); + cy.get(`[data-row=1] .slick-cell.l3.r5.active`).should('contain', 'Support'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key twice and expect to be in "Check Email" between 9:00-10:30am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}{downarrow}'); - cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); + cy.get(`[data-row=2] .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 3x times and expect to be in "Task Assign" between 9:00-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}'); - cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); + cy.get(`[data-row=4] .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times and expect to be in "Support" between 10:00-11:30am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}'); - cy.get(`[data-row=5] > .slick-cell.l4.r6.active`).should('contain', 'Support'); + cy.get(`[data-row=5] .slick-cell.l4.r6.active`).should('contain', 'Support'); }); // going up from inverse it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" once and expect to be in "Task Assign" between 9:00-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}'); - cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); + cy.get(`[data-row=4] .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 2x times and expect to be in "Task Assign" between 9:00-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}{uparrow}'); - cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); + cy.get(`[data-row=2] .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 3x times and expect to be in "Support" between 10:00-11:30am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}{uparrow}{uparrow}'); - cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support'); + cy.get(`[data-row=1] .slick-cell.l3.r5.active`).should('contain', 'Support'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 4x times and expect to be back to same "Team Meeting"', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}{uparrow}{uparrow}{uparrow}'); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); }); }); describe('Grid Navigate Functions', () => { it('should start at Employee 10004, then type "Navigate Right" twice and expect to be in "Check Mail" between 9:00-10:30am', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); cy.get('[data-test="goto-next"]').click().click(); - cy.get('[data-row=2] > .slick-cell.l2.r4.active').should('contain', 'Check Mail'); + cy.get('[data-row=2] .slick-cell.l2.r4.active').should('contain', 'Check Mail'); }); it('should start at Employee 10004, then type "Navigate Right" 4x times and expect to be in "Testing" between 11:00-1:00pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('[data-test="goto-next"]').click().click().click().click(); - cy.get('[data-row=3] > .slick-cell.l6.r9.active').should('contain', 'Testing'); + cy.get('[data-row=3] .slick-cell.l6.r9.active').should('contain', 'Testing'); }); it('should start at Employee 10004, then type "Navigate Right" 5x times and expect to be in "Lunch Break"', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('[data-test="goto-next"]').click().click().click().click().click(); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); }); it('should start at Employee 10004, then type "Navigate Right" 6x times and expect to be in "Development" between 2:30-3:30pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('[data-test="goto-next"]').click().click().click().click().click().click(); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); }); // then rollback by going backward it('should be on Employee 10004 row at previous "Development" cell, then type "Navigate Left" once and expect to be in "Lunch Break"', () => { - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).as('active_cell').click(); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); + cy.get('.slick-horizontal-scroller').scrollTo('right', { ensureScrollable: false }); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).as('active_cell').scrollIntoView().click(); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); cy.get('[data-test="goto-prev"]').click(); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" once and expect to be in "Conference" between 4:00-5:00pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); cy.get('[data-test="goto-prev"]').click(); - cy.get(`[data-row=3] > .slick-cell.l16.r17.active`).should('contain', 'Conference'); + cy.get(`[data-row=3] .slick-cell.l16.r17.active`).should('contain', 'Conference'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" 3x times and expect to be back to "Development" between 2:30-3:30pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); cy.get('[data-test="goto-prev"]').click().click().click(); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" 4x times and expect to be back to "Lunch Break"', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); cy.get('[data-test="goto-prev"]').click().click().click().click(); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); }); it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" 5x times and expect to be back to "Testing" between 11:00-1:00pm', () => { - cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); - cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-row=3] .slick-cell.l0.r0').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=3] .slick-cell.l0.r0.active').should('contain', '10004'); cy.get('@active_cell').type('{end}'); - cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); + cy.get(`[data-row=3] .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); cy.get('[data-test="goto-prev"]').click().click().click().click().click(); - cy.get(`[data-row=3] > .slick-cell.l6.r9.active`).should('contain', 'Testing'); + cy.get(`[data-row=3] .slick-cell.l6.r9.active`).should('contain', 'Testing'); }); // going down it('should start at 10am "Team Meeting, then type "ArrowDown" key once and expect to be in "Support" between 9:30-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('.slick-horizontal-scroller').scrollTo(0, 0, { ensureScrollable: false }); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click(); - cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support'); + cy.get(`[data-row=1] .slick-cell.l3.r5.active`).should('contain', 'Support'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key twice and expect to be in "Check Email" between 9:00-10:30am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click().click(); - cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); + cy.get(`[data-row=2] .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 3x times and expect to be in "Task Assign" between 9:00-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click().click().click(); - cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); + cy.get(`[data-row=4] .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times and expect to be in "Support" between 10:00-11:30am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click().click().click().click(); - cy.get(`[data-row=5] > .slick-cell.l4.r6.active`).should('contain', 'Support'); + cy.get(`[data-row=5] .slick-cell.l4.r6.active`).should('contain', 'Support'); }); // going up from inverse it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" once and expect to be in "Task Assign" between 9:00-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click().click().click().click(); cy.get('[data-test="goto-up"]').click(); - cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); + cy.get(`[data-row=4] .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 2x times and expect to be in "Task Assign" between 9:00-11:00am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click().click().click().click(); cy.get('[data-test="goto-up"]').click().click(); - cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); + cy.get(`[data-row=2] .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 3x times and expect to be in "Support" between 10:00-11:30am', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click().click().click().click(); cy.get('[data-test="goto-up"]').click().click().click(); - cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support'); + cy.get(`[data-row=1] .slick-cell.l3.r5.active`).should('contain', 'Support'); }); it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 4x times and expect to be back to same "Team Meeting"', () => { - cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4').as('active_cell').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); cy.get('[data-test="goto-down"]').click().click().click().click(); cy.get('[data-test="goto-up"]').click().click().click().click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-row=0] .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); }); }); describe('Grid Editing', () => { it('should toggle editing', () => { cy.get('#isEditable').contains('false'); - cy.get('[data-row=0] > .slick-cell.l4.r4').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active .editor-text').should('not.exist'); + cy.get('[data-row=0] .slick-cell.l4.r4').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active .editor-text').should('not.exist'); cy.get('[data-test=toggle-editing]').click(); cy.get('#isEditable').contains('true'); - cy.get('[data-row=0] > .slick-cell.l4.r4').click(); - cy.get('[data-row=0] > .slick-cell.l4.r4.active.editable .editor-text').should('exist'); - cy.get('[data-row=0] > .slick-cell.l4.r4.active.editable .editor-text').type('Team Meeting.xyz{enter}'); + cy.get('[data-row=0] .slick-cell.l4.r4').scrollIntoView().click(); + cy.get('[data-row=0] .slick-cell.l4.r4.active.editable .editor-text').should('exist'); + cy.get('[data-row=0] .slick-cell.l4.r4.active.editable .editor-text').type('Team Meeting.xyz{enter}'); }); // going down it('should have changed active cell to "Support" between 9:30-11:00am', () => { - cy.get('[data-row=1] > .slick-cell.l3.r5.active.editable .editor-text') + cy.get('[data-row=1] .slick-cell.l3.r5.active.editable .editor-text') .invoke('val') .then((text) => expect(text).to.eq('Support')); - cy.get('[data-row=1] > .slick-cell.l3.r5.active.editable .editor-text').type('Support.xyz{enter}'); + cy.get('[data-row=1] .slick-cell.l3.r5.active.editable .editor-text').type('Support.xyz{enter}'); }); it('should have changed active cell to "Check Email" between 9:00-10:30am', () => { - cy.get('[data-row=2] > .slick-cell.l2.r4.active.editable .editor-text') + cy.get('[data-row=2] .slick-cell.l2.r4.active.editable .editor-text') .invoke('val') .then((text) => expect(text).to.eq('Check Mail')); - cy.get('[data-row=2] > .slick-cell.l2.r4.active.editable .editor-text').type('Check Mail.xyz{enter}'); + cy.get('[data-row=2] .slick-cell.l2.r4.active.editable .editor-text').type('Check Mail.xyz{enter}'); }); it('should have changed active cell to "Task Assign" between 9:00-11:00am', () => { - cy.get('[data-row=4] > .slick-cell.l2.r5.active.editable .editor-text') + cy.get('[data-row=4] .slick-cell.l2.r5.active.editable .editor-text') .invoke('val') .then((text) => expect(text).to.eq('Task Assign')); - cy.get('[data-row=4] > .slick-cell.l2.r5.active.editable .editor-text').type('Task Assign.xyz{enter}'); + cy.get('[data-row=4] .slick-cell.l2.r5.active.editable .editor-text').type('Task Assign.xyz{enter}'); }); it('should have changed active cell to "Support" between 10:00-11:30am', () => { - cy.get('[data-row=5] > .slick-cell.l4.r6.active.editable .editor-text') + cy.get('[data-row=5] .slick-cell.l4.r6.active.editable .editor-text') .invoke('val') .then((text) => expect(text).to.eq('Support')); - cy.get('[data-row=5] > .slick-cell.l4.r6.active.editable .editor-text').type('Support.xyz{enter}'); + cy.get('[data-row=5] .slick-cell.l4.r6.active.editable .editor-text').type('Support.xyz{enter}'); }); it('should have changed active cell to "Testing" and cancel editing when typing "Escape" key', () => { - cy.get('[data-row=6] > .slick-cell.l4.r4.active.editable .editor-text') + cy.get('[data-row=6] .slick-cell.l4.r4.active.editable .editor-text') .invoke('val') .then((text) => expect(text).to.eq('Testing')); - cy.get('[data-row=6] > .slick-cell.l4.r4.active.editable .editor-text').type('{esc}'); - cy.get('[data-row=6] > .slick-cell.l4.r4.active.editable .editor-text').should('not.exist'); + cy.get('[data-row=6] .slick-cell.l4.r4.active.editable .editor-text').type('{esc}'); + cy.get('[data-row=6] .slick-cell.l4.r4.active.editable .editor-text').should('not.exist'); }); }); @@ -418,32 +432,32 @@ describe('Example 43 - colspan/rowspan - Employees Timesheets', { retries: 1 }, }); it('should expect EmployeeID to follow columns at index 0 column index', () => { - cy.get(`[data-row=0] > .slick-cell.l0.r0.rowspan`).should('contain', 'Davolio'); - cy.get(`[data-row=0] > .slick-cell.l0.r0.rowspan`).should(($el) => + cy.get(`[data-row=0] .slick-cell.l0.r0.rowspan`).should('contain', 'Davolio'); + cy.get(`[data-row=0] .slick-cell.l0.r0.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); - cy.get(`[data-row=2] > .slick-cell.l1.r3.rowspan`).should('contain', 'Check Mail'); - cy.get(`[data-row=2] > .slick-cell.l1.r3.rowspan`).should(($el) => + cy.get(`[data-row=2] .slick-cell.l1.r3.rowspan`).should('contain', 'Check Mail'); + cy.get(`[data-row=2] .slick-cell.l1.r3.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); - cy.get(`[data-row=8] > .slick-cell.l6.r8.rowspan`).should('contain', 'Development'); - cy.get(`[data-row=8] > .slick-cell.l6.r8.rowspan`).should(($el) => + cy.get(`[data-row=8] .slick-cell.l6.r8.rowspan`).should('contain', 'Development'); + cy.get(`[data-row=8] .slick-cell.l6.r8.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); }); it('should expect "Lunch Break" to be moved to the left by 1 index less', () => { - cy.get(`[data-row=0] > .slick-cell.l9.r11.rowspan`).should('contain', 'Lunch Break'); - cy.get(`[data-row=0] > .slick-cell.l9.r11.rowspan`).should(($el) => + cy.get(`[data-row=0] .slick-cell.l9.r11.rowspan`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l9.r11.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 10) ); }); it('should expect "Development" to be moved to the left by 1 index less', () => { - cy.get(`[data-row=1] > .slick-cell.l12.r13.rowspan`).should('contain', 'Development'); - cy.get(`[data-row=1] > .slick-cell.l12.r13.rowspan`).should(($el) => + cy.get(`[data-row=1] .slick-cell.l12.r13.rowspan`).should('contain', 'Development'); + cy.get(`[data-row=1] .slick-cell.l12.r13.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5) ); }); @@ -455,32 +469,32 @@ describe('Example 43 - colspan/rowspan - Employees Timesheets', { retries: 1 }, }); it('should expect EmployeeID to follow columns at index 1 column index', () => { - cy.get(`[data-row=0] > .slick-cell.l1.r1.rowspan`).should('contain', 'Davolio'); - cy.get(`[data-row=0] > .slick-cell.l1.r1.rowspan`).should(($el) => + cy.get(`[data-row=0] .slick-cell.l1.r1.rowspan`).should('contain', 'Davolio'); + cy.get(`[data-row=0] .slick-cell.l1.r1.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); - cy.get(`[data-row=2] > .slick-cell.l2.r4.rowspan`).should('contain', 'Check Mail'); - cy.get(`[data-row=2] > .slick-cell.l2.r4.rowspan`).should(($el) => + cy.get(`[data-row=2] .slick-cell.l2.r4.rowspan`).should('contain', 'Check Mail'); + cy.get(`[data-row=2] .slick-cell.l2.r4.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); - cy.get(`[data-row=8] > .slick-cell.l7.r9.rowspan`).should('contain', 'Development'); - cy.get(`[data-row=8] > .slick-cell.l7.r9.rowspan`).should(($el) => + cy.get(`[data-row=8] .slick-cell.l7.r9.rowspan`).should('contain', 'Development'); + cy.get(`[data-row=8] .slick-cell.l7.r9.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) ); }); it('should expect "Lunch Break" to be moved to the right by 1 index less', () => { - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); - cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should(($el) => + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] .slick-cell.l10.r12.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 10) ); }); it('should expect "Development" to be moved to the right by 1 index less and a large "Development" section that spans over multiple columns & rows in the afternoon', () => { - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); - cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should(($el) => + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); + cy.get(`[data-row=1] .slick-cell.l13.r14.rowspan`).should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5) ); }); diff --git a/demos/react/test/cypress/e2e/example44.cy.ts b/demos/react/test/cypress/e2e/example44.cy.ts index 5d5bf6f477..451c07efd7 100644 --- a/demos/react/test/cypress/e2e/example44.cy.ts +++ b/demos/react/test/cypress/e2e/example44.cy.ts @@ -30,8 +30,14 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { cy.get('h2').should('contain', 'Example 44: colspan/rowspan with large dataset'); }); + it('should hide sub-title', () => { + cy.get('[data-test=toggle-subtitle]').click(); + }); + it('should calculate a height that fits the wrapped Revenue Growth header', () => { - cy.get('.slick-header-auto-height').should('have.length', 2); + // The pinning POC uses one live header instead of separate left/right + // header panes, so auto-height is applied to a single header element. + cy.get('.slick-header-auto-height').should('have.length', 1); cy.get('.slick-header-auto-height') .first() .should(($header) => { @@ -58,7 +64,7 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { let draggedColumn: HTMLElement; cy.clock(); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo(0, 0).its('0.scrollLeft').should('equal', 0); + cy.get('.slick-horizontal-scroller').scrollTo(0, 0).its('0.scrollLeft').should('equal', 0); cy.get('.slick-header-columns-left').then(($header) => { const sortableProperty = Object.keys($header[0]).find((property) => property.startsWith('Sortable')); @@ -75,12 +81,12 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { }); cy.tick(300); - cy.get('.slick-viewport-top.slick-viewport-left').its('0.scrollLeft').should('be.greaterThan', 0); + cy.get('.slick-horizontal-scroller').its('0.scrollLeft').should('be.greaterThan', 0); cy.then(() => sortInstance.options.onEnd({ item: draggedColumn, stopPropagation: () => {} })); cy.get('.slick-header-column:nth(0)').should('contain', 'Title'); cy.get('.slick-header-column:nth(1)').should('contain', 'Revenue Growth'); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo(0, 0); + cy.get('.slick-vertical-scroller').scrollTo(0, 0); }); it('should drag Title column to swap with 2nd column "Revenue Growth" in the grid and expect rowspan to stay at same position with Task 0 to spread instead', () => { @@ -208,7 +214,7 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { }); it('should scroll to the right and still expect spans without any extra texts', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo(400, 0).wait(10); + cy.get('.slick-horizontal-scroller').scrollTo(400, 0).wait(10); cy.get(`[data-row=3] > .slick-cell:nth(1)`).contains(/\d+$/); cy.get(`[data-row=3] > .slick-cell.l1.r2.rowspan`).should('exist'); @@ -229,7 +235,7 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { }); it('should scroll back to left and expect Task 8 to have 2 different spans (Revenue Grow: rowspan=80, Policy Index: rowspan=2000,colspan=2)', () => { - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo(0, 0).wait(10); + cy.get('.slick-vertical-scroller').scrollTo(0, 0).wait(10); cy.get(`[data-row=8] > .slick-cell.l0.r0`).should('contain', 'Task 8'); cy.get(`[data-row=8] > .slick-cell.l1.r1.rowspan`).should(($el) => { @@ -350,6 +356,10 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { }); it('should start at RevenueGrowth column on first dashed cell, then type "Ctrl+End" then "Ctrl+Home" keys and expect active cell to go to bottom/top of grid on same column', () => { + // The preceding span/scroll cases share state. Reset both axes through + // the POC's actual scroll owners before interacting with the first row. + cy.get('.slick-horizontal-scroller').scrollTo(0, 0); + cy.get('.slick-vertical-scroller').scrollTo(0, 0); cy.get('[data-row=0] > .slick-cell.l2.r2').as('active_cell').click(); cy.get('[data-row=0] > .slick-cell.l2.r2.active').should('have.length', 1); cy.get('@active_cell').type('{ctrl}{end}', { release: false }); diff --git a/demos/react/test/cypress/e2e/example45.cy.ts b/demos/react/test/cypress/e2e/example45.cy.ts index 6f56e1a753..b4485b498b 100644 --- a/demos/react/test/cypress/e2e/example45.cy.ts +++ b/demos/react/test/cypress/e2e/example45.cy.ts @@ -224,9 +224,9 @@ describe('Example 45 - Row Detail with inner Grid', () => { }); it('should scroll down when the row detail is just barely visible and then scroll back up and still expect same filters/sorting', () => { - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 350); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 350); cy.wait(50); - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 0); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 0); cy.get(`#innergrid-1 .slick-row[style*="translateY(${GRID_ROW_HEIGHT * 0}px)"] > .slick-cell:nth(0)`).should('contain', '10281'); cy.get(`#innergrid-1 .slick-row[style*="translateY(${GRID_ROW_HEIGHT * 0}px)"] > .slick-cell:nth(1)`).should('contain', 'Madrid'); @@ -236,9 +236,9 @@ describe('Example 45 - Row Detail with inner Grid', () => { it('should scroll down by 2 pages down and then scroll back up and no longer the same filters/sorting', () => { cy.get('#grid45 [data-row="0"] .slick-cell.r2.l2').first().click().type('{pagedown}'); - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 2000); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 2000); cy.wait(50); - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 0); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 0); cy.get(`#innergrid-1 .slick-row[style*="translateY(${GRID_ROW_HEIGHT * 0}px)"] > .slick-cell:nth(0)`).should( 'not.contain', @@ -419,7 +419,7 @@ describe('Example 45 - Row Detail with inner Grid', () => { cy.get('#grid45').type('{pageDown}{pageDown}', { release: false }); cy.wait(50); - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 350); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 350); // expect same grid details for both grids // 2nd row detail @@ -520,9 +520,9 @@ describe('Example 45 - Row Detail with inner Grid', () => { }); it('should scroll far down (out of viewport) and back up and expect inner grid sort/filter state is PRESERVED (keepComponentAlive)', () => { - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 800); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 800); cy.wait(50); - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 0); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 0); // state should be PRESERVED because keepComponentAlive is enabled cy.get(`#innergrid-1 .slick-row[style*="translateY(${GRID_ROW_HEIGHT * 0}px)"] > .slick-cell:nth(0)`).should('contain', '10281'); @@ -532,9 +532,9 @@ describe('Example 45 - Row Detail with inner Grid', () => { }); it('should scroll out of viewport a second time and back up and still expect inner grid sort/filter state is PRESERVED', () => { - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 800); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 800); cy.wait(50); - cy.get('#grid45 .slick-viewport-top.slick-viewport-left').first().scrollTo(0, 0); + cy.get('#grid45 .slick-vertical-scroller').first().scrollTo(0, 0); cy.get(`#innergrid-1 .slick-row[style*="translateY(${GRID_ROW_HEIGHT * 0}px)"] > .slick-cell:nth(0)`).should('contain', '10281'); cy.get(`#innergrid-1 .slick-row[style*="translateY(${GRID_ROW_HEIGHT * 0}px)"] > .slick-cell:nth(1)`).should('contain', 'Madrid'); diff --git a/demos/react/test/cypress/e2e/example47.cy.ts b/demos/react/test/cypress/e2e/example47.cy.ts index 936cb3002e..3e5480a883 100644 --- a/demos/react/test/cypress/e2e/example47.cy.ts +++ b/demos/react/test/cypress/e2e/example47.cy.ts @@ -101,7 +101,7 @@ describe('Example 47 - Row Detail View + Grouping', () => { cy.get('.detail-label label').should('contain', 'Assignee:'); cy.get('.detail-label input').should('exist'); - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('.dynamic-cell-detail').find('[data-test=delete-btn]').click(); cy.get('.toast.text-bg-danger').contains(/Deleted row with Task [0-9]*/); cy.get('.dynamic-cell-detail').should('have.length', 0); @@ -112,7 +112,7 @@ describe('Example 47 - Row Detail View + Grouping', () => { cy.on('window:alert', stub); let assigneeName = ''; - cy.get('.slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('.slick-vertical-scroller').scrollTo('top'); cy.get('[data-row="1"] > .slick-cell.l1').contains(/Task [0-9]*/); cy.get('[data-row="1"] > .slick-cell.l0').click().wait(40); diff --git a/demos/react/test/cypress/e2e/example48.cy.ts b/demos/react/test/cypress/e2e/example48.cy.ts index 5e7f317681..329aac187e 100644 --- a/demos/react/test/cypress/e2e/example48.cy.ts +++ b/demos/react/test/cypress/e2e/example48.cy.ts @@ -153,7 +153,7 @@ describe('Example 48 - Hybrid Selection Model', () => { }); it('should auto scroll take effect to display the selecting element when dragging', { scrollBehavior: false }, () => { - cy.get('#grid48-1 .slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('#grid48-1 .slick-vertical-scroller').scrollTo('top'); testScroll('#grid48-1', '#grid48-1', 0, 1).then((scrollDistance: { cell: any; row: any }) => { expect(scrollDistance.cell.scrollBefore).to.be.lte(scrollDistance.cell.scrollAfter); @@ -161,11 +161,11 @@ describe('Example 48 - Hybrid Selection Model', () => { }); cy.get('#selectionRange1').contains(/"fromRow":0,"fromCell":1,"toRow":1[45],"toCell":3/); - cy.get('#grid48-1 .slick-viewport-top.slick-viewport-left').scrollTo(0, 13 * 35); + cy.get('#grid48-1 .slick-vertical-scroller').scrollTo(0, 13 * 35); }); it('should toggle multiple cell selection ranges with the checkbox', () => { - cy.get('#grid48-1 .slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('#grid48-1 .slick-vertical-scroller').scrollTo('top'); cy.get('[data-test="enable-multi-selection"]').check(); cy.get('#grid48-1 .slick-row[data-row="1"] .slick-cell.l1.r1').click(); @@ -201,7 +201,7 @@ describe('Example 48 - Hybrid Selection Model', () => { }); it('should preserve row and column offsets when copying multiple cell ranges', () => { - cy.get('#grid48-1 .slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('#grid48-1 .slick-vertical-scroller').scrollTo('top'); cy.get('[data-test="enable-multi-selection"]').should('be.checked'); cy.window().then((win) => { cy.stub(win.navigator.clipboard, 'writeText').as('clipboardWriteText'); @@ -264,7 +264,7 @@ describe('Example 48 - Hybrid Selection Model', () => { }); cy.get('#selectionRange2').contains(/"fromRow":0,"fromCell":0,"toRow":1[0-9],"toCell":7/); - cy.get('#grid48-2 .slick-viewport-top.slick-viewport-left').scrollTo(0, 12 * 35); + cy.get('#grid48-2 .slick-vertical-scroller').scrollTo(0, 12 * 35); }); it('should click on a cell outside of the selected range and expect previous selection to remain', () => { @@ -272,7 +272,7 @@ describe('Example 48 - Hybrid Selection Model', () => { cy.get('@task1x') .contains(/Task 1[0-9]/) .click(); - cy.get('#grid48-2 .slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('#grid48-2 .slick-vertical-scroller').scrollTo('top'); cy.get('#grid48-2 .slick-cell.selected').should('have.length.gte', 60); cy.get('#selectionRange2').contains(/"fromRow":0,"fromCell":0,"toRow":1[0-9],"toCell":7/); }); @@ -284,20 +284,20 @@ describe('Example 48 - Hybrid Selection Model', () => { it('should click on row 4 and 5 row checkbox and expect 5 full rows to be selected', () => { cy.get('#grid48-2 .slick-row[data-row="4"] .slick-cell.l1.r1').should('contain', '4'); cy.get('#grid48-2 .slick-row[data-row="4"] input[type=checkbox]').click({ force: true }); - cy.get('#grid48-2 .slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('#grid48-2 .slick-vertical-scroller').scrollTo('top'); cy.get('#grid48-2 .slick-row[data-row="4"] .slick-cell.l0.r0').should('have.class', 'selected'); cy.get('#grid48-2 .slick-cell.selected').should('have.length', 8 * 1); // select another row cy.get('#grid48-2 .slick-row[data-row="5"] .slick-cell.l1.r1').should('contain', '5'); cy.get('#grid48-2 .slick-row[data-row="5"] input[type=checkbox]').click({ force: true }); - cy.get('#grid48-2 .slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('#grid48-2 .slick-vertical-scroller').scrollTo('top'); cy.get('#grid48-2 .slick-row[data-row="5"] .slick-cell.l0.r0').should('have.class', 'selected'); cy.get('#grid48-2 .slick-cell.selected').should('have.length', 8 * 2); }); it('should toggle multiple row selection ranges with the checkbox', () => { - cy.get('#grid48-2 .slick-viewport-top.slick-viewport-left').scrollTo('top'); + cy.get('#grid48-2 .slick-vertical-scroller').scrollTo('top'); cy.get('#grid48-2 .slick-row[data-row="4"] input[type=checkbox]').uncheck({ force: true }); cy.get('#grid48-2 .slick-row[data-row="5"] input[type=checkbox]').uncheck({ force: true }); cy.get('[data-test="enable-multi-selection"]').should('be.checked'); @@ -321,7 +321,7 @@ describe('Example 48 - Hybrid Selection Model', () => { const secondRange = '{"fromRow":3,"fromCell":0,"toRow":4,"toCell":7}'; const combinedRanges = `${firstRange}${secondRange}`; - cy.get(`${gridSelector} .slick-viewport-top.slick-viewport-left`).scrollTo('top'); + cy.get(`${gridSelector} .slick-vertical-scroller`).scrollTo('top'); cy.get(`${gridSelector} .slick-row[data-row="1"] input[type=checkbox]`).uncheck({ force: true }); cy.get(`${gridSelector} .slick-row[data-row="2"] input[type=checkbox]`).uncheck({ force: true }); cy.get(`${gridSelector} .slick-row[data-row="4"] input[type=checkbox]`).uncheck({ force: true }); diff --git a/demos/react/test/cypress/e2e/example55.cy.ts b/demos/react/test/cypress/e2e/example55.cy.ts index 37ecd02a59..3a8477ecf3 100644 --- a/demos/react/test/cypress/e2e/example55.cy.ts +++ b/demos/react/test/cypress/e2e/example55.cy.ts @@ -38,7 +38,7 @@ describe('Example 55 - Variable Row Height (provider)', { retries: 1 }, () => { it('should keep row 90 aligned at top after clicking scroll button', () => { cy.get('[data-test="scroll-row-90-example55"]').click(); - cy.get('.slick-viewport-top.slick-viewport-left') + cy.get('.slick-vertical-scroller') .invoke('scrollTop') .then((scrollTop) => { expect(Number(scrollTop)).to.be.closeTo(topOf(90), 2); diff --git a/demos/react/test/cypress/e2e/example56.cy.ts b/demos/react/test/cypress/e2e/example56.cy.ts index c02518e284..e30d1b7f00 100644 --- a/demos/react/test/cypress/e2e/example56.cy.ts +++ b/demos/react/test/cypress/e2e/example56.cy.ts @@ -1,6 +1,6 @@ describe('Example 56 - Variable Row Height (item metadata)', { retries: 1 }, () => { const BASE_ROW_HEIGHT = 40; - const FROZEN_ROW_COUNT = 2; + const PINNED_ROW_COUNT = 2; const hDefault = (r: number) => { const cycle = [33, 44, 44, 80]; @@ -18,36 +18,41 @@ describe('Example 56 - Variable Row Height (item metadata)', { retries: 1 }, () return t; }; - const frozenTopHeight = (hOf: (row: number) => number) => topOf(FROZEN_ROW_COUNT, hOf); + const pinnedTopHeight = (hOf: (row: number) => number) => topOf(PINNED_ROW_COUNT, hOf); const relativeTopInCanvas = (r: number, hOf: (row: number) => number) => { - if (r < FROZEN_ROW_COUNT) { - return topOf(r, hOf); - } - return topOf(r, hOf) - frozenTopHeight(hOf); + // Pinned rows are moved into the overlay, but center rows retain their + // natural document coordinates behind that overlay. + return topOf(r, hOf); }; - const canvasSelector = (r: number) => (r < FROZEN_ROW_COUNT ? '.grid-canvas-top' : '.grid-canvas-bottom'); + const rowHostSelector = (r: number) => (r < PINNED_ROW_COUNT ? '.slick-docking-overlay' : '.grid-canvas-top'); const assertRowStyle = (row: number, hOf: (row: number) => number) => { const expectedHeight = hOf(row); const expectedTop = relativeTopInCanvas(row, hOf); - cy.get(`${canvasSelector(row)} .slick-row[data-row=${row}]`) + cy.get(`${rowHostSelector(row)} .slick-row[data-row=${row}]`) .should('have.attr', 'style') .and('contain', `transform: translateY(${expectedTop}px)`) .then((style) => { if (expectedHeight !== BASE_ROW_HEIGHT) { expect(style).to.contain(`height: ${expectedHeight}px`); } else { - expect(style).not.to.contain('height:'); + // Docked rows carry their resolved height inline so editor/content + // styles cannot collapse the pinned row. The base-height case is + // therefore valid with either the stylesheet fallback or an + // explicit `height: 40px` declaration. + expect(style).to.match(/(?:height: 40px;|^(?!.*height:))/); } }); - cy.get(`[data-row="${row}"] > .slick-cell:nth(3)`).should('contain', `${expectedHeight}px`); + // Rows are split into left/center/right docking regions, so cells are + // nested under their region wrapper rather than being direct row children. + cy.get(`[data-row="${row}"] .slick-cell:nth(3)`).should('contain', `${expectedHeight}px`); }; const ensureDefaultDensity = () => { - cy.get('.grid-canvas-top .slick-row[data-row=1]') + cy.get('.slick-docking-overlay .slick-row[data-row=1]') .invoke('attr', 'style') .then((style) => { if ((style ?? '').includes('height: 50px')) { @@ -55,7 +60,7 @@ describe('Example 56 - Variable Row Height (item metadata)', { retries: 1 }, () } }); - cy.get('.grid-canvas-top .slick-row[data-row=1]').should('have.attr', 'style').and('contain', 'height: 44px'); + cy.get('.slick-docking-overlay .slick-row[data-row=1]').should('have.attr', 'style').and('contain', 'height: 44px'); }; beforeEach(() => { @@ -67,7 +72,7 @@ describe('Example 56 - Variable Row Height (item metadata)', { retries: 1 }, () cy.get('h2').should('contain', 'Example 56: Variable Row Height (item metadata)'); }); - it('should render frozen and scrollable rows with expected transform and row heights from metadata fallback', () => { + it('should render pinned and scrollable rows with expected transform and row heights from metadata fallback', () => { for (const r of [0, 1, 2, 3, 4, 5, 6]) { assertRowStyle(r, hDefault); } @@ -85,12 +90,12 @@ describe('Example 56 - Variable Row Height (item metadata)', { retries: 1 }, () } }); - it('should scroll row 90 to top of scrollable pane with frozen top rows', () => { - const expectedScrollTop = topOf(90, hDefault) - frozenTopHeight(hDefault); + it('should scroll row 90 to top of scrollable pane with pinned top rows', () => { + const expectedScrollTop = topOf(90, hDefault) - pinnedTopHeight(hDefault); cy.get('[data-test="scroll-row-90-example56"]').click(); - cy.get('.slick-viewport-bottom.slick-viewport-left').should(($viewport) => { + cy.get('.slick-vertical-scroller').should(($viewport) => { expect($viewport.scrollTop()).to.be.closeTo(expectedScrollTop, 2); }); diff --git a/demos/react/test/cypress/e2e/example57.cy.ts b/demos/react/test/cypress/e2e/example57.cy.ts index df73fe98f8..7055d540b1 100644 --- a/demos/react/test/cypress/e2e/example57.cy.ts +++ b/demos/react/test/cypress/e2e/example57.cy.ts @@ -54,14 +54,14 @@ describe('Example 57 - RTL (Right-to-Left)', () => { describe('Scrolling Behavior', () => { it('should have horizontal scroll enabled', () => { - cy.get('#grid57 .slick-viewport').then(($viewport) => { + cy.get('#grid57 .slick-horizontal-scroller').then(($viewport) => { const viewport = $viewport[0] as HTMLElement; expect(viewport.scrollWidth).to.be.greaterThan(viewport.clientWidth); }); }); it('should update visible header columns when scrolling', () => { - cy.get('#grid57 .slick-viewport').then(($viewport) => { + cy.get('#grid57 .slick-horizontal-scroller').then(($viewport) => { const viewport = $viewport[0] as HTMLElement; const maxScroll = viewport.scrollWidth - viewport.clientWidth; viewport.scrollLeft = maxScroll; @@ -72,7 +72,7 @@ describe('Example 57 - RTL (Right-to-Left)', () => { cy.wait(150); - cy.get('#grid57 .slick-viewport').then(($viewport) => { + cy.get('#grid57 .slick-horizontal-scroller').then(($viewport) => { const viewport = $viewport[0] as HTMLElement; expect(Math.abs(viewport.scrollLeft)).to.be.greaterThan(0); }); diff --git a/demos/react/test/cypress/support/commands.ts b/demos/react/test/cypress/support/commands.ts index 3be5a3685b..1f58e699f1 100644 --- a/demos/react/test/cypress/support/commands.ts +++ b/demos/react/test/cypress/support/commands.ts @@ -32,19 +32,19 @@ declare global { namespace Cypress { interface Chainable { // triggerHover: (elements: NodeListOf) => void; - convertPosition(viewport: string): Chainable | { x: string; y: string }>; + convertPosition(viewport: string): Chainable<{ x: string; y: string }>; getCell( row: number, col: number, viewport?: string, options?: { parentSelector?: string; rowHeight?: number } - ): Chainable>; + ): Chainable>; getNthCell( row: number, nthCol: number, viewport?: string, options?: { parentSelector?: string; rowHeight?: number } - ): Chainable>; + ): Chainable>; saveLocalStorage: () => void; restoreLocalStorage: () => void; getTransformValue(cssTransformMatrix: string, absoluteValue: boolean, transformType?: 'rotate' | 'scale'): Chainable; diff --git a/demos/react/test/cypress/support/drag.ts b/demos/react/test/cypress/support/drag.ts index 8228176a5a..b26012a988 100644 --- a/demos/react/test/cypress/support/drag.ts +++ b/demos/react/test/cypress/support/drag.ts @@ -82,15 +82,22 @@ export function getScrollDistanceWhenDragOutsideGrid( return (cy as any).convertPosition(viewport).then((_viewportPosition: { x: number; y: number }) => { const viewportSelector = `${selector} .slick-viewport-${_viewportPosition.x}.slick-viewport-${_viewportPosition.y}`; (cy as any).getNthCell(fromRow, fromCol, viewport, { parentSelector: selector }).dragStart(); - return cy.get(viewportSelector).then(($viewport) => { - const scrollTopBefore = $viewport.scrollTop(); - const scrollLeftBefore = $viewport.scrollLeft(); + return cy.get(selector).then(($grid) => { + const viewport = ($grid.find(viewportSelector)[0] || $grid.find('.slick-vertical-scroller')[0]) as HTMLElement; + const horizontalScroller = $grid.find('.slick-horizontal-scroller')[0] as HTMLElement | undefined; + const horizontalOwner = horizontalScroller || viewport; + const scrollTopBefore = viewport.scrollTop; + const scrollLeftBefore = horizontalOwner.scrollLeft; cy.dragOutside(dragDirection, 300, px, { parentSelector: selector }); - return cy.get(viewportSelector).then(($viewportAfter) => { + return cy.get(selector).then(($gridAfter) => { + const viewportAfter = ($gridAfter.find(viewportSelector)[0] || $gridAfter.find('.slick-vertical-scroller')[0]) as HTMLElement; + const horizontalScrollerAfter = $gridAfter.find('.slick-horizontal-scroller')[0] as HTMLElement | undefined; + const horizontalOwnerAfter = horizontalScrollerAfter || viewportAfter; cy.dragEnd(selector); - const scrollTopAfter = $viewportAfter.scrollTop(); - const scrollLeftAfter = $viewportAfter.scrollLeft(); - cy.get(viewportSelector).scrollTo(0, 0, { ensureScrollable: false }); + const scrollTopAfter = viewportAfter.scrollTop; + const scrollLeftAfter = horizontalOwnerAfter.scrollLeft; + horizontalOwnerAfter.scrollLeft = 0; + viewportAfter.scrollTop = 0; return cy.wrap({ scrollTopBefore, scrollLeftBefore, diff --git a/demos/vanilla/public/i18n/en.json b/demos/vanilla/public/i18n/en.json index 600083fa11..4f039dd3c3 100644 --- a/demos/vanilla/public/i18n/en.json +++ b/demos/vanilla/public/i18n/en.json @@ -7,7 +7,7 @@ "CLEAR_ALL_FILTERS": "Clear all Filters", "CLEAR_ALL_GROUPING": "Clear all Grouping", "CLEAR_ALL_SORTING": "Clear all Sorting", - "CLEAR_PINNING": "Unfreeze Columns/Rows", + "CLEAR_PINNING": "Unpin Columns/Rows", "CLONE": "Clone", "COLLAPSE_ALL_GROUPS": "Collapse all Groups", "COLUMNS": "Columns", @@ -29,7 +29,10 @@ "FILTER_SHORTCUTS": "Filter Shortcuts", "FROM_TO_OF_TOTAL_ITEMS": "{{from}}-{{to}} of {{totalItems}} items", "FORCE_FIT_COLUMNS": "Force fit columns", - "FREEZE_COLUMNS": "Freeze Columns", + "PIN_COLUMN": "Column Pinning", + "PIN_COLUMNS": "Pin Through Here", + "PIN_LEFT": "Pin Left", + "PIN_RIGHT": "Pin Right", "INVALID_FLOAT": "The number must be valid and have a maximum of {{maxDecimal}} decimals.", "GREATER_THAN": "Greater than", "GREATER_THAN_OR_EQUAL_TO": "Greater than or equal to", @@ -68,7 +71,8 @@ "TOGGLE_DARK_MODE": "Toggle Dark Mode", "TOGGLE_FILTER_ROW": "Toggle Filter Row", "TOGGLE_PRE_HEADER_ROW": "Toggle Pre-Header Row", - "UNFREEZE_COLUMNS": "Unfreeze Columns", + "UNPIN_COLUMN": "Unpin Column", + "UNPIN_COLUMNS": "Unpin All Columns", "X_OF_Y_SELECTED": "# of % selected", "X_OF_Y_MASS_SELECTED": "{{x}} of {{y}} selected", "BILLING": { diff --git a/demos/vanilla/public/i18n/fr.json b/demos/vanilla/public/i18n/fr.json index 084d95bbb8..bc86353024 100644 --- a/demos/vanilla/public/i18n/fr.json +++ b/demos/vanilla/public/i18n/fr.json @@ -7,7 +7,7 @@ "CLEAR_ALL_FILTERS": "Supprimer tous les filtres", "CLEAR_ALL_GROUPING": "Supprimer tous les groupes", "CLEAR_ALL_SORTING": "Supprimer tous les tris", - "CLEAR_PINNING": "Dégeler les colonnes/rangées", + "CLEAR_PINNING": "Désépingler les colonnes/rangées", "CLONE": "Cloner", "COLLAPSE_ALL_GROUPS": "Réduire tous les groupes", "COLUMNS": "Colonnes", @@ -29,7 +29,10 @@ "FILTER_SHORTCUTS": "Raccourcis de filtre", "FROM_TO_OF_TOTAL_ITEMS": "{{from}}-{{to}} de {{totalItems}} éléments", "FORCE_FIT_COLUMNS": "Ajustement forcé des colonnes", - "FREEZE_COLUMNS": "Geler les colonnes", + "PIN_COLUMN": "Épinglage de colonnes", + "PIN_COLUMNS": "Épingler jusqu'ici", + "PIN_LEFT": "Épingler à gauche", + "PIN_RIGHT": "Épingler à droite", "GREATER_THAN": "Plus grand que", "GREATER_THAN_OR_EQUAL_TO": "Plus grand ou égal à", "GROUP_BY": "Grouper par", @@ -68,7 +71,8 @@ "TOGGLE_DARK_MODE": "Basculer le mode clair/sombre", "TOGGLE_FILTER_ROW": "Basculer la ligne des filtres", "TOGGLE_PRE_HEADER_ROW": "Basculer la ligne de pré-en-tête", - "UNFREEZE_COLUMNS": "Dégeler les colonnes", + "UNPIN_COLUMN": "Désépingler la colonne", + "UNPIN_COLUMNS": "Désépingler toutes les colonnes", "X_OF_Y_SELECTED": "# de % sélectionnés", "X_OF_Y_MASS_SELECTED": "{{x}} de {{y}} sélectionnés", "BILLING": { diff --git a/demos/vanilla/src/app-routing.ts b/demos/vanilla/src/app-routing.ts index 1976666f59..e62c7a9322 100644 --- a/demos/vanilla/src/app-routing.ts +++ b/demos/vanilla/src/app-routing.ts @@ -44,6 +44,7 @@ import Example43 from './examples/example43.js'; import Example44 from './examples/example44.js'; import Example45 from './examples/example45.js'; import Example46 from './examples/example46.js'; +import Example47 from './examples/example47.js'; import Icons from './examples/icons.js'; import type { RouterConfig } from './interfaces.js'; @@ -98,6 +99,7 @@ export class AppRouting { { route: 'example44', name: 'example44', view: './examples/example44.html', viewModel: Example44, title: 'Example44' }, { route: 'example45', name: 'example45', view: './examples/example45.html', viewModel: Example45, title: 'Example45' }, { route: 'example46', name: 'example46', view: './examples/example46.html', viewModel: Example46, title: 'Example46' }, + { route: 'example47', name: 'example47', view: './examples/example47.html', viewModel: Example47, title: 'Example47' }, { route: '', redirect: 'example01' }, { route: '**', redirect: 'example01' }, ]; diff --git a/demos/vanilla/src/app.html b/demos/vanilla/src/app.html index 8756cc7cb8..b12080213d 100644 --- a/demos/vanilla/src/app.html +++ b/demos/vanilla/src/app.html @@ -34,7 +34,7 @@

Slickgrid-Universal

Example01 - Basic Grids Example02 - Grouping & Aggregators Example03 - Draggable Grouping - Example04 - Pinned (frozen) Columns/Rows + Example04 - Pinned Columns/Rows Example05 - Tree Data with Parent/Child refs Example06 - Tree Data from Hierarchical View Dataset Example07 - Row Move & Row Selections @@ -87,6 +87,7 @@

Slickgrid-Universal

Example44 - Variable Row Height (provider) Example45 - Variable Row Height (metadata) Example46 - RTL (Right-to-Left) + Example47 - Sticky Financial Report
diff --git a/demos/vanilla/src/examples/example03.html b/demos/vanilla/src/examples/example03.html index b93ea06b33..b088035254 100644 --- a/demos/vanilla/src/examples/example03.html +++ b/demos/vanilla/src/examples/example03.html @@ -1,7 +1,10 @@

- Example 03 - Draggable Grouping + Example 03 - Draggable Grouping & Aggregators (with Salesforce Theme) + -
+
The Autocomplete Editor (City of Origin) might throw some console errors because it uses "http://gd.geobytes.com/" request (which is not using https). You can allow "Insecure content" in your browser for this website only to test the Autocomplete
@@ -19,24 +22,33 @@
- - + + - - + + + + + + + - + - - @@ -44,8 +56,8 @@
Toggle which row to show "Select All" checkbox -
diff --git a/demos/vanilla/src/examples/example04.ts b/demos/vanilla/src/examples/example04.ts index b3dbe176b9..57c8ef2416 100644 --- a/demos/vanilla/src/examples/example04.ts +++ b/demos/vanilla/src/examples/example04.ts @@ -19,6 +19,8 @@ import fetchJsonp from './jsonp.js'; import { showToast } from './utilities.js'; import './example04.scss'; +const ITEMS_COUNT = 500; + // you can create custom validator to pass to an inline editor const myCustomTitleValidator = (value) => { if (value === null || value === undefined || !value.length) { @@ -52,12 +54,14 @@ export default class Example04 { dataset: any[]; dataViewObj: SlickDataView; commandQueue: EditCommand[] = []; - frozenColumnCount = 2; - frozenRowCount = 3; - isFrozenBottom = false; + pinnedColumnCount = 2; + pinnedRightColumnCount = 1; + pinnedRowCount = 3; + isPinnedBottom = false; sgb: SlickVanillaGridBundle; checkboxSelectorInstance: SlickCheckboxSelectColumn; isSelectAllShownAsColumnTitle = false; + subTitleStyle = 'display: block'; constructor() { this._bindingEventService = new BindingEventService(); @@ -394,6 +398,9 @@ export default class Example04 { autoTooltipOptions: { enableForHeaderCells: true, }, + // Preserve the existing behavior: the resizer re-fits columns after a + // browser/container resize, including when the dedicated pinning + // scrollbar is active. enableAutoSizeColumns: true, enableAutoResize: true, enableCellNavigation: true, @@ -416,9 +423,15 @@ export default class Example04 { onExtensionRegistered: (instance) => (this.checkboxSelectorInstance = instance), }, enableSelection: true, - frozenColumn: this.frozenColumnCount, - frozenRow: this.frozenRowCount, - // frozenBottom: true, // if you want to freeze the bottom instead of the top, you can enable this property + pinning: { + columns: { + left: this.pinnedColumnCount, + // Keep the Action column on the trailing edge without depending on + // extension columns (such as the checkbox selector) being inserted. + right: this.pinnedRightColumnCount > 0 ? ['action'] : [], + }, + rows: { top: this.getPinnedRowIndexes() }, + }, editCommandHandler: (_item, _column, editCommand) => { this.commandQueue.push(editCommand); editCommand.execute(); @@ -439,13 +452,14 @@ export default class Example04 { } }, }, - gridMenu: { hideClearFrozenColumnsCommand: false }, - headerMenu: { hideFreezeColumnsCommand: false }, + gridMenu: { hideClearPinningCommand: false }, + headerMenu: { hidePinColumnCommand: false, hidePinningColumnsCommand: false }, enableContextMenu: true, contextMenu: { optionShownOverColumnIds: ['percentComplete'], subItemChevronClass: 'mdi mdi-chevron-down mdi-rotate-270', hideCloseButton: true, + dropSide: 'right', optionTitle: 'Change Percent Complete', onOptionSelected: (_e, args) => { // e.preventDefault(); // you could do if you wish to keep the menu open @@ -531,7 +545,7 @@ export default class Example04 { // mock data this.dataset = []; - for (let i = 0; i < 500; i++) { + for (let i = 0; i < ITEMS_COUNT; i++) { this.dataset[i] = { id: i, title: 'Task ' + i, @@ -581,39 +595,61 @@ export default class Example04 { } /** change dynamically, through slickgrid "setOptions()" the number of pinned columns */ - changeFrozenColumnCount() { - if (this.sgb?.slickGrid?.setOptions) { - this.sgb?.slickGrid.setOptions({ - frozenColumn: +this.frozenColumnCount, - }); - } + changePinnedColumnCount() { + this.setPinnedColumns(+this.pinnedColumnCount, this.pinnedRightColumnCount); + } + + changePinnedRightColumnCount() { + this.setPinnedColumns(this.pinnedColumnCount, +this.pinnedRightColumnCount); + } + + /** Toggle the demo's right-pinned Action column while preserving left pinning. */ + toggleRightPinning() { + this.setPinnedColumns(this.pinnedColumnCount, this.pinnedRightColumnCount > 0 ? 0 : 1); } /** change dynamically, through slickgrid "setOptions()" the number of pinned rows */ - changeFrozenRowCount() { + changePinnedRowCount() { if (this.sgb?.slickGrid?.setOptions) { - this.sgb?.slickGrid.setOptions({ - frozenRow: +this.frozenRowCount, + const rows = this.getPinnedRowIndexes(); + this.sgb.slickGrid.setOptions({ + pinning: { rows: this.isPinnedBottom ? { top: [], bottom: rows } : { top: rows, bottom: [] } }, }); } } - setFrozenColumns(frozenCols: number) { - this.sgb?.slickGrid?.setOptions({ frozenColumn: frozenCols, alwaysShowVerticalScroll: false }); + setPinnedColumns(pinnedCols: number, rightCols = this.pinnedRightColumnCount) { + this.sgb?.slickGrid?.setOptions({ + pinning: { + columns: { + left: pinnedCols, + right: Math.max(0, rightCols), + }, + }, + }); this.gridOptions = this.sgb?.slickGrid?.getOptions() ?? {}; - this.frozenColumnCount = frozenCols; + this.pinnedColumnCount = pinnedCols; + this.pinnedRightColumnCount = Math.max(0, rightCols); } /** toggle dynamically, through slickgrid "setOptions()" the top/bottom pinned location */ - toggleFrozenBottomRows() { + togglePinnedBottomRows() { if (this.sgb?.slickGrid && this.sgb?.slickGrid.setOptions) { - this.sgb?.slickGrid.setOptions({ - frozenBottom: !this.isFrozenBottom, + this.isPinnedBottom = !this.isPinnedBottom; + const rows = this.getPinnedRowIndexes(); + this.sgb.slickGrid.setOptions({ + pinning: { rows: this.isPinnedBottom ? { top: [], bottom: rows } : { top: rows, bottom: [] } }, }); - this.isFrozenBottom = !this.isFrozenBottom; // toggle the variable } } + private getPinnedRowIndexes() { + const rowCount = Math.max(0, +this.pinnedRowCount); + const dataLength = this.sgb?.slickGrid?.getDataLength?.() ?? this.dataset?.length ?? ITEMS_COUNT; + const firstPinnedRow = this.isPinnedBottom ? Math.max(0, dataLength - rowCount) : 0; + return Array.from({ length: rowCount }, (_value, index) => firstPinnedRow + index); + } + toggleWhichRowToShowSelectAll() { this.isSelectAllShownAsColumnTitle = !this.isSelectAllShownAsColumnTitle; this.checkboxSelectorInstance.setOptions({ @@ -651,8 +687,9 @@ export default class Example04 { } } - setLargeFreezedColumns() { - this.setFrozenColumns(2); + setLargePinnedColumns() { + // Apply target widths before pinning. Pinning validates against the + // currently rendered widths, which may still reflect a prior resize. this.sgb.gridStateService.applyColumnLayout( [ { columnId: '_checkbox_selector', cssClass: 'slick-cell-checkboxsel', headerCssClass: '', width: 40 }, @@ -663,11 +700,17 @@ export default class Example04 { { columnId: 'completed', cssClass: '', headerCssClass: '', width: 372 }, { columnId: 'cost', cssClass: '', headerCssClass: '', width: 288 }, { columnId: 'cityOfOrigin', cssClass: '', headerCssClass: '', width: 204 }, - { columnId: 'action', cssClass: '', headerCssClass: '', width: 64 }, + { columnId: 'action', cssClass: '', headerCssClass: '', width: 110 }, ], false, false ); - // console.log('setLargeFreezedColumns', this.sgb.gridStateService.getCurrentColumns()); + this.setPinnedColumns(2, this.pinnedRightColumnCount); + // console.log('setLargePinnedColumns', this.sgb.gridStateService.getCurrentColumns()); + } + + toggleSubTitle() { + this.subTitleStyle = this.subTitleStyle === 'display: block' ? 'display: none' : 'display: block'; + this.sgb.resizerService.resizeGrid(); } } diff --git a/demos/vanilla/src/examples/example05.html b/demos/vanilla/src/examples/example05.html index ae5e10483e..f40075fbb7 100644 --- a/demos/vanilla/src/examples/example05.html +++ b/demos/vanilla/src/examples/example05.html @@ -2,6 +2,9 @@

Example 05 - Tree Data (from a flat dataset with parentId references) +

-
+
NOTE: The grid will automatically sort Ascending with the column that has the Tree Data, you could add a "sortByFieldId" in your column "treeData" option if you wish to sort on a different column
diff --git a/demos/vanilla/src/examples/example05.ts b/demos/vanilla/src/examples/example05.ts index 3c6874cef6..33eb02f8d9 100644 --- a/demos/vanilla/src/examples/example05.ts +++ b/demos/vanilla/src/examples/example05.ts @@ -25,6 +25,7 @@ export default class Example05 { loadingClass = ''; isLargeDataset = false; hasNoExpandCollapseChanged = true; + subTitleStyle = 'display: block'; treeToggleItems: TreeToggledItem[] = []; constructor() { @@ -532,4 +533,9 @@ export default class Example05 { reapplyToggledItems() { this.sgb.treeDataService.applyToggledItemStateChanges(this.treeToggleItems); } + + toggleSubTitle() { + this.subTitleStyle = this.subTitleStyle === 'display: block' ? 'display: none' : 'display: block'; + this.sgb.resizerService.resizeGrid(); + } } diff --git a/demos/vanilla/src/examples/example06.html b/demos/vanilla/src/examples/example06.html index 2520dc7ace..8a75cc4d23 100644 --- a/demos/vanilla/src/examples/example06.html +++ b/demos/vanilla/src/examples/example06.html @@ -2,6 +2,9 @@

Example 06 - Tree Data with Aggregators (from a Hierarchical Dataset) +

-
+
NOTE #1: The grid will automatically sort in Ascending order with the column that has the Tree Data, you could add a "sortByFieldId" in your column "treeData" option if you wish to sort on a different column
diff --git a/demos/vanilla/src/examples/example06.ts b/demos/vanilla/src/examples/example06.ts index 5eff64b89f..4a1cb7381f 100644 --- a/demos/vanilla/src/examples/example06.ts +++ b/demos/vanilla/src/examples/example06.ts @@ -31,6 +31,7 @@ export default class Example06 { isRemoveLastInsertedPopSongDisabled = true; lastInsertedPopSongId: number | undefined; searchString = ''; + subTitleStyle = 'display: block'; attached() { this.initializeGrid(); @@ -478,4 +479,9 @@ export default class Example06 { // reset dataset to clear all tree data stat mutations (basically recreate the grid entirely to start from scratch) this.sgb.datasetHierarchical = this.mockDataset(); } + + toggleSubTitle() { + this.subTitleStyle = this.subTitleStyle === 'display: block' ? 'display: none' : 'display: block'; + this.sgb.resizerService.resizeGrid(); + } } diff --git a/demos/vanilla/src/examples/example08.html b/demos/vanilla/src/examples/example08.html index ccace4a1a1..a60a58f952 100644 --- a/demos/vanilla/src/examples/example08.html +++ b/demos/vanilla/src/examples/example08.html @@ -28,7 +28,7 @@

Grid 2 - (with Header Grouping & Frozen/Pinned Columns) + (with Header Grouping & Pinned Columns)
@@ -67,13 +67,13 @@

  
- -
diff --git a/demos/vanilla/src/examples/example08.ts b/demos/vanilla/src/examples/example08.ts index bc426a7b68..b28d58810c 100644 --- a/demos/vanilla/src/examples/example08.ts +++ b/demos/vanilla/src/examples/example08.ts @@ -144,11 +144,11 @@ export default class Example08 { showPreHeaderPanel: true, preHeaderPanelHeight: 35, explicitInitialization: true, - frozenColumn: 2, + pinning: { columns: { left: 2 } }, rowHeight: 33, showCustomFooter: true, - gridMenu: { hideClearFrozenColumnsCommand: false }, - headerMenu: { hideFreezeColumnsCommand: false }, + gridMenu: { hideClearPinningCommand: false }, + headerMenu: { hidePinColumnCommand: false, hidePinningColumnsCommand: false }, // enable the filtering but hide the user filter row since we use our own single filter enableFiltering: true, @@ -174,8 +174,15 @@ export default class Example08 { return mockDataset; } - setFrozenColumns2(frozenCols: number) { - this.sgb2.slickGrid?.setOptions({ frozenColumn: frozenCols, alwaysShowVerticalScroll: false }); + setPinnedColumns2(pinnedCols: number) { + this.sgb2.slickGrid?.setOptions({ + pinning: { + columns: { + left: pinnedCols >= 0 ? pinnedCols : [], + }, + }, + alwaysShowVerticalScroll: false, + }); this.gridOptions2 = this.sgb2.slickGrid!.getOptions(); } diff --git a/demos/vanilla/src/examples/example09.html b/demos/vanilla/src/examples/example09.html index 3e2a3d8f5b..9e21c3ac27 100644 --- a/demos/vanilla/src/examples/example09.html +++ b/demos/vanilla/src/examples/example09.html @@ -1,5 +1,8 @@

Example 09 - Grid with OData Backend Service +

-
+
NOTE: For demo purposes, the last column (filter & sort) will always throw an error and its only purpose is to demo what would happen when you encounter a backend server error (the UI should rollback to previous state before you did the action). Also changing Page Size to 50,000 will also throw which again is for demo purposes. Moreover, the example will persist diff --git a/demos/vanilla/src/examples/example09.ts b/demos/vanilla/src/examples/example09.ts index 80562f9a41..e4739d4c1d 100644 --- a/demos/vanilla/src/examples/example09.ts +++ b/demos/vanilla/src/examples/example09.ts @@ -36,6 +36,7 @@ export default class Example09 { errorStatusClass = 'hidden'; status = ''; statusClass = 'is-success'; + subTitleStyle = 'display: block'; isPageErrorTest = false; constructor() { @@ -527,6 +528,11 @@ export default class Example09 { return true; } + toggleSubTitle() { + this.subTitleStyle = this.subTitleStyle === 'display: block' ? 'display: none' : 'display: block'; + this.sgb.resizerService.resizeGrid(); + } + private resetOptions(options: Partial) { this.displaySpinner(true); const odataService = this.gridOptions.backendServiceApi!.service; diff --git a/demos/vanilla/src/examples/example10.html b/demos/vanilla/src/examples/example10.html index 0e0a1a7dc3..46ed6c6f15 100644 --- a/demos/vanilla/src/examples/example10.html +++ b/demos/vanilla/src/examples/example10.html @@ -1,13 +1,16 @@

Example 10 - Grid with GraphQL Backend Service + -