diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 12298e9675..226fef537b 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -2,6 +2,42 @@ # develop +## Change + +The behaviour of the default constructed FocusTraverser objects has changed, and +they will now navigate onto disabled components. This only affects navigation by +screen readers and not general keyboard navigation, as the latter depends on the +KeyboardFocusTraverser class. + +**Possible Issues** + +Disabled child components of focus containers that used the JUCE default +FocusTraverser will now be discoverable by screen readers. They will accept +accessibility focus, their title will be reported as well as their disabled +state. + +Children of components that returned a custom ComponentTraverser object are not +affected. + +**Workaround** + +If you wish to hide disabled components from screen readers, you can restore the +old behaviour by overriding `Component::createFocusTraverser()` for your focus +containers, and returning a FocusTraverser object created using the +`FocusTraverser::SkipDisabledComponents::yes` argument. + +**Rationale** + +Disabled components are typically rendered in a dimmed or inactive state, but +are still prominently visible for sighted users. The old behaviour made these +components entirely missing from the accessibility tree, making them +non-discoverable with screen readers. + +This was in contrast to the behaviour of native OS components, that are still +accessible using screen readers, but their disabled/dimmed state is also +reported. + + ## Change The default Visual Studio project settings for "Debug Information Format" have diff --git a/modules/juce_gui_basics/components/juce_FocusTraverser.cpp b/modules/juce_gui_basics/components/juce_FocusTraverser.cpp index 44dd629a30..0ece10edc7 100644 --- a/modules/juce_gui_basics/components/juce_FocusTraverser.cpp +++ b/modules/juce_gui_basics/components/juce_FocusTraverser.cpp @@ -36,6 +36,11 @@ namespace juce { //============================================================================== +FocusTraverser::FocusTraverser (SkipDisabledComponents skipDisabledComponentsIn) + : skipDisabledComponents (skipDisabledComponentsIn) +{ +} + Component* FocusTraverser::getNextComponent (Component* current) { jassert (current != nullptr); @@ -43,7 +48,8 @@ Component* FocusTraverser::getNextComponent (Component* current) return detail::FocusHelpers::navigateFocus (current, current->findFocusContainer(), detail::FocusHelpers::NavigationDirection::forwards, - &Component::isFocusContainer); + &Component::isFocusContainer, + skipDisabledComponents); } Component* FocusTraverser::getPreviousComponent (Component* current) @@ -53,7 +59,8 @@ Component* FocusTraverser::getPreviousComponent (Component* current) return detail::FocusHelpers::navigateFocus (current, current->findFocusContainer(), detail::FocusHelpers::NavigationDirection::backwards, - &Component::isFocusContainer); + &Component::isFocusContainer, + skipDisabledComponents); } Component* FocusTraverser::getDefaultComponent (Component* parentComponent) @@ -61,9 +68,11 @@ Component* FocusTraverser::getDefaultComponent (Component* parentComponent) if (parentComponent != nullptr) { std::vector components; + detail::FocusHelpers::findAllComponents (parentComponent, components, - &Component::isFocusContainer); + &Component::isFocusContainer, + skipDisabledComponents); if (! components.empty()) return components.front(); @@ -77,7 +86,8 @@ std::vector FocusTraverser::getAllComponents (Component* parentCompo std::vector components; detail::FocusHelpers::findAllComponents (parentComponent, components, - &Component::isFocusContainer); + &Component::isFocusContainer, + skipDisabledComponents); return components; } @@ -116,14 +126,23 @@ struct FocusTraverserTests final : public UnitTest [] (const Component* c1, const Component& c2) { return c1 == &c2; })); } - beginTest ("Disabled components are ignored"); + beginTest ("Disabled components are not ignored by default"); { - checkIgnored ([] (Component& c) { c.setEnabled (false); }); + TestComponent parent; + parent.children[2].setEnabled (false); + parent.children[5].setEnabled (false); + expect (traverser.getAllComponents (&parent).size() == parent.children.size()); + } + + beginTest ("Disabled components can be ignored"); + { + FocusTraverser ignoringTraverser { FocusTraverser::SkipDisabledComponents::yes }; + checkIgnored ([] (Component& c) { c.setEnabled (false); }, ignoringTraverser); } beginTest ("Invisible components are ignored"); { - checkIgnored ([] (Component& c) { c.setVisible (false); }); + checkIgnored ([] (Component& c) { c.setVisible (false); }, traverser); } beginTest ("Explicit focus order comes before unspecified"); @@ -253,21 +272,21 @@ private: } } - void checkIgnored (const std::function& makeIgnored) + void checkIgnored (const std::function& makeIgnored, FocusTraverser& traverserToUse) { TestComponent parent; auto iter = parent.children.begin(); makeIgnored (*iter); - expect (traverser.getDefaultComponent (&parent) == std::addressof (*std::next (iter))); + expect (traverserToUse.getDefaultComponent (&parent) == std::addressof (*std::next (iter))); iter += 5; makeIgnored (*iter); - expect (traverser.getNextComponent (std::addressof (*std::prev (iter))) == std::addressof (*std::next (iter))); - expect (traverser.getPreviousComponent (std::addressof (*std::next (iter))) == std::addressof (*std::prev (iter))); + expect (traverserToUse.getNextComponent (std::addressof (*std::prev (iter))) == std::addressof (*std::next (iter))); + expect (traverserToUse.getPreviousComponent (std::addressof (*std::next (iter))) == std::addressof (*std::prev (iter))); - auto allComponents = traverser.getAllComponents (&parent); + auto allComponents = traverserToUse.getAllComponents (&parent); expect (std::find (allComponents.cbegin(), allComponents.cend(), &parent.children.front()) == allComponents.cend()); expect (std::find (allComponents.cbegin(), allComponents.cend(), std::addressof (*iter)) == allComponents.cend()); diff --git a/modules/juce_gui_basics/components/juce_FocusTraverser.h b/modules/juce_gui_basics/components/juce_FocusTraverser.h index 86d67952ae..627b7b069e 100644 --- a/modules/juce_gui_basics/components/juce_FocusTraverser.h +++ b/modules/juce_gui_basics/components/juce_FocusTraverser.h @@ -62,6 +62,25 @@ namespace juce class JUCE_API FocusTraverser : public ComponentTraverser { public: + /** Controls whether the FocusTraverser will allow navigation to disabled components. + */ + enum class SkipDisabledComponents + { + no, ///< Disabled components can receive focus. + yes ///< Disabled components can't receive focus. + }; + + /** Creates a FocusTraverser that will not skip disabled components. */ + FocusTraverser() = default; + + /** Creates a FocusTraverser. + + @param skipDisabledComponents If set to SkipDisabledComponents::yes, the traverser will ignore + disabled components. This makes such components non-discoverable + using screen readers. + */ + explicit FocusTraverser (SkipDisabledComponents skipDisabledComponents); + /** Destructor. */ ~FocusTraverser() override = default; @@ -97,6 +116,9 @@ public: The default implementation will return all visible and enabled child components. */ std::vector getAllComponents (Component* parentComponent) override; + +private: + SkipDisabledComponents skipDisabledComponents = SkipDisabledComponents::no; }; } // namespace juce diff --git a/modules/juce_gui_basics/detail/juce_FocusHelpers.h b/modules/juce_gui_basics/detail/juce_FocusHelpers.h index 7b64f36cc5..1a3cc24ad9 100644 --- a/modules/juce_gui_basics/detail/juce_FocusHelpers.h +++ b/modules/juce_gui_basics/detail/juce_FocusHelpers.h @@ -45,10 +45,10 @@ struct FocusHelpers return order > 0 ? order : std::numeric_limits::max(); } - template static void findAllComponents (const Component* parent, std::vector& components, - FocusContainerFn isFocusContainer) + bool (Component::* isFocusContainer)() const, + FocusTraverser::SkipDisabledComponents skipDisabledComponents) { if (parent == nullptr || parent->getNumChildComponents() == 0) return; @@ -56,8 +56,12 @@ struct FocusHelpers std::vector localComponents; for (auto* c : parent->getChildren()) - if (c->isVisible() && c->isEnabled()) + { + constexpr auto no = FocusTraverser::SkipDisabledComponents::no; + + if (c->isVisible() && (c->isEnabled() || skipDisabledComponents == no)) localComponents.push_back (c); + } const auto compareComponents = [&] (const Component* a, const Component* b) { @@ -81,23 +85,23 @@ struct FocusHelpers components.push_back (c); if (! (c->*isFocusContainer)()) - findAllComponents (c, components, isFocusContainer); + findAllComponents (c, components, isFocusContainer, skipDisabledComponents); } } enum class NavigationDirection { forwards, backwards }; - template static Component* navigateFocus (const Component* current, const Component* focusContainer, NavigationDirection direction, - FocusContainerFn isFocusContainer) + bool (Component::* isFocusContainer)() const, + FocusTraverser::SkipDisabledComponents skipDisabledComponents) { if (focusContainer == nullptr) return nullptr; std::vector components; - findAllComponents (focusContainer, components, isFocusContainer); + findAllComponents (focusContainer, components, isFocusContainer, skipDisabledComponents); const auto iter = std::find (components.cbegin(), components.cend(), current); diff --git a/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.cpp b/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.cpp index 14098abc70..b76a65026b 100644 --- a/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.cpp +++ b/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.cpp @@ -47,7 +47,8 @@ namespace KeyboardFocusTraverserHelpers detail::FocusHelpers::NavigationDirection direction) { if (auto* comp = detail::FocusHelpers::navigateFocus (current, container, direction, - &Component::isKeyboardFocusContainer)) + &Component::isKeyboardFocusContainer, + FocusTraverser::SkipDisabledComponents::yes)) { if (isKeyboardFocusable (comp, container)) return comp; @@ -85,7 +86,8 @@ std::vector KeyboardFocusTraverser::getAllComponents (Component* par std::vector components; detail::FocusHelpers::findAllComponents (parentComponent, components, - &Component::isKeyboardFocusContainer); + &Component::isKeyboardFocusContainer, + FocusTraverser::SkipDisabledComponents::yes); auto removePredicate = [parentComponent] (const Component* comp) {