diff --git a/fixtures/realistic.ods b/fixtures/realistic.ods index 8df58eb..0d5c2f2 100644 Binary files a/fixtures/realistic.ods and b/fixtures/realistic.ods differ diff --git a/lib/importer/assets/css/selectable_table.css b/lib/importer/assets/css/selectable_table.css index 39ab961..aefd048 100644 --- a/lib/importer/assets/css/selectable_table.css +++ b/lib/importer/assets/css/selectable_table.css @@ -29,13 +29,15 @@ table.selectable td { border-right-width: 1px; white-space: pre-wrap; font-variant-numeric: tabular-nums; + min-width: 8em; + max-width: 15em; } table.selectable th *, table.selectable td * { box-sizing: border-box; font-family: inherit; - cursor: inherit + cursor: inherit; } table.selectable th a, @@ -237,6 +239,10 @@ table.selectable:not(.editable) tbody:empty::before { box-sizing: border-box } +table.selectable td.selected.focus { + border: 2px solid blue; +} + table.selectable tbody td, table.selectable tbody td.selected.focus { background-color: white diff --git a/lib/importer/assets/js/selectable_table.js b/lib/importer/assets/js/selectable_table.js index a51484a..bb64372 100644 --- a/lib/importer/assets/js/selectable_table.js +++ b/lib/importer/assets/js/selectable_table.js @@ -1,3 +1,10 @@ +/* + Accessability TODOs: + + - Implement all of the data grid keyboard commands in https://www.w3.org/WAI/ARIA/apg/patterns/grid/: Page Up, Page Down, Home, End, Ctrl+Home, Ctrl+End, Ctrl+Space, Shift+Space + + */ + const get_platform = () => { // userAgentData is not widely supported yet if (typeof navigator.userAgentData !== 'undefined' && navigator.userAgentData != null) { @@ -64,6 +71,48 @@ window.addEventListener("load", function() { .join("|") ; let EventState = class { + + /* + A map of event states that you want to go full-screen to view because + it's wide, but believe me, it's impossible to read when I wrote it as a + tree: + +| EventState | Event handlers active in this state | When we enter | When we leave | +|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------| +| DocumentSelectMode | keydown -> function updateFromKeyEvent, function selectModeKeyboardShortcuts | function cellSelectMode | function cellEditMode | +| | keyup -> function updateFromKeyEvent | function tableFocusIn | function loseFocus | +| | click -> function loseFocus -> -DocumentSelectMode, +DocumentUnfocusedMode | function getFocus | | +| | cut -> function cutSelection | | | +| | copy -> function copySelection | | | +| | paste -> function pasteSelection | | | +|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------| +| DocumentEditMode | keydown -> function editModeKeyboardShortcuts | function cellEditMode | function cellSelectMode | +|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------| +| DocumentUnfocusedMode | mousedown -> getFocus -> +DocumentSelectMode, -DocumentUnfocusedMode | function loseFocus | function tableFocusIn | +| | | initialisation | function getFocus | +|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------| +| TableSelectMode | selectstart -> function preventDefault | function cellSelectMode | function cellEditMode | +| | mousedown -> function startDrag | function tableFocusIn | | +| | | initialisation | | +|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------| +| TableEditMode | | function cellEditMode | function cellSelectMode | +|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------| +| TableUnfocusedMode | focusIn -> tableFocusIn -> -DocumentUnfocusedMode, -TableUnfocusedMode, +TableSelectMode, +DocumentSelectMode | function loseFocus | function tableFocusIn | +| | | | function getFocus | +|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------| +| InputSelectMode | mousedown -> function preventInputFocus, function removeFocus | function cellSelectMode (input elements inside cell) | function cellEditMode (input elements inside cell) | +| | click -> function preventInputFocus | initialisation ("new row"/"new column" logic?!?) | | +| | focus -> function enterCell | initialisation (all input elements in table) | | +| | blur -> function leaveCell | | | +|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------| +| InputEditMode | blur -> function leaveCell | function cellEditMode (input elements inside cell) | function cellSelectMode (input elements inside cell) | +|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------| +| InputAliveState | input -> function autoResize, (function createNewRows/function createNewColumns?), function addRowClasses | initialisation ("new row"/"new column" logic?!?) | | +| | change -> function autoResize, (function createNewRows/function createNewColumns?), function addRowClasses | initialisation (all input elements in table) | | +|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------| + + */ + constructor(name) { this.name = name; this.events = []; @@ -165,9 +214,9 @@ window.addEventListener("load", function() { }; get left() { return CellRef.fromCoords(this.table, this.row + 0, this.col - 1); } - get right() { return CellRef.fromCoords(this.table, this.row + 0, this.col + 1); } + get right() { return CellRef.fromCoords(this.table, this.row + 0, this.col + this.colspan); } get above() { return CellRef.fromCoords(this.table, this.row - 1, this.col + 0); } - get below() { return CellRef.fromCoords(this.table, this.row + 1, this.col + 0); } + get below() { return CellRef.fromCoords(this.table, this.row + this.rowspan, this.col + 0); } equals(cell) { return this.table == cell.table && @@ -408,7 +457,6 @@ window.addEventListener("load", function() { // getRangeOfCells will do the CellRef.fromCoords lookup for every cell, // any of which might require a scan through the entire . - // Start by normalising min/max values let minRow = Math.min(this.actualStartCell.row, this.actualEndCell.row); let maxRow = Math.max(this.actualStartCell.row, this.actualEndCell.row); @@ -605,6 +653,11 @@ window.addEventListener("load", function() { var tables = document.querySelectorAll("table.selectable"); for (var table of tables) { + { + const selectables = table.querySelectorAll('td'); + selectables[0].setAttribute("tabindex", 0); // First one is tabbable, to get the user into the table + } + var selection = NilSelection; const changeSelection = function(startCell, endCell) { @@ -619,13 +672,25 @@ window.addEventListener("load", function() { for (var cell of oldCells) { for (var className of CellSelectedClasses) { cell.node.classList.remove(className); + cell.node.setAttribute("aria-selected", "false"); } } + // Should just be one, but let's be sure + var oldSelectables = table.querySelectorAll("[tabindex=\"0\"]") + for (var cell of oldSelectables) { + cell.setAttribute("tabindex", -1); + } + selection = newSelection; if (selection == NilSelection) { return; } + selection.focus.node.focus(); + selection.focus.node.setAttribute("tabindex", 0); selection.focus.node.classList.add(CellSelectedFocusClassName); - for (var cell of selection.cells) { cell.node.classList.add(CellSelectedClassName); } + for (var cell of selection.cells) { + cell.node.classList.add(CellSelectedClassName); + cell.node.setAttribute("aria-selected", "truee"); + } for (var cell of selection.bottomCells) { cell.node.classList.add(CellSelectedBottomClassName); } for (var cell of selection.topCells) { cell.node.classList.add(CellSelectedTopClassName); } for (var cell of selection.leftCells) { cell.node.classList.add(CellSelectedLeftClassName); } @@ -783,6 +848,7 @@ window.addEventListener("load", function() { } var TableSelectMode = new EventState("select"); + var TableUnfocusedMode = new EventState("unfocused"); var TableEditMode = new EventState("edit"); var InputSelectMode = new EventState("select"); var InputEditMode = new EventState("edit"); @@ -901,7 +967,7 @@ window.addEventListener("load", function() { } var selectAll = function() { - changeSelection(TableSelection.fromElement(table)); + applySelection(TableSelection.fromElement(table)); } // List of key codes that we think shouldn't trigger cell editing @@ -909,7 +975,8 @@ window.addEventListener("load", function() { .filter(g => g != "Whitespace" && g != "IMEAndComposition") .map(k => KeyGroups[k]) .reduce((acc, cur) => acc.concat(cur), []) - .filter(k => k != "Backspace"); + .filter(k => k != "Backspace") + .concat("Tab"); var valueKeyPressed = function(keyEvent) { return (!ControlKeyCodes.includes(keyEvent.key) && !keyEvent.ctrlKey && !keyEvent.metaKey); @@ -925,14 +992,12 @@ window.addEventListener("load", function() { else if (!keyEvent.shiftKey && keyEvent.key == "ArrowRight") { changeSelection(selection.focus.right); keyEvent.preventDefault(); } else if (!keyEvent.shiftKey && keyEvent.key == "ArrowDown") { changeSelection(selection.focus.below); keyEvent.preventDefault(); } else if (!keyEvent.shiftKey && keyEvent.key == "ArrowUp") { changeSelection(selection.focus.above); keyEvent.preventDefault(); } - else if (keyEvent.shiftKey && keyEvent.key == "ArrowLeft") { changeSelection(selection.focus, selection.endCell.left); keyEvent.preventDefault(); } - else if (keyEvent.shiftKey && keyEvent.key == "ArrowRight") { changeSelection(selection.focus, selection.endCell.right); keyEvent.preventDefault(); } - else if (keyEvent.shiftKey && keyEvent.key == "ArrowDown") { changeSelection(selection.focus, selection.endCell.below); keyEvent.preventDefault(); } - else if (keyEvent.shiftKey && keyEvent.key == "ArrowUp") { changeSelection(selection.focus, selection.endCell.above); keyEvent.preventDefault(); } + else if (keyEvent.shiftKey && keyEvent.key == "ArrowLeft") { changeSelection(selection.focus, selection.actualEndCell.left); keyEvent.preventDefault(); } + else if (keyEvent.shiftKey && keyEvent.key == "ArrowRight") { changeSelection(selection.focus, selection.actualEndCell.right); keyEvent.preventDefault(); } + else if (keyEvent.shiftKey && keyEvent.key == "ArrowDown") { changeSelection(selection.focus, selection.actualEndCell.below); keyEvent.preventDefault(); } + else if (keyEvent.shiftKey && keyEvent.key == "ArrowUp") { changeSelection(selection.focus, selection.actualEndCell.above); keyEvent.preventDefault(); } else if (!keyEvent.shiftKey && keyEvent.key == "Enter") { applySelection(selection.focusCursor.nextFocusByColumn()); keyEvent.preventDefault(); } - else if (!keyEvent.shiftKey && keyEvent.key == "Tab") { applySelection(selection.focusCursor.nextFocusByRow()); keyEvent.preventDefault(); } else if (keyEvent.shiftKey && keyEvent.key == "Enter") { applySelection(selection.focusCursor.prevFocusByColumn()); keyEvent.preventDefault(); } - else if (keyEvent.shiftKey && keyEvent.key == "Tab") { applySelection(selection.focusCursor.prevFocusByRow()); keyEvent.preventDefault(); } else if (valueKeyPressed(keyEvent)) { var input = selection.focus.node.querySelector(InputElementsSelector); var event = new KeyboardEvent(keyEvent.type, keyEvent); @@ -952,10 +1017,6 @@ window.addEventListener("load", function() { cellSelectMode(selection.focus); applySelection(selection.focusCursor.nextFocusByColumn()); } - if (keyEvent.key == "Tab") { - cellSelectMode(selection.focus); - applySelection(selection.focusCursor.nextFocusByRow()); - } if (keyEvent.key == "Escape") { cellSelectMode(selection.focus); } @@ -986,14 +1047,14 @@ window.addEventListener("load", function() { if (event.target.closest("table") !== table) { DocumentSelectMode.leave(document); - - // If we not set the table's data-persist-selection attribute to "true" then we will apply - // the Nil selection when the table loses focus. - if ( !table.dataset.persistSelection || table.dataset.persistSelection.toLowerCase() != "true") { - applySelection(NilSelection); - } + // If we not set the table's data-persist-selection attribute to "true" then we will apply + // the Nil selection when the table loses focus. + if ( !table.dataset.persistSelection || table.dataset.persistSelection.toLowerCase() != "true") { + applySelection(NilSelection); + } DocumentUnfocusedMode.enter(document); + TableUnfocusedMode.enter(table); } } DocumentSelectMode.addEvent("click", loseFocus); @@ -1002,8 +1063,25 @@ window.addEventListener("load", function() { elementsOutsideTable.snapshotItem(e).addEventListener("focus", loseFocus); } + var tableFocusIn = function(event) { + var tableSelectables = table.querySelectorAll("[tabindex=\"0\"]") + if(tableSelectables[0]) { + // If we don't have a focus, set it to the externally-focussed cell + if(selection == NilSelection) { + selection = TableSelection.fromElement(tableSelectables[0]); + applySelection(selection); + } + } + DocumentUnfocusedMode.leave(document); + TableUnfocusedMode.leave(table); + TableSelectMode.enter(table); + DocumentSelectMode.enter(document); + } + TableUnfocusedMode.addEvent("focusin", tableFocusIn); + var getFocus = function(event) { DocumentUnfocusedMode.leave(document); + TableUnfocusedMode.leave(table); DocumentSelectMode.enter(document); } DocumentUnfocusedMode.addEvent("mousedown", getFocus); diff --git a/lib/importer/nunjucks/importer/macros/range_selector.njk b/lib/importer/nunjucks/importer/macros/range_selector.njk index d8de0a4..7c3baf4 100644 --- a/lib/importer/nunjucks/importer/macros/range_selector.njk +++ b/lib/importer/nunjucks/importer/macros/range_selector.njk @@ -9,15 +9,21 @@
-
+
{% if caption %} - + {% endif %} - + {% for row in rows %} - + {% for cell in row %} -
{{caption}}{{caption}}