Skip to content

Commit d0cbaad

Browse files
authored
feat: redesign Tooltip (#3116)
1 parent 397fadd commit d0cbaad

6 files changed

Lines changed: 126 additions & 69 deletions

File tree

examples/vite/src/AppSettings/ActionsMenu/ActionsMenu.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ContextMenu,
66
ContextMenuButton,
77
IconBolt,
8+
Tooltip,
89
useContextMenuContext,
910
useDialogIsOpen,
1011
useDialogOnNearestManager,
@@ -48,12 +49,12 @@ const ActionsMenuButton = ({
4849
)}
4950
</Button>
5051
{iconOnly && (
51-
<div
52+
<Tooltip
5253
aria-hidden='true'
53-
className='str-chat__chat-view__selector-button-tooltip str-chat__tooltip'
54+
className='str-chat__chat-view__selector-button-tooltip'
5455
>
5556
Actions
56-
</div>
57+
</Tooltip>
5758
)}
5859
</div>
5960
);

examples/vite/src/AppSettings/ActionsMenu/NotificationPromptDialog.tsx

Lines changed: 78 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
IconExclamationTriangleFill,
1717
IconPlusSmall,
1818
NumericInput,
19+
PopperTooltip,
1920
Prompt,
2021
TextInput,
2122
useNotificationApi,
@@ -105,54 +106,85 @@ const NotificationChipList = ({
105106
}: {
106107
notifications: QueuedNotification[];
107108
removeQueuedNotification: (id: string) => void;
108-
}) => (
109-
<div className='app__notification-dialog__chips' role='list'>
110-
{notifications.map((notification) => {
111-
const SeverityIcon = severityIcons[notification.severity];
112-
const DirectionIcon = directionIcons[notification.entryDirection];
113-
114-
return (
115-
<div
116-
className='app__notification-dialog__chip'
117-
key={notification.id}
118-
role='listitem'
119-
title={notification.message}
120-
>
121-
{SeverityIcon && (
122-
<SeverityIcon className='app__notification-dialog__chip-icon' />
123-
)}
124-
<span className='app__notification-dialog__chip-text'>
125-
{notification.message}
126-
</span>
127-
<span className='app__notification-dialog__chip-meta'>
128-
<span className='app__notification-dialog__chip-meta-item'>
129-
<IconClock className='app__notification-dialog__chip-meta-icon' />
130-
{formatDurationLabel(notification.duration)}
131-
</span>
132-
<span className='app__notification-dialog__chip-meta-item'>
133-
<DirectionIcon className='app__notification-dialog__chip-meta-icon' />
134-
{notification.entryDirection}
109+
}) => {
110+
const [tooltipState, setTooltipState] = useState<{
111+
referenceElement: HTMLDivElement;
112+
text: string;
113+
} | null>(null);
114+
115+
return (
116+
<div className='app__notification-dialog__chips' role='list'>
117+
{notifications.map((notification) => {
118+
const SeverityIcon = severityIcons[notification.severity];
119+
const DirectionIcon = directionIcons[notification.entryDirection];
120+
121+
return (
122+
<div
123+
className='app__notification-dialog__chip'
124+
key={notification.id}
125+
onBlurCapture={(event) => {
126+
if (event.currentTarget.contains(event.relatedTarget as Node | null)) {
127+
return;
128+
}
129+
setTooltipState(null);
130+
}}
131+
onFocusCapture={(event) =>
132+
setTooltipState({
133+
referenceElement: event.currentTarget,
134+
text: notification.message,
135+
})
136+
}
137+
onMouseEnter={(event) =>
138+
setTooltipState({
139+
referenceElement: event.currentTarget,
140+
text: notification.message,
141+
})
142+
}
143+
onMouseLeave={() => setTooltipState(null)}
144+
role='listitem'
145+
>
146+
{SeverityIcon && (
147+
<SeverityIcon className='app__notification-dialog__chip-icon' />
148+
)}
149+
<span className='app__notification-dialog__chip-text'>
150+
{notification.message}
135151
</span>
136-
<span className='app__notification-dialog__chip-panel'>
137-
{notification.targetPanel}
152+
<span className='app__notification-dialog__chip-meta'>
153+
<span className='app__notification-dialog__chip-meta-item'>
154+
<IconClock className='app__notification-dialog__chip-meta-icon' />
155+
{formatDurationLabel(notification.duration)}
156+
</span>
157+
<span className='app__notification-dialog__chip-meta-item'>
158+
<DirectionIcon className='app__notification-dialog__chip-meta-icon' />
159+
{notification.entryDirection}
160+
</span>
161+
<span className='app__notification-dialog__chip-panel'>
162+
{notification.targetPanel}
163+
</span>
138164
</span>
139-
</span>
140-
<Button
141-
appearance='ghost'
142-
aria-label={`Remove ${notification.message}`}
143-
circular
144-
className='app__notification-dialog__chip-remove'
145-
onClick={() => removeQueuedNotification(notification.id)}
146-
size='xs'
147-
variant='secondary'
148-
>
149-
<IconXmark />
150-
</Button>
151-
</div>
152-
);
153-
})}
154-
</div>
155-
);
165+
<Button
166+
appearance='ghost'
167+
aria-label={`Remove ${notification.message}`}
168+
circular
169+
className='app__notification-dialog__chip-remove'
170+
onClick={() => removeQueuedNotification(notification.id)}
171+
size='xs'
172+
variant='secondary'
173+
>
174+
<IconXmark />
175+
</Button>
176+
</div>
177+
);
178+
})}
179+
<PopperTooltip
180+
referenceElement={tooltipState?.referenceElement ?? null}
181+
visible={!!tooltipState}
182+
>
183+
{tooltipState?.text ?? ''}
184+
</PopperTooltip>
185+
</div>
186+
);
187+
};
156188

157189
const NotificationDraftForm = ({
158190
draft,
Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,40 @@
1+
import { useState } from 'react';
12
import { useSidebar } from '../ChatLayout/SidebarContext.tsx';
2-
import { Button, useTranslationContext } from 'stream-chat-react';
3+
import { Button, PopperTooltip, useTranslationContext } from 'stream-chat-react';
34
import { IconSidebar } from '../icons.tsx';
45

56
export const SidebarToggle = () => {
67
const { closeSidebar, openSidebar, sidebarOpen } = useSidebar();
78
const { t } = useTranslationContext();
9+
const [buttonElement, setButtonElement] = useState<HTMLButtonElement | null>(null);
10+
const [tooltipVisible, setTooltipVisible] = useState(false);
11+
const tooltipText = sidebarOpen ? 'Close sidebar' : 'Open sidebar';
12+
813
return (
9-
<Button
10-
appearance='ghost'
11-
aria-label={sidebarOpen ? t('aria/Collapse sidebar') : t('aria/Expand sidebar')}
12-
circular
13-
className='str-chat__header-sidebar-toggle'
14-
onClick={sidebarOpen ? closeSidebar : openSidebar}
15-
size='md'
16-
variant='secondary'
17-
>
18-
<IconSidebar />
19-
</Button>
14+
<>
15+
<Button
16+
appearance='ghost'
17+
aria-label={sidebarOpen ? t('aria/Collapse sidebar') : t('aria/Expand sidebar')}
18+
circular
19+
className='str-chat__header-sidebar-toggle'
20+
onBlur={() => setTooltipVisible(false)}
21+
onClick={sidebarOpen ? closeSidebar : openSidebar}
22+
onFocus={() => setTooltipVisible(true)}
23+
onMouseEnter={() => setTooltipVisible(true)}
24+
onMouseLeave={() => setTooltipVisible(false)}
25+
ref={setButtonElement}
26+
size='md'
27+
variant='secondary'
28+
>
29+
<IconSidebar />
30+
</Button>
31+
<PopperTooltip
32+
offset={[0, 8]}
33+
referenceElement={buttonElement}
34+
visible={tooltipVisible}
35+
>
36+
{tooltipText}
37+
</PopperTooltip>
38+
</>
2039
);
2140
};

src/components/ChatView/styling/ChatView.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
display: flex;
3535
position: relative;
3636

37-
&:focus-within .str-chat__chat-view__selector-button-tooltip,
37+
&:focus-visible + .str-chat__chat-view__selector-button-tooltip,
3838
&:hover .str-chat__chat-view__selector-button-tooltip {
3939
opacity: 1;
4040
transform: translate3d(0, -50%, 0);

src/components/Tooltip/Tooltip.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@ import type { ComponentProps } from 'react';
22
import React, { useEffect, useState } from 'react';
33
import type { PopperLikePlacement } from '../Dialog';
44
import { usePopoverPosition } from '../Dialog/hooks/usePopoverPosition';
5+
import clsx from 'clsx';
56

6-
export const Tooltip = ({ children, ...rest }: ComponentProps<'div'>) => (
7-
<div className='str-chat__tooltip' {...rest}>
7+
export const Tooltip = ({ children, className, ...rest }: ComponentProps<'div'>) => (
8+
<div className={clsx('str-chat__tooltip', className)} {...rest}>
89
{children}
910
</div>
1011
);
1112

1213
export type PopperTooltipProps<T extends HTMLElement> = React.PropsWithChildren<{
1314
/** Reference element to which the tooltip should attach to */
1415
referenceElement: T | null;
16+
/** Custom class to be merged along the defaults */
17+
className?: string;
1518
/** Popper's modifier (offset) property - [xAxis offset, yAxis offset], default [0, 10] */
1619
offset?: [number, number];
1720
/** Popper's placement property defining default position of the tooltip, default 'top' */
@@ -22,6 +25,7 @@ export type PopperTooltipProps<T extends HTMLElement> = React.PropsWithChildren<
2225

2326
export const PopperTooltip = <T extends HTMLElement>({
2427
children,
28+
className,
2529
offset = [0, 10],
2630
placement = 'top',
2731
referenceElement,
@@ -51,7 +55,7 @@ export const PopperTooltip = <T extends HTMLElement>({
5155

5256
return (
5357
<div
54-
className='str-chat__tooltip'
58+
className={clsx('str-chat__tooltip', className)}
5559
data-placement={resolvedPlacement}
5660
ref={setPopperElement}
5761
style={{ left: x ?? 0, position: strategy, top: y ?? 0 }}

src/components/Tooltip/styling/Tooltip.scss

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
--str-chat__tooltip-border-radius: var(--radius-md);
66

77
/* The text/icon color of the component */
8-
--str-chat__tooltip-color: var(--str-chat__text-color);
8+
--str-chat__tooltip-color: var(--text-inverse);
99

1010
/* The background color of the component */
11-
--str-chat__tooltip-background-color: var(--background-core-elevation-2);
11+
--str-chat__tooltip-background-color: var(--background-core-inverse);
1212

1313
/* Top border of the component */
1414
--str-chat__tooltip-border-block-start: none;
@@ -23,14 +23,15 @@
2323
--str-chat__tooltip-border-inline-end: none;
2424

2525
/* Box shadow applied to the component */
26-
--str-chat__tooltip-box-shadow: 0 0 20px var(--str-chat__box-shadow-color);
26+
--str-chat__tooltip-box-shadow: var(--str-chat__box-shadow-3);
2727
}
2828

2929
.str-chat__tooltip {
3030
@include utils.component-layer-overrides('tooltip');
3131
@include utils.prevent-glitch-text-overflow;
3232
display: flex;
33-
padding: var(--space-8);
33+
gap: var(--spacing-xs);
34+
padding: var(--spacing-xs);
3435
z-index: 1;
3536
max-width: calc(var(--str-chat__spacing-px) * 150);
3637
width: max-content;

0 commit comments

Comments
 (0)