feat: replace window confirm with a dialog (#898)

* feat: replace window confirm with react dialog

* add dialog provider

* export func

* remove unused code

* changes

* Create file_open.tldr

* Update TldrawApp.ts

* clean, and add description

* add a custom container for alert dialog

* Fix fonts

* Fix logic for open project

* Style panel

* Improve styling

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
pull/915/head
Judicael 2022-08-16 00:45:48 +03:00 zatwierdzone przez GitHub
rodzic 6dccf26c43
commit 97b0b52a6e
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
11 zmienionych plików z 1487 dodań i 65 usunięć

Wyświetl plik

@ -6,11 +6,14 @@ import { ContextMenu } from '~components/ContextMenu'
import { ErrorFallback } from '~components/ErrorFallback'
import { FocusButton } from '~components/FocusButton'
import { Loading } from '~components/Loading'
import { AlertDialog } from '~components/Primitives/AlertDialog'
import { ToolsPanel } from '~components/ToolsPanel'
import { TopPanel } from '~components/TopPanel'
import { GRID_SIZE } from '~constants'
import {
AlertDialogContext,
ContainerContext,
DialogState,
TldrawContext,
useKeyboardShortcuts,
useStylesheet,
@ -163,6 +166,21 @@ export function Tldraw({
return app
})
const [onCancel, setOnCancel] = React.useState<(() => void) | null>(null)
const [onYes, setOnYes] = React.useState<(() => void) | null>(null)
const [onNo, setOnNo] = React.useState<(() => void) | null>(null)
const [dialogState, setDialogState] = React.useState<DialogState | null>(null)
const openDialog = React.useCallback(
(dialogState: DialogState, onYes: () => void, onNo: () => void, onCancel: () => void) => {
setDialogState(() => dialogState)
setOnCancel(() => onCancel)
setOnYes(() => onYes)
setOnNo(() => onNo)
},
[]
)
// Create a new app if the `id` prop changes.
React.useLayoutEffect(() => {
if (id === sId) return
@ -297,19 +315,23 @@ export function Tldraw({
// Use the `key` to ensure that new selector hooks are made when the id changes
return (
<TldrawContext.Provider value={app}>
<InnerTldraw
key={sId || 'Tldraw'}
id={sId}
autofocus={autofocus}
showPages={showPages}
showMenu={showMenu}
showMultiplayerMenu={showMultiplayerMenu}
showStyles={showStyles}
showZoom={showZoom}
showTools={showTools}
showUI={showUI}
readOnly={readOnly}
/>
<AlertDialogContext.Provider
value={{ onYes, onCancel, onNo, dialogState, setDialogState, openDialog }}
>
<InnerTldraw
key={sId || 'Tldraw'}
id={sId}
autofocus={autofocus}
showPages={showPages}
showMenu={showMenu}
showMultiplayerMenu={showMultiplayerMenu}
showStyles={showStyles}
showZoom={showZoom}
showTools={showTools}
showUI={showUI}
readOnly={readOnly}
/>
</AlertDialogContext.Provider>
</TldrawContext.Provider>
)
}
@ -340,6 +362,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
showUI,
}: InnerTldrawProps) {
const app = useTldrawApp()
const [dialogContainer, setDialogContainer] = React.useState<any>(null)
const rWrapper = React.useRef<HTMLDivElement>(null)
@ -439,6 +462,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
return (
<ContainerContext.Provider value={rWrapper}>
<IntlProvider locale={translation.locale} messages={translation.messages}>
<AlertDialog container={dialogContainer} />
<StyledLayout ref={rWrapper} tabIndex={-0}>
<Loading />
<OneOff focusableRef={rWrapper} autofocus={autofocus} />
@ -523,7 +547,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
</ErrorBoundary>
</ContextMenu>
{showUI && (
<StyledUI>
<StyledUI ref={setDialogContainer}>
{settings.isFocusMode ? (
<FocusButton onSelect={app.toggleFocusMode} />
) : (

Wyświetl plik

@ -0,0 +1,160 @@
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
import * as React from 'react'
import { DialogState, useDialog } from '~hooks'
import { styled } from '~styles'
interface ContentProps {
children: React.ReactNode
onClose?: () => void
container: any
}
function Content({ children, onClose, container }: ContentProps) {
const handleKeyDown = (event: React.KeyboardEvent) => {
switch (event.key) {
case 'Escape':
onClose?.()
break
}
}
return (
<AlertDialogPrimitive.Portal container={container}>
<StyledOverlay />
<StyledContent onKeyDown={handleKeyDown}>{children}</StyledContent>
</AlertDialogPrimitive.Portal>
)
}
const StyledDescription = styled(AlertDialogPrimitive.Description, {
marginBottom: 20,
color: '$text',
fontSize: '$2',
lineHeight: 1.5,
textAlign: 'center',
maxWidth: '62%',
minWidth: 0,
alignSelf: 'center',
})
export const AlertDialogRoot = AlertDialogPrimitive.Root
export const AlertDialogContent = Content
export const AlertDialogDescription = StyledDescription
export const AlertDialogAction = AlertDialogPrimitive.Action
export const AlertDialogCancel = AlertDialogPrimitive.Cancel
const descriptions: Record<DialogState, string> = {
saveFirstTime: 'Do you want to save your current project?',
saveAgain: 'Do you want to save changes to your current project?',
}
export const AlertDialog = ({ container }: { container: any }) => {
const { setDialogState, dialogState, onCancel, onNo, onYes } = useDialog()
return (
<AlertDialogRoot open={dialogState !== null}>
<AlertDialogContent onClose={() => setDialogState(null)} container={container}>
{dialogState && (
<AlertDialogDescription>{descriptions[dialogState]}</AlertDialogDescription>
)}
<div
style={{
width: '100%',
gap: '$6',
display: 'flex',
justifyContent: 'space-between',
}}
>
{onCancel && (
<AlertDialogCancel asChild>
<Button
css={{ color: '$text' }}
onClick={() => {
onCancel()
setDialogState(null)
}}
>
Cancel
</Button>
</AlertDialogCancel>
)}
<div style={{ flexShrink: 0 }}>
{onNo && (
<AlertDialogAction asChild>
<Button
onClick={() => {
onNo()
setDialogState(null)
}}
>
No
</Button>
</AlertDialogAction>
)}
{onYes && (
<AlertDialogAction asChild>
<Button
css={{ backgroundColor: '#2F80ED', color: 'White' }}
onClick={() => {
onYes()
setDialogState(null)
}}
>
Yes
</Button>
</AlertDialogAction>
)}
</div>
</div>
</AlertDialogContent>
</AlertDialogRoot>
)
}
const StyledOverlay = styled(AlertDialogPrimitive.Overlay, {
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0, 0, 0, .15)',
pointerEvents: 'all',
})
export const StyledDialogOverlay = styled(AlertDialogPrimitive.Overlay, {
backgroundColor: 'rgba(0, 0, 0, .15)',
position: 'absolute',
pointerEvents: 'all',
inset: 0,
})
const StyledContent = styled(AlertDialogPrimitive.Content, {
position: 'fixed',
font: '$ui',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 'max-content',
padding: '$3',
pointerEvents: 'all',
backgroundColor: '$panel',
borderRadius: '$3',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
fontFamily: '$ui',
border: '1px solid $panelContrast',
boxShadow: '$panel',
})
const Button = styled('button', {
all: 'unset',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '$2',
padding: '0 15px',
fontSize: '$1',
lineHeight: 1,
fontWeight: 'normal',
height: 36,
color: '$text',
cursor: 'pointer',
minWidth: 48,
})

Wyświetl plik

@ -0,0 +1 @@
export * from './AlertDialog'

Wyświetl plik

@ -29,7 +29,7 @@ export function DMContent({
const container = useContainer()
return (
<DropdownMenu.Portal container={container.current} dir="ltr">
<DropdownMenu.Portal container={overflow ? undefined : container.current} dir="ltr">
<DropdownMenu.Content
align={align}
alignOffset={alignOffset}

Wyświetl plik

@ -5,3 +5,4 @@ export * from './useStylesheet'
export * from './useTheme'
export * from './useTldrawApp'
export * from './useTranslation'
export * from './useDialog'

Wyświetl plik

@ -0,0 +1,25 @@
import * as React from 'react'
export type DialogState = 'saveFirstTime' | 'saveAgain'
interface AlertDialogProps {
dialogState: DialogState | null
setDialogState: (dialogState: DialogState | null) => void
onYes: (() => void) | null
onNo: (() => void) | null
onCancel: (() => void) | null
openDialog: (
dialogState: DialogState,
onYes: () => void,
onNo: () => void,
onCancel: () => void
) => void
}
export const AlertDialogContext = React.createContext<AlertDialogProps>({} as AlertDialogProps)
export const useDialog = () => {
const context = React.useContext(AlertDialogContext)
if (!context) throw new Error('useCtx must be inside a Provider with a value')
return context
}

Wyświetl plik

@ -1,29 +1,72 @@
import * as React from 'react'
import type { TldrawApp } from '~state'
import { DialogState } from './useDialog'
export function useFileSystem() {
const promptSaveBeforeChange = React.useCallback(async (app: TldrawApp) => {
if (app.isDirty) {
if (app.fileSystemHandle) {
if (window.confirm('Do you want to save changes to your current project?')) {
await app.saveProject()
}
} else {
if (window.confirm('Do you want to save your current project?')) {
await app.saveProject()
}
}
}
}, [])
const onNewProject = React.useCallback(
async (app: TldrawApp) => {
if (window.confirm('Do you want to create a new project?')) {
await promptSaveBeforeChange(app)
app.newProject()
}
async (
app: TldrawApp,
openDialog: (
dialogState: DialogState,
onYes: () => Promise<void>,
onNo: () => Promise<void>,
onCancel: () => Promise<void>
) => void
) => {
openDialog(
app.fileSystemHandle ? 'saveFirstTime' : 'saveAgain',
async () => {
// user pressed yes
try {
await app.saveProject()
app.newProject()
} catch (e) {
// noop
}
},
async () => {
// user pressed no
app.newProject()
},
async () => {
// user pressed cancel
}
)
},
[promptSaveBeforeChange]
[]
)
const onOpenProject = React.useCallback(
async (
app: TldrawApp,
openDialog: (
dialogState: DialogState,
onYes: () => Promise<void>,
onNo: () => Promise<void>,
onCancel: () => Promise<void>
) => void
) => {
openDialog(
app.fileSystemHandle ? 'saveFirstTime' : 'saveAgain',
async () => {
// user pressed yes
try {
await app.saveProject()
await app.openProject()
} catch (e) {
// noop
}
},
async () => {
// user pressed no
app.openProject()
},
async () => {
// user pressed cancel
}
)
},
[]
)
const onSaveProject = React.useCallback((app: TldrawApp) => {
@ -34,14 +77,6 @@ export function useFileSystem() {
app.saveProjectAs()
}, [])
const onOpenProject = React.useCallback(
async (app: TldrawApp) => {
await promptSaveBeforeChange(app)
app.openProject()
},
[promptSaveBeforeChange]
)
const onOpenMedia = React.useCallback(async (app: TldrawApp) => {
app.openAsset?.()
}, [])

Wyświetl plik

@ -1,15 +1,17 @@
import * as React from 'react'
import { useTldrawApp } from '~hooks'
import { useDialog, useTldrawApp } from '~hooks'
export function useFileSystemHandlers() {
const app = useTldrawApp()
const { openDialog } = useDialog()
const onNewProject = React.useCallback(
async (e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => {
if (e && app.callbacks.onOpenProject) e.preventDefault()
app.callbacks.onNewProject?.(app)
app.callbacks.onNewProject?.(app, openDialog)
},
[app]
[app, openDialog]
)
const onSaveProject = React.useCallback(
@ -31,9 +33,9 @@ export function useFileSystemHandlers() {
const onOpenProject = React.useCallback(
async (e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => {
if (e && app.callbacks.onOpenProject) e.preventDefault()
app.callbacks.onOpenProject?.(app)
app.callbacks.onOpenProject?.(app, openDialog)
},
[app]
[app, openDialog]
)
const onOpenMedia = React.useCallback(

Wyświetl plik

@ -199,8 +199,8 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
useHotkeys(
'ctrl+n,⌘+n',
(e) => {
e.preventDefault()
if (!canHandleEvent()) return
onNewProject(e)
},
undefined,

Wyświetl plik

@ -21,6 +21,7 @@ import {
USER_COLORS,
VIDEO_EXTENSIONS,
} from '~constants'
import { DialogState } from '~hooks'
import { shapeUtils } from '~state/shapes'
import { defaultStyle } from '~state/shapes/shared'
import {
@ -97,7 +98,16 @@ export interface TDCallbacks {
/**
* (optional) A callback to run when the user creates a new project through the menu or through a keyboard shortcut.
*/
onNewProject?: (app: TldrawApp, e?: KeyboardEvent) => void
onNewProject?: (
app: TldrawApp,
openDialog: (
dialogState: DialogState,
onYes: () => void,
onNo: () => void,
onCancel: () => void
) => void,
e?: KeyboardEvent
) => void
/**
* (optional) A callback to run when the user saves a project through the menu or through a keyboard shortcut.
*/
@ -109,7 +119,16 @@ export interface TDCallbacks {
/**
* (optional) A callback to run when the user opens new project through the menu or through a keyboard shortcut.
*/
onOpenProject?: (app: TldrawApp, e?: KeyboardEvent) => void
onOpenProject?: (
app: TldrawApp,
openDialog: (
dialogState: DialogState,
onYes: () => void,
onNo: () => void,
onCancel: () => void
) => void,
e?: KeyboardEvent
) => void
/**
* (optional) A callback to run when the opens a file to upload.
*/
@ -1384,18 +1403,13 @@ export class TldrawApp extends StateManager<TDSnapshot> {
*/
saveProject = async () => {
if (this.readOnly) return
try {
const fileHandle = await saveToFileSystem(
migrate(this.state, TldrawApp.version).document,
this.fileSystemHandle
)
this.fileSystemHandle = fileHandle
this.persist({})
this.isDirty = false
} catch (e: any) {
// Likely cancelled
console.error(e.message)
}
const fileHandle = await saveToFileSystem(
migrate(this.state, TldrawApp.version).document,
this.fileSystemHandle
)
this.fileSystemHandle = fileHandle
this.persist({})
this.isDirty = false
return this
}
@ -2100,7 +2114,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
/**
* Copy one or more shapes as SVG.
* @param ids The ids of the shapes to copy.
* @param pageId The page from which to copy the shapes.
* @returns A string containing the JSON.
*/
copySvg = async (
@ -2220,7 +2233,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
/**
* Copy one or more shapes as JSON.
* @param ids The ids of the shapes to copy.
* @param pageId The page from which to copy the shapes.
* @returns A string containing the JSON.
*/
copyJson = (ids = this.selectedIds) => {