Perspective Table Row Navigation using Keyboard

In Perspective, the Table control does not seem to allow row navigation using the keyboard (arrow up-down) but only by selecting the row with the mouse. Is this the expected behavior?

Yep :slight_smile: I haven't used a SCADA platform with this ability, not saying there isn't one, but I wouldn't expect it. I certainly haven't seen many web brower tables with this functionality

Edit: although there do seem to be some cool js/react tables out there with this functionality

1 Like

Thank you for the confirmation.

I had the same question.

I had resolve this by adding a sessions Events, Key Event.
I send a 'page' message. The table catch it and change the selectedRow.

Not sure why you would use a Session Event. I've done this using an Event on the table itself...

A client asked for the same thing, and we solved it with an event on the table doing +/-1 to the props.selection.selectedRow depending on the pressed arrow (event.key) like you described.

However, if a user filters or sorts the table without modifying the underlying query, selectedRow still refers to the original row index of the props.data dataset in the original order, not the visual rowIndex of the data in the displayed order. This means that pressing up/down can make the selected row jump around or disappear (when it refers to a filtered row), which is obviously not the desired behavior. Is there a way to solve this issue?

My best leads right now are:

  1. The fact that some events can return row and rowIndex separately (like clicking on a row, but not events like pressing a key or changing selection):
  2. When using subviews, the two are default input parameters too, so I can create a column that shows the original row and the current rowIndex together like this:

As an example of the desired behavior, if selectedRow=88 and the user presses the down arrow key, it needs to change into 92, not 89. My idea is to find the rowIndex of row=88, which is 1, then add 1, then find the row of rowIndex=2, which is 92.

But I haven’t found a direct way to find these properties and do an easy mapping. Making each subview send a message back to the main table with its own pair to reconstruct an array seems like it could easily overload the system. Is there something I’m missing?

1 Like

When you say you want to use the keys to navigate rows, do you just mean like this?

There are some bugs if the table is virtualised – i.e. it will not work properly. Same for tables with a pager probably…

If so, copy this markdown component into your View.

Markdown Component JSON
[
  {
    "type": "ia.display.markdown",
    "version": 0,
    "props": {
      "style": {
        "position": "absolute"
      },
      "markdown": {
        "escapeHtml": false
      }
    },
    "meta": {
      "name": "JSInject_Table_AddArrowKeyRowSelection"
    },
    "position": {
      "shrink": 0,
      "basis": 0
    },
    "custom": {
      "inlineJavascript": "(() => {\n    const view = [...window.__client.page.views._data.values()].find(view => view.value.mountPath == this.parentNode.parentNode.parentNode.getAttributeNode('data-component-path').value.split('.')[0]).value;\n    const grid = document.querySelector('.ReactVirtualized__Grid');\n    const table = document.querySelector('.ia_table');\n\n    if (!grid || !table) return;\n\n    grid.setAttribute('tabindex', '0');\n\n    let selectedRowIndex = -1;\n\n    function getRows() {\n        return table.querySelectorAll('.ia_table__body__rowGroup');\n    }\n\n    function selectRow(index) {\n        const rows = getRows();\n        if (rows.length === 0) return;\n\n        index = Math.max(0, Math.min(index, rows.length - 1));\n\n        rows.forEach(row => {\n            row.style.outline = '';\n            row.style.backgroundColor = '';\n        });\n\n        const target = rows[index];\n        target.style.outline = '2px solid #0078d4';\n        target.style.backgroundColor = 'rgba(0, 120, 212, 0.15)';\n\n        selectedRowIndex = index;\n        target.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n        view.custom.write('selectedRow', selectedRowIndex);\n    }\n\n    grid.addEventListener('keydown', (e) => {\n        const rows = getRows();\n        if (rows.length === 0) return;\n\n        switch (e.key) {\n            case 'ArrowDown':\n                e.preventDefault();\n                selectRow(selectedRowIndex < 0 ? 0 : selectedRowIndex + 1);\n                break;\n            case 'ArrowUp':\n                e.preventDefault();\n                selectRow(selectedRowIndex <= 0 ? 0 : selectedRowIndex - 1);\n                break;\n            case 'Home':\n                e.preventDefault();\n                selectRow(0);\n                break;\n            case 'End':\n                e.preventDefault();\n                selectRow(rows.length - 1);\n                break;\n        }\n    });\n\ntable.addEventListener('click', (e) => {\n    const clickedRow = e.target.closest('.ia_table__body__rowGroup');\n    if (!clickedRow) return;\n    const rows = Array.from(getRows());\n    const index = rows.indexOf(clickedRow);\n    if (index !== -1) selectRow(index);\n});\n\n    grid.focus();\n})()"
    },
    "propConfig": {
      "props.source": {
        "binding": {
          "config": {
            "struct": {
              "script": "{this.custom.inlineJavascript}"
            },
            "waitOnAll": true
          },
          "transforms": [
            {
              "code": "#\tcode =  \"<img style='display:none' src='/favicon.ico' onload=\\\"\" + value.script.replace('\"', '&quot;') + '\\\"></img>'\n#\t\n#\tcode = code.replace(\"\\n\", \"\").replace(\"\\t\", \"\").replace(\"\\n\", \"\").replace(\"\\r\", \"\").replace(\"\\r\", \"\").replace(\"  \", \" \").replace(\"  \", \" \").replace(\"  \", \" \").replace(\"  \", \" \")\n#\t\n#\treturn code\n\tscript = value.script\n\tscript = script.replace('\"', '&quot;')\n\tscript = script.replace('\\n', '').replace('\\t', '').replace('\\r', '')\n\t\n\t# Collapse multiple spaces - repeat until stable\n\twhile '  ' in script:\n\t    script = script.replace('  ', ' ')\n\t\n\tcode = '<img style=\"display:none\" src=\"/favicon.ico\" onload=\"{}\"></img>'.format(script)\n\treturn code",
              "type": "script"
            }
          ],
          "type": "expr-struct"
        }
      }
    }
  }
]

Note: this will write the selected row index with the keyboard to view.custom.selectedRow. You can bidirectionally bind your table’s props.selection.selectedRow to this.

2 Likes

Wow, thanks!

However, I tried it and ran into a couple of issues. I created a new view, inserted a default table component and copied this markdown over. Then I made the bidirectional binding to selectedRow as indicated. Following this comment:

There are some bugs if the table is virtualised – i.e. it will not work properly. Same for tables with a pager probably…

I disabled the pager and set props.virtualized = false on the table and tried on preview mode, but nothing moved when pressing the arrow keys. Then I changed it back to true and it worked like in your video, which was promising.

However, when sorting or filtering the issue is the same as with a simple event changing selectedRow, doing index+/-1 is based on the original order, not the visual one. Maybe that’s to be expected when the table is virtualized, and I missed a step to make this work, but I’m unsure. Hopefully this video helps see what’s happening:


When I sort by “city”, Folsom (row=0) visually shows up at the 11th row (rowIndex=10), but when pressing down, setting selectedRow=10+1 makes the table select row=11, not rowIndex=11, and thus the selection changes to what was the original 12th row, Delhi, which now is actually a couple rows before Folsom. Unless this is the bug you mentioned when the table is virtualized, we should obtain the row value of rowIndex=11 first and then write that into selectedRow so it selects Guadalajara.

Please let me know if it didn’t work like this on your end and if you see anything I did wrong. I’m testing with subviews, where we can use both properties, and messages to send the index back and forth, will report back if it works. Thanks again for looking into this!