diff options
Diffstat (limited to 'src/view/com/util/forms')
-rw-r--r-- | src/view/com/util/forms/DateInput.tsx | 13 | ||||
-rw-r--r-- | src/view/com/util/forms/DropdownButton.tsx | 8 | ||||
-rw-r--r-- | src/view/com/util/forms/NativeDropdown.web.tsx | 241 | ||||
-rw-r--r-- | src/view/com/util/forms/PostDropdownBtn.tsx | 34 | ||||
-rw-r--r-- | src/view/com/util/forms/SearchInput.tsx | 6 |
5 files changed, 286 insertions, 16 deletions
diff --git a/src/view/com/util/forms/DateInput.tsx b/src/view/com/util/forms/DateInput.tsx index 4aa5cb610..c5f0afc8f 100644 --- a/src/view/com/util/forms/DateInput.tsx +++ b/src/view/com/util/forms/DateInput.tsx @@ -13,6 +13,9 @@ import {Text} from '../text/Text' import {TypographyVariant} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext' import {usePalette} from 'lib/hooks/usePalette' +import {getLocales} from 'expo-localization' + +const LOCALE = getLocales()[0] interface Props { testID?: string @@ -25,6 +28,7 @@ interface Props { accessibilityLabel: string accessibilityHint: string accessibilityLabelledBy?: string + handleAsUTC?: boolean } export function DateInput(props: Props) { @@ -32,6 +36,12 @@ export function DateInput(props: Props) { const theme = useTheme() const pal = usePalette('default') + const formatter = React.useMemo(() => { + return new Intl.DateTimeFormat(LOCALE.languageTag, { + timeZone: props.handleAsUTC ? 'UTC' : undefined, + }) + }, [props.handleAsUTC]) + const onChangeInternal = useCallback( (event: DateTimePickerEvent, date: Date | undefined) => { setShow(false) @@ -64,7 +74,7 @@ export function DateInput(props: Props) { <Text type={props.buttonLabelType} style={[pal.text, props.buttonLabelStyle]}> - {props.value.toLocaleDateString()} + {formatter.format(props.value)} </Text> </View> </Button> @@ -73,6 +83,7 @@ export function DateInput(props: Props) { <DateTimePicker testID={props.testID ? `${props.testID}-datepicker` : undefined} mode="date" + timeZoneName={props.handleAsUTC ? 'Etc/UTC' : undefined} display="spinner" // @ts-ignore applies in iOS only -prf themeVariant={theme.colorScheme} diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx index ad8f50f5e..411b77484 100644 --- a/src/view/com/util/forms/DropdownButton.tsx +++ b/src/view/com/util/forms/DropdownButton.tsx @@ -75,6 +75,8 @@ export function DropdownButton({ bottomOffset = 0, accessibilityLabel, }: PropsWithChildren<DropdownButtonProps>) { + const {_} = useLingui() + const ref1 = useRef<TouchableOpacity>(null) const ref2 = useRef<View>(null) @@ -141,7 +143,9 @@ export function DropdownButton({ hitSlop={HITSLOP_10} ref={ref1} accessibilityRole="button" - accessibilityLabel={accessibilityLabel || `Opens ${numItems} options`} + accessibilityLabel={ + accessibilityLabel || _(msg`Opens ${numItems} options`) + } accessibilityHint=""> {children} </TouchableOpacity> @@ -247,7 +251,7 @@ const DropdownItems = ({ onPress={() => onPressItem(index)} accessibilityRole="button" accessibilityLabel={item.label} - accessibilityHint={`Option ${index + 1} of ${numItems}`}> + accessibilityHint={_(msg`Option ${index + 1} of ${numItems}`)}> {item.icon && ( <FontAwesomeIcon style={styles.icon} diff --git a/src/view/com/util/forms/NativeDropdown.web.tsx b/src/view/com/util/forms/NativeDropdown.web.tsx new file mode 100644 index 000000000..9e9888ad8 --- /dev/null +++ b/src/view/com/util/forms/NativeDropdown.web.tsx @@ -0,0 +1,241 @@ +import React from 'react' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import {Pressable, StyleSheet, View, Text} from 'react-native' +import {IconProp} from '@fortawesome/fontawesome-svg-core' +import {MenuItemCommonProps} from 'zeego/lib/typescript/menu' +import {usePalette} from 'lib/hooks/usePalette' +import {useTheme} from 'lib/ThemeContext' +import {HITSLOP_10} from 'lib/constants' + +// Custom Dropdown Menu Components +// == +export const DropdownMenuRoot = DropdownMenu.Root +export const DropdownMenuContent = DropdownMenu.Content + +type ItemProps = React.ComponentProps<(typeof DropdownMenu)['Item']> +export const DropdownMenuItem = (props: ItemProps & {testID?: string}) => { + const theme = useTheme() + const [focused, setFocused] = React.useState(false) + const backgroundColor = theme.colorScheme === 'dark' ? '#fff1' : '#0001' + + return ( + <DropdownMenu.Item + {...props} + style={StyleSheet.flatten([ + styles.item, + focused && {backgroundColor: backgroundColor}, + ])} + onFocus={() => { + setFocused(true) + }} + onBlur={() => { + setFocused(false) + }} + /> + ) +} + +// Types for Dropdown Menu and Items +export type DropdownItem = { + label: string | 'separator' + onPress?: () => void + testID?: string + icon?: { + ios: MenuItemCommonProps['ios'] + android: string + web: IconProp + } +} +type Props = { + items: DropdownItem[] + testID?: string + accessibilityLabel?: string + accessibilityHint?: string +} + +export function NativeDropdown({ + items, + children, + testID, + accessibilityLabel, + accessibilityHint, +}: React.PropsWithChildren<Props>) { + const pal = usePalette('default') + const theme = useTheme() + const dropDownBackgroundColor = + theme.colorScheme === 'dark' ? pal.btn : pal.view + const [open, setOpen] = React.useState(false) + const buttonRef = React.useRef<HTMLButtonElement>(null) + const menuRef = React.useRef<HTMLDivElement>(null) + const {borderColor: separatorColor} = + theme.colorScheme === 'dark' ? pal.borderDark : pal.border + + React.useEffect(() => { + function clickHandler(e: MouseEvent) { + const t = e.target + + if (!open) return + if (!t) return + if (!buttonRef.current || !menuRef.current) return + + if ( + t !== buttonRef.current && + !buttonRef.current.contains(t as Node) && + t !== menuRef.current && + !menuRef.current.contains(t as Node) + ) { + // prevent clicking through to links beneath dropdown + // only applies to mobile web + e.preventDefault() + e.stopPropagation() + + // close menu + setOpen(false) + } + } + + function keydownHandler(e: KeyboardEvent) { + if (e.key === 'Escape' && open) { + setOpen(false) + } + } + + document.addEventListener('click', clickHandler, true) + window.addEventListener('keydown', keydownHandler, true) + return () => { + document.removeEventListener('click', clickHandler, true) + window.removeEventListener('keydown', keydownHandler, true) + } + }, [open, setOpen]) + + return ( + <DropdownMenuRoot open={open} onOpenChange={o => setOpen(o)}> + <DropdownMenu.Trigger asChild onPointerDown={e => e.preventDefault()}> + <Pressable + ref={buttonRef as unknown as React.Ref<View>} + testID={testID} + accessibilityRole="button" + accessibilityLabel={accessibilityLabel} + accessibilityHint={accessibilityHint} + onPress={() => setOpen(o => !o)} + hitSlop={HITSLOP_10}> + {children} + </Pressable> + </DropdownMenu.Trigger> + + <DropdownMenu.Portal> + <DropdownMenu.Content + ref={menuRef} + style={ + StyleSheet.flatten([ + styles.content, + dropDownBackgroundColor, + ]) as React.CSSProperties + } + loop> + {items.map((item, index) => { + if (item.label === 'separator') { + return ( + <DropdownMenu.Separator + key={getKey(item.label, index, item.testID)} + style={ + StyleSheet.flatten([ + styles.separator, + {backgroundColor: separatorColor}, + ]) as React.CSSProperties + } + /> + ) + } + if (index > 1 && items[index - 1].label === 'separator') { + return ( + <DropdownMenu.Group + key={getKey(item.label, index, item.testID)}> + <DropdownMenuItem + key={getKey(item.label, index, item.testID)} + onSelect={item.onPress}> + <Text + selectable={false} + style={[pal.text, styles.itemTitle]}> + {item.label} + </Text> + {item.icon && ( + <FontAwesomeIcon + icon={item.icon.web} + size={20} + color={pal.colors.textLight} + /> + )} + </DropdownMenuItem> + </DropdownMenu.Group> + ) + } + return ( + <DropdownMenuItem + key={getKey(item.label, index, item.testID)} + onSelect={item.onPress}> + <Text selectable={false} style={[pal.text, styles.itemTitle]}> + {item.label} + </Text> + {item.icon && ( + <FontAwesomeIcon + icon={item.icon.web} + size={20} + color={pal.colors.textLight} + /> + )} + </DropdownMenuItem> + ) + })} + </DropdownMenu.Content> + </DropdownMenu.Portal> + </DropdownMenuRoot> + ) +} + +const getKey = (label: string, index: number, id?: string) => { + if (id) { + return id + } + return `${label}_${index}` +} + +const styles = StyleSheet.create({ + separator: { + height: 1, + marginTop: 4, + marginBottom: 4, + }, + content: { + backgroundColor: '#f0f0f0', + borderRadius: 8, + paddingTop: 4, + paddingBottom: 4, + paddingLeft: 4, + paddingRight: 4, + marginTop: 6, + + // @ts-ignore web only -prf + boxShadow: 'rgba(0, 0, 0, 0.3) 0px 5px 20px', + }, + item: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + columnGap: 20, + // @ts-ignore -web + cursor: 'pointer', + paddingTop: 8, + paddingBottom: 8, + paddingLeft: 12, + paddingRight: 12, + borderRadius: 8, + }, + itemTitle: { + fontSize: 16, + fontWeight: '500', + paddingRight: 10, + }, +}) diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 1f2e067c2..b21caf2e7 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -2,7 +2,12 @@ import React, {memo} from 'react' import {Linking, StyleProp, View, ViewStyle} from 'react-native' import Clipboard from '@react-native-clipboard/clipboard' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {AppBskyActorDefs, AppBskyFeedPost, AtUri} from '@atproto/api' +import { + AppBskyActorDefs, + AppBskyFeedPost, + AtUri, + RichText as RichTextAPI, +} from '@atproto/api' import {toShareUrl} from 'lib/strings/url-helpers' import {useTheme} from 'lib/ThemeContext' import {shareUrl} from 'lib/sharing' @@ -24,6 +29,7 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useSession} from '#/state/session' import {isWeb} from '#/platform/detection' +import {richTextToString} from '#/lib/strings/rich-text-helpers' let PostDropdownBtn = ({ testID, @@ -31,6 +37,7 @@ let PostDropdownBtn = ({ postCid, postUri, record, + richText, style, showAppealLabelItem, }: { @@ -39,6 +46,7 @@ let PostDropdownBtn = ({ postCid: string postUri: string record: AppBskyFeedPost.Record + richText: RichTextAPI style?: StyleProp<ViewStyle> showAppealLabelItem?: boolean }): React.ReactNode => { @@ -71,32 +79,36 @@ let PostDropdownBtn = ({ const onDeletePost = React.useCallback(() => { postDeleteMutation.mutateAsync({uri: postUri}).then( () => { - Toast.show('Post deleted') + Toast.show(_(msg`Post deleted`)) }, e => { logger.error('Failed to delete post', {error: e}) - Toast.show('Failed to delete post, please try again') + Toast.show(_(msg`Failed to delete post, please try again`)) }, ) - }, [postUri, postDeleteMutation]) + }, [postUri, postDeleteMutation, _]) const onToggleThreadMute = React.useCallback(() => { try { const muted = toggleThreadMute(rootUri) if (muted) { - Toast.show('You will no longer receive notifications for this thread') + Toast.show( + _(msg`You will no longer receive notifications for this thread`), + ) } else { - Toast.show('You will now receive notifications for this thread') + Toast.show(_(msg`You will now receive notifications for this thread`)) } } catch (e) { logger.error('Failed to toggle thread mute', {error: e}) } - }, [rootUri, toggleThreadMute]) + }, [rootUri, toggleThreadMute, _]) const onCopyPostText = React.useCallback(() => { - Clipboard.setString(record?.text || '') - Toast.show('Copied to clipboard') - }, [record]) + const str = richTextToString(richText, true) + + Clipboard.setString(str) + Toast.show(_(msg`Copied to clipboard`)) + }, [_, richText]) const onOpenTranslate = React.useCallback(() => { Linking.openURL(translatorUrl) @@ -253,7 +265,7 @@ let PostDropdownBtn = ({ <NativeDropdown testID={testID} items={dropdownItems} - accessibilityLabel="More post options" + accessibilityLabel={_(msg`More post options`)} accessibilityHint=""> <View style={style}> <FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} /> diff --git a/src/view/com/util/forms/SearchInput.tsx b/src/view/com/util/forms/SearchInput.tsx index 02b462b55..a78d23c9b 100644 --- a/src/view/com/util/forms/SearchInput.tsx +++ b/src/view/com/util/forms/SearchInput.tsx @@ -11,6 +11,7 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' +import {HITSLOP_10} from 'lib/constants' import {MagnifyingGlassIcon} from 'lib/icons' import {useTheme} from 'lib/ThemeContext' import {usePalette} from 'lib/hooks/usePalette' @@ -49,7 +50,7 @@ export function SearchInput({ <TextInput testID="searchTextInput" ref={textInput} - placeholder="Search" + placeholder={_(msg`Search`)} placeholderTextColor={pal.colors.textLight} selectTextOnFocus returnKeyType="search" @@ -71,7 +72,8 @@ export function SearchInput({ onPress={onPressCancelSearchInner} accessibilityRole="button" accessibilityLabel={_(msg`Clear search query`)} - accessibilityHint=""> + accessibilityHint="" + hitSlop={HITSLOP_10}> <FontAwesomeIcon icon="xmark" size={16} |