✨(frontend) create a11y store to manage user option toggles
sets up state handling for enabling or disabling a11y preferences
This commit is contained in:
@@ -6,6 +6,8 @@ import { Participant } from 'livekit-client'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Reaction } from '@/features/rooms/livekit/components/controls/ReactionsToggle'
|
import { Reaction } from '@/features/rooms/livekit/components/controls/ReactionsToggle'
|
||||||
import { getEmojiLabel } from '@/features/rooms/livekit/utils/reactionUtils'
|
import { getEmojiLabel } from '@/features/rooms/livekit/utils/reactionUtils'
|
||||||
|
import { accessibilityStore } from '@/stores/accessibility'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
|
||||||
export const ANIMATION_DURATION = 3000
|
export const ANIMATION_DURATION = 3000
|
||||||
export const ANIMATION_DISTANCE = 300
|
export const ANIMATION_DISTANCE = 300
|
||||||
@@ -155,6 +157,7 @@ export function ReactionPortal({
|
|||||||
|
|
||||||
export const ReactionPortals = ({ reactions }: { reactions: Reaction[] }) => {
|
export const ReactionPortals = ({ reactions }: { reactions: Reaction[] }) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
|
||||||
|
const { announceReactions } = useSnapshot(accessibilityStore)
|
||||||
const [announcement, setAnnouncement] = useState<string | null>(null)
|
const [announcement, setAnnouncement] = useState<string | null>(null)
|
||||||
const [lastAnnouncedId, setLastAnnouncedId] = useState<number | null>(null)
|
const [lastAnnouncedId, setLastAnnouncedId] = useState<number | null>(null)
|
||||||
|
|
||||||
@@ -162,6 +165,10 @@ export const ReactionPortals = ({ reactions }: { reactions: Reaction[] }) => {
|
|||||||
reactions.length > 0 ? reactions[reactions.length - 1] : undefined
|
reactions.length > 0 ? reactions[reactions.length - 1] : undefined
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!announceReactions) {
|
||||||
|
setAnnouncement(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!latestReaction) return
|
if (!latestReaction) return
|
||||||
const isNewReaction = latestReaction.id !== lastAnnouncedId
|
const isNewReaction = latestReaction.id !== lastAnnouncedId
|
||||||
if (!isNewReaction) return
|
if (!isNewReaction) return
|
||||||
@@ -175,7 +182,7 @@ export const ReactionPortals = ({ reactions }: { reactions: Reaction[] }) => {
|
|||||||
|
|
||||||
const timer = setTimeout(() => setAnnouncement(null), 1200)
|
const timer = setTimeout(() => setAnnouncement(null), 1200)
|
||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
}, [latestReaction, lastAnnouncedId, t])
|
}, [latestReaction, lastAnnouncedId, announceReactions, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
76
src/frontend/src/stores/accessibility.ts
Normal file
76
src/frontend/src/stores/accessibility.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { proxy, subscribe } from 'valtio'
|
||||||
|
import { STORAGE_KEYS } from '@/utils/storageKeys'
|
||||||
|
import { deserializeToProxyMap } from '@/utils/valtio'
|
||||||
|
|
||||||
|
type AccessibilityState = {
|
||||||
|
announceReactions: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_STATE: AccessibilityState = {
|
||||||
|
announceReactions: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAccessibilityState(): AccessibilityState {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEYS.ACCESSIBILITY)
|
||||||
|
if (stored) {
|
||||||
|
const parsed = JSON.parse(stored)
|
||||||
|
return {
|
||||||
|
...DEFAULT_STATE,
|
||||||
|
...parsed,
|
||||||
|
announceReactions:
|
||||||
|
typeof parsed.announceReactions === 'boolean'
|
||||||
|
? parsed.announceReactions
|
||||||
|
: DEFAULT_STATE.announceReactions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy migration: if the setting was previously stored in notifications
|
||||||
|
const legacy = localStorage.getItem(STORAGE_KEYS.NOTIFICATIONS)
|
||||||
|
if (legacy) {
|
||||||
|
try {
|
||||||
|
const parsedLegacy = JSON.parse(legacy, deserializeToProxyMap)
|
||||||
|
if (typeof parsedLegacy?.announceReactions === 'boolean') {
|
||||||
|
const migratedState: AccessibilityState = {
|
||||||
|
...DEFAULT_STATE,
|
||||||
|
...parsedLegacy,
|
||||||
|
announceReactions: parsedLegacy.announceReactions,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_KEYS.ACCESSIBILITY,
|
||||||
|
JSON.stringify(migratedState)
|
||||||
|
)
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.NOTIFICATIONS)
|
||||||
|
} catch {
|
||||||
|
// ignore persistence issues during migration
|
||||||
|
}
|
||||||
|
|
||||||
|
return migratedState
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore legacy parsing issues
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DEFAULT_STATE
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error(
|
||||||
|
'[AccessibilityStore] Failed to parse stored settings:',
|
||||||
|
error
|
||||||
|
)
|
||||||
|
return DEFAULT_STATE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const accessibilityStore = proxy<AccessibilityState>(
|
||||||
|
getAccessibilityState()
|
||||||
|
)
|
||||||
|
|
||||||
|
subscribe(accessibilityStore, () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_KEYS.ACCESSIBILITY,
|
||||||
|
JSON.stringify(accessibilityStore)
|
||||||
|
)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user