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.

1 Like