Skip to content

Commit

Permalink
Merge pull request #288 from department-of-veterans-affairs/feature/2…
Browse files Browse the repository at this point in the history
…46-narin-alert-collapsible

[Feature] Alert Collapse/Expand
  • Loading branch information
narin committed Apr 23, 2024
2 parents d553181 + d18b496 commit ded5d3b
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 73 deletions.
22 changes: 22 additions & 0 deletions packages/components/src/components/Alert/Alert.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,25 @@ export const Info: Story = {
},
},
}

export const Expandable: Story = {
args: {
variant: 'info',
header: 'Header',
description: 'Description',
children: children,
expandable: true,
primaryButton: {
label: 'Button Text',
onPress: () => {
null
},
},
secondaryButton: {
label: 'Button Text',
onPress: () => {
null
},
},
},
}
207 changes: 134 additions & 73 deletions packages/components/src/components/Alert/Alert.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Colors } from '@department-of-veterans-affairs/mobile-tokens'
// import { HapticFeedbackTypes } from 'react-native-haptic-feedback'
import { Text, TextStyle, View, ViewStyle } from 'react-native'
// ^ ScrollView,
import React, { FC } from 'react'
// ^ , RefObject, useEffect, useState

// import { triggerHaptic } from 'utils/haptics'
// import { useAutoScrollToElement } from 'utils/hooks'
import {
Insets,
Pressable,
Text,
TextStyle,
View,
ViewStyle,
useWindowDimensions,
} from 'react-native'
import React, { FC, useState } from 'react'

import { BaseColor, Spacer, useColorScheme } from '../../utils'
import { Button, ButtonProps, ButtonVariants } from '../Button/Button'
Expand All @@ -18,8 +20,6 @@ export const AlertContentColor = BaseColor
export type AlertProps = {
/** Alert variant */
variant: 'info' | 'success' | 'warning' | 'error'
/** Optional header text */
header?: string
/** Optional a11y override for header */
headerA11yLabel?: string
/** Optional description text */
Expand All @@ -33,13 +33,25 @@ export type AlertProps = {
primaryButton?: ButtonProps
/** Optional secondary action button */
secondaryButton?: ButtonProps
/** Optional boolean for determining when to focus on error alert boxes (e.g. onSaveClicked). */
// focusOnError?: boolean
/** Optional ref for the parent scroll view. Used for scrolling to error alert boxes. */
// scrollViewRef?: RefObject<ScrollView>
/** optional testID */
/** Optional testID */
testId?: string
}
} & (
| {
/** True to make the Alert expandable */
expandable: true
/** Header text. Required when Alert is expandable */
header: string
/** True if Alert should start expanded. Defaults to false */
initializeExpanded?: boolean
}
| {
/** True to make the Alert expandable */
expandable?: false
/** Header text. Optional when Alert is not expandable */
header?: string
initializeExpanded?: never
}
)

/**
* Work in progress:
Expand All @@ -52,42 +64,27 @@ export const Alert: FC<AlertProps> = ({
description,
descriptionA11yLabel,
children,
expandable,
initializeExpanded,
primaryButton,
secondaryButton,
// focusOnError = true,
// scrollViewRef,
testId,
}) => {
const colorScheme = useColorScheme()
const fontScale = useWindowDimensions().fontScale
const isDarkMode = colorScheme === 'dark'
// const [scrollRef, viewRef, scrollToAlert] = useAutoScrollToElement()
// const [shouldFocus, setShouldFocus] = useState(true)

// useEffect(() => {
// if (
// variant === 'error' &&
// scrollViewRef?.current &&
// (header || description)
// ) {
// scrollRef.current = scrollViewRef.current
// scrollToAlert(-boxPadding)
// }
// setShouldFocus(focusOnError)
// }, [
// variant,
// header,
// description,
// focusOnError,
// scrollRef,
// scrollToAlert,
// scrollViewRef,
// ])
const [expanded, setExpanded] = useState(
expandable ? initializeExpanded : true,
)

// TODO: Replace with sizing/dimension tokens
const Sizing = {
_8: 8,
_10: 10,
_12: 12,
_16: 16,
_20: 20,
_24: 24,
_30: 30,
}
const contentColor = AlertContentColor()
Expand Down Expand Up @@ -151,32 +148,52 @@ export const Alert: FC<AlertProps> = ({
borderLeftWidth: Sizing._8,
padding: Sizing._20,
paddingLeft: Sizing._12, // Adds with borderLeftWidth for 20
width: '100%' // Ensure Alert fills horizontal space, regardless of flexing content
width: '100%', // Ensure Alert fills horizontal space, regardless of flexing content
}

const iconViewStyle: ViewStyle = {
flexDirection: 'row',
// Below keeps icon aligned with first row of text, centered, and scalable
alignSelf: 'flex-start',
minHeight: Sizing._30,
minHeight: Sizing._30 * fontScale,
alignItems: 'center',
justifyContent: 'center',
}

const iconDisplay = (
<View style={iconViewStyle}>
<Icon fill={contentColor} {...iconProps} />
<Icon fill={contentColor} {...iconProps} preventScaling />
<Spacer horizontal />
</View>
)

// const vibrate = (): void => {
// if (variant === 'error') {
// triggerHaptic(HapticFeedbackTypes.notificationError)
// } else if (variant === 'warning') {
// triggerHaptic(HapticFeedbackTypes.notificationWarning)
// }
// }
const expandIconProps: IconProps = {
fill: contentColor,
width: Sizing._16,
height: Sizing._16,
maxWidth: Sizing._24,
name: expanded ? 'ChevronUp' : 'ChevronDown',
}

const expandableIcon = (
<View style={iconViewStyle}>
<Spacer horizontal />
<Icon {...expandIconProps} />
</View>
)

/**
* When an alert is expandable, the content should have additional padding on
* the right to appear within the expandable icon. Since the expandable icon
* has a maxWidth, this hidden icon matches the spacing of the icon insteading
* instead of adding a <Spacer /> with a calculated value.
*/
const spacerIcon = (
<View style={iconViewStyle} aria-hidden>
<Spacer horizontal />
<Icon {...expandIconProps} fill="none" />
</View>
)

// TODO: Replace with typography tokens
const headerFont: TextStyle = {
Expand All @@ -194,6 +211,46 @@ export const Alert: FC<AlertProps> = ({
lineHeight: 30,
}

const _header = () => {
if (!header) return null

const headerText = <Text style={headerFont}>{header}</Text>
const a11yLabel = headerA11yLabel || header
const hitSlop: Insets = {
// left border + left padding + spacer + icon width
left: Sizing._8 + Sizing._12 + Sizing._10 + Sizing._24,
top: Sizing._20,
// bottom spacing changes depending on expanded state
bottom: expanded ? Sizing._10 : Sizing._20,
right: Sizing._20,
}

/**
* Wrap header text and expand icon in Pressable if the Alert is expandable
* Otherwise wrap in View with accessibility props
*/
if (expandable) {
return (
<Pressable
onPress={() => setExpanded(!expanded)}
role="tab"
aria-expanded={expanded}
aria-label={a11yLabel}
hitSlop={hitSlop}
style={{ flexDirection: 'row' }}>
<View style={{ flex: 1 }}>{headerText}</View>
{expandableIcon}
</Pressable>
)
}

return (
<View accessible={true} aria-label={a11yLabel} role="heading">
{headerText}
</View>
)
}

const _primaryButton = () => {
if (!primaryButton) return null

Expand Down Expand Up @@ -225,35 +282,39 @@ export const Alert: FC<AlertProps> = ({
}

return (
<View style={contentBox} testID={testId}>
<View
style={contentBox}
testID={testId}
role={expandable ? 'tablist' : 'none'}>
<View style={{ flexDirection: 'row' }}>
{iconDisplay}
<View style={{ flex: 1 }}>
{header ? (
<View
// ref={viewRef}
accessible={true}
aria-label={headerA11yLabel || header}
role="heading">
<Text style={headerFont}>{header}</Text>
</View>
) : null}
{header && (description || children) ? <Spacer /> : null}
{description ? (
<View
// ref={!header ? viewRef : undefined}
accessible={true}
aria-label={descriptionA11yLabel || description}>
<Text style={descriptionFont}>{description}</Text>
{_header()}
{expanded && (
<View style={{ flexDirection: 'row' }}>
<View style={{ flex: 1 }}>
{header && (description || children) ? <Spacer /> : null}
{description ? (
<View
accessible={true}
aria-label={descriptionA11yLabel || description}>
<Text style={descriptionFont}>{description}</Text>
</View>
) : null}
{description && children ? <Spacer /> : null}
{children}
</View>
{expandable && spacerIcon}
</View>
) : null}
{description && children ? <Spacer /> : null}
{children}
{/* {shouldFocus && vibrate()} */}
)}
</View>
</View>
{_primaryButton()}
{_secondaryButton()}
{expanded && (
<>
{_primaryButton()}
{_secondaryButton()}
</>
)}
</View>
)
}
1 change: 1 addition & 0 deletions packages/components/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ if (expoApp && App.initiateExpo) {
}

// Export components here so they are exported through npm
export { Alert } from './components/Alert/Alert'
export { Button, ButtonVariants } from './components/Button/Button'
export { Icon } from './components/Icon/Icon'
export { Link } from './components/Link/Link'
Expand Down

0 comments on commit ded5d3b

Please sign in to comment.