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 = {
"Escape": evt => evt.target === evt.currentTarget && this.context.clearActiveRoom(),
// "Escape": evt => evt.target === evt.currentTarget && this.context.clearActiveRoom(),
}
listen(): () => void {

View file

@ -13,7 +13,8 @@
//
// 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/>.
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"
@ -72,12 +73,8 @@ export class Lightbox extends Component<LightboxProps> {
zoom = 1
rotate = 0
maybePanning = false
readonly ref: RefObject<HTMLImageElement | null>
constructor(props: LightboxProps) {
super(props)
this.ref = createRef<HTMLImageElement>()
}
readonly ref = createRef<HTMLImageElement>()
readonly wrapperRef = createRef<HTMLDivElement>()
get style() {
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 = () => {
if (!this.ref.current) {
return
@ -95,10 +99,7 @@ export class Lightbox extends Component<LightboxProps> {
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,6 +143,14 @@ export class Lightbox extends Component<LightboxProps> {
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) => {
evt.stopPropagation()
if (!this.ref.current) {
@ -153,6 +162,15 @@ export class Lightbox extends Component<LightboxProps> {
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<LightboxProps> {
className="overlay dimmed lightbox"
onClick={this.onClick}
onMouseMove={isTouchDevice ? undefined : this.onMouseMove}
tabIndex={-1}
onKeyDown={this.onKeyDown}
ref={this.wrapperRef}
>
<div className="controls" onClick={this.stopPropagation}>
<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
// 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 {
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<HTMLDivElement>(null)
useLayoutEffect(() => {
if (wrapperRef.current && (!document.activeElement || !wrapperRef.current.contains(document.activeElement))) {
wrapperRef.current.focus()
}
}, [state])
return <ModalContext value={setState}>
{children}
{state && <div
className={`overlay ${state.wrapperClass ?? "modal"} ${state.dimmed ? "dimmed" : ""}`}
onClick={onClickWrapper}
onKeyDown={onKeyWrapper}
tabIndex={-1}
ref={wrapperRef}
>
<ModalCloseContext value={onClickWrapper}>
{state.content}

View file

@ -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 = ({
<div className="confirm-description">
{description}
</div>
<input value={reason} type="text" placeholder={placeholder} onChange={onChangeReason} />
<input autoFocus value={reason} type="text" placeholder={placeholder} onChange={onChangeReason} />
<div className="confirm-buttons">
<button onClick={closeModal}>Cancel</button>
<button onClick={onConfirmWrapped}>{confirmButton}</button>
@ -56,4 +56,4 @@ const ConfirmWithMessageProps = ({
</div>
}
export default ConfirmWithMessageProps
export default ConfirmWithMessageModal