about summary refs log tree commit diff
path: root/src/components/Menu
diff options
context:
space:
mode:
authorMinseo Lee <itoupluk427@gmail.com>2024-03-13 10:30:07 +0900
committerGitHub <noreply@github.com>2024-03-13 10:30:07 +0900
commit3ead08ab2649533583c300904bbd85c250292014 (patch)
tree48366b4d1d847eb59f0187520d2c5fb4150bba3c /src/components/Menu
parent2456ca828fc4ba05a085fa03c6f7c37b3edcd45e (diff)
parent653240bc056236489e8a7882b7b6f902ed0885c2 (diff)
downloadvoidsky-3ead08ab2649533583c300904bbd85c250292014.tar.zst
Merge branch 'bluesky-social:main' into patch-3
Diffstat (limited to 'src/components/Menu')
-rw-r--r--src/components/Menu/index.tsx35
-rw-r--r--src/components/Menu/index.web.tsx96
-rw-r--r--src/components/Menu/types.ts33
3 files changed, 133 insertions, 31 deletions
diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx
index ee96a5667..9be9dd86b 100644
--- a/src/components/Menu/index.tsx
+++ b/src/components/Menu/index.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {View, Pressable} from 'react-native'
+import {View, Pressable, ViewStyle, StyleProp} from 'react-native'
 import flattenReactChildren from 'react-keyed-flatten-children'
 
 import {atoms as a, useTheme} from '#/alf'
@@ -16,6 +16,10 @@ import {
   ItemTextProps,
   ItemIconProps,
 } from '#/components/Menu/types'
+import {Button, ButtonText} from '#/components/Button'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {isNative} from 'platform/detection'
 
 export {useDialogControl as useMenuControl} from '#/components/Dialog'
 
@@ -68,7 +72,13 @@ export function Trigger({children, label}: TriggerProps) {
   })
 }
 
-export function Outer({children}: React.PropsWithChildren<{}>) {
+export function Outer({
+  children,
+  showCancel,
+}: React.PropsWithChildren<{
+  showCancel?: boolean
+  style?: StyleProp<ViewStyle>
+}>) {
   const context = React.useContext(Context)
 
   return (
@@ -78,7 +88,10 @@ export function Outer({children}: React.PropsWithChildren<{}>) {
       {/* Re-wrap with context since Dialogs are portal-ed to root */}
       <Context.Provider value={context}>
         <Dialog.ScrollableInner label="Menu TODO">
-          <View style={[a.gap_lg]}>{children}</View>
+          <View style={[a.gap_lg]}>
+            {children}
+            {isNative && showCancel && <Cancel />}
+          </View>
           <View style={{height: a.gap_lg.gap}} />
         </Dialog.ScrollableInner>
       </Context.Provider>
@@ -185,6 +198,22 @@ export function Group({children, style}: GroupProps) {
   )
 }
 
+function Cancel() {
+  const {_} = useLingui()
+  const {control} = React.useContext(Context)
+
+  return (
+    <Button
+      label={_(msg`Close this dialog`)}
+      size="small"
+      variant="ghost"
+      color="secondary"
+      onPress={() => control.close()}>
+      <ButtonText>Cancel</ButtonText>
+    </Button>
+  )
+}
+
 export function Divider() {
   return null
 }
diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx
index 054e51b01..f4b03f680 100644
--- a/src/components/Menu/index.web.tsx
+++ b/src/components/Menu/index.web.tsx
@@ -1,6 +1,10 @@
+/* eslint-disable react/prop-types */
+
 import React from 'react'
-import {View, Pressable} from 'react-native'
+import {View, Pressable, ViewStyle, StyleProp} from 'react-native'
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 import * as Dialog from '#/components/Dialog'
 import {useInteractionState} from '#/components/hooks/useInteractionState'
@@ -14,8 +18,10 @@ import {
   GroupProps,
   ItemTextProps,
   ItemIconProps,
+  RadixPassThroughTriggerProps,
 } from '#/components/Menu/types'
 import {Context} from '#/components/Menu/context'
+import {Portal} from '#/components/Portal'
 
 export function useMenuControl(): Dialog.DialogControlProps {
   const id = React.useId()
@@ -47,6 +53,7 @@ export function Root({
 }: React.PropsWithChildren<{
   control?: Dialog.DialogOuterProps['control']
 }>) {
+  const {_} = useLingui()
   const defaultControl = useMenuControl()
   const context = React.useMemo<ContextType>(
     () => ({
@@ -67,6 +74,18 @@ export function Root({
 
   return (
     <Context.Provider value={context}>
+      {context.control.isOpen && (
+        <Portal>
+          <Pressable
+            style={[a.fixed, a.inset_0, a.z_50]}
+            onPress={() => context.control.close()}
+            accessibilityHint=""
+            accessibilityLabel={_(
+              msg`Context menu backdrop, click to close the menu.`,
+            )}
+          />
+        </Portal>
+      )}
       <DropdownMenu.Root
         open={context.control.isOpen}
         onOpenChange={onOpenChange}>
@@ -76,7 +95,24 @@ export function Root({
   )
 }
 
-export function Trigger({children, label, style}: TriggerProps) {
+const RadixTriggerPassThrough = React.forwardRef(
+  (
+    props: {
+      children: (
+        props: RadixPassThroughTriggerProps & {
+          ref: React.Ref<any>
+        },
+      ) => React.ReactNode
+    },
+    ref,
+  ) => {
+    // @ts-expect-error Radix provides no types of this stuff
+    return props.children({...props, ref})
+  },
+)
+RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
+
+export function Trigger({children, label}: TriggerProps) {
   const {control} = React.useContext(Context)
   const {
     state: hovered,
@@ -87,33 +123,42 @@ export function Trigger({children, label, style}: TriggerProps) {
 
   return (
     <DropdownMenu.Trigger asChild>
-      <Pressable
-        accessibilityHint=""
-        accessibilityLabel={label}
-        onFocus={onFocus}
-        onBlur={onBlur}
-        style={flatten([style, focused && web({outline: 0})])}
-        onPointerDown={() => control.open()}
-        {...web({
-          onMouseEnter,
-          onMouseLeave,
-        })}>
-        {children({
-          isNative: false,
-          control,
-          state: {
-            hovered,
-            focused,
-            pressed: false,
-          },
-          props: {},
-        })}
-      </Pressable>
+      <RadixTriggerPassThrough>
+        {props =>
+          children({
+            isNative: false,
+            control,
+            state: {
+              hovered,
+              focused,
+              pressed: false,
+            },
+            props: {
+              ...props,
+              // disable on web, use `onPress`
+              onPointerDown: () => false,
+              onPress: () =>
+                control.isOpen ? control.close() : control.open(),
+              onFocus: onFocus,
+              onBlur: onBlur,
+              onMouseEnter,
+              onMouseLeave,
+              accessibilityLabel: label,
+            },
+          })
+        }
+      </RadixTriggerPassThrough>
     </DropdownMenu.Trigger>
   )
 }
 
-export function Outer({children}: React.PropsWithChildren<{}>) {
+export function Outer({
+  children,
+  style,
+}: React.PropsWithChildren<{
+  showCancel?: boolean
+  style?: StyleProp<ViewStyle>
+}>) {
   const t = useTheme()
 
   return (
@@ -125,6 +170,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) {
             a.p_xs,
             t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
             t.atoms.shadow_md,
+            style,
           ]}>
           {children}
         </View>
diff --git a/src/components/Menu/types.ts b/src/components/Menu/types.ts
index 2f52e6390..e710971ee 100644
--- a/src/components/Menu/types.ts
+++ b/src/components/Menu/types.ts
@@ -1,5 +1,9 @@
 import React from 'react'
-import {GestureResponderEvent, PressableProps} from 'react-native'
+import {
+  GestureResponderEvent,
+  PressableProps,
+  AccessibilityProps,
+} from 'react-native'
 
 import {Props as SVGIconProps} from '#/components/icons/common'
 import * as Dialog from '#/components/Dialog'
@@ -9,7 +13,23 @@ export type ContextType = {
   control: Dialog.DialogOuterProps['control']
 }
 
-export type TriggerProps = ViewStyleProp & {
+export type RadixPassThroughTriggerProps = {
+  id: string
+  type: 'button'
+  disabled: boolean
+  ['data-disabled']: boolean
+  ['data-state']: string
+  ['aria-controls']?: string
+  ['aria-haspopup']?: boolean
+  ['aria-expanded']?: AccessibilityProps['aria-expanded']
+  onKeyDown: (e: React.KeyboardEvent) => void
+  /**
+   * Radix provides this, but we override on web to use `onPress` instead,
+   * which is less sensitive while scrolling.
+   */
+  onPointerDown: PressableProps['onPointerDown']
+}
+export type TriggerProps = {
   children(props: TriggerChildProps): React.ReactNode
   label: string
 }
@@ -52,7 +72,14 @@ export type TriggerChildProps =
          */
         pressed: false
       }
-      props: {}
+      props: RadixPassThroughTriggerProps & {
+        onPress: () => void
+        onFocus: () => void
+        onBlur: () => void
+        onMouseEnter: () => void
+        onMouseLeave: () => void
+        accessibilityLabel: string
+      }
     }
 
 export type ItemProps = React.PropsWithChildren<