From 4572a9c8823dd646dd52a73a31085c8bcaaa58f7 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 17 Nov 2024 01:43:31 +0200 Subject: [PATCH] web/lightbox,web/modal: close on esc --- web/src/ui/keybindings.ts | 2 +- web/src/ui/modal/Lightbox.tsx | 47 ++++++++++++++----- web/src/ui/modal/Modal.tsx | 11 ++++- .../timeline/menu/ConfirmWithMessageModal.tsx | 6 +-- 4 files changed, 48 insertions(+), 18 deletions(-) diff --git a/web/src/ui/keybindings.ts b/web/src/ui/keybindings.ts index 62c2cb6..f6c2b0c 100644 --- a/web/src/ui/keybindings.ts +++ b/web/src/ui/keybindings.ts @@ -69,7 +69,7 @@ export default class Keybindings { } private keyUpMap: KeyMap = { - "Escape": evt => evt.target === evt.currentTarget && this.context.clearActiveRoom(), + // "Escape": evt => evt.target === evt.currentTarget && this.context.clearActiveRoom(), } listen(): () => void { diff --git a/web/src/ui/modal/Lightbox.tsx b/web/src/ui/modal/Lightbox.tsx index b09138b..82384fc 100644 --- a/web/src/ui/modal/Lightbox.tsx +++ b/web/src/ui/modal/Lightbox.tsx @@ -13,7 +13,8 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { Component, RefObject, createContext, createRef, useCallback, useLayoutEffect, useState } from "react" +import React, { Component, createContext, createRef, useCallback, useLayoutEffect, useState } from "react" +import { keyToString } from "../keybindings.ts" import CloseIcon from "@/icons/close.svg?react" import DownloadIcon from "@/icons/download.svg?react" import RotateLeftIcon from "@/icons/rotate-left.svg?react" @@ -59,7 +60,7 @@ export const LightboxWrapper = ({ children }: { children: React.ReactNode }) => {children} - {params && } + {params && } } @@ -72,12 +73,8 @@ export class Lightbox extends Component { zoom = 1 rotate = 0 maybePanning = false - readonly ref: RefObject - - constructor(props: LightboxProps) { - super(props) - this.ref = createRef() - } + readonly ref = createRef() + readonly wrapperRef = createRef() get style() { return { @@ -87,6 +84,13 @@ export class Lightbox extends Component { } } + close = () => { + this.translate = { x: 0, y: 0 } + this.rotate = 0 + this.zoom = 1 + this.props.onClose() + } + onClick = () => { if (!this.ref.current) { return @@ -95,10 +99,7 @@ export class Lightbox extends Component { this.ref.current.style.cursor = "auto" this.maybePanning = false } else { - this.translate = { x: 0, y: 0 } - this.rotate = 0 - this.zoom = 1 - this.props.onClose() + this.close() } } @@ -142,7 +143,15 @@ export class Lightbox extends Component { this.ref.current.style.cursor = "grabbing" } - transformer = (callback: () => void)=> (evt: React.MouseEvent) => { + onKeyDown = (evt: React.KeyboardEvent) => { + const key = keyToString(evt) + if (key === "Escape") { + this.close() + } + evt.stopPropagation() + } + + transformer = (callback: () => void) => (evt: React.MouseEvent) => { evt.stopPropagation() if (!this.ref.current) { return @@ -153,6 +162,15 @@ export class Lightbox extends Component { this.ref.current.style.scale = style.scale } + componentDidMount() { + if ( + this.wrapperRef.current + && (!document.activeElement || !this.wrapperRef.current.contains(document.activeElement)) + ) { + this.wrapperRef.current.focus() + } + } + stopPropagation = (evt: React.MouseEvent) => evt.stopPropagation() zoomIn = this.transformer(() => this.zoom = Math.min(this.zoom * 1.1, 10)) zoomOut = this.transformer(() => this.zoom = Math.max(this.zoom / 1.1, 0.01)) @@ -164,6 +182,9 @@ export class Lightbox extends Component { className="overlay dimmed lightbox" onClick={this.onClick} onMouseMove={isTouchDevice ? undefined : this.onMouseMove} + tabIndex={-1} + onKeyDown={this.onKeyDown} + ref={this.wrapperRef} >
diff --git a/web/src/ui/modal/Modal.tsx b/web/src/ui/modal/Modal.tsx index 24d5745..adbc50c 100644 --- a/web/src/ui/modal/Modal.tsx +++ b/web/src/ui/modal/Modal.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { JSX, createContext, useCallback, useReducer } from "react" +import React, { JSX, createContext, useCallback, useLayoutEffect, useReducer, useRef } from "react" export interface ModalState { content: JSX.Element @@ -44,13 +44,22 @@ export const ModalWrapper = ({ children }: { children: React.ReactNode }) => { if (evt.key === "Escape") { setState(null) } + evt.stopPropagation() }, []) + const wrapperRef = useRef(null) + useLayoutEffect(() => { + if (wrapperRef.current && (!document.activeElement || !wrapperRef.current.contains(document.activeElement))) { + wrapperRef.current.focus() + } + }, [state]) return {children} {state &&
{state.content} diff --git a/web/src/ui/timeline/menu/ConfirmWithMessageModal.tsx b/web/src/ui/timeline/menu/ConfirmWithMessageModal.tsx index 7f66da6..46c70a0 100644 --- a/web/src/ui/timeline/menu/ConfirmWithMessageModal.tsx +++ b/web/src/ui/timeline/menu/ConfirmWithMessageModal.tsx @@ -28,7 +28,7 @@ interface ConfirmWithMessageProps { onConfirm: (reason: string) => void } -const ConfirmWithMessageProps = ({ +const ConfirmWithMessageModal = ({ evt, title, description, placeholder, confirmButton, onConfirm, }: ConfirmWithMessageProps) => { const [reason, setReason] = useState("") @@ -48,7 +48,7 @@ const ConfirmWithMessageProps = ({
{description}
- +
@@ -56,4 +56,4 @@ const ConfirmWithMessageProps = ({
} -export default ConfirmWithMessageProps +export default ConfirmWithMessageModal