Skip to content

Commit

Permalink
Merge pull request #215 from department-of-veterans-affairs/feature/1…
Browse files Browse the repository at this point in the history
…83-roettger-AddAttachmentSupportToLink

[Feature] Link - Add attachment type support
  • Loading branch information
TimRoe authored Mar 1, 2024
2 parents 9f1eca0 + 14bbc1d commit e730499
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 132 deletions.
30 changes: 4 additions & 26 deletions packages/components/src/components/Link/Link.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,34 +55,12 @@ export const DefaultWithIcon: Story = {
},
}

const paragraphText: LinkProps['paragraphText'] = [
// @ts-ignore: TS being wrong and thinking all should be LinkProps and none normalText
{ text: 'A sentence may include a ' },
{
text: 'link that opens in the app',
type: 'custom',
onPress: () => {
null
},
a11yLabel: 'a11y override',
},
// @ts-ignore: TS being wrong and thinking all should be LinkProps and none normalText
{ text: ' or a ' },
{
text: 'link that opens in an external app',
type: 'url',
url: 'https://department-of-veterans-affairs.github.io/va-mobile-app/design/intro',
},
// @ts-ignore: TS being wrong and thinking all should be LinkProps and none normalText
{ text: '.' },
]

export const Inline: Story = {
export const _Attachment: Story = {
args: {
text: '',
text: 'Attachment.pdf',
onPress: undefined, // Storybook sends a truthy function shell otherwise
type: 'inline',
paragraphText: paragraphText,
type: 'attachment',
a11yValue: { index: 2, total: 5 },
},
}

Expand Down
142 changes: 36 additions & 106 deletions packages/components/src/components/Link/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,35 @@ import {
TextStyle,
View,
} from 'react-native'
import React, { FC, useState } from 'react'
import React, { FC } from 'react'

import {
FormDirectionsUrl,
LocationData,
isIOS,
leaveAppPromptText,
useExternalLink,
} from '../../utils/OSfunctions'
import { Icon, IconProps } from '../Icon/Icon'
import { useColorScheme, useIsScreenReaderEnabled } from '../../utils'
import { t } from 'i18next'
import { useColorScheme } from '../../utils'

// Convenience type to default type-specific props to not existing/being optional
type nullTypeSpecifics = {
calendarData?: never
locationData?: never
/** Optional onPress override logic */
onPress?: () => void
paragraphText?: never
phoneNumber?: never
textNumber?: never
TTYnumber?: never
url?: never
}

type attachment = Omit<nullTypeSpecifics, 'onPress'> & {
type: 'attachment'
/** Required onPress override logic */
onPress: () => void
}

type calendar = Omit<nullTypeSpecifics, 'calendarData'> & {
type: 'calendar'
/** Required onPress override logic */
Expand Down Expand Up @@ -60,16 +64,6 @@ type directions = Omit<nullTypeSpecifics, 'locationData'> & {
locationData: LocationData
}

type normalText = {
text: string
textA11y?: string
}

type inline = Omit<nullTypeSpecifics, 'paragraphText'> & {
type: 'inline'
paragraphText: normalText[] | LinkProps[]
}

type text = Omit<nullTypeSpecifics, 'textNumber'> & {
type: 'text'
textNumber: string
Expand All @@ -81,15 +75,21 @@ type url = Omit<nullTypeSpecifics, 'url'> & {
}

type linkTypes =
| attachment
| calendar
| call
| callTTY
| custom
| directions
| inline
| text
| url

type a11yValue = {
/** Index value of item in list, will have +1 added to */
index: number
total: number
}

export type LinkAnalytics = {
onPress?: () => void
onConfirm?: () => void
Expand All @@ -107,14 +107,12 @@ export type LinkProps = linkTypes & {
a11yLabel?: string
/** Optional a11yHint to provide additional context */
a11yHint?: string
/** Optional a11yValue for "[position #] of [list total #]" or a custom value descriptive string */
a11yValue?: a11yValue | string
/** Optional override text for leaving app confirmation prompt */
promptText?: leaveAppPromptText
/** Optional analytics event logging */
analytics?: LinkAnalytics
/** Internally used by 'inline' type. Not recommended for consumer use, but
* available to manually insert a link into a paragraph. True builds link
* component with RN Text instead of Pressable for improved wrapping behavior */
inlineSingle?: boolean
/** Optional TestID */
testID?: string
}
Expand All @@ -128,13 +126,12 @@ export const Link: FC<LinkProps> = ({
icon,
a11yLabel,
a11yHint,
a11yValue,
promptText,
analytics,
inlineSingle,
testID,
// Type-specific props
locationData,
paragraphText,
phoneNumber,
textNumber,
TTYnumber,
Expand All @@ -155,6 +152,10 @@ export const Link: FC<LinkProps> = ({
}

switch (type) {
case 'attachment':
icon = icon ? icon : { name: 'PaperClip' }
_onPress = customOnPress
break
case 'calendar':
icon = icon ? icon : { name: 'Calendar' }
_onPress = customOnPress
Expand Down Expand Up @@ -182,8 +183,6 @@ export const Link: FC<LinkProps> = ({
launchExternalLink(directions, analytics, promptText)
}
break
case 'inline':
return <InlineLink paragraphText={paragraphText} />
case 'text':
icon = icon ? icon : { name: 'Text' }
_onPress = async (): Promise<void> => {
Expand Down Expand Up @@ -214,21 +213,26 @@ export const Link: FC<LinkProps> = ({
}

const iconDisplay =
icon === 'no icon' ? null : inlineSingle ? (
<>
<Icon fill={linkColor} {...icon} />
{/* Space forms padding prior to link text */}
<Text> </Text>
</>
) : (
icon === 'no icon' ? null : (
<View style={{ marginRight: 5 }}>
<Icon fill={linkColor} {...icon} />
</View>
)

let ariaValue
if (typeof a11yValue === 'string') {
ariaValue = a11yValue
} else if (a11yValue) {
ariaValue = t('listPosition', {
position: a11yValue.index + 1,
total: a11yValue.total,
})
}

const a11yProps: TextProps = {
'aria-label': a11yLabel,
'aria-label': a11yLabel || text, // or text for Android not reading text if aria-value set
accessibilityHint: a11yHint,
'aria-valuetext': ariaValue,
role: 'link',
accessible: true,
}
Expand Down Expand Up @@ -265,27 +269,6 @@ export const Link: FC<LinkProps> = ({
return { ...(pressed ? pressedFont : regularFont), ...textStyle }
}

const [pressStyle, setPressStyle] = useState(false)
if (inlineSingle) {
const onPressProps: TextProps = {
onPressIn: () => {
setPressStyle(true)
},
onPress: onPress ? onPress : _onPress,
onPressOut: () => {
setPressStyle(false)
},
}
return (
<Text>
{iconDisplay}
<Text {...onPressProps} {...a11yProps} style={getTextStyle(pressStyle)}>
{text}
</Text>
</Text>
)
}

return (
<Pressable {...pressableProps} testID={testID}>
{({ pressed }: PressableStateCallbackType) => (
Expand All @@ -297,56 +280,3 @@ export const Link: FC<LinkProps> = ({
</Pressable>
)
}

const ParagraphText: FC<normalText> = ({ text, textA11y }) => {
const colorScheme = useColorScheme()
const isDarkMode = colorScheme === 'dark'

// TODO: Replace with typography tokens
const regularFont: TextStyle = {
fontFamily: 'SourceSansPro-Regular',
fontSize: 20,
lineHeight: 30,
color: isDarkMode ? Colors.grayLightest : Colors.grayDark,
}

return (
<Text style={regularFont} accessible={true} aria-label={textA11y}>
{text}
</Text>
)
}

const InlineLink: FC<Pick<inline, 'paragraphText'>> = ({ paragraphText }) => {
const screenReaderEnabled = useIsScreenReaderEnabled()
if (screenReaderEnabled && isIOS) {
return (
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
{paragraphText.map((item, index) => {
// key included as this is a list of React components and the renderer worries about losing track
if ('type' in item) {
// Link if type prop exists
item.inlineSingle = undefined
return <Link {...item} key={index} />
} else {
return <ParagraphText {...item} key={index} />
}
})}
</View>
)
}
return (
<Text>
{paragraphText.map((item, index) => {
// key included as this is a list of React components and the renderer worries about losing track
if ('type' in item) {
// Link if type prop exists
item.inlineSingle = true
return <Link {...item} key={index} />
} else {
return <ParagraphText {...item} key={index} />
}
})}
</Text>
)
}

0 comments on commit e730499

Please sign in to comment.