Misc · Codes and checks: the captcha keeps focus through every verdict
Verify never rests, so no verdict or new ask takes its focus handle out of the tree; with nothing typed it sends focus to the field. Enter hands nothing over while the owner checks. A pass moves focus from the resting field to the Verified mark, so the field shows no ring and no caret.
This commit is contained in:
1 parent
fd052086eb
commit
a184b4ef9a
2 files changed
+67
-10
No files matched your search
+17
-7
@@ -21,15 +21,15 @@ type OnRefresh = Rc<dyn Fn(&mut Window, &mut App)>;
|
|||||||
pub enum CaptchaState {
|
pub enum CaptchaState {
|
||||||
#[default]
|
#[default]
|
||||||
Asking,
|
Asking,
|
||||||
/// The owner checks the answer: Verify spins and takes no press.
|
/// The owner checks the answer: Verify spins and takes no press, and Enter hands nothing over.
|
||||||
Checking,
|
Checking,
|
||||||
/// The answer did not match.
|
/// The answer did not match.
|
||||||
Wrong,
|
Wrong,
|
||||||
/// The field rests, and Verify becomes a mark that keeps its focus.
|
/// The field rests, and Verify becomes a mark that takes its focus and the field's.
|
||||||
Passed,
|
Passed,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A picture of characters to copy, from the owner, over a field to type them. Verify or Enter hands the answer to the owner, who checks it and says how it stood; the field empties with each answer and keeps focus. With `on_refresh`, a button asks the owner for a new picture.
|
/// A picture of characters to copy, from the owner, over a field to type them. Verify or Enter hands the answer to the owner, who checks it and says how it stood; the field empties with each answer and keeps focus, and Verify with nothing typed sends focus to it. With `on_refresh`, a button asks the owner for a new picture.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Captcha {
|
pub struct Captcha {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
@@ -88,12 +88,20 @@ impl RenderOnce for Captcha {
|
|||||||
field.update(cx, |field, cx| field.set_disabled(passed, cx));
|
field.update(cx, |field, cx| field.set_disabled(passed, cx));
|
||||||
}
|
}
|
||||||
let verify = tab_stop((id.clone(), "verify").into(), !passed, window, cx);
|
let verify = tab_stop((id.clone(), "verify").into(), !passed, window, cx);
|
||||||
let empty = field.read(cx).text().trim().is_empty();
|
if passed && field.read(cx).focus().is_focused(window) {
|
||||||
|
log::info!("captcha: passed; focus goes to the mark");
|
||||||
|
window.focus(&verify);
|
||||||
|
}
|
||||||
|
let checking = self.state == CaptchaState::Checking;
|
||||||
let answer = {
|
let answer = {
|
||||||
let field = field.clone();
|
let field = field.clone();
|
||||||
Rc::new(move |window: &mut Window, cx: &mut App| {
|
Rc::new(move |window: &mut Window, cx: &mut App| {
|
||||||
|
if checking {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let text = SharedString::from(field.read(cx).text().trim().to_string());
|
let text = SharedString::from(field.read(cx).text().trim().to_string());
|
||||||
if text.is_empty() {
|
if text.is_empty() {
|
||||||
|
window.focus(field.read(cx).focus());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log::info!("captcha: answered");
|
log::info!("captcha: answered");
|
||||||
@@ -108,7 +116,10 @@ impl RenderOnce for Captcha {
|
|||||||
let action = match passed {
|
let action = match passed {
|
||||||
true => div()
|
true => div()
|
||||||
.id((id.clone(), "verified"))
|
.id((id.clone(), "verified"))
|
||||||
.debug_selector(|| "captcha-verified".into())
|
.debug_selector(|| match verify.is_focused(window) {
|
||||||
|
true => "captcha-verified-focused".into(),
|
||||||
|
false => "captcha-verified".into(),
|
||||||
|
})
|
||||||
.track_focus(&verify)
|
.track_focus(&verify)
|
||||||
.flex()
|
.flex()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
@@ -133,8 +144,7 @@ impl RenderOnce for Captcha {
|
|||||||
Button::new((id.clone(), "verify-button"), "Verify")
|
Button::new((id.clone(), "verify-button"), "Verify")
|
||||||
.variant(ButtonVariant::Primary)
|
.variant(ButtonVariant::Primary)
|
||||||
.focus_handle(&verify)
|
.focus_handle(&verify)
|
||||||
.loading(self.state == CaptchaState::Checking)
|
.loading(checking)
|
||||||
.disabled(empty && self.state != CaptchaState::Checking)
|
|
||||||
.on_click(move |_, window, cx| answer(window, cx))
|
.on_click(move |_, window, cx| answer(window, cx))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-3
@@ -99,15 +99,62 @@ fn a_captcha_hands_over_answers_and_keeps_focus(cx: &mut TestAppContext) {
|
|||||||
STATE.set(CaptchaState::Passed);
|
STATE.set(CaptchaState::Passed);
|
||||||
show(cx);
|
show(cx);
|
||||||
assert!(
|
assert!(
|
||||||
shown("captcha-verified", cx) && !at_root(cx),
|
shown("captcha-verified-focused", cx),
|
||||||
"the field kept focus"
|
"the resting field handed its focus to the mark"
|
||||||
);
|
);
|
||||||
cx.simulate_input("again");
|
cx.simulate_input("again");
|
||||||
press("enter", cx);
|
press("enter", cx);
|
||||||
assert_eq!(said().len(), 2, "a passed captcha takes no more answers");
|
assert_eq!(said().len(), 2, "a passed captcha takes no more answers");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enter on Verify hands the answer over and focus to the emptied field, as Verify rests.
|
/// With the field emptied, verdicts and a new ask turn Verify and its mark into each other on one focus handle.
|
||||||
|
#[gpui::test]
|
||||||
|
fn the_owners_verdicts_leave_focus_in_place(cx: &mut TestAppContext) {
|
||||||
|
let (_, cx) = stage(captcha, cx);
|
||||||
|
press("tab", cx);
|
||||||
|
cx.simulate_input("W7XK");
|
||||||
|
press("enter", cx);
|
||||||
|
STATE.set(CaptchaState::Checking);
|
||||||
|
show(cx);
|
||||||
|
press("tab", cx);
|
||||||
|
STATE.set(CaptchaState::Wrong);
|
||||||
|
show(cx);
|
||||||
|
assert!(!at_root(cx), "Verify stayed a stop through the verdict");
|
||||||
|
STATE.set(CaptchaState::Passed);
|
||||||
|
show(cx);
|
||||||
|
STATE.set(CaptchaState::Asking);
|
||||||
|
show(cx);
|
||||||
|
assert!(!at_root(cx), "the mark's focus went back to Verify");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enter hands nothing over while the owner checks.
|
||||||
|
#[gpui::test]
|
||||||
|
fn enter_waits_while_the_owner_checks(cx: &mut TestAppContext) {
|
||||||
|
let (_, cx) = stage(captcha, cx);
|
||||||
|
press("tab", cx);
|
||||||
|
cx.simulate_input("W7XK");
|
||||||
|
press("enter", cx);
|
||||||
|
STATE.set(CaptchaState::Checking);
|
||||||
|
show(cx);
|
||||||
|
cx.simulate_input("again");
|
||||||
|
press("enter", cx);
|
||||||
|
assert_eq!(said(), ["W7XK"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify with nothing typed sends focus to the field.
|
||||||
|
#[gpui::test]
|
||||||
|
fn an_empty_verify_sends_focus_to_the_field(cx: &mut TestAppContext) {
|
||||||
|
let (_, cx) = stage(captcha, cx);
|
||||||
|
press("tab", cx);
|
||||||
|
press("tab", cx);
|
||||||
|
press("enter", cx);
|
||||||
|
assert!(said().is_empty());
|
||||||
|
cx.simulate_input("W7XK");
|
||||||
|
press("enter", cx);
|
||||||
|
assert_eq!(said(), ["W7XK"], "the typing reached the field");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enter on Verify hands the answer over and focus to the emptied field.
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn verify_hands_focus_to_the_field(cx: &mut TestAppContext) {
|
fn verify_hands_focus_to_the_field(cx: &mut TestAppContext) {
|
||||||
let (_, cx) = stage(captcha, cx);
|
let (_, cx) = stage(captcha, cx);
|
||||||
|
|||||||
Reference in new issue
Block a user