mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 17:11:31 +03:00
Animated resizing for dialogs (#11466)
- Cherry-picked out of #10827 - Add `framer-motion` to dialog to animate between dialog sizes. - Currently visible when switching between types in the Datalink modal. - Will also be visible when switching between types in the Schedule modal. # Important Notes None
This commit is contained in:
parent
3d38b7174f
commit
af0b95b1d9
@ -12,6 +12,9 @@ import * as suspense from '#/components/Suspense'
|
||||
|
||||
import * as mergeRefs from '#/utilities/mergeRefs'
|
||||
|
||||
import { useDimensions } from '#/hooks/dimensionsHooks'
|
||||
import type { Spring } from '#/utilities/motion'
|
||||
import { motion } from '#/utilities/motion'
|
||||
import type { VariantProps } from '#/utilities/tailwindVariants'
|
||||
import { tv } from '#/utilities/tailwindVariants'
|
||||
import * as dialogProvider from './DialogProvider'
|
||||
@ -20,13 +23,9 @@ import type * as types from './types'
|
||||
import * as utlities from './utilities'
|
||||
import { DIALOG_BACKGROUND } from './variants'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
/** Props for the {@link Dialog} component. */
|
||||
export interface DialogProps
|
||||
extends types.DialogProps,
|
||||
Omit<VariantProps<typeof DIALOG_STYLES>, 'scrolledToTop'> {}
|
||||
// This is a JSX component, even though it does not contain function syntax.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const MotionDialog = motion(aria.Dialog)
|
||||
|
||||
const OVERLAY_STYLES = tv({
|
||||
base: 'fixed inset-0 isolate flex items-center justify-center bg-primary/20 z-tooltip',
|
||||
@ -54,7 +53,7 @@ const MODAL_STYLES = tv({
|
||||
|
||||
const DIALOG_STYLES = tv({
|
||||
base: DIALOG_BACKGROUND({
|
||||
className: 'w-full max-w-full flex flex-col text-left align-middle shadow-xl',
|
||||
className: 'w-full max-w-full flex flex-col text-left align-middle shadow-xl overflow-clip',
|
||||
}),
|
||||
variants: {
|
||||
type: {
|
||||
@ -124,6 +123,7 @@ const DIALOG_STYLES = tv({
|
||||
closeButton: 'col-start-1 col-end-1 mr-auto',
|
||||
heading: 'col-start-2 col-end-2 my-0 text-center',
|
||||
content: 'relative flex-auto overflow-y-auto max-h-[inherit]',
|
||||
measuredContent: 'flex flex-col max-h-[90vh]',
|
||||
},
|
||||
compoundVariants: [
|
||||
{ type: 'modal', size: 'small', class: 'max-w-sm' },
|
||||
@ -144,10 +144,24 @@ const DIALOG_STYLES = tv({
|
||||
},
|
||||
})
|
||||
|
||||
const RESIZE_TRANSITION_STYLES: Spring = {
|
||||
type: 'spring',
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
stiffness: 300,
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
damping: 25,
|
||||
mass: 1,
|
||||
}
|
||||
|
||||
// ==============
|
||||
// === Dialog ===
|
||||
// ==============
|
||||
|
||||
/** Props for the {@link Dialog} component. */
|
||||
export interface DialogProps
|
||||
extends types.DialogProps,
|
||||
Omit<VariantProps<typeof DIALOG_STYLES>, 'scrolledToTop'> {}
|
||||
|
||||
/**
|
||||
* A dialog is an overlay shown above other content in an application.
|
||||
* Can be used to display alerts, confirmations, or other content.
|
||||
@ -187,8 +201,10 @@ export function Dialog(props: DialogProps) {
|
||||
}
|
||||
|
||||
const dialogId = aria.useId()
|
||||
const dialogLayoutId = `dialog-${dialogId}`
|
||||
const titleId = `${dialogId}-title`
|
||||
|
||||
const [contentDimensionsRef, { width: dialogWidth, height: dialogHeight }] = useDimensions()
|
||||
const dialogRef = React.useRef<HTMLDivElement>(null)
|
||||
const overlayState = React.useRef<aria.OverlayTriggerState | null>(null)
|
||||
const root = portal.useStrictPortalContext()
|
||||
@ -250,7 +266,11 @@ export function Dialog(props: DialogProps) {
|
||||
id={dialogId}
|
||||
type={TYPE_TO_DIALOG_TYPE[type]}
|
||||
>
|
||||
<aria.Dialog
|
||||
<MotionDialog
|
||||
layout
|
||||
layoutId={dialogLayoutId}
|
||||
animate={{ width: dialogWidth, height: dialogHeight }}
|
||||
transition={RESIZE_TRANSITION_STYLES}
|
||||
id={dialogId}
|
||||
ref={mergeRefs.mergeRefs(dialogRef, (element) => {
|
||||
if (element) {
|
||||
@ -268,8 +288,8 @@ export function Dialog(props: DialogProps) {
|
||||
aria-labelledby={titleId}
|
||||
{...ariaDialogProps}
|
||||
>
|
||||
{(opts) => {
|
||||
return (
|
||||
{(opts) => (
|
||||
<div className={styles.measuredContent()} ref={contentDimensionsRef}>
|
||||
<dialogProvider.DialogProvider value={{ close: opts.close, dialogId }}>
|
||||
{(closeButton !== 'none' || title != null) && (
|
||||
<aria.Header className={styles.header({ scrolledToTop: isScrolledToTop })}>
|
||||
@ -313,9 +333,9 @@ export function Dialog(props: DialogProps) {
|
||||
</errorBoundary.ErrorBoundary>
|
||||
</div>
|
||||
</dialogProvider.DialogProvider>
|
||||
)
|
||||
}}
|
||||
</aria.Dialog>
|
||||
</div>
|
||||
)}
|
||||
</MotionDialog>
|
||||
</dialogStackProvider.DialogStackRegistrar>
|
||||
</aria.Modal>
|
||||
)
|
||||
|
@ -106,7 +106,7 @@ export function useDimensions({
|
||||
(entries) => {
|
||||
if (entries[0]) {
|
||||
measure()
|
||||
updateChildPosition() // entries[0].boundingClientRect)
|
||||
updateChildPosition()
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
|
75
app/gui/src/dashboard/utilities/motion.ts
Normal file
75
app/gui/src/dashboard/utilities/motion.ts
Normal file
@ -0,0 +1,75 @@
|
||||
/** @file Type-safe `motion` from `framer-motion`. */
|
||||
import {
|
||||
motion as originalMotion,
|
||||
type ForwardRefComponent,
|
||||
type HTMLMotionProps,
|
||||
type MotionProps,
|
||||
type SVGMotionProps,
|
||||
} from 'framer-motion'
|
||||
|
||||
import type {
|
||||
ComponentType,
|
||||
DetailedHTMLFactory,
|
||||
ForwardRefExoticComponent,
|
||||
PropsWithChildren,
|
||||
PropsWithoutRef,
|
||||
ReactHTML,
|
||||
RefAttributes,
|
||||
SVGProps,
|
||||
} from 'react'
|
||||
|
||||
/** The options parameter for {@link motion}. */
|
||||
interface CustomMotionComponentConfig {
|
||||
readonly forwardMotionProps?: boolean
|
||||
}
|
||||
|
||||
/** Get the inner type of a {@link DetailedHTMLFactory}. */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type UnwrapFactoryElement<F> = F extends DetailedHTMLFactory<any, infer P> ? P : never
|
||||
/** Get the inner type of a {@link SVGProps}. */
|
||||
type UnwrapSVGFactoryElement<F> = F extends SVGProps<infer P> ? P : never
|
||||
|
||||
export * from 'framer-motion'
|
||||
|
||||
/**
|
||||
* HTML & SVG components, optimised for use with gestures and animation.
|
||||
* These can be used as drop-in replacements for any HTML & SVG component -
|
||||
* all CSS & SVG properties are supported.
|
||||
*/
|
||||
// This is a function, even though it does not contain function syntax.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
export const motion = originalMotion as unknown as (<Props extends object>(
|
||||
Component: ComponentType<PropsWithChildren<Props>> | string,
|
||||
customMotionComponentConfig?: CustomMotionComponentConfig,
|
||||
) => ForwardRefExoticComponent<
|
||||
PropsWithoutRef<
|
||||
Omit<MotionProps & Props, 'children' | 'style'> &
|
||||
(Props extends { readonly children?: infer Children } ?
|
||||
// `Props` has a key `Children` but it may be optional.
|
||||
// Use a homomorphic mapped type (a mapped type with `keyof T` in the key set)
|
||||
// to preserve modifiers (optional and readonly).
|
||||
{
|
||||
[K in keyof Props as K extends 'children' ? K : never]: Children | MotionProps['children']
|
||||
}
|
||||
: // `Props` has no key `Children`.
|
||||
{ children?: MotionProps['children'] }) &
|
||||
(Props extends { readonly style?: infer Style } ?
|
||||
// `Props` has a key `Style` but it may be optional.
|
||||
// Use a homomorphic mapped type (a mapped type with `keyof T` in the key set)
|
||||
// to preserve modifiers (optional and readonly).
|
||||
{ [K in keyof Props as K extends 'style' ? K : never]: MotionProps['style'] | Style }
|
||||
: // `Props` has no key `Style`.
|
||||
{ style?: MotionProps['style'] })
|
||||
> &
|
||||
RefAttributes<HTMLElement | SVGElement>
|
||||
>) & {
|
||||
[K in keyof HTMLElementTagNameMap]: ForwardRefComponent<
|
||||
UnwrapFactoryElement<ReactHTML[K]>,
|
||||
HTMLMotionProps<K>
|
||||
>
|
||||
} & {
|
||||
[K in keyof SVGElementTagNameMap]: ForwardRefComponent<
|
||||
UnwrapSVGFactoryElement<JSX.IntrinsicElements[K]>,
|
||||
SVGMotionProps<UnwrapSVGFactoryElement<JSX.IntrinsicElements[K]>>
|
||||
>
|
||||
}
|
Loading…
Reference in New Issue
Block a user