about summary refs log tree commit diff
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-02-12 21:18:26 +0000
committerGitHub <noreply@github.com>2025-02-12 21:18:26 +0000
commit5d3e2e14679b3d8eafdf9a563727ec46a7a370ea (patch)
tree15c3857e2706085105e26a8a06eac4cd96ad2d04
parent521a764d4f896518af7f668e2d196b720461ec13 (diff)
downloadvoidsky-5d3e2e14679b3d8eafdf9a563727ec46a7a370ea.tar.zst
Better animations for dialogs, animate web composer (#7703)
* animation atoms, use for modals

* respect reduced motion

* simplify animtions

* fix atoms
-rw-r--r--src/alf/atoms.ts20
-rw-r--r--src/components/Dialog/index.web.tsx24
-rw-r--r--src/style.css9
-rw-r--r--src/view/shell/Composer.web.tsx33
4 files changed, 58 insertions, 28 deletions
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index a7cf6cb3f..6982de75f 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -965,6 +965,26 @@ export const atoms = {
     transitionDelay: '50ms',
   }),
 
+  /*
+   * Animaations
+   */
+  fade_in: web({
+    animation: 'fadeIn ease-out 0.15s',
+  }),
+  fade_out: web({
+    animation: 'fadeOut ease-out 0.15s',
+  }),
+  zoom_in: web({
+    animation: 'zoomIn ease-out 0.1s',
+  }),
+  zoom_out: web({
+    animation: 'zoomOut ease-out 0.1s',
+  }),
+  // special composite animation for dialogs
+  zoom_fade_in: web({
+    animation: 'zoomIn ease-out 0.1s, fadeIn ease-out 0.1s',
+  }),
+
   /**
    * {@link Layout.SCROLLBAR_OFFSET}
    */
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index e45133dc5..9f4f8bb3f 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -15,6 +15,7 @@ import {FocusScope} from '@radix-ui/react-focus-scope'
 import {RemoveScrollBar} from 'react-remove-scroll-bar'
 
 import {logger} from '#/logger'
+import {useA11y} from '#/state/a11y'
 import {useDialogStateControlContext} from '#/state/dialogs'
 import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf'
 import {Button, ButtonIcon} from '#/components/Button'
@@ -152,6 +153,7 @@ export function Inner({
   const t = useTheme()
   const {close} = React.useContext(Context)
   const {gtMobile} = useBreakpoints()
+  const {reduceMotionEnabled} = useA11y()
   useFocusGuards()
   return (
     <FocusScope loop asChild trapped>
@@ -161,7 +163,7 @@ export function Inner({
         aria-label={label}
         aria-labelledby={accessibilityLabelledBy}
         aria-describedby={accessibilityDescribedBy}
-        // @ts-ignore web only -prf
+        // @ts-expect-error web only -prf
         onClick={stopPropagation}
         onStartShouldSetResponder={_ => true}
         onTouchEnd={stopPropagation}
@@ -177,10 +179,9 @@ export function Inner({
             shadowColor: t.palette.black,
             shadowOpacity: t.name === 'light' ? 0.1 : 0.4,
             shadowRadius: 30,
-            // @ts-ignore web only
-            animation: 'fadeIn ease-out 0.1s',
           },
-          flatten(style),
+          !reduceMotionEnabled && a.zoom_fade_in,
+          style,
         ])}>
         <DismissableLayer
           onInteractOutside={preventDefault}
@@ -216,7 +217,7 @@ export const InnerFlatList = React.forwardRef<
       style={[
         a.overflow_hidden,
         a.px_0,
-        // @ts-ignore web only -sfn
+        // @ts-expect-error web only -sfn
         {maxHeight: 'calc(-36px + 100vh)'},
         webInnerStyle,
       ]}
@@ -262,20 +263,15 @@ export function Handle() {
 
 function Backdrop() {
   const t = useTheme()
+  const {reduceMotionEnabled} = useA11y()
   return (
-    <View
-      style={{
-        opacity: 0.8,
-      }}>
+    <View style={{opacity: 0.8}}>
       <View
         style={[
           a.fixed,
           a.inset_0,
-          {
-            backgroundColor: t.palette.black,
-            // @ts-ignore web only
-            animation: 'fadeIn ease-out 0.15s',
-          },
+          {backgroundColor: t.palette.black},
+          !reduceMotionEnabled && a.fade_in,
         ]}
       />
     </View>
diff --git a/src/style.css b/src/style.css
index 96c51014f..770cb5e00 100644
--- a/src/style.css
+++ b/src/style.css
@@ -205,6 +205,15 @@ input:focus {
   }
 }
 
+@keyframes zoomIn {
+  from {
+    transform: scale(0.95);
+  }
+  to {
+    transform: scale(1);
+  }
+}
+
 .force-no-clicks > *,
 .force-no-clicks * {
   pointer-events: none !important;
diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx
index 80a112705..b76e88372 100644
--- a/src/view/shell/Composer.web.tsx
+++ b/src/view/shell/Composer.web.tsx
@@ -5,6 +5,7 @@ import {useFocusGuards} from '@radix-ui/react-focus-guards'
 import {FocusScope} from '@radix-ui/react-focus-scope'
 import {RemoveScrollBar} from 'react-remove-scroll-bar'
 
+import {useA11y} from '#/state/a11y'
 import {useModals} from '#/state/modals'
 import {ComposerOpts, useComposerState} from '#/state/shell/composer'
 import {
@@ -12,7 +13,7 @@ import {
   EmojiPickerPosition,
   EmojiPickerState,
 } from '#/view/com/composer/text-input/web/EmojiPicker.web'
-import {useBreakpoints, useTheme} from '#/alf'
+import {atoms as a, flatten, useBreakpoints, useTheme} from '#/alf'
 import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
 
 const BOTTOM_BAR_HEIGHT = 61
@@ -41,6 +42,7 @@ function Inner({state}: {state: ComposerOpts}) {
   const {isModalActive} = useModals()
   const t = useTheme()
   const {gtMobile} = useBreakpoints()
+  const {reduceMotionEnabled} = useA11y()
   const [pickerState, setPickerState] = React.useState<EmojiPickerState>({
     isOpen: false,
     pos: {top: 0, left: 0, right: 0, bottom: 0, nextFocusRef: null},
@@ -71,17 +73,15 @@ function Inner({state}: {state: ComposerOpts}) {
       <DismissableLayer
         role="dialog"
         aria-modal
-        style={{
-          position: 'fixed',
-          top: 0,
-          left: 0,
-          width: '100%',
-          height: '100%',
-          backgroundColor: '#000c',
-          display: 'flex',
-          flexDirection: 'column',
-          alignItems: 'center',
-        }}
+        style={flatten([
+          {position: 'fixed'},
+          a.inset_0,
+          {backgroundColor: '#000c'},
+          a.flex,
+          a.flex_col,
+          a.align_center,
+          !reduceMotionEnabled && a.fade_in,
+        ])}
         onFocusOutside={evt => evt.preventDefault()}
         onInteractOutside={evt => evt.preventDefault()}
         onDismiss={() => {
@@ -96,6 +96,11 @@ function Inner({state}: {state: ComposerOpts}) {
             !gtMobile && styles.containerMobile,
             t.atoms.bg,
             t.atoms.border_contrast_medium,
+            !reduceMotionEnabled && [
+              a.zoom_fade_in,
+              {animationDelay: 0.1},
+              {animationFillMode: 'backwards'},
+            ],
           ]}>
           <ComposePost
             cancelRef={ref}
@@ -123,14 +128,14 @@ const styles = StyleSheet.create({
     borderRadius: 8,
     marginBottom: 0,
     borderWidth: 1,
-    // @ts-ignore web only
+    // @ts-expect-error web only
     maxHeight: 'calc(100% - (40px * 2))',
     overflow: 'hidden',
   },
   containerMobile: {
     borderRadius: 0,
     marginBottom: BOTTOM_BAR_HEIGHT,
-    // @ts-ignore web only
+    // @ts-expect-error web only
     maxHeight: `calc(100% - ${BOTTOM_BAR_HEIGHT}px)`,
   },
 })