Accessibility (screen reader) follows tabKey KeyboardFocus - but not for Sliders

I’m new to trying to get the Accessibility features working, but I’ve noticed that if you have VoiceOver turned on (Mac), when you use the tab key to move between KeyboardFocusable components (i.e. TextButtons, ComboBoxes), the screen reader follows the focus, and reads out each component. Great. Except it doesn’t work for Sliders, or in my case, Sliders with IncDec buttons used to enter values.

(I understand that you can use control+option+arrowKeys to move the screen reader, and gain access to everything including Labels etc. That’s not what I’m talking about; rather it seems the reader will also follow the tabKey to KeyboardFocusable Components. But not Sliders with text boxes.)

When you tab into a Slider, it makes it editable so you can enter a value (and opens a label’s TextEditor). But this results in no movement from the screen reader. It ignores that action. If you tab to another non-Slider component like a TextButton, then the screen reader jumps to that one, effectively having skipped the Slider.

Is there any way to force the screen reader to follow the tab key into the Slider?

If I use setWantsKeyboardFocus(true) on the Slider, then tabbing to it does make the screen reader follow it; however then it doesn’t activate the editor unless you tab twice. (First tab focuses the Slider, the screen reader reads it, but then you need a second tab to actually get into the text box).

Maybe I’m missing something here…

Is there something like grabKeyboardFocus() except for Accessibility focus?

I mean, maybe it’s supposed to work this way, but it seems remarkably inconsistent. I’ve put together a simple project that shows what I’m talking about.

FocusTraverseTest2.zip (12.2 KB)

When you turn on VoiceOver, it first places the Accessibility Outline around the first slider component and reads it.

Click in the Slider’s text box to focus it.

Press the tab key. The Keyboard Focus moves to the second slider’s text box - but not the Accessibility focus. VoiceOver does nothing.

Press the tab key again. The Keyboard Focus moves to the Button - but so does the Accessibility Focus! VoiceOver reads the Button.

Press the tab key again. The Keyboard Focus movers to the third slider, but VoiceOver does nothing.

Press the tab key again. The Keyboard Focus moves to the Combo Box - and so does the Accessibility Focus! VoiceOver reads the Combo Box.

Press the tab key again. This time, the Accessibility Focus moves to Slider 1. VoiceOver reads it out. But it is NOT keyboard-focused.

Keep pressing the tab key - it continues cycling through all five components, but the VoiceOver only reads the Button and the Combo Box and the first Slider. Why not the other two Sliders?

It seems somewhat illogical…

I do understand you can use the command+option+arrow keys to move the screen reader around, and that does work. But it seems that if it’s going to follow the tab key SOME of the time, it should do it ALL of the time.

Thanks for the report and the test case.

I agree that the behaviour is a bit counter-intuitive - what is happening is that the slider’s text box is explicitly marked as inaccessible in the internal slider code to make sure that when navigating with the VO keys the slider appears as a single widget controllable with the VO + arrow keys. However because the text editor grabs the keyboard focus (and this is different to the accessible focus) the two can sometimes get out of sync. I’ve pushed a fix to develop that will still allow tab navigation for keyboard focus but will also match the accessible focus correctly, and will only navigate to the entire slider when using the VO keys:

Thanks, that is better, although it’s not entirely what I expected.

As I debug into this stuff, I’m seeing how complex the Accessibility code is, so forgive me if my observations are somewhat unenlightened; I have a lot to learn.

Let me ask this: would a visually-impaired user expect the tab key to give them descriptions like the VO keys?

With this fix, you are told you are in a text area. You don’t get the name of the slider, or the tooltip/helpText read (if it has one), or anything descriptive. Perhaps I have not investigated the Accessibility deeply enough, but I have tooltips assigned to all of my UI components, and names, and the VO reads them, but not in this case when you (now) tab into a slider.

I understand that the Slider is a particularly complex case, since you have a TextEditor inside a SliderLabelComp inside a Slider. But really, this Accessibility info needs to be passed down to the TextEditor.

You can hack this to happen by doing something like this in Label::showEditor():

void Label::showEditor()
{
    if (editor == nullptr)
    {
        editor.reset (createEditorComponent());
        editor->setSize (10, 10);
        addAndMakeVisible (editor.get());
        editor->setText (getText(), false);
        editor->setKeyboardType (keyboardType);
        editor->addListener (this);
        editor->grabKeyboardFocus();
        
        // HACK TO PASS ACCESSIBILITY INFO
        if (auto pc = dynamic_cast<Slider*>(getParentComponent()))
        {
            editor->setTitle(pc->getTitle());
            editor->setTooltip(pc->getTooltip());
        }
		// END HACK

        if (editor == nullptr) // may be deleted by a callback
            return;

        editor->setHighlightedRegion (Range<int> (0, textValue.toString().length()));

        resized();
        repaint();

        editorShown (editor.get());

        enterModalState (false);
        editor->grabKeyboardFocus();
    }
}

But, this is currently impossible in a Slider (without modifying JUCE) since there is no access provided to the Label component (private) or the Text Area. I’ll just add this old request here for reference:

But even if you hack something together using the SliderLabelComp ptr you can get in LookAndFeel_V2::createSliderTextBox, you can’t get a ptr to the TextEditor that is created by Label::showEditor().

Alternately, it would be nice to have a lookAndFeel method for createEditorComponent().

Is there some other way to accomplish this?

EDIT: One other observation:

Why, when you keep tabbing around in a loop (even with VO off), does it first select Slider 1 as a whole, then requiring a second tab to get into the text box? When you go from one slider to the next, it does not focus the outer slider first. But when you tab around to the first slider from the ComboBox (looping around), it first focuses the outer slider, then requiring a second tab to focus the TextEditor.

The difference in behaviour is due to their being two focus types - accessibility and keyboard focus - where keyboard focus is a subset of accessibility focus and is disabled by default for components. When navigating with the tab and shift-tab keys you are traversing components that are keyboard focusable whereas when navigating using the VO keys you are traversing accessible components.

This is happening in your case because you have marked the first slider as wanting keyboard focus:

slider1.setWantsKeyboardFocus(true);

Oops. Don’t remember why I did that in this test. Sorry about not noticing that! :flushed:

I do understand there are two different focus types. My questions are about the inconsistencies when traversing them.

With the change you made above, the tab key does move (sync) Accessibility to the Slider (err, the TextEditor inside the Label inside the Slider), but it does not read out any pertinent information to the visually-impaired user, unlike the Button or the ComboBox under the same circumstances. They read the Title, the HelpText etc. The Slider just tells you you are in an unnamed text area.

I illustrated a quick hack I made that solves that issue; but unless JUCE were to do something similar, it requires modifying the JUCE source unless I’m mistaken. **

I’d be happy to have to program this sort of behaviour myself, if I only had access to the TextEditor once it has been created by Label::showEditor(). Couldn’t Label::createEditorComponent() be a LookAndFeel method?

**EDIT:
You can do this without modifying the JUCE source by duplicating and replacing the class LookAndFeel_V2::SliderLabelComp with your own, and adding overrides for focusGained() and showEditor(). Then you can pass back a ptr to one of these in your LookAndFeel::createSliderTextBox().
But I still think it should work this way by default.

For anyone interested:

class AppearanceData::NumberBoxLookAndFeel::SliderLabelComp2  : public Label
{
public:
    SliderLabelComp2() : Label ({}, {}) {}
    
    void mouseWheelMove (const MouseEvent&, const MouseWheelDetails&) override {}
    
    // override to be able to override showEditor
    void focusGained (FocusChangeType cause) override
    {
        if (isEditableOnSingleClick()
            && isEnabled()
            && cause == focusChangedByTabKey)
        {
            showEditor();
        }
    }
    
    // override to pass the Accessibility info to the TextEditor
    void showEditor()
    {
        Label::showEditor();
        
        if (auto editor = getCurrentTextEditor())
        {
            if (auto pc = dynamic_cast<Slider*>(getParentComponent()))
            {
                editor->setTitle(pc->getTitle());
                editor->setTooltip(pc->getTooltip());
                editor->setName(pc->getName() + " editor");
            }
        }
    }
    
    std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
    {
        return createIgnoredAccessibilityHandler (*this);
    }
};

Label* AppearanceData::NumberBoxLookAndFeel::createSliderTextBox (Slider& slider)
{
    auto l = new SliderLabelComp2();
    [...]

That feature request seems reasonable, but will require a bit of thought for how to implement it correctly. Glad to see you’ve got a workaround for the time being.

1 Like

Ed, I need to report an additional issue with the change you made above (commit b421159be31ecf18f4b1600d3a90e2a34d62aab8). Prior to that (6.1.3 release), clicking with the mouse on the slider did the same thing as moving with the tab key to the VoiceOver (nothing).

Now, it at least transfers VoiceOver to the slider with a mouse click, similar to the tab key (which is a step in the right direction), but it reads wrong Tooltip after mouse click on another component

I modified the above demo slightly, here is the new version (same name):

FocusTraverseTest2.zip (12.8 KB)

All 5 components now have Tooltips saying “this is Slider1 Tooltip”, “this is Slider2 Tooltip”, “this is Button tooltip” etc.

Turn on VoiceOver after launching the app. It will focus Accessibility on Slider1; you will hear the Slider1 tooltip being read.

Use the control+option+arrows to advance VoiceOver through the 5 components. You will hear the correct tooltip read for each component.

Now, use control+option+left arrow to return focus to Slider 1. You will hear Slider1 tooltip.

With the mouse, click on the value box of Slider3. Accessibility transfers to the value box, but Voiceover then reads the tooltip for Slider1 (wrong!) which was the previous focused component.

With the mouse, click on the value box of Slider2. Voiceover again reads the tooltip for Slider1 (wrong!).

With the mouse, click on Button. Voiceover correctly reads the Button tooltip.

With the mouse, click on the value box of Slider1. Voiceover reads the Button tooltip (wrong!)

Now, here’s the really weird part:

Locate the screen reader back to Slider1 with control+option+arrow. The correct tooltip is read.

This time, however, use the tab key to advance to Slider2. Now, NO tooltip is read at all.

Try clicking with the mouse on different Sliders’ value boxes. Since using the tab key once, NO tooltips will be read for them. But tooltips still work the for the Button and ComboBox when clicked.

Additional issue:

With the mouse, click the Button. The Button is focused, and VoiceOver reads the correct tooltip.

With the mouse, click the ComboBox. The menu opens and the menu item is focused, and VoiceOver reads the menu item. This is fine, however - click with the mouse again on the ComboBox to dismiss the popup without selecting anything. Screen reader focus jumps back to the Button and reads the Button. In my opinion, I would expect it the focus the ComboBox and read the ComboBox tooltip, as if you had used the VO keys to advance to the ComboBox.

bump?

Hi stephenk,

thank you for bringing this up and also for the example project. I’ve run your code sample and agree that it would be good if the accessibility focus was following the mouse interaction more reliably. I’m still just trying to wrap my head around the lower levels of MacOS accessibility but we’ll keep an eye on this.

Thanks, attila. I agree it’s complicated; I tried to debug this and couldn’t get anywhere. Hope you can fix it at some point.