PAYATHON 2026

Dash Input 禁用后重新启用时如何恢复焦点

排障赵师傅

结论

处理期间只需禁止修改内容时,建议使用 readOnly。只读输入框可以继续保持焦点,按键不会改变输入值,处理结束后也无需重新聚焦。

如果必须设置 disabled=True,浏览器会立即移走该元素的焦点。回调完成后,需要单独发送一个聚焦信号。客户端收到信号后,先等待输入框恢复可用,再调用 focus()。

autoFocus 为什么没有生效

autoFocus=True 只在组件首次挂载时自动聚焦。disabled 从 True 恢复为 False 时,它不会再次触发。

禁用当前获得焦点的表单控件后,浏览器会主动移走焦点。具体过程如下:

  1. running 将 disabled 设置为 True。
  2. 浏览器移走输入框的焦点。
  3. 回调完成后,Dash 将 disabled 恢复为 False。
  4. 输入框恢复可用,但浏览器不会自动找回原来的焦点。

如果监听到 disabled=False 后立即调用 focus(),还可能碰到更新时序问题。客户端回调执行时,DOM 中的输入框可能仍处于禁用状态,也可能正在进行下一次渲染。

建议使用 readOnly

readOnly 可以阻止用户修改输入值,同时保留输入框的焦点。对于提交后等待处理完成的输入框,这种方式通常更合适。

from time import sleep

from dash import Dash, Input, Output, callback, dcc, html

app = Dash(__name__)

app.layout = html.Div(
    [
        dcc.Input(
            id="input",
            debounce=0.5,
            autoFocus=True,
        ),
        dcc.Input(
            id="output",
            disabled=True,
        ),
    ]
)


@callback(
    Output("output", "value"),
    Input("input", "value"),
    prevent_initial_call=True,
    running=[
        (Output("input", "readOnly"), True, False),
    ],
)
def process(value):
    sleep(2)
    return value


if __name__ == "__main__":
    app.run(debug=True)

处理期间,输入框仍有焦点,但键盘输入不会改变它的值。回调结束后,readOnly 恢复为 False,用户可以继续输入。

Tab 是焦点导航键。即使输入框处于只读状态,用户按下 Tab 后,焦点仍可能转到下一个控件。这属于正常的浏览器行为。

必须使用 disabled 时如何恢复焦点

需要原生禁用语义时,可以增加一个 dcc.Store。服务器回调完成后更新计数器,客户端回调收到变化后,反复检查输入框是否已解除禁用,并在确认可用后执行 focus()。

from time import sleep

from dash import Dash, Input, Output, State, callback, dcc, html

app = Dash(__name__)

app.layout = html.Div(
    [
        dcc.Input(
            id="input",
            debounce=0.5,
            autoFocus=True,
        ),
        dcc.Input(
            id="output",
            disabled=True,
        ),
        dcc.Store(id="focus-sequence", data=0),
        html.Div(id="focus-sink", style={"display": "none"}),
    ]
)


@callback(
    Output("output", "value"),
    Output("focus-sequence", "data"),
    Input("input", "value"),
    State("focus-sequence", "data"),
    prevent_initial_call=True,
    running=[
        (Output("input", "disabled"), True, False),
    ],
)
def process(value, sequence):
    sleep(2)
    return value, sequence + 1


app.clientside_callback(
    """
    function(sequence) {
        const maxAttempts = 40;
        const retryDelay = 25;

        function focusWhenReady(attempt) {
            const element = document.getElementById("input");

            if (element && !element.disabled) {
                window.requestAnimationFrame(function() {
                    if (!element.disabled) {
                        element.focus({preventScroll: true});
                    }
                });
                return;
            }

            if (attempt < maxAttempts) {
                window.setTimeout(function() {
                    focusWhenReady(attempt + 1);
                }, retryDelay);
            }
        }

        focusWhenReady(0);
        return window.dash_clientside.no_update;
    }
    """,
    Output("focus-sink", "children"),
    Input("focus-sequence", "data"),
    prevent_initial_call=True,
)


if __name__ == "__main__":
    app.run(debug=True)

这段代码不依赖 focus-sequence 更新与 running 恢复 disabled=False 的执行顺序。即使聚焦信号先到,客户端代码也会等到输入框解除禁用后再聚焦。

处理期间的键盘输入

disabled 元素无法获得焦点,也不能接收键盘输入。处理期间按键时,如果焦点出现在页面其他位置,这是浏览器禁用表单控件后的正常行为,并非 Dash 发生了其他故障。

不建议在页面级别拦截所有 keydown 事件来限制焦点变化,因为这会干扰 Tab 导航、快捷键和无障碍操作。可以根据需要选择:

  • 只需防止修改输入值时,使用 readOnly。
  • 需要原生禁用语义时,使用 disabled,并在处理完成后延迟恢复焦点。
  • 用户主动按下 Tab 切换控件时,通常应保留这次操作,不要强制抢回焦点。
  • 使用 dcc.Loading 时,尽量避免让加载状态替换或重新创建输入框,否则原来的 DOM 节点和焦点都会消失。

备注:内容仅供参考。