Skip to content

Commit f230ae9

Browse files
Merge pull request #859 from opentripplanner/external-link-icons
Clarify Links That Open in New Window
2 parents bfe3aa3 + 12d3812 commit f230ae9

File tree

7 files changed

+190
-98
lines changed

7 files changed

+190
-98
lines changed

i18n/en-US.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ common:
153153
tripDurationFormat: >-
154154
{hours, plural, =0 {} other {# hr }}{minutes} min { seconds, plural, =0 {}
155155
other {# sec}}
156+
linkOpensNewWindow: "(Opens new window)"
156157
components:
157158
A11yPrefs:
158159
accessibilityRoutingByDefault: Prefer accessible trips by default

lib/components/admin/field-trip-details.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
TICKET_TYPES
2929
} from '../../util/call-taker'
3030
import { IconWithText, StyledIconWrapper } from '../util/styledIcon'
31+
import { NewWindowIconA11y } from '../util/externalLink'
3132

3233
import {
3334
Bold,
@@ -98,6 +99,9 @@ class FieldTripDetails extends Component {
9899
const { request, sessionId } = this.props
99100
const cancelled = request.status === 'cancelled'
100101
const printFieldTripLink = `/#/printFieldTrip/?requestId=${request.id}&sessionId=${sessionId}`
102+
const StyledNewWindowIcon = () => (
103+
<NewWindowIconA11y size={10} style={{ margin: '-3px 0 0 4px' }} />
104+
)
101105
return (
102106
<div style={{ padding: '5px 10px 0px 10px' }}>
103107
<DropdownButton
@@ -112,12 +116,15 @@ class FieldTripDetails extends Component {
112116
target="_blank"
113117
>
114118
<IconWithText Icon={CommentDots}>Feedback link</IconWithText>
119+
<StyledNewWindowIcon />
115120
</MenuItem>
116121
<MenuItem href={this._getRequestLink('receipt')} target="_blank">
117122
<IconWithText Icon={FileAlt}>Receipt link</IconWithText>
123+
<StyledNewWindowIcon />
118124
</MenuItem>
119125
<MenuItem href={printFieldTripLink} target="_blank">
120126
<IconWithText Icon={Print}>Printable trip plan</IconWithText>
127+
<StyledNewWindowIcon />
121128
</MenuItem>
122129
</DropdownButton>
123130
<Button

lib/components/admin/field-trip-list.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { IconWithText, StyledIconWrapper } from '../util/styledIcon'
1515
import Loading from '../narrative/loading'
1616

1717
import { FieldTripRecordButton, WindowHeader } from './styled'
18+
import { LinkOpensNewWindow } from '../util/externalLink'
1819
import DraggableWindow from './draggable-window'
1920
import FieldTripStatusIcon from './field-trip-status-icon'
2021

@@ -185,8 +186,12 @@ class FieldTripList extends Component {
185186
type="date"
186187
value={date}
187188
/>
188-
<Button bsSize="xsmall" href={this._getReportUrl()} target="_blank">
189-
View report
189+
<Button bsSize="xsmall">
190+
<LinkOpensNewWindow
191+
contents="View report"
192+
style={{ textDecoration: 'none' }}
193+
url={this._getReportUrl()}
194+
/>
190195
</Button>
191196
</>
192197
}

lib/components/user/terms-of-use-pane.js

Lines changed: 0 additions & 82 deletions
This file was deleted.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Checkbox, ControlLabel, FormGroup } from 'react-bootstrap'
2+
import { connect } from 'react-redux'
3+
import { FormattedMessage, useIntl } from 'react-intl'
4+
import React, { FormEventHandler } from 'react'
5+
6+
import { LinkOpensNewWindow } from '../util/externalLink'
7+
import {
8+
TERMS_OF_SERVICE_PATH,
9+
TERMS_OF_STORAGE_PATH
10+
} from '../../util/constants'
11+
12+
/**
13+
* User terms of use pane.
14+
*/
15+
const TermsOfUsePane = ({
16+
disableCheckTerms,
17+
handleBlur,
18+
handleChange,
19+
values: userData
20+
}: {
21+
disableCheckTerms: boolean
22+
handleBlur: () => void
23+
handleChange: FormEventHandler<Checkbox>
24+
values: {
25+
hasConsentedToTerms: boolean
26+
storeTripHistory: boolean
27+
}
28+
}) => {
29+
const intl = useIntl()
30+
const { hasConsentedToTerms, storeTripHistory } = userData
31+
32+
return (
33+
<div>
34+
<ControlLabel>
35+
<FormattedMessage id="components.TermsOfUsePane.mustAgreeToTerms" />
36+
</ControlLabel>
37+
<FormGroup>
38+
<Checkbox
39+
checked={hasConsentedToTerms}
40+
disabled={disableCheckTerms}
41+
name="hasConsentedToTerms"
42+
onBlur={disableCheckTerms ? undefined : handleBlur}
43+
onChange={disableCheckTerms ? undefined : handleChange}
44+
>
45+
<FormattedMessage
46+
id="components.TermsOfUsePane.termsOfServiceStatement"
47+
values={{
48+
termsOfUseLink: (contents: JSX.Element) => (
49+
<LinkOpensNewWindow
50+
contents={contents}
51+
inline
52+
url={`/#${TERMS_OF_SERVICE_PATH}`}
53+
/>
54+
)
55+
}}
56+
/>
57+
</Checkbox>
58+
</FormGroup>
59+
<FormGroup>
60+
<Checkbox
61+
checked={storeTripHistory}
62+
name="storeTripHistory"
63+
onBlur={handleBlur}
64+
onChange={(e) => {
65+
// Show alert when user is unchecking the checkbox
66+
if (storeTripHistory) {
67+
// Do nothing if the user hits cancel
68+
if (
69+
// eslint-disable-next-line no-restricted-globals
70+
!confirm(
71+
intl.formatMessage({
72+
id: 'components.TermsOfUsePane.confirmDeletionPrompt'
73+
})
74+
)
75+
) {
76+
return
77+
}
78+
}
79+
80+
handleChange(e)
81+
}}
82+
>
83+
<FormattedMessage
84+
id="components.TermsOfUsePane.termsOfStorageStatement"
85+
values={{
86+
termsOfStorageLink: (contents: JSX.Element) => (
87+
<LinkOpensNewWindow
88+
contents={contents}
89+
inline
90+
url={`/#${TERMS_OF_STORAGE_PATH}`}
91+
/>
92+
)
93+
}}
94+
/>
95+
</Checkbox>
96+
</FormGroup>
97+
</div>
98+
)
99+
}
100+
const mapStateToProps = (state: any) => {
101+
return {
102+
termsOfStorageSet: state.otp.config.persistence?.terms_of_storage
103+
}
104+
}
105+
export default connect(mapStateToProps)(TermsOfUsePane)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { ExternalLinkAlt } from '@styled-icons/fa-solid'
2+
import { useIntl } from 'react-intl'
3+
import React, { HTMLAttributes } from 'react'
4+
5+
interface LinkProps extends HTMLAttributes<HTMLElement> {
6+
contents: JSX.Element | string
7+
gap?: number
8+
inline?: boolean
9+
size?: number
10+
style?: React.CSSProperties
11+
url: string
12+
}
13+
14+
interface IconProps extends HTMLAttributes<HTMLElement> {
15+
size?: number
16+
style?: React.CSSProperties
17+
}
18+
19+
export const NewWindowIconA11y = ({
20+
size = 14,
21+
style
22+
}: IconProps): JSX.Element => {
23+
const intl = useIntl()
24+
return (
25+
<ExternalLinkAlt
26+
/* the "title" prop removes aria-hidden from styled-icons, so use the title as the aria-label as well
27+
https://github.com/styled-icons/styled-icons#accessibility */
28+
aria-labelledby="title"
29+
height={size}
30+
style={style}
31+
title={intl.formatMessage({ id: 'common.linkOpensNewWindow' })}
32+
/>
33+
)
34+
}
35+
36+
export const LinkOpensNewWindow = ({
37+
contents,
38+
gap = 6,
39+
inline,
40+
size = 14,
41+
style,
42+
url
43+
}: LinkProps): JSX.Element => {
44+
return (
45+
<a
46+
href={url}
47+
rel="noreferrer"
48+
style={{
49+
alignItems: 'center',
50+
display: `${inline ? 'inline-flex' : 'flex'}`,
51+
flexDirection: 'row',
52+
gap,
53+
whiteSpace: 'nowrap',
54+
...style
55+
}}
56+
target="_blank"
57+
>
58+
{contents}
59+
<NewWindowIconA11y size={size} />
60+
</a>
61+
)
62+
}

lib/components/viewers/route-details.js

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { connect } from 'react-redux'
44
import { FormattedMessage, injectIntl } from 'react-intl'
55
import { getMostReadableTextColor } from '@opentripplanner/core-utils/lib/route'
6-
import { Link } from '@styled-icons/fa-solid/Link'
6+
import { LinkOpensNewWindow } from '../util/externalLink'
77
import PropTypes from 'prop-types'
88
import React, { Component } from 'react'
99

@@ -14,7 +14,6 @@ import {
1414
import { findStopsForPattern } from '../../actions/api'
1515
import { getOperatorName } from '../../util/state'
1616
import { setHoveredStop, setViewedRoute, setViewedStop } from '../../actions/ui'
17-
import { StyledIconWrapper } from '../util/styledIcon'
1817

1918
import {
2019
Container,
@@ -145,20 +144,15 @@ class RouteDetails extends Component {
145144
</>
146145
)}
147146
{url && (
148-
<a
149-
href={url}
150-
rel="noreferrer"
147+
<LinkOpensNewWindow
148+
contents={
149+
<FormattedMessage id="components.RouteDetails.moreDetails" />
150+
}
151151
style={{
152-
color: getMostReadableTextColor(routeColor, route?.textColor),
153-
whiteSpace: 'nowrap'
152+
color: getMostReadableTextColor(routeColor, route?.textColor)
154153
}}
155-
target="_blank"
156-
>
157-
<StyledIconWrapper>
158-
<Link />
159-
</StyledIconWrapper>
160-
<FormattedMessage id="components.RouteDetails.moreDetails" />
161-
</a>
154+
url={url}
155+
/>
162156
)}
163157
</LogoLinkContainer>
164158
</RouteNameContainer>

0 commit comments

Comments
 (0)