diff --git a/src/components/BottomSheetButton.tsx b/src/components/BottomSheetButton.tsx index a2efa2c49b..9eaee55deb 100644 --- a/src/components/BottomSheetButton.tsx +++ b/src/components/BottomSheetButton.tsx @@ -1,110 +1,12 @@ import React from 'react' -import {AccessibilityProps, ViewStyle} from 'react-native' -import {PressableEvent} from 'react-native-gesture-handler/lib/typescript/components/Pressable/PressableProps' -import { - BlueskyBottomSheetPressable, - BlueskyBottomSheetPressableProps, -} from '@haileyok/bluesky-bottom-sheet' -import {atoms as a} from '#/alf' -import { - ButtonContext, - useSharedButtonStyles, - VariantProps, -} from '#/components/Button' - -export function BottomSheetButton({ - children, - label, - color, - variant, - shape = 'default', - size, - disabled, - style, - ...rest -}: BlueskyBottomSheetPressableProps & - AccessibilityProps & - VariantProps & { - /** - * For a11y, try to make this descriptive and clear - */ - label: string - }) { - const [state, setState] = React.useState({ - pressed: false, - hovered: false, - focused: false, - }) - - const onPressInOuter = rest.onPressIn - const onPressIn = React.useCallback( - (e: PressableEvent) => { - setState(s => ({ - ...s, - pressed: true, - })) - onPressInOuter?.(e) - }, - [setState, onPressInOuter], - ) - const onPressOutOuter = rest.onPressOut - const onPressOut = React.useCallback( - (e: PressableEvent) => { - setState(s => ({ - ...s, - pressed: false, - })) - onPressOutOuter?.(e) - }, - [setState, onPressOutOuter], - ) - - const {baseStyles, hoverStyles} = useSharedButtonStyles({ - /** - * For a11y, try to make this descriptive and clear - */ - color, - variant, - shape, - size, - disabled, - }) - - const context = React.useMemo( - () => ({ - ...state, - variant, - color, - size, - disabled: disabled || false, - }), - [state, variant, color, size, disabled], - ) +import {Button, ButtonProps} from '#/components/Button' +import {NormalizedRNGHPressable} from '#/components/NormalizedRNGHPressable' +export function BottomSheetButton({children, ...rest}: ButtonProps) { return ( - - - {typeof children === 'function' ? children(context) : children} - - + ) } diff --git a/src/components/BottomSheetLink.tsx b/src/components/BottomSheetLink.tsx index 6b9f4ebd96..36d52c90c1 100644 --- a/src/components/BottomSheetLink.tsx +++ b/src/components/BottomSheetLink.tsx @@ -1,5 +1,4 @@ import React from 'react' -import {BlueskyBottomSheetPressable} from '@haileyok/bluesky-bottom-sheet' import {StackActions, useNavigation} from '@react-navigation/native' import {NavigationProp} from '#/lib/routes/types' @@ -7,6 +6,7 @@ import {flatten, useTheme} from '#/alf' import {useDialogContext} from '#/components/Dialog' import {useInteractionState} from '#/components/hooks/useInteractionState' import {InlineLinkProps, useLink} from '#/components/Link' +import {NormalizedRNGHPressable} from '#/components/NormalizedRNGHPressable' import {Text} from '#/components/Typography' import {router} from '#/routes' @@ -64,7 +64,7 @@ export function BottomSheetInlineLinkText({ // eslint-disable-next-line bsky-internal/avoid-unwrapped-text return ( - {children} - + ) } diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 17db0332e0..98ad1709c4 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -15,7 +15,9 @@ import { import {LinearGradient} from 'expo-linear-gradient' import {atoms as a, flatten, select, tokens, useTheme, web} from '#/alf' +import {useDialogContext} from '#/components/Dialog' import {Props as SVGIconProps} from '#/components/icons/common' +import {NormalizedRNGHPressable} from '#/components/NormalizedRNGHPressable' import {Text} from '#/components/Typography' export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' @@ -87,10 +89,10 @@ export type ButtonProps = Pick< style?: StyleProp hoverStyle?: StyleProp children: NonTextElements | ((context: ButtonContext) => NonTextElements) + Component?: React.ComponentType } -export type ButtonTextProps = TextProps & - VariantProps & {disabled?: boolean | null} +export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} const Context = React.createContext({ hovered: false, @@ -98,7 +100,6 @@ const Context = React.createContext({ pressed: false, disabled: false, }) -export const ButtonContext = Context export function useButtonContext() { return React.useContext(Context) @@ -116,10 +117,23 @@ export const Button = React.forwardRef( disabled = false, style, hoverStyle: hoverStyleProp, + Component, ...rest }, ref, ) => { + // This will pick the correct default pressable to use. If we are inside a dialog, we need to use the RNGH + // pressable so that it is usable inside the dialog. + const {insideDialog} = useDialogContext() + if (!Component) { + if (insideDialog) { + Component = NormalizedRNGHPressable + } else { + Component = Pressable + } + } + + const t = useTheme() const [state, setState] = React.useState({ pressed: false, hovered: false, @@ -183,13 +197,226 @@ export const Button = React.forwardRef( })) }, [setState]) - const {baseStyles, hoverStyles} = useSharedButtonStyles({ - color, - variant, - disabled, - shape, - size, - }) + const {baseStyles, hoverStyles} = React.useMemo(() => { + const baseStyles: ViewStyle[] = [] + const hoverStyles: ViewStyle[] = [] + + if (color === 'primary') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.primary_500, + }) + hoverStyles.push({ + backgroundColor: t.palette.primary_600, + }) + } else { + baseStyles.push({ + backgroundColor: select(t.name, { + light: t.palette.primary_700, + dim: t.palette.primary_300, + dark: t.palette.primary_300, + }), + }) + } + } else if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, + }) + + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.primary_500, + }) + hoverStyles.push(a.border, { + backgroundColor: t.palette.primary_50, + }) + } else { + baseStyles.push(a.border, { + borderColor: t.palette.primary_200, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: t.palette.primary_100, + }) + } + } + } else if (color === 'secondary') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push(t.atoms.bg_contrast_25) + hoverStyles.push(t.atoms.bg_contrast_50) + } else { + baseStyles.push(t.atoms.bg_contrast_100) + } + } else if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, + }) + + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.contrast_300, + }) + hoverStyles.push(t.atoms.bg_contrast_50) + } else { + baseStyles.push(a.border, { + borderColor: t.palette.contrast_200, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: t.palette.contrast_25, + }) + } + } + } else if (color === 'secondary_inverted') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.contrast_900, + }) + hoverStyles.push({ + backgroundColor: t.palette.contrast_950, + }) + } else { + baseStyles.push({ + backgroundColor: t.palette.contrast_600, + }) + } + } else if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, + }) + + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.contrast_300, + }) + hoverStyles.push(t.atoms.bg_contrast_50) + } else { + baseStyles.push(a.border, { + borderColor: t.palette.contrast_200, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: t.palette.contrast_25, + }) + } + } + } else if (color === 'negative') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.negative_500, + }) + hoverStyles.push({ + backgroundColor: t.palette.negative_600, + }) + } else { + baseStyles.push({ + backgroundColor: select(t.name, { + light: t.palette.negative_700, + dim: t.palette.negative_300, + dark: t.palette.negative_300, + }), + }) + } + } else if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, + }) + + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.negative_500, + }) + hoverStyles.push(a.border, { + backgroundColor: t.palette.negative_50, + }) + } else { + baseStyles.push(a.border, { + borderColor: t.palette.negative_200, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: t.palette.negative_100, + }) + } + } + } + + if (shape === 'default') { + if (size === 'large') { + baseStyles.push({ + paddingVertical: 13, + paddingHorizontal: 20, + borderRadius: 8, + gap: 8, + }) + } else if (size === 'small') { + baseStyles.push({ + paddingVertical: 8, + paddingHorizontal: 12, + borderRadius: 6, + gap: 6, + }) + } else if (size === 'tiny') { + baseStyles.push({ + paddingVertical: 4, + paddingHorizontal: 8, + borderRadius: 4, + gap: 4, + }) + } + } else if (shape === 'round' || shape === 'square') { + if (size === 'large') { + if (shape === 'round') { + baseStyles.push({height: 46, width: 46}) + } else { + baseStyles.push({height: 44, width: 44}) + } + } else if (size === 'small') { + if (shape === 'round') { + baseStyles.push({height: 36, width: 36}) + } else { + baseStyles.push({height: 34, width: 34}) + } + } else if (size === 'tiny') { + if (shape === 'round') { + baseStyles.push({height: 22, width: 22}) + } else { + baseStyles.push({height: 21, width: 21}) + } + } + + if (shape === 'round') { + baseStyles.push(a.rounded_full) + } else if (shape === 'square') { + if (size === 'tiny') { + baseStyles.push(a.rounded_xs) + } else { + baseStyles.push(a.rounded_sm) + } + } + } + + return { + baseStyles, + hoverStyles, + } + }, [t, variant, color, size, shape, disabled]) const {gradientColors, gradientHoverColors, gradientLocations} = React.useMemo(() => { @@ -237,10 +464,11 @@ export const Button = React.forwardRef( const flattenedBaseStyles = flatten([baseStyles, style]) return ( - ( {typeof children === 'function' ? children(context) : children} - + ) }, ) Button.displayName = 'Button' -export function useSharedButtonStyles({ - color, - variant, - disabled, - shape, - size, -}: VariantProps & {disabled?: boolean | null}) { - const t = useTheme() - - const {baseStyles, hoverStyles} = React.useMemo(() => { - const baseStyles: ViewStyle[] = [] - const hoverStyles: ViewStyle[] = [] - - if (color === 'primary') { - if (variant === 'solid') { - if (!disabled) { - baseStyles.push({ - backgroundColor: t.palette.primary_500, - }) - hoverStyles.push({ - backgroundColor: t.palette.primary_600, - }) - } else { - baseStyles.push({ - backgroundColor: select(t.name, { - light: t.palette.primary_700, - dim: t.palette.primary_300, - dark: t.palette.primary_300, - }), - }) - } - } else if (variant === 'outline') { - baseStyles.push(a.border, t.atoms.bg, { - borderWidth: 1, - }) - - if (!disabled) { - baseStyles.push(a.border, { - borderColor: t.palette.primary_500, - }) - hoverStyles.push(a.border, { - backgroundColor: t.palette.primary_50, - }) - } else { - baseStyles.push(a.border, { - borderColor: t.palette.primary_200, - }) - } - } else if (variant === 'ghost') { - if (!disabled) { - baseStyles.push(t.atoms.bg) - hoverStyles.push({ - backgroundColor: t.palette.primary_100, - }) - } - } - } else if (color === 'secondary') { - if (variant === 'solid') { - if (!disabled) { - baseStyles.push(t.atoms.bg_contrast_25) - hoverStyles.push(t.atoms.bg_contrast_50) - } else { - baseStyles.push(t.atoms.bg_contrast_100) - } - } else if (variant === 'outline') { - baseStyles.push(a.border, t.atoms.bg, { - borderWidth: 1, - }) - - if (!disabled) { - baseStyles.push(a.border, { - borderColor: t.palette.contrast_300, - }) - hoverStyles.push(t.atoms.bg_contrast_50) - } else { - baseStyles.push(a.border, { - borderColor: t.palette.contrast_200, - }) - } - } else if (variant === 'ghost') { - if (!disabled) { - baseStyles.push(t.atoms.bg) - hoverStyles.push({ - backgroundColor: t.palette.contrast_25, - }) - } - } - } else if (color === 'secondary_inverted') { - if (variant === 'solid') { - if (!disabled) { - baseStyles.push({ - backgroundColor: t.palette.contrast_900, - }) - hoverStyles.push({ - backgroundColor: t.palette.contrast_950, - }) - } else { - baseStyles.push({ - backgroundColor: t.palette.contrast_600, - }) - } - } else if (variant === 'outline') { - baseStyles.push(a.border, t.atoms.bg, { - borderWidth: 1, - }) - - if (!disabled) { - baseStyles.push(a.border, { - borderColor: t.palette.contrast_300, - }) - hoverStyles.push(t.atoms.bg_contrast_50) - } else { - baseStyles.push(a.border, { - borderColor: t.palette.contrast_200, - }) - } - } else if (variant === 'ghost') { - if (!disabled) { - baseStyles.push(t.atoms.bg) - hoverStyles.push({ - backgroundColor: t.palette.contrast_25, - }) - } - } - } else if (color === 'negative') { - if (variant === 'solid') { - if (!disabled) { - baseStyles.push({ - backgroundColor: t.palette.negative_500, - }) - hoverStyles.push({ - backgroundColor: t.palette.negative_600, - }) - } else { - baseStyles.push({ - backgroundColor: select(t.name, { - light: t.palette.negative_700, - dim: t.palette.negative_300, - dark: t.palette.negative_300, - }), - }) - } - } else if (variant === 'outline') { - baseStyles.push(a.border, t.atoms.bg, { - borderWidth: 1, - }) - - if (!disabled) { - baseStyles.push(a.border, { - borderColor: t.palette.negative_500, - }) - hoverStyles.push(a.border, { - backgroundColor: t.palette.negative_50, - }) - } else { - baseStyles.push(a.border, { - borderColor: t.palette.negative_200, - }) - } - } else if (variant === 'ghost') { - if (!disabled) { - baseStyles.push(t.atoms.bg) - hoverStyles.push({ - backgroundColor: t.palette.negative_100, - }) - } - } - } - - if (shape === 'default') { - if (size === 'large') { - baseStyles.push({ - paddingVertical: 13, - paddingHorizontal: 20, - borderRadius: 8, - gap: 8, - }) - } else if (size === 'small') { - baseStyles.push({ - paddingVertical: 8, - paddingHorizontal: 12, - borderRadius: 6, - gap: 6, - }) - } else if (size === 'tiny') { - baseStyles.push({ - paddingVertical: 4, - paddingHorizontal: 8, - borderRadius: 4, - gap: 4, - }) - } - } else if (shape === 'round' || shape === 'square') { - if (size === 'large') { - if (shape === 'round') { - baseStyles.push({height: 46, width: 46}) - } else { - baseStyles.push({height: 44, width: 44}) - } - } else if (size === 'small') { - if (shape === 'round') { - baseStyles.push({height: 36, width: 36}) - } else { - baseStyles.push({height: 34, width: 34}) - } - } else if (size === 'tiny') { - if (shape === 'round') { - baseStyles.push({height: 22, width: 22}) - } else { - baseStyles.push({height: 21, width: 21}) - } - } - - if (shape === 'round') { - baseStyles.push(a.rounded_full) - } else if (shape === 'square') { - if (size === 'tiny') { - baseStyles.push(a.rounded_xs) - } else { - baseStyles.push(a.rounded_sm) - } - } - } - - return { - baseStyles, - hoverStyles, - } - }, [t, variant, color, size, shape, disabled]) - - return {baseStyles, hoverStyles} -} - export function useSharedButtonTextStyles() { const t = useTheme() const {color, variant, disabled, size} = useButtonContext() diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts index 859f8edd77..27b8a56375 100644 --- a/src/components/Dialog/context.ts +++ b/src/components/Dialog/context.ts @@ -9,6 +9,7 @@ import { export const Context = React.createContext({ close: () => {}, + insideDialog: false, }) export function useDialogContext() { diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 73b592ff27..7493e7748b 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -56,7 +56,6 @@ export function OuterWithoutPortal({ const insets = useSafeAreaInsets() const closeCallbacks = React.useRef<(() => void)[]>([]) const {setDialogIsOpen} = useDialogStateControlContext() - // @TODO DIALOG REFACTOR - can i get rid of this? seems pointless tbh const callQueuedCallbacks = React.useCallback(() => { for (const cb of closeCallbacks.current) { @@ -104,21 +103,13 @@ export function OuterWithoutPortal({ [open, close], ) - // @TODO DIALOG REFACTOR - what is this? rm i think? - // React.useEffect(() => { - // return () => { - // setDialogIsOpen(control.id, false) - // } - // }, [control.id, setDialogIsOpen]) - - const context = React.useMemo(() => ({close}), [close]) + const context = React.useMemo(() => ({close, insideDialog: true}), [close]) const Wrapper = isIOS ? View : GestureHandlerRootView return ( {children} - + ) } diff --git a/src/components/Menu/types.ts b/src/components/Menu/types.ts index 75cc613679..2f7aea5de5 100644 --- a/src/components/Menu/types.ts +++ b/src/components/Menu/types.ts @@ -4,8 +4,6 @@ import { GestureResponderEvent, PressableProps, } from 'react-native' -import {PressableEvent} from 'react-native-gesture-handler/lib/typescript/components/Pressable/PressableProps' -import {BlueskyBottomSheetPressableProps} from '@haileyok/bluesky-bottom-sheet' import {TextStyleProp, ViewStyleProp} from '#/alf' import * as Dialog from '#/components/Dialog' @@ -89,10 +87,10 @@ export type TriggerChildProps = } export type ItemProps = React.PropsWithChildren< - Omit & + Omit & ViewStyleProp & { label: string - onPress: (e: PressableEvent | GestureResponderEvent) => void + onPress: (e: GestureResponderEvent) => void } > diff --git a/src/components/NormalizedRNGHPressable.tsx b/src/components/NormalizedRNGHPressable.tsx new file mode 100644 index 0000000000..351005619d --- /dev/null +++ b/src/components/NormalizedRNGHPressable.tsx @@ -0,0 +1,128 @@ +import React from 'react' +import { + GestureResponderEvent, + MeasureOnSuccessCallback, + NativeMouseEvent, + NativeSyntheticEvent, + PressableProps, +} from 'react-native' +import {Pressable as BSPressable} from 'react-native-gesture-handler' +import {PressableEvent} from 'react-native-gesture-handler/lib/typescript/components/Pressable/PressableProps' + +function pressableEventToGestureResponderEvent( + event: PressableEvent, + target: NormalizedRNGHPressable, +): GestureResponderEvent { + return { + nativeEvent: { + ...event.nativeEvent, + touches: [], + changedTouches: [], + identifier: event.nativeEvent.identifier.toString(), + target: event.nativeEvent.target.toString(), + }, + // @ts-expect-error + target: target, + // @ts-expect-error + currentTarget: target, + preventDefault() {}, + stopPropagation() {}, + cancelable: false, + defaultPrevented: false, + eventPhase: 0, + isTrusted: false, + bubbles: false, + timeStamp: event.nativeEvent.timestamp, + isDefaultPrevented(): boolean { + return false + }, + isPropagationStopped(): boolean { + return false + }, + persist() {}, + type: 'press', + } +} + +function pressableEventToMouseEvent( + event: PressableEvent, + target: NormalizedRNGHPressable, +): MouseEvent & NativeSyntheticEvent { + return { + ...event.nativeEvent, + // @ts-expect-error + target: target, + // @ts-expect-error + currentTarget: target, + preventDefault() {}, + stopPropagation() {}, + cancelable: false, + defaultPrevented: false, + eventPhase: 0, + isTrusted: false, + bubbles: false, + timeStamp: event.nativeEvent.timestamp, + } +} + +export class NormalizedRNGHPressable extends React.Component { + static displayName = 'Pressable' + + measure = (_: MeasureOnSuccessCallback) => {} + + measureLayout = (_: number) => {} + + measureInWindow = ( + _: (x: number, y: number, width: number, height: number) => void, + ) => {} + + setNativeProps = (_: PressableProps) => {} + + focus = () => {} + + blur = () => {} + + onPress = (event: PressableEvent) => { + if (!this.props.onPress) return + this.props.onPress(pressableEventToGestureResponderEvent(event, this)) + } + + onLongPress = (event: PressableEvent) => { + if (!this.props.onLongPress) return + this.props.onLongPress(pressableEventToGestureResponderEvent(event, this)) + } + + onPressIn = (event: PressableEvent) => { + if (!this.props.onPressIn) return + this.props.onPressIn(pressableEventToGestureResponderEvent(event, this)) + } + + onPressOut = (event: PressableEvent) => { + if (!this.props.onPressOut) return + this.props.onPressOut(pressableEventToGestureResponderEvent(event, this)) + } + + onHoverIn = (event: PressableEvent) => { + if (!this.props.onHoverIn) return + this.props.onHoverIn(pressableEventToMouseEvent(event, this)) + } + + onHoverOut = (event: PressableEvent) => { + if (!this.props.onHoverOut) return + this.props.onHoverOut(pressableEventToMouseEvent(event, this)) + } + + render() { + return ( + + ) + } +} diff --git a/src/components/NormalizedRNGHPressable.web.tsx b/src/components/NormalizedRNGHPressable.web.tsx new file mode 100644 index 0000000000..86f5730495 --- /dev/null +++ b/src/components/NormalizedRNGHPressable.web.tsx @@ -0,0 +1,3 @@ +import {Pressable as NormalizedRNGHPressable} from 'react-native' + +export {NormalizedRNGHPressable}