1
0
Fork 0
forked from Mirrors/gomuks

web/lightbox,web/modal: close on esc

This commit is contained in:
Tulir Asokan 2024-11-17 01:43:31 +02:00
parent 2b10509ceb
commit 4572a9c882
4 changed files with 48 additions and 18 deletions

View file

@ -69,7 +69,7 @@ export default class Keybindings {
} }
private keyUpMap: KeyMap = { private keyUpMap: KeyMap = {
"Escape": evt => evt.target === evt.currentTarget && this.context.clearActiveRoom(), // "Escape": evt => evt.target === evt.currentTarget && this.context.clearActiveRoom(),
} }
listen(): () => void { listen(): () => void {

View file

@ -13,7 +13,8 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
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 CloseIcon from "@/icons/close.svg?react"
import DownloadIcon from "@/icons/download.svg?react" import DownloadIcon from "@/icons/download.svg?react"
import RotateLeftIcon from "@/icons/rotate-left.svg?react" import RotateLeftIcon from "@/icons/rotate-left.svg?react"
@ -72,12 +73,8 @@ export class Lightbox extends Component<LightboxProps> {
zoom = 1 zoom = 1
rotate = 0 rotate = 0
maybePanning = false maybePanning = false
readonly ref: RefObject<HTMLImageElement | null> readonly ref = createRef<HTMLImageElement>()
readonly wrapperRef = createRef<HTMLDivElement>()
constructor(props: LightboxProps) {
super(props)
this.ref = createRef<HTMLImageElement>()
}
get style() { get style() {
return { return {
@ -87,6 +84,13 @@ export class Lightbox extends Component<LightboxProps> {
} }
} }
close = () => {
this.translate = { x: 0, y: 0 }
this.rotate = 0
this.zoom = 1
this.props.onClose()
}
onClick = () => { onClick = () => {
if (!this.ref.current) { if (!this.ref.current) {
return return
@ -95,10 +99,7 @@ export class Lightbox extends Component<LightboxProps> {
this.ref.current.style.cursor = "auto" this.ref.current.style.cursor = "auto"
this.maybePanning = false this.maybePanning = false
} else { } else {
this.translate = { x: 0, y: 0 } this.close()
this.rotate = 0
this.zoom = 1
this.props.onClose()
} }
} }
@ -142,6 +143,14 @@ export class Lightbox extends Component<LightboxProps> {
this.ref.current.style.cursor = "grabbing" this.ref.current.style.cursor = "grabbing"
} }
onKeyDown = (evt: React.KeyboardEvent<HTMLDivElement>) => {
const key = keyToString(evt)
if (key === "Escape") {
this.close()
}
evt.stopPropagation()
}
transformer = (callback: () => void) => (evt: React.MouseEvent) => { transformer = (callback: () => void) => (evt: React.MouseEvent) => {
evt.stopPropagation() evt.stopPropagation()
if (!this.ref.current) { if (!this.ref.current) {
@ -153,6 +162,15 @@ export class Lightbox extends Component<LightboxProps> {
this.ref.current.style.scale = style.scale 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() stopPropagation = (evt: React.MouseEvent) => evt.stopPropagation()
zoomIn = this.transformer(() => this.zoom = Math.min(this.zoom * 1.1, 10)) 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)) zoomOut = this.transformer(() => this.zoom = Math.max(this.zoom / 1.1, 0.01))
@ -164,6 +182,9 @@ export class Lightbox extends Component<LightboxProps> {
className="overlay dimmed lightbox" className="overlay dimmed lightbox"
onClick={this.onClick} onClick={this.onClick}
onMouseMove={isTouchDevice ? undefined : this.onMouseMove} onMouseMove={isTouchDevice ? undefined : this.onMouseMove}
tabIndex={-1}
onKeyDown={this.onKeyDown}
ref={this.wrapperRef}
> >
<div className="controls" onClick={this.stopPropagation}> <div className="controls" onClick={this.stopPropagation}>
<button onClick={this.zoomOut}><ZoomOutIcon/></button> <button onClick={this.zoomOut}><ZoomOutIcon/></button>

View file

@ -13,7 +13,7 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import React, { JSX, createContext, useCallback, useReducer } from "react" import React, { JSX, createContext, useCallback, useLayoutEffect, useReducer, useRef } from "react"
export interface ModalState { export interface ModalState {
content: JSX.Element content: JSX.Element
@ -44,13 +44,22 @@ export const ModalWrapper = ({ children }: { children: React.ReactNode }) => {
if (evt.key === "Escape") { if (evt.key === "Escape") {
setState(null) setState(null)
} }
evt.stopPropagation()
}, []) }, [])
const wrapperRef = useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
if (wrapperRef.current && (!document.activeElement || !wrapperRef.current.contains(document.activeElement))) {
wrapperRef.current.focus()
}
}, [state])
return <ModalContext value={setState}> return <ModalContext value={setState}>
{children} {children}
{state && <div {state && <div
className={`overlay ${state.wrapperClass ?? "modal"} ${state.dimmed ? "dimmed" : ""}`} className={`overlay ${state.wrapperClass ?? "modal"} ${state.dimmed ? "dimmed" : ""}`}
onClick={onClickWrapper} onClick={onClickWrapper}
onKeyDown={onKeyWrapper} onKeyDown={onKeyWrapper}
tabIndex={-1}
ref={wrapperRef}
> >
<ModalCloseContext value={onClickWrapper}> <ModalCloseContext value={onClickWrapper}>
{state.content} {state.content}

View file

@ -28,7 +28,7 @@ interface ConfirmWithMessageProps {
onConfirm: (reason: string) => void onConfirm: (reason: string) => void
} }
const ConfirmWithMessageProps = ({ const ConfirmWithMessageModal = ({
evt, title, description, placeholder, confirmButton, onConfirm, evt, title, description, placeholder, confirmButton, onConfirm,
}: ConfirmWithMessageProps) => { }: ConfirmWithMessageProps) => {
const [reason, setReason] = useState("") const [reason, setReason] = useState("")
@ -48,7 +48,7 @@ const ConfirmWithMessageProps = ({
<div className="confirm-description"> <div className="confirm-description">
{description} {description}
</div> </div>
<input value={reason} type="text" placeholder={placeholder} onChange={onChangeReason} /> <input autoFocus value={reason} type="text" placeholder={placeholder} onChange={onChangeReason} />
<div className="confirm-buttons"> <div className="confirm-buttons">
<button onClick={closeModal}>Cancel</button> <button onClick={closeModal}>Cancel</button>
<button onClick={onConfirmWrapped}>{confirmButton}</button> <button onClick={onConfirmWrapped}>{confirmButton}</button>
@ -56,4 +56,4 @@ const ConfirmWithMessageProps = ({
</div> </div>
} }
export default ConfirmWithMessageProps export default ConfirmWithMessageModal