Update hashtag menu to use Menu, convert to native link for additional a11y and click handling (#7529)
* Make tag a normal link on web * Replace old TagMenu with new RichTextTag component, expand and improve click utils * Clarify intents * Ensure we're passing down hint * ope * DRY
This commit is contained in:
+108
-18
@@ -15,7 +15,6 @@ import {
|
||||
linkRequiresWarning,
|
||||
} from '#/lib/strings/url-helpers'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import {shouldClickOpenNewTab} from '#/platform/urls'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonProps} from '#/components/Button'
|
||||
@@ -55,6 +54,12 @@ type BaseLinkProps = Pick<
|
||||
*/
|
||||
onPress?: (e: GestureResponderEvent) => void | false
|
||||
|
||||
/**
|
||||
* Callback for when the link is long pressed (on native). Prevent default
|
||||
* and return `false` to exit early and prevent default long press hander.
|
||||
*/
|
||||
onLongPress?: (e: GestureResponderEvent) => void | false
|
||||
|
||||
/**
|
||||
* Web-only attribute. Sets `download` attr on web.
|
||||
*/
|
||||
@@ -72,6 +77,7 @@ export function useLink({
|
||||
action = 'push',
|
||||
disableMismatchWarning,
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
shareOnLongPress,
|
||||
}: BaseLinkProps & {
|
||||
displayText: string
|
||||
@@ -175,8 +181,14 @@ export function useLink({
|
||||
}
|
||||
}, [disableMismatchWarning, displayText, href, isExternal, openModal])
|
||||
|
||||
const onLongPress =
|
||||
isNative && isExternal && shareOnLongPress ? handleLongPress : undefined
|
||||
const onLongPress = React.useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
const exitEarlyIfFalse = outerOnLongPress?.(e)
|
||||
if (exitEarlyIfFalse === false) return
|
||||
return isNative && shareOnLongPress ? handleLongPress() : undefined
|
||||
},
|
||||
[outerOnLongPress, handleLongPress, shareOnLongPress],
|
||||
)
|
||||
|
||||
return {
|
||||
isExternal,
|
||||
@@ -202,14 +214,16 @@ export function Link({
|
||||
to,
|
||||
action = 'push',
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
download,
|
||||
...rest
|
||||
}: LinkProps) {
|
||||
const {href, isExternal, onPress} = useLink({
|
||||
const {href, isExternal, onPress, onLongPress} = useLink({
|
||||
to,
|
||||
displayText: typeof children === 'string' ? children : '',
|
||||
action,
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -220,6 +234,7 @@ export function Link({
|
||||
accessibilityRole="link"
|
||||
href={href}
|
||||
onPress={download ? undefined : onPress}
|
||||
onLongPress={onLongPress}
|
||||
{...web({
|
||||
hrefAttrs: {
|
||||
target: download ? undefined : isExternal ? 'blank' : undefined,
|
||||
@@ -241,7 +256,7 @@ export type InlineLinkProps = React.PropsWithChildren<
|
||||
TextStyleProp &
|
||||
Pick<TextProps, 'selectable' | 'numberOfLines'>
|
||||
> &
|
||||
Pick<ButtonProps, 'label'> & {
|
||||
Pick<ButtonProps, 'label' | 'accessibilityHint'> & {
|
||||
disableUnderline?: boolean
|
||||
title?: TextProps['title']
|
||||
}
|
||||
@@ -253,6 +268,7 @@ export function InlineLinkText({
|
||||
disableMismatchWarning,
|
||||
style,
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
download,
|
||||
selectable,
|
||||
label,
|
||||
@@ -268,6 +284,7 @@ export function InlineLinkText({
|
||||
action,
|
||||
disableMismatchWarning,
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
shareOnLongPress,
|
||||
})
|
||||
const {
|
||||
@@ -319,6 +336,21 @@ export function InlineLinkText({
|
||||
)
|
||||
}
|
||||
|
||||
export function WebOnlyInlineLinkText({
|
||||
children,
|
||||
to,
|
||||
onPress,
|
||||
...props
|
||||
}: Omit<InlineLinkProps, 'onLongPress'>) {
|
||||
return isWeb ? (
|
||||
<InlineLinkText {...props} to={to} onPress={onPress}>
|
||||
{children}
|
||||
</InlineLinkText>
|
||||
) : (
|
||||
<Text {...props}>{children}</Text>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to create a static `onPress` handler for a `Link` that would otherwise link to a URI
|
||||
*
|
||||
@@ -327,7 +359,10 @@ export function InlineLinkText({
|
||||
*/
|
||||
export function createStaticClick(
|
||||
onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
|
||||
): Pick<BaseLinkProps, 'to' | 'onPress'> {
|
||||
): {
|
||||
to: BaseLinkProps['to']
|
||||
onPress: Exclude<BaseLinkProps['onPress'], undefined>
|
||||
} {
|
||||
return {
|
||||
to: '#',
|
||||
onPress(e: GestureResponderEvent) {
|
||||
@@ -338,17 +373,72 @@ export function createStaticClick(
|
||||
}
|
||||
}
|
||||
|
||||
export function WebOnlyInlineLinkText({
|
||||
children,
|
||||
to,
|
||||
onPress,
|
||||
...props
|
||||
}: InlineLinkProps) {
|
||||
return isWeb ? (
|
||||
<InlineLinkText {...props} to={to} onPress={onPress}>
|
||||
{children}
|
||||
</InlineLinkText>
|
||||
) : (
|
||||
<Text {...props}>{children}</Text>
|
||||
/**
|
||||
* Utility to create a static `onPress` handler for a `Link`, but only if the
|
||||
* click was not modified in some way e.g. `Cmd` or a middle click.
|
||||
*
|
||||
* On native, this behaves the same as `createStaticClick` because there are no
|
||||
* options to "modify" the click in this sense.
|
||||
*
|
||||
* Example:
|
||||
* `<Link {...createStaticClick(e => {...})} />`
|
||||
*/
|
||||
export function createStaticClickIfUnmodified(
|
||||
onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
|
||||
): {onPress: Exclude<BaseLinkProps['onPress'], undefined>} {
|
||||
return {
|
||||
onPress(e: GestureResponderEvent) {
|
||||
if (!isWeb || !isModifiedClickEvent(e)) {
|
||||
e.preventDefault()
|
||||
onPressHandler(e)
|
||||
return false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the click event has a meta key pressed, indicating the user
|
||||
* intends to deviate from default behavior.
|
||||
*/
|
||||
export function isClickEventWithMetaKey(e: GestureResponderEvent) {
|
||||
if (!isWeb) return false
|
||||
const event = e as unknown as MouseEvent
|
||||
return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the web click target is anything other than `_self`
|
||||
*/
|
||||
export function isClickTargetExternal(e: GestureResponderEvent) {
|
||||
if (!isWeb) return false
|
||||
const event = e as unknown as MouseEvent
|
||||
const el = event.currentTarget as HTMLAnchorElement
|
||||
return el && el.target && el.target !== '_self'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a click event has been modified in some way from its default
|
||||
* behavior, e.g. `Cmd` or a middle click.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
|
||||
*/
|
||||
export function isModifiedClickEvent(e: GestureResponderEvent): boolean {
|
||||
if (!isWeb) return false
|
||||
const event = e as unknown as MouseEvent
|
||||
const isPrimaryButton = event.button === 0
|
||||
return (
|
||||
isClickEventWithMetaKey(e) || isClickTargetExternal(e) || !isPrimaryButton
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a click event has been modified in a way that should indiciate
|
||||
* that the user intends to open a new tab.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
|
||||
*/
|
||||
export function shouldClickOpenNewTab(e: GestureResponderEvent) {
|
||||
if (!isWeb) return false
|
||||
const event = e as unknown as MouseEvent
|
||||
const isMiddleClick = isWeb && event.button === 1
|
||||
return isClickEventWithMetaKey(e) || isClickTargetExternal(e) || isMiddleClick
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user