fix: Link, Pages, Topic, Hook and Others (#178)

* chore: remove useKeyDownListener

* chore: remove react-use, update jazz version and add query string

* chore: update jazz version

* chore: use simple mac or win utils code

* feat(util): add isTextInput

* feat(hooks): all needed hooks

* fix: link bunch stuff

* fix: page bunch stuff

* chore: bunch update for custom component

* chore: use throttle from internal hook

* chore: topic bunch stuff

* chore: update layout

* fix: truncate content header of topic detail
This commit is contained in:
Aslam
2024-09-23 23:16:02 +07:00
committed by GitHub
parent 21084cd3f3
commit 867478d55c
43 changed files with 616 additions and 466 deletions
+2 -2
View File
@@ -11,11 +11,11 @@ export function useActiveItemScroll<T extends HTMLElement>(options: ActiveItemSc
const { activeIndex } = options
const elementRefs = useRef<ElementRefs<T>>([])
const scrollActiveElementIntoView = useCallback((index: number) => {
const scrollActiveElementIntoView = (index: number) => {
const activeElement = elementRefs.current[index]
activeElement?.focus()
// activeElement?.scrollIntoView({ block: "nearest" })
}, [])
}
useEffect(() => {
if (activeIndex !== null) {
+33
View File
@@ -0,0 +1,33 @@
import * as React from "react"
type EventMap = WindowEventMap & HTMLElementEventMap & VisualViewportEventMap
export function useEventListener<
K extends keyof EventMap,
T extends Window | HTMLElement | VisualViewport | null = Window
>(
eventName: K,
handler: (event: EventMap[K]) => void,
element: T = window as unknown as T, // Cast to `unknown` first, then `T`
options: AddEventListenerOptions = {}
) {
const savedHandler = React.useRef<(event: EventMap[K]) => void>()
const { capture, passive, once } = options
React.useEffect(() => {
savedHandler.current = handler
}, [handler])
React.useEffect(() => {
const isSupported = element && element.addEventListener
if (!isSupported) return
const eventListener = (event: EventMap[K]) => savedHandler.current?.(event)
const opts = { capture, passive, once }
element.addEventListener(eventName, eventListener as EventListener, opts)
return () => {
element.removeEventListener(eventName, eventListener as EventListener, opts)
}
}, [eventName, element, capture, passive, once])
}
+19
View File
@@ -0,0 +1,19 @@
import * as React from "react"
/**
* Hook to check if component is still mounted
*
* @returns {boolean} true if the component is mounted, false otherwise
*/
export function useIsMounted() {
const isMounted = React.useRef(false)
React.useEffect(() => {
isMounted.current = true
return () => {
isMounted.current = false
}
}, [])
return React.useCallback(() => isMounted.current, [])
}
+79
View File
@@ -0,0 +1,79 @@
import { isModKey, isTextInput } from "@/lib/utils"
import * as React from "react"
type Callback = (event: KeyboardEvent) => void
export type KeyFilter = ((event: KeyboardEvent) => boolean) | string
export type Options = {
allowInInput?: boolean
}
type RegisteredCallback = {
callback: Callback
options?: Options
}
// Registered keyboard event callbacks
let callbacks: RegisteredCallback[] = []
// Track if IME input suggestions are open so we can ignore keydown shortcuts
// in this case, they should never be triggered from mobile keyboards.
let imeOpen = false
// Based on implementation in react-use
// https://github.com/streamich/react-use/blob/master/src/useKey.ts#L15-L22
const createKeyPredicate = (keyFilter: KeyFilter) =>
typeof keyFilter === "function"
? keyFilter
: typeof keyFilter === "string"
? (event: KeyboardEvent) => event.key === keyFilter
: keyFilter
? (_event: KeyboardEvent) => true
: (_event: KeyboardEvent) => false
export function useKeyDown(key: KeyFilter, fn: Callback, options?: Options): void {
const predicate = createKeyPredicate(key)
React.useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (predicate(event)) {
fn(event)
}
}
callbacks.push({
callback: handler,
options
})
return () => {
callbacks = callbacks.filter(cb => cb.callback !== handler)
}
}, [fn, predicate, options])
}
window.addEventListener("keydown", event => {
if (imeOpen) {
return
}
// reverse so that the last registered callbacks get executed first
for (const registered of callbacks.reverse()) {
if (event.defaultPrevented === true) {
break
}
if (!isTextInput(event.target as HTMLElement) || registered.options?.allowInInput || isModKey(event)) {
registered.callback(event)
}
}
})
window.addEventListener("compositionstart", () => {
imeOpen = true
})
window.addEventListener("compositionend", () => {
imeOpen = false
})
+12 -1
View File
@@ -2,7 +2,18 @@ import { useAtom } from "jotai"
import { useEffect, useCallback } from "react"
import { keyboardDisableSourcesAtom } from "@/store/keydown-manager"
const allowedKeys = ["Escape", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Enter"]
const allowedKeys = [
"Escape",
"ArrowUp",
"ArrowDown",
"ArrowLeft",
"ArrowRight",
"Enter",
"Tab",
"Backspace",
"Home",
"End"
]
export function useKeyboardManager(sourceId: string) {
const [disableSources, setDisableSources] = useAtom(keyboardDisableSourcesAtom)
-21
View File
@@ -1,21 +0,0 @@
import { useAtomValue } from "jotai"
import { useEffect, useCallback } from "react"
import { keyboardDisableSourcesAtom } from "@/store/keydown-manager"
export function useKeydownListener(callback: (event: KeyboardEvent) => void) {
const disableSources = useAtomValue(keyboardDisableSourcesAtom)
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (disableSources.size === 0) {
callback(event)
}
},
[disableSources, callback]
)
useEffect(() => {
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [handleKeyDown])
}
+23
View File
@@ -0,0 +1,23 @@
import { useState, useEffect } from "react"
export function useMedia(query: string): boolean {
const [matches, setMatches] = useState<boolean>(false)
useEffect(() => {
if (window.matchMedia) {
const media = window.matchMedia(query)
if (media.matches !== matches) {
setMatches(media.matches)
}
const listener = () => {
setMatches(media.matches)
}
media.addListener(listener)
return () => media.removeListener(listener)
}
return undefined
}, [matches, query])
return matches
}
+28
View File
@@ -0,0 +1,28 @@
import * as React from "react"
import { useEventListener } from "./use-event-listener"
/**
* Hook to detect clicks outside of a specified element.
*
* @param ref The React ref to the element.
* @param callback The handler to call when a click outside the element is detected.
*/
export function useOnClickOutside(
ref: React.RefObject<HTMLElement | null>,
callback?: (event: MouseEvent | TouchEvent) => void,
options: AddEventListenerOptions = {}
) {
const listener = React.useCallback(
(event: MouseEvent | TouchEvent) => {
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target as Node)) {
return
}
callback?.(event)
},
[ref, callback]
)
useEventListener("mousedown", listener, window, options)
useEventListener("touchstart", listener, window, options)
}
+34
View File
@@ -0,0 +1,34 @@
import { useRef, useCallback } from "react"
export function useThrottle<T extends (...args: any[]) => void>(
callback: T,
delay: number
): (...args: Parameters<T>) => void {
const lastRan = useRef(Date.now())
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
return useCallback(
(...args: Parameters<T>) => {
const handler = () => {
if (Date.now() - lastRan.current >= delay) {
callback(...args)
lastRan.current = Date.now()
} else {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
timeoutRef.current = setTimeout(
() => {
callback(...args)
lastRan.current = Date.now()
},
delay - (Date.now() - lastRan.current)
)
}
}
handler()
},
[callback, delay]
)
}
+8 -1
View File
@@ -1,11 +1,18 @@
import { useState, useEffect } from "react"
const SSR = typeof window === "undefined"
export function useTouchSensor() {
const [isTouchDevice, setIsTouchDevice] = useState(false)
useEffect(() => {
const detectTouch = () => {
setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0)
setIsTouchDevice(
!SSR &&
(window.matchMedia?.("(hover: none) and (pointer: coarse)")?.matches ||
"ontouchstart" in window ||
navigator.maxTouchPoints > 0)
)
}
detectTouch()