diff --git a/resources/ios/NativeUISimpleRenderers.swift b/resources/ios/NativeUISimpleRenderers.swift index d3cfeae..4731636 100644 --- a/resources/ios/NativeUISimpleRenderers.swift +++ b/resources/ios/NativeUISimpleRenderers.swift @@ -23,6 +23,13 @@ struct NativeUIPressableRenderer: View { NativeUIColumnRenderer(node: node, children: content) .contentShape(Rectangle()) } + // Core skips its accessibility modifier for `:menu` pressables + // (the Menu owns the tap), so the label and hint go on the Menu + // here. Without them an icon-only menu reads as "pop up button". + .modifier(PressableMenuAccessibility( + label: node.props.getString("a11y_label"), + hint: node.props.getString("a11y_hint") + )) } else if node.props.getBool("has_context_menu") { // `:context-menu` attached — the same items, opened by a long // press. The tap stays core's `@press`. SwiftUI's context menu @@ -41,6 +48,25 @@ struct NativeUIPressableRenderer: View { } } +/// Gives a `:menu` pressable's Menu its `a11y-label` and `a11y-hint`. +private struct PressableMenuAccessibility: ViewModifier { + let label: String + let hint: String + + func body(content: Content) -> some View { + switch (label.isEmpty, hint.isEmpty) { + case (true, true): + content + case (false, true): + content.accessibilityLabel(Text(label)) + case (true, false): + content.accessibilityHint(Text(hint)) + case (false, false): + content.accessibilityLabel(Text(label)).accessibilityHint(Text(hint)) + } + } +} + /// Render one menu item as either a Button or a Divider, mirroring the /// `TopBarActionView` dropdown pattern from `NativeRootStackRenderer`. @ViewBuilder diff --git a/tests/IosPressableMenuItemsTest.php b/tests/IosPressableMenuItemsTest.php index c56b19a..fc74293 100644 --- a/tests/IosPressableMenuItemsTest.php +++ b/tests/IosPressableMenuItemsTest.php @@ -19,3 +19,19 @@ ->toContain('childNodes: children') ->toContain('ForEach(children) { child in'); }); + +it('gives a menu pressable its accessibility label and hint on iOS', function () { + $source = file_get_contents(dirname(__DIR__).'/resources/ios/NativeUISimpleRenderers.swift'); + $start = strpos($source, 'struct NativeUIPressableRenderer'); + $pressable = substr($source, $start, strpos($source, 'private struct PressableMenuAccessibility') - $start); + $modifier = substr($source, strpos($source, 'private struct PressableMenuAccessibility'), strpos($source, 'private func pressableMenuItem') - strpos($source, 'private struct PressableMenuAccessibility')); + + expect($pressable) + ->toContain('.modifier(PressableMenuAccessibility(') + ->toContain('label: node.props.getString("a11y_label")') + ->toContain('hint: node.props.getString("a11y_hint")') + ->and($modifier) + ->toContain('case (true, true):') + ->toContain('content.accessibilityLabel(Text(label))') + ->toContain('content.accessibilityHint(Text(hint))'); +});