dcc.Tab keyboard navigation (accesibility requirements)

Due to new legislation WCAG2.1 my site needs to have keyboard navigation enabled. Which is giving me a few headaches and some issues with the dcc.Tab.

My preliminary site is organised via 5 tabs, and I have failed at making them accessible via keyboard navigation. Any assistance would be much appreciated.

1 Like

I just had an accessibility review that flagged the same thing. I haven’t spent much time looking into it yet, but this is an absolute must-have. If anyone has any suggestions, they’d be greatly appreciated. In the meantime, I’ll be contacting Plotly support.

You could try the Keyboard component in dash extensions (0.0.24). Here is a small example,

import dash
import dash_html_components as html
import json

from dash.dependencies import Output, Input
from dash_extensions import Keyboard

app = dash.Dash()
app.layout = html.Div([Keyboard(id="keyboard"), html.Div(id="output")])


@app.callback(Output("output", "children"), [Input("keyboard", "keydown")])
def keydown(event):
    return json.dumps(event)


if __name__ == '__main__':
    app.run_server()
1 Like

That’s pretty neat. I’m not sure how I would use that to help with the tab-indexing though. I have users that need to be able to press the tab key to highlight a tab and then select it by pressing the Enter key. If you run the example below, notice that the selection changes when you press the tab key, but that none of the tabs are included in the possible selections. I was toying with the source code to include tabindex=“0” for each tab, but even then, it doesn’t seem like pressing 'Enter" does anything when the tab is selected.

import dash
import dash_html_components as html
import dash_core_components as dcc

from dash.dependencies import Input, Output

app = dash.Dash(__name__)

tabs_styles = {
    'height': '44px'
}
tab_style = {
    'borderBottom': '1px solid #d6d6d6',
    'padding': '6px',
    'fontWeight': 'bold'
}

tab_selected_style = {
    'borderTop': '1px solid #d6d6d6',
    'borderBottom': '1px solid #d6d6d6',
    'backgroundColor': '#119DFF',
    'color': 'white',
    'padding': '6px'
}

app.layout = html.Div([
    dcc.Tabs(id="tabs-styled-with-inline", value='tab-1', children=[
        dcc.Tab(label='Tab 1', value='tab-1', style=tab_style, selected_style=tab_selected_style),
        dcc.Tab(label='Tab 2', value='tab-2', style=tab_style, selected_style=tab_selected_style),
        dcc.Tab(label='Tab 3', value='tab-3', style=tab_style, selected_style=tab_selected_style),
        dcc.Tab(label='Tab 4', value='tab-4', style=tab_style, selected_style=tab_selected_style),
    ], style=tabs_styles),
    html.Div(id='tabs-content-inline')
])

@app.callback(Output('tabs-content-inline', 'children'),
              [Input('tabs-styled-with-inline', 'value')])
def render_content(tab):
    if tab == 'tab-1':
        return html.Div([
            html.H3('Tab content 1'),
            html.P('Tab Index 1', tabIndex="0"),
            html.P('Tab Index 2', tabIndex="0"),
            html.P('Tab Index 3', tabIndex="0")
        ])
    elif tab == 'tab-2':
        return html.Div([
            html.H3('Tab content 2'),
            html.P('Tab Index 1', tabIndex="0"),
            html.P('Tab Index 2', tabIndex="0"),
            html.P('Tab Index 3', tabIndex="0")
        ])
    elif tab == 'tab-3':
        return html.Div([
            html.H3('Tab content 3'),
            html.P('Tab Index 1', tabIndex="0"),
            html.P('Tab Index 2', tabIndex="0"),
            html.P('Tab Index 3', tabIndex="0")
        ])
    elif tab == 'tab-4':
        return html.Div([
            html.H3('Tab content 4'),
            html.P('Tab Index 1', tabIndex="0"),
            html.P('Tab Index 2', tabIndex="0"),
            html.P('Tab Index 3', tabIndex="0")
        ])

if __name__ == '__main__':
    app.run_server(debug=True)

I finally found a solution for this by making the following changes to the dash_core_components.min.js file. The modified sections of the code are copied below, but they’ll have to be manually modified after an update. I’ll pass this along to Plotly support as well. Who knows, maybe they’ll include something similar in a future update.

Tabs (Additional code):

onKeyPress:function(){u||s(g)},tabIndex:"0",role:"tab"

Tabs Before:

return u&&(m+=" tab--disabled ".concat(b||"")),r&&(m+=" tab--selected ".concat(c||"")),y=tn(Array,t)?t[0].props.children:t,i.a.createElement("div",{"data-dash-is-loading":f&&f.is_loading||void 0,id:n,style:h,onClick:function(){u||s(g)},className:nn.a.dynamic([["1358318571",[p.background,p.border,p.border,p.border,p.primary,d,p.border,A?"":"width: calc(100% / ".concat(C,");

Tabs After:

return u&&(m+=" tab--disabled ".concat(b||"")),r&&(m+=" tab--selected ".concat(c||"")),y=tn(Array,t)?t[0].props.children:t,i.a.createElement("div",{"data-dash-is-loading":f&&f.is_loading||void 0,id:n,style:h,onClick:function(){u||s(g)},onKeyPress:function(){u||s(g)},tabIndex:"0",role:"tab",className:nn.a.dynamic([["1358318571",[p.background,p.border,p.border,p.border,p.primary,d,p.border,A?"":"width: calc(100% / ".concat(C,");

Tabs Parent (Additional code):

role:"tablist"

Tabs Parent (Before):

return i.a.createElement("div",{"data-dash-is-loading":this.props.loading_state&&this.props.loading_state.is_loading||void 0,style:this.props.parent_style,id:"".concat(this.props.id,"-parent"),className:nn.a.dynamic([["2495343579",[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]]])+" "+"".concat(s," ").concat(this.props.parent_className||"")},i.a.createElement("div",{style:this.props.style,id:this.props.id,className:nn.a.dynamic([["2495343579",[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]]])+" "+"".concat(c," ").concat(this.props.className||"")},e),i.a.createElement("div",{style:this.props.content_style,className:nn.a.dynamic([["2495343579",[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]]])+" "+"".concat(l," ").concat(this.props.content_className||"")},a||""),i.a.createElement(nn.a,{id:"2495343579",dynamic:[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]},".tab-parent.__jsx-style-dynamic-selector{display:-webkit-box;

Tabs Parent (After):

return i.a.createElement("div",{"data-dash-is-loading":this.props.loading_state&&this.props.loading_state.is_loading||void 0,style:this.props.parent_style,id:"".concat(this.props.id,"-parent"),role:"tablist",className:nn.a.dynamic([["2495343579",[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]]])+" "+"".concat(s," ").concat(this.props.parent_className||"")},i.a.createElement("div",{style:this.props.style,id:this.props.id,className:nn.a.dynamic([["2495343579",[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]]])+" "+"".concat(c," ").concat(this.props.className||"")},e),i.a.createElement("div",{style:this.props.content_style,className:nn.a.dynamic([["2495343579",[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]]])+" "+"".concat(l," ").concat(this.props.content_className||"")},a||""),i.a.createElement(nn.a,{id:"2495343579",dynamic:[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]},".tab-parent.__jsx-style-dynamic-selector{display:-webkit-box;
4 Likes

Hello, I am here for a `tabs’ accessibility requirement as well. Do you mind sharing your modified dash_core_components.min.js file? Also, I haven’t messed around with js files in dcc. If you don’t mind, how do I incorporate this into dash_core_components?

Sure thing. I’ll message you directly.

1 Like

Hello, for the tab accessibility I would need the modified dash_core_components.min.js file as well, if you don’t mind sharing it. Thanks in advance!