commit 14b6142ef53677ae1f035200faecf653bea892f2
parent 61bc8b5b1befa2829c799ba76b81c96e2a1d9eaa
Author: triesap <tyson@radroots.org>
Date: Tue, 21 Apr 2026 04:13:56 +0000
app: add deterministic keyboard autofocus contract
- map startup, buyer, seller, settings, and about surfaces to stable first-focus targets
- focus existing button and input handles after route and panel transitions without changing shared ui primitives
- cover the autofocus routing with launcher unit tests for startup, buyer, seller, and settings states
- keep the change scoped to window.rs while preserving the current localized id and copy contracts
Diffstat:
1 file changed, 771 insertions(+), 26 deletions(-)
diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs
@@ -1,7 +1,7 @@
use gpui::{
- Animation, AnimationExt, AnyElement, App, AppContext, Bounds, ClickEvent, Context, Entity,
- InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
- Timer, Window, WindowBackgroundAppearance, WindowBounds, WindowOptions, div,
+ Animation, AnimationExt, AnyElement, App, AppContext, Bounds, ClickEvent, Context, ElementId,
+ Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
+ Subscription, Timer, Window, WindowBackgroundAppearance, WindowBounds, WindowOptions, div,
prelude::FluentBuilder, px, relative, rgb, size,
};
use gpui_component::{
@@ -235,6 +235,51 @@ struct StartupSignerPollCycleResult {
outcome: Result<RadrootsAppRemoteSignerPendingPollOutcome, String>,
}
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+struct HomeAutoFocusState {
+ has_startup_signer_input: bool,
+ startup_signer_input_is_editable: bool,
+ has_farm_setup_form: bool,
+ has_personal_search_input: bool,
+ has_buyer_checkout_form: bool,
+ has_products_search_input: bool,
+ has_products_stock_editor: bool,
+ has_product_editor_form: bool,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum HomeAutoFocusTarget {
+ StartupContinue,
+ StartupGenerateKey,
+ StartupSignerInput,
+ StartupSignerBack,
+ BuyerSearchInput,
+ BuyerListingOpenFirst,
+ BuyerDetailBack,
+ BuyerCartOpenCheckout,
+ BuyerCheckoutNameInput,
+ BuyerOrderOpenFirst,
+ BuyerOrderConfirmReplace,
+ BuyerOrderRepeatDemand,
+ FarmerReminderPrimary,
+ FarmerReminderDismiss,
+ FarmerSetupStart,
+ FarmerSetupContinue,
+ FarmerSetupFarmNameInput,
+ FarmerTodayReminderChipFirst,
+ FarmerTodayOpenPackDay,
+ FarmerTodayOpenOrders,
+ FarmerTodayOpenProductsLowStock,
+ FarmerTodayOpenProductsDrafts,
+ ProductsSearchInput,
+ ProductsRowOpenFirst,
+ ProductsStockInput,
+ ProductEditorTitleInput,
+ OrdersRowOpenFirst,
+ OrdersDetailMarkPacked,
+ OrdersDetailMarkCompleted,
+}
+
impl HomeView {
pub fn new(runtime: DesktopAppRuntime) -> Self {
Self {
@@ -254,6 +299,150 @@ impl HomeView {
}
}
+ fn auto_focus_state(&self) -> HomeAutoFocusState {
+ HomeAutoFocusState {
+ has_startup_signer_input: self.startup_signer_entry.is_some(),
+ startup_signer_input_is_editable: startup_signer_source_input_is_editable(
+ &self.startup_signer_connect_state,
+ ),
+ has_farm_setup_form: self.farm_setup_form.is_some(),
+ has_personal_search_input: self.personal_search.is_some(),
+ has_buyer_checkout_form: self.buyer_checkout_form.is_some(),
+ has_products_search_input: self.products_search.is_some(),
+ has_products_stock_editor: self.products_stock_editor.is_some(),
+ has_product_editor_form: self.product_editor_form.is_some(),
+ }
+ }
+
+ fn apply_auto_focus(
+ &mut self,
+ runtime: &DesktopAppRuntimeSummary,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let desired_target = home_auto_focus_target(runtime, self.auto_focus_state());
+ let focus_state = window.use_state(cx, |_, _| Option::<HomeAutoFocusTarget>::None);
+ let should_focus = {
+ let last_target = focus_state.read(cx);
+ last_target.as_ref().copied() != desired_target
+ };
+
+ if !should_focus {
+ return;
+ }
+
+ if let Some(target) = desired_target {
+ match target {
+ HomeAutoFocusTarget::StartupContinue => {
+ focus_button(window, "home-continue", cx);
+ }
+ HomeAutoFocusTarget::StartupGenerateKey => {
+ focus_button(window, "home-generate-key", cx);
+ }
+ HomeAutoFocusTarget::StartupSignerInput => {
+ if let Some(entry) = self.startup_signer_entry.as_ref() {
+ entry.input.update(cx, |input, cx| input.focus(window, cx));
+ }
+ }
+ HomeAutoFocusTarget::StartupSignerBack => {
+ focus_button(window, "home-signer-back", cx);
+ }
+ HomeAutoFocusTarget::BuyerSearchInput => {
+ if let Some(search) = self.personal_search.as_ref() {
+ search.input.update(cx, |input, cx| input.focus(window, cx));
+ }
+ }
+ HomeAutoFocusTarget::BuyerListingOpenFirst => {
+ focus_button(window, ("buyer-listing-open", 0_usize), cx);
+ }
+ HomeAutoFocusTarget::BuyerDetailBack => {
+ focus_button(window, "buyer-detail-back", cx);
+ }
+ HomeAutoFocusTarget::BuyerCartOpenCheckout => {
+ focus_button(window, "buyer-cart-open-checkout", cx);
+ }
+ HomeAutoFocusTarget::BuyerCheckoutNameInput => {
+ if let Some(form) = self.buyer_checkout_form.as_ref() {
+ form.name_input
+ .update(cx, |input, cx| input.focus(window, cx));
+ }
+ }
+ HomeAutoFocusTarget::BuyerOrderOpenFirst => {
+ focus_button(window, ("buyer-order-open", 0_usize), cx);
+ }
+ HomeAutoFocusTarget::BuyerOrderConfirmReplace => {
+ focus_button(window, "buyer-order-confirm-replace", cx);
+ }
+ HomeAutoFocusTarget::BuyerOrderRepeatDemand => {
+ focus_button(window, "buyer-order-repeat-demand", cx);
+ }
+ HomeAutoFocusTarget::FarmerReminderPrimary => {
+ focus_button(window, "reminder-banner-action", cx);
+ }
+ HomeAutoFocusTarget::FarmerReminderDismiss => {
+ focus_button(window, "reminder-banner-dismiss", cx);
+ }
+ HomeAutoFocusTarget::FarmerSetupStart => {
+ focus_button(window, "home-farm-setup-start", cx);
+ }
+ HomeAutoFocusTarget::FarmerSetupContinue => {
+ focus_button(window, "home-farm-setup-continue", cx);
+ }
+ HomeAutoFocusTarget::FarmerSetupFarmNameInput => {
+ if let Some(form) = self.farm_setup_form.as_ref() {
+ form.farm_name_input
+ .update(cx, |input, cx| input.focus(window, cx));
+ }
+ }
+ HomeAutoFocusTarget::FarmerTodayReminderChipFirst => {
+ focus_button(window, ("today-reminder-chip", 0_usize), cx);
+ }
+ HomeAutoFocusTarget::FarmerTodayOpenPackDay => {
+ focus_button(window, "home-today-open-pack-day", cx);
+ }
+ HomeAutoFocusTarget::FarmerTodayOpenOrders => {
+ focus_button(window, "home-today-open-orders", cx);
+ }
+ HomeAutoFocusTarget::FarmerTodayOpenProductsLowStock => {
+ focus_button(window, "home-today-open-products-low-stock", cx);
+ }
+ HomeAutoFocusTarget::FarmerTodayOpenProductsDrafts => {
+ focus_button(window, "home-today-open-products-drafts", cx);
+ }
+ HomeAutoFocusTarget::ProductsSearchInput => {
+ if let Some(search) = self.products_search.as_ref() {
+ search.input.update(cx, |input, cx| input.focus(window, cx));
+ }
+ }
+ HomeAutoFocusTarget::ProductsRowOpenFirst => {
+ focus_button(window, ("products-row-open", 0_usize), cx);
+ }
+ HomeAutoFocusTarget::ProductsStockInput => {
+ if let Some(editor) = self.products_stock_editor.as_ref() {
+ editor.input.update(cx, |input, cx| input.focus(window, cx));
+ }
+ }
+ HomeAutoFocusTarget::ProductEditorTitleInput => {
+ if let Some(form) = self.product_editor_form.as_ref() {
+ form.title_input
+ .update(cx, |input, cx| input.focus(window, cx));
+ }
+ }
+ HomeAutoFocusTarget::OrdersRowOpenFirst => {
+ focus_button(window, ("orders-row-open", 0_usize), cx);
+ }
+ HomeAutoFocusTarget::OrdersDetailMarkPacked => {
+ focus_button(window, "orders-detail-mark-packed", cx);
+ }
+ HomeAutoFocusTarget::OrdersDetailMarkCompleted => {
+ focus_button(window, "orders-detail-mark-completed", cx);
+ }
+ }
+ }
+
+ focus_state.update(cx, |last_target, _| *last_target = desired_target);
+ }
+
fn generate_local_account(&mut self, cx: &mut Context<Self>) -> bool {
if self.runtime.generate_local_account(None).unwrap_or(false) {
cx.refresh_windows();
@@ -3948,6 +4137,7 @@ impl Render for HomeView {
self.sync_products_search(&runtime_summary, window, cx);
self.sync_products_stock_editor(&runtime_summary);
self.sync_product_editor_form(&runtime_summary, window, cx);
+ self.apply_auto_focus(&runtime_summary, window, cx);
match home_stage(&runtime_summary) {
HomeStage::Setup => self
.startup_view
@@ -5381,6 +5571,15 @@ pub struct SettingsWindowView {
about_panel_notice: Option<String>,
}
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum SettingsAutoFocusTarget {
+ Navigation(SettingsPanelViewKey),
+ AccountAdd,
+ FarmNameInput,
+ SettingsAllowRelayConnections,
+ AboutRefresh,
+}
+
impl SettingsWindowView {
pub fn new(runtime: DesktopAppRuntime, initial_view: SettingsPanelViewKey) -> Self {
let _ = initial_view;
@@ -6585,6 +6784,50 @@ impl SettingsWindowView {
SettingsPanelViewKey::About => self.about_panel(cx).into_any_element(),
}
}
+
+ fn apply_auto_focus(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ let runtime = self.runtime.summary();
+ let desired_target = settings_auto_focus_target(
+ self.selected_view(),
+ self.farm_panel_state.as_ref(),
+ &runtime,
+ );
+ let focus_state = window.use_state(cx, |_, _| Option::<SettingsAutoFocusTarget>::None);
+ let should_focus = {
+ let last_target = focus_state.read(cx);
+ last_target.as_ref().copied() != desired_target
+ };
+
+ if !should_focus {
+ return;
+ }
+
+ if let Some(target) = desired_target {
+ match target {
+ SettingsAutoFocusTarget::Navigation(view) => {
+ let (navigation_id, _) = settings_panel_spec(view);
+ focus_button(window, navigation_id, cx);
+ }
+ SettingsAutoFocusTarget::AccountAdd => {
+ focus_button(window, "account-add", cx);
+ }
+ SettingsAutoFocusTarget::FarmNameInput => {
+ if let Some(form) = self.farm_panel_state.as_ref() {
+ form.farm_name_input
+ .update(cx, |input, cx| input.focus(window, cx));
+ }
+ }
+ SettingsAutoFocusTarget::SettingsAllowRelayConnections => {
+ focus_button(window, "settings-allow-relay-connections", cx);
+ }
+ SettingsAutoFocusTarget::AboutRefresh => {
+ focus_button(window, "settings-about-refresh-sync", cx);
+ }
+ }
+ }
+
+ focus_state.update(cx, |last_target, _| *last_target = desired_target);
+ }
}
fn about_status_rows(runtime: &DesktopAppRuntimeSummary) -> Vec<LabelValueRow> {
@@ -6854,6 +7097,234 @@ fn path_or_none(path: Option<&PathBuf>) -> String {
.unwrap_or_else(|| app_text(AppTextKey::ValueNone))
}
+fn focus_button<V>(window: &mut Window, id: impl Into<ElementId>, cx: &mut Context<V>) {
+ let focus_handle = window
+ .use_keyed_state(id, cx, |_, cx| cx.focus_handle())
+ .read(cx)
+ .clone();
+ focus_handle.focus(window);
+}
+
+fn home_auto_focus_target(
+ runtime: &DesktopAppRuntimeSummary,
+ state: HomeAutoFocusState,
+) -> Option<HomeAutoFocusTarget> {
+ match home_stage(runtime) {
+ HomeStage::Setup => startup_auto_focus_target(runtime, state),
+ HomeStage::BuyerWorkspace => buyer_auto_focus_target(runtime, state),
+ HomeStage::FarmerWorkspace => farmer_auto_focus_target(runtime, state),
+ }
+}
+
+fn startup_auto_focus_target(
+ runtime: &DesktopAppRuntimeSummary,
+ state: HomeAutoFocusState,
+) -> Option<HomeAutoFocusTarget> {
+ match startup_home_surface(runtime) {
+ StartupHomeSurface::ContinuePrompt => Some(HomeAutoFocusTarget::StartupContinue),
+ StartupHomeSurface::IdentityChoice => Some(HomeAutoFocusTarget::StartupGenerateKey),
+ StartupHomeSurface::GenerateKeyStarting | StartupHomeSurface::IssueCard => None,
+ StartupHomeSurface::SignerEntry => {
+ if state.has_startup_signer_input && state.startup_signer_input_is_editable {
+ Some(HomeAutoFocusTarget::StartupSignerInput)
+ } else {
+ Some(HomeAutoFocusTarget::StartupSignerBack)
+ }
+ }
+ }
+}
+
+fn buyer_auto_focus_target(
+ runtime: &DesktopAppRuntimeSummary,
+ state: HomeAutoFocusState,
+) -> Option<HomeAutoFocusTarget> {
+ match selected_personal_section(runtime) {
+ PersonalSection::Browse => {
+ if runtime.personal_projection.browse.detail.is_some() {
+ Some(HomeAutoFocusTarget::BuyerDetailBack)
+ } else if !runtime.personal_projection.browse.listings.rows.is_empty() {
+ Some(HomeAutoFocusTarget::BuyerListingOpenFirst)
+ } else {
+ None
+ }
+ }
+ PersonalSection::Search => {
+ if runtime.personal_projection.search.detail.is_some() {
+ Some(HomeAutoFocusTarget::BuyerDetailBack)
+ } else if state.has_personal_search_input {
+ Some(HomeAutoFocusTarget::BuyerSearchInput)
+ } else if !runtime.personal_projection.search.listings.rows.is_empty() {
+ Some(HomeAutoFocusTarget::BuyerListingOpenFirst)
+ } else {
+ None
+ }
+ }
+ PersonalSection::Cart => {
+ if state.has_buyer_checkout_form {
+ Some(HomeAutoFocusTarget::BuyerCheckoutNameInput)
+ } else if !runtime.personal_projection.cart.cart.lines.is_empty() {
+ Some(HomeAutoFocusTarget::BuyerCartOpenCheckout)
+ } else {
+ None
+ }
+ }
+ PersonalSection::Orders => {
+ if let Some(detail) = runtime.personal_projection.orders.detail.as_ref() {
+ let replace_confirmation = runtime
+ .personal_projection
+ .cart
+ .cart
+ .replace_confirmation
+ .as_ref()
+ .is_some_and(|confirmation| {
+ confirmation.incoming_farm_display_name == detail.farm_display_name
+ });
+ if replace_confirmation {
+ Some(HomeAutoFocusTarget::BuyerOrderConfirmReplace)
+ } else if detail.repeat_demand.as_ref().is_some_and(|repeat_demand| {
+ repeat_demand.eligibility != RepeatDemandEligibility::Unavailable
+ }) {
+ Some(HomeAutoFocusTarget::BuyerOrderRepeatDemand)
+ } else if !runtime.personal_projection.orders.list.rows.is_empty() {
+ Some(HomeAutoFocusTarget::BuyerOrderOpenFirst)
+ } else {
+ None
+ }
+ } else if !runtime.personal_projection.orders.list.rows.is_empty() {
+ Some(HomeAutoFocusTarget::BuyerOrderOpenFirst)
+ } else {
+ None
+ }
+ }
+ }
+}
+
+fn farmer_auto_focus_target(
+ runtime: &DesktopAppRuntimeSummary,
+ state: HomeAutoFocusState,
+) -> Option<HomeAutoFocusTarget> {
+ if let Some(reminder) = presented_farmer_reminder(runtime) {
+ if reminder.action_label.is_some() {
+ return Some(HomeAutoFocusTarget::FarmerReminderPrimary);
+ }
+ return Some(HomeAutoFocusTarget::FarmerReminderDismiss);
+ }
+
+ match selected_farmer_section(runtime) {
+ FarmerSection::Today | FarmerSection::Farm => today_auto_focus_target(runtime, state),
+ FarmerSection::Products if farmer_products_available(runtime) => {
+ if state.has_product_editor_form {
+ Some(HomeAutoFocusTarget::ProductEditorTitleInput)
+ } else if state.has_products_stock_editor {
+ Some(HomeAutoFocusTarget::ProductsStockInput)
+ } else if state.has_products_search_input {
+ Some(HomeAutoFocusTarget::ProductsSearchInput)
+ } else if !runtime.products_projection.list.rows.is_empty() {
+ Some(HomeAutoFocusTarget::ProductsRowOpenFirst)
+ } else {
+ None
+ }
+ }
+ FarmerSection::Orders if farmer_products_available(runtime) => {
+ if let Some(detail) = runtime.orders_projection.detail.as_ref() {
+ match detail.primary_action {
+ Some(OrderPrimaryAction::MarkPacked) => {
+ Some(HomeAutoFocusTarget::OrdersDetailMarkPacked)
+ }
+ Some(OrderPrimaryAction::MarkCompleted) => {
+ Some(HomeAutoFocusTarget::OrdersDetailMarkCompleted)
+ }
+ Some(OrderPrimaryAction::Review) | None
+ if !runtime.orders_projection.list.rows.is_empty() =>
+ {
+ Some(HomeAutoFocusTarget::OrdersRowOpenFirst)
+ }
+ Some(OrderPrimaryAction::Review) | None => None,
+ }
+ } else if !runtime.orders_projection.list.rows.is_empty() {
+ Some(HomeAutoFocusTarget::OrdersRowOpenFirst)
+ } else {
+ None
+ }
+ }
+ FarmerSection::PackDay if farmer_pack_day_available(runtime) => None,
+ FarmerSection::Products | FarmerSection::Orders | FarmerSection::PackDay => {
+ today_auto_focus_target(runtime, state)
+ }
+ }
+}
+
+fn today_auto_focus_target(
+ runtime: &DesktopAppRuntimeSummary,
+ state: HomeAutoFocusState,
+) -> Option<HomeAutoFocusTarget> {
+ let projection = &runtime.today_projection;
+
+ if state.has_farm_setup_form {
+ return Some(HomeAutoFocusTarget::FarmerSetupFarmNameInput);
+ }
+
+ if let Some(spec) = farm_setup_onboarding_card_spec(runtime.home_route) {
+ if spec.action_key.is_some() {
+ return Some(HomeAutoFocusTarget::FarmerSetupStart);
+ }
+ } else if projection.needs_setup()
+ && farmer_home_farm_state(runtime) == FarmerHomeFarmState::IncompleteFarm
+ {
+ return Some(HomeAutoFocusTarget::FarmerSetupContinue);
+ }
+
+ if projection
+ .reminders
+ .items
+ .iter()
+ .any(|reminder| reminder_action_target(reminder).is_some())
+ {
+ return Some(HomeAutoFocusTarget::FarmerTodayReminderChipFirst);
+ }
+ if projection.next_fulfillment_window.is_some() {
+ return Some(HomeAutoFocusTarget::FarmerTodayOpenPackDay);
+ }
+ if !projection.orders_needing_action.is_empty() {
+ return Some(HomeAutoFocusTarget::FarmerTodayOpenOrders);
+ }
+ if !projection.low_stock_products.is_empty() {
+ return Some(HomeAutoFocusTarget::FarmerTodayOpenProductsLowStock);
+ }
+ if !projection.draft_products.is_empty() {
+ return Some(HomeAutoFocusTarget::FarmerTodayOpenProductsDrafts);
+ }
+
+ None
+}
+
+fn settings_auto_focus_target(
+ selected_view: SettingsPanelViewKey,
+ farm_panel_state: Option<&SettingsFarmPanelState>,
+ runtime: &DesktopAppRuntimeSummary,
+) -> Option<SettingsAutoFocusTarget> {
+ match selected_view {
+ SettingsPanelViewKey::Account => Some(SettingsAutoFocusTarget::AccountAdd),
+ SettingsPanelViewKey::Farm => farm_panel_state
+ .map(|_| SettingsAutoFocusTarget::FarmNameInput)
+ .or(Some(SettingsAutoFocusTarget::Navigation(
+ SettingsPanelViewKey::Farm,
+ ))),
+ SettingsPanelViewKey::Settings => {
+ Some(SettingsAutoFocusTarget::SettingsAllowRelayConnections)
+ }
+ SettingsPanelViewKey::About => {
+ if about_manual_refresh_enabled(&runtime.sync_status) {
+ Some(SettingsAutoFocusTarget::AboutRefresh)
+ } else {
+ Some(SettingsAutoFocusTarget::Navigation(
+ SettingsPanelViewKey::About,
+ ))
+ }
+ }
+ }
+}
+
impl Render for SettingsWindowView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let navigation_buttons = SETTINGS_NAVIGATION_ORDER
@@ -6861,6 +7332,8 @@ impl Render for SettingsWindowView {
.copied()
.map(|view| self.navigation_button(view, cx).into_any_element())
.collect::<Vec<_>>();
+ let panel_content = self.settings_panel_content(window, cx);
+ self.apply_auto_focus(window, cx);
app_window_shell(
APP_UI_THEME.foundation.surfaces.panel_background,
@@ -6886,12 +7359,7 @@ impl Render for SettingsWindowView {
),
)
.child(section_divider())
- .child(
- div()
- .flex_1()
- .overflow_hidden()
- .child(self.settings_panel_content(window, cx)),
- ),
+ .child(div().flex_1().overflow_hidden().child(panel_content)),
)
}
}
@@ -11123,19 +11591,20 @@ fn home_farm_order_method_label_key(method: FarmOrderMethod) -> AppTextKey {
#[cfg(test)]
mod tests {
use super::{
- APP_UI_THEME, AppTextKey, FarmerHomeFarmState, HomeStage, ReminderActionTarget,
- SETTINGS_FARM_PANEL_SECTIONS, SETTINGS_NAVIGATION_ORDER,
- SETTINGS_OPERATIONS_PANEL_SECTIONS, SettingsInventorySectionSpec, SettingsPanelViewKey,
- StartupHomeSurface, StartupSignerConnectState, about_conflict_action_specs,
- about_conflict_aggregate_text, about_conflict_detail_rows, about_conflict_review_body_key,
- about_manual_refresh_enabled, about_runtime_rows, about_status_rows, app_text,
- buyer_orders_status_key, farm_setup_onboarding_card_spec, farmer_home_farm_state,
- farmer_pack_day_available, home_content_scroll_id, home_saved_farm,
- home_sidebar_navigation_sections, home_stage, home_window_launch_size_px,
- home_window_minimum_size_px, parse_optional_product_editor_stock_input,
- parse_product_editor_price_input, presented_farmer_reminder, product_display_title,
- reminder_action_target, reminder_deadline_text, reminder_delivery_state_key,
- reminder_urgency_color, reminder_urgency_key, startup_home_surface,
+ APP_UI_THEME, AppTextKey, FarmerHomeFarmState, HomeAutoFocusState, HomeAutoFocusTarget,
+ HomeStage, ReminderActionTarget, SETTINGS_FARM_PANEL_SECTIONS, SETTINGS_NAVIGATION_ORDER,
+ SETTINGS_OPERATIONS_PANEL_SECTIONS, SettingsAutoFocusTarget, SettingsInventorySectionSpec,
+ SettingsPanelViewKey, StartupHomeSurface, StartupSignerConnectState,
+ about_conflict_action_specs, about_conflict_aggregate_text, about_conflict_detail_rows,
+ about_conflict_review_body_key, about_manual_refresh_enabled, about_runtime_rows,
+ about_status_rows, app_text, buyer_orders_status_key, farm_setup_onboarding_card_spec,
+ farmer_home_farm_state, farmer_pack_day_available, home_auto_focus_target,
+ home_content_scroll_id, home_saved_farm, home_sidebar_navigation_sections, home_stage,
+ home_window_launch_size_px, home_window_minimum_size_px,
+ parse_optional_product_editor_stock_input, parse_product_editor_price_input,
+ presented_farmer_reminder, product_display_title, reminder_action_target,
+ reminder_deadline_text, reminder_delivery_state_key, reminder_urgency_color,
+ reminder_urgency_key, settings_auto_focus_target, startup_home_surface,
startup_signer_preview_summary, startup_signer_preview_summary_for_connect_state,
startup_signer_source_input_is_editable, startup_signer_status_spec,
startup_signer_transport_failure_requires_notice,
@@ -11146,12 +11615,15 @@ mod tests {
};
use radroots_app_models::SettingsAccountProjection;
use radroots_app_models::{
- ActiveSurface, AppStartupGate, BuyerOrderStatus, FarmId, FarmOrderMethod, FarmReadiness,
- FarmSetupDraft, FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId,
+ ActiveSurface, AppStartupGate, BuyerOrderDetailProjection, BuyerOrderStatus,
+ BuyerOrdersListRow, FarmId, FarmOrderMethod, FarmReadiness, FarmSetupDraft,
+ FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId,
FulfillmentWindowSummary, LoggedOutStartupPhase, LoggedOutStartupProjection,
+ OrderDetailProjection, OrderId, OrderPrimaryAction, OrderStatus, OrdersListRow,
PackDayProjection, PersonalSection, ReminderDeadlineProjection, ReminderDeliveryState,
- ReminderId, ReminderKind, ReminderSurface, ReminderUrgency, ShellSection,
- TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind,
+ ReminderId, ReminderKind, ReminderSurface, ReminderUrgency, RepeatDemandEligibility,
+ RepeatDemandHandoffProjection, ShellSection, TodayAgendaProjection, TodaySetupTask,
+ TodaySetupTaskKind,
};
use radroots_app_remote_signer::{
RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingSession,
@@ -11344,6 +11816,279 @@ mod tests {
}
#[test]
+ fn home_auto_focus_target_tracks_startup_surface_contract() {
+ assert_eq!(
+ home_auto_focus_target(
+ &summary_with_logged_out_phase(LoggedOutStartupPhase::ContinuePrompt),
+ HomeAutoFocusState::default(),
+ ),
+ Some(HomeAutoFocusTarget::StartupContinue)
+ );
+ assert_eq!(
+ home_auto_focus_target(
+ &summary_with_logged_out_phase(LoggedOutStartupPhase::IdentityChoice),
+ HomeAutoFocusState::default(),
+ ),
+ Some(HomeAutoFocusTarget::StartupGenerateKey)
+ );
+ assert_eq!(
+ home_auto_focus_target(
+ &summary_with_logged_out_phase(LoggedOutStartupPhase::SignerEntry),
+ HomeAutoFocusState {
+ has_startup_signer_input: true,
+ startup_signer_input_is_editable: true,
+ ..HomeAutoFocusState::default()
+ },
+ ),
+ Some(HomeAutoFocusTarget::StartupSignerInput)
+ );
+ assert_eq!(
+ home_auto_focus_target(
+ &summary_with_logged_out_phase(LoggedOutStartupPhase::SignerEntry),
+ HomeAutoFocusState {
+ has_startup_signer_input: true,
+ startup_signer_input_is_editable: false,
+ ..HomeAutoFocusState::default()
+ },
+ ),
+ Some(HomeAutoFocusTarget::StartupSignerBack)
+ );
+ }
+
+ #[test]
+ fn home_auto_focus_target_tracks_buyer_surface_contract() {
+ let mut buyer_search = summary(
+ HomeRoute::Personal,
+ TodayAgendaProjection::default(),
+ FarmSetupProjection::default(),
+ );
+ buyer_search.startup_gate = AppStartupGate::Personal;
+ buyer_search.shell_projection = AppShellProjection::new(
+ ActiveSurface::Personal,
+ ShellSection::Personal(PersonalSection::Search),
+ );
+ assert_eq!(
+ home_auto_focus_target(
+ &buyer_search,
+ HomeAutoFocusState {
+ has_personal_search_input: true,
+ ..HomeAutoFocusState::default()
+ },
+ ),
+ Some(HomeAutoFocusTarget::BuyerSearchInput)
+ );
+
+ let mut buyer_cart_checkout = buyer_search.clone();
+ buyer_cart_checkout.shell_projection = AppShellProjection::new(
+ ActiveSurface::Personal,
+ ShellSection::Personal(PersonalSection::Cart),
+ );
+ assert_eq!(
+ home_auto_focus_target(
+ &buyer_cart_checkout,
+ HomeAutoFocusState {
+ has_buyer_checkout_form: true,
+ ..HomeAutoFocusState::default()
+ },
+ ),
+ Some(HomeAutoFocusTarget::BuyerCheckoutNameInput)
+ );
+
+ let order_id = OrderId::new();
+ let farm_id = FarmId::new();
+ let mut buyer_orders = buyer_search.clone();
+ buyer_orders.shell_projection = AppShellProjection::new(
+ ActiveSurface::Personal,
+ ShellSection::Personal(PersonalSection::Orders),
+ );
+ buyer_orders.personal_projection.orders.list.rows = vec![BuyerOrdersListRow {
+ order_id,
+ farm_id,
+ order_number: String::new(),
+ farm_display_name: String::new(),
+ fulfillment_summary: String::new(),
+ status: BuyerOrderStatus::Placed,
+ repeat_demand: None,
+ }];
+ buyer_orders.personal_projection.orders.detail = Some(BuyerOrderDetailProjection {
+ order_id,
+ farm_id,
+ order_number: String::new(),
+ farm_display_name: String::new(),
+ fulfillment_summary: String::new(),
+ status: BuyerOrderStatus::Placed,
+ items: Vec::new(),
+ order_note: None,
+ repeat_demand: Some(RepeatDemandHandoffProjection {
+ order_id,
+ farm_id,
+ eligibility: RepeatDemandEligibility::Eligible,
+ available_item_count: 1,
+ unavailable_item_count: 0,
+ }),
+ });
+ assert_eq!(
+ home_auto_focus_target(&buyer_orders, HomeAutoFocusState::default()),
+ Some(HomeAutoFocusTarget::BuyerOrderRepeatDemand)
+ );
+ }
+
+ #[test]
+ fn home_auto_focus_target_tracks_farmer_surface_contract() {
+ let mut onboarding = summary(
+ HomeRoute::FarmSetupOnboarding,
+ TodayAgendaProjection::default(),
+ FarmSetupProjection::default(),
+ );
+ onboarding.startup_gate = AppStartupGate::Farmer;
+ onboarding.shell_projection = AppShellProjection::new(
+ ActiveSurface::Farmer,
+ ShellSection::Farmer(FarmerSection::Today),
+ );
+ assert_eq!(
+ home_auto_focus_target(&onboarding, HomeAutoFocusState::default()),
+ Some(HomeAutoFocusTarget::FarmerSetupStart)
+ );
+
+ let farm_id = FarmId::new();
+ let incomplete_farm = FarmSummary {
+ farm_id,
+ display_name: String::new(),
+ readiness: FarmReadiness::Incomplete,
+ };
+ let incomplete_today = summary(
+ HomeRoute::Today,
+ TodayAgendaProjection {
+ farm: Some(incomplete_farm.clone()),
+ setup_checklist: vec![TodaySetupTask {
+ kind: TodaySetupTaskKind::AddFulfillmentWindow,
+ is_complete: false,
+ }],
+ ..TodayAgendaProjection::default()
+ },
+ FarmSetupProjection::new(
+ FarmSetupDraft::new(String::new(), String::new(), [FarmOrderMethod::Pickup]),
+ Some(incomplete_farm),
+ ),
+ );
+ assert_eq!(
+ home_auto_focus_target(&incomplete_today, HomeAutoFocusState::default()),
+ Some(HomeAutoFocusTarget::FarmerSetupContinue)
+ );
+
+ let saved_farm = FarmSummary {
+ farm_id: FarmId::new(),
+ display_name: String::new(),
+ readiness: FarmReadiness::Ready,
+ };
+ let mut products = summary(
+ HomeRoute::Today,
+ TodayAgendaProjection::default(),
+ FarmSetupProjection::from_saved_farm(saved_farm.clone()),
+ );
+ products.startup_gate = AppStartupGate::Farmer;
+ products.shell_projection = AppShellProjection::new(
+ ActiveSurface::Farmer,
+ ShellSection::Farmer(FarmerSection::Products),
+ );
+ assert_eq!(
+ home_auto_focus_target(
+ &products,
+ HomeAutoFocusState {
+ has_products_search_input: true,
+ ..HomeAutoFocusState::default()
+ },
+ ),
+ Some(HomeAutoFocusTarget::ProductsSearchInput)
+ );
+ assert_eq!(
+ home_auto_focus_target(
+ &products,
+ HomeAutoFocusState {
+ has_product_editor_form: true,
+ ..HomeAutoFocusState::default()
+ },
+ ),
+ Some(HomeAutoFocusTarget::ProductEditorTitleInput)
+ );
+
+ let mut orders = summary(
+ HomeRoute::Today,
+ TodayAgendaProjection::default(),
+ FarmSetupProjection::from_saved_farm(saved_farm),
+ );
+ orders.startup_gate = AppStartupGate::Farmer;
+ orders.shell_projection = AppShellProjection::new(
+ ActiveSurface::Farmer,
+ ShellSection::Farmer(FarmerSection::Orders),
+ );
+ orders.orders_projection.list.rows = vec![OrdersListRow {
+ order_id: OrderId::new(),
+ farm_id: FarmId::new(),
+ fulfillment_window_id: None,
+ order_number: String::new(),
+ customer_display_name: String::new(),
+ fulfillment_window_label: None,
+ pickup_location_label: None,
+ status: OrderStatus::Scheduled,
+ primary_action: Some(OrderPrimaryAction::MarkPacked),
+ }];
+ orders.orders_projection.detail = Some(OrderDetailProjection {
+ order_id: OrderId::new(),
+ farm_id: FarmId::new(),
+ order_number: String::new(),
+ customer_display_name: String::new(),
+ status: OrderStatus::Scheduled,
+ fulfillment_window_id: None,
+ fulfillment_window_label: None,
+ pickup_location_label: None,
+ items: Vec::new(),
+ primary_action: Some(OrderPrimaryAction::MarkPacked),
+ recoveries: Vec::new(),
+ });
+ assert_eq!(
+ home_auto_focus_target(&orders, HomeAutoFocusState::default()),
+ Some(HomeAutoFocusTarget::OrdersDetailMarkPacked)
+ );
+ }
+
+ #[test]
+ fn settings_auto_focus_target_tracks_panel_contract() {
+ let runtime = summary(
+ HomeRoute::Today,
+ TodayAgendaProjection::default(),
+ FarmSetupProjection::default(),
+ );
+ assert_eq!(
+ settings_auto_focus_target(SettingsPanelViewKey::Account, None, &runtime),
+ Some(SettingsAutoFocusTarget::AccountAdd)
+ );
+ assert_eq!(
+ settings_auto_focus_target(SettingsPanelViewKey::Farm, None, &runtime),
+ Some(SettingsAutoFocusTarget::Navigation(
+ SettingsPanelViewKey::Farm
+ ))
+ );
+ assert_eq!(
+ settings_auto_focus_target(SettingsPanelViewKey::Settings, None, &runtime),
+ Some(SettingsAutoFocusTarget::SettingsAllowRelayConnections)
+ );
+
+ let mut about_enabled = runtime.clone();
+ about_enabled.sync_status.account_id = Some("guest".to_owned());
+ assert_eq!(
+ settings_auto_focus_target(SettingsPanelViewKey::About, None, &about_enabled),
+ Some(SettingsAutoFocusTarget::AboutRefresh)
+ );
+ assert_eq!(
+ settings_auto_focus_target(SettingsPanelViewKey::About, None, &runtime),
+ Some(SettingsAutoFocusTarget::Navigation(
+ SettingsPanelViewKey::About
+ ))
+ );
+ }
+
+ #[test]
fn buyer_orders_status_keys_use_buyer_facing_copy() {
assert_eq!(
buyer_orders_status_key(BuyerOrderStatus::Placed),