diff --git a/flexible-table-block.php b/flexible-table-block.php index 2be023a6..6a24b261 100644 --- a/flexible-table-block.php +++ b/flexible-table-block.php @@ -4,7 +4,7 @@ * Description: Easily create flexible configuration tables. * Requires at least: 6.1 * Requires PHP: 7.4 - * Version: 3.0.0 + * Version: 3.0.1 * Author: Aki Hamano * Author URI: https://github.com/t-hamano * License: GPL2 or later diff --git a/readme.txt b/readme.txt index c4e5fe2e..7f705d11 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: gutenberg, block, table Donate link: https://www.paypal.me/thamanoJP Requires at least: 6.1 Tested up to: 6.3 -Stable tag: 3.0.0 +Stable tag: 3.0.1 Requires PHP: 7.4 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -42,6 +42,11 @@ The breakpoints for switching between Desktop and mobile can be changed freely. == Changelog == += 3.0.1 = +* Fix: Keyboard controls don't work within the link control popover +* Fix: Tab key focus doesn't work when cell text contains footnote links +* Enhancement: Release cell selection when the block is unselected + = 3.0.0 = * Tested to WordPress 6.3 * Fix: Missing top border in the block sidebar diff --git a/src/edit.tsx b/src/edit.tsx index 2e9234e7..995d2b6a 100644 --- a/src/edit.tsx +++ b/src/edit.tsx @@ -8,7 +8,7 @@ import type { Properties } from 'csstype'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useState } from '@wordpress/element'; +import { useEffect, useState } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { InspectorControls, BlockControls, useBlockProps } from '@wordpress/block-editor'; import { @@ -77,6 +77,14 @@ function TableEdit( props: BlockEditProps< BlockAttributes > ) { [] ); + // Release cell selection. + useEffect( () => { + if ( ! isSelected ) { + setSelectedCells( undefined ); + setSelectedLine( undefined ); + } + }, [ isSelected ] ); + // Create virtual table object with the cells placed in positions based on how they actually look. const vTable: VTable = toVirtualTable( attributes ); @@ -86,7 +94,9 @@ function TableEdit( props: BlockEditProps< BlockAttributes > ) { }; const onInsertRow = ( offset: number ) => { - if ( ! selectedCells || selectedCells.length !== 1 ) return; + if ( ! selectedCells || selectedCells.length !== 1 ) { + return; + } const { sectionName, rowIndex, rowSpan } = selectedCells[ 0 ]; @@ -101,7 +111,9 @@ function TableEdit( props: BlockEditProps< BlockAttributes > ) { }; const onDeleteRow = () => { - if ( ! selectedCells || selectedCells.length !== 1 ) return; + if ( ! selectedCells || selectedCells.length !== 1 ) { + return; + } const { sectionName, rowIndex } = selectedCells[ 0 ]; @@ -123,7 +135,9 @@ function TableEdit( props: BlockEditProps< BlockAttributes > ) { }; const onInsertColumn = ( offset: number ) => { - if ( ! selectedCells || selectedCells.length !== 1 ) return; + if ( ! selectedCells || selectedCells.length !== 1 ) { + return; + } const { vColIndex, colSpan } = selectedCells[ 0 ]; @@ -138,7 +152,9 @@ function TableEdit( props: BlockEditProps< BlockAttributes > ) { }; const onDeleteColumn = () => { - if ( ! selectedCells || selectedCells.length !== 1 ) return; + if ( ! selectedCells || selectedCells.length !== 1 ) { + return; + } const { vColIndex } = selectedCells[ 0 ]; diff --git a/src/elements/table-placeholder.tsx b/src/elements/table-placeholder.tsx index 68acbd61..1cf3284b 100644 --- a/src/elements/table-placeholder.tsx +++ b/src/elements/table-placeholder.tsx @@ -49,7 +49,9 @@ export default function TablePlaceholder( { setAttributes }: Props ) { const onCreateTable = ( event: FormEvent ) => { event.preventDefault(); - if ( ! rowCount || ! colCount ) return; + if ( ! rowCount || ! colCount ) { + return; + } const vTable: VTable = createTable( { rowCount: Math.min( rowCount, MAX_PREVIEW_TABLE_ROW ), diff --git a/src/elements/table.tsx b/src/elements/table.tsx index ab039ae1..6af3097e 100644 --- a/src/elements/table.tsx +++ b/src/elements/table.tsx @@ -35,14 +35,7 @@ import { import { convertToObject } from '../utils/style-converter'; import type { SectionName, CellTagValue, BlockAttributes } from '../BlockAttributes'; -import type { - VTable, - VCell, - VRow, - VSelectMode, - VSelectedLine, - VSelectedCells, -} from '../utils/table-state'; +import type { VTable, VCell, VRow, VSelectedLine, VSelectedCells } from '../utils/table-state'; import type { StoreOptions } from '../store'; function TSection( props: any ) { @@ -86,7 +79,7 @@ export default function Table( { const colorProps = useColorProps( attributes ); - const [ selectMode, setSelectMode ] = useState< VSelectMode >( undefined ); + const [ isSelectMode, setIsSelectMode ] = useState< boolean >( false ); // Manage rendering status as state since some processing may be performed before rendering components. const [ isReady, setIdReady ] = useState< boolean >( false ); @@ -197,7 +190,9 @@ export default function Table( { const onChangeCellContent = ( content: string, targetCell: VCell ) => { // If inline highlight is applied to the RichText, this process is performed before rendering the component, causing a warning error. // Therefore, nothing is performed if the component has not yet been rendered. - if ( ! isReady ) return; + if ( ! isReady ) { + return; + } const { sectionName, rowIndex: selectedRowIndex, vColIndex: selectedVColIndex } = targetCell; setSelectedCells( [ { ...targetCell, isFirstSelected: true } ] ); @@ -229,78 +224,109 @@ export default function Table( { const onKeyDown = ( event: KeyboardEvent ) => { const { key } = event; - if ( key === 'Shift' ) { - // range-select mode. - setSelectMode( 'range' ); - } else if ( key === 'Control' || key === 'Meta' ) { - // multi-select mode. - setSelectMode( 'multi' ); + if ( key === 'Shift' || key === 'Control' || key === 'Meta' ) { + // range-select mode or multi-select mode. + setIsSelectMode( true ); } else if ( key === 'Tab' && options.tab_move && tableRef.current ) { // Focus on the next cell. isTabMove = true; const tableElement: HTMLElement = tableRef.current; - const activeElement = tableElement.querySelector( - 'th.is-selected [contenteditable], td.is-selected [contenteditable]' + const { ownerDocument } = tableElement; + const { activeElement } = ownerDocument; + + const activeCell = tableElement.querySelector( + 'th.is-selected > [contenteditable="true"], td.is-selected > [contenteditable="true"]' ); + const isInsidePopover = + activeElement && activeElement.closest( '.components-popover' ) !== null; + const hasLinkControl = !! ownerDocument.querySelector( '.block-editor-link-control' ); + + if ( ! activeCell || isInsidePopover || hasLinkControl ) { + return; + } - if ( ! activeElement ) return; + const tabbableNodes = tableElement.querySelectorAll( + 'th > [contenteditable="true"], td > [contenteditable="true"]' + ); - const tabbableNodes = tableElement.querySelectorAll( '[contenteditable]' ); const tabbableElements = [].slice.call( tabbableNodes ); - const activeIndex = tabbableElements.findIndex( - ( element: Node ) => element === activeElement + const activeCellIndex = tabbableElements.findIndex( + ( element: Node ) => element === activeCell ); - if ( activeIndex === -1 ) return; + if ( activeCellIndex === -1 ) { + return; + } - let nextIndex = event.shiftKey ? activeIndex - 1 : activeIndex + 1; + let nextCellIndex = event.shiftKey ? activeCellIndex - 1 : activeCellIndex + 1; - if ( nextIndex < 0 ) { - nextIndex = tabbableElements.length - 1; - } else if ( nextIndex >= tabbableElements.length ) { - nextIndex = 0; + if ( nextCellIndex < 0 ) { + nextCellIndex = tabbableElements.length - 1; + } else if ( nextCellIndex >= tabbableElements.length ) { + nextCellIndex = 0; } - const focusbleElement: HTMLElement = tabbableElements[ nextIndex ]; - const { ownerDocument } = tableElement; + const focusbleElement: HTMLElement = tabbableElements[ nextCellIndex ]; + + if ( ! focusbleElement ) { + return; + } - if ( focusbleElement ) { - event.preventDefault(); - setSelectMode( undefined ); - focusbleElement.focus(); + event.preventDefault(); + setIsSelectMode( false ); + focusbleElement.focus(); - // Select all text if the next cell is not empty. - const selection = ownerDocument.getSelection(); - const range = ownerDocument.createRange(); + // Select all text if the next cell is not empty. + const selection = ownerDocument.getSelection(); + const range = ownerDocument.createRange(); - if ( selection && focusbleElement.innerText.trim().length ) { - range.selectNodeContents( focusbleElement ); - selection.removeAllRanges(); - selection.addRange( range ); - } + if ( selection && focusbleElement.innerText.trim().length ) { + range.selectNodeContents( focusbleElement ); + selection.removeAllRanges(); + selection.addRange( range ); } } }; const onKeyUp = ( event: KeyboardEvent ) => { const { key } = event; + if ( key === 'Shift' || key === 'Control' || key === 'Meta' ) { - setSelectMode( undefined ); + const targetElement = event.target as HTMLElement; + const isInsideTableBlock = + targetElement.closest( '.wp-block-flexible-table-block-table' ) !== null; + + if ( ! isInsideTableBlock ) { + return; + } + + setIsSelectMode( false ); } }; const onClickCell = ( event: MouseEvent, clickedCell: VCell ) => { + const { shiftKey, ctrlKey, metaKey } = event; + const clickedElement = event.target as HTMLElement; + const isInsideTableBlock = + clickedElement.closest( '.wp-block-flexible-table-block-table' ) !== null; + + if ( ! isInsideTableBlock ) { + return; + } + const { sectionName, rowIndex, vColIndex } = clickedCell; - if ( event.shiftKey ) { + if ( shiftKey ) { // Range select. if ( ! selectedCells ) { setSelectedCells( [ { ...clickedCell, isFirstSelected: true } ] ); } else { const fromCell = selectedCells.find( ( { isFirstSelected } ) => isFirstSelected ); - if ( ! fromCell ) return; + if ( ! fromCell ) { + return; + } if ( fromCell.sectionName !== sectionName ) { // eslint-disable-next-line no-alert, no-undef @@ -311,7 +337,7 @@ export default function Table( { } setSelectedCells( toRectangledSelectedCells( vTable, { fromCell, toCell: clickedCell } ) ); } - } else if ( event.ctrlKey || event.metaKey ) { + } else if ( ctrlKey || metaKey ) { // Multple select. const newSelectedCells = selectedCells ? [ ...selectedCells ] : []; const existCellIndex = newSelectedCells.findIndex( ( cell ) => { @@ -403,25 +429,19 @@ export default function Table( { // Use only onFocus. const useOnFocus = !! window?.ftbObj?.useOnFocus; - const focusProp = useOnFocus - ? { - onFocus: () => { - if ( ! selectMode || isTabMove ) { - isTabMove = false; - setSelectedLine( undefined ); - setSelectedCells( [ { ...cell, isFirstSelected: true } ] ); - } - }, - } - : { - unstableOnFocus: () => { - if ( ! selectMode || isTabMove ) { - isTabMove = false; - setSelectedLine( undefined ); - setSelectedCells( [ { ...cell, isFirstSelected: true } ] ); - } - }, - }; + const focusEvent = () => { + isTabMove = false; + setSelectedLine( undefined ); + setSelectedCells( [ { ...cell, isFirstSelected: true } ] ); + }; + + const focusProp = + ! isSelectMode || isTabMove + ? { + ...( useOnFocus && { onFocus: focusEvent } ), + ...( ! useOnFocus && { unstableOnFocus: focusEvent } ), + } + : {}; return ( +
Cell 1Cell 2
+" +`; + +exports[`Flexible table cell allows keyboard operation within the link popover 1`] = ` +" +
Link
+" +`; + +exports[`Flexible table cell allows keyboard operation within the link popover 2`] = ` +" +
Link-updated
+" +`; diff --git a/test/e2e/specs/__snapshots__/table.test.ts.snap b/test/e2e/specs/__snapshots__/table.test.ts.snap index 21f1bc1b..c3142c67 100644 --- a/test/e2e/specs/__snapshots__/table.test.ts.snap +++ b/test/e2e/specs/__snapshots__/table.test.ts.snap @@ -65,9 +65,3 @@ exports[`Flexible table should create block with option 1`] = `
" `; - -exports[`Flexible table should move cells with the TAB key. 1`] = ` -" -
Cell 1Cell 2
-" -`; diff --git a/test/e2e/specs/global-setting.test.ts b/test/e2e/specs/global-setting.test.ts index dbd02767..23eda56a 100644 --- a/test/e2e/specs/global-setting.test.ts +++ b/test/e2e/specs/global-setting.test.ts @@ -30,7 +30,7 @@ describe( 'Global Setting', () => { await openSidebar(); await clickButton( 'Global setting' ); - // Restor settings. + // Restore settings. await clickButton( 'Restore default settings' ); await clickButtonWithText( '//div[contains(@class,"ftb-global-setting-modal__confirm-popover")]', diff --git a/test/e2e/specs/table-cell.test.ts b/test/e2e/specs/table-cell.test.ts new file mode 100644 index 00000000..d078f90a --- /dev/null +++ b/test/e2e/specs/table-cell.test.ts @@ -0,0 +1,134 @@ +/** + * WordPress dependencies + */ +import { + getEditedPostContent, + createNewPost, + clickButton, + clickBlockToolbarButton, + pressKeyWithModifier, +} from '@wordpress/e2e-test-utils'; + +/** + * Internal dependencies + */ +import { + flexibleTableCellSelector, + createNewFlexibleTableBlock, + clickButtonWithAriaLabel, + clickButtonWithText, + clickToggleControlWithText, + openSidebar, + getWpVersion, +} from '../helper'; +import { link } from '@wordpress/icons'; + +/** @type {import('puppeteer').Page} */ +const page = global.page; + +describe( 'Flexible table cell', () => { + beforeEach( async () => { + await createNewPost(); + } ); + + it( 'allows cell movement with tab key.', async () => { + const wpVersion = await getWpVersion(); + await createNewFlexibleTableBlock(); + await openSidebar(); + await clickButton( 'Global setting' ); + + // Restore settings. + await clickButton( 'Restore default settings' ); + await clickButtonWithText( + '//div[contains(@class,"ftb-global-setting-modal__confirm-popover")]', + 'Restore' + ); + await page.waitForSelector( '.ftb-global-setting-modal__notice' ); + await clickButtonWithAriaLabel( '.ftb-global-setting-modal__notice', 'Dismiss this notice' ); + + await clickButton( 'Editor options' ); + await clickToggleControlWithText( 'Use the tab key to move cells' ); + await clickButtonWithText( + '//div[@class="ftb-global-setting-modal__buttons"]', + 'Save setting' + ); + await page.waitForSelector( '.ftb-global-setting-modal__notice' ); + const modalCloseLabel = [ '6-2', '6-3', '6-4' ].includes( wpVersion ) + ? 'Close' + : 'Close dialog'; + await clickButtonWithAriaLabel( '.ftb-global-setting-modal', modalCloseLabel ); + const cells = await page.$$( flexibleTableCellSelector ); + await cells[ 0 ].click(); + await page.keyboard.type( 'Cell 1' ); + await page.keyboard.down( 'Tab' ); + await page.keyboard.up( 'Tab' ); + await page.keyboard.down( 'Tab' ); + await page.keyboard.up( 'Tab' ); + await page.keyboard.down( 'Shift' ); + await page.keyboard.down( 'Tab' ); + await page.keyboard.up( 'Tab' ); + await page.keyboard.up( 'Shift' ); + await page.keyboard.type( 'Cell 2' ); + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'allows keyboard operation within the link popover', async () => { + const wpVersion = await getWpVersion(); + await createNewFlexibleTableBlock(); + const cells = await page.$$( flexibleTableCellSelector ); + await cells[ 0 ].click(); + await page.keyboard.type( 'Link' ); + await pressKeyWithModifier( 'primary', 'a' ); + await clickBlockToolbarButton( 'Link' ); + + // Create a link. + await page.keyboard.down( 'Shift' ); + await page.keyboard.type( '#anchor' ); + await page.keyboard.up( 'Shift' ); + + // To avoid React warning error in WordPres 6.1 + if ( wpVersion === '6-1' ) { + await page.waitForTimeout( 1000 ); + } + + await page.keyboard.press( 'Enter' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + // Edit the link. + await pressKeyWithModifier( 'primary', 'a' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Enter' ); + await page.focus( '.components-popover .components-text-control__input' ); + await page.keyboard.type( '-updated' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.type( '#anchor-updated' ); + + // To avoid React warning error in WordPres 6.1 + if ( wpVersion === '6-1' ) { + await page.waitForTimeout( 1000 ); + } + + await page.keyboard.press( 'Enter' ); + + // Toggle "Open in new tab". + await pressKeyWithModifier( 'primary', 'a' ); + + if ( [ '6-3', '6-4' ].includes( wpVersion ) ) { + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Space' ); + await clickButtonWithText( '//div[contains(@class,"components-popover")]', 'Save' ); + } else { + await clickToggleControlWithText( 'Open in new tab' ); + } + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); +} ); diff --git a/test/e2e/specs/table.test.ts b/test/e2e/specs/table.test.ts index c271207c..bbad20a9 100644 --- a/test/e2e/specs/table.test.ts +++ b/test/e2e/specs/table.test.ts @@ -152,35 +152,4 @@ describe( 'Flexible table', () => { await clickButtonWithAriaLabel( flexibleTableSelector, 'Delete column' ); expect( await getEditedPostContent() ).toMatchSnapshot(); } ); - - it( 'should move cells with the TAB key.', async () => { - const wpVersion = await getWpVersion(); - await createNewFlexibleTableBlock(); - await openSidebar(); - await clickButton( 'Global setting' ); - await clickButton( 'Editor options' ); - await clickToggleControlWithText( 'Use the tab key to move cells' ); - await clickButtonWithText( - '//div[@class="ftb-global-setting-modal__buttons"]', - 'Save setting' - ); - await page.waitForSelector( '.ftb-global-setting-modal__notice' ); - const modalCloseLabel = [ '6-2', '6-3', '6-4' ].includes( wpVersion ) - ? 'Close' - : 'Close dialog'; - await clickButtonWithAriaLabel( '.ftb-global-setting-modal', modalCloseLabel ); - const cells = await page.$$( flexibleTableCellSelector ); - await cells[ 0 ].click(); - await page.keyboard.type( 'Cell 1' ); - await page.keyboard.down( 'Tab' ); - await page.keyboard.up( 'Tab' ); - await page.keyboard.down( 'Tab' ); - await page.keyboard.up( 'Tab' ); - await page.keyboard.down( 'Shift' ); - await page.keyboard.down( 'Tab' ); - await page.keyboard.up( 'Tab' ); - await page.keyboard.up( 'Shift' ); - await page.keyboard.type( 'Cell 2' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); } );