Skip to content

Commit b623b06

Browse files
authored
Merge pull request #446 from mashmatrix/add/optional-tooltip-prop-to-tabs
Add optional tooltip prop to Tabs component
2 parents bc8435f + 6e92913 commit b623b06

File tree

4 files changed

+169
-26
lines changed

4 files changed

+169
-26
lines changed

src/scripts/AutoAlign.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ export type AutoAlignProps = {
168168
preventPortalize?: boolean;
169169
align?: Align;
170170
alignment?: RectangleAlignment;
171+
offsetX?: number;
172+
offsetY?: number;
171173
children: (props: AutoAlignInjectedProps) => ReactElement;
172174
};
173175

@@ -398,6 +400,8 @@ export const AutoAlign: FC<AutoAlignProps> = (props) => {
398400
preventPortalize,
399401
portalClassName: additionalPortalClassName,
400402
portalStyle: additionalPortalStyle = {},
403+
offsetX = 0,
404+
offsetY = 0,
401405
children,
402406
} = props;
403407
const {
@@ -419,6 +423,8 @@ export const AutoAlign: FC<AutoAlignProps> = (props) => {
419423
right: 0,
420424
},
421425
} = compSettings;
426+
const adjustedOffsetLeft = offsetLeft + offsetX;
427+
const adjustedOffsetTop = offsetTop + offsetY;
422428
if (typeof children !== 'function') {
423429
return React.isValidElement(children) ? children : <>{children}</>;
424430
}
@@ -429,9 +435,9 @@ export const AutoAlign: FC<AutoAlignProps> = (props) => {
429435
<div ref={elRef}>
430436
<RelativePortal
431437
fullWidth
432-
left={offsetLeft}
433-
right={-offsetLeft}
434-
top={offsetTop}
438+
top={adjustedOffsetTop}
439+
left={adjustedOffsetLeft}
440+
right={-adjustedOffsetLeft}
435441
onScroll={onScroll}
436442
component='div'
437443
className={classnames(portalClassName, additionalPortalClassName)}

src/scripts/Popover.tsx

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,28 @@ import React, {
22
HTMLAttributes,
33
CSSProperties,
44
FC,
5-
useRef,
65
ReactNode,
6+
forwardRef,
7+
useEffect,
78
} from 'react';
89
import classnames from 'classnames';
910
import {
1011
AutoAlign,
1112
AutoAlignInjectedProps,
1213
RectangleAlignment,
1314
} from './AutoAlign';
15+
import { registerStyle } from './util';
16+
17+
/**
18+
*
19+
*/
20+
function useInitComponentStyle() {
21+
useEffect(() => {
22+
registerStyle('popover', [
23+
['.react-slds-popover.slds-popover_tooltip a', '{ color: white; }'],
24+
]);
25+
}, []);
26+
}
1427

1528
/**
1629
*
@@ -55,14 +68,17 @@ export type PopoverProps = {
5568
theme?: PopoverTheme;
5669
tooltip?: boolean;
5770
bodyStyle?: CSSProperties;
71+
offsetX?: number;
72+
offsetY?: number;
5873
} & HTMLAttributes<HTMLDivElement>;
5974

6075
/**
6176
*
6277
*/
63-
export const PopoverInner: FC<PopoverProps & AutoAlignInjectedProps> = (
64-
props
65-
) => {
78+
export const PopoverInner = forwardRef<
79+
HTMLDivElement,
80+
PopoverProps & AutoAlignInjectedProps
81+
>((props, ref) => {
6682
const {
6783
children,
6884
alignment,
@@ -73,10 +89,10 @@ export const PopoverInner: FC<PopoverProps & AutoAlignInjectedProps> = (
7389
bodyStyle,
7490
...rprops
7591
} = props;
76-
const elRef = useRef<HTMLDivElement | null>(null);
7792
const nubbinPosition = alignment.join('-');
7893
const [firstAlign, secondAlign] = alignment;
7994
const popoverClassNames = classnames(
95+
'react-slds-popover',
8096
'slds-popover',
8197
{
8298
'slds-hide': hidden,
@@ -103,7 +119,7 @@ export const PopoverInner: FC<PopoverProps & AutoAlignInjectedProps> = (
103119
};
104120
return (
105121
<div
106-
ref={elRef}
122+
ref={ref}
107123
className={popoverClassNames}
108124
role={tooltip ? 'tooltip' : 'dialog'}
109125
style={rootStyle}
@@ -112,22 +128,30 @@ export const PopoverInner: FC<PopoverProps & AutoAlignInjectedProps> = (
112128
<PopoverBody style={bodyStyle}>{children}</PopoverBody>
113129
</div>
114130
);
115-
};
131+
});
116132

117133
/**
118134
*
119135
*/
120-
export const Popover: FC<PopoverProps> = ({ position, ...props }) => {
121-
const alignment: RectangleAlignment | undefined = position?.split('-') as
122-
| RectangleAlignment
123-
| undefined;
124-
return (
125-
<AutoAlign
126-
triggerSelector='.slds-dropdown-trigger'
127-
alignmentStyle='popover'
128-
alignment={alignment}
129-
>
130-
{(injectedProps) => <PopoverInner {...props} {...injectedProps} />}
131-
</AutoAlign>
132-
);
133-
};
136+
export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
137+
({ position, offsetX = 0, offsetY = 0, ...props }, ref) => {
138+
useInitComponentStyle();
139+
140+
const alignment: RectangleAlignment | undefined = position?.split('-') as
141+
| RectangleAlignment
142+
| undefined;
143+
return (
144+
<AutoAlign
145+
triggerSelector='.slds-dropdown-trigger'
146+
alignmentStyle='popover'
147+
alignment={alignment}
148+
offsetX={offsetX}
149+
offsetY={offsetY}
150+
>
151+
{(injectedProps) => (
152+
<PopoverInner {...props} {...injectedProps} ref={ref} />
153+
)}
154+
</AutoAlign>
155+
);
156+
}
157+
);

src/scripts/Tabs.tsx

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, {
22
FC,
3+
FocusEvent,
34
ComponentType,
45
HTMLAttributes,
56
ReactElement,
@@ -11,12 +12,15 @@ import React, {
1112
useRef,
1213
useState,
1314
useEffect,
15+
useCallback,
1416
} from 'react';
1517
import classnames from 'classnames';
1618
import { registerStyle } from './util';
1719
import { DropdownButton, DropdownButtonProps } from './DropdownButton';
1820
import { useControlledValue, useEventCallback } from './hooks';
1921
import { Bivariant } from './typeUtils';
22+
import { Button } from './Button';
23+
import { Popover } from './Popover';
2024

2125
/**
2226
*
@@ -95,6 +99,38 @@ const TabMenu: FC<TabMenuProps> = (props) => {
9599
);
96100
};
97101

102+
/**
103+
*
104+
*/
105+
const TooltipContent = (props: { children: ReactNode; icon?: string }) => {
106+
const { children, icon = 'info' } = props;
107+
const [isHideTooltip, setIsHideTooltip] = useState(true);
108+
const popoverRef = useRef<HTMLDivElement>(null);
109+
const tooltipToggle = useCallback(() => {
110+
setIsHideTooltip((hidden) => !hidden);
111+
}, []);
112+
const onBlur = useCallback((e: FocusEvent<HTMLElement>) => {
113+
if (!popoverRef.current?.contains(e.relatedTarget)) {
114+
setIsHideTooltip(true);
115+
}
116+
}, []);
117+
return (
118+
<span className='slds-dropdown-trigger react-slds-tooltip-content'>
119+
<Button type='icon' icon={icon} onClick={tooltipToggle} onBlur={onBlur} />
120+
<Popover
121+
ref={popoverRef}
122+
hidden={isHideTooltip}
123+
tabIndex={-1}
124+
onBlur={onBlur}
125+
offsetX={-15}
126+
tooltip
127+
>
128+
{children}
129+
</Popover>
130+
</span>
131+
);
132+
};
133+
98134
/**
99135
*
100136
*/
@@ -112,6 +148,8 @@ export type TabItemRendererProps = {
112148
onTabKeyDown?: Bivariant<
113149
(eventKey: TabKey, e: React.KeyboardEvent<HTMLAnchorElement>) => void
114150
>;
151+
tooltip?: ReactNode;
152+
tooltipIcon?: string;
115153
};
116154

117155
const DefaultTabItemRenderer: FC<{ children?: ReactNode }> = (props) => {
@@ -136,7 +174,7 @@ export type TabItemProps<RendererProps extends TabItemRendererProps> = {
136174
const TabItem = <RendererProps extends TabItemRendererProps>(
137175
props: TabItemProps<RendererProps>
138176
) => {
139-
const { title, eventKey, menu, menuIcon } = props;
177+
const { title, eventKey, menu, menuIcon, tooltip, tooltipIcon } = props;
140178
const { type, activeTabRef } = useContext(TabsContext);
141179
const activeKey = useContext(TabsActiveKeyContext);
142180
const { onTabClick, onTabKeyDown } = useContext(TabsHandlersContext);
@@ -172,7 +210,11 @@ const TabItem = <RendererProps extends TabItemRendererProps>(
172210
return (
173211
<li className={tabItemClassName} role='presentation'>
174212
<TabItemRenderer {...itemRendererProps}>
175-
<span className='react-slds-tab-item-content'>
213+
<span
214+
className={`react-slds-tab-item-content ${
215+
tooltip ? 'react-slds-tooltip-enabled' : ''
216+
}`}
217+
>
176218
<a
177219
className={tabLinkClassName}
178220
role='tab'
@@ -188,6 +230,9 @@ const TabItem = <RendererProps extends TabItemRendererProps>(
188230
>
189231
{title}
190232
</a>
233+
{tooltip ? (
234+
<TooltipContent icon={tooltipIcon}>{tooltip}</TooltipContent>
235+
) : null}
191236
{menuItems ? (
192237
<TabMenu icon={menuIcon} {...menuProps}>
193238
{menuItems}
@@ -274,7 +319,15 @@ function useInitComponentStyle() {
274319
'.slds-tabs__item.react-slds-tab-with-menu > .react-slds-tab-item-content > a',
275320
'{ padding-right: 2rem; }',
276321
],
322+
[
323+
'.slds-tabs__item.react-slds-tab-with-menu > .react-slds-tab-item-content.react-slds-tooltip-enabled > a',
324+
'{ padding-right: 3.5rem; }',
325+
],
277326
['.react-slds-tab-menu', '{ position: absolute; top: 0; right: 0; }'],
327+
[
328+
'.react-slds-tooltip-content',
329+
'{ position: absolute; top: 0.6rem; right: 2.25rem; }',
330+
],
278331
[
279332
'.react-slds-tab-menu button',
280333
'{ height: 2.5rem; line-height: 2rem; width: 2rem; visibility: hidden; justify-content: center }',

stories/Tabs.stories.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,66 @@ export const WithDropdownScoped: ComponentStoryObj<typeof Tabs> = {
190190
},
191191
};
192192

193+
/**
194+
*
195+
*/
196+
export const WithTooltipScoped: ComponentStoryObj<typeof Tabs> = {
197+
render: (args) => (
198+
<Tabs {...args}>
199+
<Tab
200+
eventKey='1'
201+
title='Tab 1'
202+
menuItems={createMenu()}
203+
tooltip={
204+
<div>
205+
This is a tooltip for tab #1
206+
<br />
207+
<a
208+
href='https://www.example.com/helllo?name=world'
209+
target='_blank'
210+
rel='noreferrer'
211+
>
212+
https://www.example.com/helllo?name=world
213+
</a>
214+
</div>
215+
}
216+
>
217+
This is in tab #1
218+
</Tab>
219+
<Tab
220+
eventKey='2'
221+
title='Tab 2'
222+
menuItems={createMenu()}
223+
tooltip={<div>Warning!</div>}
224+
tooltipIcon='warning'
225+
>
226+
This is in tab #2
227+
</Tab>
228+
<Tab
229+
eventKey='3'
230+
title='Tab 3'
231+
menuItems={createMenu()}
232+
tooltip={<div>Error!</div>}
233+
tooltipIcon='error'
234+
>
235+
This is in tab #3
236+
</Tab>
237+
</Tabs>
238+
),
239+
name: 'With Tooltip (Scoped)',
240+
args: {
241+
type: 'scoped',
242+
defaultActiveKey: '1',
243+
},
244+
parameters: {
245+
docs: {
246+
description: {
247+
story: 'Scoped tabs with dropdown menu',
248+
},
249+
},
250+
},
251+
};
252+
193253
/**
194254
*
195255
*/

0 commit comments

Comments
 (0)