user3058870
user3058870

Reputation: 83

Tab to next tabindex field on down arrow key

I have a webform and want the users to be able to go to the next tabindex field when the down arrow is pressed (behave like the tab key). The following code works when enter key is pressed but if I change the keycode to 40 this code will not work for the down arrow key.

Any help greatly appreciated.

<div>
    <input name="line[]" class=""  type="text" tabindex="1"/>
</div>
<div>
    <input name="line[]" class=""  type="text" tabindex="2"/>
</div>
<div>
    <input name="line[]" class=""  type="text" tabindex="3"/>
</div>

//tab to next tabindex on enter key
<script>
    var id;
    $(document).ready(function(eOuter) {
        $('input').bind('keypress', function(eInner) {
            if (eInner.keyCode == 13){ //enter key
                var tabindex = $(this).attr('tabindex');
                tabindex++; //increment tabindex
                $('[tabindex=' + tabindex + ']').focus();

                $('#Msg').text($(this).attr('id') + " tabindex: " + tabindex + " next element: " + $('*').attr('tabindex').id);
            }
        });
    });
</script>

Upvotes: 6

Views: 18008

Answers (4)

bbsimonbb
bbsimonbb

Reputation: 28992

This was harder than I thought, but if it's important to you, as it is to us, here's what you need to do...

  • walk the dom to create and maintain your own ordered list of elements that can receive focus.
  • create a variable, active, that will track the currently focussed input.
  • listen for focus events so you can update the value of active if the user focusses an input by tabbing or clicking.
  • listen for the arrow keys, and use active to determine the next or previous element in your list and call focus() on it.
  • blur. Tabbing resets to zero if the user clicks outside an input. If you want this for arrow keys, you'll have to add it. (The code below doesn't manage this)

The code below does all this (except the last point) in react. It should be easily adaptable to any other setting. It builds its list once, 500ms after it's called. This is adequate for us for the moment, but if you're adding and removing inputs from your DOM, you'll need to manage this yourself.

import { useEffect, useRef, useState } from "react";
import { IHasHistory, withHistory } from "./traceableEvents";


function addArrowNavigation() {

    function onFocus(e: FocusEvent & IHasHistory) {
        if (!e.history.includes("focusManager.OnFocus")) {
            const activeIndex = inputsRef.current.findIndex((anInput) => anInput === e.target);
            setActive(activeIndex);
            console.log(activeIndex + " active");
            e.history.push("focusManager.OnFocus");
        }
    }

    // stores list of all elements in the page that
    // can receive focus, in order of their appearance.
    const [inputs, setInputs] = useState([]);
    const inputsRef = useRef(inputs)
    useEffect(() => { inputsRef.current = inputs })

    // stores the currently focussed input
    const [active, setActive] = useState<number | undefined>(undefined);
    const activeRef = useRef(active)
    useEffect(() => { activeRef.current = active })

    function registerTabbable(doc: Document) {
        const inputs = [];
        function traverse(el: HTMLElement) {
            if (el.tabIndex >= 0) {
                inputs.push(el);
            }
            if (el.childNodes) {
                for (const node of el.childNodes)
                    if (node instanceof HTMLElement)
                        traverse(node);
            }
        }
        for (const node of doc.childNodes)
            if (node instanceof HTMLElement)
                traverse(node);

        console.log(inputs);
        setInputs(inputs);
    }


    useEffect(() => {
        document.addEventListener("keydown", keyPress);
        document.addEventListener("focusin", (e) =>
            onFocus(withHistory(e))
        );

        setTimeout(() => {
            registerTabbable(document);
        }, 500);

        // Don't forget to clean up
        return function cleanup() {
            document.removeEventListener("keydown", keyPress);
        };
    }, []);

    const keyPress = (e: KeyboardEvent) => {
        console.log(e.keyCode);
        if ([38, 40].includes(e.keyCode)) e.preventDefault();
        switch (e.keyCode) {
            // DOWN ARROW
            case 40: {
                const goTo = activeRef.current === undefined ? 0 : (activeRef.current + 1) % inputsRef.current.length
                inputsRef.current[goTo].focus();
                break;
            }
            // UP ARROW
            case 38: {
                const goTo = activeRef.current === undefined ? 0 : (activeRef.current - 1) % inputsRef.current.length
                inputsRef.current[goTo].focus();
                break;
            }
        }
    }
}

The traceableEvents component, referenced above, is as follows...

export interface IHasHistory {
    history: string[];
}

export function withHistory<TargetShape>(target: any): TargetShape & IHasHistory {
    if (!target.history) {
        (target as IHasHistory).history = [];
    }
    return target;
}

Upvotes: 0

Stevey
Stevey

Reputation: 127

For anyone who wonders how this work in React/Typescript codes, here is a code snippet that works for me (to allow using ArrowDown and ArrowUp keys to achieve the effect of tab through a ul list):

<ul>
            {props.suggestions.map((o, i, a) => (
                <li
                    key={o.id}
                    tabIndex={props.tabIndex}
                    onClick={(e) => (props.onSelect ? props.onSelect(o) : undefined)}
                    onBlur={props.onBlur}
                    onKeyDown={(e) => {
                        if (e.key === 'Enter') {
                            if (props.onSelect) props.onSelect(o);
                        }

                        if (e.key === 'ArrowDown') {
                            e.currentTarget.nextSibling && e.currentTarget.nextSibling.focus();
                        }
                        if (e.key === 'ArrowUp') {
                            e.currentTarget.previousSibling && e.currentTarget.previousSibling.focus();
                        }
                    }}
                />
                
            ))}
        </ul>

The key is e.currentTarget.nextSibling.focus() and e.currentTarget.previousSibling.focus()

Upvotes: 2

Asiful Islam Sakib
Asiful Islam Sakib

Reputation: 552

100% working for up arrow and down arrow

$(document).ready(function(eOuter) {
     $('input').on('keydown', function(eInner) {
         var keyValue = eInner.which; //enter key
         if (keyValue == 40 || keyValue == 38){ 
             var tabindex = $(this).attr('tabindex');
             if(keyValue == 40){ //down arrow 40
                 tabindex++;
             }else{ //up arrow 38
                 tabindex--;
             }
             $('[tabindex=' + tabindex + ']').focus();
         }
     });
});

Upvotes: 3

adeneo
adeneo

Reputation: 318182

The arrow keys don't fire consistently on the keypress event, you want to use keydown instead

$('input').on('keydown', function(eInner) {

    if (eInner.which === 40) { //arrow down

FIDDLE

Your message has some issues as well, the elements don't have ID's, jQuery's attr returns a primitive that has no id property etc.

Upvotes: 3

Related Questions