From cf9770b57e71db4e3df0f2165f32aadd59a8e554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 2 Oct 2026 18:52:08 -0400 Subject: [PATCH] =?UTF-8?q?Dialogs,=20a11y=20=C2=B7=20Dialog=20card=20name?= =?UTF-8?q?s=20its=20role,=20title=20and=20detail;=20prompt=20error=20read?= =?UTF-8?q?s=20as=20a=20label;=20Cancel=20and=20Close=20read=20i18n;=20a?= =?UTF-8?q?=20disabled=20button=20says=20so=20in=20its=20description?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/buttons/button.rs | 6 +++++- src/buttons/icon_button.rs | 6 +++++- src/i18n/builtin.rs | 6 ++++++ src/navigation/editor_tabs.rs | 3 ++- src/overlays/dialog.rs | 15 +++++++++++---- src/overlays/dialogs.rs | 14 ++++++++++---- 6 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/buttons/button.rs b/src/buttons/button.rs index 1cbab59..3de04cf 100644 --- a/src/buttons/button.rs +++ b/src/buttons/button.rs @@ -5,6 +5,7 @@ use gpui::{ }; use crate::{ + i18n, motion::Spinner, primitives::{FocusRing, Icon, IconName}, theme::{ActiveTheme, ControlSize, IconSize, Mix, Palette, Platform, Radius, TextSize}, @@ -291,7 +292,10 @@ impl RenderOnce for Button { }) .map(|el| { if self.disabled { - return el.opacity(0.45).cursor_not_allowed(); + return el + .aria_description(i18n::text(cx, "state.unavailable", &[])) + .opacity(0.45) + .cursor_not_allowed(); } let el = match &self.focus { Some(handle) => el.track_focus(handle), diff --git a/src/buttons/icon_button.rs b/src/buttons/icon_button.rs index ff73720..ca183b7 100644 --- a/src/buttons/icon_button.rs +++ b/src/buttons/icon_button.rs @@ -5,6 +5,7 @@ use gpui::{ use super::button::{ButtonVariant, ClickHandler, label_size, tone}; use crate::{ + i18n, primitives::{FocusRing, Icon, IconName, Tooltip}, theme::{ActiveTheme, ControlSize, Radius}, }; @@ -92,7 +93,10 @@ impl RenderOnce for IconButton { .when_some(self.tooltip, |el, text| el.tooltip(Tooltip::text(text))) .map(|el| { if self.disabled { - return el.opacity(0.45).cursor_not_allowed(); + return el + .aria_description(i18n::text(cx, "state.unavailable", &[])) + .opacity(0.45) + .cursor_not_allowed(); } el.cursor_pointer() .tab_index(0) diff --git a/src/i18n/builtin.rs b/src/i18n/builtin.rs index e3fa070..cf323c0 100644 --- a/src/i18n/builtin.rs +++ b/src/i18n/builtin.rs @@ -32,6 +32,9 @@ const EN: &[(&str, &str)] = &[ ("palette.branch.delete", "Delete"), ("palette.history.placeholder", "Search past commands"), ("palette.history.empty", "No past command fits"), + ("dialog.cancel", "Cancel"), + ("dialog.close", "Close"), + ("state.unavailable", "Unavailable"), ]; const ZH_CN: &[(&str, &str)] = &[ @@ -62,6 +65,9 @@ const ZH_CN: &[(&str, &str)] = &[ ("palette.branch.delete", "删除"), ("palette.history.placeholder", "搜索历史命令"), ("palette.history.empty", "没有匹配的历史命令"), + ("dialog.cancel", "取消"), + ("dialog.close", "关闭"), + ("state.unavailable", "不可用"), ]; /// Ely's own message for `key` in `locale`, English for a locale Ely does not ship; fails on an unknown key. diff --git a/src/navigation/editor_tabs.rs b/src/navigation/editor_tabs.rs index 7154220..6b4d155 100644 --- a/src/navigation/editor_tabs.rs +++ b/src/navigation/editor_tabs.rs @@ -10,6 +10,7 @@ use super::TabOverflowMenu; use crate::{ buttons::{ButtonVariant, IconButton}, forms::Choice, + i18n, primitives::{DragGhost, Icon, IconName}, theme::{ActiveTheme, ControlSize, IconSize, TextSize}, }; @@ -206,7 +207,7 @@ impl RenderOnce for EditorTabs { IconButton::new(("close", ix), IconName::X) .size(ControlSize::Sm) .variant(ButtonVariant::Ghost) - .tooltip("Close") + .tooltip(i18n::text(cx, "dialog.close", &[])) .on_click(move |_, window, cx| { cx.stop_propagation(); run(&close, "close", &slot_id, window, cx); diff --git a/src/overlays/dialog.rs b/src/overlays/dialog.rs index c0b99c2..7463ba2 100644 --- a/src/overlays/dialog.rs +++ b/src/overlays/dialog.rs @@ -2,15 +2,15 @@ use std::rc::Rc; use gpui::{ Animation, AnimationExt, AnyElement, App, ElementId, FocusHandle, FontWeight, - InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce, SharedString, Styled, - Window, anchored, div, point, prelude::*, + InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce, Role, SharedString, + StatefulInteractiveElement, Styled, Window, anchored, div, point, prelude::*, }; use smallvec::SmallVec; use crate::{ buttons::{ButtonVariant, IconButton}, forms::{Enter, Run}, - motion, + i18n, motion, primitives::{Backdrop, FocusScope, IconName, give_back, raise, take_focus}, theme::{ActiveTheme, Elevation, Radius, TextSize}, }; @@ -119,6 +119,7 @@ impl RenderOnce for Dialog { }; let theme = cx.theme(); let colors = &theme.colors; + let (title, description) = (self.title.clone(), self.detail.clone()); let heading = div() .flex_none() .flex() @@ -164,6 +165,9 @@ impl RenderOnce for Dialog { let card = if fullscreen { div() .id(self.id.clone()) + .role(Role::Dialog) + .aria_label(title) + .when_some(description, |card, text| card.aria_description(text)) .flex() .flex_col() .size_full() @@ -182,7 +186,7 @@ impl RenderOnce for Dialog { .child( IconButton::new((self.id.clone(), "close"), IconName::X) .variant(ButtonVariant::Ghost) - .tooltip("Close") + .tooltip(i18n::text(cx, "dialog.close", &[])) .on_click(move |_, window, cx| button(window, cx)), ), ) @@ -214,6 +218,9 @@ impl RenderOnce for Dialog { }); div() .id(self.id.clone()) + .role(Role::Dialog) + .aria_label(title) + .when_some(description, |card, text| card.aria_description(text)) .flex() .flex_col() .gap_5() diff --git a/src/overlays/dialogs.rs b/src/overlays/dialogs.rs index 02a1d6c..b412fbe 100644 --- a/src/overlays/dialogs.rs +++ b/src/overlays/dialogs.rs @@ -1,14 +1,15 @@ use std::rc::Rc; use gpui::{ - App, ElementId, Entity, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, - div, prelude::*, + App, ElementId, Entity, IntoElement, ParentElement, RenderOnce, Role, SharedString, + StatefulInteractiveElement, Styled, Window, div, prelude::*, }; use super::{Close, Dialog}; use crate::{ buttons::{Button, ButtonVariant}, forms::{Input, Run, TextInput}, + i18n, primitives::{Icon, Severity}, theme::{ActiveTheme, ControlSize, IconSize, TextSize}, }; @@ -156,10 +157,11 @@ impl RenderOnce for ConfirmDialog { }; let (cancel_id, confirm_id) = ((self.id.clone(), "cancel"), (self.id.clone(), "confirm")); let on_close = self.on_close; + let cancel = i18n::text(cx, "dialog.cancel", &[]); Dialog::new(self.id, self.title, move |window, cx| on_close(window, cx)) .child(said(None, self.message, cx)) .action(move |close| { - Button::new(cancel_id, "Cancel") + Button::new(cancel_id, cancel) .variant(ButtonVariant::Ghost) .on_click(move |_, window, cx| close(window, cx)) }) @@ -295,6 +297,9 @@ impl RenderOnce for PromptDialog { |field, reason| { field.child( div() + .id((self.id.clone(), "error")) + .role(Role::Label) + .aria_label(reason.clone()) .text_size(theme.text_size(TextSize::Xs)) .text_color(colors.danger) .child(reason), @@ -306,13 +311,14 @@ impl RenderOnce for PromptDialog { (self.id.clone(), "submit"), self.submit, ); + let cancel = i18n::text(cx, "dialog.cancel", &[]); let on_close = self.on_close; Dialog::new(self.id, self.title, move |window, cx| on_close(window, cx)) .focus_first(self.field.read(cx).focus().clone()) .on_enter(move |close| Rc::new(move |window, cx| lifted(&close, window, cx))) .child(field) .action(move |close| { - Button::new(cancel_id, "Cancel") + Button::new(cancel_id, cancel) .variant(ButtonVariant::Ghost) .on_click(move |_, window, cx| close(window, cx)) })