diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index d5e55de04cc41f..c925985bd9b95e 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -730,6 +730,11 @@ Widget::Widget( _search->customUpDown(true); + // The bars above the list and the folder tabs are created only once they + // are needed, long after the list and the buttons below it, so the Tab + // chain has to follow the column instead of the creation order. + setVisualTabOrder(true); + updateJumpToDateVisibility(true); updateSearchFromVisibility(true); setupSupportMode(); diff --git a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp index b0d70d77f762da..7912bb5099fe77 100644 --- a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp +++ b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp @@ -7,7 +7,9 @@ For license and copyright information please follow this link: */ #include "ui/widgets/chat_filters_tabs_slider.h" +#include "lang/lang_keys.h" #include "ui/effects/ripple_animation.h" +#include "ui/widgets/popup_menu.h" #include "ui/widgets/side_bar_button.h" #include "styles/style_dialogs.h" #include "styles/style_widgets.h" @@ -169,15 +171,18 @@ void ChatsFiltersTabs::setUnreadCount(int index, int unreadCount, bool mute) { .muted = mute, }); update(); + accessibilityChildNameChanged(index); } } else if (!unreadCount) { _unreadCounts.erase(it); update(); + accessibilityChildNameChanged(index); } else if (it->second.count != unreadCount || it->second.muted != mute) { it->second.count = unreadCount; it->second.muted = mute; it->second.cache = cacheUnreadCount(unreadCount, mute); update(); + accessibilityChildNameChanged(index); } updateSectionsContentWidths(); } @@ -420,6 +425,24 @@ void ChatsFiltersTabs::mouseReleaseEvent(QMouseEvent *e) { } void ChatsFiltersTabs::contextMenuEvent(QContextMenuEvent *e) { + if (e->reason() == QContextMenuEvent::Keyboard) { + // A menu asked for from the keyboard comes with the center of an + // empty input method rect instead of a position over a tab, so the + // lookup below would land outside of them all - take the tab the + // browse position is on, or the active one when it is on none. + const auto browsed = accessibilityBrowsedSection(); + const auto index = (browsed >= 0) ? browsed : activeSection(); + if (!_lockedFrom || index < _lockedFrom) { + _contextMenuRequested.fire({ + .index = index, + .position = ContextMenuPosition( + this, + e, + accessibilityChildRect(index)), + }); + } + return; + } const auto pos = e->pos(); if (pos.x() >= _lockedFromX) { return; @@ -435,10 +458,49 @@ void ChatsFiltersTabs::contextMenuEvent(QContextMenuEvent *e) { index++; return true; }); - _contextMenuRequested.fire_copy(index); + _contextMenuRequested.fire({ + .index = index, + .position = QCursor::pos(), + }); +} + +QString ChatsFiltersTabs::accessibilityChildName(int index) const { + const auto title = Ui::SettingsSlider::accessibilityChildName(index); + if (_lockedFrom > 0 && index >= _lockedFrom) { + // Surface a locked folder's premium-gated status, which the visual + // lock glyph alone can't convey to a screen reader. + return tr::lng_sr_folder_locked(tr::now, lt_text, title); + } + const auto it = _unreadCounts.find(index); + return (it != _unreadCounts.end()) + ? tr::lng_filter_unread_chats( + tr::now, + lt_count, + it->second.count, + lt_text, + title) + : title; +} + +QString ChatsFiltersTabs::accessibilityChildDescription(int index) const { + return (_lockedFrom > 0 && index >= _lockedFrom) + ? tr::lng_sr_folder_locked_about(tr::now) + : QString(); +} + +void ChatsFiltersTabs::activateSectionByAccessibility(int index) { + // Keyboard or screen reader activation of a locked (premium) folder + // behaves like a click on it: show the premium promo instead of + // switching to a folder the user can't use. + if (_lockedFrom > 0 && index >= _lockedFrom) { + _lockedClicked.fire({}); + } else { + Ui::SettingsSlider::activateSectionByAccessibility(index); + } } -rpl::producer ChatsFiltersTabs::contextMenuRequested() const { +auto ChatsFiltersTabs::contextMenuRequested() const +-> rpl::producer { return _contextMenuRequested.events(); } diff --git a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.h b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.h index fc55a47336c547..0452189e1bd0aa 100644 --- a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.h +++ b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.h @@ -25,6 +25,11 @@ class SettingsSlider; class ChatsFiltersTabsReorder; +struct ChatsFiltersTabMenuRequest { + int index = 0; + QPoint position; // Global coordinates for showing the menu. +}; + class ChatsFiltersTabs final : public Ui::SettingsSlider { public: ChatsFiltersTabs( @@ -45,7 +50,8 @@ class ChatsFiltersTabs final : public Ui::SettingsSlider { return _lockedFrom; } - [[nodiscard]] rpl::producer contextMenuRequested() const; + [[nodiscard]] auto contextMenuRequested() const + -> rpl::producer; [[nodiscard]] rpl::producer<> lockedClicked() const; void setHorizontalShift(int index, int shift); @@ -58,6 +64,9 @@ class ChatsFiltersTabs final : public Ui::SettingsSlider { void stopAnimation(); + QString accessibilityChildName(int index) const override; + QString accessibilityChildDescription(int index) const override; + protected: struct ShiftedSection { not_null section; @@ -71,6 +80,7 @@ class ChatsFiltersTabs final : public Ui::SettingsSlider { void mouseMoveEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; void contextMenuEvent(QContextMenuEvent *e) override; + void activateSectionByAccessibility(int index) override; std::vector _sections; @@ -110,7 +120,7 @@ class ChatsFiltersTabs final : public Ui::SettingsSlider { int _reordering = 0; rpl::lifetime _paletteLifetime; - rpl::event_stream _contextMenuRequested; + rpl::event_stream _contextMenuRequested; rpl::event_stream<> _lockedClicked; }; diff --git a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp index 9505861b5db6fc..1dc8cc6448e3a0 100644 --- a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp +++ b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp @@ -62,7 +62,8 @@ void ShowMenu( not_null parent, not_null controller, not_null state, - int index) { + int index, + QPoint position) { const auto session = &controller->session(); auto id = FilterId(0); @@ -133,7 +134,7 @@ void ShowMenu( state->menu = nullptr; return; } - state->menu->popup(QCursor::pos()); + state->menu->popup(position); } void ShowFiltersListMenu( @@ -141,6 +142,7 @@ void ShowFiltersListMenu( not_null session, not_null state, int active, + QPoint position, Fn changeActive) { const auto &list = session->data().chatsFilters().list(); @@ -195,7 +197,7 @@ void ShowFiltersListMenu( state->menu = nullptr; return; } - state->menu->popup(QCursor::pos()); + state->menu->popup(position); } } // namespace @@ -230,6 +232,11 @@ not_null AddChatFiltersTabsStrip( trackActiveFilterAndUnreadAndReorder ? st::dialogsSearchTabs : st::chatsFiltersTabs)); + slider->setAccessibleName(tr::lng_filters_title(tr::now)); + // Switching a folder reloads the whole chat list, and a locked premium + // folder opens an upsell on activation - so the arrows only browse the + // folder tabs, committing on Enter / Space. + slider->setAccessibilityActivateOnBrowse(false); const auto state = wrap->lifetime().make_state(); const auto reassignUnreadValue = [=] { state->reorderLifetime.destroy(); @@ -364,6 +371,14 @@ not_null AddChatFiltersTabsStrip( } }; + slider->accessibilitySectionBrowsed( + ) | rpl::on_next([=](int index) { + // Browsing with a screen reader moves between sections without + // activating them, so nothing else scrolls the strip; keep the + // browsed section visible. + scrollToIndex(index, anim::type::normal); + }, slider->lifetime()); + const auto applyFilter = [=](const Data::ChatFilter &filter) { if (slider->reordering()) { return; @@ -492,15 +507,22 @@ not_null AddChatFiltersTabsStrip( } applyFilter(filter); }, state->rebuildLifetime); - slider->contextMenuRequested() | rpl::on_next([=](int index) { + slider->contextMenuRequested() | rpl::on_next([=]( + Ui::ChatsFiltersTabMenuRequest request) { if (trackActiveFilterAndUnreadAndReorder) { - ShowMenu(wrap, controller, state, index); + ShowMenu( + wrap, + controller, + state, + request.index, + request.position); } else { ShowFiltersListMenu( wrap, session, state, slider->activeSection(), + request.position, [=](int i) { slider->setActiveSection(i); }); } }, state->rebuildLifetime);