Add cursor to clickable elements (#491)

* Add cursor to clickable elements

* Add cursor to clickable elements

* Update per comments

* Fix word wrap in notifications

* Center the web login-load screen

* Add hover states on web

* Fix lint

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
zio/stable
Ollie Hsieh 2023-04-19 18:05:10 -07:00 committed by GitHub
parent 1472bd4f17
commit b24ba3adc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 184 additions and 142 deletions

View File

@ -1,7 +1,8 @@
import React from 'react' import React from 'react'
import {ActivityIndicator, StyleSheet, View} from 'react-native' import {ActivityIndicator, StyleSheet} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {CenteredView} from '../util/Views'
import {LoggedOut} from './LoggedOut' import {LoggedOut} from './LoggedOut'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@ -30,14 +31,14 @@ function Loading() {
}, [setIsTakingTooLong]) }, [setIsTakingTooLong])
return ( return (
<View style={[styles.loading, pal.view]}> <CenteredView style={[styles.loading, pal.view]}>
<ActivityIndicator size="large" /> <ActivityIndicator size="large" />
<Text type="2xl" style={[styles.loadingText, pal.textLight]}> <Text type="2xl" style={[styles.loadingText, pal.textLight]}>
{isTakingTooLong {isTakingTooLong
? "This is taking too long. There may be a problem with your internet or with the service, but we're going to try a couple more times..." ? "This is taking too long. There may be a problem with your internet or with the service, but we're going to try a couple more times..."
: 'Connecting...'} : 'Connecting...'}
</Text> </Text>
</View> </CenteredView>
) )
} }

View File

@ -3,7 +3,7 @@ import {observer} from 'mobx-react-lite'
import { import {
Animated, Animated,
TouchableOpacity, TouchableOpacity,
TouchableWithoutFeedback, Pressable,
StyleSheet, StyleSheet,
View, View,
} from 'react-native' } from 'react-native'
@ -124,7 +124,7 @@ export const FeedItem = observer(function FeedItem({
return <></> return <></>
} }
let authors: Author[] = [ const authors: Author[] = [
{ {
href: `/profile/${item.author.handle}`, href: `/profile/${item.author.handle}`,
handle: item.author.handle, handle: item.author.handle,
@ -132,18 +132,18 @@ export const FeedItem = observer(function FeedItem({
avatar: item.author.avatar, avatar: item.author.avatar,
labels: item.author.labels, labels: item.author.labels,
}, },
...(item.additional?.map(
({author: {avatar, labels, handle, displayName}}) => {
return {
href: `/profile/${handle}`,
handle,
displayName,
avatar,
labels,
}
},
) || []),
] ]
if (item.additional?.length) {
authors = authors.concat(
item.additional.map(item2 => ({
href: `/profile/${item2.author.handle}`,
handle: item2.author.handle,
displayName: item2.author.displayName,
avatar: item2.author.avatar,
labels: item2.author.labels,
})),
)
}
return ( return (
<Link <Link
@ -161,62 +161,52 @@ export const FeedItem = observer(function FeedItem({
href={itemHref} href={itemHref}
title={itemTitle} title={itemTitle}
noFeedback> noFeedback>
<View style={styles.layout}> <View style={styles.layoutIcon}>
<View style={styles.layoutIcon}> {icon === 'HeartIconSolid' ? (
{icon === 'HeartIconSolid' ? ( <HeartIconSolid size={28} style={[styles.icon, ...iconStyle]} />
<HeartIconSolid size={28} style={[styles.icon, ...iconStyle]} /> ) : (
) : ( <FontAwesomeIcon
<FontAwesomeIcon icon={icon}
icon={icon} size={24}
size={24} style={[styles.icon, ...iconStyle]}
style={[styles.icon, ...iconStyle]} />
)}
</View>
<View style={styles.layoutContent}>
<Pressable
onPress={authors.length > 1 ? onToggleAuthorsExpanded : () => {}}>
<CondensedAuthorsList
visible={!isAuthorsExpanded}
authors={authors}
onToggleAuthorsExpanded={onToggleAuthorsExpanded}
/>
<ExpandedAuthorsList visible={isAuthorsExpanded} authors={authors} />
<View style={styles.meta}>
<TextLink
key={authors[0].href}
style={[pal.text, s.bold, styles.metaItem]}
href={authors[0].href}
text={sanitizeDisplayName(
authors[0].displayName || authors[0].handle,
)}
/> />
)} {authors.length > 1 ? (
</View> <>
<View style={styles.layoutContent}> <Text style={[styles.metaItem, pal.text]}>and</Text>
<TouchableWithoutFeedback <Text style={[styles.metaItem, pal.text, s.bold]}>
onPress={authors.length > 1 ? onToggleAuthorsExpanded : () => {}}> {authors.length - 1} {pluralize(authors.length - 1, 'other')}
<View>
<CondensedAuthorsList
visible={!isAuthorsExpanded}
authors={authors}
onToggleAuthorsExpanded={onToggleAuthorsExpanded}
/>
<ExpandedAuthorsList
visible={isAuthorsExpanded}
authors={authors}
/>
<View style={styles.meta}>
<TextLink
key={authors[0].href}
style={[pal.text, s.bold, styles.metaItem]}
href={authors[0].href}
text={sanitizeDisplayName(
authors[0].displayName || authors[0].handle,
)}
/>
{authors.length > 1 ? (
<>
<Text style={[styles.metaItem, pal.text]}>and</Text>
<Text style={[styles.metaItem, pal.text, s.bold]}>
{authors.length - 1}{' '}
{pluralize(authors.length - 1, 'other')}
</Text>
</>
) : undefined}
<Text style={[styles.metaItem, pal.text]}>{action}</Text>
<Text style={[styles.metaItem, pal.textLight]}>
{ago(item.indexedAt)}
</Text> </Text>
</View> </>
</View> ) : undefined}
</TouchableWithoutFeedback> <Text style={[styles.metaItem, pal.text]}>{action}</Text>
{item.isLike || item.isRepost || item.isQuote ? ( <Text style={[styles.metaItem, pal.textLight]}>
<AdditionalPostText additionalPost={item.additionalPost} /> {ago(item.indexedAt)}
) : ( </Text>
<></> </View>
)} </Pressable>
</View> {item.isLike || item.isRepost || item.isQuote ? (
<AdditionalPostText additionalPost={item.additionalPost} />
) : null}
</View> </View>
</Link> </Link>
) )
@ -392,8 +382,6 @@ const styles = StyleSheet.create({
padding: 10, padding: 10,
paddingRight: 15, paddingRight: 15,
borderTopWidth: 1, borderTopWidth: 1,
},
layout: {
flexDirection: 'row', flexDirection: 'row',
}, },
layoutIcon: { layoutIcon: {
@ -405,6 +393,9 @@ const styles = StyleSheet.create({
marginRight: 10, marginRight: 10,
marginTop: 4, marginTop: 4,
}, },
layoutContent: {
flex: 1,
},
avis: { avis: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@ -413,9 +404,6 @@ const styles = StyleSheet.create({
fontWeight: 'bold', fontWeight: 'bold',
paddingLeft: 6, paddingLeft: 6,
}, },
layoutContent: {
flex: 1,
},
meta: { meta: {
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',

View File

@ -1,11 +1,7 @@
import React, {createRef, useState, useMemo, useRef} from 'react' import React, {createRef, useState, useMemo, useRef} from 'react'
import { import {Animated, StyleSheet, View} from 'react-native'
Animated,
StyleSheet,
TouchableWithoutFeedback,
View,
} from 'react-native'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {PressableWithHover} from '../util/PressableWithHover'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {isDesktopWeb} from 'platform/detection' import {isDesktopWeb} from 'platform/detection'
@ -109,20 +105,21 @@ export function TabBar({
{items.map((item, i) => { {items.map((item, i) => {
const selected = i === selectedPage const selected = i === selectedPage
return ( return (
<TouchableWithoutFeedback <PressableWithHover
key={i} ref={itemRefs[i]}
testID={testID ? `${testID}-${item}` : undefined} key={item}
style={
indicatorPosition === 'top' ? styles.itemTop : styles.itemBottom
}
hoverStyle={pal.viewLight}
onPress={() => onPressItem(i)}> onPress={() => onPressItem(i)}>
<View <Text
style={ type="xl-bold"
indicatorPosition === 'top' ? styles.itemTop : styles.itemBottom testID={testID ? `${testID}-${item}` : undefined}
} style={selected ? pal.text : pal.textLight}>
ref={itemRefs[i]}> {item}
<Text type="xl-bold" style={selected ? pal.text : pal.textLight}> </Text>
{item} </PressableWithHover>
</Text>
</View>
</TouchableWithoutFeedback>
) )
})} })}
</View> </View>
@ -138,18 +135,19 @@ const styles = isDesktopWeb
itemTop: { itemTop: {
paddingTop: 16, paddingTop: 16,
paddingBottom: 14, paddingBottom: 14,
marginRight: 24, paddingHorizontal: 12,
}, },
itemBottom: { itemBottom: {
paddingTop: 14, paddingTop: 14,
paddingBottom: 16, paddingBottom: 16,
marginRight: 24, paddingHorizontal: 12,
}, },
indicator: { indicator: {
position: 'absolute', position: 'absolute',
left: 0, left: 0,
width: 1, width: 1,
height: 3, height: 3,
zIndex: 1,
}, },
}) })
: StyleSheet.create({ : StyleSheet.create({

View File

@ -48,7 +48,6 @@ export function ProfileCard({
]} ]}
href={`/profile/${handle}`} href={`/profile/${handle}`}
title={handle} title={handle}
noFeedback
asAnchor> asAnchor>
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>

View File

@ -64,7 +64,7 @@ export const ProfileHeader = observer(
<View style={[styles.buttonsLine]}> <View style={[styles.buttonsLine]}>
<LoadingPlaceholder width={100} height={31} style={styles.br50} /> <LoadingPlaceholder width={100} height={31} style={styles.br50} />
</View> </View>
<View style={styles.displayNameLine}> <View>
<Text type="title-2xl" style={[pal.text, styles.title]}> <Text type="title-2xl" style={[pal.text, styles.title]}>
{sanitizeDisplayName(view.displayName || view.handle)} {sanitizeDisplayName(view.displayName || view.handle)}
</Text> </Text>
@ -273,7 +273,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
</DropdownButton> </DropdownButton>
) : undefined} ) : undefined}
</View> </View>
<View style={styles.displayNameLine}> <View>
<Text <Text
testID="profileHeaderDisplayName" testID="profileHeaderDisplayName"
type="title-2xl" type="title-2xl"
@ -431,11 +431,6 @@ const styles = StyleSheet.create({
borderRadius: 50, borderRadius: 50,
marginLeft: 6, marginLeft: 6,
}, },
displayNameLine: {
// paddingLeft: 86,
// marginBottom: 14,
},
title: {lineHeight: 38}, title: {lineHeight: 38},
handleLine: { handleLine: {

View File

@ -0,0 +1,45 @@
import React, {
useState,
useCallback,
PropsWithChildren,
forwardRef,
Ref,
} from 'react'
import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native'
import {addStyle} from 'lib/styles'
interface PressableWithHover extends PressableProps {
hoverStyle: StyleProp<ViewStyle>
}
export const PressableWithHover = forwardRef(
(
{
children,
style,
hoverStyle,
...props
}: PropsWithChildren<PressableWithHover>,
ref: Ref<any>,
) => {
const [isHovering, setIsHovering] = useState(false)
const onHoverIn = useCallback(() => setIsHovering(true), [setIsHovering])
const onHoverOut = useCallback(() => setIsHovering(false), [setIsHovering])
style =
typeof style !== 'function' && isHovering
? addStyle(style, hoverStyle)
: style
return (
<Pressable
{...props}
style={style}
onHoverIn={onHoverIn}
onHoverOut={onHoverOut}
ref={ref}>
{children}
</Pressable>
)
},
)

View File

@ -1,10 +1,5 @@
import React, {createRef, useState, useMemo, useRef} from 'react' import React, {createRef, useState, useMemo, useRef} from 'react'
import { import {Animated, Pressable, StyleSheet, View} from 'react-native'
Animated,
StyleSheet,
TouchableWithoutFeedback,
View,
} from 'react-native'
import {Text} from './text/Text' import {Text} from './text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@ -99,7 +94,7 @@ export function Selector({
{items.map((item, i) => { {items.map((item, i) => {
const selected = i === selectedIndex const selected = i === selectedIndex
return ( return (
<TouchableWithoutFeedback key={i} onPress={() => onPressItem(i)}> <Pressable key={item} onPress={() => onPressItem(i)}>
<View style={styles.item} ref={itemRefs[i]}> <View style={styles.item} ref={itemRefs[i]}>
<Text <Text
style={ style={
@ -110,7 +105,7 @@ export function Selector({
{item} {item}
</Text> </Text>
</View> </View>
</TouchableWithoutFeedback> </Pressable>
) )
})} })}
</View> </View>

View File

@ -40,7 +40,7 @@ export function ContentHider({
} }
if (labelPref.pref === 'hide') { if (labelPref.pref === 'hide') {
return <></> return null
} }
return ( return (

View File

@ -1,8 +1,8 @@
import React from 'react' import React, {useCallback} from 'react'
import { import {
Keyboard,
StyleSheet, StyleSheet,
TouchableWithoutFeedback, TouchableWithoutFeedback,
Keyboard,
View, View,
} from 'react-native' } from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
@ -26,6 +26,7 @@ import {s} from 'lib/styles'
import {ProfileCard} from 'view/com/profile/ProfileCard' import {ProfileCard} from 'view/com/profile/ProfileCard'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
import {isAndroid, isIOS} from 'platform/detection'
type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'> type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
export const SearchScreen = withAuthRequired( export const SearchScreen = withAuthRequired(
@ -110,8 +111,14 @@ export const SearchScreen = withAuthRequired(
}, [store, autocompleteView, foafs, suggestedActors, onSoftReset]), }, [store, autocompleteView, foafs, suggestedActors, onSoftReset]),
) )
const onPress = useCallback(() => {
if (isIOS || isAndroid) {
Keyboard.dismiss()
}
}, [])
return ( return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}> <TouchableWithoutFeedback onPress={onPress}>
<View style={[pal.view, styles.container]}> <View style={[pal.view, styles.container]}>
<HeaderWithInput <HeaderWithInput
isInputFocused={isInputFocused} isInputFocused={isInputFocused}
@ -139,16 +146,19 @@ export const SearchScreen = withAuthRequired(
scrollEventThrottle={100}> scrollEventThrottle={100}>
{query && autocompleteView.searchRes.length ? ( {query && autocompleteView.searchRes.length ? (
<> <>
{autocompleteView.searchRes.map(item => ( {autocompleteView.searchRes.map(
<ProfileCard ({did, handle, displayName, labels, avatar}, index) => (
key={item.did} <ProfileCard
testID={`searchAutoCompleteResult-${item.handle}`} key={did}
handle={item.handle} testID={`searchAutoCompleteResult-${handle}`}
displayName={item.displayName} handle={handle}
labels={item.labels} displayName={displayName}
avatar={item.avatar} labels={labels}
/> avatar={avatar}
))} noBorder={index === 0}
/>
),
)}
</> </>
) : query && !autocompleteView.searchRes.length ? ( ) : query && !autocompleteView.searchRes.length ? (
<View> <View>

View File

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {StyleSheet, TouchableOpacity, View} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {PressableWithHover} from 'view/com/util/PressableWithHover'
import {useNavigation, useNavigationState} from '@react-navigation/native' import {useNavigation, useNavigationState} from '@react-navigation/native'
import { import {
FontAwesomeIcon, FontAwesomeIcon,
@ -88,19 +89,23 @@ const NavItem = observer(
const isCurrent = isTab(currentRouteName, pathName) const isCurrent = isTab(currentRouteName, pathName)
return ( return (
<Link href={href} style={styles.navItem}> <PressableWithHover
<View style={[styles.navItemIconWrapper]}> style={styles.navItemWrapper}
{isCurrent ? iconFilled : icon} hoverStyle={pal.viewLight}>
{typeof count === 'number' && count > 0 && ( <Link href={href} style={styles.navItem}>
<Text type="button" style={styles.navItemCount}> <View style={[styles.navItemIconWrapper]}>
{count} {isCurrent ? iconFilled : icon}
</Text> {typeof count === 'number' && count > 0 && (
)} <Text type="button" style={styles.navItemCount}>
</View> {count}
<Text type="title" style={[isCurrent ? s.bold : s.normal, pal.text]}> </Text>
{label} )}
</Text> </View>
</Link> <Text type="title" style={[isCurrent ? s.bold : s.normal, pal.text]}>
{label}
</Text>
</Link>
</PressableWithHover>
) )
}, },
) )
@ -193,13 +198,14 @@ const styles = StyleSheet.create({
leftNav: { leftNav: {
position: 'absolute', position: 'absolute',
top: 10, top: 10,
right: 'calc(50vw + 300px)', right: 'calc(50vw + 312px)',
width: 220, width: 220,
}, },
profileCard: { profileCard: {
marginVertical: 10, marginVertical: 10,
width: 60, width: 60,
paddingLeft: 12,
}, },
backBtn: { backBtn: {
@ -210,11 +216,15 @@ const styles = StyleSheet.create({
height: 30, height: 30,
}, },
navItemWrapper: {
paddingHorizontal: 12,
borderRadius: 8,
},
navItem: { navItem: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingTop: 14, paddingTop: 12,
paddingBottom: 10, paddingBottom: 12,
}, },
navItemIconWrapper: { navItemIconWrapper: {
alignItems: 'center', alignItems: 'center',
@ -245,6 +255,7 @@ const styles = StyleSheet.create({
paddingVertical: 10, paddingVertical: 10,
paddingHorizontal: 16, paddingHorizontal: 16,
backgroundColor: colors.blue3, backgroundColor: colors.blue3,
marginLeft: 12,
marginTop: 20, marginTop: 20,
marginBottom: 10, marginBottom: 10,
}, },