♿️(frontend) adjust visual-only tooltip a11y labels
Ensure tooltips stay visual while exposing correct aria-labels.
This commit is contained in:
@@ -8,14 +8,15 @@ and this project adheres to
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- ♿️(frontend) adjust visual-only tooltip a11y labels #910
|
||||||
|
- ♿(frontend) adjust sr announcements for idle disconnect timer #908
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- 🔒️(frontend) fix an XSS vulnerability on the recording page #911
|
- 🔒️(frontend) fix an XSS vulnerability on the recording page #911
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- ♿(frontend) adjust sr announcements for idle disconnect timer #908
|
|
||||||
|
|
||||||
## [1.4.0] - 2026-01-25
|
## [1.4.0] - 2026-01-25
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { FeedbackBanner } from '@/components/FeedbackBanner'
|
|||||||
import { Menu } from '@/primitives/Menu'
|
import { Menu } from '@/primitives/Menu'
|
||||||
import { MenuList } from '@/primitives/MenuList'
|
import { MenuList } from '@/primitives/MenuList'
|
||||||
import { LoginButton } from '@/components/LoginButton'
|
import { LoginButton } from '@/components/LoginButton'
|
||||||
|
import { VisualOnlyTooltip } from '@/primitives/VisualOnlyTooltip'
|
||||||
|
|
||||||
import { useLoginHint } from '@/hooks/useLoginHint'
|
import { useLoginHint } from '@/hooks/useLoginHint'
|
||||||
|
|
||||||
@@ -90,6 +91,11 @@ export const Header = () => {
|
|||||||
const isTermsOfService = useMatchesRoute('termsOfService')
|
const isTermsOfService = useMatchesRoute('termsOfService')
|
||||||
const isRoom = useMatchesRoute('room')
|
const isRoom = useMatchesRoute('room')
|
||||||
const { user, isLoggedIn, logout } = useUser()
|
const { user, isLoggedIn, logout } = useUser()
|
||||||
|
const userLabel = user?.full_name || user?.email
|
||||||
|
const loggedInTooltip = t('loggedInUserTooltip')
|
||||||
|
const loggedInAriaLabel = userLabel
|
||||||
|
? `${loggedInTooltip} ${userLabel}`
|
||||||
|
: loggedInTooltip
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -153,23 +159,24 @@ export const Header = () => {
|
|||||||
)}
|
)}
|
||||||
{!!user && (
|
{!!user && (
|
||||||
<Menu>
|
<Menu>
|
||||||
<Button
|
<Button size="sm" variant="secondaryText">
|
||||||
size="sm"
|
<VisualOnlyTooltip
|
||||||
variant="secondaryText"
|
tooltip={loggedInTooltip}
|
||||||
tooltip={t('loggedInUserTooltip')}
|
ariaLabel={loggedInAriaLabel}
|
||||||
tooltipType="delayed"
|
tooltipPosition="bottom"
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={css({
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
maxWidth: '350px',
|
|
||||||
display: { base: 'none', xsm: 'block' },
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{user?.full_name || user?.email}
|
<span
|
||||||
</span>
|
className={css({
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
maxWidth: '350px',
|
||||||
|
display: { base: 'none', xsm: 'block' },
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{user?.full_name || user?.email}
|
||||||
|
</span>
|
||||||
|
</VisualOnlyTooltip>
|
||||||
</Button>
|
</Button>
|
||||||
<MenuList
|
<MenuList
|
||||||
variant={'light'}
|
variant={'light'}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"heading": "You don't have the permission to view this page"
|
"heading": "You don't have the permission to view this page"
|
||||||
},
|
},
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"loggedInUserTooltip": "Logged in as…",
|
"loggedInUserTooltip": "Logged in as ",
|
||||||
"login": {
|
"login": {
|
||||||
"buttonLabel": "Login",
|
"buttonLabel": "Login",
|
||||||
"proconnectButtonLabel": "Login with ProConnect",
|
"proconnectButtonLabel": "Login with ProConnect",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"heading": "Accès interdit"
|
"heading": "Accès interdit"
|
||||||
},
|
},
|
||||||
"loading": "Chargement…",
|
"loading": "Chargement…",
|
||||||
"loggedInUserTooltip": "Connecté en tant que…",
|
"loggedInUserTooltip": "Connecté en tant que ",
|
||||||
"login": {
|
"login": {
|
||||||
"buttonLabel": "Se connecter",
|
"buttonLabel": "Se connecter",
|
||||||
"proconnectButtonLabel": "S'identifier avec ProConnect",
|
"proconnectButtonLabel": "S'identifier avec ProConnect",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"heading": "U hebt geen toestemming om deze pagina te bekijken"
|
"heading": "U hebt geen toestemming om deze pagina te bekijken"
|
||||||
},
|
},
|
||||||
"loading": "Laden ...",
|
"loading": "Laden ...",
|
||||||
"loggedInUserTooltip": "Ingelogd als ...",
|
"loggedInUserTooltip": "Ingelogd als ",
|
||||||
"login": {
|
"login": {
|
||||||
"buttonLabel": "Log in",
|
"buttonLabel": "Log in",
|
||||||
"proconnectButtonLabel": "Log in met Proconnect",
|
"proconnectButtonLabel": "Log in met Proconnect",
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import { type ReactNode, useRef, useState } from 'react'
|
import {
|
||||||
|
type ReactElement,
|
||||||
|
cloneElement,
|
||||||
|
isValidElement,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
|
|
||||||
export type VisualOnlyTooltipProps = {
|
export type VisualOnlyTooltipProps = {
|
||||||
children: ReactNode
|
children: ReactElement
|
||||||
tooltip: string
|
tooltip: string
|
||||||
|
ariaLabel?: string
|
||||||
|
tooltipPosition?: 'top' | 'bottom'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,32 +28,50 @@ export type VisualOnlyTooltipProps = {
|
|||||||
export const VisualOnlyTooltip = ({
|
export const VisualOnlyTooltip = ({
|
||||||
children,
|
children,
|
||||||
tooltip,
|
tooltip,
|
||||||
|
ariaLabel,
|
||||||
|
tooltipPosition = 'top',
|
||||||
}: VisualOnlyTooltipProps) => {
|
}: VisualOnlyTooltipProps) => {
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
|
||||||
const [isVisible, setIsVisible] = useState(false)
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [position, setPosition] = useState<{
|
||||||
|
top: number
|
||||||
|
left: number
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
const getPosition = () => {
|
const isBottom = tooltipPosition === 'bottom'
|
||||||
if (!wrapperRef.current) return null
|
|
||||||
|
const showTooltip = () => {
|
||||||
|
if (!wrapperRef.current) return
|
||||||
const rect = wrapperRef.current.getBoundingClientRect()
|
const rect = wrapperRef.current.getBoundingClientRect()
|
||||||
return {
|
setPosition({
|
||||||
top: rect.top - 8,
|
top: isBottom ? rect.bottom + 8 : rect.top - 8,
|
||||||
left: rect.left + rect.width / 2,
|
left: rect.left + rect.width / 2,
|
||||||
}
|
})
|
||||||
|
setIsVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideTooltip = () => {
|
||||||
|
setIsVisible(false)
|
||||||
|
setPosition(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = getPosition()
|
|
||||||
const tooltipData = isVisible && position ? { isVisible, position } : null
|
const tooltipData = isVisible && position ? { isVisible, position } : null
|
||||||
|
const wrappedChild = isValidElement(children)
|
||||||
|
? cloneElement(children, {
|
||||||
|
...(ariaLabel ? { 'aria-label': ariaLabel } : {}),
|
||||||
|
})
|
||||||
|
: children
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
ref={wrapperRef}
|
ref={wrapperRef}
|
||||||
onMouseEnter={() => setIsVisible(true)}
|
onMouseEnter={showTooltip}
|
||||||
onMouseLeave={() => setIsVisible(false)}
|
onMouseLeave={hideTooltip}
|
||||||
onFocus={() => setIsVisible(true)}
|
onFocus={showTooltip}
|
||||||
onBlur={() => setIsVisible(false)}
|
onBlur={hideTooltip}
|
||||||
>
|
>
|
||||||
{children}
|
{wrappedChild}
|
||||||
</div>
|
</div>
|
||||||
{tooltipData &&
|
{tooltipData &&
|
||||||
createPortal(
|
createPortal(
|
||||||
@@ -66,17 +92,26 @@ export const VisualOnlyTooltip = ({
|
|||||||
'&::after': {
|
'&::after': {
|
||||||
content: '""',
|
content: '""',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '100%',
|
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
border: '4px solid transparent',
|
border: '4px solid transparent',
|
||||||
borderTopColor: 'primaryDark.100',
|
...(isBottom
|
||||||
|
? {
|
||||||
|
bottom: '100%',
|
||||||
|
borderBottomColor: 'primaryDark.100',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
top: '100%',
|
||||||
|
borderTopColor: 'primaryDark.100',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
top: `${tooltipData.position.top}px`,
|
top: `${tooltipData.position.top}px`,
|
||||||
left: `${tooltipData.position.left}px`,
|
left: `${tooltipData.position.left}px`,
|
||||||
transform: 'translate(-50%, -100%)',
|
transform: isBottom
|
||||||
|
? 'translate(-50%, 0)'
|
||||||
|
: 'translate(-50%, -100%)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tooltip}
|
{tooltip}
|
||||||
|
|||||||
Reference in New Issue
Block a user