diff --git a/packages/app/obojobo-document-engine/__tests__/oboeditor/components/visual-editor.test.js b/packages/app/obojobo-document-engine/__tests__/oboeditor/components/visual-editor.test.js index 7bd38f546e..7f11446b51 100644 --- a/packages/app/obojobo-document-engine/__tests__/oboeditor/components/visual-editor.test.js +++ b/packages/app/obojobo-document-engine/__tests__/oboeditor/components/visual-editor.test.js @@ -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() + 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() + 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() + 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', { diff --git a/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/visual-editor.js b/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/visual-editor.js index 700f66b907..544f1d526c 100644 --- a/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/visual-editor.js +++ b/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/visual-editor.js @@ -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 diff --git a/packages/obonode/obojobo-chunks-table/components/cell/__snapshots__/editor-component.test.js.snap b/packages/obonode/obojobo-chunks-table/components/cell/__snapshots__/editor-component.test.js.snap index 823fbac872..6c4ed33d5a 100644 --- a/packages/obonode/obojobo-chunks-table/components/cell/__snapshots__/editor-component.test.js.snap +++ b/packages/obonode/obojobo-chunks-table/components/cell/__snapshots__/editor-component.test.js.snap @@ -9,6 +9,7 @@ exports[`Cell Editor Node Cell component as selected header 1`] = `
-
+
diff --git a/packages/obonode/obojobo-chunks-table/components/cell/editor-component.test.js b/packages/obonode/obojobo-chunks-table/components/cell/editor-component.test.js index 1113de1d19..7555c5f4dd 100644 --- a/packages/obonode/obojobo-chunks-table/components/cell/editor-component.test.js +++ b/packages/obonode/obojobo-chunks-table/components/cell/editor-component.test.js @@ -17,6 +17,20 @@ const TABLE_NODE = 'ObojoboDraft.Chunks.Table' const TABLE_CELL_NODE = 'ObojoboDraft.Chunks.Table.Cell' describe('Cell Editor Node', () => { + let tableComponent + + beforeAll(() => { + tableComponent = mount( + + + + + + +
+ ) + }) + beforeEach(() => { jest.resetAllMocks() jest.restoreAllMocks() @@ -55,15 +69,9 @@ describe('Cell Editor Node', () => { }) test('Cell component handles tabbing', () => { - const component = mount( - - - - - - -
- ) + const component = tableComponent + + document.body.innerHTML = component.html() component .find('button') @@ -79,22 +87,12 @@ describe('Cell Editor Node', () => { }) test('Cell component opens drop down', () => { - const component = mount( - - - - - - -
- ) - - component + tableComponent .find('button') .at(0) .simulate('click') - const tree = component.html() + const tree = tableComponent.html() expect(tree).toMatchSnapshot() }) @@ -601,6 +599,146 @@ describe('Cell Editor Node', () => { expect(thisValue.setState).toHaveBeenCalledWith({ isShowingDropDownMenu: true }) }) + describe('focusing dropdown option', () => { + const button = { + classList: { + add: jest.fn(), + remove: jest.fn() + } + } + + const event = { + target: button + } + + beforeEach(() => { + jest.restoreAllMocks() + }) + + test('onFocus updates classname', () => { + Cell.prototype.onFocus.bind({}, event)() + + expect(button.classList.add).toHaveBeenCalled() + }) + + test('onEndFocus updates classname', () => { + Cell.prototype.onEndFocus.bind({}, event)() + + expect(button.classList.remove).toHaveBeenCalled() + }) + }) + + describe('handling key presses on dropdown options', () => { + let component + let cellControls + + beforeEach(() => { + component = tableComponent + document.body.innerHTML = component.html() + cellControls = Array.from( + document.getElementsByClassName('dropdown-cell')[0].getElementsByTagName('button') + ) + }) + + test('selects option below when [ArrowDown] is pressed', () => { + const event = { + target: cellControls[1], + preventDefault: jest.fn(), + key: 'ArrowDown' + } + + const focus = jest.spyOn(cellControls[2], 'focus') + + Cell.prototype.onKeyDown(event) + + expect(event.preventDefault).toHaveBeenCalled() + expect(focus).toHaveBeenCalled() + }) + + test('does nothing when [ArrowDown] is pressed with no option below', () => { + const event = { + target: cellControls[cellControls.length - 1], + preventDefault: jest.fn(), + key: 'ArrowDown' + } + + const onFocus = jest.spyOn(Cell.prototype, 'onFocus') + Cell.prototype.onKeyDown(event) + + expect(event.preventDefault).toHaveBeenCalled() + expect(onFocus).not.toHaveBeenCalled() + }) + + test('selects option above when [ArrowUp] is pressed', () => { + const event = { + target: cellControls[1], + preventDefault: jest.fn(), + key: 'ArrowUp' + } + + const focus = jest.spyOn(cellControls[0], 'focus') + + Cell.prototype.onKeyDown(event) + + expect(event.preventDefault).toHaveBeenCalled() + expect(focus).toHaveBeenCalled() + }) + + test('does nothing when [ArrowUp] is pressed with no option above', () => { + const event = { + target: cellControls[0], + preventDefault: jest.fn(), + key: 'ArrowUp' + } + + const onFocus = jest.spyOn(Cell.prototype, 'onFocus') + + Cell.prototype.onKeyDown(event) + + expect(event.preventDefault).toHaveBeenCalled() + expect(onFocus).not.toHaveBeenCalled() + }) + + test('executes default behavior when [Tab] is pressed', () => { + const event = { + target: cellControls[1], + preventDefault: jest.fn(), + key: 'Tab' + } + + Cell.prototype.onKeyDown(event) + + expect(event.preventDefault).not.toHaveBeenCalled() + }) + + test('closes dropdown menu when [Tab] is pressed on bottommost dropdown option', () => { + const event = { + target: cellControls[cellControls.length - 1], + preventDefault: jest.fn(), + key: 'Tab' + } + + const click = jest.spyOn(cellControls[0], 'click') + + Cell.prototype.onKeyDown(event) + + expect(event.preventDefault).not.toHaveBeenCalled() + expect(click).toHaveBeenCalled() + }) + + test('executes default behavior when [Enter] is pressed', () => { + const event = { + target: cellControls[1], + preventDefault: jest.fn(), + key: 'Enter' + } + + Cell.prototype.onKeyDown(event) + + expect(event.preventDefault).not.toHaveBeenCalled() + }) + }) + test('componentDidMount does nothing when Cell is not selected', () => { const thisValue = { props: { diff --git a/packages/obonode/obojobo-chunks-table/editor-component.js b/packages/obonode/obojobo-chunks-table/editor-component.js index e334256f0e..fde4e1c033 100644 --- a/packages/obonode/obojobo-chunks-table/editor-component.js +++ b/packages/obonode/obojobo-chunks-table/editor-component.js @@ -73,11 +73,7 @@ class Table extends React.Component { return (
-