Skip to content

Commit e035b38

Browse files
feat(DropdownIconButton): add support for DropdownIconButton and tooltip (#2514)
* feat: add support for DropdownIconButton and tooltip * feat: add docs for DropdownIconButton * Create small-pans-sip.md * fix: snapshots * feat: remove tooltip prop * docs: add story
1 parent c0ad493 commit e035b38

21 files changed

+414
-60
lines changed

.changeset/small-pans-sip.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@razorpay/blade": minor
3+
---
4+
5+
feat(DropdownIconButton): add support for DropdownIconButton and tooltip for Dropdown triggers

packages/blade/src/components/Button/IconButton/StyledIconButton.native.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const StyledIconButton = React.forwardRef<View, StyledIconButtonProps>(
3939
size,
4040
emphasis,
4141
accessibilityLabel,
42+
accessibilityProps,
4243
},
4344
ref,
4445
) => {
@@ -72,7 +73,11 @@ const StyledIconButton = React.forwardRef<View, StyledIconButtonProps>(
7273
onPointerEnter={onPointerEnter}
7374
onTouchEnd={onTouchEnd}
7475
onTouchStart={onTouchStart}
75-
{...makeAccessible({ label: accessibilityLabel, role: 'button' })}
76+
{...makeAccessible({
77+
...accessibilityProps,
78+
label: accessibilityLabel ?? accessibilityProps?.label,
79+
role: accessibilityProps?.role ?? 'button',
80+
})}
7681
>
7782
<Icon size={size} color={iconColorToken} />
7883
</StyledPressable>

packages/blade/src/components/Button/IconButton/StyledIconButton.web.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ const StyledIconButton = React.forwardRef<HTMLButtonElement, StyledIconButtonPro
8787
size,
8888
emphasis,
8989
accessibilityLabel,
90+
accessibilityProps,
9091
isDisabled,
9192
isHighlighted,
9293
testID,
@@ -98,6 +99,7 @@ const StyledIconButton = React.forwardRef<HTMLButtonElement, StyledIconButtonPro
9899
onPointerEnter,
99100
onTouchEnd,
100101
onTouchStart,
102+
onKeyDown,
101103
tabIndex,
102104
...rest
103105
},
@@ -116,11 +118,15 @@ const StyledIconButton = React.forwardRef<HTMLButtonElement, StyledIconButtonPro
116118
onPointerEnter={onPointerEnter}
117119
onTouchEnd={onTouchEnd}
118120
onTouchStart={onTouchStart}
121+
onKeyDown={onKeyDown}
119122
disabled={isDisabled}
120123
$isHighlighted={isHighlighted}
121124
$size={size}
122125
tabIndex={tabIndex}
123-
{...makeAccessible({ label: accessibilityLabel })}
126+
{...makeAccessible({
127+
...accessibilityProps,
128+
label: accessibilityLabel ?? accessibilityProps?.label,
129+
})}
124130
{...metaAttribute({ name: MetaConstants.IconButton, testID })}
125131
{...makeAnalyticsAttribute(rest)}
126132
{...rest}

packages/blade/src/components/Button/IconButton/types.ts

+8
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,24 @@ import type { DataAnalyticsAttribute, RemoveUndefinedFromUnion, TestID } from '~
44
import type { BladeCommonEvents } from '~components/types';
55
import type { SubtleOrIntense } from '~tokens/theme/theme';
66
import type { StyledPropsBlade } from '~components/Box/styledProps';
7+
import type { AccessibilityProps } from '~utils/makeAccessible';
8+
import type { Platform } from '~utils';
79

810
export type StyledIconButtonProps = {
911
icon: IconComponent;
1012
size: RemoveUndefinedFromUnion<IconButtonProps['size']>;
1113
emphasis: SubtleOrIntense;
1214
accessibilityLabel: string;
15+
accessibilityProps?: Partial<AccessibilityProps>;
1316
isDisabled?: IconButtonProps['isDisabled'];
1417
isHighlighted?: IconButtonProps['isHighlighted'];
1518
tabIndex?: IconButtonProps['_tabIndex'];
1619
onClick?: IconButtonProps['onClick'];
20+
onKeyDown?: Platform.Select<{
21+
web: React.KeyboardEventHandler;
22+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
23+
native: undefined | ((event: any) => void);
24+
}>;
1725
} & TestID &
1826
BladeCommonEvents &
1927
DataAnalyticsAttribute &

packages/blade/src/components/Dropdown/Dropdown.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const validDropdownChildren = [
2323
dropdownComponentIds.triggers.SelectInput,
2424
dropdownComponentIds.triggers.SearchInput,
2525
dropdownComponentIds.triggers.DropdownButton,
26+
dropdownComponentIds.triggers.DropdownIconButton,
2627
dropdownComponentIds.triggers.DropdownLink,
2728
dropdownComponentIds.DropdownOverlay,
2829
dropdownComponentIds.triggers.AutoComplete,
@@ -153,6 +154,10 @@ const _Dropdown = (
153154
dropdownTriggerer.current = 'DropdownButton';
154155
}
155156

157+
if (isValidAllowedChildren(child, dropdownComponentIds.triggers.DropdownIconButton)) {
158+
dropdownTriggerer.current = 'DropdownIconButton';
159+
}
160+
156161
if (isValidAllowedChildren(child, dropdownComponentIds.triggers.AutoComplete)) {
157162
dropdownTriggerer.current = 'AutoComplete';
158163
}

packages/blade/src/components/Dropdown/DropdownButton.tsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
3+
14
import React from 'react';
25
import { useDropdown } from './useDropdown';
36
import { dropdownComponentIds } from './dropdownComponentIds';
@@ -53,7 +56,6 @@ const _DropdownButton = ({
5356
type={type}
5457
variant={variant}
5558
testID={testID}
56-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5759
ref={triggererRef as any}
5860
accessibilityProps={{
5961
label: accessibilityLabel,
@@ -65,20 +67,16 @@ const _DropdownButton = ({
6567
onClick={(e) => {
6668
onTriggerClick();
6769
// Setting it for web fails it on native typecheck and vice versa
68-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-explicit-any
6970
onClick?.(e as any);
7071
}}
7172
onBlur={(e) => {
7273
// With button trigger, there is no "value" as such. It's just clickable items
7374
// Setting it for web fails it on native typecheck and vice versa
74-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-explicit-any
7575
onBlur?.(e as any);
7676
}}
7777
onKeyDown={(e) => {
78-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-explicit-any
7978
onTriggerKeydown?.({ event: e as any });
8079
// Setting it for web fails it on native typecheck and vice versa
81-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-explicit-any
8280
onKeyDown?.(e as any);
8381
}}
8482
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
3+
4+
import React from 'react';
5+
import { useDropdown } from './useDropdown';
6+
import { dropdownComponentIds } from './dropdownComponentIds';
7+
import { getActionListContainerRole } from '~components/ActionList/getA11yRoles';
8+
import type { BaseButtonProps } from '~components/Button/BaseButton/BaseButton';
9+
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
10+
import type { IconButtonProps } from '~components/Button/IconButton';
11+
import StyledIconButton from '~components/Button/IconButton/StyledIconButton';
12+
13+
type DropdownIconButtonProps = Omit<IconButtonProps, 'onClick'> & {
14+
onBlur?: BaseButtonProps['onBlur'];
15+
onKeyDown?: BaseButtonProps['onKeyDown'];
16+
onClick?: IconButtonProps['onClick'];
17+
};
18+
19+
const _DropdownIconButton = ({
20+
icon,
21+
isDisabled = false,
22+
onClick,
23+
onBlur,
24+
onKeyDown,
25+
size = 'medium',
26+
accessibilityLabel,
27+
emphasis = 'intense',
28+
...rest
29+
}: DropdownIconButtonProps): React.ReactElement => {
30+
const {
31+
onTriggerClick,
32+
onTriggerKeydown,
33+
dropdownBaseId,
34+
isOpen,
35+
activeIndex,
36+
hasFooterAction,
37+
triggererRef,
38+
} = useDropdown();
39+
40+
return (
41+
// Using StyledIconButton here to avoid exporting onKeydown, and accessibiltiyProps object
42+
<StyledIconButton
43+
{...rest}
44+
icon={icon}
45+
isDisabled={isDisabled}
46+
size={size}
47+
emphasis={emphasis}
48+
ref={triggererRef as any}
49+
accessibilityLabel={accessibilityLabel}
50+
accessibilityProps={{
51+
label: accessibilityLabel,
52+
hasPopup: getActionListContainerRole(hasFooterAction, 'DropdownIconButton'),
53+
expanded: isOpen,
54+
controls: `${dropdownBaseId}-actionlist`,
55+
activeDescendant: activeIndex >= 0 ? `${dropdownBaseId}-${activeIndex}` : undefined,
56+
}}
57+
onClick={(e) => {
58+
onTriggerClick();
59+
// Setting it for web fails it on native typecheck and vice versa
60+
onClick?.(e as any);
61+
}}
62+
onBlur={(e) => {
63+
// With button trigger, there is no "value" as such. It's just clickable items
64+
// Setting it for web fails it on native typecheck and vice versa
65+
onBlur?.(e as any);
66+
}}
67+
onKeyDown={(e) => {
68+
onTriggerKeydown?.({ event: e as any });
69+
// Setting it for web fails it on native typecheck and vice versa
70+
onKeyDown?.(e as any);
71+
}}
72+
/>
73+
);
74+
};
75+
76+
const DropdownIconButton = assignWithoutSideEffects(_DropdownIconButton, {
77+
componentId: dropdownComponentIds.triggers.DropdownIconButton,
78+
});
79+
80+
export { DropdownIconButton };

packages/blade/src/components/Dropdown/DropdownLink.tsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
3+
14
import React from 'react';
25
import { getActionListContainerRole } from '../ActionList/getA11yRoles';
36
import { BaseLink } from '../Link/BaseLink';
@@ -55,7 +58,6 @@ const _DropdownLink = ({
5558
isDisabled={isDisabled}
5659
{...props}
5760
{...makeAnalyticsAttribute(props)}
58-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5961
ref={triggererRef as any}
6062
accessibilityProps={{
6163
label: accessibilityLabel,
@@ -67,19 +69,15 @@ const _DropdownLink = ({
6769
onClick={(e) => {
6870
onTriggerClick();
6971
// Setting it for web fails it on native typecheck and vice versa
70-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-explicit-any
7172
onClick?.(e as any);
7273
}}
7374
onBlur={(e) => {
7475
// Setting it for web fails it on native typecheck and vice versa
75-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-explicit-any
7676
onBlur?.(e as any);
7777
}}
7878
onKeyDown={(e) => {
79-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-explicit-any
8079
onTriggerKeydown?.({ event: e as any });
8180
// Setting it for web fails it on native typecheck and vice versa
82-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-explicit-any
8381
onKeyDown?.(e as any);
8482
}}
8583
/>

packages/blade/src/components/Dropdown/docs/DropdownWithButton.stories.tsx

+70
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@ import React from 'react';
22
import { DropdownButton } from '../DropdownButton';
33
import { Dropdown, DropdownLink, DropdownOverlay } from '..';
44
import { DropdownFooter, DropdownHeader } from '../DropdownHeaderFooter';
5+
import { DropdownIconButton } from '../DropdownIconButton';
56
import {
67
WithControlledMenuStory,
78
WithControlledMultiSelectMenuStory,
89
WithLinkStory,
910
WithAutoPositioningMenuStory,
1011
WithSimpleMenuStory,
12+
WithIconButtonStory,
13+
WithTooltipStory,
1114
} from './stories';
1215
import { Sandbox } from '~utils/storybook/Sandbox';
1316
import { Box } from '~components/Box';
1417
import { ActionList, ActionListItem, ActionListItemIcon } from '~components/ActionList';
1518
import {
19+
BoxIcon,
1620
CheckIcon,
1721
ChevronDownIcon,
1822
ChevronUpIcon,
@@ -25,6 +29,7 @@ import { Checkbox } from '~components/Checkbox';
2529
import { Button } from '~components/Button';
2630
import { Badge } from '~components/Badge';
2731
import { Amount } from '~components/Amount';
32+
import { Tooltip, TooltipInteractiveWrapper } from '~components/Tooltip';
2833

2934
const DropdownStoryMeta = {
3035
title: 'Components/Dropdown/With Button and Link',
@@ -65,6 +70,14 @@ export const WithLink = (): React.ReactElement => {
6570
);
6671
};
6772

73+
export const WithIconButton = (): React.ReactElement => {
74+
return (
75+
<Sandbox padding="spacing.0" editorHeight="100vh">
76+
{WithIconButtonStory}
77+
</Sandbox>
78+
);
79+
};
80+
6881
export const WithAutoPositioning = (): React.ReactElement => {
6982
return (
7083
<Sandbox padding="spacing.0" editorHeight="100vh">
@@ -89,6 +102,14 @@ export const WithControlledMultiSelect = (): React.ReactElement => {
89102
);
90103
};
91104

105+
export const WithTooltip = (): React.ReactElement => {
106+
return (
107+
<Sandbox padding="spacing.0" editorHeight="100vh">
108+
{WithTooltipStory}
109+
</Sandbox>
110+
);
111+
};
112+
92113
// This is for Chromatic and react native testing
93114
export const InternalMenu = (): React.ReactElement => {
94115
const [status, setStatus] = React.useState<string | undefined>();
@@ -271,4 +292,53 @@ InternalLinkDropdown.parameters = {
271292
},
272293
};
273294

295+
export const InternalIconButtonDropdown = (): React.ReactElement => {
296+
const [status, setStatus] = React.useState<string | undefined>('latest-added');
297+
const [isDropdownOpen, setIsDropdownOpen] = React.useState(false);
298+
299+
return (
300+
<Box padding="spacing.10">
301+
<Tooltip content="Check Status">
302+
<TooltipInteractiveWrapper>
303+
<Dropdown onOpenChange={setIsDropdownOpen} isOpen={isDropdownOpen}>
304+
<DropdownIconButton icon={BoxIcon} accessibilityLabel="Status Dropdown" />
305+
<DropdownOverlay>
306+
<ActionList>
307+
<ActionListItem
308+
onClick={({ name, value }) => {
309+
console.log({ name, value });
310+
setStatus(name);
311+
}}
312+
isSelected={status === 'latest-added'}
313+
title="Latest Added"
314+
value="latest-added"
315+
/>
316+
<ActionListItem
317+
onClick={({ name, value }) => {
318+
console.log({ name, value });
319+
setStatus(name);
320+
}}
321+
isSelected={status === 'latest-invoice'}
322+
title="Latest Invoice"
323+
value="latest-invoice"
324+
/>
325+
326+
<ActionListItem
327+
onClick={({ name, value }) => {
328+
console.log({ name, value });
329+
setStatus(name);
330+
}}
331+
isSelected={status === 'oldest-due-date'}
332+
title="Oldest Due Date"
333+
value="oldest-due-date"
334+
/>
335+
</ActionList>
336+
</DropdownOverlay>
337+
</Dropdown>
338+
</TooltipInteractiveWrapper>
339+
</Tooltip>
340+
</Box>
341+
);
342+
};
343+
274344
export default DropdownStoryMeta;

0 commit comments

Comments
 (0)