forked from Mirrors/gomuks
web/lightbox,web/modal: close on esc
This commit is contained in:
parent
2b10509ceb
commit
4572a9c882
4 changed files with 48 additions and 18 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -59,7 +60,7 @@ export const LightboxWrapper = ({ children }: { children: React.ReactNode }) =>
|
||||||
<LightboxContext value={onOpen}>
|
<LightboxContext value={onOpen}>
|
||||||
{children}
|
{children}
|
||||||
</LightboxContext>
|
</LightboxContext>
|
||||||
{params && <Lightbox {...params} onClose={onClose} />}
|
{params && <Lightbox {...params} onClose={onClose}/>}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,7 +143,15 @@ export class Lightbox extends Component<LightboxProps> {
|
||||||
this.ref.current.style.cursor = "grabbing"
|
this.ref.current.style.cursor = "grabbing"
|
||||||
}
|
}
|
||||||
|
|
||||||
transformer = (callback: () => void)=> (evt: React.MouseEvent) => {
|
onKeyDown = (evt: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
const key = keyToString(evt)
|
||||||
|
if (key === "Escape") {
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
evt.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
transformer = (callback: () => void) => (evt: React.MouseEvent) => {
|
||||||
evt.stopPropagation()
|
evt.stopPropagation()
|
||||||
if (!this.ref.current) {
|
if (!this.ref.current) {
|
||||||
return
|
return
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue