Skip to content

Commit 20635b6

Browse files
authored
feat: support new chip variants (read-only, icon, badged), deprecate StatusLabel (#1217)
1 parent 8eadfd6 commit 20635b6

File tree

4 files changed

+171
-3
lines changed

4 files changed

+171
-3
lines changed

src/components/Chip/Chip.stories.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,43 @@ export const Dismissible: Story = {
4343

4444
name: "Dismissible",
4545
};
46+
47+
export const Dense: Story = {
48+
render: () => <Chip lead="Owner" value="Bob" isDense={true} />,
49+
name: "Dense",
50+
};
51+
52+
export const Inline: Story = {
53+
render: () => (
54+
<p>
55+
This is text with an inline{" "}
56+
<Chip value="chip" isDense={true} isInline={true} />
57+
</p>
58+
),
59+
name: "Inline",
60+
};
61+
62+
export const ReadOnly: Story = {
63+
render: () => <Chip lead="Owner" value="Bob" isReadOnly={true} />,
64+
name: "Read-only",
65+
};
66+
67+
export const WithIcon: Story = {
68+
render: () => <Chip value="Users" iconName="user" />,
69+
name: "With Icon",
70+
};
71+
72+
export const WithBadge: Story = {
73+
render: () => (
74+
<Chip
75+
lead="Owner"
76+
value="Bob"
77+
badge={
78+
<span className="p-badge" aria-label="More than 2.5 billion items">
79+
2.5B
80+
</span>
81+
}
82+
/>
83+
),
84+
name: "With Badge",
85+
};

src/components/Chip/Chip.test.tsx

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,21 @@ describe("Chip ", () => {
107107
expect(screen.getByTestId("chip")).toHaveClass("p-chip--information");
108108
});
109109

110+
it("renders dense chip", () => {
111+
render(<Chip data-testid="chip" isDense={true} value="dense" />);
112+
expect(screen.getByTestId("chip")).toHaveClass("is-dense");
113+
});
114+
115+
it("renders inline chip", () => {
116+
render(<Chip data-testid="chip" isInline={true} value="inline" />);
117+
expect(screen.getByTestId("chip")).toHaveClass("is-inline");
118+
});
119+
120+
it("renders readonly chip", () => {
121+
render(<Chip data-testid="chip" isReadOnly={true} value="readonly" />);
122+
expect(screen.getByTestId("chip")).toHaveClass("is-readonly");
123+
});
124+
110125
it("renders extra props", () => {
111126
render(
112127
<Chip
@@ -120,6 +135,29 @@ describe("Chip ", () => {
120135
expect(screen.getByTestId("chip")).toBeDisabled();
121136
});
122137

138+
it("renders icon chip", () => {
139+
render(<Chip data-testid="chip" iconName="user" value="Users" />);
140+
const chip = screen.getByTestId("chip");
141+
// Icons don't have roles (they are decorative), so we need to use query selector
142+
const icon = chip.querySelector(".p-icon--user");
143+
expect(icon).toBeInTheDocument();
144+
});
145+
146+
it("renders chip with badge", () => {
147+
render(
148+
<Chip
149+
data-testid="chip"
150+
lead="Owner"
151+
value="Bob"
152+
badge={<span className="p-badge">1</span>}
153+
/>,
154+
);
155+
156+
const chip = screen.getByTestId("chip");
157+
const badge = within(chip).getByText("1");
158+
expect(badge).toBeInTheDocument();
159+
});
160+
123161
it("passes additional classes", () => {
124162
render(
125163
<Chip
@@ -177,4 +215,54 @@ describe("Chip ", () => {
177215
await userEvent.click(dismissButton);
178216
expect(onSubmit).not.toHaveBeenCalled();
179217
});
218+
219+
it("does not render a dismiss button when isReadOnly is true", () => {
220+
const onDismiss = jest.fn();
221+
render(
222+
<Chip
223+
data-testid="chip"
224+
lead="Owner"
225+
value="Bob"
226+
isReadOnly={true}
227+
onDismiss={onDismiss}
228+
/>,
229+
);
230+
const chip = screen.getByTestId("chip");
231+
232+
const dismissButton = within(chip).queryByRole("button", {
233+
name: Label.Dismiss,
234+
});
235+
expect(dismissButton).not.toBeInTheDocument();
236+
});
237+
238+
it("does not call onClick when isReadOnly is true", async () => {
239+
const onClick = jest.fn();
240+
render(
241+
<Chip
242+
data-testid="chip"
243+
lead="Owner"
244+
onClick={onClick}
245+
value="Bob"
246+
isReadOnly={true}
247+
/>,
248+
);
249+
await userEvent.click(screen.getByTestId("chip"));
250+
expect(onClick).not.toHaveBeenCalled();
251+
});
252+
253+
it("cannot be focused when isReadOnly is true", async () => {
254+
const onClick = jest.fn();
255+
render(
256+
<Chip
257+
data-testid="chip"
258+
lead="Owner"
259+
onClick={onClick}
260+
value="Bob"
261+
isReadOnly={true}
262+
/>,
263+
);
264+
const chip = screen.getByTestId("chip");
265+
await userEvent.tab();
266+
expect(chip).not.toHaveFocus();
267+
});
180268
});

src/components/Chip/Chip.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import React from "react";
1+
import React, { type ReactNode } from "react";
22
import { highlightSubString } from "../../utils";
33
import type { KeyboardEvent, MouseEvent, HTMLProps } from "react";
44
import { ValueOf, PropsWithSpread } from "types";
55
import classNames from "classnames";
6+
import { ICONS } from "../Icon";
67

78
export enum Label {
89
Dismiss = "Dismiss",
@@ -22,6 +23,29 @@ export type Props = PropsWithSpread<
2223
*/
2324
appearance?: ValueOf<typeof ChipType>;
2425

26+
/**
27+
* Whether the chip is read-only.
28+
* If true, the chip will not be interactive.
29+
*/
30+
isReadOnly?: boolean;
31+
/**
32+
* A badge to display on the chip.
33+
*/
34+
badge?: ReactNode;
35+
/**
36+
* The name of an icon to display on the chip.
37+
*/
38+
iconName?: ValueOf<typeof ICONS> | string;
39+
/**
40+
* Whether the chip is dense.
41+
* Reduces the height of the chip by removing padding.
42+
*/
43+
isDense?: boolean;
44+
/**
45+
* Whether the chip is inline.
46+
* If true, the chip will be displayed inline with other content (such as text).
47+
*/
48+
isInline?: boolean;
2549
/**
2650
* The lead for the chip.
2751
*/
@@ -70,6 +94,11 @@ const Chip = ({
7094
quoteValue,
7195
selected,
7296
subString = "",
97+
isReadOnly = false,
98+
isDense = false,
99+
isInline = false,
100+
iconName,
101+
badge,
73102
value,
74103
...props
75104
}: Props): React.JSX.Element => {
@@ -88,25 +117,36 @@ const Chip = ({
88117

89118
const chipContent = (
90119
<>
120+
{iconName && <i className={`p-icon--${iconName}`} />}
91121
{lead && <span className="p-chip__lead">{lead.toUpperCase()}</span>}
92122
<span
93123
className="p-chip__value"
94124
dangerouslySetInnerHTML={{
95125
__html: quoteValue ? `'${chipValue}'` : chipValue,
96126
}}
97127
/>
128+
{badge && badge}
98129
</>
99130
);
100131

101132
const chipClassName = classNames(
102133
{
103134
[`p-chip--${appearance}`]: !!appearance,
104135
"p-chip": !appearance,
136+
"is-dense": isDense,
137+
"is-readonly": isReadOnly,
138+
"is-inline": isInline,
105139
},
106140
props.className,
107141
);
108142

109-
if (onDismiss) {
143+
if (isReadOnly) {
144+
return (
145+
<span {...props} className={chipClassName}>
146+
{chipContent}
147+
</span>
148+
);
149+
} else if (onDismiss) {
110150
return (
111151
<span {...props} className={chipClassName}>
112152
{chipContent}

src/components/StatusLabel/StatusLabel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ export type Props = PropsWithSpread<
3535

3636
/**
3737
* This is a [React](https://reactjs.org/) component for the Vanilla [Label](https://vanillaframework.io/docs/patterns/labels).
38-
*
3938
* Labels are static elements which you can apply to signify status, tags or any other information you find useful.
39+
* @deprecated StatusLabel is deprecated. Use Read-only Chip instead.
4040
*/
4141
const StatusLabel = ({
4242
appearance,

0 commit comments

Comments
 (0)