Skip to content

Commit

Permalink
feat(iOS): Combined stop details VoiceOver pass (#639)
Browse files Browse the repository at this point in the history
* fix: Filtered stop header doesn't ignore safe area

* feat(iOS): Get VoiceOver working on stop details

* feat(iOS): Announce page changed on nav change

* fix(iOS): Add combined stop page visits to history

* feat(iOS): Make announcement when trip disappears

* feat(iOS): Shift focus to auto selected trip tile

* fix(iOS): Header rearrangement and general cleanup

* test: Add UI tests for accessibility labels
  • Loading branch information
EmmaSimon authored Jan 13, 2025
1 parent 0cccf74 commit 74fa418
Show file tree
Hide file tree
Showing 33 changed files with 585 additions and 125 deletions.
2 changes: 1 addition & 1 deletion iosApp/iosApp/ComponentViews/LineCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct LineCard<Content: View>: View {
TransitCard(header: {
LineHeader(line: line, routes: routes) {
PinButton(pinned: pinned, action: { onPin(line.id) })
}
}.accessibilityAddTraits(.isButton)
}, content: content)
}
}
2 changes: 1 addition & 1 deletion iosApp/iosApp/ComponentViews/LineHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ struct LineHeader<Content: View>: View {
if let route = routes.first {
TransitHeader(
name: line.longName,
routeType: route.type,
backgroundColor: Color(hex: line.color),
textColor: Color(hex: line.textColor),
modeIcon: routeIcon(route),
rightContent: rightContent
)
.accessibilityElement(children: .combine)
Expand Down
2 changes: 1 addition & 1 deletion iosApp/iosApp/ComponentViews/RouteCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct RouteCard<Content: View>: View {
TransitCard(header: {
RouteHeader(route: route) {
PinButton(pinned: pinned, action: { onPin(route.id) })
}
}.accessibilityAddTraits(.isButton)
}, content: content)
}
}
2 changes: 1 addition & 1 deletion iosApp/iosApp/ComponentViews/RouteHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ struct RouteHeader<Content: View>: View {
var body: some View {
TransitHeader(
name: route.label,
routeType: route.type,
backgroundColor: route.uiColor,
textColor: route.uiTextColor,
modeIcon: routeIcon(route),
rightContent: rightContent
)
.accessibilityElement(children: .combine)
Expand Down
1 change: 0 additions & 1 deletion iosApp/iosApp/ComponentViews/RoutePill.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ struct RoutePill: View {
.modifier(ColorModifier(pill: self))
.modifier(ClipShapeModifier(spec: spec))
.accessibilityElement()
.accessibilityAddTraits(isActive ? [.isSelected] : [])
.accessibilityLabel(
"\(route?.label ?? line?.longName ?? "") \(route?.type.typeText(isOnly: true) ?? "")"
)
Expand Down
5 changes: 3 additions & 2 deletions iosApp/iosApp/ComponentViews/TransitHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import SwiftUI

struct TransitHeader<Content: View>: View {
let name: String
let routeType: RouteType
let backgroundColor: Color
let textColor: Color
let modeIcon: Image
var rightContent: () -> Content?

@ScaledMetric private var modeIconHeight: CGFloat = 24
Expand All @@ -27,10 +27,11 @@ struct TransitHeader<Content: View>: View {
.foregroundStyle(textColor)
.textCase(.none)
.frame(maxWidth: .infinity, maxHeight: modeIconHeight, alignment: .leading)
.accessibilityLabel(Text("\(name) \(routeType.typeText(isOnly: true))"))
rightContent()
.foregroundStyle(textColor)
} icon: {
modeIcon
routeIcon(routeType)
.resizable()
.aspectRatio(contentMode: .fit)
.scaledToFit()
Expand Down
61 changes: 58 additions & 3 deletions iosApp/iosApp/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@
}
},
"%@ %@ %@" : {
"comment" : "VoiceOver text for the vehicle status on the trip details page,\nex '[train] [approaching] [Alewife]' or '[bus] [now at] [Harvard]'\nPossible values for the vehicle status are \"Approaching\", \"Next stop\", or \"Now at\"",
"comment" : "Screen reader text for the vehicle status on the trip details page,\nex '[train] [approaching] [Alewife]' or '[bus] [now at] [Harvard]'\nPossible values for the vehicle status are \"Approaching\", \"Next stop\", or \"Now at\"\nVoiceOver text for the vehicle status on the trip details page,\nex '[train] [approaching] [Alewife]' or '[bus] [now at] [Harvard]'\nPossible values for the vehicle status are \"Approaching\", \"Next stop\", or \"Now at\"",
"localizations" : {
"en" : {
"stringUnit" : {
Expand Down Expand Up @@ -320,6 +320,17 @@
}
}
},
"%@ %@ %@, selected stop" : {
"comment" : "Screen reader text for the vehicle status on the trip details page when the stop is selected,\nex '[train] [approaching] [Alewife]' or '[bus] [now at] [Harvard], selected stop'\nPossible values for the vehicle status are \"Approaching\", \"Next stop\", or \"Now at\"",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ %2$@ %3$@, selected stop"
}
}
}
},
"%@ %@ at %@" : {
"comment" : "VoiceOver text for the stop details page header,\ndescribes the selected route and and stop, ex '[Red Line] [train] at [Porter]'",
"localizations" : {
Expand Down Expand Up @@ -860,7 +871,7 @@
}
},
"%@ scheduled to depart %@" : {
"comment" : "VoiceOver text for the departure status on the trip details page,\nex '[train] scheduled to depart [Alewife]' or '[bus] scheduled to depart [Harvard]'",
"comment" : "Screen reader text for the departure status on the trip details page,\nex '[train] scheduled to depart [Alewife]' or '[bus] scheduled to depart [Harvard]'",
"localizations" : {
"en" : {
"stringUnit" : {
Expand Down Expand Up @@ -906,6 +917,17 @@
}
}
},
"%@ scheduled to depart %@, selected stop" : {
"comment" : "Screen reader text for the departure status on the trip details page when the stop is selected,\nex '[train] scheduled to depart [Alewife]' or '[bus] scheduled to depart [Harvard], selected stop'",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ scheduled to depart %2$@, selected stop"
}
}
}
},
"%@ to" : {
"comment" : "Label the direction a list of arrivals is for.\nPossible values include Northbound, Southbound, Inbound, Outbound, Eastbound, Westbound.\nFor example, \"[Northbound] to [Alewife]",
"localizations" : {
Expand Down Expand Up @@ -947,6 +969,21 @@
}
}
},
"%@, first stop" : {
"comment" : "Screen reader text for a stop name on the stop details page when that stop is the first stop on the line"
},
"%@, selected stop" : {
"comment" : "Screen reader text for a stop name on the stop details page when that stop is the selected one"
},
"%@, selected stop, first stop" : {
"comment" : "Screen reader text for a stop name on the stop details page when that stop is both selected and first"
},
"%1$@ has departed %2$@" : {
"comment" : "Screen reader text that is announced when a trip disappears from the screen.,\nin the format \"[train/bus/ferry] to [destination] has departed [stop name]\",\nex. \"[train] has departed [Central]\", \"[bus] has departed [Harvard]\""
},
"%1$@ to %2$@ has departed %3$@" : {
"comment" : "Screen reader text that is announced when a trip disappears from the screen.,\nin the format \"[train/bus/ferry] to [destination] has departed [stop name]\",\nex. \"[train] to [Alewife] has departed [Central]\", \"[bus] to [Nubian] has departed [Harvard]\""
},
"%ld stops away" : {
"comment" : "How many stops away the vehicle is from the target stop",
"localizations" : {
Expand Down Expand Up @@ -3146,6 +3183,12 @@
}
}
},
"displays more information" : {
"comment" : "Screen reader hint for tapping on the trip details header on the stop page"
},
"displays more information about this trip" : {
"comment" : "Screen reader hint for tapping a departure card in stop details"
},
"Dock Closure" : {
"comment" : "Possible alert effect",
"localizations" : {
Expand Down Expand Up @@ -4169,6 +4212,9 @@
}
}
},
"Hides remaining stops" : {
"comment" : "Screen reader hint explaining what happens when 'x stops away'\nis selected when it's already open (closes the accordion listing those stops)"
},
"High Winds" : {
"comment" : "Possible alert cause",
"localizations" : {
Expand Down Expand Up @@ -4544,6 +4590,9 @@
}
}
},
"Lists remaining stops" : {
"comment" : "Screen reader hint explaining what happens when 'x stops away'\nis selected (open an accordion listing those stops)"
},
"Live" : {
"comment" : "Indicates that data is being updated in real-time",
"localizations" : {
Expand Down Expand Up @@ -6908,8 +6957,11 @@
}
}
},
"Service is running, but predicted arrival times aren’t available." : {
"comment" : "Explanation under the 'Predictions unavailable' header in stop details."
},
"Service is running, but predicted arrival times aren’t available. The map shows where %@ on this route currently are." : {
"comment" : "Explanation under the 'Predictions unavailable' header in stop details.\nThe interpolated value can be \"buses\" or \"trains\".",
"comment" : "Explanation under the 'Predictions unavailable' header in stop details when maps are enabled.\nThe interpolated value can be \"buses\" or \"trains\".",
"localizations" : {
"es" : {
"stringUnit" : {
Expand Down Expand Up @@ -8181,6 +8233,9 @@
}
}
},
"switches direction" : {
"comment" : "Screen reader hint for the direction toggle action"
},
"Technical Problem" : {
"comment" : "Possible alert cause",
"localizations" : {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ struct StopDetailsFilterPills: View {
type: .flex,
isActive: filter == nil || filter?.routeId == route.id
)
.accessibilityAddTraits(filter?.routeId == route.id ? [.isSelected] : [])
.accessibilityAddTraits(.isButton)
.accessibilityHint(routePillHint)
.frame(minWidth: 44, minHeight: 44, alignment: .center)
.onTapGesture { tapRoutePill(filterBy) }
Expand All @@ -52,6 +54,8 @@ struct StopDetailsFilterPills: View {
type: .flex,
isActive: filter == nil || filter?.routeId == line.id
)
.accessibilityAddTraits(filter?.routeId == line.id ? [.isSelected] : [])
.accessibilityAddTraits(.isButton)
.accessibilityHint(routePillHint)
.frame(minWidth: 44, minHeight: 44, alignment: .center)
.onTapGesture { tapRoutePill(filterBy) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ struct SearchResultsContainer: View {

func handleStopTap(stopId: String) {
guard let stop = searchVM.getStopFor(id: stopId) else { return }
nearbyVM.navigationStack.append(.legacyStopDetails(stop, nil))
nearbyVM.pushNavEntry(.legacyStopDetails(stop, nil))
}

var body: some View {
Expand Down
6 changes: 6 additions & 0 deletions iosApp/iosApp/Pages/StopDetails/DepartureTile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ struct DepartureTile: View {
.clipShape(.rect(cornerRadius: 8))
.padding(1)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(isSelected ? Color.halo : Color.clear, lineWidth: 2))
.accessibilityAddTraits(isSelected ? [.isHeader, .isSelected, .updatesFrequently] : [])
.accessibilityHeading(isSelected ? .h3 : .unspecified)
.accessibilityHint(isSelected ? "" : NSLocalizedString(
"displays more information about this trip",
comment: "Screen reader hint for tapping a departure card in stop details"
))
}

private func deselectedBackgroundColor(_ route: Route) -> Color {
Expand Down
11 changes: 10 additions & 1 deletion iosApp/iosApp/Pages/StopDetails/DirectionPicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ struct DirectionPicker: View {
.padding(8)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
.accessibilityAddTraits(isSelected ? [.isSelected] : [])
.accessibilityAddTraits(isSelected ? [.isSelected, .isHeader] : [])
.accessibilityHeading(isSelected ? .h2 : .unspecified)
.accessibilitySortPriority(isSelected ? 1 : 0)
.accessibilityHint(isSelected ? "" : NSLocalizedString(
"switches direction",
comment: "Screen reader hint for the direction toggle action"
))
.background(isSelected ? route.uiColor : deselectedBackroundColor)
.foregroundStyle(isSelected ? route.uiTextColor : .deselectedToggleText)
.clipShape(.rect(cornerRadius: 6))
Expand All @@ -56,6 +62,9 @@ struct DirectionPicker: View {
.foregroundStyle(route.uiTextColor)
.padding(8)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isHeader)
.accessibilityHeading(.h2)
}
}

Expand Down
11 changes: 9 additions & 2 deletions iosApp/iosApp/Pages/StopDetails/ExplainerPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ struct ExplainerPage: View {
VStack(alignment: .leading, spacing: 0) {
header
VStack(alignment: .leading, spacing: 24) {
explanationHeadline.font(Typography.title2Bold)
explanationHeadline
.font(Typography.title2Bold)
.accessibilityHeading(.h2)
.accessibilityAddTraits(.isHeader)
explanationImage
explanationText.font(Typography.body)
Spacer()
Expand All @@ -50,8 +53,12 @@ struct ExplainerPage: View {
.aspectRatio(contentMode: .fit)
.scaledToFit()
.frame(maxHeight: modeIconHeight, alignment: .topLeading)
.accessibilityHidden(true)

Text("Details", comment: "Header on the general explainer details page").font(Typography.headline)
Text("Details", comment: "Header on the general explainer details page")
.font(Typography.headline)
.accessibilityHeading(.h1)
.accessibilityAddTraits(.isHeader)
Spacer()
ActionButton(kind: .close) { onClose() }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ struct StopDetailsFilteredDepartureDetails: View {
}
}

@AccessibilityFocusState private var selectedDepartureFocus: String?
private let cardFocusId = "_card"

var body: some View {
ZStack(alignment: .top) {
routeColor.ignoresSafeArea(.all)
Expand Down Expand Up @@ -89,8 +92,11 @@ struct StopDetailsFilteredDepartureDetails: View {
StopDetailsNoTripCard(
status: noPredictionsStatus,
accentColor: routeColor,
routeType: routeType
routeType: routeType,
hideMaps: stopDetailsVM.hideMaps
)
.accessibilityHeading(.h3)
.accessibilityFocused($selectedDepartureFocus, equals: cardFocusId)
} else if selectedTripIsCancelled {
StopDetailsIconCard(
accentColor: routeColor,
Expand All @@ -104,6 +110,8 @@ struct StopDetailsFilteredDepartureDetails: View {
),
icon: routeSlashIcon(routeType)
)
.accessibilityHeading(.h4)
.accessibilityFocused($selectedDepartureFocus, equals: cardFocusId)
} else {
TripDetailsView(
tripFilter: tripFilter,
Expand All @@ -121,6 +129,9 @@ struct StopDetailsFilteredDepartureDetails: View {
.onAppear { handleViewportForStatus(noPredictionsStatus) }
.onChange(of: noPredictionsStatus) { status in handleViewportForStatus(status) }
.onChange(of: selectedTripIsCancelled) { if $0 { setViewportToStop() } }
.onChange(of: tripFilter) { tripFilter in
selectedDepartureFocus = tiles.first { $0.upcoming?.trip.id == tripFilter?.tripId }?.id ?? cardFocusId
}
.ignoresSafeArea(.all)
}

Expand Down Expand Up @@ -170,7 +181,9 @@ struct StopDetailsFilteredDepartureDetails: View {
.line != nil ? .onPrediction(route: tileData.route) : .none,
showHeadsign: showTileHeadsigns,
isSelected: tileData.upcoming?.trip.id == tripFilter?.tripId
).padding(.horizontal, 4)
)
.accessibilityFocused($selectedDepartureFocus, equals: tileData.id)
.padding(.horizontal, 4)
}
}
.padding(.horizontal, 12)
Expand Down
16 changes: 4 additions & 12 deletions iosApp/iosApp/Pages/StopDetails/StopDetailsFilteredView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,28 +111,20 @@ struct StopDetailsFilteredView: View {
func toggledPinnedRoute() {
Task {
if let routeId = patternsByStop?.routeIdentifier {
do {
let pinned = try await stopDetailsVM.togglePinnedUsecase.execute(route: routeId).boolValue
analytics.toggledPinnedRouteAtStop(pinned: pinned, routeId: routeId)
stopDetailsVM.loadPinnedRoutes()
} catch is CancellationError {
// do nothing on cancellation
} catch {
// execute shouldn't actually fail
debugPrint(error)
}
let pinned = await stopDetailsVM.togglePinnedRoute(routeId)
analytics.toggledPinnedRouteAtStop(pinned: pinned, routeId: routeId)
stopDetailsVM.loadPinnedRoutes()
}
}
}

var body: some View {
VStack(spacing: 0) {
ZStack {
Color.fill2
Color.fill2.ignoresSafeArea(.all)
header
}
.fixedSize(horizontal: false, vertical: true)
.ignoresSafeArea(.all)

if let patternsByStop {
StopDetailsFilteredDepartureDetails(
Expand Down
2 changes: 2 additions & 0 deletions iosApp/iosApp/Pages/StopDetails/StopDetailsIconCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ struct StopDetailsIconCard<Header: View, Details: View>: View {
.frame(width: 35, height: 35)
.frame(width: 48, height: 48)
.foregroundStyle(accentColor)
.accessibilityHidden(true)
header
.font(Typography.title2Bold)
.foregroundStyle(Color.text)
.accessibilityAddTraits(.isHeader)
}.frame(maxWidth: .infinity, alignment: .leading)

if let details {
Expand Down
Loading

0 comments on commit 74fa418

Please sign in to comment.