1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +00:00

Accessibility: Make the FocusTraverser navigate onto disabled components

With this change disabled components become discoverable by screen
readers, similarly to how OS native user interface components behave by
default.

The KeyboardFocusTraverser will still skip disabled components so this
does not affect keyboard navigation without screen readers.
This commit is contained in:
attila 2025-07-04 15:38:18 +02:00 committed by Attila Szarvas
parent 02e826dddb
commit 8433428036
5 changed files with 104 additions and 21 deletions

View file

@ -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

View file

@ -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<Component*> components;
detail::FocusHelpers::findAllComponents (parentComponent,
components,
&Component::isFocusContainer);
&Component::isFocusContainer,
skipDisabledComponents);
if (! components.empty())
return components.front();
@ -77,7 +86,8 @@ std::vector<Component*> FocusTraverser::getAllComponents (Component* parentCompo
std::vector<Component*> 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<void(Component&)>& makeIgnored)
void checkIgnored (const std::function<void (Component&)>& 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());

View file

@ -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<Component*> getAllComponents (Component* parentComponent) override;
private:
SkipDisabledComponents skipDisabledComponents = SkipDisabledComponents::no;
};
} // namespace juce

View file

@ -45,10 +45,10 @@ struct FocusHelpers
return order > 0 ? order : std::numeric_limits<int>::max();
}
template <typename FocusContainerFn>
static void findAllComponents (const Component* parent,
std::vector<Component*>& 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<Component*> 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 <typename FocusContainerFn>
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<Component*> components;
findAllComponents (focusContainer, components, isFocusContainer);
findAllComponents (focusContainer, components, isFocusContainer, skipDisabledComponents);
const auto iter = std::find (components.cbegin(), components.cend(), current);

View file

@ -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<Component*> KeyboardFocusTraverser::getAllComponents (Component* par
std::vector<Component*> components;
detail::FocusHelpers::findAllComponents (parentComponent,
components,
&Component::isKeyboardFocusContainer);
&Component::isKeyboardFocusContainer,
FocusTraverser::SkipDisabledComponents::yes);
auto removePredicate = [parentComponent] (const Component* comp)
{