Skip to content
This repository has been archived by the owner on Jun 30, 2023. It is now read-only.

Add keyboard shortcuts for playback #35

Merged
merged 1 commit into from
Oct 17, 2021
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
3 changes: 3 additions & 0 deletions packages/lito/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { initReactI18next } from 'react-i18next'
import { HashRouter as Router, Redirect, Route, Switch } from 'react-router-dom'
import styled, { ThemeProvider } from 'styled-components'
import { SWRConfig } from 'swr'
import { useBlurAfterClick, useKeyboardShortcuts } from './a11y'
import AdditionalControls from './AdditionalControls'
import { fetcher } from './api'
import Authorize from './Authorize'
Expand Down Expand Up @@ -58,6 +59,8 @@ const App = () => {
const authorized = useAuthorized()
const [lyricsVisible, setLyricsVisible] = useState(false)
const scrollRef = useRef<HTMLDivElement>(null)
useBlurAfterClick()
useKeyboardShortcuts()
return (
<SetThemeContext.Provider value={setTheme}>
<ThemeProvider theme={theme}>
Expand Down
15 changes: 15 additions & 0 deletions packages/lito/src/a11y/focus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useEffect } from 'react'

export const useBlurAfterClick = () => {
useEffect(() => {
const handleClick = (e: MouseEvent) => {
if (e.target instanceof HTMLElement && e.target.closest('a, button, input[type=button], input[type=checkbox]')) {
e.target.blur()
}
}
addEventListener('click', handleClick)
return () => {
removeEventListener('click', handleClick)
}
}, [])
}
3 changes: 3 additions & 0 deletions packages/lito/src/a11y/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './focus'
export * from './shortcuts'
export * from './utils'
84 changes: 84 additions & 0 deletions packages/lito/src/a11y/shortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useEffect } from 'react'
import { isInputLike } from '.'
import { isMacOS } from '../utils'

enum ModifierKey {
Alt = 1 << 0,
Accel = 1 << 1, // Ctrl on Windows, Command on macOS
Shift = 1 << 2,
}

const getModifierKey = (e: KeyboardEvent) => {
let value = 0
if (e.altKey) value |= ModifierKey.Alt
if (isMacOS()) {
if (e.metaKey) value |= ModifierKey.Accel
} else {
if (e.ctrlKey) value |= ModifierKey.Accel
}
if (e.shiftKey) value |= ModifierKey.Shift
return value
}

interface KeyboardShortcutDescriptor {
code: string
modifier?: number
allowRepeat?: boolean
allowInInput?: boolean
}

const matchKeyboardShortcut = (
e: KeyboardEvent,
{ code, modifier = 0, allowRepeat = false, allowInInput = modifier !== 0 }: KeyboardShortcutDescriptor
) => {
if (e.code === code && getModifierKey(e) === modifier) {
if (allowRepeat || !e.repeat) {
if (allowInInput || !isInputLike(e.target)) {
e.preventDefault()
return true
}
}
}
return false
}

export const useKeyboardShortcuts = () => {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.isComposing) return
const instance = MusicKit.getInstance()
switch (true) {
case matchKeyboardShortcut(e, { code: 'Space' }):
switch (instance.playbackState as unknown as MusicKit.PlaybackStates) {
case MusicKit.PlaybackStates.playing:
instance.pause()
break
case MusicKit.PlaybackStates.paused:
case MusicKit.PlaybackStates.stopped:
instance.play()
break
}
break
case matchKeyboardShortcut(e, { code: 'Enter' }):
if (instance.nowPlayingItem) {
instance.seekToTime(0)
instance.play()
}
break
case matchKeyboardShortcut(e, { code: 'ArrowLeft', modifier: ModifierKey.Alt | ModifierKey.Accel }):
instance.skipToPreviousItem()
break
case matchKeyboardShortcut(e, { code: 'ArrowRight', modifier: ModifierKey.Alt | ModifierKey.Accel }):
instance.skipToNextItem()
break
case matchKeyboardShortcut(e, { code: 'Period', modifier: ModifierKey.Accel }):
instance.stop()
break
}
}
addEventListener('keydown', handleKeyDown)
return () => {
removeEventListener('keydown', handleKeyDown)
}
}, [])
}
5 changes: 5 additions & 0 deletions packages/lito/src/a11y/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const isInputLike = (target: EventTarget | null) => {
if (!target) return false
if (!(target instanceof HTMLElement)) return false
return !!target.closest('a, button, input, textarea')
}