Skip to content

Commit ab5cf3a

Browse files
Merge pull request #753 from opentripplanner/itinerary-a11y-grouping-labels
Grouping, Labels, and Headings in Itineraries
2 parents 22b3537 + 3111d00 commit ab5cf3a

File tree

6 files changed

+219
-126
lines changed

6 files changed

+219
-126
lines changed

Diff for: i18n/en-US.yml

+1
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ components:
206206
FormNavigationButtons:
207207
ariaLabel: Form navigation
208208
ItinerarySummary:
209+
itineraryDetails: Itinerary details
209210
minMaxFare: "{minTotalFare} - {maxTotalFare}"
210211
LiveStopTimes:
211212
autoRefresh: Auto-refresh arrivals?

Diff for: lib/components/narrative/default/default-itinerary.js

+78-41
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,26 @@ const LegIconWrapper = styled.div`
5252
}
5353
`
5454

55+
const DetailsHintButton = styled.button`
56+
&.visible {
57+
display: contents;
58+
}
59+
60+
/* the width/height/overflow trick still renders a tiny button,
61+
so make it invisible and out of the way, but still visible to
62+
screen readers */
63+
64+
background: transparent;
65+
border: none;
66+
height: 0;
67+
overflow: hidden;
68+
position: absolute;
69+
width: 0;
70+
`
5571
const DetailsHint = styled.div`
5672
clear: both;
5773
color: #685c5c;
74+
cursor: pointer;
5875
font-size: small;
5976
text-align: center;
6077
`
@@ -254,6 +271,21 @@ class DefaultItinerary extends NarrativeItinerary {
254271
.filter((number) => !!number)[0]
255272
}
256273

274+
const itineraryAttributeOptions = {
275+
co2Config,
276+
configCosts,
277+
currency,
278+
LegIcon
279+
}
280+
281+
const renderItineraryAttributes = (attribute) => {
282+
return attribute.render(
283+
itinerary,
284+
itineraryAttributeOptions,
285+
defaultFareKey
286+
)
287+
}
288+
257289
return (
258290
<div
259291
className={`option default-itin${active ? ' active' : ''}${
@@ -263,68 +295,73 @@ class DefaultItinerary extends NarrativeItinerary {
263295
onMouseLeave={this._onMouseLeave}
264296
role="presentation"
265297
>
266-
<button
298+
<ItinerarySummaryWrapper
267299
className="header"
268300
// _onHeaderClick comes from super component (NarrativeItinerary).
269301
onClick={this._onHeaderClick}
270302
>
271-
{/* FIXME: semantics - replace the divs with span (we are inside a button) */}
272-
<ItinerarySummaryWrapper>
273-
<div className="title">
303+
<div className="title">
304+
<h3>
274305
<ItineraryDescription intl={intl} itinerary={itinerary} />
275-
<ItinerarySummary itinerary={itinerary} LegIcon={LegIcon} />
276-
{itineraryHasAccessibilityScores(itinerary) && (
277-
<AccessibilityRating
278-
gradationMap={localizedGradationMapWithIcons}
279-
large
280-
score={getAccessibilityScoreForItinerary(itinerary)}
281-
/>
282-
)}
283-
</div>
284-
{isFlexItinerary && (
285-
<FlexIndicator
286-
isCallAhead={isCallAhead}
287-
isContinuousDropoff={isCoordinationRequired}
288-
phoneNumber={phone}
306+
</h3>
307+
<ItinerarySummary itinerary={itinerary} LegIcon={LegIcon} />
308+
{itineraryHasAccessibilityScores(itinerary) && (
309+
<AccessibilityRating
310+
gradationMap={localizedGradationMapWithIcons}
311+
large
312+
score={getAccessibilityScoreForItinerary(itinerary)}
289313
/>
290314
)}
291-
<ul className="list-unstyled itinerary-attributes">
292-
<FieldTripGroupSize itinerary={itinerary} />
293-
{ITINERARY_ATTRIBUTES.sort((a, b) => {
294-
const aSelected = this._isSortingOnAttribute(a)
295-
const bSelected = this._isSortingOnAttribute(b)
296-
if (aSelected) return -1
297-
if (bSelected) return 1
298-
return a.order - b.order
299-
}).map((attribute) => {
315+
</div>
316+
{isFlexItinerary && (
317+
<FlexIndicator
318+
isCallAhead={isCallAhead}
319+
isContinuousDropoff={isCoordinationRequired}
320+
phoneNumber={phone}
321+
/>
322+
)}
323+
<ul
324+
aria-label={intl.formatMessage({
325+
id: 'components.ItinerarySummary.itineraryDetails'
326+
})}
327+
className="list-unstyled itinerary-attributes"
328+
>
329+
<FieldTripGroupSize itinerary={itinerary} />
330+
{ITINERARY_ATTRIBUTES.sort((a, b) => {
331+
const aSelected = this._isSortingOnAttribute(a)
332+
const bSelected = this._isSortingOnAttribute(b)
333+
if (aSelected) return -1
334+
if (bSelected) return 1
335+
return a.order - b.order
336+
})
337+
.filter((x) => renderItineraryAttributes(x) !== undefined)
338+
.map((attribute) => {
300339
const isSelected = this._isSortingOnAttribute(attribute)
301-
const options = {
302-
co2Config,
303-
configCosts,
304-
currency,
305-
LegIcon
306-
}
307340
if (isSelected) {
308-
options.isSelected = true
309-
options.selection = this.props.sort.type
341+
itineraryAttributeOptions.isSelected = true
342+
itineraryAttributeOptions.selection = this.props.sort.type
310343
}
311344
return (
312345
<li
313346
className={`${attribute.id}${isSelected ? ' main' : ''}`}
314347
key={attribute.id}
315348
>
316-
{attribute.render(itinerary, options, defaultFareKey)}
349+
{renderItineraryAttributes(attribute)}
317350
</li>
318351
)
319352
})}
320-
</ul>
321-
</ItinerarySummaryWrapper>
322-
{active && !expanded && (
353+
</ul>
354+
</ItinerarySummaryWrapper>
355+
{!expanded && (
356+
<DetailsHintButton
357+
className={active && !expanded && 'visible'}
358+
onClick={this._onDirectClick}
359+
>
323360
<DetailsHint>
324361
<FormattedMessage id="components.DefaultItinerary.clickDetails" />
325362
</DetailsHint>
326-
)}
327-
</button>
363+
</DetailsHintButton>
364+
)}
328365
{active && expanded && (
329366
<>
330367
{showRealtimeAnnotation && <SimpleRealtimeAnnotation />}

Diff for: lib/components/narrative/default/itinerary.css

+5
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@
143143
font-size: large;
144144
}
145145

146+
.otp .option.default-itin > .header .title h3 {
147+
margin-top: 5px;
148+
margin-bottom: 20px;
149+
}
150+
146151
.otp .option.default-itin > .header .itinerary-attributes {
147152
float: right;
148153
min-width: 6em;

Diff for: lib/components/narrative/metro/departure-times-list.tsx

+14-7
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,19 @@ type DepartureTimesProps = {
2222
export const DepartureTimesList = (props: DepartureTimesProps): JSX.Element => {
2323
const { activeItineraryTimeIndex, itinerary, setItineraryTimeIndex } = props
2424
const intl = useIntl()
25+
const isRealTime = firstTransitLegIsRealtime(itinerary)
2526
if (!itinerary.allStartTimes) {
2627
return (
2728
<button
28-
className={
29-
firstTransitLegIsRealtime(itinerary) ? 'realtime active' : 'active'
30-
}
31-
title={intl.formatMessage(
29+
className={isRealTime ? 'realtime active' : 'active'}
30+
title={`${intl.formatMessage(
3231
{ id: 'components.MetroUI.arriveAtTime' },
3332
{ time: intl.formatTime(itinerary.endTime) }
34-
)}
33+
)} ${
34+
isRealTime
35+
? intl.formatMessage({ id: 'components.StopTimeCell.realtime' })
36+
: ''
37+
}`}
3538
>
3639
<FormattedTime value={itinerary.startTime} />
3740
</button>
@@ -55,10 +58,14 @@ export const DepartureTimesList = (props: DepartureTimesProps): JSX.Element => {
5558
className={classNames.join(' ')}
5659
key={getFirstLegStartTime(time.legs)}
5760
onClick={() => setItineraryTimeIndex(index)}
58-
title={intl.formatMessage(
61+
title={`${intl.formatMessage(
5962
{ id: 'components.MetroUI.arriveAtTime' },
6063
{ time: intl.formatTime(getLastLegEndTime(time.legs)) }
61-
)}
64+
)} ${
65+
time.realtime
66+
? intl.formatMessage({ id: 'components.StopTimeCell.realtime' })
67+
: ''
68+
}`}
6269
>
6370
<FormattedTime value={getFirstLegStartTime(time.legs)} />
6471
</button>

0 commit comments

Comments
 (0)