├── .gitignore ├── README.md ├── cypress.json ├── cypress ├── data │ └── dataAndColumns.ts ├── integration │ ├── autofill.tsx │ ├── custom-rendering.tsx │ ├── data-resizing.tsx │ ├── edit-events.tsx │ ├── edit-inline.tsx │ ├── focused-columns.tsx │ ├── frozen-cells.tsx │ ├── grid-resizing.tsx │ ├── keyboard-events.tsx │ ├── keyboard-nav.tsx │ ├── scrollbars.tsx │ ├── select-events-example.tsx │ ├── selection-highlight.tsx │ ├── simple-rendering.tsx │ └── small-grid.tsx ├── plugins │ └── index.js ├── snapshots │ ├── autofill.tsx │ │ ├── autofill-complete.snap.png │ │ ├── autofill-drag-outline.snap.png │ │ ├── autofill-hover-highlight.snap.png │ │ ├── multi-autofill-handle.snap.png │ │ ├── none-no-autofill-handle.snap.png │ │ ├── single-autofill-handle.snap.png │ │ └── single-multi-no-autofill-handle.snap.png │ ├── custom-rendering.tsx │ │ ├── custom-render-background.snap.png │ │ └── custom-render-text.snap.png │ ├── data-resizing.tsx │ │ ├── clear-selection-after-col-num-change.snap.png │ │ ├── clear-selection-when-cols-change.snap.png │ │ ├── clear-selection-when-num-rows-changes.snap.png │ │ ├── increase-number-of-columns.snap.png │ │ ├── keep-selection-when-only-data-values-change.snap.png │ │ ├── reduce-number-of-columns.snap.png │ │ └── truncate-scroll-when-shrinking-data.snap.png │ ├── edit-inline.tsx │ │ ├── editing-updates-grid.snap.png │ │ ├── inline-editor-arrows-dont-change-selection.snap.png │ │ └── inline-editor-shown-on-dblclick.snap.png │ ├── focused-columns.tsx │ │ ├── focused-col-to-left-with-frozen-cols.snap.png │ │ ├── focused-col-to-left.snap.png │ │ └── focused-col-to-right.snap.png │ ├── frozen-cells.tsx │ │ └── scrolled-grid-with-frozen-cells.snap.png │ ├── grid-resizing.tsx │ │ └── resize-grid-small-to-large.snap.png │ ├── scrollbars.tsx │ │ ├── scrollbar-drag-off-bar.snap.png │ │ ├── scrollbar-hover.snap.png │ │ └── scrollbar-release-drag-off-bar.snap.png │ ├── selection-highlight.tsx │ │ ├── selection-highlight-click-shift-click.snap.png │ │ ├── simple-grid-after-click-then-scroll.snap.png │ │ ├── simple-grid-after-click.snap.png │ │ ├── simple-grid-drag-down-and-right.snap.png │ │ ├── simple-grid-drag-down.snap.png │ │ ├── simple-grid-drag-left.snap.png │ │ ├── simple-grid-drag-release-move.snap.png │ │ ├── simple-grid-drag-right-and-release.snap.png │ │ ├── simple-grid-drag-right.snap.png │ │ └── simple-grid-drag-up.snap.png │ ├── simple-rendering.tsx │ │ ├── scrolled-grid-in-scroll.snap.png │ │ └── simple-grid-in-scroll.snap.png │ └── small-grid.tsx │ │ └── small-grid.snap.png ├── support │ ├── commands.js │ ├── imageSnapshot.d.ts │ └── index.js └── tsconfig.json ├── examples ├── .env ├── .gitignore ├── package-lock.json ├── package.json ├── public │ └── index.html ├── src │ ├── App.css │ ├── App.tsx │ ├── Examples.css │ ├── Examples.tsx │ ├── Home.css │ ├── Home.tsx │ ├── assets │ │ ├── autofill.png │ │ ├── custom-render.png │ │ ├── feature-icon-gradient.svg │ │ ├── frozen-rows-cols.png │ │ ├── inline-edit.png │ │ ├── logo.svg │ │ ├── react-js.svg │ │ ├── rocket-launch-lines.svg │ │ ├── selection-range.png │ │ └── structure.svg │ ├── components │ │ ├── ControlsForm.css │ │ ├── ControlsForm.tsx │ │ ├── EventLog.css │ │ ├── EventLog.tsx │ │ ├── FixedSizeHolder.css │ │ └── FixedSizeHolder.tsx │ ├── data │ │ └── dataAndColumns.ts │ ├── examples │ │ ├── Autofill.data.ts │ │ ├── Autofill.grid.tsx │ │ ├── Autofill.text.tsx │ │ ├── CustomBackground.grid.tsx │ │ ├── CustomBackground.text.tsx │ │ ├── CustomText.grid.tsx │ │ ├── CustomText.text.tsx │ │ ├── CustomTitle.grid.tsx │ │ ├── CustomTitle.text.tsx │ │ ├── DynamicData.grid.tsx │ │ ├── DynamicData.text.tsx │ │ ├── EditEvents.grid.tsx │ │ ├── EditEvents.text.tsx │ │ ├── Editable.grid.tsx │ │ ├── Editable.text.tsx │ │ ├── Everything.css │ │ ├── Everything.grid.tsx │ │ ├── ExamplePage.css │ │ ├── ExamplePage.tsx │ │ ├── FocusColumn.grid.tsx │ │ ├── FocusColumn.text.tsx │ │ ├── FrozenCells.grid.tsx │ │ ├── FrozenCells.text.tsx │ │ ├── Index.tsx │ │ ├── KeyboardEvents.grid.tsx │ │ ├── KeyboardEvents.text.tsx │ │ ├── Resize.grid.tsx │ │ ├── Resize.text.tsx │ │ ├── SelectionEvents.grid.tsx │ │ ├── SelectionEvents.text.tsx │ │ ├── Simple.grid.tsx │ │ ├── Simple.text.tsx │ │ ├── SmallGrid.grid.tsx │ │ ├── SmallGrid.text.tsx │ │ └── exampleMeta.tsx │ ├── index.css │ ├── index.tsx │ └── react-app-env.d.ts └── tsconfig.json ├── jest.config.js ├── package-lock.json ├── package.json ├── setupTests.js ├── src ├── FrozenCanvas.tsx ├── FrozenColsCanvas.tsx ├── FrozenCornerCanvas.tsx ├── FrozenRowsCanvas.tsx ├── GridCanvas.test.tsx ├── GridCanvas.tsx ├── HighlightCanvas.tsx ├── HighlightedGridCanvas.tsx ├── InlineEditor.tsx ├── MainCanvas.tsx ├── ReactCanvasGrid.test.tsx ├── ReactCanvasGrid.tsx ├── autofill.test.ts ├── autofill.ts ├── baseGridOffsetRenderer.ts ├── cellRenderer.ts ├── commonCanvasRenderer.test.ts ├── commonCanvasRenderer.ts ├── eventHandlers │ ├── autofillMouseEvents.test.ts │ ├── autofillMouseEvents.ts │ ├── gridMouseEvents.test.ts │ ├── gridMouseEvents.ts │ ├── keyboardEvents.test.ts │ ├── keyboardEvents.ts │ ├── mouseCellAndRegionCalc.ts │ ├── mouseEvents.ts │ ├── scrollbarMouseEvents.ts │ ├── scrolling.test.ts │ ├── scrolling.ts │ ├── scrollingTimer.test.ts │ └── scrollingTimer.ts ├── gridCanvasRenderer.test.ts ├── gridCanvasRenderer.ts ├── gridGeometry.test.ts ├── gridGeometry.ts ├── gridState.ts ├── highlightCanvasRenderer.ts ├── index.ts ├── rafTestHelper.ts ├── scrollbarGeometry.test.ts ├── scrollbarGeometry.ts ├── scrollbars │ ├── CornerScrollbarCanvas.tsx │ ├── HorizontalScrollbarCanvas.tsx │ ├── ScrollbarCanvas.tsx │ ├── VerticalScrollbarCanvas.tsx │ ├── baseScrollbarRenderer.ts │ ├── cornerScrollbarRenderer.ts │ ├── horizontalScrollbarRenderer.ts │ └── verticalScrollbarRenderer.ts ├── selectionState │ ├── allGridSelection.test.ts │ ├── allGridSelection.ts │ ├── cellsSelection.test.ts │ ├── cellsSelection.ts │ ├── cellsSelectionBuilder.ts │ ├── colSelection.test.ts │ ├── colsSelection.ts │ ├── focusOffset.ts │ ├── noSelection.ts │ ├── rowsSelection.test.ts │ ├── rowsSelection.ts │ ├── selectionState.ts │ ├── selectionStateFactory.ts │ └── selectionTypes.ts ├── types.ts └── utils.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | __diff_output__ 4 | coverage 5 | cypress/screenshots -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [react-canvas-grid](https://rowanhill.github.io/react-canvas-grid) 2 | A canvas-powered, React-friendly datagrid component 3 | 4 | Note: This library is still considered beta, because it currently has few consumers. Feel free to give it a try and report any issues! 5 | 6 | There are already lots of excellent grid / table components that can be used with React, so why another one? Well, react-canvas-grid's 7 | particular goals are: 8 | * First-class support for complex data objects - rather than assuming every cell's value is a simple scalar 9 | * High performance handling of all major operations (data editing, scrolling, selecting, etc) even with large grids - no visible redaws, lag, or hanging the main thead 10 | * Range selection (although only a single range at a time), including the possibility to bulk update the selected area of the grid 11 | * Arbitrary numbers of frozen rows / columns 12 | * Support for column reordering / filtering (even if that's largely handled externally to the component) 13 | * Column reordering by dragging (i.e. internally to the component) 14 | * Focus/scroll to a column (for use with an external 'search' function) 15 | 16 | There was also originally a goal of making use of browser-native scrolling, although that ultimately wasn't quite tennable. :man_shrugging: 17 | 18 | ## TODO :memo: 19 | * Middle-click "auto-scrolling" 20 | * Column dragging (callback to with suggested reordering of ColumnDefs, up to parent to actually re-order & re-render) 21 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "video": false, 3 | "baseUrl": "http://localhost:3000" 4 | } -------------------------------------------------------------------------------- /cypress/data/dataAndColumns.ts: -------------------------------------------------------------------------------- 1 | import { ColumnDef, DataRow } from '../../src/types'; 2 | 3 | export function createFakeDataAndColumns(numRows: number, numCols: number, dataGen: (x: number, y: number) => T) { 4 | const cols: ColumnDef[] = []; 5 | for (let i = 0; i < numCols; i++) { 6 | cols.push({ 7 | fieldName: `col-${i}`, 8 | width: 50, 9 | }); 10 | } 11 | 12 | const rows: Array> = []; 13 | for (let i = 0; i < numRows; i++) { 14 | const row: DataRow = {}; 15 | for (let j = 0; j < numCols; j++) { 16 | row[`col-${j}`] = { 17 | getText: () => `${i + 1}x${j + 1}`, 18 | data: dataGen(j, i), 19 | }; 20 | } 21 | rows.push(row); 22 | } 23 | 24 | return { 25 | columns: cols, 26 | rows, 27 | }; 28 | } 29 | 30 | export function createEditableFakeDataAndColumns( 31 | numRows: number, numCols: number, dataGen: (x: number, y: number) => string, 32 | ) { 33 | const cols: ColumnDef[] = []; 34 | for (let i = 0; i < numCols; i++) { 35 | cols.push({ 36 | fieldName: `col-${i}`, 37 | width: 50, 38 | }); 39 | } 40 | 41 | const rows: Array> = []; 42 | for (let i = 0; i < numRows; i++) { 43 | const row: DataRow = {}; 44 | for (let j = 0; j < numCols; j++) { 45 | row[`col-${j}`] = { 46 | getText: (data: string) => data, 47 | data: dataGen(j, i), 48 | editor: { 49 | serialise: (data: string) => data, 50 | deserialise: (value: string) => value, 51 | }, 52 | }; 53 | } 54 | rows.push(row); 55 | } 56 | 57 | return { 58 | columns: cols, 59 | rows, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /cypress/integration/autofill.tsx: -------------------------------------------------------------------------------- 1 | describe('ReactCanvasGrid autofill', () => { 2 | beforeEach(() => { 3 | cy.visit('/#/examples/autofill'); 4 | 5 | cy.get('.fixed-size-holder').as('Holder'); 6 | cy.get('.fixed-size-holder .react-canvas-grid').as('Root'); 7 | cy.get('.fixed-size-holder canvas').eq(1).as('Canvas'); 8 | 9 | cy.get('Canvas').invoke('width').should('be.greaterThan', 0); 10 | }); 11 | 12 | describe('multi mode', () => { 13 | it('displays an autofill handle regardless of selection size', () => { 14 | cy.get('@Root') 15 | .trigger('mousedown', 60, 30, { buttons: 1, force: true }) 16 | .trigger('mousemove', 120, 75, { buttons: 1, force: true }) 17 | .trigger('mouseup', 120, 75, { force: true }) 18 | .matchImageSnapshot('multi-autofill-handle'); 19 | }); 20 | }); 21 | 22 | describe('single mode', () => { 23 | beforeEach(() => { 24 | cy.get('input[value="single"]') 25 | .click(); 26 | }); 27 | 28 | it('displays an autofill handle on a one-cell selection', () => { 29 | cy.get('@Root') 30 | .click(60, 30, { force: true }) 31 | .matchImageSnapshot('single-autofill-handle'); 32 | }); 33 | 34 | it('does not display an autofill handle on a multi-cell selection', () => { 35 | cy.get('@Root') 36 | .trigger('mousedown', 60, 30, { buttons: 1, force: true }) 37 | .trigger('mousemove', 120, 75, { buttons: 1, force: true }) 38 | .trigger('mouseup', 120, 75, { force: true }) 39 | .matchImageSnapshot('single-multi-no-autofill-handle'); 40 | }); 41 | }); 42 | 43 | describe('none mode', () => { 44 | beforeEach(() => { 45 | cy.get('input[value="none"]') 46 | .click(); 47 | }); 48 | 49 | it('does not display an autofill handle regardless of selection size', () => { 50 | cy.get('@Root') 51 | .click(60, 30, { force: true }) 52 | .matchImageSnapshot('none-no-autofill-handle'); 53 | }); 54 | }); 55 | 56 | it('displays an outline around the area to be filled before filling', () => { 57 | cy.get('@Root') 58 | .trigger('mousedown', 60, 30, { buttons: 1, force: true }) 59 | .trigger('mousemove', 120, 75, { buttons: 1, force: true }) 60 | .trigger('mouseup', 120, 75, { force: true }); 61 | 62 | cy.get('@Root') 63 | .trigger('mousedown', 150, 80, { buttons: 1, force: true }) 64 | .trigger('mousemove', 230, 80, { buttons: 1, force: true }) 65 | .matchImageSnapshot('autofill-drag-outline'); 66 | 67 | cy.get('@Root') 68 | .trigger('mouseup', 230, 80, { force: true }) 69 | .matchImageSnapshot('autofill-complete'); 70 | }); 71 | 72 | it('highlights the autofill handle on hover and changes the cursor to crosshair', () => { 73 | cy.get('@Root') 74 | // Set up a selection 75 | .trigger('mousedown', 60, 30, { buttons: 1, force: true }) 76 | .trigger('mousemove', 120, 75, { buttons: 1, force: true }) 77 | .trigger('mouseup', 120, 75, { force: true }) 78 | // Hover over the autofill handle 79 | .trigger('mousemove', 150, 80, { buttons: 1, force: true }) 80 | .matchImageSnapshot('autofill-hover-highlight'); 81 | 82 | cy.get('@Root') 83 | .should('have.css', 'cursor', 'crosshair'); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /cypress/integration/custom-rendering.tsx: -------------------------------------------------------------------------------- 1 | describe('ReactCanvasGrid', () => { 2 | it('uses custom background renderers from cell data', () => { 3 | cy.visit('/#/examples/custom-bg'); 4 | cy.get('.fixed-size-holder .react-canvas-grid').as('Root'); 5 | cy.get('.fixed-size-holder canvas').eq(1).as('Canvas'); 6 | 7 | cy.get('Canvas').invoke('width').should('be.greaterThan', 0); 8 | 9 | cy.get('@Root').matchImageSnapshot('custom-render-background'); 10 | }); 11 | 12 | it('uses custom text renderers from cell data', () => { 13 | cy.visit('/#/examples/custom-text'); 14 | cy.get('.fixed-size-holder .react-canvas-grid').as('Root'); 15 | cy.get('.fixed-size-holder canvas').eq(1).as('Canvas'); 16 | 17 | cy.get('Canvas').invoke('width').should('be.greaterThan', 0); 18 | 19 | cy.get('@Root').matchImageSnapshot('custom-render-text'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /cypress/integration/data-resizing.tsx: -------------------------------------------------------------------------------- 1 | describe('ReactCanvasGrid with some initial data', () => { 2 | beforeEach(() => { 3 | cy.visit('/#/examples/dynamic-data'); 4 | 5 | cy.get('.fixed-size-holder').as('Holder'); 6 | cy.get('.fixed-size-holder .react-canvas-grid').as('Root'); 7 | cy.get('.fixed-size-holder canvas').eq(1).as('Canvas'); 8 | 9 | cy.get('#num-rows').as('NumRowsInput'); 10 | cy.get('#num-cols').as('NumColsInput'); 11 | cy.get('#first-col-to-end').as('FirstColToEndButton'); 12 | cy.get('#modify-top-left').as('ModifyTopLeftButton'); 13 | 14 | cy.get('Canvas').invoke('width').should('be.greaterThan', 0); 15 | }); 16 | 17 | it('re-renders when the number of columns shrinks', () => { 18 | cy.get('@NumColsInput') 19 | .type('{selectall}5'); 20 | cy.wait(100); // Wait for 80ms debounce to trigger 21 | cy.get('@Root') 22 | .matchImageSnapshot('reduce-number-of-columns'); 23 | }); 24 | 25 | it('re-renders when the number of columns grows', () => { 26 | cy.get('@NumColsInput') 27 | .type('{selectall}50'); 28 | cy.get('@NumRowsInput') 29 | .type('{selectall}100'); 30 | cy.wait(100); // Wait for 80ms debounce to trigger 31 | 32 | cy.get('@Root') 33 | .matchImageSnapshot('increase-number-of-columns'); 34 | }); 35 | 36 | it('clears the selection when the number of columns changes', () => { 37 | cy.get('@Root') 38 | .click(); 39 | cy.get('@NumColsInput') 40 | .type('{selectall}5'); 41 | cy.wait(100); // Wait for 80ms debounce to trigger 42 | cy.get('@Root') 43 | .matchImageSnapshot('clear-selection-after-col-num-change'); 44 | }); 45 | 46 | it('clears the selection when the columns change order', () => { 47 | cy.get('@Root') 48 | .click(); 49 | cy.get('@FirstColToEndButton') 50 | .click(); 51 | cy.get('@Root') 52 | .matchImageSnapshot('clear-selection-when-cols-change'); 53 | }); 54 | 55 | it('clears the selection when the number of rows changes', () => { 56 | cy.get('@Root') 57 | .click(); 58 | cy.get('@NumRowsInput') 59 | .type('{selectall}100'); 60 | cy.wait(100); // Wait for 80ms debounce to trigger 61 | cy.get('@Root') 62 | .matchImageSnapshot('clear-selection-when-num-rows-changes'); 63 | }); 64 | 65 | it('does not clear the selection when the data changes but has the same number of rows', () => { 66 | cy.get('@Root') 67 | .click(); 68 | cy.get('@ModifyTopLeftButton') 69 | .click(); 70 | 71 | cy.get('@Root') 72 | .matchImageSnapshot('keep-selection-when-only-data-values-change'); 73 | }); 74 | 75 | it('constrains the scroll when the data and/or cols shrink', () => { 76 | cy.get('@NumColsInput') 77 | .type('{selectall}50'); 78 | cy.get('@NumRowsInput') 79 | .type('{selectall}100'); 80 | cy.wait(100); // Wait for 80ms debounce to trigger 81 | 82 | cy.get('@Root') 83 | .trigger('wheel', { deltaX: 800, deltaY: 300 }); 84 | 85 | cy.get('@NumColsInput') 86 | .type('{selectall}20'); 87 | cy.get('@NumRowsInput') 88 | .type('{selectall}20'); 89 | cy.wait(100); // Wait for 80ms debounce to trigger 90 | cy.get('@Root') 91 | .matchImageSnapshot('truncate-scroll-when-shrinking-data'); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /cypress/integration/edit-events.tsx: -------------------------------------------------------------------------------- 1 | describe('ReactCanvasGrid', () => { 2 | beforeEach(() => { 3 | cy.visit('/#/examples/edit-events'); 4 | 5 | cy.get('.fixed-size-holder').as('Holder'); 6 | cy.get('.fixed-size-holder .react-canvas-grid').as('Root'); 7 | cy.get('.fixed-size-holder canvas').eq(1).as('Canvas'); 8 | cy.get('textarea').as('Log'); 9 | 10 | cy.get('Canvas').invoke('width').should('be.greaterThan', 0); 11 | }); 12 | 13 | describe('after double-clicking an editable cell', () => { 14 | beforeEach(() => { 15 | cy.get('@Root') 16 | .trigger('dblclick', 5, 5, { force: true }); 17 | }); 18 | 19 | it('fires onCellDataChanged event when editing and hitting enter', () => { 20 | cy.get('input') 21 | .type('a{enter}'); 22 | cy.get('@Log') 23 | .invoke('text') 24 | .should('equal', 'changed: row 0 of column "col-0" (index 0) to 0,0a\n'); 25 | }); 26 | 27 | it('fires onCellDataChanged event when simply hitting enter', () => { 28 | cy.get('input') 29 | .type('{enter}'); 30 | cy.get('@Log') 31 | .invoke('text') 32 | .should('equal', 'changed: row 0 of column "col-0" (index 0) to 0,0\n'); 33 | }); 34 | 35 | it('fires onCellDataChanged event when editing clicking elsewhere', () => { 36 | cy.get('input') 37 | .type('a') 38 | .then(() => cy.get('@Canvas').click({ force: true })); 39 | cy.get('@Log') 40 | .invoke('text') 41 | .should('equal', 'changed: row 0 of column "col-0" (index 0) to 0,0a\n'); 42 | }); 43 | 44 | it('fires onCellDataChanged event when simply clicking elsewhere', () => { 45 | cy.get('@Canvas').click({ force: true }); 46 | cy.get('@Log') 47 | .invoke('text') 48 | .should('equal', 'changed: row 0 of column "col-0" (index 0) to 0,0\n'); 49 | }); 50 | 51 | it('does not fire onCellDataChanged when hitting escape', () => { 52 | cy.get('input') 53 | .type('{esc}'); 54 | cy.get('@Log') 55 | .invoke('text') 56 | .should('equal', ''); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /cypress/integration/edit-inline.tsx: -------------------------------------------------------------------------------- 1 | describe('ReactCanvasGrid with inline editor & data management callbacks', () => { 2 | beforeEach(() => { 3 | cy.visit('/#/examples/editable'); 4 | 5 | cy.get('.fixed-size-holder').as('Holder'); 6 | cy.get('.fixed-size-holder .react-canvas-grid').as('Root'); 7 | cy.get('.fixed-size-holder canvas').eq(1).as('Canvas'); 8 | 9 | cy.get('Canvas').invoke('width').should('be.greaterThan', 0); 10 | }); 11 | 12 | it('shows the inline editor over the cell on double-click', () => { 13 | kludgeCaretInvisible(); 14 | cy.get('@Canvas') 15 | .trigger('dblclick', 5, 5, { force: true }); 16 | cy.get('@Root') 17 | .matchImageSnapshot('inline-editor-shown-on-dblclick'); 18 | }); 19 | 20 | it('re-renders the grid when data is edited', () => { 21 | cy.get('@Canvas') 22 | .trigger('dblclick', 5, 5, { force: true }); 23 | cy.get('@Canvas').get('input').type('{selectall}99,99{enter}'); 24 | cy.get('@Root') 25 | .matchImageSnapshot('editing-updates-grid'); 26 | }); 27 | 28 | it('does not update the selection when using the arrow keys whilst editing text', () => { 29 | kludgeCaretInvisible(); 30 | cy.get('@Canvas') 31 | .click(5, 5, { force: true }) 32 | .trigger('dblclick', 5, 5, { force: true }) 33 | .get('input') 34 | .type('{downarrow}{rightarrow}'); 35 | cy.get('@Root') 36 | .matchImageSnapshot('inline-editor-arrows-dont-change-selection'); 37 | }); 38 | }); 39 | 40 | /** 41 | * Hack to hide the text insertion caret. Otherwise, the blinking caret is sometimes present 42 | * in screenshots and sometimes not, meaning the tests are flakey. 43 | */ 44 | function kludgeCaretInvisible() { 45 | cy.get('@Root') 46 | .invoke('css', 'caret-color', 'transparent'); 47 | } 48 | -------------------------------------------------------------------------------- /cypress/integration/focused-columns.tsx: -------------------------------------------------------------------------------- 1 | describe('ReactCanvasGrid', () => { 2 | beforeEach(() => { 3 | cy.visit('/#/examples/focused-column'); 4 | 5 | cy.get('.fixed-size-holder').as('Holder'); 6 | cy.get('.fixed-size-holder .react-canvas-grid').as('Root'); 7 | cy.get('.fixed-size-holder canvas').eq(1).as('Canvas'); 8 | 9 | cy.get('form select').as('FocusedColSelect'); 10 | cy.get('form input').as('FrozenColsToggle'); 11 | 12 | cy.get('Canvas').invoke('width').should('be.greaterThan', 0); 13 | }); 14 | 15 | it('scrolls to the right to bring a focused column to the right into view', () => { 16 | cy.get('@FocusedColSelect').select('30'); 17 | 18 | cy.get('@Root') 19 | .matchImageSnapshot('focused-col-to-right'); 20 | }); 21 | 22 | it('scrolls to the left to bring a focused column to the left into view', () => { 23 | cy.get('@Root') 24 | .trigger('wheel', { deltaX: 800, deltaY: 300 }); 25 | 26 | cy.get('@FocusedColSelect').select('2'); 27 | 28 | cy.get('@Root') 29 | .matchImageSnapshot('focused-col-to-left'); 30 | }); 31 | 32 | it('accounts for frozen columns when scrolling left to bring a frozen column into view', () => { 33 | cy.get('@FrozenColsToggle').click(); 34 | 35 | cy.get('@Root') 36 | .trigger('wheel', { deltaX: 800, deltaY: 300 }); 37 | 38 | cy.get('@FocusedColSelect').select('5'); 39 | 40 | cy.get('@Root') 41 | .matchImageSnapshot('focused-col-to-left-with-frozen-cols'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /cypress/integration/frozen-cells.tsx: -------------------------------------------------------------------------------- 1 | describe('ReactCanvasGrid with frozen rows & cells', () => { 2 | beforeEach(() => { 3 | cy.visit('/#/examples/frozen'); 4 | cy.get('.fixed-size-holder').as('Holder'); 5 | cy.get('.fixed-size-holder .react-canvas-grid').as('Root'); 6 | cy.get('.fixed-size-holder canvas').eq(1).as('Canvas'); 7 | 8 | cy.get('Canvas').invoke('width').should('be.greaterThan', 0); 9 | }); 10 | 11 | it('keeps the frozen rows and columns shown on the grid (and fixes the top-left cells in place)', () => { 12 | cy.get('@Root') 13 | .trigger('wheel', { deltaX: 300, deltaY: 300 }) 14 | .matchImageSnapshot('scrolled-grid-with-frozen-cells'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /cypress/integration/grid-resizing.tsx: -------------------------------------------------------------------------------- 1 | describe('ReactCanvasGrid with resizable cssWidth / cssHeight', () => { 2 | beforeEach(() => { 3 | cy.visit('/#/examples/resize'); 4 | cy.get('.react-canvas-grid').as('Root'); 5 | cy.get('.react-canvas-grid canvas').eq(1).as('Canvas'); 6 | 7 | cy.get('select').as('SizeSelect'); 8 | 9 | cy.get('Canvas').invoke('width').should('be.greaterThan', 0); 10 | }); 11 | 12 | it('redraws when cssWidth / cssHeight are increased', () => { 13 | cy.get('@SizeSelect').select('big'); 14 | 15 | cy.get('@Canvas') 16 | .matchImageSnapshot('resize-grid-small-to-large'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /cypress/integration/keyboard-events.tsx: -------------------------------------------------------------------------------- 1 | describe('ReactCanvasGrid', () => { 2 | beforeEach(() => { 3 | cy.visit('/#/examples/keyboard-events'); 4 | 5 | cy.get('.fixed-size-holder').as('Holder'); 6 | cy.get('.fixed-size-holder .react-canvas-grid').as('Root'); 7 | cy.get('.fixed-size-holder canvas').eq(1).as('Canvas'); 8 | cy.get('textarea').as('Log'); 9 | 10 | cy.get('Canvas').invoke('width').should('be.greaterThan', 0); 11 | }); 12 | 13 | describe('after focusing the component', () => { 14 | beforeEach(() => { 15 | cy.get('@Root').click(); 16 | }); 17 | 18 | it('logs an event when pressing a key', () => { 19 | cy.get('@Root').type('a'); 20 | 21 | cy.get('@Log') 22 | .invoke('text') 23 | .should('equal', 'key up: a\n'); 24 | }); 25 | }); 26 | 27 | describe('after opening the inline editor', () => { 28 | it('logs an event when pressing a key', () => { 29 | cy.get('@Root') 30 | .trigger('dblclick', 'center', { force: true }); 31 | cy.get('@Root').get('input').type('a'); 32 | cy.get('@Log') 33 | .invoke('text') 34 | .should('equal', 'key up: a\n'); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /cypress/integration/scrollbars.tsx: -------------------------------------------------------------------------------- 1 | describe('The scrollbars in a grid larger than the canvas', () => { 2 | beforeEach(() => { 3 | cy.visit('/#/examples/simple'); 4 | cy.get('.fixed-size-holder').as('Holder'); 5 | cy.get('.fixed-size-holder .react-canvas-grid').as('Root'); 6 | cy.get('.fixed-size-holder canvas').eq(1).as('Canvas'); 7 | 8 | cy.get('Canvas').invoke('width').should('be.greaterThan', 0); 9 | }); 10 | 11 | it('get darker and larger when hovered', () => { 12 | cy.get('@Holder') 13 | .trigger('mousemove', 10, 395) 14 | .wait(10) // Small pause to ensure grid repaints before screenshot 15 | .matchImageSnapshot('scrollbar-hover'); 16 | }); 17 | 18 | it('stay darker and larger when dragging, even if the mouse is no longer hovering over the bar', () => { 19 | cy.get('@Holder') 20 | .trigger('mousemove', 10, 395) 21 | .trigger('mousedown', 10, 395, { buttons: 1 }) 22 | .trigger('mousemove', 20, 350, { buttons: 1 }) 23 | .wait(10) // Small pause to ensure grid repaints before screenshot 24 | .matchImageSnapshot('scrollbar-drag-off-bar'); 25 | }); 26 | 27 | it('reset when releasing a drag and no longer hovering, without further mousemove', () => { 28 | cy.get('@Holder') 29 | .trigger('mousemove', 10, 395) 30 | .trigger('mousedown', 10, 395, { buttons: 1 }) 31 | .trigger('mousemove', 20, 350, { buttons: 1 }) 32 | .trigger('mouseup', 20, 350) 33 | .wait(10) // Small pause to ensure grid repaints before screenshot 34 | .matchImageSnapshot('scrollbar-release-drag-off-bar'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /cypress/integration/simple-rendering.tsx: -------------------------------------------------------------------------------- 1 | describe('ReactCanvasGrid in a fixed size parent', () => { 2 | beforeEach(() => { 3 | cy.visit('/#/examples/simple'); 4 | cy.get('.fixed-size-holder').as('Holder'); 5 | cy.get('.fixed-size-holder canvas').eq(1).as('Canvas'); 6 | 7 | cy.get('Canvas').invoke('width').should('be.greaterThan', 0); 8 | }); 9 | 10 | it('renders a grid of data', () => { 11 | cy.get('@Canvas').matchImageSnapshot('simple-grid-in-scroll'); 12 | }); 13 | 14 | it('can be scrolled to the middle', () => { 15 | cy.get('@Holder') 16 | .trigger('wheel', { deltaX: 300, deltaY: 300 }); 17 | 18 | cy.get('@Canvas') 19 | .matchImageSnapshot('scrolled-grid-in-scroll'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /cypress/integration/small-grid.tsx: -------------------------------------------------------------------------------- 1 | describe('ReactCanvasGrid with very little data', () => { 2 | beforeEach(() => { 3 | cy.visit('/#/examples/small'); 4 | }); 5 | 6 | it('renders a grid of data', () => { 7 | cy.get('div.react-canvas-grid > canvas:first-of-type').matchImageSnapshot('small-grid'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | const webpack = require('@cypress/webpack-preprocessor'); 12 | const { addMatchImageSnapshotPlugin } = require('cypress-image-snapshot/plugin'); 13 | 14 | // This function is called when a project is opened or re-opened (e.g. due to 15 | // the project's config changing) 16 | module.exports = (on, config) => { 17 | on('file:preprocessor', webpack({ 18 | webpackOptions: require('../../webpack.config'), 19 | })); 20 | 21 | addMatchImageSnapshotPlugin(on, config); 22 | }; -------------------------------------------------------------------------------- /cypress/snapshots/autofill.tsx/autofill-complete.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/autofill.tsx/autofill-complete.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/autofill.tsx/autofill-drag-outline.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/autofill.tsx/autofill-drag-outline.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/autofill.tsx/autofill-hover-highlight.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/autofill.tsx/autofill-hover-highlight.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/autofill.tsx/multi-autofill-handle.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/autofill.tsx/multi-autofill-handle.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/autofill.tsx/none-no-autofill-handle.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/autofill.tsx/none-no-autofill-handle.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/autofill.tsx/single-autofill-handle.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/autofill.tsx/single-autofill-handle.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/autofill.tsx/single-multi-no-autofill-handle.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/autofill.tsx/single-multi-no-autofill-handle.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/custom-rendering.tsx/custom-render-background.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/custom-rendering.tsx/custom-render-background.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/custom-rendering.tsx/custom-render-text.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/custom-rendering.tsx/custom-render-text.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/data-resizing.tsx/clear-selection-after-col-num-change.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/data-resizing.tsx/clear-selection-after-col-num-change.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/data-resizing.tsx/clear-selection-when-cols-change.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/data-resizing.tsx/clear-selection-when-cols-change.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/data-resizing.tsx/clear-selection-when-num-rows-changes.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/data-resizing.tsx/clear-selection-when-num-rows-changes.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/data-resizing.tsx/increase-number-of-columns.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/data-resizing.tsx/increase-number-of-columns.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/data-resizing.tsx/keep-selection-when-only-data-values-change.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/data-resizing.tsx/keep-selection-when-only-data-values-change.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/data-resizing.tsx/reduce-number-of-columns.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/data-resizing.tsx/reduce-number-of-columns.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/data-resizing.tsx/truncate-scroll-when-shrinking-data.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/data-resizing.tsx/truncate-scroll-when-shrinking-data.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/edit-inline.tsx/editing-updates-grid.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/edit-inline.tsx/editing-updates-grid.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/edit-inline.tsx/inline-editor-arrows-dont-change-selection.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/edit-inline.tsx/inline-editor-arrows-dont-change-selection.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/edit-inline.tsx/inline-editor-shown-on-dblclick.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/edit-inline.tsx/inline-editor-shown-on-dblclick.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/focused-columns.tsx/focused-col-to-left-with-frozen-cols.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/focused-columns.tsx/focused-col-to-left-with-frozen-cols.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/focused-columns.tsx/focused-col-to-left.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/focused-columns.tsx/focused-col-to-left.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/focused-columns.tsx/focused-col-to-right.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/focused-columns.tsx/focused-col-to-right.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/frozen-cells.tsx/scrolled-grid-with-frozen-cells.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/frozen-cells.tsx/scrolled-grid-with-frozen-cells.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/grid-resizing.tsx/resize-grid-small-to-large.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/grid-resizing.tsx/resize-grid-small-to-large.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/scrollbars.tsx/scrollbar-drag-off-bar.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/scrollbars.tsx/scrollbar-drag-off-bar.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/scrollbars.tsx/scrollbar-hover.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/scrollbars.tsx/scrollbar-hover.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/scrollbars.tsx/scrollbar-release-drag-off-bar.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/scrollbars.tsx/scrollbar-release-drag-off-bar.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/selection-highlight.tsx/selection-highlight-click-shift-click.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/selection-highlight.tsx/selection-highlight-click-shift-click.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/selection-highlight.tsx/simple-grid-after-click-then-scroll.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/selection-highlight.tsx/simple-grid-after-click-then-scroll.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/selection-highlight.tsx/simple-grid-after-click.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/selection-highlight.tsx/simple-grid-after-click.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/selection-highlight.tsx/simple-grid-drag-down-and-right.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/selection-highlight.tsx/simple-grid-drag-down-and-right.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/selection-highlight.tsx/simple-grid-drag-down.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/selection-highlight.tsx/simple-grid-drag-down.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/selection-highlight.tsx/simple-grid-drag-left.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/selection-highlight.tsx/simple-grid-drag-left.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/selection-highlight.tsx/simple-grid-drag-release-move.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/selection-highlight.tsx/simple-grid-drag-release-move.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/selection-highlight.tsx/simple-grid-drag-right-and-release.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/selection-highlight.tsx/simple-grid-drag-right-and-release.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/selection-highlight.tsx/simple-grid-drag-right.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/selection-highlight.tsx/simple-grid-drag-right.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/selection-highlight.tsx/simple-grid-drag-up.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/selection-highlight.tsx/simple-grid-drag-up.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/simple-rendering.tsx/scrolled-grid-in-scroll.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/simple-rendering.tsx/scrolled-grid-in-scroll.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/simple-rendering.tsx/simple-grid-in-scroll.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/simple-rendering.tsx/simple-grid-in-scroll.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/small-grid.tsx/small-grid.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/cypress/snapshots/small-grid.tsx/small-grid.snap.png -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | 27 | import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command'; 28 | 29 | addMatchImageSnapshotCommand(); -------------------------------------------------------------------------------- /cypress/support/imageSnapshot.d.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line:no-namespace 2 | declare namespace Cypress { 3 | interface Chainable { 4 | matchImageSnapshot: (name?: string) => Chainable; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "baseUrl": "../node_modules", 5 | "target": "es5", 6 | "lib": ["es5", "dom"], 7 | "types": ["cypress"], 8 | "jsx": "react", 9 | "esModuleInterop": true, 10 | }, 11 | "include": [ 12 | "**/*.ts", "**/*.tsx" 13 | ], 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } -------------------------------------------------------------------------------- /examples/.env: -------------------------------------------------------------------------------- 1 | # The package.json in the parent folder may hold conflicting versions of dependencies, 2 | # which create-react-app reports as an error / source of subtle bugs. It doesn't seem 3 | # to be a problem in practice, however, so the following disables that check. 4 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Ignore the .grid.tsx files in public/examples - they're copied over at build time 26 | /public/examples/*.grid.tsx -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-canvas-grid-examples", 3 | "version": "0.1.0", 4 | "homepage": "https://rowanhill.github.io/react-canvas-grid", 5 | "dependencies": { 6 | "@types/jest": "23.3.13", 7 | "@types/node": "10.12.21", 8 | "@types/react": "16.8.1", 9 | "@types/react-dom": "16.0.11", 10 | "@types/react-router": "^5.0.3", 11 | "@types/react-router-dom": "^4.3.4", 12 | "prism-react-renderer": "^1.1.1", 13 | "react": "^16.7.0", 14 | "react-canvas-grid": "file:../.", 15 | "react-dom": "^16.7.0", 16 | "react-router": "^5.0.1", 17 | "react-router-dom": "^5.0.1", 18 | "react-scripts": "^3.4.1", 19 | "typescript": "3.3.1" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "npm run copy-examples && react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject", 26 | "copy-examples": "cd src && copy examples/*.grid.tsx ../public", 27 | "predeploy": "npm run build", 28 | "deploy": "NODE_DEBUG=gh-pages gh-pages -d build" 29 | }, 30 | "eslintConfig": { 31 | "extends": "react-app" 32 | }, 33 | "browserslist": [ 34 | ">0.2%", 35 | "not dead", 36 | "not ie <= 11", 37 | "not op_mini all" 38 | ], 39 | "devDependencies": { 40 | "copy": "^0.3.2", 41 | "gh-pages": "^2.1.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 20 | ReactCanvasGrid 21 | 22 | 23 | 24 |
25 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/src/App.css: -------------------------------------------------------------------------------- 1 | .app-container { 2 | display: flex; 3 | flex: 1; 4 | flex-direction: column; 5 | } 6 | 7 | nav.top-nav { 8 | height: 3em; 9 | line-height: 3em; 10 | display: inline-flex; 11 | flex-direction: row; 12 | background-color: var(--primary-colour); 13 | } 14 | 15 | .top-nav .top-nav-item { 16 | margin-left: 1em; 17 | } 18 | 19 | .top-nav .top-nav-item a { 20 | color: white; 21 | } 22 | .top-nav .top-nav-item a.active { 23 | color: var(--secondary-colour); 24 | } 25 | .top-nav .top-nav-item a:hover { 26 | filter: drop-shadow(0 3px 3px rgba(0, 0, 0, 0.3)); 27 | } 28 | 29 | .top-nav .logo-svg { 30 | height: 100%; 31 | padding: 5px; 32 | } -------------------------------------------------------------------------------- /examples/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HashRouter, NavLink, Route } from 'react-router-dom'; 3 | import './App.css'; 4 | import Examples from './Examples'; 5 | import Home from './Home'; 6 | 7 | const App = () => { 8 | return ( 9 | 10 |
11 | 18 |
19 | 20 | 21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /examples/src/Examples.css: -------------------------------------------------------------------------------- 1 | .examples-container { 2 | display: flex; 3 | flex-direction: row; 4 | } 5 | 6 | main.examples-main { 7 | flex: 1; 8 | margin: 0 1.5em; 9 | } 10 | 11 | main.examples-main ul { 12 | padding: 0; 13 | list-style-type: none; 14 | } 15 | 16 | main.examples-main ul li { 17 | margin-bottom: 1.5em; 18 | } 19 | 20 | main.examples-main ul li h3 { 21 | margin: 0 0 0.5em 0; 22 | } 23 | 24 | main.examples-main ul li p { 25 | margin: 0 0 0.5em 0; 26 | } 27 | 28 | nav.examples-menu { 29 | flex: 0 0 12em; 30 | order: -1; 31 | min-height: 100vh; 32 | margin: 0.5em; 33 | } 34 | 35 | nav.examples-menu ul { 36 | border: 1px solid #eeeeee; 37 | border-radius: 3px; 38 | padding: 0; 39 | list-style-type: none; 40 | } 41 | 42 | nav.examples-menu ul li { 43 | font-size: 0.8em; 44 | font-weight: 600; 45 | border-bottom: 1px solid #eeeeee; 46 | } 47 | nav.examples-menu ul li:hover { 48 | background-color: hsl(0, 0%, 97%); 49 | } 50 | 51 | nav.examples-menu ul li:first-of-type a { 52 | border-top-right-radius: 2px; 53 | } 54 | nav.examples-menu ul li:last-of-type a { 55 | border-bottom-right-radius: 2px; 56 | } 57 | nav.examples-menu ul li:last-of-type { 58 | border-bottom: none; 59 | } 60 | 61 | nav.examples-menu ul li a { 62 | display: block; 63 | color: hsl(0, 0%, 40%); 64 | padding: 0.4em; 65 | } 66 | nav.examples-menu ul li:hover a { 67 | color: hsl(0, 0%, 50%); 68 | } 69 | nav.examples-menu ul li a.selected { 70 | border-right: 2px solid #333944; 71 | background-color: #eeeeee; 72 | } 73 | 74 | .examples-main code { 75 | font-size: 0.9em; 76 | background-color: #eee; 77 | border: 1px solid #ccc; 78 | border-radius: 2px; 79 | color: rgb(196, 78, 78); 80 | } -------------------------------------------------------------------------------- /examples/src/Examples.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { NavLink, Route, Switch } from 'react-router-dom'; 3 | import './Examples.css'; 4 | import exampleMeta from './examples/exampleMeta'; 5 | import { ExamplePage } from './examples/ExamplePage'; 6 | import { Index } from './examples/Index'; 7 | 8 | class Examples extends Component<{}, {}> { 9 | public render() { 10 | return ( 11 |
12 |
13 | 14 | 15 | {exampleMeta.map((meta) => ( 16 | 17 | 18 | 19 | ))} 20 | 21 |
22 | 32 |
33 | ); 34 | } 35 | } 36 | 37 | export default Examples; 38 | -------------------------------------------------------------------------------- /examples/src/Home.css: -------------------------------------------------------------------------------- 1 | .hero-wrapper { 2 | background: linear-gradient(to bottom, var(--primary-colour) 0, var(--primary-colour-dark) 100%); 3 | color: white; 4 | box-sizing: border-box; 5 | padding-bottom: 15vh; 6 | } 7 | 8 | .hero-wrapper svg { 9 | fill: white; 10 | stroke: white; 11 | filter: drop-shadow(0 3px 3px rgba(0, 0, 0, 0.3)); 12 | } 13 | 14 | .hero-wrapper .logo-svg { 15 | margin: 15vh 20% 3em 20%; 16 | } 17 | 18 | .hero-wrapper h1 { 19 | font-family: monospace; 20 | font-size: 5em; 21 | text-align: center; 22 | filter: drop-shadow(0 3px 3px rgba(0, 0, 0, 0.3)); 23 | } 24 | .hero-wrapper h1 .warning { 25 | color: rgb(230, 0, 0); 26 | } 27 | 28 | .features { 29 | display: flex; 30 | flex-direction: row; 31 | margin: 0 10%; 32 | padding-top: 5em; 33 | } 34 | .features .feature { 35 | margin: 0 2em; 36 | flex: 1 1 0; 37 | text-align: center; 38 | } 39 | @media (min-width: 789px) { 40 | .features .feature:first-of-type { 41 | margin-left: 0; 42 | } 43 | .features .feature:last-of-type { 44 | margin-right: 0; 45 | } 46 | } 47 | .features .feature .feature-icon-sizer { 48 | width: 60%; 49 | height: 0; 50 | padding-bottom: 60%; 51 | margin: 0 auto; 52 | } 53 | .features .feature h3 { 54 | color: var(--primary-colour-dark); 55 | font-size: 1.5em; 56 | } 57 | @media (max-width: 768px) { 58 | .features { 59 | display: block; 60 | padding-top: 2em; 61 | } 62 | .features .feature { 63 | margin-bottom: 2em; 64 | } 65 | } 66 | 67 | .install-instructions-wrapper { 68 | text-align: center; 69 | margin: 0 2em; 70 | } 71 | .install-instructions { 72 | border: 2px solid white; 73 | border-radius: 5px; 74 | padding: 2em; 75 | display: inline-block; 76 | margin-top: 3em; 77 | } 78 | .install-instructions code { 79 | font-size: 1.2em; 80 | } 81 | .install-instructions .prompt { 82 | -webkit-touch-callout: none; /* iOS Safari */ 83 | -webkit-user-select: none; /* Safari */ 84 | -khtml-user-select: none; /* Konqueror HTML */ 85 | -moz-user-select: none; /* Old versions of Firefox */ 86 | -ms-user-select: none; /* Internet Explorer/Edge */ 87 | user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */ 88 | } 89 | 90 | .capabilities { 91 | margin: 5em 10% 0 10%; 92 | } 93 | 94 | .capability { 95 | margin-bottom: 2em; 96 | } 97 | .capability h3 { 98 | margin: 2em 0 0.75em 0; 99 | color: var(--primary-colour-dark); 100 | } 101 | .capability p { 102 | margin: 0 0 1em 0; 103 | } 104 | .capability .capability-details { 105 | margin: 0 1.5em; 106 | display: flex; 107 | flex-direction: row; 108 | } 109 | @media (max-width: 768px) { 110 | .capability .capability-details { 111 | display: initial; 112 | } 113 | } 114 | .capability:nth-child(even) .capability-details { 115 | flex-direction: row-reverse; 116 | } 117 | .capability img { 118 | width: 204px; 119 | object-fit: contain; 120 | margin: 0 1em 0.5em 0; 121 | } 122 | .capability:nth-child(even) img { 123 | margin: 0 0 0.5em 1em; 124 | } -------------------------------------------------------------------------------- /examples/src/assets/autofill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/examples/src/assets/autofill.png -------------------------------------------------------------------------------- /examples/src/assets/custom-render.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/examples/src/assets/custom-render.png -------------------------------------------------------------------------------- /examples/src/assets/feature-icon-gradient.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/src/assets/frozen-rows-cols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/examples/src/assets/frozen-rows-cols.png -------------------------------------------------------------------------------- /examples/src/assets/inline-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/examples/src/assets/inline-edit.png -------------------------------------------------------------------------------- /examples/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/src/assets/react-js.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/src/assets/rocket-launch-lines.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/src/assets/selection-range.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanhill/react-canvas-grid/c430679f2e42b16b46b11efe8d5804e7fe7468c8/examples/src/assets/selection-range.png -------------------------------------------------------------------------------- /examples/src/assets/structure.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/src/components/ControlsForm.css: -------------------------------------------------------------------------------- 1 | form.controls { 2 | display: inline-block; 3 | margin-bottom: 1em; 4 | padding: 0.2em; 5 | background-color: hsl(220, 75%, 90%); 6 | border: solid 1px hsl(220, 100%, 40%); 7 | border-radius: 2px; 8 | } 9 | 10 | form.controls .inline-controls-group { 11 | margin-right: 1em; 12 | } 13 | form.controls .inline-controls-group:last-of-type { 14 | margin-right: 0; 15 | } 16 | 17 | form.controls input[type="number"] { 18 | width: 4em; 19 | } 20 | 21 | form.controls label { 22 | margin-right: 0.5em; 23 | } -------------------------------------------------------------------------------- /examples/src/components/ControlsForm.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './ControlsForm.css'; 3 | 4 | export const ControlsForm = (props: { children?: React.ReactNode }) => { 5 | return ( 6 |
7 | {props.children} 8 |
9 | ); 10 | }; 11 | 12 | export const InlineGroup = (props: { children?: React.ReactNode }) => { 13 | return ( 14 | 15 | {props.children} 16 | 17 | ); 18 | }; 19 | 20 | export const NumberInput = (props: Partial>) => 21 | ; 22 | 23 | interface SelectInputProps { 24 | values: T[]; 25 | selectedValue: T; 26 | onSelect: (event: React.ChangeEvent) => void; 27 | } 28 | export const RadioInputs = ( 29 | { values, selectedValue, onSelect}: SelectInputProps, 30 | ) => { 31 | return 32 | {values.map((val) => 33 | )} 37 | ; 38 | }; 39 | -------------------------------------------------------------------------------- /examples/src/components/EventLog.css: -------------------------------------------------------------------------------- 1 | .event-log { 2 | width: 100%; 3 | height: 6em; 4 | font-family: 'Courier New', Courier, monospace; 5 | } -------------------------------------------------------------------------------- /examples/src/components/EventLog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './EventLog.css'; 3 | 4 | interface EventLogProps { 5 | log: string; 6 | } 7 | 8 | export class EventLog extends React.PureComponent { 9 | private logRef: React.RefObject; 10 | 11 | constructor(props: EventLogProps) { 12 | super(props); 13 | this.logRef = React.createRef(); 14 | } 15 | 16 | public render() { 17 | return (