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.