From 37a4e069af980ce78eced68331a5b371e19d6644 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 29 Jul 2019 15:32:51 +0800 Subject: [PATCH] Make cell alignment work across columns instead of individual cells Add jsdocs Inherit the alignment property of the first cell in the column when adding new rows Update e2e tests Use consistent letter casing --- .../src/components/alignment-toolbar/index.js | 12 +- packages/block-library/src/table/edit.js | 132 +++++--- packages/block-library/src/table/state.js | 206 +++++++----- .../block-library/src/table/test/state.js | 313 +++++++++++++----- .../blocks/__snapshots__/table.test.js.snap | 4 +- packages/e2e-tests/specs/blocks/table.test.js | 22 +- 6 files changed, 455 insertions(+), 234 deletions(-) diff --git a/packages/block-editor/src/components/alignment-toolbar/index.js b/packages/block-editor/src/components/alignment-toolbar/index.js index 981e8d6f9914d3..aa0233ae0f7518 100644 --- a/packages/block-editor/src/components/alignment-toolbar/index.js +++ b/packages/block-editor/src/components/alignment-toolbar/index.js @@ -27,7 +27,15 @@ const DEFAULT_ALIGNMENT_CONTROLS = [ }, ]; -export function AlignmentToolbar( { value, onChange, alignmentControls = DEFAULT_ALIGNMENT_CONTROLS, isCollapsed = true } ) { +export function AlignmentToolbar( props ) { + const { + value, + onChange, + alignmentControls = DEFAULT_ALIGNMENT_CONTROLS, + label = __( 'Change text alignment' ), + isCollapsed = true, + } = props; + function applyOrUnset( align ) { return () => onChange( value === align ? undefined : align ); } @@ -38,7 +46,7 @@ export function AlignmentToolbar( { value, onChange, alignmentControls = DEFAULT { const { align } = control; const isActive = ( value === align ); diff --git a/packages/block-library/src/table/edit.js b/packages/block-library/src/table/edit.js index cdd325e585cd73..afe9d1e8b2c9d2 100644 --- a/packages/block-library/src/table/edit.js +++ b/packages/block-library/src/table/edit.js @@ -32,8 +32,7 @@ import { */ import { createTable, - updateCellContent, - updateCellAttribute, + updateSelectedCell, getCellAttribute, insertRow, deleteRow, @@ -41,6 +40,7 @@ import { deleteColumn, toggleSection, isEmptyTableSection, + isCellSelected, } from './state'; import icon from './icon'; @@ -108,6 +108,8 @@ export class TableEdit extends Component { this.onDeleteColumn = this.onDeleteColumn.bind( this ); this.onToggleHeaderSection = this.onToggleHeaderSection.bind( this ); this.onToggleFooterSection = this.onToggleFooterSection.bind( this ); + this.onChangeColumnAlignment = this.onChangeColumnAlignment.bind( this ); + this.getCellAlignment = this.getCellAlignment.bind( this ); this.state = { initialRowCount: 2, @@ -177,35 +179,47 @@ export class TableEdit extends Component { } const { attributes, setAttributes } = this.props; - const { section, rowIndex, columnIndex } = selectedCell; - setAttributes( updateCellContent( attributes, { - section, - rowIndex, - columnIndex, - content, - } ) ); + setAttributes( updateSelectedCell( + attributes, + selectedCell, + ( cellAttributes ) => ( { ...cellAttributes, content } ), + ) ); } - onChangeCellAlignment( value ) { + /** + * Align text within the a column. + * + * @param {string} align The new alignment to apply to the column. + */ + onChangeColumnAlignment( align ) { const { selectedCell } = this.state; if ( ! selectedCell ) { return; } + // Convert the cell selection to a column selection so that alignment + // is applied to the entire column. + const columnSelection = { + type: 'column', + columnIndex: selectedCell.columnIndex, + }; + const { attributes, setAttributes } = this.props; - const { section, rowIndex, columnIndex } = selectedCell; - - setAttributes( updateCellAttribute( attributes, { - section, - rowIndex, - columnIndex, - attributeName: 'align', - value, - } ) ); + const newAttributes = updateSelectedCell( + attributes, + columnSelection, + ( cellAttributes ) => ( { ...cellAttributes, align } ), + ); + setAttributes( newAttributes ); } + /** + * Get the alignment of the currently selected cell. + * + * @return {string} The new alignment to apply to the column. + */ getCellAlignment() { const { selectedCell } = this.state; @@ -218,6 +232,22 @@ export class TableEdit extends Component { return getCellAttribute( attributes, { ...selectedCell, attributeName: 'align' } ); } + /** + * Add or remove a `head` table section. + */ + onToggleHeaderSection() { + const { attributes, setAttributes } = this.props; + setAttributes( toggleSection( attributes, 'head' ) ); + } + + /** + * Add or remove a `foot` table section. + */ + onToggleFooterSection() { + const { attributes, setAttributes } = this.props; + setAttributes( toggleSection( attributes, 'foot' ) ); + } + /** * Inserts a row at the currently selected row index, plus `delta`. * @@ -231,11 +261,11 @@ export class TableEdit extends Component { } const { attributes, setAttributes } = this.props; - const { section, rowIndex } = selectedCell; + const { sectionName, rowIndex } = selectedCell; this.setState( { selectedCell: null } ); setAttributes( insertRow( attributes, { - section, + sectionName, rowIndex: rowIndex + delta, } ) ); } @@ -254,16 +284,6 @@ export class TableEdit extends Component { this.onInsertRow( 1 ); } - onToggleHeaderSection() { - const { attributes, setAttributes } = this.props; - setAttributes( toggleSection( attributes, 'head' ) ); - } - - onToggleFooterSection() { - const { attributes, setAttributes } = this.props; - setAttributes( toggleSection( attributes, 'foot' ) ); - } - /** * Deletes the currently selected row. */ @@ -275,10 +295,10 @@ export class TableEdit extends Component { } const { attributes, setAttributes } = this.props; - const { section, rowIndex } = selectedCell; + const { sectionName, rowIndex } = selectedCell; this.setState( { selectedCell: null } ); - setAttributes( deleteRow( attributes, { section, rowIndex } ) ); + setAttributes( deleteRow( attributes, { sectionName, rowIndex } ) ); } /** @@ -327,23 +347,28 @@ export class TableEdit extends Component { } const { attributes, setAttributes } = this.props; - const { section, columnIndex } = selectedCell; + const { sectionName, columnIndex } = selectedCell; this.setState( { selectedCell: null } ); - setAttributes( deleteColumn( attributes, { section, columnIndex } ) ); + setAttributes( deleteColumn( attributes, { sectionName, columnIndex } ) ); } /** * Creates an onFocus handler for a specified cell. * - * @param {Object} selectedCell Object with `section`, `rowIndex`, and + * @param {Object} cellLocation Object with `section`, `rowIndex`, and * `columnIndex` properties. * * @return {Function} Function to call on focus. */ - createOnFocus( selectedCell ) { + createOnFocus( cellLocation ) { return () => { - this.setState( { selectedCell } ); + this.setState( { + selectedCell: { + ...cellLocation, + type: 'cell', + }, + } ); }; } @@ -403,12 +428,12 @@ export class TableEdit extends Component { * * @return {Object} React element for the section. */ - renderSection( { type, rows } ) { + renderSection( { name, rows } ) { if ( isEmptyTableSection( rows ) ) { return null; } - const Tag = `t${ type }`; + const Tag = `t${ name }`; const { selectedCell } = this.state; return ( @@ -416,19 +441,15 @@ export class TableEdit extends Component { { rows.map( ( { cells }, rowIndex ) => ( { cells.map( ( { content, tag: CellTag, scope, align }, columnIndex ) => { - const isSelected = selectedCell && ( - type === selectedCell.section && - rowIndex === selectedCell.rowIndex && - columnIndex === selectedCell.columnIndex - ); - - const cell = { - section: type, + const cellLocation = { + sectionName: name, rowIndex, columnIndex, }; - const cellClasses = classnames( { + const isSelected = isCellSelected( cellLocation, selectedCell ); + + const cellClasses = classnames( { 'is-selected': isSelected, [ `has-text-align-${ align }` ]: align, } ); @@ -443,7 +464,7 @@ export class TableEdit extends Component { className="wp-block-table__cell-content" value={ content } onChange={ this.onChange } - unstableOnFocus={ this.createOnFocus( cell ) } + unstableOnFocus={ this.createOnFocus( cellLocation ) } /> ); @@ -523,10 +544,11 @@ export class TableEdit extends Component { /> this.onChangeCellAlignment( nextAlign ) } - isCollapsed + onChange={ ( nextAlign ) => this.onChangeColumnAlignment( nextAlign ) } + onHover={ this.onHoverAlignment } /> @@ -563,9 +585,9 @@ export class TableEdit extends Component {
-
-
-
+
+
+
diff --git a/packages/block-library/src/table/state.js b/packages/block-library/src/table/state.js index 833104f30b6941..7d7096b6f62925 100644 --- a/packages/block-library/src/table/state.js +++ b/packages/block-library/src/table/state.js @@ -1,7 +1,9 @@ /** * External dependencies */ -import { times, get, mapValues, every } from 'lodash'; +import { times, get, mapValues, every, pick } from 'lodash'; + +const INHERITED_COLUMN_ATTRIBUTES = [ 'align' ]; /** * Creates a table state. @@ -26,50 +28,29 @@ export function createTable( { } /** - * Updates cell content in the table state. + * Returns the first row in the table. * - * @param {Object} state Current table state. - * @param {string} options.section Section of the cell to update. - * @param {number} options.rowIndex Row index of the cell to update. - * @param {number} options.columnIndex Column index of the cell to update. - * @param {number} options.attributeName The name of the attribute to update. - * @param {number} options.value The value to update the attribute with. + * @param {Object} state Current table state. * - * @return {Object} New table state. + * @return {Object} The first table row. */ -export function updateCellAttribute( state, { - section, - rowIndex, - columnIndex, - attributeName, - value, -} ) { - return { - [ section ]: state[ section ].map( ( row, currentRowIndex ) => { - if ( currentRowIndex !== rowIndex ) { - return row; - } - - return { - cells: row.cells.map( ( cell, currentColumnIndex ) => { - if ( currentColumnIndex !== columnIndex ) { - return cell; - } - - return { - ...cell, - [ attributeName ]: value, - }; - } ), - }; - } ), - }; +export function getFirstRow( state ) { + if ( ! isEmptyTableSection( state.head ) ) { + return state.head[ 0 ]; + } + if ( ! isEmptyTableSection( state.body ) ) { + return state.body[ 0 ]; + } + if ( ! isEmptyTableSection( state.foot ) ) { + return state.foot[ 0 ]; + } } + /** * Gets an attribute for a cell. * * @param {Object} state Current table state. - * @param {string} options.section Section of the cell to update. + * @param {string} options.sectionName Section of the cell to update. * @param {number} options.rowIndex Row index of the cell to update. * @param {number} options.columnIndex Column index of the cell to update. * @param {number} options.attributeName The name of the attribute to get the value of. @@ -77,59 +58,120 @@ export function updateCellAttribute( state, { * @return {*} The attribute value. */ export function getCellAttribute( state, { - section, + sectionName, rowIndex, columnIndex, attributeName, } ) { - return get( state, [ section, rowIndex, 'cells', columnIndex, attributeName ] ); + return get( state, [ sectionName, rowIndex, 'cells', columnIndex, attributeName ] ); } /** - * Updates cell content in the table state. + * Returns updated cell attributes after applying the `updateCell` function to the selection. * - * @param {Object} state Current table state. - * @param {string} options.section Section of the cell to update. - * @param {number} options.rowIndex Row index of the cell to update. - * @param {number} options.columnIndex Column index of the cell to update. - * @param {Array} options.content Content to set for the cell. + * @param {Object} state The block attributes. + * @param {Object} selection The selection of cells to update. + * @param {Function} updateCell A function to update the selected cell attributes. * - * @return {Object} New table state. + * @return {Object} New table state including the updated cells. */ -export function updateCellContent( state, { content: value, ...options } ) { - return updateCellAttribute( state, { - ...options, - value, - attributeName: 'content', +export function updateSelectedCell( state, selection, updateCell ) { + if ( ! selection ) { + return state; + } + + const tableSections = pick( state, [ 'head', 'body', 'foot' ] ); + const { + sectionName: selectionSectionName, + rowIndex: selectionRowIndex, + } = selection; + + return mapValues( tableSections, ( section, sectionName ) => { + if ( selectionSectionName && selectionSectionName !== sectionName ) { + return section; + } + + return section.map( ( row, rowIndex ) => { + if ( selectionRowIndex && selectionRowIndex !== rowIndex ) { + return row; + } + + return { + cells: row.cells.map( ( cellAttributes, columnIndex ) => { + const cellLocation = { + sectionName, + columnIndex, + rowIndex, + }; + + if ( ! isCellSelected( cellLocation, selection ) ) { + return cellAttributes; + } + + return updateCell( cellAttributes ); + } ), + }; + } ); } ); } +/** + * Returns whether the cell at `cellLocation` is included in the selection `selection`. + * + * @param {Object} cellLocation An object containing cell location properties. + * @param {Object} selection An object containing selection properties. + * + * @return {boolean} True if the cell is selected, false otherwise. + */ +export function isCellSelected( cellLocation, selection ) { + if ( ! cellLocation || ! selection ) { + return false; + } + + switch ( selection.type ) { + case 'column': + return selection.type === 'column' && cellLocation.columnIndex === selection.columnIndex; + case 'cell': + return selection.type === 'cell' && + cellLocation.sectionName === selection.sectionName && + cellLocation.columnIndex === selection.columnIndex && + cellLocation.rowIndex === selection.rowIndex; + } +} + /** * Inserts a row in the table state. * - * @param {Object} state Current table state. - * @param {string} options.section Section in which to insert the row. - * @param {number} options.rowIndex Row index at which to insert the row. + * @param {Object} state Current table state. + * @param {string} options.sectionName Section in which to insert the row. + * @param {number} options.rowIndex Row index at which to insert the row. * * @return {Object} New table state. */ export function insertRow( state, { - section, + sectionName, rowIndex, columnCount, } ) { - const cellCount = columnCount || state[ section ][ 0 ].cells.length; + const firstRow = getFirstRow( state ); + const cellCount = columnCount || get( firstRow, [ 'cells', 'length' ], 2 ); return { - [ section ]: [ - ...state[ section ].slice( 0, rowIndex ), + [ sectionName ]: [ + ...state[ sectionName ].slice( 0, rowIndex ), { - cells: times( cellCount, () => ( { - content: '', - tag: section === 'head' ? 'th' : 'td', - } ) ), + cells: times( cellCount, ( index ) => { + const firstCellInColumn = get( firstRow, [ 'cells', index ], {} ); + const inheritedAttributes = pick( firstCellInColumn, INHERITED_COLUMN_ATTRIBUTES ); + + return { + ...inheritedAttributes, + content: '', + tag: sectionName === 'head' ? 'th' : 'td', + }; + } ), }, - ...state[ section ].slice( rowIndex ), + ...state[ sectionName ].slice( rowIndex ), ], }; } @@ -137,18 +179,18 @@ export function insertRow( state, { /** * Deletes a row from the table state. * - * @param {Object} state Current table state. - * @param {string} options.section Section in which to delete the row. - * @param {number} options.rowIndex Row index to delete. + * @param {Object} state Current table state. + * @param {string} options.sectionName Section in which to delete the row. + * @param {number} options.rowIndex Row index to delete. * * @return {Object} New table state. */ export function deleteRow( state, { - section, + sectionName, rowIndex, } ) { return { - [ section ]: state[ section ].filter( ( row, index ) => index !== rowIndex ), + [ sectionName ]: state[ sectionName ].filter( ( row, index ) => index !== rowIndex ), }; } @@ -156,7 +198,6 @@ export function deleteRow( state, { * Inserts a column in the table state. * * @param {Object} state Current table state. - * @param {string} options.section Section in which to insert the column. * @param {number} options.columnIndex Column index at which to insert the column. * * @return {Object} New table state. @@ -164,7 +205,9 @@ export function deleteRow( state, { export function insertColumn( state, { columnIndex, } ) { - return mapValues( state, ( section, sectionName ) => { + const tableSections = pick( state, [ 'head', 'body', 'foot' ] ); + + return mapValues( tableSections, ( section, sectionName ) => { // Bail early if the table section is empty. if ( isEmptyTableSection( section ) ) { return section; @@ -195,7 +238,6 @@ export function insertColumn( state, { * Deletes a column from the table state. * * @param {Object} state Current table state. - * @param {string} options.section Section in which to delete the column. * @param {number} options.columnIndex Column index to delete. * * @return {Object} New table state. @@ -203,7 +245,9 @@ export function insertColumn( state, { export function deleteColumn( state, { columnIndex, } ) { - return mapValues( state, ( section ) => { + const tableSections = pick( state, [ 'head', 'body', 'foot' ] ); + + return mapValues( tableSections, ( section ) => { // Bail early if the table section is empty. if ( isEmptyTableSection( section ) ) { return section; @@ -218,33 +262,33 @@ export function deleteColumn( state, { /** * Toggles the existance of a section. * - * @param {Object} state Current table state. - * @param {string} section Name of the section to toggle. + * @param {Object} state Current table state. + * @param {string} sectionName Name of the section to toggle. * * @return {Object} New table state. */ -export function toggleSection( state, section ) { +export function toggleSection( state, sectionName ) { // Section exists, replace it with an empty row to remove it. - if ( ! isEmptyTableSection( state[ section ] ) ) { - return { [ section ]: [] }; + if ( ! isEmptyTableSection( state[ sectionName ] ) ) { + return { [ sectionName ]: [] }; } // Get the length of the first row of the body to use when creating the header. const columnCount = get( state, [ 'body', 0, 'cells', 'length' ], 1 ); // Section doesn't exist, insert an empty row to create the section. - return insertRow( state, { section, rowIndex: 0, columnCount } ); + return insertRow( state, { sectionName, rowIndex: 0, columnCount } ); } /** * Determines whether a table section is empty. * - * @param {Object} sectionRows Table section state. + * @param {Object} section Table section state. * * @return {boolean} True if the table section is empty, false otherwise. */ -export function isEmptyTableSection( sectionRows ) { - return ! sectionRows || ! sectionRows.length || every( sectionRows, isEmptyRow ); +export function isEmptyTableSection( section ) { + return ! section || ! section.length || every( section, isEmptyRow ); } /** diff --git a/packages/block-library/src/table/test/state.js b/packages/block-library/src/table/test/state.js index 41fc2309b67a86..e1a72ccff50755 100644 --- a/packages/block-library/src/table/test/state.js +++ b/packages/block-library/src/table/test/state.js @@ -8,9 +8,8 @@ import deepFreeze from 'deep-freeze'; */ import { createTable, + getFirstRow, getCellAttribute, - updateCellAttribute, - updateCellContent, insertRow, deleteRow, insertColumn, @@ -18,6 +17,8 @@ import { toggleSection, isEmptyTableSection, isEmptyRow, + isCellSelected, + updateSelectedCell, } from '../state'; const table = deepFreeze( { @@ -49,6 +50,32 @@ const table = deepFreeze( { ], } ); +const tableWithHead = deepFreeze( { + head: [ + { + cells: [ + { + content: 'test', + tag: 'th', + }, + ], + }, + ], +} ); + +const tableWithFoot = deepFreeze( { + foot: [ + { + cells: [ + { + content: 'test', + tag: 'td', + }, + ], + }, + ], +} ); + const tableWithContent = deepFreeze( { body: [ { @@ -116,50 +143,41 @@ describe( 'createTable', () => { } ); } ); -describe( 'getCellAttribute', () => { - it( 'should get the cell attribute', () => { - const state = getCellAttribute( tableWithAttribute, { - section: 'body', - rowIndex: 1, - columnIndex: 1, - attributeName: 'testAttr', - } ); +describe( 'getFirstRow', () => { + it( 'returns the first row in the head when the body is the first table section', () => { + expect( getFirstRow( tableWithHead ) ).toBe( tableWithHead.head[ 0 ] ); + } ); - expect( state ).toBe( 'testVal' ); + it( 'returns the first row in the body when the body is the first table section', () => { + expect( getFirstRow( table ) ).toBe( table.body[ 0 ] ); } ); -} ); -describe( 'updateCellAttribute', () => { - it( 'should update cell attribute', () => { - const state = updateCellAttribute( table, { - section: 'body', - rowIndex: 1, - columnIndex: 1, - attributeName: 'testAttr', - value: 'testVal', - } ); + it( 'returns the first row in the foot when the body is the first table section', () => { + expect( getFirstRow( tableWithFoot ) ).toBe( tableWithFoot.foot[ 0 ] ); + } ); - expect( state ).toEqual( tableWithAttribute ); + it( 'returns `undefined` for an empty table', () => { + expect( getFirstRow( {} ) ).toBeUndefined(); } ); } ); -describe( 'updateCellContent', () => { - it( 'should update cell content', () => { - const state = updateCellContent( table, { - section: 'body', +describe( 'getCellAttribute', () => { + it( 'should get the cell attribute', () => { + const state = getCellAttribute( tableWithAttribute, { + sectionName: 'body', rowIndex: 1, columnIndex: 1, - content: 'test', + attributeName: 'testAttr', } ); - expect( state ).toEqual( tableWithContent ); + expect( state ).toBe( 'testVal' ); } ); } ); describe( 'insertRow', () => { it( 'should insert row', () => { const state = insertRow( tableWithContent, { - section: 'body', + sectionName: 'body', rowIndex: 2, } ); @@ -209,7 +227,7 @@ describe( 'insertRow', () => { it( 'allows the number of columns to be specified', () => { const state = insertRow( tableWithContent, { - section: 'body', + sectionName: 'body', rowIndex: 2, columnCount: 4, } ); @@ -266,11 +284,16 @@ describe( 'insertRow', () => { expect( state ).toEqual( expected ); } ); - it( 'adds `th` cells to the head', () => { - const tableWithHead = { - head: [ + it( 'inherits the `align` property from the first cell in the column when adding a new row', () => { + const tableWithAlignment = { + body: [ { cells: [ + { + align: 'right', + content: 'test', + tag: 'th', + }, { content: '', tag: 'th', @@ -280,15 +303,20 @@ describe( 'insertRow', () => { ], }; - const state = insertRow( tableWithHead, { - section: 'head', + const state = insertRow( tableWithAlignment, { + sectionName: 'body', rowIndex: 1, } ); - const expected = { - head: [ + expect( state ).toEqual( { + body: [ { cells: [ + { + align: 'right', + content: 'test', + tag: 'th', + }, { content: '', tag: 'th', @@ -298,21 +326,27 @@ describe( 'insertRow', () => { { cells: [ { + align: 'right', content: '', - tag: 'th', + tag: 'td', + }, + { + content: '', + tag: 'td', }, ], }, ], - }; - - expect( state ).toEqual( expected ); + } ); } ); -} ); -describe( 'insertColumn', () => { - it( 'inserts before existing content by default', () => { - const tableWithHead = { + it( 'adds `th` cells to the head', () => { + const state = insertRow( tableWithHead, { + sectionName: 'head', + rowIndex: 1, + } ); + + const expected = { head: [ { cells: [ @@ -322,9 +356,23 @@ describe( 'insertColumn', () => { }, ], }, + { + cells: [ + { + content: '', + tag: 'th', + }, + ], + }, ], }; + expect( state ).toEqual( expected ); + } ); +} ); + +describe( 'insertColumn', () => { + it( 'inserts before existing content by default', () => { const state = insertColumn( tableWithHead, { columnIndex: 0, } ); @@ -395,19 +443,6 @@ describe( 'insertColumn', () => { } ); it( 'adds `th` cells to the head', () => { - const tableWithHead = { - head: [ - { - cells: [ - { - content: '', - tag: 'th', - }, - ], - }, - ], - }; - const state = insertColumn( tableWithHead, { columnIndex: 1, } ); @@ -417,7 +452,7 @@ describe( 'insertColumn', () => { { cells: [ { - content: '', + content: 'test', tag: 'th', }, { @@ -433,7 +468,7 @@ describe( 'insertColumn', () => { } ); it( 'avoids adding cells to empty rows', () => { - const tableWithHead = { + const tableWithEmptyRow = { head: [ { cells: [ @@ -449,7 +484,7 @@ describe( 'insertColumn', () => { ], }; - const state = insertColumn( tableWithHead, { + const state = insertColumn( tableWithEmptyRow, { columnIndex: 0, } ); @@ -476,8 +511,8 @@ describe( 'insertColumn', () => { expect( state ).toEqual( expected ); } ); - it( 'adds cells across table sections that already have cells', () => { - const tableWithHead = { + it( 'adds cells across table sections that already have rows', () => { + const tableWithAllSections = { head: [ { cells: [ @@ -510,7 +545,7 @@ describe( 'insertColumn', () => { ], }; - const state = insertColumn( tableWithHead, { + const state = insertColumn( tableWithAllSections, { columnIndex: 1, } ); @@ -563,7 +598,7 @@ describe( 'insertColumn', () => { } ); it( 'adds cells only to rows that have enough cells when rows have an unequal number of cells', () => { - const tableWithHead = { + const tableWithUnequalColumns = { head: [ { cells: [ @@ -604,7 +639,7 @@ describe( 'insertColumn', () => { ], }; - const state = insertColumn( tableWithHead, { + const state = insertColumn( tableWithUnequalColumns, { columnIndex: 3, } ); @@ -660,7 +695,7 @@ describe( 'insertColumn', () => { describe( 'deleteRow', () => { it( 'should delete row', () => { const state = deleteRow( tableWithContent, { - section: 'body', + sectionName: 'body', rowIndex: 0, } ); @@ -688,7 +723,6 @@ describe( 'deleteRow', () => { describe( 'deleteColumn', () => { it( 'should delete column', () => { const state = deleteColumn( tableWithContent, { - section: 'body', columnIndex: 0, } ); @@ -738,7 +772,6 @@ describe( 'deleteColumn', () => { ], }; const state = deleteColumn( tableWithOneColumn, { - section: 'body', columnIndex: 0, } ); @@ -791,7 +824,6 @@ describe( 'deleteColumn', () => { ], }; const state = deleteColumn( tableWithOneColumn, { - section: 'body', columnIndex: 0, } ); @@ -847,7 +879,6 @@ describe( 'deleteColumn', () => { }; const state = deleteColumn( tableWithOneColumn, { - section: 'body', columnIndex: 1, } ); @@ -890,19 +921,6 @@ describe( 'deleteColumn', () => { describe( 'toggleSection', () => { it( 'removes rows from the head section if the table already has them', () => { - const tableWithHead = { - head: [ - { - cells: [ - { - content: '', - tag: 'th', - }, - ], - }, - ], - }; - const state = toggleSection( tableWithHead, 'head' ); const expected = { @@ -913,11 +931,11 @@ describe( 'toggleSection', () => { } ); it( 'adds a row to the head section if the table has none', () => { - const tableWithHead = { + const tableWithEmptyHead = { head: [], }; - const state = toggleSection( tableWithHead, 'head' ); + const state = toggleSection( tableWithEmptyHead, 'head' ); const expected = { head: [ @@ -936,7 +954,7 @@ describe( 'toggleSection', () => { } ); it( 'uses the number of cells in the first row of the body for the added table row', () => { - const tableWithHead = { + const tableWithEmptyHead = { head: [], body: [ { @@ -958,7 +976,7 @@ describe( 'toggleSection', () => { ], }; - const state = toggleSection( tableWithHead, 'head' ); + const state = toggleSection( tableWithEmptyHead, 'head' ); const expected = { head: [ @@ -1055,3 +1073,122 @@ describe( 'isEmptyRow', () => { expect( isEmptyRow( row ) ).toBe( false ); } ); } ); + +describe( 'isCellSelected', () => { + it( 'returns false when no cellLocation is provided', () => { + const tableSelection = { type: 'table' }; + + expect( isCellSelected( undefined, tableSelection ) ).toBe( false ); + } ); + + it( 'returns false when no selection is provided', () => { + const cellLocation = { sectionName: 'head', columnIndex: 0, rowIndex: 0 }; + + expect( isCellSelected( cellLocation ) ).toBe( false ); + } ); + + it( `considers only cells with the same columnIndex to be selected when the selection.type is 'column'`, () => { + // Valid locations and selections. + const headCellLocationA = { sectionName: 'head', columnIndex: 0, rowIndex: 0 }; + const headCellLocationB = { sectionName: 'head', columnIndex: 0, rowIndex: 1 }; + const bodyCellLocationA = { sectionName: 'body', columnIndex: 0, rowIndex: 0 }; + const bodyCellLocationB = { sectionName: 'body', columnIndex: 0, rowIndex: 1 }; + const footCellLocationA = { sectionName: 'foot', columnIndex: 0, rowIndex: 0 }; + const footCellLocationB = { sectionName: 'foot', columnIndex: 0, rowIndex: 1 }; + const columnSelection = { type: 'column', columnIndex: 0 }; + + // Invalid locations and selections. + const otherColumnCellLocationA = { sectionName: 'head', columnIndex: 1, rowIndex: 0 }; + const otherColumnCellLocationB = { sectionName: 'body', columnIndex: 2, rowIndex: 0 }; + const otherColumnCellLocationC = { sectionName: 'foot', columnIndex: 3, rowIndex: 0 }; + + expect( isCellSelected( headCellLocationA, columnSelection ) ).toBe( true ); + expect( isCellSelected( headCellLocationB, columnSelection ) ).toBe( true ); + expect( isCellSelected( bodyCellLocationA, columnSelection ) ).toBe( true ); + expect( isCellSelected( bodyCellLocationB, columnSelection ) ).toBe( true ); + expect( isCellSelected( footCellLocationA, columnSelection ) ).toBe( true ); + expect( isCellSelected( footCellLocationB, columnSelection ) ).toBe( true ); + expect( isCellSelected( otherColumnCellLocationA, columnSelection ) ).toBe( false ); + expect( isCellSelected( otherColumnCellLocationB, columnSelection ) ).toBe( false ); + expect( isCellSelected( otherColumnCellLocationC, columnSelection ) ).toBe( false ); + } ); + + it( `considers only cells with the same section, columnIndex and rowIndex to be selected when the selection.type is 'cell'`, () => { + // Valid locations and selections. + const cellLocation = { sectionName: 'head', columnIndex: 0, rowIndex: 0 }; + const cellSelection = { type: 'cell', sectionName: 'head', rowIndex: 0, columnIndex: 0 }; + + // Invalid locations and selections. + const otherColumnCellLocation = { sectionName: 'head', columnIndex: 1, rowIndex: 0 }; + const otherRowCellLocation = { sectionName: 'head', columnIndex: 0, rowIndex: 1 }; + const bodyCellLocation = { sectionName: 'body', columnIndex: 0, rowIndex: 0 }; + const footCellLocation = { sectionName: 'foot', columnIndex: 0, rowIndex: 0 }; + + expect( isCellSelected( cellLocation, cellSelection ) ).toBe( true ); + expect( isCellSelected( otherColumnCellLocation, cellSelection ) ).toBe( false ); + expect( isCellSelected( otherRowCellLocation, cellSelection ) ).toBe( false ); + expect( isCellSelected( bodyCellLocation, cellSelection ) ).toBe( false ); + expect( isCellSelected( footCellLocation, cellSelection ) ).toBe( false ); + } ); +} ); + +describe( 'updateSelectedCell', () => { + it( 'returns an unchanged table state if there is no selection', () => { + const updated = updateSelectedCell( table, undefined, ( cell ) => ( { ...cell, content: 'test' } ) ); + expect( table ).toEqual( updated ); + } ); + + it( 'returns an unchanged table state if the selection is outside the bounds of the table', () => { + const cellSelection = { type: 'cell', sectionName: 'body', rowIndex: 100, columnIndex: 100 }; + const updated = updateSelectedCell( table, cellSelection, ( cell ) => ( { ...cell, content: 'test' } ) ); + expect( table ).toEqual( updated ); + } ); + + it( 'updates only the individual cell when the selection type is `cell`', () => { + const cellSelection = { type: 'cell', sectionName: 'body', rowIndex: 0, columnIndex: 0 }; + const updated = updateSelectedCell( table, cellSelection, ( cell ) => ( { ...cell, content: 'test' } ) ); + + expect( updated ).toEqual( { + body: [ + { + cells: [ + { + ...table.body[ 0 ].cells[ 0 ], + content: 'test', + }, + table.body[ 0 ].cells[ 1 ], + ], + }, + table.body[ 1 ], + ], + } ); + } ); + + it( 'updates every cell in the column when the selection type is `column`', () => { + const cellSelection = { type: 'column', columnIndex: 1 }; + const updated = updateSelectedCell( table, cellSelection, ( cell ) => ( { ...cell, content: 'test' } ) ); + + expect( updated ).toEqual( { + body: [ + { + cells: [ + table.body[ 0 ].cells[ 0 ], + { + ...table.body[ 0 ].cells[ 1 ], + content: 'test', + }, + ], + }, + { + cells: [ + table.body[ 1 ].cells[ 0 ], + { + ...table.body[ 1 ].cells[ 1 ], + content: 'test', + }, + ], + }, + ], + } ); + } ); +} ); diff --git a/packages/e2e-tests/specs/blocks/__snapshots__/table.test.js.snap b/packages/e2e-tests/specs/blocks/__snapshots__/table.test.js.snap index 291c797b40e2ef..37189e26d2f43f 100644 --- a/packages/e2e-tests/specs/blocks/__snapshots__/table.test.js.snap +++ b/packages/e2e-tests/specs/blocks/__snapshots__/table.test.js.snap @@ -12,9 +12,9 @@ exports[`Table allows adding and deleting columns across the table header, body " `; -exports[`Table allows cells to be aligned 1`] = ` +exports[`Table allows columns to be aligned 1`] = ` " -
NoneTo the left
CenteredTo the right
+
NoneTo the leftCenteredRight aligned
" `; diff --git a/packages/e2e-tests/specs/blocks/table.test.js b/packages/e2e-tests/specs/blocks/table.test.js index ba9d29a5105e40..19e9a0ca51bef3 100644 --- a/packages/e2e-tests/specs/blocks/table.test.js +++ b/packages/e2e-tests/specs/blocks/table.test.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { capitalize } from 'lodash'; + /** * WordPress dependencies */ @@ -16,8 +21,8 @@ const createButtonSelector = "//div[@data-type='core/table']//button[text()='Cre * @param {string} align The alignment (one of 'left', 'center', or 'right'). */ async function changeCellAlignment( align ) { - await clickBlockToolbarButton( 'Change Text Alignment' ); - const alignButton = await page.$x( `//button[text()='Align text ${ align }']` ); + await clickBlockToolbarButton( 'Change column alignment' ); + const alignButton = await page.$x( `//button[text()='Align Column ${ capitalize( align ) }']` ); await alignButton[ 0 ].click(); } @@ -166,12 +171,17 @@ describe( 'Table', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); - it( 'allows cells to be aligned', async () => { + it( 'allows columns to be aligned', async () => { await insertBlock( 'Table' ); + const [ columnCountLabel ] = await page.$x( "//div[@data-type='core/table']//label[text()='Column Count']" ); + await columnCountLabel.click(); + await page.keyboard.press( 'Backspace' ); + await page.keyboard.type( '4' ); + // Create the table. - const createButton = await page.$x( createButtonSelector ); - await createButton[ 0 ].click(); + const [ createButton ] = await page.$x( createButtonSelector ); + await createButton.click(); // Click the first cell and add some text. Don't align. const cells = await page.$$( '.wp-block-table__cell-content' ); @@ -190,7 +200,7 @@ describe( 'Table', () => { // Tab to the next cell and add some text. Align right. await cells[ 3 ].click(); - await page.keyboard.type( 'To the right' ); + await page.keyboard.type( 'Right aligned' ); await changeCellAlignment( 'right' ); // Expect the post to have the correct written content inside the table.