mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-27 18:56:58 +00:00
♻️(refactor) Share reactions toggle and paginated toolbar
Unify main and PiP emoji UI via context; remove PiP-only duplicates.
This commit is contained in:
+1
-2
@@ -14,6 +14,7 @@ and this project adheres to
|
|||||||
- ✨(helm) add support multiple transcribe worker / endpoint #1247
|
- ✨(helm) add support multiple transcribe worker / endpoint #1247
|
||||||
- ✨(backend) make LiveKit Egress recording encoding configurable #1288
|
- ✨(backend) make LiveKit Egress recording encoding configurable #1288
|
||||||
- ✨(summary) add speaker-to-participant assignment
|
- ✨(summary) add speaker-to-participant assignment
|
||||||
|
- ✨(feat) Introduce Picture-in-Picture (PiP) #890
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -235,8 +236,6 @@ and this project adheres to
|
|||||||
|
|
||||||
- ✨(backend) monitor throttling rate failure through sentry #964
|
- ✨(backend) monitor throttling rate failure through sentry #964
|
||||||
- 🚀(paas) add PaaS deployment scripts, tested on Scalingo #957
|
- 🚀(paas) add PaaS deployment scripts, tested on Scalingo #957
|
||||||
- ✨(feat) Introduce Picture-in-Picture (PiP) #890
|
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { HandToggle } from '@/features/rooms/livekit/components/controls/HandTog
|
|||||||
import { StartMediaButton } from '@/features/rooms/livekit/components/controls/StartMediaButton'
|
import { StartMediaButton } from '@/features/rooms/livekit/components/controls/StartMediaButton'
|
||||||
import { usePipElementSize } from '../hooks/usePipElementSize'
|
import { usePipElementSize } from '../hooks/usePipElementSize'
|
||||||
import { PipOptionsMenu } from './controls/PipOptionsMenu'
|
import { PipOptionsMenu } from './controls/PipOptionsMenu'
|
||||||
import { PipReactionsToggle } from './PipReactionsToggle'
|
import { ReactionsToggle } from '@/features/reactions/components/ReactionsToggle'
|
||||||
|
|
||||||
export const CollapsibleControls = {
|
export const CollapsibleControls = {
|
||||||
HAND: 'hand',
|
HAND: 'hand',
|
||||||
@@ -85,8 +85,12 @@ export const PipControlBar = ({
|
|||||||
<PipControlsCenter>
|
<PipControlsCenter>
|
||||||
<AudioDevicesControl hideMenu />
|
<AudioDevicesControl hideMenu />
|
||||||
<VideoDeviceControl hideMenu />
|
<VideoDeviceControl hideMenu />
|
||||||
{!hidden.has(CollapsibleControls.REACTIONS) && <PipReactionsToggle />}
|
{!hidden.has(CollapsibleControls.REACTIONS) && (
|
||||||
{showScreenShare && !hidden.has(CollapsibleControls.SCREEN_SHARE) && <ScreenShareToggle />}
|
<ReactionsToggle id="pip-reactions-toggle" />
|
||||||
|
)}
|
||||||
|
{showScreenShare && !hidden.has(CollapsibleControls.SCREEN_SHARE) && (
|
||||||
|
<ScreenShareToggle />
|
||||||
|
)}
|
||||||
{!hidden.has(CollapsibleControls.HAND) && <HandToggle />}
|
{!hidden.has(CollapsibleControls.HAND) && <HandToggle />}
|
||||||
<PipOptionsMenu overflowControls={hidden} />
|
<PipOptionsMenu overflowControls={hidden} />
|
||||||
<LeaveButton />
|
<LeaveButton />
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { useRoomPiP } from '../hooks/useRoomPiP'
|
import { useRoomPiP } from '../hooks/useRoomPiP'
|
||||||
import pipIllustration from '/assets/pip.svg'
|
|
||||||
|
|
||||||
export const PipPlaceholder = () => {
|
export const PipPlaceholder = () => {
|
||||||
const { t } = useTranslation('rooms', {
|
const { t } = useTranslation('rooms', {
|
||||||
@@ -11,7 +10,13 @@ export const PipPlaceholder = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Illustration src={pipIllustration} alt="" width={102} height={72} aria-hidden="true" />
|
<Illustration
|
||||||
|
src="/assets/pip.svg"
|
||||||
|
alt=""
|
||||||
|
width={102}
|
||||||
|
height={72}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
<Title>{t('title')}</Title>
|
<Title>{t('title')}</Title>
|
||||||
<Description>{t('description')}</Description>
|
<Description>{t('description')}</Description>
|
||||||
<BringBackLink onClick={close}>{t('bringBack')}</BringBackLink>
|
<BringBackLink onClick={close}>{t('bringBack')}</BringBackLink>
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { RiEmotionLine } from '@remixicon/react'
|
|
||||||
import { ToggleButton } from '@/primitives'
|
|
||||||
import { useSnapshot } from 'valtio'
|
|
||||||
import { pipLayoutStore } from '../stores/pipLayoutStore'
|
|
||||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
|
||||||
|
|
||||||
export const PipReactionsToggle = () => {
|
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
|
|
||||||
const { showReactionsToolbar: isOpen } = useSnapshot(pipLayoutStore)
|
|
||||||
|
|
||||||
const toggle = useCallback(() => {
|
|
||||||
pipLayoutStore.showReactionsToolbar = !pipLayoutStore.showReactionsToolbar
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useRegisterKeyboardShortcut({ id: 'reaction', handler: toggle })
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToggleButton
|
|
||||||
id="pip-reactions-toggle"
|
|
||||||
data-attr="pip-reactions-toggle"
|
|
||||||
square
|
|
||||||
variant="primaryDark"
|
|
||||||
aria-label={t('button')}
|
|
||||||
aria-expanded={isOpen}
|
|
||||||
tooltip={t('button')}
|
|
||||||
isSelected={isOpen}
|
|
||||||
onChange={toggle}
|
|
||||||
>
|
|
||||||
<RiEmotionLine />
|
|
||||||
</ToggleButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
|
||||||
import { FocusScope } from '@react-aria/focus'
|
|
||||||
import { styled } from '@/styled-system/jsx'
|
|
||||||
import { useSnapshot } from 'valtio'
|
|
||||||
import { pipLayoutStore } from '../stores/pipLayoutStore'
|
|
||||||
import { useDelayUnmount } from '@/hooks/useDelayUnmount'
|
|
||||||
import { usePipElementSize } from '../hooks/usePipElementSize'
|
|
||||||
import { PipReactionsKeyboardNavigation } from './reactions/PipReactionsKeyboardNavigation'
|
|
||||||
import { PipReactionsPill } from './reactions/PipReactionsPill'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reactions toolbar for the PiP window. Owns only the open/close orchestration;
|
|
||||||
* layout and pagination live in `PipReactionsPill`, keyboard nav in
|
|
||||||
* `PipReactionsKeyboardNavigation`.
|
|
||||||
*/
|
|
||||||
export const PipReactionsToolbar = () => {
|
|
||||||
const { showReactionsToolbar: isOpen } = useSnapshot(pipLayoutStore)
|
|
||||||
// Unmount content after the close transition so hidden emojis leave the tab order.
|
|
||||||
const renderContent = useDelayUnmount(isOpen, 500)
|
|
||||||
const contentRef = useRef<HTMLDivElement>(null)
|
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
|
||||||
const { width: availableWidth } = usePipElementSize(wrapperRef)
|
|
||||||
|
|
||||||
// Mark the subtree inert during the fade-out so Tab can't land on it.
|
|
||||||
useEffect(() => {
|
|
||||||
const el = contentRef.current
|
|
||||||
if (!el) return
|
|
||||||
if (isOpen) el.removeAttribute('inert')
|
|
||||||
else el.setAttribute('inert', '')
|
|
||||||
}, [isOpen, renderContent])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper ref={wrapperRef} isOpen={isOpen}>
|
|
||||||
{renderContent && (
|
|
||||||
<div ref={contentRef}>
|
|
||||||
{/* eslint-disable-next-line jsx-a11y/no-autofocus */}
|
|
||||||
<FocusScope autoFocus>
|
|
||||||
<PipReactionsKeyboardNavigation>
|
|
||||||
<PipReactionsPill
|
|
||||||
isOpen={isOpen}
|
|
||||||
availableWidth={availableWidth}
|
|
||||||
/>
|
|
||||||
</PipReactionsKeyboardNavigation>
|
|
||||||
</FocusScope>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Wrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Wrapper = styled('div', {
|
|
||||||
base: {
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
overflow: 'hidden',
|
|
||||||
maxHeight: 0,
|
|
||||||
padding: '0 0.5rem',
|
|
||||||
transition:
|
|
||||||
'max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1), padding 0.5s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
isOpen: {
|
|
||||||
true: {
|
|
||||||
maxHeight: '60px',
|
|
||||||
padding: '0.5rem 0.5rem 0.25rem',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -7,13 +7,14 @@ import {
|
|||||||
SidePanelStoreProvider,
|
SidePanelStoreProvider,
|
||||||
useSidePanel,
|
useSidePanel,
|
||||||
} from '@/features/rooms/livekit/hooks/useSidePanel'
|
} from '@/features/rooms/livekit/hooks/useSidePanel'
|
||||||
|
import { ReactionsToolbarStoreProvider } from '@/features/reactions/hooks/useReactionsToolbar'
|
||||||
import { pipLayoutStore } from '../stores/pipLayoutStore'
|
import { pipLayoutStore } from '../stores/pipLayoutStore'
|
||||||
import { useEscapeDismiss } from '../hooks/useEscapeDismiss'
|
import { useEscapeDismiss } from '../hooks/useEscapeDismiss'
|
||||||
import { usePipKeyboardShortcuts } from '../hooks/usePipKeyboardShortcuts'
|
import { usePipKeyboardShortcuts } from '../hooks/usePipKeyboardShortcuts'
|
||||||
import { usePipRestoreFocus } from '../hooks/usePipRestoreFocus'
|
import { usePipRestoreFocus } from '../hooks/usePipRestoreFocus'
|
||||||
import { usePipFocusModality } from '../hooks/usePipFocusModality'
|
import { usePipFocusModality } from '../hooks/usePipFocusModality'
|
||||||
import { PipControlBar } from './PipControlBar'
|
import { PipControlBar } from './PipControlBar'
|
||||||
import { PipReactionsToolbar } from './PipReactionsToolbar'
|
import { ReactionsToolbar } from '@/features/reactions/components/toolbar/ReactionsToolbar'
|
||||||
import { PipStage } from './layouts/PipStage'
|
import { PipStage } from './layouts/PipStage'
|
||||||
import { PipNotificationOverlay } from './notifications/PipNotificationOverlay'
|
import { PipNotificationOverlay } from './notifications/PipNotificationOverlay'
|
||||||
import { PipConnectionStateToast } from './notifications/PipConnectionStateToast'
|
import { PipConnectionStateToast } from './notifications/PipConnectionStateToast'
|
||||||
@@ -22,7 +23,9 @@ import { PipReactionPortals } from './PipReactionPortals'
|
|||||||
export const PipView = () => {
|
export const PipView = () => {
|
||||||
return (
|
return (
|
||||||
<SidePanelStoreProvider value={pipLayoutStore}>
|
<SidePanelStoreProvider value={pipLayoutStore}>
|
||||||
<PipViewContent />
|
<ReactionsToolbarStoreProvider value={pipLayoutStore}>
|
||||||
|
<PipViewContent />
|
||||||
|
</ReactionsToolbarStoreProvider>
|
||||||
</SidePanelStoreProvider>
|
</SidePanelStoreProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -62,7 +65,10 @@ const PipViewContent = () => {
|
|||||||
aria-label={t('windowLabel')}
|
aria-label={t('windowLabel')}
|
||||||
>
|
>
|
||||||
<PipStage />
|
<PipStage />
|
||||||
<PipReactionsToolbar />
|
<ReactionsToolbar
|
||||||
|
toggleId="pip-reactions-toggle"
|
||||||
|
controlBarId="pip-control-bar"
|
||||||
|
/>
|
||||||
<PipControlBar showScreenShare={browserSupportsScreenSharing} />
|
<PipControlBar showScreenShare={browserSupportsScreenSharing} />
|
||||||
<SidePanel />
|
<SidePanel />
|
||||||
<PipReactionPortals />
|
<PipReactionPortals />
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ type PipOptionsMenuItemsProps = {
|
|||||||
export const PipOptionsMenuItems = ({
|
export const PipOptionsMenuItems = ({
|
||||||
overflowControls,
|
overflowControls,
|
||||||
}: PipOptionsMenuItemsProps) => {
|
}: PipOptionsMenuItemsProps) => {
|
||||||
|
|
||||||
const hasOverflow = (overflowControls?.size ?? 0) > 0
|
const hasOverflow = (overflowControls?.size ?? 0) > 0
|
||||||
|
|
||||||
const hasOverflowControls = hasOverflow && overflowControls
|
const hasOverflowControls = hasOverflow && overflowControls
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RACMenu
|
<RACMenu
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { MenuItem } from 'react-aria-components'
|
import { MenuItem } from 'react-aria-components'
|
||||||
import {
|
import { RiHand, RiArrowUpLine, RiEmotionLine } from '@remixicon/react'
|
||||||
RiHand,
|
|
||||||
RiArrowUpLine,
|
|
||||||
RiEmotionLine,
|
|
||||||
} from '@remixicon/react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Track } from 'livekit-client'
|
import { Track } from 'livekit-client'
|
||||||
import { pipLayoutStore } from '@/features/pip/stores/pipLayoutStore'
|
|
||||||
import { menuRecipe } from '@/primitives/menuRecipe'
|
import { menuRecipe } from '@/primitives/menuRecipe'
|
||||||
import { useRoomContext, useTrackToggle } from '@livekit/components-react'
|
import { useRoomContext, useTrackToggle } from '@livekit/components-react'
|
||||||
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
|
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useReactionsToolbar } from '@/features/reactions/hooks/useReactionsToolbar'
|
||||||
import { CollapsibleControls, type CollapsibleControl } from '../PipControlBar'
|
import { CollapsibleControls, type CollapsibleControl } from '../PipControlBar'
|
||||||
|
|
||||||
type PipOverflowItemsProps = {
|
type PipOverflowItemsProps = {
|
||||||
@@ -31,10 +26,7 @@ export const PipOverflowItems = ({
|
|||||||
source: Track.Source.ScreenShare,
|
source: Track.Source.ScreenShare,
|
||||||
captureOptions: { audio: true, selfBrowserSurface: 'include' },
|
captureOptions: { audio: true, selfBrowserSurface: 'include' },
|
||||||
})
|
})
|
||||||
const pipLayoutSnap = useSnapshot(pipLayoutStore)
|
const { toggle: toggleReactions } = useReactionsToolbar()
|
||||||
const toggleReactions = () => {
|
|
||||||
pipLayoutStore.showReactionsToolbar = !pipLayoutSnap.showReactionsToolbar
|
|
||||||
}
|
|
||||||
const itemClass = menuRecipe({ icon: true, variant: 'dark' }).item
|
const itemClass = menuRecipe({ icon: true, variant: 'dark' }).item
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,11 +39,19 @@ export const PipOverflowItems = ({
|
|||||||
)}
|
)}
|
||||||
{overflowControls.has(CollapsibleControls.SCREEN_SHARE) && (
|
{overflowControls.has(CollapsibleControls.SCREEN_SHARE) && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onAction={() => screenShareProps.onClick?.({} as React.MouseEvent<HTMLButtonElement>)}
|
onAction={() =>
|
||||||
|
screenShareProps.onClick?.(
|
||||||
|
{} as React.MouseEvent<HTMLButtonElement>
|
||||||
|
)
|
||||||
|
}
|
||||||
className={itemClass}
|
className={itemClass}
|
||||||
>
|
>
|
||||||
<RiArrowUpLine size={20} />
|
<RiArrowUpLine size={20} />
|
||||||
{t(isScreenSharing ? 'controls.screenShare.stop' : 'controls.screenShare.start')}
|
{t(
|
||||||
|
isScreenSharing
|
||||||
|
? 'controls.screenShare.stop'
|
||||||
|
: 'controls.screenShare.start'
|
||||||
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{overflowControls.has(CollapsibleControls.HAND) && (
|
{overflowControls.has(CollapsibleControls.HAND) && (
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ export const PipFocusLayout = memo(
|
|||||||
return (
|
return (
|
||||||
<FocusContainer>
|
<FocusContainer>
|
||||||
<MainSlot>
|
<MainSlot>
|
||||||
<ParticipantTile
|
<ParticipantTile key={getTrackKey(mainTrack)} trackRef={mainTrack} />
|
||||||
key={getTrackKey(mainTrack)}
|
|
||||||
trackRef={mainTrack}
|
|
||||||
/>
|
|
||||||
</MainSlot>
|
</MainSlot>
|
||||||
{thumbnailTrack && (
|
{thumbnailTrack && (
|
||||||
<Thumbnail>
|
<Thumbnail>
|
||||||
|
|||||||
@@ -36,7 +36,13 @@ export const PipNotificationOverlay = () => {
|
|||||||
<StyledToastContainer
|
<StyledToastContainer
|
||||||
key={toast.key}
|
key={toast.key}
|
||||||
aria-atomic="true"
|
aria-atomic="true"
|
||||||
css={{ margin: 0, marginLeft: 0, display: 'flex', alignItems: 'center', paddingRight: '0.25rem' }}
|
css={{
|
||||||
|
margin: 0,
|
||||||
|
marginLeft: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingRight: '0.25rem',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PipToastBody toast={toast} />
|
<PipToastBody toast={toast} />
|
||||||
<Button
|
<Button
|
||||||
@@ -63,4 +69,3 @@ const Region = styled('div', {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RiEmotionLine } from '@remixicon/react'
|
import { RiEmotionLine } from '@remixicon/react'
|
||||||
import { ToggleButton } from '@/primitives'
|
import { ToggleButton } from '@/primitives'
|
||||||
@@ -6,40 +6,37 @@ import { ToggleButton } from '@/primitives'
|
|||||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||||
import { REACTIONS_TOOLBAR_ID } from '../constants'
|
import { REACTIONS_TOOLBAR_ID } from '../constants'
|
||||||
import { useReactionsToolbar } from '../hooks/useReactionsToolbar'
|
import { useReactionsToolbar } from '../hooks/useReactionsToolbar'
|
||||||
import { layoutStore } from '@/stores/layout'
|
|
||||||
import { useRoomPiP } from '@/features/pip/hooks/useRoomPiP'
|
|
||||||
|
|
||||||
const focusReactionsToolbar = () => {
|
type ReactionsToggleProps = {
|
||||||
document
|
id?: string
|
||||||
.getElementById(REACTIONS_TOOLBAR_ID)
|
|
||||||
?.querySelector<HTMLElement>('button')
|
|
||||||
?.focus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReactionsToggle = () => {
|
export const ReactionsToggle = ({
|
||||||
|
id = 'reactions-toggle',
|
||||||
|
}: ReactionsToggleProps) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
|
||||||
const { isOpen: isPiPOpen } = useRoomPiP()
|
|
||||||
|
|
||||||
const { isOpen, toggle } = useReactionsToolbar()
|
const { isOpen, toggle } = useReactionsToolbar()
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
const handleShortcut = useCallback(() => {
|
const handleShortcut = useCallback(() => {
|
||||||
if (layoutStore.showReactionsToolbar) {
|
if (isOpen) {
|
||||||
focusReactionsToolbar()
|
const doc = buttonRef.current?.ownerDocument ?? document
|
||||||
|
doc
|
||||||
|
.getElementById(REACTIONS_TOOLBAR_ID)
|
||||||
|
?.querySelector<HTMLElement>('button')
|
||||||
|
?.focus()
|
||||||
} else {
|
} else {
|
||||||
layoutStore.showReactionsToolbar = true
|
toggle()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [isOpen, toggle])
|
||||||
|
|
||||||
useRegisterKeyboardShortcut({
|
useRegisterKeyboardShortcut({ id: 'reaction', handler: handleShortcut })
|
||||||
id: 'reaction',
|
|
||||||
handler: handleShortcut,
|
|
||||||
isDisabled: isPiPOpen,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
id="reactions-toggle"
|
ref={buttonRef}
|
||||||
data-attr="reactions-toggle"
|
id={id}
|
||||||
|
data-attr={id}
|
||||||
square
|
square
|
||||||
variant="primaryDark"
|
variant="primaryDark"
|
||||||
aria-label={t('button')}
|
aria-label={t('button')}
|
||||||
|
|||||||
+20
-20
@@ -2,31 +2,24 @@ import { useRef, type ReactNode } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useFocusManager } from '@react-aria/focus'
|
import { useFocusManager } from '@react-aria/focus'
|
||||||
import { findFirstFocusable } from '@/utils/dom'
|
import { findFirstFocusable } from '@/utils/dom'
|
||||||
import { pipLayoutStore } from '@/features/pip/stores/pipLayoutStore'
|
import { useReactionsToolbar } from '../../hooks/useReactionsToolbar'
|
||||||
import { useEscapeDismiss } from '@/features/pip/hooks/useEscapeDismiss'
|
import { REACTIONS_TOOLBAR_ID } from '../../constants'
|
||||||
|
|
||||||
const REACTIONS_TOGGLE_ID = 'pip-reactions-toggle'
|
type Props = {
|
||||||
const CONTROL_BAR_ID = 'pip-control-bar'
|
children: ReactNode
|
||||||
|
toggleId?: string
|
||||||
const closeToolbar = () => {
|
controlBarId?: string
|
||||||
pipLayoutStore.showReactionsToolbar = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Keyboard navigation for the PiP reactions toolbar (mirrors the main app). */
|
export const ReactionsKeyboardNavigation = ({
|
||||||
export const PipReactionsKeyboardNavigation = ({
|
|
||||||
children,
|
children,
|
||||||
}: {
|
toggleId = 'reactions-toggle',
|
||||||
children: ReactNode
|
controlBarId = 'control-bar',
|
||||||
}) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
|
||||||
const focusManager = useFocusManager()
|
const focusManager = useFocusManager()
|
||||||
const rootRef = useRef<HTMLDivElement>(null)
|
const rootRef = useRef<HTMLDivElement>(null)
|
||||||
|
const { close } = useReactionsToolbar()
|
||||||
useEscapeDismiss(rootRef, true, () => {
|
|
||||||
const doc = rootRef.current?.ownerDocument ?? document
|
|
||||||
doc.getElementById(REACTIONS_TOGGLE_ID)?.focus()
|
|
||||||
closeToolbar()
|
|
||||||
})
|
|
||||||
|
|
||||||
const onFocus = (event: React.FocusEvent<HTMLDivElement>) => {
|
const onFocus = (event: React.FocusEvent<HTMLDivElement>) => {
|
||||||
const fromOutside = !event.currentTarget.contains(event.relatedTarget)
|
const fromOutside = !event.currentTarget.contains(event.relatedTarget)
|
||||||
@@ -34,6 +27,8 @@ export const PipReactionsKeyboardNavigation = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
const doc = rootRef.current?.ownerDocument ?? document
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
focusManager?.focusNext({ wrap: true })
|
focusManager?.focusNext({ wrap: true })
|
||||||
@@ -41,11 +36,15 @@ export const PipReactionsKeyboardNavigation = ({
|
|||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
focusManager?.focusPrevious({ wrap: true })
|
focusManager?.focusPrevious({ wrap: true })
|
||||||
break
|
break
|
||||||
|
case 'Escape':
|
||||||
|
event.preventDefault()
|
||||||
|
doc.getElementById(toggleId)?.focus()
|
||||||
|
close()
|
||||||
|
break
|
||||||
case 'Tab':
|
case 'Tab':
|
||||||
if (!event.shiftKey) {
|
if (!event.shiftKey) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const doc = rootRef.current?.ownerDocument ?? document
|
findFirstFocusable(doc.getElementById(controlBarId))?.focus()
|
||||||
findFirstFocusable(doc.getElementById(CONTROL_BAR_ID))?.focus()
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -54,6 +53,7 @@ export const PipReactionsKeyboardNavigation = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={rootRef}
|
ref={rootRef}
|
||||||
|
id={REACTIONS_TOOLBAR_ID}
|
||||||
role="toolbar"
|
role="toolbar"
|
||||||
aria-label={t('toolbar')}
|
aria-label={t('toolbar')}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
+4
-9
@@ -2,21 +2,17 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
|
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { ReactionButton } from '@/features/reactions/components/toolbar/ReactionButton'
|
import { ReactionButton } from './ReactionButton'
|
||||||
import {
|
import {
|
||||||
computeReactionsPage,
|
computeReactionsPage,
|
||||||
getMaxPageStart,
|
getMaxPageStart,
|
||||||
} from '../../utils/pipReactionsPagination'
|
} from '../../utils/reactionsPagination'
|
||||||
|
|
||||||
type Props = { isOpen: boolean; availableWidth: number }
|
type Props = { isOpen: boolean; availableWidth: number }
|
||||||
|
|
||||||
/**
|
export const ReactionsPill = ({ isOpen, availableWidth }: Props) => {
|
||||||
* Paginated emoji pill with animated entry/exit. Responsibility: layout the
|
|
||||||
* visible emojis for the currently available width and expose prev/next arrows.
|
|
||||||
*/
|
|
||||||
export const PipReactionsPill = ({ isOpen, availableWidth }: Props) => {
|
|
||||||
const { t } = useTranslation('rooms', {
|
const { t } = useTranslation('rooms', {
|
||||||
keyPrefix: 'options.items.pictureInPicture',
|
keyPrefix: 'controls.reactions',
|
||||||
})
|
})
|
||||||
const [isVisible, setIsVisible] = useState(false)
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
const [pageStart, setPageStart] = useState(0)
|
const [pageStart, setPageStart] = useState(0)
|
||||||
@@ -36,7 +32,6 @@ export const PipReactionsPill = ({ isOpen, availableWidth }: Props) => {
|
|||||||
[availableWidth, pageStart]
|
[availableWidth, pageStart]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Clamp pageStart if the window was resized and the current page no longer fits.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasOverflow) {
|
if (!hasOverflow) {
|
||||||
setPageStart(0)
|
setPageStart(0)
|
||||||
@@ -1,155 +1,69 @@
|
|||||||
import { FocusScope, useFocusManager } from '@react-aria/focus'
|
import { useEffect, useRef } from 'react'
|
||||||
import { REACTIONS_TOOLBAR_ID } from '../../constants'
|
import { FocusScope } from '@react-aria/focus'
|
||||||
import { useReactionsToolbar } from '../../hooks/useReactionsToolbar'
|
|
||||||
import { ReactionButton } from './ReactionButton'
|
|
||||||
import { Emoji } from '../../types'
|
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { layoutStore } from '@/stores/layout'
|
import { useReactionsToolbar } from '../../hooks/useReactionsToolbar'
|
||||||
import { getFirstControlBarFocusable } from '@/utils/dom'
|
|
||||||
import { useIsMobile } from '@/utils/useIsMobile'
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import { useDelayUnmount } from '@/hooks/useDelayUnmount'
|
import { useDelayUnmount } from '@/hooks/useDelayUnmount'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { usePipElementSize } from '@/features/pip/hooks/usePipElementSize'
|
||||||
|
import { ReactionsKeyboardNavigation } from './ReactionsKeyboardNavigation'
|
||||||
|
import { ReactionsPill } from './ReactionsPill'
|
||||||
|
|
||||||
const Container = styled('div', {
|
type ReactionsToolbarProps = {
|
||||||
|
toggleId?: string
|
||||||
|
controlBarId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReactionsToolbar = ({
|
||||||
|
toggleId = 'reactions-toggle',
|
||||||
|
controlBarId = 'control-bar',
|
||||||
|
}: ReactionsToolbarProps) => {
|
||||||
|
const { isOpen } = useReactionsToolbar()
|
||||||
|
const renderContent = useDelayUnmount(isOpen, 500)
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null)
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
|
const { width: availableWidth } = usePipElementSize(wrapperRef)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = contentRef.current
|
||||||
|
if (!el) return
|
||||||
|
if (isOpen) el.removeAttribute('inert')
|
||||||
|
else el.setAttribute('inert', '')
|
||||||
|
}, [isOpen, renderContent])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper ref={wrapperRef} isOpen={isOpen}>
|
||||||
|
{renderContent && (
|
||||||
|
<div ref={contentRef}>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/no-autofocus */}
|
||||||
|
<FocusScope autoFocus>
|
||||||
|
<ReactionsKeyboardNavigation
|
||||||
|
toggleId={toggleId}
|
||||||
|
controlBarId={controlBarId}
|
||||||
|
>
|
||||||
|
<ReactionsPill isOpen={isOpen} availableWidth={availableWidth} />
|
||||||
|
</ReactionsKeyboardNavigation>
|
||||||
|
</FocusScope>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = styled('div', {
|
||||||
base: {
|
base: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
position: 'absolute',
|
overflow: 'hidden',
|
||||||
bottom: 'var(--sizes-room-control-bar)',
|
maxHeight: 0,
|
||||||
left: 0,
|
width: '100%',
|
||||||
right: 0,
|
transition:
|
||||||
pointerEvents: 'none',
|
'max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1), padding 0.5s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const StyledStrip = styled('div', {
|
|
||||||
base: {
|
|
||||||
display: 'flex',
|
|
||||||
gap: '0.2rem',
|
|
||||||
borderRadius: '21px',
|
|
||||||
padding: '0.15rem',
|
|
||||||
backgroundColor: 'primaryDark.100',
|
|
||||||
opacity: 0,
|
|
||||||
transform: 'translateY(3.25rem)',
|
|
||||||
transition: 'opacity, transform',
|
|
||||||
transitionDuration: '0.5s',
|
|
||||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
isVisible: {
|
isOpen: {
|
||||||
true: {
|
true: {
|
||||||
opacity: 1,
|
maxHeight: '60px',
|
||||||
transform: 'translateY(0)',
|
padding: '0.5rem 0',
|
||||||
pointerEvents: 'auto',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
desktopOffset: {
|
|
||||||
true: {
|
|
||||||
// Ideally this value should be calculated dynamically in JavaScript to keep
|
|
||||||
// the reaction toolbar perfectly centered relative to the reaction toggle.
|
|
||||||
// However, for simplicity and to follow a pragmatic 80/20 approach,
|
|
||||||
// this value is currently hardcoded in CSS.
|
|
||||||
marginRight: '30px',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const Strip = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
const { isOpen } = useReactionsToolbar()
|
|
||||||
const isMobile = useIsMobile()
|
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const [isVisible, setIsVisible] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
// defer one frame so the browser paints opacity:0 first
|
|
||||||
const id = requestAnimationFrame(() => setIsVisible(true))
|
|
||||||
return () => cancelAnimationFrame(id)
|
|
||||||
} else {
|
|
||||||
setIsVisible(false)
|
|
||||||
}
|
|
||||||
}, [isOpen])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledStrip
|
|
||||||
ref={ref}
|
|
||||||
aria-hidden={!isOpen}
|
|
||||||
isVisible={isVisible}
|
|
||||||
desktopOffset={!isMobile}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledStrip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const KeyboardNavigation = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
|
|
||||||
const focusManager = useFocusManager()
|
|
||||||
|
|
||||||
const onFocus = (e: React.FocusEvent<HTMLDivElement>) => {
|
|
||||||
const comingFromOutside = !e.currentTarget.contains(e.relatedTarget)
|
|
||||||
if (comingFromOutside) {
|
|
||||||
focusManager?.focusFirst()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
switch (e.key) {
|
|
||||||
case 'ArrowRight':
|
|
||||||
focusManager?.focusNext({ wrap: true })
|
|
||||||
break
|
|
||||||
case 'ArrowLeft':
|
|
||||||
focusManager?.focusPrevious({ wrap: true })
|
|
||||||
break
|
|
||||||
case 'Escape':
|
|
||||||
e.preventDefault()
|
|
||||||
document.getElementById('reactions-toggle')?.focus()
|
|
||||||
layoutStore.showReactionsToolbar = false
|
|
||||||
break
|
|
||||||
case 'Tab':
|
|
||||||
if (!e.shiftKey) {
|
|
||||||
e.preventDefault()
|
|
||||||
getFirstControlBarFocusable('control-bar')?.focus()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id={REACTIONS_TOOLBAR_ID}
|
|
||||||
role="toolbar"
|
|
||||||
aria-label={t('toolbar')}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onFocus={onFocus}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ReactionsToolbar = () => {
|
|
||||||
const { isOpen } = useReactionsToolbar()
|
|
||||||
const shouldMount = useDelayUnmount(isOpen, 300)
|
|
||||||
|
|
||||||
if (!shouldMount) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
{/* eslint-disable-next-line jsx-a11y/no-autofocus*/}
|
|
||||||
<FocusScope autoFocus>
|
|
||||||
<KeyboardNavigation>
|
|
||||||
<Strip>
|
|
||||||
{Object.values(Emoji).map((emoji) => (
|
|
||||||
<ReactionButton key={emoji} emoji={emoji} />
|
|
||||||
))}
|
|
||||||
</Strip>
|
|
||||||
</KeyboardNavigation>
|
|
||||||
</FocusScope>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,28 @@
|
|||||||
|
import { createContext, useContext } from 'react'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { layoutStore } from '@/stores/layout'
|
import { layoutStore } from '@/stores/layout'
|
||||||
|
|
||||||
|
export type ReactionsToolbarStore = {
|
||||||
|
showReactionsToolbar: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReactionsToolbarStoreContext =
|
||||||
|
createContext<ReactionsToolbarStore>(layoutStore)
|
||||||
|
|
||||||
|
export const ReactionsToolbarStoreProvider =
|
||||||
|
ReactionsToolbarStoreContext.Provider
|
||||||
|
|
||||||
export const useReactionsToolbar = () => {
|
export const useReactionsToolbar = () => {
|
||||||
const layoutSnap = useSnapshot(layoutStore)
|
const store = useContext(ReactionsToolbarStoreContext)
|
||||||
|
const snap = useSnapshot(store)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isOpen: layoutSnap.showReactionsToolbar,
|
isOpen: snap.showReactionsToolbar,
|
||||||
toggle: () => {
|
toggle: () => {
|
||||||
layoutStore.showReactionsToolbar = !layoutSnap.showReactionsToolbar
|
store.showReactionsToolbar = !store.showReactionsToolbar
|
||||||
|
},
|
||||||
|
close: () => {
|
||||||
|
store.showReactionsToolbar = false
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,6 +219,8 @@
|
|||||||
"toolbar": "Reaktion senden",
|
"toolbar": "Reaktion senden",
|
||||||
"announce": "{{name}} : {{emoji}}",
|
"announce": "{{name}} : {{emoji}}",
|
||||||
"you": "du",
|
"you": "du",
|
||||||
|
"previousReactions": "Vorherige Reaktionen",
|
||||||
|
"nextReactions": "Nächste Reaktionen",
|
||||||
"emojis": {
|
"emojis": {
|
||||||
"thumbs-up": "Daumen hoch",
|
"thumbs-up": "Daumen hoch",
|
||||||
"thumbs-down": "Daumen runter",
|
"thumbs-down": "Daumen runter",
|
||||||
@@ -249,8 +251,6 @@
|
|||||||
"windowLabel": "Bild-im-Bild Besprechung",
|
"windowLabel": "Bild-im-Bild Besprechung",
|
||||||
"stage": "Teilnehmer",
|
"stage": "Teilnehmer",
|
||||||
"controlBar": "Besprechungssteuerung",
|
"controlBar": "Besprechungssteuerung",
|
||||||
"previousReactions": "Vorherige Reaktionen",
|
|
||||||
"nextReactions": "Nächste Reaktionen",
|
|
||||||
"notificationsLabel": "Benachrichtigungen",
|
"notificationsLabel": "Benachrichtigungen",
|
||||||
"dismissNotification": "Benachrichtigung schließen",
|
"dismissNotification": "Benachrichtigung schließen",
|
||||||
"connection": {
|
"connection": {
|
||||||
|
|||||||
@@ -219,6 +219,8 @@
|
|||||||
"toolbar": "Send reaction",
|
"toolbar": "Send reaction",
|
||||||
"announce": "{{name}} : {{emoji}}",
|
"announce": "{{name}} : {{emoji}}",
|
||||||
"you": "you",
|
"you": "you",
|
||||||
|
"previousReactions": "Previous reactions",
|
||||||
|
"nextReactions": "Next reactions",
|
||||||
"emojis": {
|
"emojis": {
|
||||||
"thumbs-up": "thumbs up",
|
"thumbs-up": "thumbs up",
|
||||||
"thumbs-down": "thumbs down",
|
"thumbs-down": "thumbs down",
|
||||||
@@ -249,8 +251,6 @@
|
|||||||
"windowLabel": "Picture-in-picture meeting",
|
"windowLabel": "Picture-in-picture meeting",
|
||||||
"stage": "Participants",
|
"stage": "Participants",
|
||||||
"controlBar": "Meeting controls",
|
"controlBar": "Meeting controls",
|
||||||
"previousReactions": "Previous reactions",
|
|
||||||
"nextReactions": "Next reactions",
|
|
||||||
"notificationsLabel": "Notifications",
|
"notificationsLabel": "Notifications",
|
||||||
"dismissNotification": "Dismiss notification",
|
"dismissNotification": "Dismiss notification",
|
||||||
"connection": {
|
"connection": {
|
||||||
|
|||||||
@@ -219,6 +219,8 @@
|
|||||||
"toolbar": "Envoyer une réaction",
|
"toolbar": "Envoyer une réaction",
|
||||||
"announce": "{{name}} : {{emoji}}",
|
"announce": "{{name}} : {{emoji}}",
|
||||||
"you": "vous",
|
"you": "vous",
|
||||||
|
"previousReactions": "Réactions précédentes",
|
||||||
|
"nextReactions": "Réactions suivantes",
|
||||||
"emojis": {
|
"emojis": {
|
||||||
"thumbs-up": "pouce levé",
|
"thumbs-up": "pouce levé",
|
||||||
"thumbs-down": "pouce baissé",
|
"thumbs-down": "pouce baissé",
|
||||||
@@ -249,8 +251,6 @@
|
|||||||
"windowLabel": "Réunion en image dans l'image",
|
"windowLabel": "Réunion en image dans l'image",
|
||||||
"stage": "Participants",
|
"stage": "Participants",
|
||||||
"controlBar": "Commandes de la réunion",
|
"controlBar": "Commandes de la réunion",
|
||||||
"previousReactions": "Réactions précédentes",
|
|
||||||
"nextReactions": "Réactions suivantes",
|
|
||||||
"notificationsLabel": "Notifications",
|
"notificationsLabel": "Notifications",
|
||||||
"dismissNotification": "Fermer la notification",
|
"dismissNotification": "Fermer la notification",
|
||||||
"connection": {
|
"connection": {
|
||||||
|
|||||||
@@ -219,6 +219,8 @@
|
|||||||
"toolbar": "Stuur reactie",
|
"toolbar": "Stuur reactie",
|
||||||
"announce": "{{name}} : {{emoji}}",
|
"announce": "{{name}} : {{emoji}}",
|
||||||
"you": "U",
|
"you": "U",
|
||||||
|
"previousReactions": "Vorige reacties",
|
||||||
|
"nextReactions": "Volgende reacties",
|
||||||
"emojis": {
|
"emojis": {
|
||||||
"thumbs-up": "duim omhoog",
|
"thumbs-up": "duim omhoog",
|
||||||
"thumbs-down": "duim omlaag",
|
"thumbs-down": "duim omlaag",
|
||||||
@@ -249,8 +251,6 @@
|
|||||||
"windowLabel": "Beeld-in-beeld vergadering",
|
"windowLabel": "Beeld-in-beeld vergadering",
|
||||||
"stage": "Deelnemers",
|
"stage": "Deelnemers",
|
||||||
"controlBar": "Vergaderbesturing",
|
"controlBar": "Vergaderbesturing",
|
||||||
"previousReactions": "Vorige reacties",
|
|
||||||
"nextReactions": "Volgende reacties",
|
|
||||||
"notificationsLabel": "Meldingen",
|
"notificationsLabel": "Meldingen",
|
||||||
"dismissNotification": "Melding sluiten",
|
"dismissNotification": "Melding sluiten",
|
||||||
"connection": {
|
"connection": {
|
||||||
|
|||||||
@@ -24,10 +24,7 @@ export const Menu = ({
|
|||||||
return (
|
return (
|
||||||
<MenuTrigger>
|
<MenuTrigger>
|
||||||
{trigger}
|
{trigger}
|
||||||
<StyledPopover
|
<StyledPopover placement={placement} boundaryElement={boundaryElement}>
|
||||||
placement={placement}
|
|
||||||
boundaryElement={boundaryElement}
|
|
||||||
>
|
|
||||||
<Box size="sm" type="popover" variant={variant}>
|
<Box size="sm" type="popover" variant={variant}>
|
||||||
{menu}
|
{menu}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -7,5 +7,4 @@ import { createContext, useContext } from 'react'
|
|||||||
*/
|
*/
|
||||||
export const VisualOnlyTooltipsContext = createContext(false)
|
export const VisualOnlyTooltipsContext = createContext(false)
|
||||||
|
|
||||||
export const useVisualOnlyTooltips = () =>
|
export const useVisualOnlyTooltips = () => useContext(VisualOnlyTooltipsContext)
|
||||||
useContext(VisualOnlyTooltipsContext)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user