Skip to content

Commit fe90ac8

Browse files
feat(badge-indicator): ui-shell and tab implementation (#18583)
* feat(badge-indicators): new component * fix(badge): clean up stories and add imports * test(avt): badge indicator * fix(button): revert * fix(Styles): badge * chore(test): update * fix(badge-indicator): make internal and add prop * fix(badgeIndicator): add badgeCountLabel * refactor: combines label and badgeLabel props * fix: updates SR announcement & stories * fix(iconButton): default is really lg and label * fix(badgeIndicator): remove badgeCountLabel use aria-describedby * fix(button): cleanup * fix(icon-button): badgeId and label description * feat(badge-indicator): ui-shell/tab/treeview implementation * fix(badge-indicator): remove treeview and cleanup tab and ui-shell * chore(props): export shape/icon indicator props * fix(styles): remove unused styles * fix(api): update * feat: adds badge to WC - UI shell * chore: slot clean up * feat(storybook): add controls --------- Co-authored-by: Nikhil Tomar <nikhiltomar753@gmail.com> Co-authored-by: Nikhil Tomar <63502271+2nikhiltom@users.noreply.github.com>
1 parent 7822f63 commit fe90ac8

File tree

13 files changed

+150
-32
lines changed

13 files changed

+150
-32
lines changed

packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4403,6 +4403,9 @@ Map {
44034403
"IconTab" => Object {
44044404
"$$typeof": Symbol(react.forward_ref),
44054405
"propTypes": Object {
4406+
"badgeIndicator": Object {
4407+
"type": "bool",
4408+
},
44064409
"children": Object {
44074410
"type": "node",
44084411
},

packages/react/src/components/IconIndicator/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const iconTypes = {
5656

5757
export type IconIndicatorKind = (typeof IconIndicatorKinds)[number];
5858

59-
interface IconIndicatorProps {
59+
export interface IconIndicatorProps {
6060
/**
6161
* Specify an optional className to add.
6262
*/

packages/react/src/components/ShapeIndicator/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const shapeTypes = {
7373

7474
export type ShapeIndicatorKind = (typeof ShapeIndicatorKinds)[number];
7575

76-
interface ShapeIndicatorProps {
76+
export interface ShapeIndicatorProps {
7777
/**
7878
* Specify an optional className to add.
7979
*/

packages/react/src/components/Tabs/Tabs.stories.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ export const Manual = () => {
348348
);
349349
};
350350

351-
export const Icon20Only = () => {
351+
export const Icon20Only = (args) => {
352352
return (
353353
<Tabs>
354354
<TabList iconSize="lg">
@@ -358,7 +358,7 @@ export const Icon20Only = () => {
358358
<IconTab label="Activity">
359359
<Activity size={20} aria-label="Activity" />
360360
</IconTab>
361-
<IconTab label="Notification">
361+
<IconTab label="New Notifications" {...args}>
362362
<Notification size={20} aria-label="Notification" />
363363
</IconTab>
364364
<IconTab label="Chat">
@@ -375,7 +375,16 @@ export const Icon20Only = () => {
375375
);
376376
};
377377

378-
export const IconOnly = () => {
378+
Icon20Only.argTypes = {
379+
badgeIndicator: {
380+
description: '**Experimental**: Display an empty dot badge on the Tab.',
381+
control: {
382+
type: 'boolean',
383+
},
384+
},
385+
};
386+
387+
export const IconOnly = (args) => {
379388
return (
380389
<Tabs>
381390
<TabList iconSize="default">
@@ -385,7 +394,7 @@ export const IconOnly = () => {
385394
<IconTab label="Activity">
386395
<Activity aria-label="Activity" />
387396
</IconTab>
388-
<IconTab label="Notification">
397+
<IconTab label="New Notifications" {...args}>
389398
<Notification aria-label="Notification" />
390399
</IconTab>
391400
<IconTab label="Chat">
@@ -402,6 +411,15 @@ export const IconOnly = () => {
402411
);
403412
};
404413

414+
IconOnly.argTypes = {
415+
badgeIndicator: {
416+
description: '**Experimental**: Display an empty dot badge on the Tab.',
417+
control: {
418+
type: 'boolean',
419+
},
420+
},
421+
};
422+
405423
export const Contained = () => {
406424
return (
407425
<Tabs>

packages/react/src/components/Tabs/Tabs.tsx

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import React, {
1515
useState,
1616
useRef,
1717
useEffect,
18+
useMemo,
1819
forwardRef,
20+
createContext,
1921
type ReactNode,
2022
type MouseEvent,
2123
type KeyboardEvent,
@@ -43,6 +45,7 @@ import { Close } from '@carbon/icons-react';
4345
import { useEvent } from '../../internal/useEvent';
4446
import { useMatchMedia } from '../../internal/useMatchMedia';
4547
import { Text } from '../Text';
48+
import BadgeIndicator from '../BadgeIndicator';
4649

4750
const verticalTabHeight = 64;
4851

@@ -1211,6 +1214,7 @@ const Tab = forwardRef<HTMLElement, TabProps>(function Tab(
12111214
onTabCloseRequest,
12121215
} = React.useContext(TabsContext);
12131216
const { index, hasSecondaryLabel, contained } = React.useContext(TabContext);
1217+
const { badgeIndicator } = React.useContext(IconTabContext) || {};
12141218
const dismissIconRef = useRef<HTMLButtonElement>(null);
12151219
const tabRef = useRef<HTMLElement>(null);
12161220
const ref = useMergedRefs([forwardRef, tabRef]);
@@ -1441,6 +1445,7 @@ const Tab = forwardRef<HTMLElement, TabProps>(function Tab(
14411445
{secondaryLabel}
14421446
</Text>
14431447
)}
1448+
{!disabled && badgeIndicator && <BadgeIndicator />}
14441449
</BaseComponent>
14451450
{/* always rendering dismissIcon so we don't lose reference to it, otherwise events do not work when switching from/to dismissable state */}
14461451
{DismissIcon}
@@ -1504,7 +1509,15 @@ Tab.propTypes = {
15041509
* IconTab
15051510
*/
15061511

1512+
const IconTabContext = createContext<{ badgeIndicator?: boolean } | false>(
1513+
false
1514+
);
1515+
15071516
export interface IconTabProps extends DivAttributes {
1517+
/**
1518+
* **Experimental**: Display an empty dot badge on the Tab.
1519+
*/
1520+
badgeIndicator?: boolean;
15081521
/**
15091522
* Provide an icon to be rendered inside `IconTab` as the visual label for Tab.
15101523
*/
@@ -1529,7 +1542,8 @@ export interface IconTabProps extends DivAttributes {
15291542
* Provide the label to be rendered inside the Tooltip. The label will use
15301543
* `aria-labelledby` and will fully describe the child node that is provided.
15311544
* This means that if you have text in the child node it will not be
1532-
* announced to the screen reader.
1545+
* announced to the screen reader. If using the badgeIndicator then provide a
1546+
* label with describing that there is a new notification.
15331547
*/
15341548
label: ReactNode;
15351549

@@ -1541,6 +1555,7 @@ export interface IconTabProps extends DivAttributes {
15411555

15421556
const IconTab = React.forwardRef<HTMLDivElement, IconTabProps>(function IconTab(
15431557
{
1558+
badgeIndicator,
15441559
children,
15451560
className: customClassName,
15461561
defaultOpen = false,
@@ -1552,27 +1567,38 @@ const IconTab = React.forwardRef<HTMLDivElement, IconTabProps>(function IconTab(
15521567
ref
15531568
) {
15541569
const prefix = usePrefix();
1570+
const value = useMemo(() => ({ badgeIndicator }), [badgeIndicator]);
1571+
1572+
const hasSize20 =
1573+
React.isValidElement(children) && children.props?.size === 20;
15551574

15561575
const classNames = cx(
15571576
`${prefix}--tabs__nav-item--icon-only`,
1558-
customClassName
1577+
customClassName,
1578+
{ [`${prefix}--tabs__nav-item--icon-only__20`]: hasSize20 }
15591579
);
15601580
return (
1561-
<Tooltip
1562-
align="bottom"
1563-
defaultOpen={defaultOpen}
1564-
className={`${prefix}--icon-tooltip`}
1565-
enterDelayMs={enterDelayMs}
1566-
label={label}
1567-
leaveDelayMs={leaveDelayMs}>
1568-
<Tab className={classNames} ref={ref} {...rest}>
1569-
{children}
1570-
</Tab>
1571-
</Tooltip>
1581+
<IconTabContext.Provider value={value}>
1582+
<Tooltip
1583+
align="bottom"
1584+
defaultOpen={defaultOpen}
1585+
className={`${prefix}--icon-tooltip`}
1586+
enterDelayMs={enterDelayMs}
1587+
label={label}
1588+
leaveDelayMs={leaveDelayMs}>
1589+
<Tab className={classNames} ref={ref} {...rest}>
1590+
{children}
1591+
</Tab>
1592+
</Tooltip>
1593+
</IconTabContext.Provider>
15721594
);
15731595
});
15741596

15751597
IconTab.propTypes = {
1598+
/**
1599+
* **Experimental**: Display an empty dot badge on the Tab.
1600+
*/
1601+
badgeIndicator: PropTypes.bool,
15761602
/**
15771603
* Provide an icon to be rendered inside `IconTab` as the visual label for Tab.
15781604
*/
@@ -1597,7 +1623,8 @@ IconTab.propTypes = {
15971623
* Provide the label to be rendered inside the Tooltip. The label will use
15981624
* `aria-labelledby` and will fully describe the child node that is provided.
15991625
* This means that if you have text in the child node it will not be
1600-
* announced to the screen reader.
1626+
* announced to the screen reader. If using the badgeIndicator then provide a
1627+
* label with describing that there is a new notification.
16011628
*/
16021629
label: PropTypes.node.isRequired,
16031630

packages/react/src/components/Tabs/__tests__/Tabs-test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import {
77
TabPanels,
88
TabList,
99
TabListVertical,
10+
IconTab,
1011
} from '../Tabs';
1112
import { act } from 'react';
13+
import { Notification } from '@carbon/icons-react';
1214

1315
import { render, screen } from '@testing-library/react';
1416
import userEvent from '@testing-library/user-event';
@@ -234,6 +236,28 @@ describe('Tab', () => {
234236
'cds--tabs__nav-item--icon'
235237
);
236238
});
239+
it('should render badge indicator when badgeIndicator prop is true', () => {
240+
render(
241+
<Tabs>
242+
<TabList aria-label="List of tabs">
243+
<IconTab
244+
badgeIndicator
245+
data-testid="icon-tab-with-badge"
246+
label="New Notifications">
247+
<Notification size={20} aria-label="Notification" />
248+
</IconTab>
249+
</TabList>
250+
<TabPanels>
251+
<TabPanel>Icon Tab Panel</TabPanel>
252+
</TabPanels>
253+
</Tabs>
254+
);
255+
256+
// Get the icon tab
257+
const iconTab = screen.getByTestId('icon-tab-with-badge');
258+
const badgeIndicator = iconTab.querySelector(`.${prefix}--badge-indicator`);
259+
expect(badgeIndicator).not.toBeNull();
260+
});
237261

238262
it('should call onClick from props if provided', async () => {
239263
const onClick = jest.fn();

packages/react/src/components/UIShell/HeaderGlobalAction.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ const HeaderGlobalAction: React.FC<HeaderGlobalActionProps> = React.forwardRef(
105105
onClick={onClick}
106106
type="button"
107107
hasIconOnly
108+
size="lg"
109+
kind="ghost"
108110
iconDescription={ariaLabel}
109111
tooltipPosition="bottom"
110112
tooltipAlignment={tooltipAlignment}

packages/react/src/components/UIShell/UIShell.HeaderBase.stories.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ export const HeaderWSideNav = () => (
505505

506506
HeaderWSideNav.storyName = 'Header w/ SideNav';
507507

508-
export const HeaderWActionsAndRightPanel = () => (
508+
export const HeaderWActionsAndRightPanel = (args) => (
509509
<>
510510
<Header aria-label="IBM Platform Name">
511511
<HeaderName href="#" prefix="IBM">
@@ -519,6 +519,7 @@ export const HeaderWActionsAndRightPanel = () => (
519519
</HeaderGlobalAction>
520520
<HeaderGlobalAction
521521
aria-label="Notifications"
522+
badgeCount={args.badgeCount}
522523
isActive
523524
onClick={action('notification click')}>
524525
<Notification size={20} />
@@ -538,6 +539,20 @@ export const HeaderWActionsAndRightPanel = () => (
538539

539540
HeaderWActionsAndRightPanel.storyName = 'Header w/ Actions and Right Panel';
540541

542+
HeaderWActionsAndRightPanel.argTypes = {
543+
badgeCount: {
544+
description:
545+
' **Experimental**: Display a badge on the button. An empty/dot badge if 0, a numbered badge if > 0. Must be used with size="lg" and kind="ghost"',
546+
control: {
547+
type: 'number',
548+
},
549+
},
550+
};
551+
552+
HeaderWActionsAndRightPanel.args = {
553+
badgeCount: 4,
554+
};
555+
541556
export const HeaderWActionsAndSwitcher = (args) => (
542557
<HeaderContainer
543558
{...args}

packages/react/src/index.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ export {
158158
export * from './components/Popover';
159159
export * from './components/ProgressBar';
160160
export { AILabel, AILabelContent, AILabelActions } from './components/AILabel';
161+
export { IconIndicator as unstable__IconIndicator } from './components/IconIndicator';
162+
export { ShapeIndicator as unstable__ShapeIndicator } from './components/ShapeIndicator';
161163
// Keep until V12
162164
export {
163165
AILabel as unstable__Slug,
@@ -370,6 +372,9 @@ export type { SectionProps } from './components/Heading/index';
370372
export type { IconSkeletonProps } from './components/Icon/Icon.Skeleton';
371373
export type { IconButtonProps } from './components/IconButton/index';
372374

375+
// icon indicator
376+
export type { IconIndicatorProps } from './components/IconIndicator/index';
377+
373378
//idprefix
374379
export type { IdPrefixProps } from './components/IdPrefix/index';
375380

@@ -480,6 +485,9 @@ export type { SelectSkeletonProps } from './components/Select/Select.Skeleton';
480485
export type { SelectItemProps } from './components/SelectItem/SelectItem';
481486
export type { SelectItemGroupProps } from './components/SelectItemGroup/SelectItemGroup';
482487

488+
// shape indicator
489+
export type { ShapeIndicatorProps } from './components/ShapeIndicator/index';
490+
483491
//skeleton items
484492
export type { SkeletonIconProps } from './components/SkeletonIcon/SkeletonIcon';
485493
export type { SkeletonPlaceholderProps } from './components/SkeletonPlaceholder/SkeletonPlaceholder';
@@ -606,7 +614,3 @@ export type { SwitcherItemProps } from './components/UIShell/SwitcherItem';
606614

607615
//unordered list
608616
export type { UnorderedListProps } from './components/UnorderedList/UnorderedList';
609-
610-
// status indicators
611-
export { IconIndicator as unstable__IconIndicator } from './components/IconIndicator';
612-
export { ShapeIndicator as unstable__ShapeIndicator } from './components/ShapeIndicator';

packages/styles/scss/components/tabs/_tabs.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@
315315
.#{$prefix}--tabs__nav-item {
316316
@include reset;
317317

318+
position: relative;
318319
display: flex;
319320
flex: 1 0 auto;
320321
padding: 0;
@@ -547,6 +548,14 @@
547548
}
548549
}
549550

551+
.#{$prefix}--tabs__nav-item--icon-only:not(
552+
.#{$prefix}--tabs__nav-item--icon-only__20
553+
)
554+
.#{$prefix}--badge-indicator {
555+
margin-block-start: $spacing-02;
556+
margin-inline-end: $spacing-02;
557+
}
558+
550559
//-----------------------------
551560
// Item Hover
552561
//-----------------------------

0 commit comments

Comments
 (0)