diff options
author | Eric Bailey <git@esb.lol> | 2024-01-18 20:28:04 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-18 20:28:04 -0600 |
commit | 66b8774ecb9c5d465987909577ddad3dd4a3ab8e (patch) | |
tree | b1874c6cedd0111eca41db237e606f8e50739d55 /src/components/Link.tsx | |
parent | 9cbd3c0937d22e8dccbd9c086d3a3a24dbd27b3a (diff) | |
download | voidsky-66b8774ecb9c5d465987909577ddad3dd4a3ab8e.tar.zst |
New component library based on ALF (#2459)
* Install on native as well * Add button and link components * Comments * Use new prop * Add some form elements * Add labels to input * Fix line height, add suffix * Date inputs * Autofill styles * Clean up InputDate types * Improve types for InputText, value handling * Enforce a11y props on buttons * Add Dialog, Portal * Dialog contents * Native dialog * Clean up * Fix animations * Improvements to web modal, exiting still broken * Clean up dialog types * Add Prompt, Dialog refinement, mobile refinement * Integrate new design tokens, reorg storybook * Button colors * Dim mode * Reorg * Some styles * Toggles * Improve a11y * Autosize dialog, handle max height, Dialog.ScrolLView not working * Try to use BottomSheet's own APIs * Scrollable dialogs * Add web shadow * Handle overscroll * Styles * Dialog text input * Shadows * Button focus states * Button pressed states * Gradient poc * Gradient colors and hovers * Add hrefAttrs to Link * Some more a11y * Toggle invalid states * Update dialog descriptions for demo * Icons * WIP Toggle cleanup * Refactor toggle to not rely on immediate children * Make Toggle controlled * Clean up Toggles storybook * ToggleButton styles * Improve a11y labels * ToggleButton hover darkmode * Some i18n * Refactor input * Allow extension of input * Remove old input * Improve icons, add CalendarDays * Refactor DateField, web done * Add label example * Clean up old InputDate, DateField android, text area example * Consistent imports * Button context, icons * Add todo * Add closeAllDialogs control * Alignment * Expand color palette * Hitslops, add shortcut to Storybook in dev * Fix multiline on ios * Mark dialog close button as unused
Diffstat (limited to 'src/components/Link.tsx')
-rw-r--r-- | src/components/Link.tsx | 191 |
1 files changed, 191 insertions, 0 deletions
diff --git a/src/components/Link.tsx b/src/components/Link.tsx new file mode 100644 index 000000000..8f686f3c4 --- /dev/null +++ b/src/components/Link.tsx @@ -0,0 +1,191 @@ +import React from 'react' +import { + Text, + TextStyle, + StyleProp, + GestureResponderEvent, + Linking, +} from 'react-native' +import { + useLinkProps, + useNavigation, + StackActions, +} from '@react-navigation/native' +import {sanitizeUrl} from '@braintree/sanitize-url' + +import {isWeb} from '#/platform/detection' +import {useTheme, web, flatten} from '#/alf' +import {Button, ButtonProps, useButtonContext} from '#/components/Button' +import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types' +import { + convertBskyAppUrlIfNeeded, + isExternalUrl, + linkRequiresWarning, +} from '#/lib/strings/url-helpers' +import {useModalControls} from '#/state/modals' +import {router} from '#/routes' + +export type LinkProps = Omit< + ButtonProps, + 'style' | 'onPress' | 'disabled' | 'label' +> & { + /** + * `TextStyle` to apply to the anchor element itself. Does not apply to any children. + */ + style?: StyleProp<TextStyle> + /** + * The React Navigation `StackAction` to perform when the link is pressed. + */ + action?: 'push' | 'replace' | 'navigate' + /** + * If true, will warn the user if the link text does not match the href. Only + * works for Links with children that are strings i.e. text links. + */ + warnOnMismatchingTextChild?: boolean + label?: ButtonProps['label'] +} & Pick<Parameters<typeof useLinkProps<AllNavigatorParams>>[0], 'to'> + +/** + * A interactive element that renders as a `<a>` tag on the web. On mobile it + * will translate the `href` to navigator screens and params and dispatch a + * navigation action. + * + * Intended to behave as a web anchor tag. For more complex routing, use a + * `Button`. + */ +export function Link({ + children, + to, + action = 'push', + warnOnMismatchingTextChild, + style, + ...rest +}: LinkProps) { + const navigation = useNavigation<NavigationProp>() + const {href} = useLinkProps<AllNavigatorParams>({ + to: + typeof to === 'string' ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) : to, + }) + const isExternal = isExternalUrl(href) + const {openModal, closeModal} = useModalControls() + const onPress = React.useCallback( + (e: GestureResponderEvent) => { + const stringChildren = typeof children === 'string' ? children : '' + const requiresWarning = Boolean( + warnOnMismatchingTextChild && + stringChildren && + isExternal && + linkRequiresWarning(href, stringChildren), + ) + + if (requiresWarning) { + e.preventDefault() + + openModal({ + name: 'link-warning', + text: stringChildren, + href: href, + }) + } else { + e.preventDefault() + + if (isExternal) { + Linking.openURL(href) + } else { + /** + * A `GestureResponderEvent`, but cast to `any` to avoid using a bunch + * of @ts-ignore below. + */ + const event = e as any + const isMiddleClick = isWeb && event.button === 1 + const isMetaKey = + isWeb && + (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) + const shouldOpenInNewTab = isMetaKey || isMiddleClick + + if ( + shouldOpenInNewTab || + href.startsWith('http') || + href.startsWith('mailto') + ) { + Linking.openURL(href) + } else { + closeModal() // close any active modals + + if (action === 'push') { + navigation.dispatch(StackActions.push(...router.matchPath(href))) + } else if (action === 'replace') { + navigation.dispatch( + StackActions.replace(...router.matchPath(href)), + ) + } else if (action === 'navigate') { + // @ts-ignore + navigation.navigate(...router.matchPath(href)) + } else { + throw Error('Unsupported navigator action.') + } + } + } + } + }, + [ + href, + isExternal, + warnOnMismatchingTextChild, + navigation, + action, + children, + closeModal, + openModal, + ], + ) + + return ( + <Button + label={href} + {...rest} + role="link" + accessibilityRole="link" + href={href} + onPress={onPress} + {...web({ + hrefAttrs: { + target: isExternal ? 'blank' : undefined, + rel: isExternal ? 'noopener noreferrer' : undefined, + }, + dataSet: { + // default to no underline, apply this ourselves + noUnderline: '1', + }, + })}> + {typeof children === 'string' ? ( + <LinkText style={style}>{children}</LinkText> + ) : ( + children + )} + </Button> + ) +} + +function LinkText({ + children, + style, +}: React.PropsWithChildren<{ + style?: StyleProp<TextStyle> +}>) { + const t = useTheme() + const {hovered} = useButtonContext() + return ( + <Text + style={[ + {color: t.palette.primary_500}, + hovered && { + textDecorationLine: 'underline', + textDecorationColor: t.palette.primary_500, + }, + flatten(style), + ]}> + {children as string} + </Text> + ) +} |