Skip to content

Commit de73392

Browse files
feat(accessibility): add screen reader support for country select box
1 parent 2f80db3 commit de73392

1 file changed

Lines changed: 168 additions & 0 deletions

File tree

Telegram/SourceFiles/ui/boxes/country_select_box.cpp

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,22 @@ For license and copyright information please follow this link:
88
#include "ui/boxes/country_select_box.h"
99

1010
#include "lang/lang_keys.h"
11+
#include "base/screen_reader_state.h"
12+
#include "ui/accessible/ui_accessible_item.h"
1113
#include "ui/widgets/scroll_area.h"
1214
#include "ui/widgets/multi_select.h"
1315
#include "ui/effects/ripple_animation.h"
1416
#include "ui/painter.h"
17+
#include "base/invoke_queued.h"
1518
#include "countries/countries_instance.h"
1619
#include "styles/style_layers.h"
1720
#include "styles/style_boxes.h"
1821
#include "styles/style_intro.h"
1922

23+
#include <unordered_map>
24+
2025
#include <QtCore/QRegularExpression>
26+
#include <QtWidgets/QApplication>
2127

2228
namespace Ui {
2329
namespace {
@@ -48,8 +54,89 @@ class CountrySelectBox::Inner : public RpWidget {
4854
return _mustScrollTo.events();
4955
}
5056

57+
QAccessible::Role accessibilityRole() override {
58+
return QAccessible::List;
59+
}
60+
61+
Ui::AccessibilityState accessibilityState() const override {
62+
return { .readOnly = true, .focusable = 0 };
63+
}
64+
65+
QAccessible::Role accessibilityChildRole() const override {
66+
return QAccessible::ListItem;
67+
}
68+
69+
QAccessible::State accessibilityChildState(int index) const override {
70+
QAccessible::State state;
71+
state.selectable = true;
72+
state.focusable = true;
73+
if (index == _selected) {
74+
state.selected = true;
75+
state.active = true;
76+
if (hasFocus()) {
77+
state.focused = true;
78+
}
79+
}
80+
return state;
81+
}
82+
83+
int accessibilityChildCount() const override {
84+
return int(current().size());
85+
}
86+
87+
QString accessibilityChildName(int index) const override {
88+
const auto &list = current();
89+
if (index < 0 || index >= int(list.size())) {
90+
return {};
91+
}
92+
if (_type == Type::Phones) {
93+
return list[index].country + u", +"_q + list[index].code;
94+
}
95+
return list[index].country;
96+
}
97+
98+
QRect accessibilityChildRect(int index) const override {
99+
const auto &list = current();
100+
if (index < 0 || index >= int(list.size())) {
101+
return QRect();
102+
}
103+
return QRect(0, st::countriesSkip + index * _rowHeight, width(), _rowHeight);
104+
}
105+
106+
int accessibilityChildColumnCount(int row) const override {
107+
return (_type == Type::Phones) ? 2 : 1;
108+
}
109+
110+
QAccessible::Role accessibilityChildSubItemRole() const override {
111+
return QAccessible::Cell;
112+
}
113+
114+
QString accessibilityChildSubItemName(int row, int column) const override {
115+
if (column == 0) {
116+
return tr::lng_country_column_name(tr::now);
117+
} else if (column == 1 && _type == Type::Phones) {
118+
return tr::lng_country_code(tr::now);
119+
}
120+
return {};
121+
}
122+
123+
QString accessibilityChildSubItemValue(int row, int column) const override {
124+
const auto &list = current();
125+
if (row < 0 || row >= int(list.size())) {
126+
return {};
127+
}
128+
if (column == 0) {
129+
return list[row].country;
130+
} else if (column == 1 && _type == Type::Phones) {
131+
return u"+"_q + list[row].code;
132+
}
133+
return {};
134+
}
135+
51136
protected:
137+
void focusInEvent(QFocusEvent *e) override;
52138
void paintEvent(QPaintEvent *e) override;
139+
void keyPressEvent(QKeyEvent *e) override;
53140
void enterEventHook(QEnterEvent *e) override;
54141
void leaveEventHook(QEvent *e) override;
55142
void mouseMoveEvent(QMouseEvent *e) override;
@@ -203,6 +290,13 @@ CountrySelectBox::Inner::Inner(
203290
_filter = u"a"_q;
204291
updateFilter(filter);
205292
}, lifetime());
293+
294+
setAccessibleName(tr::lng_country_select(tr::now));
295+
296+
base::ScreenReaderState::Instance()->activeValue(
297+
) | rpl::on_next([=](bool active) {
298+
setFocusPolicy(active ? Qt::TabFocus : Qt::NoFocus);
299+
}, lifetime());
206300
}
207301

208302
void CountrySelectBox::Inner::init() {
@@ -262,6 +356,28 @@ void CountrySelectBox::Inner::init() {
262356
}
263357
}
264358

359+
void CountrySelectBox::Inner::focusInEvent(QFocusEvent *e) {
360+
// Select first item when focus enters.
361+
const auto &list = current();
362+
if (_selected < 0 && !list.empty()) {
363+
_selected = 0;
364+
updateSelectedRow();
365+
}
366+
367+
RpWidget::focusInEvent(e);
368+
369+
if (_selected >= 0 && base::ScreenReaderState::Instance()->active()) {
370+
const auto index = _selected;
371+
InvokeQueued(this, [=] {
372+
if (_selected != index || !hasFocus()) {
373+
return;
374+
}
375+
accessibilityChildStateChanged(index, { .focused = true });
376+
accessibilityChildFocused(index);
377+
});
378+
}
379+
}
380+
265381
void CountrySelectBox::Inner::paintEvent(QPaintEvent *e) {
266382
Painter p(this);
267383
QRect r(e->rect());
@@ -316,6 +432,50 @@ void CountrySelectBox::Inner::paintEvent(QPaintEvent *e) {
316432
}
317433
}
318434

435+
void CountrySelectBox::Inner::keyPressEvent(QKeyEvent *e) {
436+
if (e->key() == Qt::Key_Down) {
437+
selectSkip(1);
438+
} else if (e->key() == Qt::Key_Up) {
439+
selectSkip(-1);
440+
} else if (e->key() == Qt::Key_PageDown || e->key() == Qt::Key_PageUp) {
441+
const auto visibleHeight = visibleRegion().boundingRect().height();
442+
const auto rowsPerPage = std::max(visibleHeight / _rowHeight, 1);
443+
selectSkip(e->key() == Qt::Key_PageDown ? rowsPerPage : -rowsPerPage);
444+
} else if (e->key() == Qt::Key_Home) {
445+
const auto &list = current();
446+
if (!list.empty()) {
447+
_selected = 0;
448+
_mustScrollTo.fire(ScrollToRequest(
449+
st::countriesSkip,
450+
st::countriesSkip + _rowHeight));
451+
update();
452+
if (base::ScreenReaderState::Instance()->active()) {
453+
accessibilityChildNameChanged(_selected);
454+
accessibilityChildFocused(_selected);
455+
}
456+
}
457+
} else if (e->key() == Qt::Key_End) {
458+
const auto &list = current();
459+
if (!list.empty()) {
460+
_selected = int(list.size()) - 1;
461+
_mustScrollTo.fire(ScrollToRequest(
462+
st::countriesSkip + _selected * _rowHeight,
463+
st::countriesSkip + (_selected + 1) * _rowHeight));
464+
update();
465+
if (base::ScreenReaderState::Instance()->active()) {
466+
accessibilityChildNameChanged(_selected);
467+
accessibilityChildFocused(_selected);
468+
}
469+
}
470+
} else if (!e->isAutoRepeat()
471+
&& (e->key() == Qt::Key_Return
472+
|| e->key() == Qt::Key_Enter)) {
473+
chooseCountry();
474+
} else {
475+
RpWidget::keyPressEvent(e);
476+
}
477+
}
478+
319479
void CountrySelectBox::Inner::enterEventHook(QEnterEvent *e) {
320480
setMouseTracking(true);
321481
}
@@ -426,6 +586,10 @@ void CountrySelectBox::Inner::selectSkip(int32 dir) {
426586
st::countriesSkip + (_selected + 1) * _rowHeight));
427587
}
428588
update();
589+
if (_selected >= 0 && base::ScreenReaderState::Instance()->active()) {
590+
accessibilityChildNameChanged(_selected);
591+
accessibilityChildFocused(_selected);
592+
}
429593
}
430594

431595
void CountrySelectBox::Inner::selectSkipPage(int32 h, int32 dir) {
@@ -457,6 +621,10 @@ void CountrySelectBox::Inner::updateSelected(QPoint localPos) {
457621
updateSelectedRow();
458622
_selected = selected;
459623
updateSelectedRow();
624+
if (_selected >= 0 && base::ScreenReaderState::Instance()->active()) {
625+
accessibilityChildNameChanged(_selected);
626+
accessibilityChildFocused(_selected);
627+
}
460628
}
461629
}
462630

0 commit comments

Comments
 (0)