about summary refs log tree commit diff
path: root/src/components/Dialog/index.web.tsx
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-01-18 20:28:04 -0600
committerGitHub <noreply@github.com>2024-01-18 20:28:04 -0600
commit66b8774ecb9c5d465987909577ddad3dd4a3ab8e (patch)
treeb1874c6cedd0111eca41db237e606f8e50739d55 /src/components/Dialog/index.web.tsx
parent9cbd3c0937d22e8dccbd9c086d3a3a24dbd27b3a (diff)
downloadvoidsky-66b8774ecb9c5d465987909577ddad3dd4a3ab8e.tar.zst
New component library based on ALF (#2459)
* Install on native as well

* Add button and link components

* Comments

* Use new prop

* Add some form elements

* Add labels to input

* Fix line height, add suffix

* Date inputs

* Autofill styles

* Clean up InputDate types

* Improve types for InputText, value handling

* Enforce a11y props on buttons

* Add Dialog, Portal

* Dialog contents

* Native dialog

* Clean up

* Fix animations

* Improvements to web modal, exiting still broken

* Clean up dialog types

* Add Prompt, Dialog refinement, mobile refinement

* Integrate new design tokens, reorg storybook

* Button colors

* Dim mode

* Reorg

* Some styles

* Toggles

* Improve a11y

* Autosize dialog, handle max height, Dialog.ScrolLView not working

* Try to use BottomSheet's own APIs

* Scrollable dialogs

* Add web shadow

* Handle overscroll

* Styles

* Dialog text input

* Shadows

* Button focus states

* Button pressed states

* Gradient poc

* Gradient colors and hovers

* Add hrefAttrs to Link

* Some more a11y

* Toggle invalid states

* Update dialog descriptions for demo

* Icons

* WIP Toggle cleanup

* Refactor toggle to not rely on immediate children

* Make Toggle controlled

* Clean up Toggles storybook

* ToggleButton styles

* Improve a11y labels

* ToggleButton hover darkmode

* Some i18n

* Refactor input

* Allow extension of input

* Remove old input

* Improve icons, add CalendarDays

* Refactor DateField, web done

* Add label example

* Clean up old InputDate, DateField android, text area example

* Consistent imports

* Button context, icons

* Add todo

* Add closeAllDialogs control

* Alignment

* Expand color palette

* Hitslops, add shortcut to Storybook in dev

* Fix multiline on ios

* Mark dialog close button as unused
Diffstat (limited to 'src/components/Dialog/index.web.tsx')
-rw-r--r--src/components/Dialog/index.web.tsx194
1 files changed, 194 insertions, 0 deletions
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
new file mode 100644
index 000000000..305c00e97
--- /dev/null
+++ b/src/components/Dialog/index.web.tsx
@@ -0,0 +1,194 @@
+import React, {useImperativeHandle} from 'react'
+import {View, TouchableWithoutFeedback} from 'react-native'
+import {FocusScope} from '@tamagui/focus-scope'
+import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useTheme, atoms as a, useBreakpoints, web} from '#/alf'
+import {Portal} from '#/components/Portal'
+
+import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
+import {Context} from '#/components/Dialog/context'
+
+export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
+export * from '#/components/Dialog/types'
+export {Input} from '#/components/forms/TextField'
+
+const stopPropagation = (e: any) => e.stopPropagation()
+
+export function Outer({
+  control,
+  onClose,
+  children,
+}: React.PropsWithChildren<DialogOuterProps>) {
+  const {_} = useLingui()
+  const t = useTheme()
+  const {gtMobile} = useBreakpoints()
+  const [isOpen, setIsOpen] = React.useState(false)
+  const [isVisible, setIsVisible] = React.useState(true)
+
+  const open = React.useCallback(() => {
+    setIsOpen(true)
+  }, [setIsOpen])
+
+  const close = React.useCallback(async () => {
+    setIsVisible(false)
+    await new Promise(resolve => setTimeout(resolve, 150))
+    setIsOpen(false)
+    setIsVisible(true)
+    onClose?.()
+  }, [onClose, setIsOpen])
+
+  useImperativeHandle(
+    control.ref,
+    () => ({
+      open,
+      close,
+    }),
+    [open, close],
+  )
+
+  React.useEffect(() => {
+    if (!isOpen) return
+
+    function handler(e: KeyboardEvent) {
+      if (e.key === 'Escape') close()
+    }
+
+    document.addEventListener('keydown', handler)
+
+    return () => document.removeEventListener('keydown', handler)
+  }, [isOpen, close])
+
+  const context = React.useMemo(
+    () => ({
+      close,
+    }),
+    [close],
+  )
+
+  return (
+    <>
+      {isOpen && (
+        <Portal>
+          <Context.Provider value={context}>
+            <TouchableWithoutFeedback
+              accessibilityHint={undefined}
+              accessibilityLabel={_(msg`Close active dialog`)}
+              onPress={close}>
+              <View
+                style={[
+                  web(a.fixed),
+                  a.inset_0,
+                  a.z_10,
+                  a.align_center,
+                  gtMobile ? a.p_lg : a.p_md,
+                  {overflowY: 'auto'},
+                ]}>
+                {isVisible && (
+                  <Animated.View
+                    entering={FadeIn.duration(150)}
+                    // exiting={FadeOut.duration(150)}
+                    style={[
+                      web(a.fixed),
+                      a.inset_0,
+                      {opacity: 0.5, backgroundColor: t.palette.black},
+                    ]}
+                  />
+                )}
+
+                <View
+                  style={[
+                    a.w_full,
+                    a.z_20,
+                    a.justify_center,
+                    a.align_center,
+                    {
+                      minHeight: web('calc(90vh - 36px)') || undefined,
+                    },
+                  ]}>
+                  {isVisible ? children : null}
+                </View>
+              </View>
+            </TouchableWithoutFeedback>
+          </Context.Provider>
+        </Portal>
+      )}
+    </>
+  )
+}
+
+export function Inner({
+  children,
+  style,
+  label,
+  accessibilityLabelledBy,
+  accessibilityDescribedBy,
+}: DialogInnerProps) {
+  const t = useTheme()
+  const {gtMobile} = useBreakpoints()
+  return (
+    <FocusScope loop enabled trapped>
+      <Animated.View
+        role="dialog"
+        aria-role="dialog"
+        aria-label={label}
+        aria-labelledby={accessibilityLabelledBy}
+        aria-describedby={accessibilityDescribedBy}
+        // @ts-ignore web only -prf
+        onClick={stopPropagation}
+        onStartShouldSetResponder={_ => true}
+        onTouchEnd={stopPropagation}
+        entering={FadeInDown.duration(100)}
+        // exiting={FadeOut.duration(100)}
+        style={[
+          a.relative,
+          a.rounded_md,
+          a.w_full,
+          a.border,
+          gtMobile ? a.p_xl : a.p_lg,
+          t.atoms.bg,
+          {
+            maxWidth: 600,
+            borderColor: t.palette.contrast_200,
+            shadowColor: t.palette.black,
+            shadowOpacity: t.name === 'light' ? 0.1 : 0.4,
+            shadowRadius: 30,
+          },
+          ...(Array.isArray(style) ? style : [style || {}]),
+        ]}>
+        {children}
+      </Animated.View>
+    </FocusScope>
+  )
+}
+
+export const ScrollableInner = Inner
+
+export function Handle() {
+  return null
+}
+
+/**
+ * TODO(eric) unused rn
+ */
+// export function Close() {
+//   const {_} = useLingui()
+//   const t = useTheme()
+//   const {close} = useDialogContext()
+//   return (
+//     <View
+//       style={[
+//         a.absolute,
+//         a.z_10,
+//         {
+//           top: a.pt_lg.paddingTop,
+//           right: a.pr_lg.paddingRight,
+//         },
+//       ]}>
+//       <Button onPress={close} label={_(msg`Close active dialog`)}>
+//       </Button>
+//     </View>
+//   )
+// }