Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions playwright/e2e/table-tab-navigation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { expect, mergeTests } from '@playwright/test'
import { test as editorTest } from '../support/fixtures/editor'
import { test as uploadFileTest } from '../support/fixtures/upload-file'

const test = mergeTests(editorTest, uploadFileTest)

test.describe('Tab navigation inside table cell', () => {
test('indents list item inside table cell', async ({ editor, open }) => {
await open()
await editor.getMenu('Table').click()

const firstCell = editor.content.locator('table td').first()
await firstCell.click()
await editor.type('- one')
await editor.content.press('Enter')
await editor.type('two')

await editor.content.press('Tab')

const nested = firstCell.locator('ul > li').first().locator('ul > li')
await expect(nested).toHaveText('two')

await editor.content.press('Shift+Tab')
await expect(firstCell.locator('> ul > li')).toHaveCount(2)
await expect(firstCell.locator('> ul > li > ul')).toHaveCount(0)
})

test('navigates cells when caret is not inside a list', async ({
editor,
open,
}) => {
await open()
await editor.getMenu('Table').click()

const header = editor.content.locator('table th').first()
await header.click()
await editor.type('h1')
await editor.content.press('Tab')
await editor.type('h2')

const headers = editor.content.locator('table th')
await expect(headers.nth(0)).toContainText('h1')
await expect(headers.nth(0)).toContainText('h1')
})
})
2 changes: 1 addition & 1 deletion playwright/support/sections/EditorSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class EditorSection {
}

public getMenu(name: string): Locator {
return this.el.getByRole('button', { name })
return this.menubar.getByRole('button', { name })
}

public getMenuItem(name: string): Locator {
Expand Down
49 changes: 46 additions & 3 deletions src/nodes/Table/Table.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ function findSameCellInNextRow($cell) {
}
}

/**
* Return the node type name if the selection sits inside a listItem/taskItem.
*
* @param {object} selection - the editor selection
*/
function findListItemAtSelection(selection) {
const { $from } = selection
for (let depth = $from.depth; depth > 0; depth--) {
const name = $from.node(depth).type.name
if (name === 'listItem' || name === 'taskItem') {
return name
}
}
}

export default Table.extend({
content: 'tableCaption? tableHeadRow tableRow*',

Expand Down Expand Up @@ -319,11 +334,39 @@ export default Table.extend({
...this.parent(),
/**
* <Tab> inside a table cell
* Jump to next cell or outside table if already in last cell
* When inside a list, indent the list item. Otherwise jump to
* the next cell (or out of the table if in the last cell).
*/
Tab: () =>
Tab: () => {
const listItemName = findListItemAtSelection(
this.editor.state.selection,
)
if (
listItemName
&& this.editor.commands.sinkListItem(listItemName)
) {
return true
}
this.editor.commands.goToNextCell()
|| this.editor.commands.leaveTable(),
|| this.editor.commands.leaveTable()
},
/**
* <Tab> inside a table cell
* When inside a list, indent the list item. Otherwise jump to
* the next cell (or out of the table if in the last cell).
*/
'Shift-Tab': () => {
const listItemName = findListItemAtSelection(
this.editor.state.selection,
)
if (
listItemName
&& this.editor.commands.liftListItem(listItemName)
) {
return true
}
this.editor.commands.goToPreviousCell()
},
}
},
})
Loading