Link updates (#2890)
* Link updates, add atoms * Update comments * Support download * Don't open new window for downloadzio/stable
parent
0ff61e08e9
commit
1d729721e5
|
@ -122,6 +122,9 @@ export const atoms = {
|
||||||
flex_shrink: {
|
flex_shrink: {
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
},
|
},
|
||||||
|
justify_start: {
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
},
|
||||||
justify_center: {
|
justify_center: {
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
|
@ -140,10 +143,31 @@ export const atoms = {
|
||||||
align_end: {
|
align_end: {
|
||||||
alignItems: 'flex-end',
|
alignItems: 'flex-end',
|
||||||
},
|
},
|
||||||
|
self_auto: {
|
||||||
|
alignSelf: 'auto',
|
||||||
|
},
|
||||||
|
self_start: {
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
},
|
||||||
|
self_end: {
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
},
|
||||||
|
self_center: {
|
||||||
|
alignSelf: 'center',
|
||||||
|
},
|
||||||
|
self_stretch: {
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
},
|
||||||
|
self_baseline: {
|
||||||
|
alignSelf: 'baseline',
|
||||||
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Text
|
* Text
|
||||||
*/
|
*/
|
||||||
|
text_left: {
|
||||||
|
textAlign: 'left',
|
||||||
|
},
|
||||||
text_center: {
|
text_center: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
|
@ -195,10 +219,16 @@ export const atoms = {
|
||||||
font_bold: {
|
font_bold: {
|
||||||
fontWeight: tokens.fontWeight.semibold,
|
fontWeight: tokens.fontWeight.semibold,
|
||||||
},
|
},
|
||||||
|
italic: {
|
||||||
|
fontStyle: 'italic',
|
||||||
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Border
|
* Border
|
||||||
*/
|
*/
|
||||||
|
border_0: {
|
||||||
|
borderWidth: 0,
|
||||||
|
},
|
||||||
border: {
|
border: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
|
@ -208,6 +238,12 @@ export const atoms = {
|
||||||
border_b: {
|
border_b: {
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
},
|
},
|
||||||
|
border_l: {
|
||||||
|
borderLeftWidth: 1,
|
||||||
|
},
|
||||||
|
border_r: {
|
||||||
|
borderRightWidth: 1,
|
||||||
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Shadow
|
* Shadow
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {sanitizeUrl} from '@braintree/sanitize-url'
|
||||||
|
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {useTheme, web, flatten, TextStyleProp} from '#/alf'
|
import {useTheme, web, flatten, TextStyleProp, atoms as a} from '#/alf'
|
||||||
import {Button, ButtonProps} from '#/components/Button'
|
import {Button, ButtonProps} from '#/components/Button'
|
||||||
import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
||||||
import {
|
import {
|
||||||
|
@ -35,6 +35,13 @@ type BaseLinkProps = Pick<
|
||||||
Parameters<typeof useLinkProps<AllNavigatorParams>>[0],
|
Parameters<typeof useLinkProps<AllNavigatorParams>>[0],
|
||||||
'to'
|
'to'
|
||||||
> & {
|
> & {
|
||||||
|
testID?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label for a11y. Defaults to the href.
|
||||||
|
*/
|
||||||
|
label?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The React Navigation `StackAction` to perform when the link is pressed.
|
* The React Navigation `StackAction` to perform when the link is pressed.
|
||||||
*/
|
*/
|
||||||
|
@ -46,6 +53,18 @@ type BaseLinkProps = Pick<
|
||||||
* Note: atm this only works for `InlineLink`s with a string child.
|
* Note: atm this only works for `InlineLink`s with a string child.
|
||||||
*/
|
*/
|
||||||
warnOnMismatchingTextChild?: boolean
|
warnOnMismatchingTextChild?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for when the link is pressed.
|
||||||
|
*
|
||||||
|
* DO NOT use this for navigation, that's what the `to` prop is for.
|
||||||
|
*/
|
||||||
|
onPress?: (e: GestureResponderEvent) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web-only attribute. Sets `download` attr on web.
|
||||||
|
*/
|
||||||
|
download?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLink({
|
export function useLink({
|
||||||
|
@ -53,6 +72,7 @@ export function useLink({
|
||||||
displayText,
|
displayText,
|
||||||
action = 'push',
|
action = 'push',
|
||||||
warnOnMismatchingTextChild,
|
warnOnMismatchingTextChild,
|
||||||
|
onPress: outerOnPress,
|
||||||
}: BaseLinkProps & {
|
}: BaseLinkProps & {
|
||||||
displayText: string
|
displayText: string
|
||||||
}) {
|
}) {
|
||||||
|
@ -66,6 +86,8 @@ export function useLink({
|
||||||
|
|
||||||
const onPress = React.useCallback(
|
const onPress = React.useCallback(
|
||||||
(e: GestureResponderEvent) => {
|
(e: GestureResponderEvent) => {
|
||||||
|
outerOnPress?.(e)
|
||||||
|
|
||||||
const requiresWarning = Boolean(
|
const requiresWarning = Boolean(
|
||||||
warnOnMismatchingTextChild &&
|
warnOnMismatchingTextChild &&
|
||||||
displayText &&
|
displayText &&
|
||||||
|
@ -132,6 +154,7 @@ export function useLink({
|
||||||
displayText,
|
displayText,
|
||||||
closeModal,
|
closeModal,
|
||||||
openModal,
|
openModal,
|
||||||
|
outerOnPress,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -143,16 +166,7 @@ export function useLink({
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LinkProps = Omit<BaseLinkProps, 'warnOnMismatchingTextChild'> &
|
export type LinkProps = Omit<BaseLinkProps, 'warnOnMismatchingTextChild'> &
|
||||||
Omit<ButtonProps, 'style' | 'onPress' | 'disabled' | 'label'> & {
|
Omit<ButtonProps, 'onPress' | 'disabled' | 'label'>
|
||||||
/**
|
|
||||||
* Label for a11y. Defaults to the href.
|
|
||||||
*/
|
|
||||||
label?: string
|
|
||||||
/**
|
|
||||||
* Web-only attribute. Sets `download` attr on web.
|
|
||||||
*/
|
|
||||||
download?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A interactive element that renders as a `<a>` tag on the web. On mobile it
|
* A interactive element that renders as a `<a>` tag on the web. On mobile it
|
||||||
|
@ -166,6 +180,7 @@ export function Link({
|
||||||
children,
|
children,
|
||||||
to,
|
to,
|
||||||
action = 'push',
|
action = 'push',
|
||||||
|
onPress: outerOnPress,
|
||||||
download,
|
download,
|
||||||
...rest
|
...rest
|
||||||
}: LinkProps) {
|
}: LinkProps) {
|
||||||
|
@ -173,24 +188,26 @@ export function Link({
|
||||||
to,
|
to,
|
||||||
displayText: typeof children === 'string' ? children : '',
|
displayText: typeof children === 'string' ? children : '',
|
||||||
action,
|
action,
|
||||||
|
onPress: outerOnPress,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
label={href}
|
label={href}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
style={[a.justify_start, flatten(rest.style)]}
|
||||||
role="link"
|
role="link"
|
||||||
accessibilityRole="link"
|
accessibilityRole="link"
|
||||||
href={href}
|
href={href}
|
||||||
onPress={onPress}
|
onPress={download ? undefined : onPress}
|
||||||
{...web({
|
{...web({
|
||||||
hrefAttrs: {
|
hrefAttrs: {
|
||||||
target: isExternal ? 'blank' : undefined,
|
target: download ? undefined : isExternal ? 'blank' : undefined,
|
||||||
rel: isExternal ? 'noopener noreferrer' : undefined,
|
rel: isExternal ? 'noopener noreferrer' : undefined,
|
||||||
download,
|
download,
|
||||||
},
|
},
|
||||||
dataSet: {
|
dataSet: {
|
||||||
// default to no underline, apply this ourselves
|
// no underline, only `InlineLink` has underlines
|
||||||
noUnderline: '1',
|
noUnderline: '1',
|
||||||
},
|
},
|
||||||
})}>
|
})}>
|
||||||
|
@ -200,13 +217,7 @@ export function Link({
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InlineLinkProps = React.PropsWithChildren<
|
export type InlineLinkProps = React.PropsWithChildren<
|
||||||
BaseLinkProps &
|
BaseLinkProps & TextStyleProp
|
||||||
TextStyleProp & {
|
|
||||||
/**
|
|
||||||
* Label for a11y. Defaults to the href.
|
|
||||||
*/
|
|
||||||
label?: string
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
|
|
||||||
export function InlineLink({
|
export function InlineLink({
|
||||||
|
@ -215,6 +226,8 @@ export function InlineLink({
|
||||||
action = 'push',
|
action = 'push',
|
||||||
warnOnMismatchingTextChild,
|
warnOnMismatchingTextChild,
|
||||||
style,
|
style,
|
||||||
|
onPress: outerOnPress,
|
||||||
|
download,
|
||||||
...rest
|
...rest
|
||||||
}: InlineLinkProps) {
|
}: InlineLinkProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
@ -224,18 +237,25 @@ export function InlineLink({
|
||||||
displayText: stringChildren ? children : '',
|
displayText: stringChildren ? children : '',
|
||||||
action,
|
action,
|
||||||
warnOnMismatchingTextChild,
|
warnOnMismatchingTextChild,
|
||||||
|
onPress: outerOnPress,
|
||||||
})
|
})
|
||||||
|
const {
|
||||||
|
state: hovered,
|
||||||
|
onIn: onHoverIn,
|
||||||
|
onOut: onHoverOut,
|
||||||
|
} = useInteractionState()
|
||||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||||
const {
|
const {
|
||||||
state: pressed,
|
state: pressed,
|
||||||
onIn: onPressIn,
|
onIn: onPressIn,
|
||||||
onOut: onPressOut,
|
onOut: onPressOut,
|
||||||
} = useInteractionState()
|
} = useInteractionState()
|
||||||
|
const flattenedStyle = flatten(style)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
onPress={onPress}
|
onPress={download ? undefined : onPress}
|
||||||
onPressIn={onPressIn}
|
onPressIn={onPressIn}
|
||||||
onPressOut={onPressOut}
|
onPressOut={onPressOut}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
|
@ -245,27 +265,28 @@ export function InlineLink({
|
||||||
{...rest}
|
{...rest}
|
||||||
style={[
|
style={[
|
||||||
{color: t.palette.primary_500},
|
{color: t.palette.primary_500},
|
||||||
(focused || pressed) && {
|
(hovered || focused || pressed) && {
|
||||||
outline: 0,
|
outline: 0,
|
||||||
textDecorationLine: 'underline',
|
textDecorationLine: 'underline',
|
||||||
textDecorationColor: t.palette.primary_500,
|
textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
|
||||||
},
|
},
|
||||||
flatten(style),
|
flattenedStyle,
|
||||||
]}
|
]}
|
||||||
role="link"
|
role="link"
|
||||||
|
onMouseEnter={onHoverIn}
|
||||||
|
onMouseLeave={onHoverOut}
|
||||||
accessibilityRole="link"
|
accessibilityRole="link"
|
||||||
href={href}
|
href={href}
|
||||||
{...web({
|
{...web({
|
||||||
hrefAttrs: {
|
hrefAttrs: {
|
||||||
target: isExternal ? 'blank' : undefined,
|
target: download ? undefined : isExternal ? 'blank' : undefined,
|
||||||
rel: isExternal ? 'noopener noreferrer' : undefined,
|
rel: isExternal ? 'noopener noreferrer' : undefined,
|
||||||
|
download,
|
||||||
|
},
|
||||||
|
dataSet: {
|
||||||
|
// default to no underline, apply this ourselves
|
||||||
|
noUnderline: '1',
|
||||||
},
|
},
|
||||||
dataSet: stringChildren
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
// default to no underline, apply this ourselves
|
|
||||||
noUnderline: '1',
|
|
||||||
},
|
|
||||||
})}>
|
})}>
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -19,9 +19,14 @@ export function Links() {
|
||||||
style={[a.text_md]}>
|
style={[a.text_md]}>
|
||||||
External
|
External
|
||||||
</InlineLink>
|
</InlineLink>
|
||||||
<InlineLink to="https://bsky.social" style={[a.text_md]}>
|
<InlineLink to="https://bsky.social" style={[a.text_md, t.atoms.text]}>
|
||||||
<H3>External with custom children</H3>
|
<H3>External with custom children</H3>
|
||||||
</InlineLink>
|
</InlineLink>
|
||||||
|
<InlineLink
|
||||||
|
to="https://bsky.social"
|
||||||
|
style={[a.text_md, t.atoms.text_contrast_low]}>
|
||||||
|
External with custom children
|
||||||
|
</InlineLink>
|
||||||
<InlineLink
|
<InlineLink
|
||||||
to="https://bsky.social"
|
to="https://bsky.social"
|
||||||
warnOnMismatchingTextChild
|
warnOnMismatchingTextChild
|
||||||
|
|
Loading…
Reference in New Issue