Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 3 additions & 13 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import {
Dispatch,
SetStateAction,
createContext,
useEffect,
useState,
} from 'react'
import { Dispatch, SetStateAction, createContext, useState } from 'react'
import Footer from './components/UI/footer/Footer'
import StreamFeed from './components/streamFeed/StreamFeed'
import Navigation from './components/UI/navigation/Navigation'
import { StreamProps } from './types/StreamProps'
import { getEnglishLanguageName } from './helper/getEnglishLanguageName'
import { getItemFromStorage } from './helper/getItemFromStorage'
import { useDocumentTitle } from './hooks/useDocumentTitle'
import { useScreenWidth } from './hooks/useScreenWidth'

export const ContextScreenWidth = createContext<
Expand Down Expand Up @@ -85,11 +79,7 @@ const App = () => {
const [inputFocussed, setInputFocussed] = useState(false)
const [hideSearch, setHideSearch] = useState(true)

useEffect(() => {
document.title = `Twitch-App | ${getEnglishLanguageName(
language
)} Livestreams${seoSearchText ? ` | ${seoSearchText}` : ''}`
}, [language, seoSearchText])
useDocumentTitle(language, seoSearchText)

return (
<div className="min-w-64 min-h-screen bg-zinc-800">
Expand Down
74 changes: 11 additions & 63 deletions src/components/UI/navigation/DesktopSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { forwardRef, useContext, useEffect, useRef } from 'react'
import { forwardRef, useContext, useRef } from 'react'
import './search.css'
import {
ContextDisableFocusTrap,
ContextFocusInput,
ContextSearchResults,
ContextSearchText,
} from '../../../App'
import { SearchProps } from '../../../types/SearchProps'
import Icon from '../Icon'
import SearchResultSuggestion from './SearchResultSuggestion'
import './search.css'
import { SearchProps } from '../../../types/SearchProps'
import { useFocusInput } from '../../../hooks/useFocusInput'
import { useFocusTrapSearch } from '../../../hooks/useFocusTrapSearch'

const DesktopSearch = forwardRef<HTMLDivElement, SearchProps>(
(
Expand Down Expand Up @@ -66,68 +68,14 @@ const DesktopSearch = forwardRef<HTMLDivElement, SearchProps>(
const buttonRef = useRef<HTMLButtonElement | null>(null)
const searchResultsRef = useRef<HTMLDivElement | null>(null)

useEffect(() => {
const handleFocusTrap = (e: KeyboardEvent) => {
if (
searchText.length === 0 ||
focusTrapDisabled ||
e.key !== 'Tab'
)
return

const focusableElements = [
inputRef?.current,
buttonRef.current,
...(searchResultsRef.current
? Array.from(
searchResultsRef.current.querySelectorAll(
'button'
)
)
: []),
].filter((el) => el !== null) as (
| HTMLInputElement
| HTMLButtonElement
)[]

if (focusableElements.length === 0) return

const firstElement = focusableElements[0]
const lastElement =
focusableElements[focusableElements.length - 1]

if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault()
lastElement.focus()
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault()
firstElement.focus()
}
}
}

document.addEventListener('keydown', handleFocusTrap)

return () => {
document.removeEventListener('keydown', handleFocusTrap)
}
}, [
useFocusTrapSearch(
buttonRef,
focusTrapDisabled,
inputRef,
searchResults,
searchResultsExpanded,
searchText,
])

useEffect(() => {
if (inputFocussed && inputRef?.current) {
inputRef.current.focus()
setInputFocussed(false)
}
}, [inputFocussed, inputRef, setInputFocussed])
searchResultsRef,
searchText
)
useFocusInput(inputFocussed, inputRef, setInputFocussed)

return (
<div
Expand Down
68 changes: 11 additions & 57 deletions src/components/UI/navigation/MobileSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { forwardRef, useContext, useEffect, useRef } from 'react'
import { forwardRef, useContext, useRef } from 'react'
import './search.css'
import {
ContextDisableFocusTrap,
ContextFocusInput,
ContextSearchResults,
ContextSearchText,
} from '../../../App'
import { SearchProps } from '../../../types/SearchProps'
import Icon from '../Icon'
import SearchResultSuggestion from './SearchResultSuggestion'
import './search.css'
import { SearchProps } from '../../../types/SearchProps'
import { useFocusInput } from '../../../hooks/useFocusInput'
import { useFocusTrapSearch } from '../../../hooks/useFocusTrapSearch'

const MobileSearch = forwardRef<HTMLDivElement, SearchProps>(
(
Expand Down Expand Up @@ -66,62 +68,14 @@ const MobileSearch = forwardRef<HTMLDivElement, SearchProps>(
const buttonRef = useRef<HTMLButtonElement | null>(null)
const searchResultsRef = useRef<HTMLDivElement | null>(null)

useEffect(() => {
const handleFocusTrap = (e: KeyboardEvent) => {
if (e.key !== 'Tab' || focusTrapDisabled) return

const focusableElements = [
searchMobileRef?.current,
buttonRef.current,
...(searchResultsRef.current
? Array.from(
searchResultsRef.current.querySelectorAll(
'button'
)
)
: []),
].filter((el) => el !== null) as (
| HTMLInputElement
| HTMLButtonElement
)[]

if (focusableElements.length === 0) return

const firstElement = focusableElements[0]
const lastElement =
focusableElements[focusableElements.length - 1]

if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault()
lastElement.focus()
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault()
firstElement.focus()
}
}
}

document.addEventListener('keydown', handleFocusTrap)

return () => {
document.removeEventListener('keydown', handleFocusTrap)
}
}, [
useFocusTrapSearch(
buttonRef,
focusTrapDisabled,
searchMobileRef,
searchResults,
searchResultsExpanded,
])

useEffect(() => {
if (inputFocussed && searchMobileRef?.current) {
searchMobileRef.current.focus()
setInputFocussed(false)
}
}, [inputFocussed, searchMobileRef, setInputFocussed])
searchResultsRef,
searchText
)
useFocusInput(inputFocussed, searchMobileRef, setInputFocussed)

return (
<>
Expand Down
124 changes: 24 additions & 100 deletions src/components/UI/navigation/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useContext, useEffect, useRef, useState } from 'react'
import { useContext, useRef, useState } from 'react'
import ButtonIcon from '../ButtonIcon'
import DesktopSearch from './DesktopSearch'
import MobileSearch from './MobileSearch'
import { HomeIcon } from './HomeIcon'
import { UserIcon } from './UserIcon'
import MobileSearch from './MobileSearch'
import {
ContextDisableFocusTrap,
ContextFilteredStreamData,
Expand All @@ -14,9 +15,11 @@ import {
ContextSearchText,
ContextStreamData,
} from '../../../App'
import DesktopSearch from './DesktopSearch'
import { getSearchFilter } from '../../../helper/getSearchFilter'
import { setItemInStorage } from '../../../helper/setItemInStorage'
import { useNavigationScrollY } from '../../../hooks/useNavigationScrollY'
import { useCloseSearchResults } from '../../../hooks/useCloseSearchResults'
import { useHideMobileSearch } from '../../../hooks/useHideMobileSearch'

const Navigation = () => {
const contextScreenWidth = useContext(ContextScreenWidth)
Expand Down Expand Up @@ -253,103 +256,24 @@ const Navigation = () => {
}, 0)
}

useEffect(() => {
let lastScrollY = window.scrollY
let timer: NodeJS.Timeout

const handleScroll = () => {
if (window.scrollY === 0) {
setNavOpacity('opacity-100')
} else if (window.scrollY < lastScrollY && !blockOpacity) {
setNavOpacity('opacity-75')
} else if (!blockOpacity) {
setNavOpacity('opacity-95')
}
lastScrollY = window.scrollY

clearTimeout(timer)
if (navOpacity !== 'opacity-100') {
timer = setTimeout(() => {
setNavOpacity('opacity-100')
}, 500)
}
}

window.addEventListener('scroll', handleScroll)

return () => {
window.removeEventListener('scroll', handleScroll)
clearTimeout(timer)
}
}, [blockOpacity, navOpacity])

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
((desktopSearchRef.current &&
!desktopSearchRef.current.contains(event.target as Node)) ||
(mobileSearchRef.current &&
!mobileSearchRef.current.contains(
event.target as Node
))) &&
searchResultsExpanded
) {
event.stopPropagation()
setSearchResultsExpanded(false)
}
}

const handleEscape = (event: KeyboardEvent) => {
if (
((desktopSearchRef.current &&
desktopSearchRef.current.contains(event.target as Node)) ||
(mobileSearchRef.current &&
mobileSearchRef.current.contains(
event.target as Node
))) &&
searchResultsExpanded &&
event.key === 'Escape'
) {
event.stopPropagation()
setSearchResultsExpanded(false)
if (!inputRef?.current?.onfocus) {
if (
contextScreenWidth === 'MOBILE' ||
contextScreenWidth === 'TABLET_SMALL'
) {
buttonIconRef?.current?.focus()
} else {
userIconRef?.current?.focus()
anchorRef?.current?.focus()
}
}
}
}

if (searchResultsExpanded) {
document.addEventListener('keydown', handleEscape)
document.addEventListener('mousedown', handleClickOutside)
} else {
document.removeEventListener('keydown', handleEscape)
document.removeEventListener('mousedown', handleClickOutside)
}

return () => {
document.removeEventListener('keydown', handleEscape)
document.removeEventListener('mousedown', handleClickOutside)
}
}, [contextScreenWidth, searchResultsExpanded])

useEffect(() => {
if (
(contextScreenWidth === 'MOBILE' ||
contextScreenWidth === 'TABLET_SMALL') &&
inputFocussed
) {
setHideSearch(false)
setAriaPressed(true)
}
}, [contextScreenWidth, inputFocussed, setHideSearch])
useNavigationScrollY(blockOpacity, navOpacity, setNavOpacity)
useCloseSearchResults(
anchorRef,
buttonIconRef,
contextScreenWidth,
desktopSearchRef,
inputRef,
mobileSearchRef,
searchResultsExpanded,
setSearchResultsExpanded,
userIconRef
)
useHideMobileSearch(
contextScreenWidth,
inputFocussed,
setAriaPressed,
setHideSearch
)

return (
<div className="sticky top-0 z-10" data-testid="navigation-container">
Expand Down
Loading
Loading