Page MenuHomePhabricator

Mobile site's collapsed sections not accessible for screen readers
Closed, ResolvedPublic2 Estimated Story PointsBUG REPORT

Description

Steps to replicate the issue (include links if applicable):

  1. Open any Wikipedia page (mobile interface) that has collapsible sections i.e. https://en.wikipedia.org/wiki/Apple_Inc.
  2. The "collapsed sections" that come after the introductory paragraphs of the article, can be expanded and collapsed by clicking/tapping on them

What happens?:

  1. However, for keyboard-only users (i.e. Tab/Enter/Space key), these sections cannot be interacted with
  2. And, for blind screen reader users, they do not hear the correct role, or the expanded and collapsed state
  3. This is a failure of the Web Content Accessibility Guidelines 2.2

This is caused by the collapsed sections being implemented using non-semantic HTML (i.e., not using details and summary elements).

What should have happened instead?:

  1. Collapsed sections should be able to be keyboard-focused, opened and closed using the keyboard only
  2. Screen readers should announce the correct role i.e. button, or use details/summary elements.
  3. Screen readers should announce the expanded and collapsed state i.e. using aria-expanded attributes, or using details/summary elements.

I recommend that Wikipedia switches to using HTML details/summary elements for its expanding/collapsing sections, as they require no JavaScript, and are accessible by default, and require no complex ARIA attributes.

Software version (on Special:Version page; skip for WMF-hosted wikis like Wikipedia): Public Wikipedia

Other information (browser name/version, screenshots, etc.):

I tried to fix the problem myself in two different ways — one is a 'quick and dirty' way, and the other way is a more comprehensive transformation in how collapsible secrtions is implemented.

I cloned Mediawiki, MinervaNeue, Mobilefrontend.

Quick and dirty solution

I noticed the mobile.init.js of MinervaNeue did not initialise Toggler.js, but, MobielFrontend's mobile.init.js does initialise Toggler.js. Toggler.js appears to set correct(ish) ARIA attributes on the collapsible headings.

I modified mobile.init.js of MinervaNeue so it ran the mobile.init.js of MobileFrontend. But this didn't work.

I discovered Toggler.js was applying ARIA attributes on a non-existent element, which is why it wasn't working. So I updated the element it applied the attributes to, and the correct attributes were applied.

Changes I made to Toggler.js:

// before...
$headingLabel
	.attr( {
		tabindex: 0,
		role: 'button',
		'aria-controls': id,
		'aria-expanded': 'false'
	} );

// after
$heading
	.attr( {
		tabindex: 0,
		role: 'button',
		'aria-controls': id,
		'aria-expanded': 'false'
	} );
//before
$headingLabel.attr( 'aria-expanded', !wasExpanded );

//after
$heading.attr( 'aria-expanded', !wasExpanded );

So... Toggler.js could perhaps be fixed up, and it'd *mostly* fix this issue, and you'd also have to ensure Toggler.js runs when using MinervaNeue (I assume). I'm not very familiar with this codebase.

Better, more accessible long-term solution

But, frankly, my preferred solution, would be to use details and summary HTML elements instead of these complex JavaScript/ARIA based expanding/collapsing sections — this should be the goal. One advantage of using the details and summary elements instead, is Reader modes like Safari Reader will expose the content properly, giving an additional accessibility benefit. It also needs absolutely no JavaScript - all interactivity is handled by the browser, and it's fully accessible out of the box.

I also attempted to modify MakeSectionsTransform.js in MobileFrontend so it uses details/summary elements instead of complex JavaScript based disclosure widgets. I got it working pretty well but I can't be bothered finishing the job, so I'm hoping someone more familiar with the codebase can pick up this task. From an accessibility perspective, switching to details/summary elements is far better IMO.

Example code to get details/summary elements working for collapsible sections on mobile Wikipedia:

MakeSectionsTransform.js, makeSections function

/**
	* Actually splits the body of the document into sections
	*
	* @param DOMElement $body representing the HTML of the current article. In the HTML the sections
	*  should not be wrapped.
	* @param DOMElement[] $headingWrappers The headings (or wrappers) returned by getTopHeadings():
	*  `<h1>` to `<h6>` nodes, or `<div class="mw-heading">` nodes wrapping them.
	*  In the future `<div class="mw-heading">` will be required (T13555).
	*/
private function makeSections( DOMElement $body, array $headingWrappers ) {
	$ownerDocument = $body->ownerDocument;
	if ( $ownerDocument === null ) {
		return;
	}
	// Find the parser output wrapper div
	$xpath = new DOMXPath( $ownerDocument );
	$containers = $xpath->query(
		// Equivalent of CSS attribute `~=` to support multiple classes
		'body/div[contains(concat(" ",normalize-space(@class)," ")," mw-parser-output ")][1]'
	);
	if ( !$containers->length ) {
		// No wrapper? This could be an old parser cache entry, or perhaps the
		// OutputPage contained something that was not generated by the parser.
		// Try using the <body> as the container.
		$containers = $xpath->query( 'body' );
		if ( !$containers->length ) {
			throw new LogicException( "HTML lacked body element even though we put it there ourselves" );
		}
	}

	$container = $containers->item( 0 );
	$containerChild = $container->firstChild;
	$firstHeading = reset( $headingWrappers );
	$firstHeadingName = $this->getHeadingName( $firstHeading );
	$sectionNumber = 0;
	$sectionBody = $this->createSectionBodyElement( $ownerDocument, $sectionNumber, false );
	$hasPrepended = false;

	while ( $containerChild ) {
		$node = $containerChild;
		$containerChild = $containerChild->nextSibling;
		// Insert lead content before the first heading
		if ( !$hasPrepended && $this->getHeadingName( $node ) === $firstHeadingName ) {
			// Insert lead content before the first heading
			$container->insertBefore( $sectionBody, $node );
			$hasPrepended = true;
		}
		// If we've found a top level heading, insert the previous section if
		// necessary and clear the container div.
		if ( $firstHeadingName && $this->getHeadingName( $node ) === $firstHeadingName ) {
			// The heading we are transforming is always 1 section ahead of the
			// section we are currently processing
			/** @phan-suppress-next-line PhanTypeMismatchArgumentSuperType DOMNode vs. DOMElement */
			$this->prepareHeading( $ownerDocument, $node, $sectionNumber + 1, $this->scriptsEnabled );

			$sectionNumber += 1;
			$sectionBody = $this->createSectionBodyElement(
				$ownerDocument,
				$sectionNumber,
				$this->scriptsEnabled
			);

			// Move the edit section span into the details block after summary
			$editSection = DOMCompat::querySelector($node, '.mw-editsection');
			$details = $ownerDocument->createElement( 'details' );
			$details->setAttribute( 'id', 'mf-section-' . $sectionNumber );
			$details->setAttribute( 'class', 'section-details' );

			// Find the actual heading tag (h2–h6) inside the wrapper
			$innerHeading = DOMCompat::querySelector( $node, implode( ',', $this->topHeadingTags ) );
			$summary = $ownerDocument->createElement( 'summary' );
			if ($node->hasAttribute("class")) {
				$summary->setAttribute('class', $node->getAttribute('class'));
			}
			if ( $innerHeading ) {
				$summary->appendChild( $ownerDocument->createTextNode( trim( $innerHeading->textContent ) ) );
			} else {
				// Fallback if inner heading can't be found
				$summary->appendChild( $ownerDocument->createTextNode( trim( $node->textContent ) ) );
			}
			$details->appendChild( $summary );
			if ( $editSection ) {
				$details->appendChild( $editSection );
			}
			$details->appendChild( $sectionBody );
			$container->insertBefore( $details, $node );
			$node->parentNode->removeChild( $node );
			
			continue;
		}

		// If it is not a top level heading, keep appending the nodes to the
		// section body container.
		$sectionBody->appendChild( $node );
	}
}
/* updated main.less */

/* find wherever .collapsible-block has display: none set, and change it to display: block (there's two occurrences) */

.collapsible-block:not( .collapsible-block-js ) {
    display: block;
}

.section-details {
	border-bottom: 1px solid var(--border-color-muted, #dadde3);
	margin-bottom: 0.5em;

	summary {
		cursor: pointer;
		list-style: none;
		display: flex;
		align-items: center;
		gap: 0.5em;

		&::before {
			content: '▶';
			display: inline-block;
			font-size: 0.75em;
		}
	}

	&[open] summary::before {
		transform: rotate(90deg);
	}
}

Requirement

Collapsed sections on the mobile site must be fully accessible to keyboard-only users and screen readers:

  • Section headings must be keyboard focusable with visible outlines
  • Toggling must work with Enter or Space
  • Screen readers must identify the elements as interactive
  • Expanded/collapsed states must be announced via ARIA or semantic HTML (e.g., <details>/<summary>)
  • Screen reader rotor must navigate section headings properly

BDD

Feature: Mobile collapsed sections accessibility

Scenario: Keyboard-only user interacts with collapsed sections
Given I open a mobile article (e.g., https://en.m.wikipedia.org/wiki/Apple_Inc.)
When I navigate using the Tab key
Then each collapsible section heading should receive focus and show a visible outline
And the heading should be announced as interactive (e.g., role=“button” and aria-expanded state)
When I press Enter or Space
Then the section should expand or collapse
And the section content should appear or disappear accordingly

Scenario: Screen reader rotor access to collapsed sections
Given I have VoiceOver or TalkBack enabled
When I use the rotor to navigate to Headings
Then each section heading should be reachable and read with its correct expanded/collapsed state
When I double-tap a heading
Then the section should toggle
And the screen reader should announce the updated state

Test Steps

Test Case 1: Keyboard-only interaction

  1. Open a mobile Wikipedia article (e.g., https://en.m.wikipedia.org/wiki/Apple_Inc.)
  2. Use Tab key to navigate through the page
  3. AC1: Each collapsible section heading receives focus and has a visible outline
  4. AC2: Section heading is announced as interactive (e.g., “button”, “collapsed”/“expanded”)
  5. Press Enter or Space on a heading
  6. AC3: Section content expands or collapses
  7. AC4: Toggling works for multiple headings

Test Case 2: Screen reader rotor navigation

  1. On a mobile device, enable a screen reader (VoiceOver or TalkBack)
  2. Open the same article in mobile view
  3. Use rotor/quick navigation to switch to Headings
  4. Swipe to navigate between section headings
  5. AC5: Each heading is announced with its level and expanded/collapsed state
  6. AC6: Each heading appears in the rotor navigation order
  7. Double-tap a heading
  8. AC7: Section toggles open/closed
  9. AC8: Screen reader announces state change (e.g., “expanded”)

QA Results - Beta

Event Timeline

KSarabia-WMF added a subscriber: matmarex.
KSarabia-WMF subscribed.

Collapsible H2s broken due to heading markup changes. Past fixes outdated. <details> suggested but requires investigation. @matmarex We are pinging you because we think this was broken with the heading html changes.

You're right, I overlooked this when working on T13555.

I have updated Toggler.js for the "transitional" heading markup once used by DiscussionTools in rEMFR45387806cb40: Allow collapsible sections with DiscussionTools wrappers on headings, and in that commit I wrote that "this code will also work with the new MediaWiki core markup for headings proposed in T13555", and then I must have mentally checked off MobileFrontend from the list of things to update. But it only mostly works – the collapsing superficially works, which is why no one noticed the problem with the accessibility attributes until now, but it still uses mw-headline, which is definitely not correct. The code uses jQuery in such a way that this mistake is silently ignored, rather than causing exceptions that would have been noticed.

I once reviewed all uses of mw-headline in T323773, but I was not very thorough there, since at that time we still used that class in correct code sometimes. I also – more recently – reviewed all mentions of the config options related to old heading HTML in T371756, but the MobileFrontend code doesn't mention them, and I didn't thoroughly review the uses of the mw-headline class this time either (there are a lot of false positives in old third-party code when you Codesearch for it).

By the way, I found that this issue was already reported once in November 2024 as T381099 / T218404#10365388, but that was closed without being resolved seemingly due to a misunderstanding.

This bug is not present in the new section collapsing code that was rewritten for Parsoid HTML (sectionCollapsing.js).

I'll work on this next week.

Change #1150599 had a related patch set uploaded (by Bartosz Dziewoński; author: Bartosz Dziewoński):

[mediawiki/extensions/MobileFrontend@master] Legacy Toggler: Fix accessibility attributes

https://gerrit.wikimedia.org/r/1150599

Slack-bot changed the task status from Open to In Progress.May 28 2025, 12:12 AM

Change #1150599 merged by jenkins-bot:

[mediawiki/extensions/MobileFrontend@master] Legacy Toggler: Fix accessibility attributes

https://gerrit.wikimedia.org/r/1150599

Keyboard-Only Testing

  1. Open a Wikipedia article on mobile (e.g., en.m.wikipedia.org).
  2. Navigate using Tab only:
    • Ensure all section headings are focusable and have visible focus outlines.
  1. For each section heading:
    • Confirm it’s announced as interactive (e.g., “expanded”).
    • Press Enter or Space to toggle the section open/closed.
    • Confirm section content appears/disappears correctly.

Screen Reader Rotor Testing (Mobile Device)

  1. Enable screen reader (VoiceOver, TalkBack).
  2. Use rotor/quick navigation to select Headings.
  3. Swipe up/down to navigate through all section headings.
  4. Confirm each section heading is:
    • Announced with proper label (e.g., heading level, “collapsed”/“expanded”).
    • Reachable in rotor order.
  5. Double-tap on each section heading:
    • Confirm section toggles open/closed.
    • Verify state change is announced (e.g., “expanded”).
Jdlrobson-WMF lowered the priority of this task from High to Medium.May 29 2025, 8:54 PM
Edtadros subscribed.

Test Result - Beta

Status: ✅ PASS
Environment: beta
OS: iOS 18.5
Browser: Safari
Device: iPhone 16 Pro Max
Emulated Device: NA

Test Steps

Test Case 1: Keyboard-only accessibility of collapsed sections

  1. Visit https://en.m.wikipedia.org/wiki/Apple_Inc. on a mobile browser.
  2. Press Tab to navigate through the page.
  3. AC1: Section headings are focusable and show visible focus outlines.
  4. AC2: Each section heading is announced as interactive (e.g., “button” or “expanded”).
  5. Press Enter or Space on a section heading.
  6. AC3: Section toggles open and closed correctly with keyboard input.
  7. AC4: Section content appears/disappears appropriately on toggle.

screenshot 104.mov.gif (1×840 px, 921 KB)

Test Case 2: Screen reader accessibility via rotor navigation

  1. Enable VoiceOver (iOS) or TalkBack (Android).
  2. Visit https://en.m.wikipedia.org/wiki/Apple_Inc. in a mobile browser.
  3. Use rotor/quick navigation to filter by headings.
  4. AC5: All collapsible section headings are included in the rotor’s heading list.
  5. AC6: Each section is announced with heading level and state (e.g., “collapsed” or “expanded”).
  6. Swipe to a section heading and double-tap.
  7. AC7: Section toggles open or closed.
  8. AC8: Screen reader announces state change correctly (e.g., “expanded” or “collapsed”).

@KSarabia-WMF I tried testing on https://en.m.wikipedia.org/w/index.php?title=PayScale with VO on mobile, and I didnt get any notification of expanded. it just said heading level 2 and then the name of the heading. Is the fix in prod?

@bwang Looks like it isnt yet. Feel free to check in Beta or wait till Thursday