From 27e2a7a5740a905a690cbc6a289f2f06125cc7bd Mon Sep 17 00:00:00 2001 From: Tim R Date: Tue, 30 Jan 2024 15:15:30 -0600 Subject: [PATCH 01/14] Committing off progress, need heavy rework --- .../components/src/components/Link/Link.tsx | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 packages/components/src/components/Link/Link.tsx diff --git a/packages/components/src/components/Link/Link.tsx b/packages/components/src/components/Link/Link.tsx new file mode 100644 index 00000000..b82b0667 --- /dev/null +++ b/packages/components/src/components/Link/Link.tsx @@ -0,0 +1,71 @@ +import { AccessibilityProps, TouchableWithoutFeedback, TouchableWithoutFeedbackProps } from 'react-native' +import { SvgProps } from 'react-native-svg' +import Box from './Box' +import React, { FC } from 'react' + +import { Icon, IconProps } from '../Icon/Icon' +import { useTheme } from 'utils/hooks' +import TextView, { ColorVariant, TextViewProps } from './TextView' + +/** + * Signifies the props that need to be passed in to {@link ClickForActionLink} + */ +export type LinkButtonProps = AccessibilityProps & { + /** phone number or text for url that is displayed to the user, may be different than actual number or url used */ + text: string + + /** */ + onPress: () => void + + /** */ + icon?: IconProps + + /** Accessibility label for the link, mandatory for every element with a link role */ + a11yLabel: string + + /** Optional TestID */ + testID?: string +} + +/** + * Reusable component used for opening native calling app, texting app, or opening a url in the browser + */ +export const Link: FC = ({ + text, + onPress, + icon, + a11yLabel, + testID, + ...props +}) => { + const theme = useTheme() + + const textViewProps: TextViewProps = { + color: 'link', + variant: 'MobileBody', + ml: 4, + textDecoration: 'underline', + textDecorationColor: 'link', + } + + const pressableProps: TouchableWithoutFeedbackProps = { + onPress, + accessibilityLabel: a11yLabel, + accessibilityRole: 'link', + accessible: true, + ...props, + } + + // ON PRESS ASYNC, DOES THAT NEED ANY SPECIAL HANDLING!? + + return ( + + + {icon ? : null} + + {text} + + + + ) +} From c1e1872c1b5456355ffc19cecad5d9e18354b177 Mon Sep 17 00:00:00 2001 From: Tim R Date: Thu, 1 Feb 2024 12:46:24 -0600 Subject: [PATCH 02/14] Building out props model for input data --- .../components/src/components/Link/Link.tsx | 128 ++++++++++++++---- 1 file changed, 104 insertions(+), 24 deletions(-) diff --git a/packages/components/src/components/Link/Link.tsx b/packages/components/src/components/Link/Link.tsx index b82b0667..b421c82b 100644 --- a/packages/components/src/components/Link/Link.tsx +++ b/packages/components/src/components/Link/Link.tsx @@ -1,28 +1,108 @@ import { AccessibilityProps, TouchableWithoutFeedback, TouchableWithoutFeedbackProps } from 'react-native' import { SvgProps } from 'react-native-svg' -import Box from './Box' +// import Box from './Box' import React, { FC } from 'react' import { Icon, IconProps } from '../Icon/Icon' -import { useTheme } from 'utils/hooks' -import TextView, { ColorVariant, TextViewProps } from './TextView' +// import { useTheme } from 'utils/hooks' +// import TextView, { ColorVariant, TextViewProps } from './TextView' -/** - * Signifies the props that need to be passed in to {@link ClickForActionLink} - */ -export type LinkButtonProps = AccessibilityProps & { - /** phone number or text for url that is displayed to the user, may be different than actual number or url used */ - text: string +type CalendarData = { + title: string + startTime: number + endTime: number + location: string + latitude: number + longitude: number +} - /** */ - onPress: () => void +type calendar = { + type: 'calendar' + eventData: CalendarData +} - /** */ - icon?: IconProps +type call = { + type: 'call' | 'call TTY' + phoneNumber: string +} - /** Accessibility label for the link, mandatory for every element with a link role */ - a11yLabel: string +type chat = { + type: 'chat' + url: string +} +type custom = { + type: 'custom' + onPress: onPress +} + +type AppointmentAddress = { + street: string + city: string + state: string // 2 letter abbreviation + zipCode: string +} + +type LocationData = { + name: string + address?: AppointmentAddress + lat?: number + long?: number +} + +type directions = { + type: 'directions' + locationData: LocationData +} + +type text = { + type: 'text' + textNumber: string +} + +type url = { + type: 'url' + url: string +} + +type linkType = calendar + | call + | chat + | custom + | directions + | text + | url + +type onPress = { + /** Custom logic to overrides built in onPress logic */ + custom?: () => void +} + +type analytics = { + onPress?: () => void + onConfirm?: () => void + hasCalendarPermission?: () => void + onRequestCalendarPermission?: () => void + onCalendarPermissionSuccess?: () => void + onCalendarPermissionFailure?: () => void +} + +/** + * Signifies the props that need to be passed in to {@link ClickForActionLink} + */ +export type LinkProps = { + /** Display text for the link */ + text: string + /** Preset link types that include default icons and onPress behavior */ + type: linkType + /** Optional onPress logic */ + onPress?: onPress + /** Optional icon override */ + icon?: IconProps | 'no icon' + /** Optional a11yLabel override; should be used for phone numbers */ + a11yLabel?: string + /** Optional analytics event logging */ + analytics?: analytics /** Optional TestID */ testID?: string } @@ -30,17 +110,18 @@ export type LinkButtonProps = AccessibilityProps & { /** * Reusable component used for opening native calling app, texting app, or opening a url in the browser */ -export const Link: FC = ({ +export const Link: FC = ({ text, + type, onPress, icon, a11yLabel, - testID, - ...props + analytics, + testID }) => { - const theme = useTheme() + // const theme = useTheme() - const textViewProps: TextViewProps = { + const textViewProps = { color: 'link', variant: 'MobileBody', ml: 4, @@ -49,23 +130,22 @@ export const Link: FC = ({ } const pressableProps: TouchableWithoutFeedbackProps = { - onPress, + onPress: onPress?.custom, accessibilityLabel: a11yLabel, accessibilityRole: 'link', accessible: true, - ...props, } // ON PRESS ASYNC, DOES THAT NEED ANY SPECIAL HANDLING!? return ( - + {/* {icon ? : null} {text} - + */} ) } From 795e900fb7ce247e7d2b9295cddfc4a43d9abeb0 Mon Sep 17 00:00:00 2001 From: Tim R Date: Fri, 2 Feb 2024 16:46:48 -0600 Subject: [PATCH 03/14] Significant progress on changes --- .../.storybook/native/storybook.requires.js | 1 + .../src/components/Link/Link.stories.tsx | 112 +++++++++ .../components/src/components/Link/Link.tsx | 222 +++++++++++++----- packages/components/src/index.tsx | 1 + packages/components/src/utils/OSfunctions.tsx | 148 ++++++++++++ .../src/utils/nativeModules/RNCalendar.kt | 60 +++++ 6 files changed, 485 insertions(+), 59 deletions(-) create mode 100644 packages/components/src/components/Link/Link.stories.tsx create mode 100644 packages/components/src/utils/OSfunctions.tsx create mode 100644 packages/components/src/utils/nativeModules/RNCalendar.kt diff --git a/packages/components/.storybook/native/storybook.requires.js b/packages/components/.storybook/native/storybook.requires.js index 45c926f7..8c93dceb 100644 --- a/packages/components/.storybook/native/storybook.requires.js +++ b/packages/components/.storybook/native/storybook.requires.js @@ -56,6 +56,7 @@ const getStories = () => { return { "./src/components/Button/Button.stories.tsx": require("../../src/components/Button/Button.stories.tsx"), "./src/components/Icon/Icon.stories.tsx": require("../../src/components/Icon/Icon.stories.tsx"), + "./src/components/Link/Link.stories.tsx": require("../../src/components/Link/Link.stories.tsx"), "./src/components/SegmentedControl/SegmentedControl.stories.tsx": require("../../src/components/SegmentedControl/SegmentedControl.stories.tsx"), }; }; diff --git a/packages/components/src/components/Link/Link.stories.tsx b/packages/components/src/components/Link/Link.stories.tsx new file mode 100644 index 00000000..83f158e1 --- /dev/null +++ b/packages/components/src/components/Link/Link.stories.tsx @@ -0,0 +1,112 @@ +import { Meta, StoryObj } from '@storybook/react-native' +import { Platform, View } from 'react-native' +import React from 'react' + +import { Link, LinkProps } from './Link' +import { generateDocs } from '../../utils/storybook' + +const meta: Meta = { + title: 'Link', + component: Link, + argTypes: { + onPress: { + action: 'onPress event', + }, + }, + parameters: { + docs: generateDocs({ + name: 'Link', + docUrl: + 'https://department-of-veterans-affairs.github.io/va-mobile-app/design/Components/Buttons%20and%20links/Link', + }), + }, + decorators: [ + (Story) => ( + + + + ), + ], +} + +export default meta + +type Story = StoryObj + +const startTime = new Date() +const endTime = new Date(startTime.setMinutes(startTime.getMinutes() + 30)) +const location = { + "id": "983GC", + "lat": 40.553875, + "long": -105.08795, + "name":"Fort Collins VA Clinic", + "address": { + "street": "2509 Research Boulevard", + "city": "Fort Collins", + "state": "CO", + "zipCode": "80526-8108" + } +} +const getLocation = (): string => { + const { lat, long, name, address } = location + if (Platform.OS === 'ios' && lat && long) { + return name || '' + } else if (address?.street && address?.city && address?.state && address?.zipCode) { + return `${address.street} ${address.city}, ${address.state} ${address.zipCode}` + } else { + return name || '' + } +} + +export const Calendar: Story = { + storyName: 'Calendar', + args: { + text: 'Button text', + type: {type: 'calendar', calendarData: { + title: 'Test', + startTime: startTime.getTime(), + endTime: endTime.getTime(), + location: getLocation(), + latitude: location.lat, + longitude: location.long + }} + // a11yLabel: 'Alternate a11y text', + }, +} + +export const Phone: Story = { + args: { + text: 'Call number', + type: {type: 'call', phoneNumber: '555'} + // a11yLabel: 'Alternate a11y text', + }, +} + +export const PhoneTTY: Story = { + args: { + text: 'Call TTY number', + type: {type: 'call TTY', TTYnumber: '711'} + // a11yLabel: 'Alternate a11y text', + }, +} +export const Text: Story = { + args: { + text: 'Text SMS number', + type: {type: 'text', textNumber: '55555'} + // a11yLabel: 'Alternate a11y text', + }, +} + +export const URL: Story = { + args: { + text: 'External link', + type: {type: 'url', url: 'https://www.va.gov/'} + // a11yLabel: 'Alternate a11y text', + }, +} \ No newline at end of file diff --git a/packages/components/src/components/Link/Link.tsx b/packages/components/src/components/Link/Link.tsx index b421c82b..d5fc9b70 100644 --- a/packages/components/src/components/Link/Link.tsx +++ b/packages/components/src/components/Link/Link.tsx @@ -1,58 +1,73 @@ -import { AccessibilityProps, TouchableWithoutFeedback, TouchableWithoutFeedbackProps } from 'react-native' -import { SvgProps } from 'react-native-svg' -// import Box from './Box' +import * as Colors from '@department-of-veterans-affairs/mobile-tokens' +import { + Pressable, + PressableProps, + PressableStateCallbackType, + Text, + TextStyle, + View, + ViewProps, + useColorScheme, +} from 'react-native' import React, { FC } from 'react' +import { + CalendarData, + onPressCalendar, + useExternalLink, +} from '../../utils/OSfunctions' import { Icon, IconProps } from '../Icon/Icon' -// import { useTheme } from 'utils/hooks' -// import TextView, { ColorVariant, TextViewProps } from './TextView' - -type CalendarData = { - title: string - startTime: number - endTime: number - location: string - latitude: number - longitude: number -} +import { webStorybookColorScheme } from '../../utils' type calendar = { type: 'calendar' - eventData: CalendarData + calendarData: CalendarData } type call = { - type: 'call' | 'call TTY' + type: 'call' phoneNumber: string } -type chat = { - type: 'chat' - url: string +type callTTY = { + type: 'call TTY' + TTYnumber: string } type custom = { type: 'custom' - onPress: onPress + onPress: () => void } -type AppointmentAddress = { +type appointmentAddress = { street: string city: string state: string // 2 letter abbreviation zipCode: string } -type LocationData = { +type locationData = { name: string - address?: AppointmentAddress + address?: appointmentAddress lat?: number long?: number } type directions = { type: 'directions' - locationData: LocationData + locationData: locationData +} + +type normalText = { + text: string + textA11y: string +} + +// Split to separate ticket, see lines 373-390 for app code: +// src/screens/BenefitsScreen/ClaimsScreen/AppealDetailsScreen/AppealStatus/AppealCurrentStatus/AppealCurrentStatus.tsx +type inLineLink = { + type: 'in line link' + paragraphText: normalText[] | LinkProps[] } type text = { @@ -65,18 +80,7 @@ type url = { url: string } -type linkType = calendar - | call - | chat - | custom - | directions - | text - | url - -type onPress = { - /** Custom logic to overrides built in onPress logic */ - custom?: () => void -} +type linkType = calendar | call | callTTY | custom | directions | text | url type analytics = { onPress?: () => void @@ -95,9 +99,11 @@ export type LinkProps = { text: string /** Preset link types that include default icons and onPress behavior */ type: linkType + /** */ + variant?: 'default' | 'base' /** Optional onPress logic */ - onPress?: onPress - /** Optional icon override */ + onPress?: () => void | ((data: string | CalendarData | locationData) => void) + /** Optional icon override, sized by default to 24x24 */ icon?: IconProps | 'no icon' /** Optional a11yLabel override; should be used for phone numbers */ a11yLabel?: string @@ -113,39 +119,137 @@ export type LinkProps = { export const Link: FC = ({ text, type, + variant = 'default', onPress, icon, a11yLabel, analytics, - testID + testID, }) => { - // const theme = useTheme() - - const textViewProps = { - color: 'link', - variant: 'MobileBody', - ml: 4, - textDecoration: 'underline', - textDecorationColor: 'link', + const colorScheme = webStorybookColorScheme() || useColorScheme() + const isDarkMode = colorScheme === 'dark' + const launchExternalLink = useExternalLink() + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let _onPress: (() => void) | (() => Promise) | any = () => { + return + } + + switch (type.type) { + case 'calendar': + icon = icon ? icon : { name: 'Calendar' } + console.log('calendar switch') + _onPress = async (): Promise => { + await onPressCalendar(type.calendarData) + return + } + break + case 'call': + icon = icon ? icon : { name: 'Phone' } + _onPress = async (): Promise => { + launchExternalLink(`tel:${type.phoneNumber}`) + } + break + case 'call TTY': + icon = icon ? icon : { name: 'PhoneTTY' } + _onPress = async (): Promise => { + launchExternalLink(`tel:${type.TTYnumber}`) + } + break + case 'custom': + icon = icon ? icon : 'no icon' + onPress = type.onPress + break + case 'directions': + icon = icon ? icon : { name: 'Directions' } + _onPress = () => { + null + } + break + case 'text': + icon = icon ? icon : { name: 'Text' } + _onPress = async (): Promise => { + launchExternalLink(`sms:${type.textNumber}`) + } + break + case 'url': + icon = icon ? icon : { name: 'ExternalLink' } + _onPress = async (): Promise => { + launchExternalLink(type.url) + } + break } - const pressableProps: TouchableWithoutFeedbackProps = { - onPress: onPress?.custom, - accessibilityLabel: a11yLabel, - accessibilityRole: 'link', + if (icon !== 'no icon' && (!icon.height || !icon.width)) { + icon.height = 24 + icon.width = 24 + } + + let linkColor: string + + switch (variant) { + case 'base': + linkColor = isDarkMode ? Colors.colorGrayLightest : Colors.colorGrayMedium + break + default: + linkColor = isDarkMode + ? Colors.colorUswdsSystemColorBlueVivid30 + : Colors.colorUswdsSystemColorBlueVivid60 + } + + const pressableProps: PressableProps = { + onPress: _onPress ? _onPress : onPress, + 'aria-label': a11yLabel, + role: 'link', accessible: true, } + const viewStyle: ViewProps['style'] = { + alignItems: 'center', + flexDirection: 'row', + } + + const innerViewStyle: ViewProps['style'] = { + flexShrink: 1, + marginLeft: icon === 'no icon' ? 0 : 5, + } + + const getTextStyle = (pressed: boolean): TextStyle => { + // TODO: Replace with typography tokens + const regularFont: TextStyle = { + fontFamily: 'SourceSansPro-Regular', + fontSize: 20, + lineHeight: 30, + } + const pressedFont: TextStyle = { + fontFamily: 'SourceSansPro-Bold', + fontSize: 20, + lineHeight: 30, + } + + // ON PRESS STYLE BUGGED ON IOS (STORYBOOK) BUT WORKS ON WEB/ANDROID + + const textStyle: TextStyle = { + color: linkColor, + textDecorationColor: linkColor, + textDecorationLine: 'underline', + } + + return { ...(pressed ? pressedFont : regularFont), ...textStyle } + } + // ON PRESS ASYNC, DOES THAT NEED ANY SPECIAL HANDLING!? return ( - - {/* - {icon ? : null} - - {text} - - */} - + + {({ pressed }: PressableStateCallbackType) => ( + + {icon === 'no icon' ? null : } + + {text} + + + )} + ) } diff --git a/packages/components/src/index.tsx b/packages/components/src/index.tsx index c304d6a4..c8dfa04a 100644 --- a/packages/components/src/index.tsx +++ b/packages/components/src/index.tsx @@ -9,4 +9,5 @@ if (expoApp && App.initiateExpo) { // Export components here so they are exported through npm export { Button, ButtonVariants } from './components/Button/Button' export { Icon } from './components/Icon/Icon' +export { Link } from './components/Link/Link' export { SegmentedControl } from './components/SegmentedControl/SegmentedControl' diff --git a/packages/components/src/utils/OSfunctions.tsx b/packages/components/src/utils/OSfunctions.tsx new file mode 100644 index 00000000..5ff7edf6 --- /dev/null +++ b/packages/components/src/utils/OSfunctions.tsx @@ -0,0 +1,148 @@ +/** A file for functions that leverage OS functionality */ + +import { Alert, Linking, NativeModules, PermissionsAndroid, Platform } from 'react-native' +// import { logNonFatalErrorToFirebase } from './analytics' + +const isIOS = Platform.OS === 'ios' + +/** + * Hook to display a warning that the user is leaving the app when tapping an external link + * + * @returns an alert showing user they are leaving the app + */ +export function useExternalLink(): (url: string) => void { + // const { t } = useTranslation(NAMESPACE.COMMON) + + return (url: string) => { + // logAnalyticsEvent(Events.vama_link_click({ url, ...eventParams })) + + const onOKPress = () => { + // logAnalyticsEvent(Events.vama_link_confirm({ url, ...eventParams })) + return Linking.openURL(url) + } + + if (url.startsWith('http')) { + // Alert.alert(t('leavingApp.title'), t('leavingApp.body'), [ + Alert.alert("You’re leaving the app", "You're navigating to a website outside of the app.", [ + { + text: 'Cancel', + // style: 'cancel', + }, + { text: 'Ok', onPress: (): Promise => onOKPress(), style: 'default' }, + ]) + } else { + Linking.openURL(url) + } + } +} + +// Calendar bridge from iOS and Android +const RNCal = NativeModules.RNCalendar + +export type CalendarData = { + title: string + startTime: number + endTime: number + location: string + latitude: number + longitude: number +} + +export const onPressCalendar = async (calendarData: CalendarData): Promise => { + // const { title, endTime, startTime, location, latitude, longitude } = calendarData + + // console.log('test') + // let hasPermission = await checkCalendarPermission() + // if (!hasPermission) { + // hasPermission = await requestCalendarPermission() + // } + + // if (hasPermission) { + // await addToCalendar(title, startTime, endTime, location, latitude, longitude) + // } +} + +// /** +// * This function adds requests the device add a date to the native calendar and display it to the user. +// * @param title - The title or name of the event to add to the calendar. +// * @param beginTime - The number of seconds UTC from 1970 when the event will start. +// * @param endTime - The number of seconds UTC from 1970 when the event will end. +// * @param location - The address or name of place where the event will take place. +// * @param latitude - iOS only: Latitude of place where the event will take place. +// * @param longitude - iOS only: Longitude of place where the event will take place. +// * @returns Returns an empty Promise +// */ +// const addToCalendar = async ( +// title: string, +// beginTime: number, +// endTime: number, +// location: string, +// latitude: number, +// longitude: number, +// ): Promise => { +// if (isIOS) { +// await RNCal.addToCalendar( +// title, +// beginTime, +// endTime, +// location, +// latitude, +// longitude, +// ) +// } else { +// await RNCal.addToCalendar(title, beginTime, endTime, location) +// } +// } + +// /** +// * This function is used to check and see if the app has permission to add to the calendar before sending a request +// * to add an event to the calendar. This should be called every time before addToCalendar. +// * @returns Returns a Promise with a boolan value that indicates whether or not the permission is currently granted. +// */ +// const checkCalendarPermission = async (): Promise => { +// if (isIOS) { +// return await RNCal.hasPermission() +// } else { +// return PermissionsAndroid.check( +// PermissionsAndroid.PERMISSIONS.WRITE_CALENDAR, +// ) +// } +// } + +// /** +// * This function is used to request permission from the user to add or edit events in the calendar. +// * This should only be called if checkCalendarPermissions returns a false value. +// * @returns Returns a Promise with a boolean value that indicates whether or not the permission was granted. +// */ +// const requestCalendarPermission = async (): Promise => { +// if (isIOS) { +// return await RNCal.requestPermission() +// } else { +// // RN has android built in so we just use this instead of Native Modules +// try { +// const granted = await PermissionsAndroid.request( +// PermissionsAndroid.PERMISSIONS.WRITE_CALENDAR, +// { +// title: 'Calendar Permission Needed for this Action', +// message: +// 'VA:Health and Benefits needs calendar permission to add your appointments to your calendar', +// buttonNegative: 'Deny', +// buttonPositive: 'Grant', +// }, +// ) +// if (granted === PermissionsAndroid.RESULTS.GRANTED) { +// return true +// } else { +// // todo: if this is "never_ask_again" we need to prompt to go to settings? +// return false +// } +// } catch (err) { +// // logNonFatalErrorToFirebase( +// // err, +// // 'requestCalendarPermission: RnCalendar Error', +// // ) +// console.warn(err) +// return false +// } +// } +// } diff --git a/packages/components/src/utils/nativeModules/RNCalendar.kt b/packages/components/src/utils/nativeModules/RNCalendar.kt new file mode 100644 index 00000000..693d6f46 --- /dev/null +++ b/packages/components/src/utils/nativeModules/RNCalendar.kt @@ -0,0 +1,60 @@ +package gov.va.mobileapp.native_modules + +import android.Manifest.permission.WRITE_CALENDAR +import android.content.Intent +import android.content.Intent.ACTION_INSERT +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.os.Bundle +import android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME +import android.provider.CalendarContract.EXTRA_EVENT_END_TIME +import android.provider.CalendarContract.Events.* +import android.util.Log +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod + +const val INSERT_EVENT_CODE = 1001 + +/** + * React Native NativeModule to expose Calendar functionality to a React Native instance + */ +class RNCalendar(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + override fun getName() = "RNCalendar" + + /** + * This method is used to check to see if the user has granted the WRITE_CALENDAR permission to the app + * + * @return true if the app has write permissions and false if it has not asked or if the user has + * permanently denied the app. + */ + @ReactMethod + fun checkHasCalendarPermission(): Boolean { + return reactApplicationContext.checkSelfPermission(WRITE_CALENDAR) == PERMISSION_GRANTED + } + + /** + * This method receives a title, start, end, and location for an event to send to the calendar + * + * @param title The title to display for the new event. + * @param beginTime number of seconds UTC since 1970 when the event will start + * @param endTime number of seconds UTC since 1970 when the event will end. + * @param location The address or name of place where the event will be taking place + */ + @ReactMethod + fun addToCalendar(title: String, beginTime: Int, endTime: Int, location: String) { + Log.d("RNCal", "${beginTime * 1000}, $endTime") + val i = Intent(ACTION_INSERT).apply { + data = CONTENT_URI + type = "vnd.android.cursor.item/event" + putExtra(TITLE, title) + putExtra(EVENT_LOCATION, location) + putExtra(EXTRA_EVENT_BEGIN_TIME, beginTime.toLong() * 1000L) + putExtra(EXTRA_EVENT_END_TIME, endTime .toLong() * 1000L) + addFlags(FLAG_ACTIVITY_NEW_TASK) + } + + reactApplicationContext.startActivityForResult(i, INSERT_EVENT_CODE, Bundle.EMPTY) + } + +} \ No newline at end of file From f0a7727922b7f9e898d7bc7a1689471ced7546c8 Mon Sep 17 00:00:00 2001 From: Tim R Date: Mon, 5 Feb 2024 17:54:02 -0600 Subject: [PATCH 04/14] Adding directions support --- .../src/components/Link/Link.stories.tsx | 85 ++++++++++++------- .../components/src/components/Link/Link.tsx | 29 ++----- packages/components/src/utils/OSfunctions.tsx | 52 +++++++++++- 3 files changed, 113 insertions(+), 53 deletions(-) diff --git a/packages/components/src/components/Link/Link.stories.tsx b/packages/components/src/components/Link/Link.stories.tsx index 83f158e1..4a78393a 100644 --- a/packages/components/src/components/Link/Link.stories.tsx +++ b/packages/components/src/components/Link/Link.stories.tsx @@ -27,8 +27,7 @@ const meta: Meta = { flex: 1, justifyContent: 'center', alignItems: 'center', - }} - > + }}> ), @@ -42,22 +41,26 @@ type Story = StoryObj const startTime = new Date() const endTime = new Date(startTime.setMinutes(startTime.getMinutes() + 30)) const location = { - "id": "983GC", - "lat": 40.553875, - "long": -105.08795, - "name":"Fort Collins VA Clinic", - "address": { - "street": "2509 Research Boulevard", - "city": "Fort Collins", - "state": "CO", - "zipCode": "80526-8108" - } + lat: 33.7764681, + long: -118.1189664, + name: 'Tibor Rubin VA Medical Center', + address: { + street: '5901 E 7th St', + city: 'Long Beach', + state: 'CA', + zipCode: '90822', + }, } const getLocation = (): string => { const { lat, long, name, address } = location if (Platform.OS === 'ios' && lat && long) { return name || '' - } else if (address?.street && address?.city && address?.state && address?.zipCode) { + } else if ( + address?.street && + address?.city && + address?.state && + address?.zipCode + ) { return `${address.street} ${address.city}, ${address.state} ${address.zipCode}` } else { return name || '' @@ -67,46 +70,66 @@ const getLocation = (): string => { export const Calendar: Story = { storyName: 'Calendar', args: { - text: 'Button text', - type: {type: 'calendar', calendarData: { - title: 'Test', - startTime: startTime.getTime(), - endTime: endTime.getTime(), - location: getLocation(), - latitude: location.lat, - longitude: location.long - }} + text: 'Add to calendar', + type: { + type: 'calendar', + calendarData: { + title: 'Test', + startTime: startTime.getTime(), + endTime: endTime.getTime(), + location: getLocation(), + latitude: location.lat, + longitude: location.long, + }, + }, // a11yLabel: 'Alternate a11y text', + }, +} + +export const Directions: Story = { + storyName: 'Directions', + args: { + text: 'Get directions', + type: { + type: 'directions', + locationData: { + name: 'VA Long Beach Healthcare System', + address: location.address, + latitude: location.lat, + longitude: location.long, + }, }, + // a11yLabel: 'Alternate a11y text', + }, } export const Phone: Story = { args: { text: 'Call number', - type: {type: 'call', phoneNumber: '555'} + type: { type: 'call', phoneNumber: '555' }, // a11yLabel: 'Alternate a11y text', - }, + }, } export const PhoneTTY: Story = { args: { text: 'Call TTY number', - type: {type: 'call TTY', TTYnumber: '711'} + type: { type: 'call TTY', TTYnumber: '711' }, // a11yLabel: 'Alternate a11y text', - }, + }, } export const Text: Story = { args: { text: 'Text SMS number', - type: {type: 'text', textNumber: '55555'} + type: { type: 'text', textNumber: '55555' }, // a11yLabel: 'Alternate a11y text', - }, + }, } export const URL: Story = { args: { text: 'External link', - type: {type: 'url', url: 'https://www.va.gov/'} + type: { type: 'url', url: 'https://www.va.gov/' }, // a11yLabel: 'Alternate a11y text', - }, -} \ No newline at end of file + }, +} diff --git a/packages/components/src/components/Link/Link.tsx b/packages/components/src/components/Link/Link.tsx index d5fc9b70..9a2b663e 100644 --- a/packages/components/src/components/Link/Link.tsx +++ b/packages/components/src/components/Link/Link.tsx @@ -13,6 +13,8 @@ import React, { FC } from 'react' import { CalendarData, + FormDirectionsUrl, + LocationData, onPressCalendar, useExternalLink, } from '../../utils/OSfunctions' @@ -39,23 +41,9 @@ type custom = { onPress: () => void } -type appointmentAddress = { - street: string - city: string - state: string // 2 letter abbreviation - zipCode: string -} - -type locationData = { - name: string - address?: appointmentAddress - lat?: number - long?: number -} - type directions = { type: 'directions' - locationData: locationData + locationData: LocationData } type normalText = { @@ -101,8 +89,8 @@ export type LinkProps = { type: linkType /** */ variant?: 'default' | 'base' - /** Optional onPress logic */ - onPress?: () => void | ((data: string | CalendarData | locationData) => void) + /** Optional onPress override logic */ + onPress?: () => void | ((data: string | CalendarData | LocationData) => void) /** Optional icon override, sized by default to 24x24 */ icon?: IconProps | 'no icon' /** Optional a11yLabel override; should be used for phone numbers */ @@ -162,8 +150,9 @@ export const Link: FC = ({ break case 'directions': icon = icon ? icon : { name: 'Directions' } - _onPress = () => { - null + const directions = FormDirectionsUrl(type.locationData) + _onPress = async (): Promise => { + launchExternalLink(directions) } break case 'text': @@ -189,7 +178,7 @@ export const Link: FC = ({ switch (variant) { case 'base': - linkColor = isDarkMode ? Colors.colorGrayLightest : Colors.colorGrayMedium + linkColor = isDarkMode ? Colors.colorGrayLightest : Colors.colorGrayDark break default: linkColor = isDarkMode diff --git a/packages/components/src/utils/OSfunctions.tsx b/packages/components/src/utils/OSfunctions.tsx index 5ff7edf6..7dba35f1 100644 --- a/packages/components/src/utils/OSfunctions.tsx +++ b/packages/components/src/utils/OSfunctions.tsx @@ -1,10 +1,57 @@ /** A file for functions that leverage OS functionality */ -import { Alert, Linking, NativeModules, PermissionsAndroid, Platform } from 'react-native' +import { Alert, Linking, Platform } from 'react-native' // import { logNonFatalErrorToFirebase } from './analytics' const isIOS = Platform.OS === 'ios' +type address = { + street: string + city: string + state: string // 2 letter abbreviation + zipCode: string +} + +type hasAddress = { + name: string + address: address + latitude?: number + longitude?: number +} + +type hasLatLong = { + name: string + address?: address + latitude: number + longitude: number +} + +export type LocationData = hasAddress | hasLatLong + +const APPLE_MAPS_BASE_URL = 'https://maps.apple.com/' +const GOOGLE_MAPS_BASE_URL = 'https://www.google.com/maps/dir/' + +export const FormDirectionsUrl = (location: LocationData): string => { + const { name, address, latitude, longitude } = location + const addressString = Object.values(address || {}).join(' ') + + if (isIOS) { + const queryString = new URLSearchParams({ + // apply type parameter = m (map) + t: 'm', + daddr: `${addressString}+${name}+${latitude},${longitude}`, + }).toString() + return `${APPLE_MAPS_BASE_URL}?${queryString}` + } else { + const queryString = new URLSearchParams({ + api: '1', + destination: addressString || `${latitude},${longitude}`, + }).toString() + return `${GOOGLE_MAPS_BASE_URL}?${queryString}` + } +} + + /** * Hook to display a warning that the user is leaving the app when tapping an external link * @@ -37,7 +84,7 @@ export function useExternalLink(): (url: string) => void { } // Calendar bridge from iOS and Android -const RNCal = NativeModules.RNCalendar +// const RNCal = NativeModules.RNCalendar export type CalendarData = { title: string @@ -49,6 +96,7 @@ export type CalendarData = { } export const onPressCalendar = async (calendarData: CalendarData): Promise => { + calendarData // const { title, endTime, startTime, location, latitude, longitude } = calendarData // console.log('test') From 4078fb771bd611f0ff643a28eca7670e39fc912b Mon Sep 17 00:00:00 2001 From: Tim R Date: Mon, 5 Feb 2024 19:02:06 -0600 Subject: [PATCH 05/14] Adding TODO's for split off tickets --- .../components/src/components/Link/Link.tsx | 47 +++++++++---------- packages/components/src/utils/OSfunctions.tsx | 1 + 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/packages/components/src/components/Link/Link.tsx b/packages/components/src/components/Link/Link.tsx index 9a2b663e..002c4ad5 100644 --- a/packages/components/src/components/Link/Link.tsx +++ b/packages/components/src/components/Link/Link.tsx @@ -46,17 +46,18 @@ type directions = { locationData: LocationData } -type normalText = { - text: string - textA11y: string -} - -// Split to separate ticket, see lines 373-390 for app code: +// TODO: Ticket 168 created for in-line link +// See lines 373-390 for app code: // src/screens/BenefitsScreen/ClaimsScreen/AppealDetailsScreen/AppealStatus/AppealCurrentStatus/AppealCurrentStatus.tsx -type inLineLink = { - type: 'in line link' - paragraphText: normalText[] | LinkProps[] -} +// type normalText = { +// text: string +// textA11y: string +// } + +// type inLineLink = { +// type: 'in line link' +// paragraphText: normalText[] | LinkProps[] +// } type text = { type: 'text' @@ -70,14 +71,15 @@ type url = { type linkType = calendar | call | callTTY | custom | directions | text | url -type analytics = { - onPress?: () => void - onConfirm?: () => void - hasCalendarPermission?: () => void - onRequestCalendarPermission?: () => void - onCalendarPermissionSuccess?: () => void - onCalendarPermissionFailure?: () => void -} +// TODO: Ticket 170 created to revisit adding analytics after calendar support added/or deemed infeasible +// type analytics = { +// onPress?: () => void +// onConfirm?: () => void +// hasCalendarPermission?: () => void +// onRequestCalendarPermission?: () => void +// onCalendarPermissionSuccess?: () => void +// onCalendarPermissionFailure?: () => void +// } /** * Signifies the props that need to be passed in to {@link ClickForActionLink} @@ -96,7 +98,7 @@ export type LinkProps = { /** Optional a11yLabel override; should be used for phone numbers */ a11yLabel?: string /** Optional analytics event logging */ - analytics?: analytics + // analytics?: analytics /** Optional TestID */ testID?: string } @@ -111,7 +113,7 @@ export const Link: FC = ({ onPress, icon, a11yLabel, - analytics, + // analytics, testID, }) => { const colorScheme = webStorybookColorScheme() || useColorScheme() @@ -126,7 +128,6 @@ export const Link: FC = ({ switch (type.type) { case 'calendar': icon = icon ? icon : { name: 'Calendar' } - console.log('calendar switch') _onPress = async (): Promise => { await onPressCalendar(type.calendarData) return @@ -216,8 +217,6 @@ export const Link: FC = ({ lineHeight: 30, } - // ON PRESS STYLE BUGGED ON IOS (STORYBOOK) BUT WORKS ON WEB/ANDROID - const textStyle: TextStyle = { color: linkColor, textDecorationColor: linkColor, @@ -227,8 +226,6 @@ export const Link: FC = ({ return { ...(pressed ? pressedFont : regularFont), ...textStyle } } - // ON PRESS ASYNC, DOES THAT NEED ANY SPECIAL HANDLING!? - return ( {({ pressed }: PressableStateCallbackType) => ( diff --git a/packages/components/src/utils/OSfunctions.tsx b/packages/components/src/utils/OSfunctions.tsx index 7dba35f1..bb72fd55 100644 --- a/packages/components/src/utils/OSfunctions.tsx +++ b/packages/components/src/utils/OSfunctions.tsx @@ -83,6 +83,7 @@ export function useExternalLink(): (url: string) => void { } } +// TODO: Ticket 169 created to implement support for adding to calendar (or clean up if not feasible) // Calendar bridge from iOS and Android // const RNCal = NativeModules.RNCalendar From 2a702d34fa32350aae2fa93ba309dc3c7ee4ba3a Mon Sep 17 00:00:00 2001 From: Tim R Date: Tue, 6 Feb 2024 14:56:03 -0600 Subject: [PATCH 06/14] Clean-up, adding prompt override support --- .../src/components/Link/Link.stories.tsx | 12 +++++ .../components/src/components/Link/Link.tsx | 29 +++++------ packages/components/src/utils/OSfunctions.tsx | 48 ++++++++++++------- .../components/src/utils/translation/en.json | 6 ++- 4 files changed, 62 insertions(+), 33 deletions(-) diff --git a/packages/components/src/components/Link/Link.stories.tsx b/packages/components/src/components/Link/Link.stories.tsx index 4a78393a..b168cd22 100644 --- a/packages/components/src/components/Link/Link.stories.tsx +++ b/packages/components/src/components/Link/Link.stories.tsx @@ -71,6 +71,7 @@ export const Calendar: Story = { storyName: 'Calendar', args: { text: 'Add to calendar', + onPress: undefined, // Storybook sends a truthy function shell otherwise type: { type: 'calendar', calendarData: { @@ -90,6 +91,7 @@ export const Directions: Story = { storyName: 'Directions', args: { text: 'Get directions', + onPress: undefined, // Storybook sends a truthy function shell otherwise type: { type: 'directions', locationData: { @@ -99,6 +101,12 @@ export const Directions: Story = { longitude: location.long, }, }, + promptText: { + body: "You're navigating to your Maps app.", + cancel: "No thanks", + confirm: "Let's go!", + title: 'Title override' + } // a11yLabel: 'Alternate a11y text', }, } @@ -106,6 +114,7 @@ export const Directions: Story = { export const Phone: Story = { args: { text: 'Call number', + onPress: undefined, // Storybook sends a truthy function shell otherwise type: { type: 'call', phoneNumber: '555' }, // a11yLabel: 'Alternate a11y text', }, @@ -114,6 +123,7 @@ export const Phone: Story = { export const PhoneTTY: Story = { args: { text: 'Call TTY number', + onPress: undefined, // Storybook sends a truthy function shell otherwise type: { type: 'call TTY', TTYnumber: '711' }, // a11yLabel: 'Alternate a11y text', }, @@ -121,6 +131,7 @@ export const PhoneTTY: Story = { export const Text: Story = { args: { text: 'Text SMS number', + onPress: undefined, // Storybook sends a truthy function shell otherwise type: { type: 'text', textNumber: '55555' }, // a11yLabel: 'Alternate a11y text', }, @@ -129,6 +140,7 @@ export const Text: Story = { export const URL: Story = { args: { text: 'External link', + onPress: undefined, // Storybook sends a truthy function shell otherwise type: { type: 'url', url: 'https://www.va.gov/' }, // a11yLabel: 'Alternate a11y text', }, diff --git a/packages/components/src/components/Link/Link.tsx b/packages/components/src/components/Link/Link.tsx index 002c4ad5..6b49969e 100644 --- a/packages/components/src/components/Link/Link.tsx +++ b/packages/components/src/components/Link/Link.tsx @@ -15,7 +15,8 @@ import { CalendarData, FormDirectionsUrl, LocationData, - onPressCalendar, + OnPressCalendar, + leaveAppPromptText, useExternalLink, } from '../../utils/OSfunctions' import { Icon, IconProps } from '../Icon/Icon' @@ -81,31 +82,27 @@ type linkType = calendar | call | callTTY | custom | directions | text | url // onCalendarPermissionFailure?: () => void // } -/** - * Signifies the props that need to be passed in to {@link ClickForActionLink} - */ export type LinkProps = { /** Display text for the link */ text: string /** Preset link types that include default icons and onPress behavior */ type: linkType - /** */ + /** Color variant, primary by default */ variant?: 'default' | 'base' /** Optional onPress override logic */ - onPress?: () => void | ((data: string | CalendarData | LocationData) => void) + onPress?: () => void /** Optional icon override, sized by default to 24x24 */ icon?: IconProps | 'no icon' /** Optional a11yLabel override; should be used for phone numbers */ a11yLabel?: string + /** Optional override text for leaving app confirmation prompt */ + promptText?: leaveAppPromptText /** Optional analytics event logging */ // analytics?: analytics /** Optional TestID */ testID?: string } -/** - * Reusable component used for opening native calling app, texting app, or opening a url in the browser - */ export const Link: FC = ({ text, type, @@ -113,6 +110,7 @@ export const Link: FC = ({ onPress, icon, a11yLabel, + promptText, // analytics, testID, }) => { @@ -120,16 +118,15 @@ export const Link: FC = ({ const isDarkMode = colorScheme === 'dark' const launchExternalLink = useExternalLink() - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let _onPress: (() => void) | (() => Promise) | any = () => { - return + let _onPress: () => Promise = async () => { + null // Empty function to keep TS happy a function exists } switch (type.type) { case 'calendar': icon = icon ? icon : { name: 'Calendar' } _onPress = async (): Promise => { - await onPressCalendar(type.calendarData) + await OnPressCalendar(type.calendarData) return } break @@ -153,7 +150,7 @@ export const Link: FC = ({ icon = icon ? icon : { name: 'Directions' } const directions = FormDirectionsUrl(type.locationData) _onPress = async (): Promise => { - launchExternalLink(directions) + launchExternalLink(directions, promptText) } break case 'text': @@ -165,7 +162,7 @@ export const Link: FC = ({ case 'url': icon = icon ? icon : { name: 'ExternalLink' } _onPress = async (): Promise => { - launchExternalLink(type.url) + launchExternalLink(type.url, promptText) } break } @@ -188,7 +185,7 @@ export const Link: FC = ({ } const pressableProps: PressableProps = { - onPress: _onPress ? _onPress : onPress, + onPress: onPress ? onPress : _onPress, 'aria-label': a11yLabel, role: 'link', accessible: true, diff --git a/packages/components/src/utils/OSfunctions.tsx b/packages/components/src/utils/OSfunctions.tsx index bb72fd55..161748fe 100644 --- a/packages/components/src/utils/OSfunctions.tsx +++ b/packages/components/src/utils/OSfunctions.tsx @@ -1,7 +1,7 @@ /** A file for functions that leverage OS functionality */ import { Alert, Linking, Platform } from 'react-native' -// import { logNonFatalErrorToFirebase } from './analytics' +import { useTranslation } from 'react-i18next' const isIOS = Platform.OS === 'ios' @@ -31,14 +31,14 @@ export type LocationData = hasAddress | hasLatLong const APPLE_MAPS_BASE_URL = 'https://maps.apple.com/' const GOOGLE_MAPS_BASE_URL = 'https://www.google.com/maps/dir/' +/** Function to convert location data into a URL for handling by Apple/Google Maps */ export const FormDirectionsUrl = (location: LocationData): string => { const { name, address, latitude, longitude } = location const addressString = Object.values(address || {}).join(' ') if (isIOS) { const queryString = new URLSearchParams({ - // apply type parameter = m (map) - t: 'm', + t: 'm', // type: map daddr: `${addressString}+${name}+${latitude},${longitude}`, }).toString() return `${APPLE_MAPS_BASE_URL}?${queryString}` @@ -51,31 +51,45 @@ export const FormDirectionsUrl = (location: LocationData): string => { } } +export type leaveAppPromptText = { + body?: string + cancel?: string + confirm?: string + title?: string +} /** - * Hook to display a warning that the user is leaving the app when tapping an external link - * - * @returns an alert showing user they are leaving the app + * Hook to handle a link that leaves the app; conditionally displays alert prior to leaving */ -export function useExternalLink(): (url: string) => void { - // const { t } = useTranslation(NAMESPACE.COMMON) - - return (url: string) => { +export function useExternalLink(): ( + url: string, + text?: leaveAppPromptText, +) => void { + const { t } = useTranslation() + + return (url: string, text?: leaveAppPromptText) => { + // TODO: Ticket 170 // logAnalyticsEvent(Events.vama_link_click({ url, ...eventParams })) const onOKPress = () => { + // TODO: Ticket 170 // logAnalyticsEvent(Events.vama_link_confirm({ url, ...eventParams })) return Linking.openURL(url) } + const body = text?.body ? text.body : t('leaveAppAlert.body') + const cancel = text?.cancel ? text.cancel : t('cancel') + const confirm = text?.confirm ? text.confirm : t('ok') + const title = text?.title ? text.title : t('leaveAppAlert.title') + if (url.startsWith('http')) { - // Alert.alert(t('leavingApp.title'), t('leavingApp.body'), [ - Alert.alert("You’re leaving the app", "You're navigating to a website outside of the app.", [ + Alert.alert(title, body, [ + { text: cancel, style: 'cancel' }, { - text: 'Cancel', - // style: 'cancel', + text: confirm, + onPress: (): Promise => onOKPress(), + style: 'default', }, - { text: 'Ok', onPress: (): Promise => onOKPress(), style: 'default' }, ]) } else { Linking.openURL(url) @@ -96,7 +110,9 @@ export type CalendarData = { longitude: number } -export const onPressCalendar = async (calendarData: CalendarData): Promise => { +export const OnPressCalendar = async ( + calendarData: CalendarData, +): Promise => { calendarData // const { title, endTime, startTime, location, latitude, longitude } = calendarData diff --git a/packages/components/src/utils/translation/en.json b/packages/components/src/utils/translation/en.json index 924abfe7..9407a23a 100644 --- a/packages/components/src/utils/translation/en.json +++ b/packages/components/src/utils/translation/en.json @@ -1,3 +1,7 @@ { - "listPosition": "{{position}} of {{total}}" + "cancel": "Cancel", + "leaveAppAlert.body": "You're navigating to a website outside of the app.", + "leaveAppAlert.title": "You’re leaving the app", + "listPosition": "{{position}} of {{total}}", + "ok": "Ok" } \ No newline at end of file From fa6667036aeaa1aecb4e2fcabb7d26d17e1c7b98 Mon Sep 17 00:00:00 2001 From: Tim R Date: Tue, 6 Feb 2024 14:58:54 -0600 Subject: [PATCH 07/14] Updating color tokens --- packages/components/src/components/Link/Link.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/components/src/components/Link/Link.tsx b/packages/components/src/components/Link/Link.tsx index 6b49969e..31cd455b 100644 --- a/packages/components/src/components/Link/Link.tsx +++ b/packages/components/src/components/Link/Link.tsx @@ -1,4 +1,4 @@ -import * as Colors from '@department-of-veterans-affairs/mobile-tokens' +import { Colors } from '@department-of-veterans-affairs/mobile-tokens' import { Pressable, PressableProps, @@ -176,12 +176,10 @@ export const Link: FC = ({ switch (variant) { case 'base': - linkColor = isDarkMode ? Colors.colorGrayLightest : Colors.colorGrayDark + linkColor = isDarkMode ? Colors.grayLightest : Colors.grayDark break default: - linkColor = isDarkMode - ? Colors.colorUswdsSystemColorBlueVivid30 - : Colors.colorUswdsSystemColorBlueVivid60 + linkColor = isDarkMode ? Colors.uswdsBlueVivid30 : Colors.primary } const pressableProps: PressableProps = { From 91a3a9c9d42b3a86334da5388e8d5870cccd76fb Mon Sep 17 00:00:00 2001 From: Tim R Date: Tue, 6 Feb 2024 15:50:16 -0600 Subject: [PATCH 08/14] Story updates to expose more conditional behavior --- .../src/components/Link/Link.stories.tsx | 44 +++++++++++++++---- .../components/src/components/Link/Link.tsx | 2 +- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/packages/components/src/components/Link/Link.stories.tsx b/packages/components/src/components/Link/Link.stories.tsx index b168cd22..1998c468 100644 --- a/packages/components/src/components/Link/Link.stories.tsx +++ b/packages/components/src/components/Link/Link.stories.tsx @@ -83,7 +83,35 @@ export const Calendar: Story = { longitude: location.long, }, }, - // a11yLabel: 'Alternate a11y text', + }, +} + +export const Custom: Story = { + name: 'Custom Link w/o Icon', + args: { + text: 'Custom Link - no icon', + onPress: undefined, // Storybook sends a truthy function shell otherwise + type: { + type: 'custom', + onPress: () => { + null + }, + }, + }, +} + +export const CustomWithIcon: Story = { + name: 'Custom Link with Icon', + args: { + text: 'Custom Link', + icon: { name: 'Truck' }, + onPress: undefined, // Storybook sends a truthy function shell otherwise + type: { + type: 'custom', + onPress: () => { + null + }, + }, }, } @@ -103,11 +131,11 @@ export const Directions: Story = { }, promptText: { body: "You're navigating to your Maps app.", - cancel: "No thanks", + cancel: 'No thanks', confirm: "Let's go!", - title: 'Title override' - } - // a11yLabel: 'Alternate a11y text', + title: 'Title override', + }, + a11yLabel: 'Get directions with Maps app', }, } @@ -116,7 +144,6 @@ export const Phone: Story = { text: 'Call number', onPress: undefined, // Storybook sends a truthy function shell otherwise type: { type: 'call', phoneNumber: '555' }, - // a11yLabel: 'Alternate a11y text', }, } @@ -125,15 +152,15 @@ export const PhoneTTY: Story = { text: 'Call TTY number', onPress: undefined, // Storybook sends a truthy function shell otherwise type: { type: 'call TTY', TTYnumber: '711' }, - // a11yLabel: 'Alternate a11y text', }, } + export const Text: Story = { args: { text: 'Text SMS number', + variant: 'base', onPress: undefined, // Storybook sends a truthy function shell otherwise type: { type: 'text', textNumber: '55555' }, - // a11yLabel: 'Alternate a11y text', }, } @@ -142,6 +169,5 @@ export const URL: Story = { text: 'External link', onPress: undefined, // Storybook sends a truthy function shell otherwise type: { type: 'url', url: 'https://www.va.gov/' }, - // a11yLabel: 'Alternate a11y text', }, } diff --git a/packages/components/src/components/Link/Link.tsx b/packages/components/src/components/Link/Link.tsx index 31cd455b..035f7fae 100644 --- a/packages/components/src/components/Link/Link.tsx +++ b/packages/components/src/components/Link/Link.tsx @@ -87,7 +87,7 @@ export type LinkProps = { text: string /** Preset link types that include default icons and onPress behavior */ type: linkType - /** Color variant, primary by default */ + /** Color variant */ variant?: 'default' | 'base' /** Optional onPress override logic */ onPress?: () => void From 37e2a388ff7ea77c99523d8d1c814860a65f1cac Mon Sep 17 00:00:00 2001 From: VA Automation Bot Date: Wed, 7 Feb 2024 17:39:16 +0000 Subject: [PATCH 09/14] Version bump: components-v0.5.2-alpha.0 --- packages/components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index c7893ace..b5d250e3 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@department-of-veterans-affairs/mobile-component-library", - "version": "0.5.1", + "version": "0.5.2-alpha.0", "description": "VA Design System Mobile Component Library", "main": "src/index.tsx", "scripts": { From bc77cd4b8acf354930d3c7b5a648965cc51878f7 Mon Sep 17 00:00:00 2001 From: Tim R Date: Wed, 7 Feb 2024 13:42:54 -0600 Subject: [PATCH 10/14] Adding a11yHint, improving typing --- .../src/components/Link/Link.stories.tsx | 63 +++++------ .../components/src/components/Link/Link.tsx | 104 +++++++++++------- packages/components/src/utils/OSfunctions.tsx | 3 +- 3 files changed, 93 insertions(+), 77 deletions(-) diff --git a/packages/components/src/components/Link/Link.stories.tsx b/packages/components/src/components/Link/Link.stories.tsx index 1998c468..cbcc61e3 100644 --- a/packages/components/src/components/Link/Link.stories.tsx +++ b/packages/components/src/components/Link/Link.stories.tsx @@ -72,16 +72,14 @@ export const Calendar: Story = { args: { text: 'Add to calendar', onPress: undefined, // Storybook sends a truthy function shell otherwise - type: { - type: 'calendar', - calendarData: { - title: 'Test', - startTime: startTime.getTime(), - endTime: endTime.getTime(), - location: getLocation(), - latitude: location.lat, - longitude: location.long, - }, + type: 'calendar', + calendarData: { + title: 'Test', + startTime: startTime.getTime(), + endTime: endTime.getTime(), + location: getLocation(), + latitude: location.lat, + longitude: location.long, }, }, } @@ -90,12 +88,9 @@ export const Custom: Story = { name: 'Custom Link w/o Icon', args: { text: 'Custom Link - no icon', - onPress: undefined, // Storybook sends a truthy function shell otherwise - type: { - type: 'custom', - onPress: () => { - null - }, + type: 'custom', + onPress: () => { + null }, }, } @@ -105,12 +100,9 @@ export const CustomWithIcon: Story = { args: { text: 'Custom Link', icon: { name: 'Truck' }, - onPress: undefined, // Storybook sends a truthy function shell otherwise - type: { - type: 'custom', - onPress: () => { - null - }, + type: 'custom', + onPress: () => { + null }, }, } @@ -120,14 +112,12 @@ export const Directions: Story = { args: { text: 'Get directions', onPress: undefined, // Storybook sends a truthy function shell otherwise - type: { - type: 'directions', - locationData: { - name: 'VA Long Beach Healthcare System', - address: location.address, - latitude: location.lat, - longitude: location.long, - }, + type: 'directions', + locationData: { + name: 'VA Long Beach Healthcare System', + address: location.address, + latitude: location.lat, + longitude: location.long, }, promptText: { body: "You're navigating to your Maps app.", @@ -136,6 +126,7 @@ export const Directions: Story = { title: 'Title override', }, a11yLabel: 'Get directions with Maps app', + a11yHint: 'Opens maps app with directions to the location' }, } @@ -143,7 +134,8 @@ export const Phone: Story = { args: { text: 'Call number', onPress: undefined, // Storybook sends a truthy function shell otherwise - type: { type: 'call', phoneNumber: '555' }, + type: 'call', + phoneNumber: '555', }, } @@ -151,7 +143,8 @@ export const PhoneTTY: Story = { args: { text: 'Call TTY number', onPress: undefined, // Storybook sends a truthy function shell otherwise - type: { type: 'call TTY', TTYnumber: '711' }, + type: 'call TTY', + TTYnumber: '711', }, } @@ -160,7 +153,8 @@ export const Text: Story = { text: 'Text SMS number', variant: 'base', onPress: undefined, // Storybook sends a truthy function shell otherwise - type: { type: 'text', textNumber: '55555' }, + type: 'text', + textNumber: '55555', }, } @@ -168,6 +162,7 @@ export const URL: Story = { args: { text: 'External link', onPress: undefined, // Storybook sends a truthy function shell otherwise - type: { type: 'url', url: 'https://www.va.gov/' }, + type: 'url', + url: 'https://www.va.gov/', }, } diff --git a/packages/components/src/components/Link/Link.tsx b/packages/components/src/components/Link/Link.tsx index 035f7fae..00eff60e 100644 --- a/packages/components/src/components/Link/Link.tsx +++ b/packages/components/src/components/Link/Link.tsx @@ -22,30 +22,43 @@ import { import { Icon, IconProps } from '../Icon/Icon' import { webStorybookColorScheme } from '../../utils' -type calendar = { - type: 'calendar' - calendarData: CalendarData +// Convenience type to default type-specific props to not existing/being optional +type allNever = { + calendarData: never + locationData: never + /** Optional onPress override logic */ + onPress?: () => void + phoneNumber: never + textNumber: never + TTYnumber: never + url: never } -type call = { - type: 'call' - phoneNumber: string -} +type calendar = Omit & { + type: 'calendar' + calendarData: CalendarData + } -type callTTY = { - type: 'call TTY' - TTYnumber: string -} +type call = Omit & { + type: 'call' + phoneNumber: string + } -type custom = { - type: 'custom' - onPress: () => void -} +type callTTY = Omit & { + type: 'call TTY' + TTYnumber: string + } -type directions = { - type: 'directions' - locationData: LocationData -} +type custom = Omit & { + type: 'custom' + /** Required onPress override logic */ + onPress: () => void + } + +type directions = Omit & { + type: 'directions' + locationData: LocationData + } // TODO: Ticket 168 created for in-line link // See lines 373-390 for app code: @@ -60,17 +73,17 @@ type directions = { // paragraphText: normalText[] | LinkProps[] // } -type text = { - type: 'text' - textNumber: string -} +type text = Omit & { + type: 'text' + textNumber: string + } -type url = { - type: 'url' - url: string -} +type url = Omit & { + type: 'url' + url: string + } -type linkType = calendar | call | callTTY | custom | directions | text | url +type linkTypes = calendar | call | callTTY | custom | directions | text | url // TODO: Ticket 170 created to revisit adding analytics after calendar support added/or deemed infeasible // type analytics = { @@ -82,19 +95,17 @@ type linkType = calendar | call | callTTY | custom | directions | text | url // onCalendarPermissionFailure?: () => void // } -export type LinkProps = { +export type LinkProps = linkTypes & { /** Display text for the link */ text: string - /** Preset link types that include default icons and onPress behavior */ - type: linkType /** Color variant */ variant?: 'default' | 'base' - /** Optional onPress override logic */ - onPress?: () => void /** Optional icon override, sized by default to 24x24 */ icon?: IconProps | 'no icon' /** Optional a11yLabel override; should be used for phone numbers */ a11yLabel?: string + /** Optional a11yHint to provide additional context */ + a11yHint?: string /** Optional override text for leaving app confirmation prompt */ promptText?: leaveAppPromptText /** Optional analytics event logging */ @@ -104,15 +115,23 @@ export type LinkProps = { } export const Link: FC = ({ - text, type, + text, variant = 'default', onPress, icon, a11yLabel, + a11yHint, promptText, // analytics, testID, + // Type-specific props + calendarData, + locationData, + phoneNumber, + textNumber, + TTYnumber, + url, }) => { const colorScheme = webStorybookColorScheme() || useColorScheme() const isDarkMode = colorScheme === 'dark' @@ -122,33 +141,33 @@ export const Link: FC = ({ null // Empty function to keep TS happy a function exists } - switch (type.type) { + switch (type) { case 'calendar': icon = icon ? icon : { name: 'Calendar' } _onPress = async (): Promise => { - await OnPressCalendar(type.calendarData) + await OnPressCalendar(calendarData) return } break case 'call': icon = icon ? icon : { name: 'Phone' } _onPress = async (): Promise => { - launchExternalLink(`tel:${type.phoneNumber}`) + launchExternalLink(`tel:${phoneNumber}`) } break case 'call TTY': icon = icon ? icon : { name: 'PhoneTTY' } _onPress = async (): Promise => { - launchExternalLink(`tel:${type.TTYnumber}`) + launchExternalLink(`tel:${TTYnumber}`) } break case 'custom': icon = icon ? icon : 'no icon' - onPress = type.onPress + onPress = onPress break case 'directions': icon = icon ? icon : { name: 'Directions' } - const directions = FormDirectionsUrl(type.locationData) + const directions = FormDirectionsUrl(locationData) _onPress = async (): Promise => { launchExternalLink(directions, promptText) } @@ -156,13 +175,13 @@ export const Link: FC = ({ case 'text': icon = icon ? icon : { name: 'Text' } _onPress = async (): Promise => { - launchExternalLink(`sms:${type.textNumber}`) + launchExternalLink(`sms:${textNumber}`) } break case 'url': icon = icon ? icon : { name: 'ExternalLink' } _onPress = async (): Promise => { - launchExternalLink(type.url, promptText) + launchExternalLink(url, promptText) } break } @@ -185,6 +204,7 @@ export const Link: FC = ({ const pressableProps: PressableProps = { onPress: onPress ? onPress : _onPress, 'aria-label': a11yLabel, + accessibilityHint: a11yHint, role: 'link', accessible: true, } diff --git a/packages/components/src/utils/OSfunctions.tsx b/packages/components/src/utils/OSfunctions.tsx index 161748fe..894b75a0 100644 --- a/packages/components/src/utils/OSfunctions.tsx +++ b/packages/components/src/utils/OSfunctions.tsx @@ -8,7 +8,8 @@ const isIOS = Platform.OS === 'ios' type address = { street: string city: string - state: string // 2 letter abbreviation + /** 2 letter state abbreviation */ + state: string zipCode: string } From bdc7711c74b901eb26227bea2a64f5ce84b52287 Mon Sep 17 00:00:00 2001 From: Tim R Date: Wed, 7 Feb 2024 13:45:28 -0600 Subject: [PATCH 11/14] Name change --- packages/components/src/components/Link/Link.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/components/src/components/Link/Link.tsx b/packages/components/src/components/Link/Link.tsx index 00eff60e..5d6df96f 100644 --- a/packages/components/src/components/Link/Link.tsx +++ b/packages/components/src/components/Link/Link.tsx @@ -23,7 +23,7 @@ import { Icon, IconProps } from '../Icon/Icon' import { webStorybookColorScheme } from '../../utils' // Convenience type to default type-specific props to not existing/being optional -type allNever = { +type nullTypeSpecifics = { calendarData: never locationData: never /** Optional onPress override logic */ @@ -34,28 +34,28 @@ type allNever = { url: never } -type calendar = Omit & { +type calendar = Omit & { type: 'calendar' calendarData: CalendarData } -type call = Omit & { +type call = Omit & { type: 'call' phoneNumber: string } -type callTTY = Omit & { +type callTTY = Omit & { type: 'call TTY' TTYnumber: string } -type custom = Omit & { +type custom = Omit & { type: 'custom' /** Required onPress override logic */ onPress: () => void } -type directions = Omit & { +type directions = Omit & { type: 'directions' locationData: LocationData } @@ -73,12 +73,12 @@ type directions = Omit & { // paragraphText: normalText[] | LinkProps[] // } -type text = Omit & { +type text = Omit & { type: 'text' textNumber: string } -type url = Omit & { +type url = Omit & { type: 'url' url: string } From f1f5c6e559184e2e652d3bf800b6acadbdff350c Mon Sep 17 00:00:00 2001 From: Tim R Date: Wed, 7 Feb 2024 13:59:34 -0600 Subject: [PATCH 12/14] TS so dumb, does this fix it --- packages/components/src/components/Link/Link.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/components/src/components/Link/Link.tsx b/packages/components/src/components/Link/Link.tsx index 5d6df96f..6919ee33 100644 --- a/packages/components/src/components/Link/Link.tsx +++ b/packages/components/src/components/Link/Link.tsx @@ -24,14 +24,14 @@ import { webStorybookColorScheme } from '../../utils' // Convenience type to default type-specific props to not existing/being optional type nullTypeSpecifics = { - calendarData: never - locationData: never + calendarData?: never + locationData?: never /** Optional onPress override logic */ onPress?: () => void - phoneNumber: never - textNumber: never - TTYnumber: never - url: never + phoneNumber?: never + textNumber?: never + TTYnumber?: never + url?: never } type calendar = Omit & { From c1488d4f43d47f39438aa8a354dc2503e5b634b1 Mon Sep 17 00:00:00 2001 From: Tim R Date: Wed, 7 Feb 2024 14:00:36 -0600 Subject: [PATCH 13/14] Come on git --- packages/components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index b5d250e3..96d509ef 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@department-of-veterans-affairs/mobile-component-library", - "version": "0.5.2-alpha.0", + "version": "0.5.2-alpha.1", "description": "VA Design System Mobile Component Library", "main": "src/index.tsx", "scripts": { From f7fe362c4673324db59e484b0738a8fa38a2daa7 Mon Sep 17 00:00:00 2001 From: VA Automation Bot Date: Wed, 7 Feb 2024 20:03:28 +0000 Subject: [PATCH 14/14] Version bump: components-v0.5.2-alpha.2 --- packages/components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index 96d509ef..e0a042fe 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@department-of-veterans-affairs/mobile-component-library", - "version": "0.5.2-alpha.1", + "version": "0.5.2-alpha.2", "description": "VA Design System Mobile Component Library", "main": "src/index.tsx", "scripts": {