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:
parent
02e826dddb
commit
8433428036
5 changed files with 104 additions and 21 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue