Skip to content

Commit

Permalink
Keybind and modal refactor (#5007)
Browse files Browse the repository at this point in the history
* Fix doc string

* Allow keybind listeners to be registered on any element

* Support custom placeholders in search input

* Support modals that aren't treated as pages
  • Loading branch information
apata authored Jan 23, 2025
1 parent 585c85b commit 117cf46
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 51 deletions.
41 changes: 18 additions & 23 deletions assets/js/dashboard/components/search-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import classNames from 'classnames'

export const SearchInput = ({
onSearch,
className
className,
placeholderFocused = 'Search',
placeholderUnfocused = 'Press / to search'
}: {
className?: string
onSearch: (value: string) => void
className?: string
placeholderFocused?: string
placeholderUnfocused?: string
}) => {
const searchBoxRef = useRef<HTMLInputElement>(null)
const [isFocused, setIsFocused] = useState(false)
Expand All @@ -23,46 +27,37 @@ export const SearchInput = ({
)
const debouncedOnSearchInputChange = useDebounce(onSearchInputChange)

const blurSearchBox = useCallback(
(event: KeyboardEvent) => {
if (isFocused) {
searchBoxRef.current?.blur()
event.stopPropagation()
}
},
[isFocused]
)
const blurSearchBox = useCallback(() => {
searchBoxRef.current?.blur()
}, [])

const focusSearchBox = useCallback(
(event: KeyboardEvent) => {
if (!isFocused) {
searchBoxRef.current?.focus()
event.stopPropagation()
}
},
[isFocused]
)
const focusSearchBox = useCallback((event: KeyboardEvent) => {
searchBoxRef.current?.focus()
event.stopPropagation()
}, [])

return (
<>
<Keybind
keyboardKey="Escape"
type="keyup"
handler={blurSearchBox}
shouldIgnoreWhen={[isModifierPressed]}
shouldIgnoreWhen={[isModifierPressed, () => !isFocused]}
target={searchBoxRef.current}
/>
<Keybind
keyboardKey="/"
type="keyup"
handler={focusSearchBox}
shouldIgnoreWhen={[isModifierPressed]}
shouldIgnoreWhen={[isModifierPressed, () => isFocused]}
target={document}
/>
<input
onBlur={() => setIsFocused(false)}
onFocus={() => setIsFocused(true)}
ref={searchBoxRef}
type="text"
placeholder={isFocused ? 'Search' : 'Press / to search'}
placeholder={isFocused ? placeholderFocused : placeholderUnfocused}
className={classNames(
'shadow-sm dark:bg-gray-900 dark:text-gray-100 focus:ring-indigo-500 focus:border-indigo-500 block sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800 w-48',
className
Expand Down
1 change: 1 addition & 0 deletions assets/js/dashboard/datepicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ export default function QueryPeriodPicker() {
type="keydown"
handler={onClick || closeMenu}
shouldIgnoreWhen={[isModifierPressed, isTyping]}
target={document}
/>
) : (
<NavigateKeybind
Expand Down
45 changes: 30 additions & 15 deletions assets/js/dashboard/keybinding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,37 +60,51 @@ type KeyboardEventType = keyof Pick<
'keyup' | 'keydown' | 'keypress'
>

export function Keybind({
keyboardKey,
type,
handler,
shouldIgnoreWhen = []
}: {
type KeybindOptions = {
keyboardKey: string
type: KeyboardEventType
handler: (event: KeyboardEvent) => void
shouldIgnoreWhen?: Array<(event: KeyboardEvent) => boolean>
}) {
target?: Document | HTMLElement | null
}

function useKeybind({
keyboardKey,
type,
handler,
shouldIgnoreWhen = [],
target
}: KeybindOptions) {
const wrappedHandler = useCallback(
(event: KeyboardEvent) => {
if (isKeyPressed(event, { keyboardKey, shouldIgnoreWhen })) {
handler(event)
}
},
[keyboardKey, handler, shouldIgnoreWhen]
)
) as EventListener

useEffect(() => {
const registerKeybind = () =>
document.addEventListener(type, wrappedHandler)
const registerKeybind = (t: HTMLElement | Document) =>
t.addEventListener(type, wrappedHandler)

const deregisterKeybind = () =>
document.removeEventListener(type, wrappedHandler)
const deregisterKeybind = (t: HTMLElement | Document) =>
t.removeEventListener(type, wrappedHandler)

registerKeybind()
if (target) {
registerKeybind(target)
}

return () => {
if (target) {
deregisterKeybind(target)
}
}
}, [target, type, wrappedHandler])
}

return deregisterKeybind
}, [type, wrappedHandler])
export function Keybind(opts: KeybindOptions) {
useKeybind(opts)

return null
}
Expand All @@ -115,6 +129,7 @@ export function NavigateKeybind({
type={type}
handler={handler}
shouldIgnoreWhen={[isModifierPressed, isTyping]}
target={document}
/>
)
}
Expand Down
16 changes: 5 additions & 11 deletions assets/js/dashboard/stats/modals/modal.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import { createPortal } from "react-dom";
import { NavigateKeybind } from '../../keybinding'
import { isModifierPressed, isTyping, Keybind } from "../../keybinding"
import { rootRoute } from "../../router";
import { useAppNavigate } from "../../navigation/use-app-navigate";

Expand Down Expand Up @@ -41,20 +41,13 @@ class Modal extends React.Component {
return;
}

this.close()
this.props.onClose()
}

handleResize() {
this.setState({ viewport: window.innerWidth });
}

close() {
this.props.navigate({
path: rootRoute.path,
search: (search) => search,
})
}

/**
* @description
* Decide whether to set max-width, and if so, to what.
Expand All @@ -77,7 +70,7 @@ class Modal extends React.Component {
render() {
return createPortal(
<>
<NavigateKeybind keyboardKey="Escape" type="keyup" navigateProps={{ path: rootRoute.path, search: (search) => search }} />
<Keybind keyboardKey="Escape" type="keyup" handler={this.props.onClose} target={document} shouldIgnoreWhen={[isModifierPressed, isTyping]} />
<div className="modal is-open" onClick={this.props.onClick}>
<div className="modal__overlay">
<button className="modal__close"></button>
Expand All @@ -99,5 +92,6 @@ class Modal extends React.Component {

export default function ModalWithRouting(props) {
const navigate = useAppNavigate()
return <Modal {...props} navigate={navigate} />
const onClose = props.onClose ?? (() => navigate({ path: rootRoute.path, search: (s) => s }))
return <Modal {...props} onClose={onClose} />
}
4 changes: 2 additions & 2 deletions lib/plausible/segments/segment.ex
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ defmodule Plausible.Segments.Segment do
)

@doc """
This function handles the error from building the naive query that is used to validate segment filters,
collecting filter related errors into a list.
This function handles the error from building the naive query that is used to validate segment filters.
If the error is only about filters, it's marked as :invalid_filters error and ultimately forwarded to client.
If the error is not only about filters, the client can't do anything about the situation,
and the error message is returned as-is.
Expand Down

0 comments on commit 117cf46

Please sign in to comment.