mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-11 18:24:43 +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';
|
||||
|
||||
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;
|
||||
`;
|
||||
import * as React from 'react';
|
||||
import { ReactElement, ReactEventHandler, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
renderContent: Function;
|
||||
renderContent: ()=> ReactElement;
|
||||
className?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onClose?: Function;
|
||||
onClose?: ()=> void;
|
||||
}
|
||||
|
||||
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 (
|
||||
<DialogModalLayer className={props.className}>
|
||||
<DialogRoot>
|
||||
<dialog
|
||||
ref={setDialogRef}
|
||||
className={`dialog-modal-layer ${props.className}`}
|
||||
onClose={props.onClose}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<div className='content'>
|
||||
{props.renderContent()}
|
||||
</DialogRoot>
|
||||
</DialogModalLayer>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ interface Props {
|
||||
export const IconSelector = (props: Props) => {
|
||||
const [emojiButtonClassReady, setEmojiButtonClassReady] = useState<boolean>(false);
|
||||
const [picker, setPicker] = useState<EmojiButton>();
|
||||
const buttonRef = useRef(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
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
|
||||
const p: EmojiButton = new (window as any).EmojiButton({
|
||||
zIndex: 10000,
|
||||
rootElement: buttonRef.current?.parentElement,
|
||||
});
|
||||
|
||||
const onEmoji = (selection: FolderIcon) => {
|
||||
@ -73,6 +74,7 @@ export const IconSelector = (props: Props) => {
|
||||
|
||||
return () => {
|
||||
p.off('emoji', onEmoji);
|
||||
p.destroyPicker();
|
||||
};
|
||||
}, [emojiButtonClassReady, props.onChange]);
|
||||
|
||||
|
@ -140,17 +140,16 @@ type SyncTargetInfoName = 'dropbox' | 'onedrive' | 'joplinCloud';
|
||||
export default function(props: Props) {
|
||||
const joplinCloudDescriptionRef = useRef(null);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
function closeDialog(dispatch: Function) {
|
||||
dispatch({
|
||||
const closeDialog = useCallback(() => {
|
||||
props.dispatch({
|
||||
type: 'DIALOG_CLOSE',
|
||||
name: 'syncWizard',
|
||||
});
|
||||
}
|
||||
}, [props.dispatch]);
|
||||
|
||||
const onButtonRowClick = useCallback(() => {
|
||||
closeDialog(props.dispatch);
|
||||
}, [props.dispatch]);
|
||||
closeDialog();
|
||||
}, [closeDialog]);
|
||||
|
||||
const { height: descriptionHeight } = useElementSize(joplinCloudDescriptionRef);
|
||||
|
||||
@ -184,12 +183,12 @@ export default function(props: Props) {
|
||||
|
||||
Setting.setValue('sync.target', route.target);
|
||||
await Setting.saveAll();
|
||||
closeDialog(props.dispatch);
|
||||
closeDialog();
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: route.name,
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
}, [props.dispatch, closeDialog]);
|
||||
|
||||
function renderSelectArea(info: SyncTargetInfo) {
|
||||
return (
|
||||
@ -229,7 +228,7 @@ export default function(props: Props) {
|
||||
}
|
||||
|
||||
const onSelfHostingClick = useCallback(() => {
|
||||
closeDialog(props.dispatch);
|
||||
closeDialog();
|
||||
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
@ -238,7 +237,7 @@ export default function(props: Props) {
|
||||
defaultSection: 'sync',
|
||||
},
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
}, [props.dispatch, closeDialog]);
|
||||
|
||||
function renderContent() {
|
||||
// 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 (
|
||||
<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/TrashNotification/style.scss' as trash-notification;
|
||||
@use 'gui/Sidebar/style.scss' as sidebar-styles;
|
||||
@use 'gui/styles/index.scss';
|
||||
@use 'main.scss' as main;
|
Loading…
Reference in New Issue
Block a user