Skip to content

Commit

Permalink
Merge pull request #1965 from deundrewilliams/issue/726-table-nav
Browse files Browse the repository at this point in the history
Add arrow key and tab/enter table editor navigation
  • Loading branch information
FrenjaminBanklin authored Jan 23, 2023
2 parents 63e28c3 + a8b4b7b commit bea6ef6
Show file tree
Hide file tree
Showing 10 changed files with 868 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1272,6 +1272,122 @@ describe('VisualEditor', () => {
expect(onKeyDown).toHaveBeenCalled()
})

describe('moving to node above', () => {
const editorDefaults = {
children: [
{ type: 'mock node 1', children: [{ text: 'one' }] },
{ type: 'mock node 2', children: [{ text: 'one' }, { text: 'two' }] },
{ type: 'mock node 3', children: [{ text: 'one' }] }
],
apply: jest.fn()
}

const visualEditorProps = {
page: {
attributes: { children: [] },
get: jest.fn(),
toJSON: () => ({ children: [{}] }),
set: jest.fn(),
children: []
},
model: {
title: 'Mock Title',
flatJSON: () => ({ content: {} }),
children: []
},
draft: { accessLevel: FULL }
}

test('onKeyDown handles ArrowUp if node above has one child', () => {
jest.spyOn(Array, 'from').mockReturnValue([])
const spySetSelection = jest.spyOn(Transforms, 'setSelection')

const editor = {
...editorDefaults,
selection: {
anchor: { path: [1], offset: 0 },
focus: { path: [1], offset: 0 }
}
}

const component = mount(<VisualEditor {...visualEditorProps} />)
const instance = component.instance()
instance.editor = editor

instance.onKeyDown({
preventDefault: jest.fn(),
key: 'ArrowUp',
defaultPrevented: false
})

expect(spySetSelection).toHaveBeenCalled()
expect(spySetSelection.mock.calls[0].length).toEqual(2)

const focusSelection = spySetSelection.mock.calls[0][1].focus.path
const anchorSelection = spySetSelection.mock.calls[0][1].anchor.path

expect(focusSelection).toEqual([0, 0])
expect(anchorSelection).toEqual([0, 0])
})

test('onKeyDown handles ArrowUp if node above has multiple children', () => {
jest.spyOn(Array, 'from').mockReturnValue([])
const spySetSelection = jest.spyOn(Transforms, 'setSelection')

const editor = {
...editorDefaults,
selection: {
anchor: { path: [2], offset: 0 },
focus: { path: [2], offset: 0 }
}
}

const component = mount(<VisualEditor {...visualEditorProps} />)
const instance = component.instance()
instance.editor = editor

instance.onKeyDown({
preventDefault: jest.fn(),
key: 'ArrowUp',
defaultPrevented: false
})

expect(spySetSelection).toHaveBeenCalled()
expect(spySetSelection.mock.calls[0].length).toEqual(2)

const focusSelection = spySetSelection.mock.calls[0][1].focus.path
const anchorSelection = spySetSelection.mock.calls[0][1].anchor.path

expect(focusSelection).toEqual([1, 1])
expect(anchorSelection).toEqual([1, 1])
})

test('onKeyDown handles ArrowUp if no node above', () => {
jest.spyOn(Array, 'from').mockReturnValue([])
const spySetSelection = jest.spyOn(Transforms, 'setSelection')

const editor = {
...editorDefaults,
selection: {
anchor: { path: [0], offset: 0 },
focus: { path: [0], offset: 0 }
}
}

const component = mount(<VisualEditor {...visualEditorProps} />)
const instance = component.instance()
instance.editor = editor

instance.onKeyDown({
preventDefault: jest.fn(),
key: 'ArrowUp',
defaultPrevented: false
})

expect(spySetSelection).not.toHaveBeenCalled()
})
})

test('reload disables event listener and calls location.reload', () => {
jest.spyOn(window, 'removeEventListener').mockReturnValueOnce()
Object.defineProperty(window, 'location', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,36 @@ class VisualEditor extends React.Component {
item.plugins.onKeyDown(entry, this.editor, event)
}
}

// Handle ArrowUp from a node - this is default behavior
// in Chrome and Safari but not Firefox
if (event.key === 'ArrowUp' && !event.defaultPrevented) {
event.preventDefault()

const currentNode = this.editor.selection.anchor.path[0]

// Break out if already at top node
if (currentNode === 0) return

const aboveNode = currentNode - 1
const numChildrenAbove = this.editor.children[aboveNode].children.length

let abovePath = [aboveNode]

// If entering a node with multiple children (a table),
// go to the last child (bottom row)
if (numChildrenAbove > 1) {
abovePath = [aboveNode, numChildrenAbove - 1]
}

const focus = Editor.start(this.editor, abovePath)
const anchor = Editor.start(this.editor, abovePath)

Transforms.setSelection(this.editor, {
focus,
anchor
})
}
}

// Generates any necessary decorations, such as place holders
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ exports[`Cell Editor Node Cell component as selected header 1`] = `
<div
className="dropdown-cell"
contentEditable={false}
onKeyDown={[Function]}
>
<button
className=" is-not-open"
Expand All @@ -21,6 +22,8 @@ exports[`Cell Editor Node Cell component as selected header 1`] = `
</button>
<div
className="drop-content-cell is-not-open"
onBlur={[Function]}
onFocus={[Function]}
>
<button
onClick={[Function]}
Expand Down Expand Up @@ -66,6 +69,7 @@ exports[`Cell Editor Node Cell component selected 1`] = `
<div
className="dropdown-cell"
contentEditable={false}
onKeyDown={[Function]}
>
<button
className=" is-not-open"
Expand All @@ -78,6 +82,8 @@ exports[`Cell Editor Node Cell component selected 1`] = `
</button>
<div
className="drop-content-cell is-not-open"
onBlur={[Function]}
onFocus={[Function]}
>
<button
onClick={[Function]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ class Cell extends React.Component {
super(props)
this.state = {
isOpen: false,
isShowingDropDownMenu: false
isShowingDropDownMenu: false,
focusedDropdownSelection: null
}

this.toggleOpen = this.toggleOpen.bind(this)
Expand All @@ -27,6 +28,7 @@ class Cell extends React.Component {
this.deleteRow = this.deleteRow.bind(this)
this.deleteCol = this.deleteCol.bind(this)
this.returnFocusOnShiftTab = this.returnFocusOnShiftTab.bind(this)
this.onFocus = this.onFocus.bind(this)
}

toggleOpen() {
Expand Down Expand Up @@ -288,17 +290,66 @@ class Cell extends React.Component {
}
}

onFocus(event) {
event.target.classList.add('focused')
}

onEndFocus(event) {
event.target.classList.remove('focused')
}

onKeyDown(event) {
const cellControls = Array.from(
document.getElementsByClassName('dropdown-cell')[0].getElementsByTagName('button')
)
const currentIndex = cellControls.findIndex(e => event.target.innerHTML === e.innerHTML)

switch (event.key) {
case 'ArrowDown':
// If not at bottommost option, move to option below
event.preventDefault()

if (currentIndex === cellControls.length - 1) break

cellControls[currentIndex + 1].focus()
break

case 'ArrowUp':
// If not at topmost option, move to option above
event.preventDefault()
if (currentIndex === 0) break

cellControls[currentIndex - 1].focus()
break
case 'Tab':
// If at bottommost option and shift key not pressed, close dropdown menu
if (currentIndex === cellControls.length - 1 && !event.shiftKey) {
cellControls[0].click()
}

break
case 'Enter':
break
default:
event.preventDefault()
}
}

renderDropdown() {
return (
<div className="dropdown-cell" contentEditable={false}>
<div className="dropdown-cell" contentEditable={false} onKeyDown={this.onKeyDown}>
<button
className={isOrNot(this.state.isOpen, 'open')}
onClick={this.toggleOpen}
onKeyDown={this.returnFocusOnShiftTab}
>
<div className="table-options-icon"></div>
</button>
<div className={'drop-content-cell ' + isOrNot(this.state.isOpen, 'open')}>
<div
className={'drop-content-cell ' + isOrNot(this.state.isOpen, 'open')}
onFocus={this.onFocus}
onBlur={this.onEndFocus}
>
<button onClick={this.addRowAbove}>Insert Row Above</button>
<button onClick={this.addRowBelow}>Insert Row Below</button>
<button onClick={this.addColLeft}>Insert Column Left</button>
Expand Down
Loading

0 comments on commit bea6ef6

Please sign in to comment.