Skip to content

Commit 153ed05

Browse files
UberOpenSourceBotlinfeng-hua
authored andcommitted
sync(badge): update from web-code
[guest rides] Bug fix: Fix agent Intent
1 parent 77c2d13 commit 153ed05

11 files changed

Lines changed: 457 additions & 164 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as React from "react";
2+
import { HintDot, COLOR } from "baseui/badge";
3+
import { Skeleton } from "baseui/skeleton";
4+
5+
export default function Example() {
6+
return (
7+
<HintDot color={COLOR.accent}>
8+
<Skeleton width="48px" height="48px" />
9+
</HintDot>
10+
);
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as React from "react";
2+
import { NotificationCircle, COLOR } from "baseui/badge";
3+
import { Skeleton } from "baseui/skeleton";
4+
5+
export default function Example() {
6+
return (
7+
<NotificationCircle content={5} color={COLOR.accent}>
8+
<Skeleton width="48px" height="48px" />
9+
</NotificationCircle>
10+
);
11+
}

documentation-site/pages/components/badge.mdx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import badgeYardConfig from "../../components/yard/config/badge";
1111
import PrimaryInline from "examples/badge/primary-inline.tsx";
1212
import SecondaryInline from "examples/badge/secondary-inline.tsx";
1313
import Offset from "examples/badge/offset.tsx";
14+
import NotificationCircleExample from "examples/badge/notification-circle.tsx";
15+
import HintDotExample from "examples/badge/hint-dot.tsx";
1416

1517
export default Layout;
1618

@@ -24,7 +26,7 @@ Badge content should generally be 3 words or less.
2426

2527
## Hierarchy
2628

27-
Primary badges are bright in color to grab a users attention to an entry point of either a new product/feature, promotion or alert. Another usage is for transit lines. Avoid using multiple primary badges in the same view.
29+
Primary badges are bright in color to grab a user's attention to an entry point of either a new product/feature, promotion or alert. Another usage is for transit lines. Avoid using multiple primary badges in the same view.
2830

2931
Secondary badges should be part of the content inside the component. They are often meta data, highlighted information or simplified information.
3032

@@ -53,4 +55,24 @@ There may be situations where it makes sense to deviate from the standard badge
5355
- `horizontalOffset` sets the `right` CSS attribute when `placement` is `topRight` or `bottomRight`. Otherwise it sets the `left` attribute.
5456
- `verticalOffset` sets the `top` CSS attribute when `placement` is `topLeft`, `top`, or `topRight`. Otherwise it sets the `bottom` attribute.
5557

58+
## NotificationCircle
59+
60+
Use `NotificationCircle` to display a count or icon indicator anchored to an element — for example, an unread message count on a navigation icon.
61+
62+
<Example title="Notification circle" path="badge/notification-circle.tsx">
63+
<NotificationCircleExample />
64+
</Example>
65+
66+
The `content` prop accepts a number, an icon element, or a render prop `(size: number) => ReactNode`. Numbers greater than 99 are automatically clamped to `99+`. Use the `size` prop (`small` or `medium`, defaults to `medium`) to control the circle dimensions. `NotificationCircle` supports `topLeft`, `topRight`, `bottomLeft`, and `bottomRight` placements.
67+
68+
## HintDot
69+
70+
Use `HintDot` to render a small colored dot anchored to an element, drawing attention without conveying specific information.
71+
72+
<Example title="Hint dot" path="badge/hint-dot.tsx">
73+
<HintDotExample />
74+
</Example>
75+
76+
The dot supports `topRight`, `topLeft`, `bottomRight`, and `bottomLeft` placements (defaults to `topRight`). By default a border separates the dot from its anchor; set `hasBorder={false}` to remove it. When no `children` are provided the dot renders inline without an anchor.
77+
5678
<Exports component={BadgeExports} title="Badge exports" path="baseui/badge" />

src/badge/__tests__/utils.test.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from 'react';
2+
import { getAnchorFromChildren } from '../utils';
3+
4+
describe('getAnchorFromChildren', () => {
5+
it('returns undefined when no children are provided', () => {
6+
expect(getAnchorFromChildren()).toBeUndefined();
7+
});
8+
9+
it('returns the child with id and role when one child is provided', () => {
10+
const child = (
11+
<div id="test-id" role="button">
12+
Child
13+
</div>
14+
);
15+
const result = getAnchorFromChildren(child) as React.ReactElement;
16+
expect(result.type).toBe(child.type);
17+
expect(result.props.id).toBe('test-id');
18+
expect(result.props.role).toBe('button');
19+
});
20+
21+
it('logs an error and returns the first child when multiple children are provided', () => {
22+
const child1 = (
23+
<div id="test-id" role="button">
24+
Child
25+
</div>
26+
);
27+
const child2 = (
28+
<div id="test-id-2" role="img">
29+
Child
30+
</div>
31+
);
32+
console.error = jest.fn();
33+
34+
const result = getAnchorFromChildren([child1, child2]) as React.ReactElement;
35+
expect(result.type).toBe(child1.type);
36+
expect(result.props.id).toBe('test-id');
37+
expect(result.props.role).toBe('button');
38+
expect(console.error).toHaveBeenCalledWith(
39+
`[baseui] No more than 1 child may be passed to Badge, found 2 children`
40+
);
41+
});
42+
});

src/badge/constants.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,23 @@ export const HIERARCHY = Object.freeze({
99
secondary: 'secondary',
1010
});
1111

12+
export const NOTIFICATION_CIRCLE_SIZE = {
13+
small: 'small',
14+
medium: 'medium',
15+
} as const;
16+
1217
export const SHAPE = Object.freeze({
1318
pill: 'pill',
1419
rectangle: 'rectangle',
1520
});
1621

1722
export const COLOR = Object.freeze({
1823
accent: 'accent',
19-
primary: 'primary',
24+
primary: 'primary', // deprecated
2025
positive: 'positive',
2126
negative: 'negative',
2227
warning: 'warning',
28+
onBrand: 'onBrand',
2329
});
2430

2531
export const PLACEMENT = Object.freeze({

src/badge/hint-dot.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ This source code is licensed under the MIT license found in the
55
LICENSE file in the root directory of this source tree.
66
*/
77
import * as React from 'react';
8-
import { useStyletron } from '../styles/index';
98
import { getOverrides } from '../helpers/overrides';
109
import { StyledHintDot, StyledRoot, StyledPositioner } from './styled-components';
1110
import type { HintDotProps } from './types';
@@ -18,14 +17,15 @@ const HintDot = ({
1817
horizontalOffset: horizontalOffsetProp,
1918
verticalOffset: verticalOffsetProp,
2019
hidden,
20+
// placement was not there for hintBadge, but we need to support in for Avatar and other potential use cases.
21+
placement = PLACEMENT.topRight,
22+
hasBorder = true,
2123
overrides = {},
2224
}: HintDotProps) => {
2325
const [HintDot, hintDotProps] = getOverrides(overrides.Badge, StyledHintDot);
2426
const [Root, rootProps] = getOverrides(overrides.Root, StyledRoot);
2527
const [Positioner, positionerProps] = getOverrides(overrides.Positioner, StyledPositioner);
2628

27-
const [, theme] = useStyletron();
28-
2929
const anchor = getAnchorFromChildren(children);
3030

3131
// if the anchor is a string, we supply default offsets
@@ -39,22 +39,28 @@ const HintDot = ({
3939
verticalOffset = '-4px';
4040
}
4141
}
42+
4243
return (
4344
<Root {...rootProps}>
4445
{anchor}
4546

4647
<Positioner
48+
aria-hidden={true}
4749
$horizontalOffset={horizontalOffset}
4850
$verticalOffset={verticalOffset}
49-
$placement={theme.direction === 'rtl' ? PLACEMENT.topLeft : PLACEMENT.topRight}
51+
$placement={placement}
5052
$role={ROLE.hintDot}
53+
$noAnchor={!anchor}
54+
$hasBorder={hasBorder}
5155
{...positionerProps}
5256
>
5357
<HintDot
54-
{...hintDotProps}
58+
data-baseweb="hint-badge"
5559
$color={color}
5660
$horizontalOffset={horizontalOffset}
5761
$hidden={hidden}
62+
$hasBorder={hasBorder}
63+
{...hintDotProps}
5864
/>
5965
</Positioner>
6066
</Root>

src/badge/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export { default as Badge } from './badge';
1919
export { default as NotificationCircle } from './notification-circle';
2020
export { default as HintDot } from './hint-dot';
2121

22-
export { HIERARCHY, SHAPE, COLOR, PLACEMENT } from './constants';
22+
export { HIERARCHY, SHAPE, COLOR, PLACEMENT, NOTIFICATION_CIRCLE_SIZE } from './constants';
2323

2424
export * from './styled-components';
2525

src/badge/notification-circle.tsx

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as React from 'react';
88
import { getOverrides } from '../helpers/overrides';
99
import { StyledNotificationCircle, StyledRoot, StyledPositioner } from './styled-components';
1010
import type { NotificationCircleProps } from './types';
11-
import { PLACEMENT, ROLE } from './constants';
11+
import { PLACEMENT, ROLE, NOTIFICATION_CIRCLE_SIZE } from './constants';
1212
import { getAnchorFromChildren } from './utils';
1313

1414
const NotificationCircle = ({
@@ -19,6 +19,7 @@ const NotificationCircle = ({
1919
horizontalOffset,
2020
verticalOffset,
2121
hidden,
22+
size = NOTIFICATION_CIRCLE_SIZE.medium,
2223
overrides = {},
2324
}: NotificationCircleProps) => {
2425
const [NotificationCircle, NotificationCircleProps] = getOverrides(
@@ -34,22 +35,48 @@ const NotificationCircle = ({
3435
if (typeof contentProp === 'string') {
3536
console.error(`[baseui] NotificationCircle child must be number or icon, found string`);
3637
}
37-
if (placement && placement !== PLACEMENT.topLeft && placement !== PLACEMENT.topRight) {
38+
if (
39+
placement &&
40+
placement !== PLACEMENT.topLeft &&
41+
placement !== PLACEMENT.topRight &&
42+
placement !== PLACEMENT.bottomLeft &&
43+
placement !== PLACEMENT.bottomRight
44+
) {
3845
console.error(
39-
`[baseui] NotificationCircle must be placed topLeft or topRight, found ${placement}`
46+
`[baseui] NotificationCircle must be placed topLeft, topRight, bottomLeft, or bottomRight, found ${placement}`
4047
);
4148
}
4249
}
4350

4451
let content = contentProp;
45-
if (typeof content === 'number' && content > 9) {
46-
content = '9+';
52+
const ICON_SIZE = size === NOTIFICATION_CIRCLE_SIZE.small ? 10 : 12;
53+
const isContentNumber = typeof content === 'number';
54+
if (typeof content === 'number' && content > 99) {
55+
content = '99+';
56+
} else if (typeof content === 'function') {
57+
// add support for render prop, content = (size) => <Icon size={size} />
58+
content = content(ICON_SIZE);
59+
} else if (React.isValidElement(content)) {
60+
// backwards compatibility for icon element as child, clone the element and pass size as prop
61+
// content = <Icon />
62+
// React.cloneElement is not recommended but we need this to support the old way of passing icon element as content
63+
content = React.cloneElement(content as React.ReactElement<{ size?: number }>, {
64+
size: ICON_SIZE,
65+
});
4766
}
4867

4968
// If there's no anchor, render the badge inline
5069
if (!anchor) {
5170
return (
52-
<NotificationCircle $color={color} $hidden={hidden} {...NotificationCircleProps}>
71+
<NotificationCircle
72+
data-baseweb="notification-badge"
73+
$color={color}
74+
$hidden={hidden}
75+
$size={size}
76+
$extraPadding={isContentNumber}
77+
aria-hidden={true}
78+
{...NotificationCircleProps}
79+
>
5380
{content}
5481
</NotificationCircle>
5582
);
@@ -64,9 +91,17 @@ const NotificationCircle = ({
6491
$verticalOffset={verticalOffset}
6592
$placement={placement}
6693
$role={ROLE.notificationCircle}
94+
aria-hidden={true}
6795
{...positionerProps}
6896
>
69-
<NotificationCircle {...NotificationCircleProps} $color={color} $hidden={hidden}>
97+
<NotificationCircle
98+
data-baseweb="notification-badge"
99+
$color={color}
100+
$hidden={hidden}
101+
$size={size}
102+
$extraPadding={isContentNumber}
103+
{...NotificationCircleProps}
104+
>
70105
{content}
71106
</NotificationCircle>
72107
</Positioner>

0 commit comments

Comments
 (0)