Dash Input 禁用后重新启用时如何恢复焦点
结论
处理期间只需禁止修改内容时,建议使用 readOnly。只读输入框可以继续保持焦点,按键不会改变输入值,处理结束后也无需重新聚焦。
如果必须设置 disabled=True,浏览器会立即移走该元素的焦点。回调完成后,需要单独发送一个聚焦信号。客户端收到信号后,先等待输入框恢复可用,再调用 focus()。
autoFocus 为什么没有生效
autoFocus=True 只在组件首次挂载时自动聚焦。disabled 从 True 恢复为 False 时,它不会再次触发。
禁用当前获得焦点的表单控件后,浏览器会主动移走焦点。具体过程如下:
running将disabled设置为True。- 浏览器移走输入框的焦点。
- 回调完成后,Dash 将
disabled恢复为False。 - 输入框恢复可用,但浏览器不会自动找回原来的焦点。
如果监听到 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 节点和焦点都会消失。
备注:内容仅供参考。
