Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 125 additions & 24 deletions crates/component/src/input/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,21 +485,35 @@ impl Input {
let Some(gpui::accesskit::ActionData::Value(value)) = data else {
return;
};
if !state.presentation(cx).is_editable() {
return;
}
state.replace_all(value.to_string(), window, cx);
}

fn handle_accessibility_focus(state: &TextInputState, window: &mut Window, cx: &mut App) {
if !state.presentation(cx).is_disabled() {
state.focus(window, cx);
}
}

/// This method must after the refine_style.
fn render_editor(
input_state: TextInputState,
search_panel: Option<AnyElement>,
focus_scope: &gpui::FocusHandle,
_: &Window,
) -> impl IntoElement {
v_flex().size_full().children(search_panel).child(
div()
.relative()
.flex_1()
.child(input_state.into_any_element()),
)
v_flex()
.track_focus(focus_scope)
.size_full()
.children(search_panel)
.child(
div()
.relative()
.flex_1()
.child(input_state.into_any_element()),
)
}
}

Expand Down Expand Up @@ -661,14 +675,18 @@ impl RenderOnce for Input {
if input_focused {
sync_native_content_type(window, content_type, presentation.is_editable());
}
let frame_focus_handle = window
.use_keyed_state(("input-frame-focus", state.entity_id()), cx, |_, cx| {
cx.focus_handle()
// The semantic frame tracks the editor; addons retain their own focus scopes.
let [prefix_focus, suffix_focus, editor_scope_focus] = window
.use_keyed_state(("input-addon-focus", state.entity_id()), cx, |_, cx| {
[cx.focus_handle(), cx.focus_handle(), cx.focus_handle()]
})
.read(cx)
.clone();
let focused = input_focused
|| (frame_focus_handle.contains_focused(window, cx) && !presentation.is_disabled());
|| (!presentation.is_disabled()
&& [&prefix_focus, &suffix_focus, &editor_scope_focus]
.iter()
.any(|focus| focus.contains_focused(window, cx)));

let gap_x = match self.size {
Size::Small => px(4.),
Expand Down Expand Up @@ -713,7 +731,7 @@ impl RenderOnce for Input {
BaseInput::new(id)
.focused(focused)
.disabled(disabled)
.track_focus(&frame_focus_handle)
.track_focus(presentation.focus_handle())
.styles(|styles| {
styles.focused(|style| {
style.when(
Expand All @@ -729,7 +747,11 @@ impl RenderOnce for Input {
this.aria_placeholder(placeholder)
})
.when_some(accessibility_value, |this, value| this.aria_value(value))
.when(!disabled, |this| {
.on_a11y_action(AccessibleAction::Focus, {
let state = state.clone();
move |_, window, cx| Self::handle_accessibility_focus(&state, window, cx)
})
.when(presentation.is_editable(), |this| {
this.on_a11y_action(AccessibleAction::SetValue, move |data, window, cx| {
Self::handle_accessibility_set_value(&accessibility_state, data, window, cx);
})
Expand Down Expand Up @@ -763,11 +785,17 @@ impl RenderOnce for Input {
)
.children(prefix.map(|p| {
div()
.track_focus(&prefix_focus)
.when(presentation.is_disabled(), |this| this.opacity(0.5))
.child(p)
}))
.when(presentation.is_multi_line(), |this| {
this.child(Self::render_editor(state.clone(), overlays.search, window))
this.child(Self::render_editor(
state.clone(),
overlays.search,
&editor_scope_focus,
window,
))
})
.when(!presentation.is_multi_line(), |this| {
this.child(state.clone().into_any_element())
Expand All @@ -776,6 +804,7 @@ impl RenderOnce for Input {
this.pr(self.size.input_px()).child(
h_flex()
.id("suffix")
.track_focus(&suffix_focus)
.gap(gap_x)
.items_center()
.cursor_default()
Expand Down Expand Up @@ -1001,15 +1030,17 @@ mod tests {
) -> impl IntoElement {
let state = self.state.clone();
let emitted = self.emitted.clone();
div().on_prepaint(move |_, window, cx| {
let input = Input::new(&state).render(window, cx).into_element();
let mut node = gpui::accesskit::Node::new(Role::TextInput);
input.write_a11y_info(&mut node);
*emitted.lock().unwrap() = Some((
node.value().map(ToOwned::to_owned),
node.supports_action(AccessibleAction::SetValue),
));
})
div()
.child(Input::new(&state))
.on_prepaint(move |_, window, cx| {
let input = Input::new(&state).render(window, cx).into_element();
let mut node = gpui::accesskit::Node::new(Role::TextInput);
input.write_a11y_info(&mut node);
*emitted.lock().unwrap() = Some((
node.value().map(ToOwned::to_owned),
node.supports_action(AccessibleAction::SetValue),
));
})
}
}

Expand All @@ -1032,14 +1063,84 @@ mod tests {
let base: TextInputState = state.clone().into();
cx.update(|window, cx| {
Input::handle_accessibility_set_value(&base, None, window, cx);
Input::handle_accessibility_focus(&base, window, cx);
assert!(base.presentation(cx).focus_handle().is_focused(window));
window.draw(cx).clear(cx);
});
assert_eq!(state.read_with(cx, |state, _| state.value()), "initial");

let action = gpui::accesskit::ActionData::Value("updated".into());
let changes = std::rc::Rc::new(std::cell::Cell::new(0));
let observed = changes.clone();
let _subscription = cx.update(|_, cx| {
cx.subscribe(&state, move |_, event: &super::super::InputEvent, _| {
if matches!(event, super::super::InputEvent::Change) {
observed.set(observed.get() + 1);
}
})
});
let action = gpui::accesskit::ActionData::Value("updated🦀".into());
cx.update(|window, cx| {
Input::handle_accessibility_set_value(&base, Some(&action), window, cx);
});
assert_eq!(state.read_with(cx, |state, _| state.value()), "updated🦀");
assert_eq!(changes.get(), 1);
for disabled in [false, true] {
cx.update(|window, cx| {
base.set_disabled(disabled, cx);
base.set_readonly(!disabled, cx);
window.blur(cx);
Input::handle_accessibility_focus(&base, window, cx);
assert_eq!(
base.presentation(cx).focus_handle().is_focused(window),
!disabled
);
let action = gpui::accesskit::ActionData::Value("rejected".into());
Input::handle_accessibility_set_value(&base, Some(&action), window, cx);
});
assert_eq!(state.read_with(cx, |state, _| state.value()), "updated🦀");
assert_eq!(changes.get(), 1);
}
cx.update(|window, cx| {
base.set_disabled(false, cx);
base.set_readonly(false, cx);
Input::handle_accessibility_focus(&base, window, cx);
window.draw(cx).clear(cx);
window.dispatch_action(Box::new(super::super::Undo), cx);
});
assert_eq!(state.read_with(cx, |state, _| state.value()), "initial");
cx.update(|window, cx| {
state.update(cx, |state, cx| state.set_masked(true, window, cx));
Input::handle_accessibility_set_value(&base, Some(&action), window, cx);
window.draw(cx).clear(cx);
});
assert_eq!(state.read_with(cx, |state, _| state.value()), "updated🦀");
assert_eq!(*captured.lock().unwrap(), Some((None, true)));
}

#[gpui::test]
fn accessibility_set_value_preserves_exact_editor_text(cx: &mut gpui::TestAppContext) {
use gpui::{AppContext as _, Render};

struct Probe(Entity<crate::input::EditorState>);

impl Render for Probe {
fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
div().child(crate::input::Editor::new(&self.0))
}
}

cx.update(crate::init);
let (probe, cx) = cx.add_window_view(|window, cx| {
Probe(cx.new(|cx| crate::input::EditorState::new(window, cx).language("rust")))
});
let editor = probe.read_with(cx, |probe, _| probe.0.clone());
let state: TextInputState = editor.clone().into();
let action = gpui::accesskit::ActionData::Value("(".into());

cx.update(|window, cx| {
Input::handle_accessibility_set_value(&state, Some(&action), window, cx)
});
assert_eq!(state.read_with(cx, |state, _| state.value()), "updated");
assert_eq!(editor.read_with(cx, |editor, _| editor.value()), "(");
}

#[gpui::test]
Expand Down
Loading