mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-26 18:58:21 +02:00
Desktop: Improve focus handling for notebook edit, share, and sync dialogs (#10779)
This commit is contained in:
parent
3fbb3b6b82
commit
dd5240d018
@ -1,49 +1,46 @@
|
|||||||
import styled from 'styled-components';
|
import * as React from 'react';
|
||||||
|
import { ReactElement, ReactEventHandler, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
const DialogModalLayer = styled.div`
|
|
||||||
z-index: 9999;
|
|
||||||
display: flex;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(0,0,0,0.6);
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
overflow: auto;
|
|
||||||
scrollbar-width: none;
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DialogRoot = styled.div`
|
|
||||||
background-color: ${props => props.theme.backgroundColor};
|
|
||||||
padding: 16px;
|
|
||||||
box-shadow: 6px 6px 20px rgba(0,0,0,0.5);
|
|
||||||
margin: 20px;
|
|
||||||
min-height: fit-content;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border-radius: 10px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
renderContent: ()=> ReactElement;
|
||||||
renderContent: Function;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
onClose?: ()=> void;
|
||||||
onClose?: Function;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Dialog(props: Props) {
|
export default function Dialog(props: Props) {
|
||||||
|
const [dialogElement, setDialogRef] = useState<HTMLDialogElement>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dialogElement) return;
|
||||||
|
|
||||||
|
// Use .showModal instead of the open attribute: .showModal correctly
|
||||||
|
// traps the keyboard focus in the dialog
|
||||||
|
dialogElement.showModal();
|
||||||
|
}, [dialogElement]);
|
||||||
|
|
||||||
|
const onCloseRef = useRef(props.onClose);
|
||||||
|
onCloseRef.current = props.onClose;
|
||||||
|
|
||||||
|
const onCancel: ReactEventHandler<HTMLDialogElement> = useCallback((event) => {
|
||||||
|
const canCancel = !!onCloseRef.current;
|
||||||
|
if (canCancel) {
|
||||||
|
// Prevents [Escape] from closing the dialog. In many places, this is handled
|
||||||
|
// elsewhere.
|
||||||
|
// See https://stackoverflow.com/a/61021326
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogModalLayer className={props.className}>
|
<dialog
|
||||||
<DialogRoot>
|
ref={setDialogRef}
|
||||||
|
className={`dialog-modal-layer ${props.className}`}
|
||||||
|
onClose={props.onClose}
|
||||||
|
onCancel={onCancel}
|
||||||
|
>
|
||||||
|
<div className='content'>
|
||||||
{props.renderContent()}
|
{props.renderContent()}
|
||||||
</DialogRoot>
|
</div>
|
||||||
</DialogModalLayer>
|
</dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ interface Props {
|
|||||||
export const IconSelector = (props: Props) => {
|
export const IconSelector = (props: Props) => {
|
||||||
const [emojiButtonClassReady, setEmojiButtonClassReady] = useState<boolean>(false);
|
const [emojiButtonClassReady, setEmojiButtonClassReady] = useState<boolean>(false);
|
||||||
const [picker, setPicker] = useState<EmojiButton>();
|
const [picker, setPicker] = useState<EmojiButton>();
|
||||||
const buttonRef = useRef(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||||
const loadScripts = async () => {
|
const loadScripts = async () => {
|
||||||
@ -61,6 +61,7 @@ export const IconSelector = (props: Props) => {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
const p: EmojiButton = new (window as any).EmojiButton({
|
const p: EmojiButton = new (window as any).EmojiButton({
|
||||||
zIndex: 10000,
|
zIndex: 10000,
|
||||||
|
rootElement: buttonRef.current?.parentElement,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onEmoji = (selection: FolderIcon) => {
|
const onEmoji = (selection: FolderIcon) => {
|
||||||
@ -73,6 +74,7 @@ export const IconSelector = (props: Props) => {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
p.off('emoji', onEmoji);
|
p.off('emoji', onEmoji);
|
||||||
|
p.destroyPicker();
|
||||||
};
|
};
|
||||||
}, [emojiButtonClassReady, props.onChange]);
|
}, [emojiButtonClassReady, props.onChange]);
|
||||||
|
|
||||||
|
@ -140,17 +140,16 @@ type SyncTargetInfoName = 'dropbox' | 'onedrive' | 'joplinCloud';
|
|||||||
export default function(props: Props) {
|
export default function(props: Props) {
|
||||||
const joplinCloudDescriptionRef = useRef(null);
|
const joplinCloudDescriptionRef = useRef(null);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
const closeDialog = useCallback(() => {
|
||||||
function closeDialog(dispatch: Function) {
|
props.dispatch({
|
||||||
dispatch({
|
|
||||||
type: 'DIALOG_CLOSE',
|
type: 'DIALOG_CLOSE',
|
||||||
name: 'syncWizard',
|
name: 'syncWizard',
|
||||||
});
|
});
|
||||||
}
|
}, [props.dispatch]);
|
||||||
|
|
||||||
const onButtonRowClick = useCallback(() => {
|
const onButtonRowClick = useCallback(() => {
|
||||||
closeDialog(props.dispatch);
|
closeDialog();
|
||||||
}, [props.dispatch]);
|
}, [closeDialog]);
|
||||||
|
|
||||||
const { height: descriptionHeight } = useElementSize(joplinCloudDescriptionRef);
|
const { height: descriptionHeight } = useElementSize(joplinCloudDescriptionRef);
|
||||||
|
|
||||||
@ -184,12 +183,12 @@ export default function(props: Props) {
|
|||||||
|
|
||||||
Setting.setValue('sync.target', route.target);
|
Setting.setValue('sync.target', route.target);
|
||||||
await Setting.saveAll();
|
await Setting.saveAll();
|
||||||
closeDialog(props.dispatch);
|
closeDialog();
|
||||||
props.dispatch({
|
props.dispatch({
|
||||||
type: 'NAV_GO',
|
type: 'NAV_GO',
|
||||||
routeName: route.name,
|
routeName: route.name,
|
||||||
});
|
});
|
||||||
}, [props.dispatch]);
|
}, [props.dispatch, closeDialog]);
|
||||||
|
|
||||||
function renderSelectArea(info: SyncTargetInfo) {
|
function renderSelectArea(info: SyncTargetInfo) {
|
||||||
return (
|
return (
|
||||||
@ -229,7 +228,7 @@ export default function(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onSelfHostingClick = useCallback(() => {
|
const onSelfHostingClick = useCallback(() => {
|
||||||
closeDialog(props.dispatch);
|
closeDialog();
|
||||||
|
|
||||||
props.dispatch({
|
props.dispatch({
|
||||||
type: 'NAV_GO',
|
type: 'NAV_GO',
|
||||||
@ -238,7 +237,7 @@ export default function(props: Props) {
|
|||||||
defaultSection: 'sync',
|
defaultSection: 'sync',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [props.dispatch]);
|
}, [props.dispatch, closeDialog]);
|
||||||
|
|
||||||
function renderContent() {
|
function renderContent() {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
@ -278,6 +277,6 @@ export default function(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog renderContent={renderDialogWrapper}/>
|
<Dialog onClose={closeDialog} renderContent={renderDialogWrapper}/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
28
packages/app-desktop/gui/styles/dialog-modal-layer.scss
Normal file
28
packages/app-desktop/gui/styles/dialog-modal-layer.scss
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
.dialog-modal-layer {
|
||||||
|
display: flex;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
> .content {
|
||||||
|
background-color: var(--joplin-background-color);
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 6px 6px 20px rgba(0,0,0,0.5);
|
||||||
|
margin: 20px;
|
||||||
|
min-height: fit-content;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::backdrop {
|
||||||
|
background-color: rgba(0,0,0,0.6);
|
||||||
|
}
|
||||||
|
}
|
2
packages/app-desktop/gui/styles/index.scss
Normal file
2
packages/app-desktop/gui/styles/index.scss
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
@use './dialog-modal-layer.scss';
|
@ -10,4 +10,5 @@
|
|||||||
@use 'gui/NoteListHeader/style.scss' as note-list-header;
|
@use 'gui/NoteListHeader/style.scss' as note-list-header;
|
||||||
@use 'gui/TrashNotification/style.scss' as trash-notification;
|
@use 'gui/TrashNotification/style.scss' as trash-notification;
|
||||||
@use 'gui/Sidebar/style.scss' as sidebar-styles;
|
@use 'gui/Sidebar/style.scss' as sidebar-styles;
|
||||||
|
@use 'gui/styles/index.scss';
|
||||||
@use 'main.scss' as main;
|
@use 'main.scss' as main;
|
Loading…
x
Reference in New Issue
Block a user