Skip to content

Commit dcac6a3

Browse files
mannycarrera4manuel.carrerajosh-bagwellNicholasBoll
authoredMar 17, 2025··
feat!: Refactor Pill component to use new styling utilities and tokens (#3104)
Fixes: #2394, #2968, #2049 [category:Components] Release Note: We've updated ExternalHyperlink to use our new styling utilities and tokens. - The border color on hover has been updated from `licorice400` to `licorice500` to match our design specs. - We've removed extra elements and leverage [flex box}(https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/CSS_layout/Flexbox) to ensure only the label receives overflow styles. When `maxWidth` is set, it is set on the parent `<Pill/>` element and the child elements will be styled accordingly. Before v13, `maxWidth` wasn't calculating the width of all its elements and wasn't a true pixel value. ### BREAKING CHANGES - `maxWidth` has been removed from the `usePillModel`. This config was used to style sub components. With the refactor to use `data-part` and [stencils](https://workday.github.io/canvas-kit/?path=/docs/styling-basics--docs#create-stencil), it is no longer needed on the model. You can still apply `maxWidth` on the parent `<Pill>` element and the child elements will be styled accordingly. - `Pill.Icon` no longer has a default `aria-labe="add"`. You *must* provide an `aria-label` for `Pill.Icon` to ensure proper accessibility. Our examples have been updated to reflect this change. - `Pill.IconButton` no longer has a default `aria-label="remove"`. You *must* provide an `aria-label` for `Pill.IconButton` to ensure proper accessibility. Our examples have been updated to reflect this change. - `Pill.Label` is a required element when using other sub components like `Pill.Icon` to ensure that the label truncates correctly. Co-authored-by: manuel.carrera <manuel.carrera@workday.com> Co-authored-by: @josh-bagwell <44883293+josh-bagwell@users.noreply.github.com> Co-authored-by: @NicholasBoll <nicholas.boll@gmail.com>
1 parent 7575839 commit dcac6a3

30 files changed

+809
-390
lines changed
 

‎cypress/component/PillPreview.spec.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('Pill', () => {
1717
cy.checkA11y();
1818
});
1919

20-
it('should have two elements with a role of "button"', () => {
20+
it('should render two pills with role "button"', () => {
2121
cy.findAllByRole('button').should('have.length', 2);
2222
});
2323

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {API, FileInfo, JSXElement, JSXIdentifier, Options} from 'jscodeshift';
2+
import {getImportRenameMap} from './utils/getImportRenameMap';
3+
import {hasImportSpecifiers} from '../v6/utils';
4+
5+
const pillPackage = '@workday/canvas-kit-preview-react/pill';
6+
7+
export default function transformer(file: FileInfo, api: API, options: Options) {
8+
const j = api.jscodeshift;
9+
const root = j(file.source);
10+
11+
// Skip transformation if Pill is not imported from the target package
12+
if (!hasImportSpecifiers(api, root, pillPackage, ['Pill'])) {
13+
return file.source;
14+
}
15+
16+
const {importMap, styledMap} = getImportRenameMap(j, root, '@workday/canvas-kit-preview-react');
17+
18+
root
19+
.find(
20+
j.JSXElement,
21+
(value: JSXElement) =>
22+
value.openingElement.name.type === 'JSXIdentifier' &&
23+
(value.openingElement.name.name === importMap.Pill ||
24+
value.openingElement.name.name === styledMap.Pill)
25+
)
26+
.forEach(nodePath => {
27+
// Get the local name of the Pill component (e.g., Pill, MyPill, StyledPill)
28+
const pillName = (nodePath.node.openingElement.name as JSXIdentifier).name;
29+
30+
// Check for subcomponents using the local Pill name
31+
const hasPillSubcomponents =
32+
nodePath.node.children &&
33+
nodePath.node.children.some(child => {
34+
if (
35+
child.type === 'JSXElement' &&
36+
child.openingElement.type === 'JSXOpeningElement' &&
37+
child.openingElement.name.type === 'JSXMemberExpression' &&
38+
child.openingElement.name.object.type === 'JSXIdentifier' &&
39+
child.openingElement.name.object.name === pillName
40+
) {
41+
return (
42+
child.openingElement.name.property.name === 'Icon' ||
43+
child.openingElement.name.property.name === 'Avatar' ||
44+
child.openingElement.name.property.name === 'IconButton' ||
45+
child.openingElement.name.property.name === 'Count'
46+
);
47+
}
48+
return false;
49+
});
50+
51+
// If subcomponents are present, wrap text and expressions in Pill.Label
52+
if (hasPillSubcomponents) {
53+
nodePath.node.children = nodePath.node.children?.map(child => {
54+
if (child.type === 'JSXText' && child.value.trim() !== '') {
55+
return j.jsxElement(
56+
j.jsxOpeningElement(
57+
j.jsxMemberExpression(j.jsxIdentifier(pillName), j.jsxIdentifier('Label')),
58+
[]
59+
),
60+
j.jsxClosingElement(
61+
j.jsxMemberExpression(j.jsxIdentifier(pillName), j.jsxIdentifier('Label'))
62+
),
63+
[child]
64+
);
65+
} else if (child.type === 'JSXExpressionContainer') {
66+
return j.jsxElement(
67+
j.jsxOpeningElement(
68+
j.jsxMemberExpression(j.jsxIdentifier(pillName), j.jsxIdentifier('Label')),
69+
[]
70+
),
71+
j.jsxClosingElement(
72+
j.jsxMemberExpression(j.jsxIdentifier(pillName), j.jsxIdentifier('Label'))
73+
),
74+
[child]
75+
);
76+
}
77+
return child;
78+
});
79+
}
80+
});
81+
82+
return root.toSource();
83+
}

‎modules/codemod/lib/v13/index.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import {Transform} from 'jscodeshift';
2+
import addPillLabel from './addPillLabel';
23

34
const transform: Transform = (file, api, options) => {
45
// These will run in order. If your transform depends on others, place yours after dependent transforms
5-
// const fixes = [
6-
// // add codemods here
7-
// ];
8-
// return fixes.reduce((source, fix) => fix({...file, source}, api, options) as string, file.source);
6+
const fixes = [
7+
// add codemods here
8+
addPillLabel,
9+
];
10+
return fixes.reduce((source, fix) => fix({...file, source}, api, options) as string, file.source);
911
};
1012

1113
export default transform;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import {expectTransformFactory} from './expectTransformFactory';
2+
import transform from '../addPillLabel';
3+
import {stripIndent} from 'common-tags';
4+
5+
const expectTransform = expectTransformFactory(transform);
6+
7+
describe('Pill', () => {
8+
it('should not change non-canvas imports', () => {
9+
const input = stripIndent`
10+
import {Pill} from '@workday/any-other-package'
11+
<>
12+
<Pill>Hello<Pill.Icon/></Pill>
13+
<Pill />
14+
<Pill />
15+
</>
16+
`;
17+
18+
const expected = stripIndent`
19+
import {Pill} from '@workday/any-other-package'
20+
<>
21+
<Pill>Hello<Pill.Icon/></Pill>
22+
<Pill />
23+
<Pill />
24+
</>
25+
`;
26+
expectTransform(input, expected);
27+
});
28+
29+
it('should not change if the only children of Pill is plain text', () => {
30+
const input = stripIndent`
31+
import {Pill} from '@workday/canvas-kit-preview-react/pill';
32+
<>
33+
<Pill>
34+
Hello World
35+
</Pill>
36+
</>
37+
`;
38+
39+
const expected = stripIndent`
40+
import {Pill} from '@workday/canvas-kit-preview-react/pill';
41+
<>
42+
<Pill>
43+
Hello World
44+
</Pill>
45+
</>
46+
`;
47+
expectTransform(input, expected);
48+
});
49+
it('should not change if Pill.Label exists', () => {
50+
const input = stripIndent`
51+
import {Pill} from '@workday/canvas-kit-preview-react/pill';
52+
<>
53+
<Pill>
54+
<Pill.Label>Hello World</Pill.Label>
55+
</Pill>
56+
</>
57+
`;
58+
59+
const expected = stripIndent`
60+
import {Pill} from '@workday/canvas-kit-preview-react/pill';
61+
<>
62+
<Pill>
63+
<Pill.Label>Hello World</Pill.Label>
64+
</Pill>
65+
</>
66+
`;
67+
expectTransform(input, expected);
68+
});
69+
70+
it('should wrap plain text in label if Pill.Icon is present and imported from main Preview package', () => {
71+
const input = stripIndent`
72+
import {Pill} from '@workday/canvas-kit-preview-react';
73+
<>
74+
<Pill>
75+
<Pill.Icon />
76+
Hello World
77+
</Pill>
78+
</>
79+
`;
80+
81+
const expected = stripIndent`
82+
import {Pill} from '@workday/canvas-kit-preview-react';
83+
<>
84+
<Pill>
85+
<Pill.Icon /><Pill.Label>Hello World</Pill.Label></Pill>
86+
</>
87+
`;
88+
expectTransform(input, expected);
89+
});
90+
91+
it('should wrap plain text in label if Pill.Icon is present and Pill is renamed at the import level', () => {
92+
const input = stripIndent`
93+
import {Pill as MyPill} from '@workday/canvas-kit-preview-react';
94+
<>
95+
<MyPill>
96+
<MyPill.Icon />
97+
Hello World
98+
</MyPill>
99+
</>
100+
`;
101+
102+
const expected = stripIndent`
103+
import {Pill as MyPill} from '@workday/canvas-kit-preview-react';
104+
<>
105+
<MyPill>
106+
<MyPill.Icon /><MyPill.Label>Hello World</MyPill.Label></MyPill>
107+
</>
108+
`;
109+
expectTransform(input, expected);
110+
});
111+
112+
it('should change styled Pill', () => {
113+
const input = stripIndent`
114+
import {Pill} from '@workday/canvas-kit-preview-react/pill'
115+
const StyledPill = styled(Pill)({color: "#000"});
116+
<>
117+
<StyledPill>
118+
<StyledPill.Icon />
119+
Hello World
120+
</StyledPill>
121+
</>
122+
`;
123+
124+
const expected = stripIndent`
125+
import {Pill} from '@workday/canvas-kit-preview-react/pill'
126+
const StyledPill = styled(Pill)({color: "#000"});
127+
<>
128+
<StyledPill>
129+
<StyledPill.Icon /><StyledPill.Label>Hello World</StyledPill.Label></StyledPill>
130+
</>
131+
`;
132+
expectTransform(input, expected);
133+
});
134+
135+
it('should wrap plain text in label if Pill.Icon is present and Pill is renamed at the import level', () => {
136+
const input = stripIndent`
137+
import {Pill as MyPill} from '@workday/canvas-kit-preview-react';
138+
<>
139+
<MyPill>
140+
<MyPill.Icon />
141+
Hello World
142+
</MyPill>
143+
</>
144+
`;
145+
146+
const expected = stripIndent`
147+
import {Pill as MyPill} from '@workday/canvas-kit-preview-react';
148+
<>
149+
<MyPill>
150+
<MyPill.Icon /><MyPill.Label>Hello World</MyPill.Label></MyPill>
151+
</>
152+
`;
153+
expectTransform(input, expected);
154+
});
155+
156+
it('should wrap plain text in label if Pill.Icon is present', () => {
157+
const input = stripIndent`
158+
import {Pill} from '@workday/canvas-kit-preview-react/pill';
159+
<>
160+
<Pill>
161+
<Pill.Icon />
162+
Hello World
163+
</Pill>
164+
</>
165+
`;
166+
167+
const expected = stripIndent`
168+
import {Pill} from '@workday/canvas-kit-preview-react/pill';
169+
<>
170+
<Pill>
171+
<Pill.Icon /><Pill.Label>Hello World</Pill.Label></Pill>
172+
</>
173+
`;
174+
expectTransform(input, expected);
175+
});
176+
it('should wrap text in label if Pill.IconButton is present and the text is rendered as an expression', () => {
177+
const input = stripIndent`
178+
import {Pill} from '@workday/canvas-kit-preview-react/pill';
179+
const myVar = 'Hello World';
180+
<>
181+
<Pill>
182+
{myVar}
183+
<Pill.IconButton />
184+
</Pill>
185+
</>
186+
`;
187+
188+
const expected = stripIndent`
189+
import {Pill} from '@workday/canvas-kit-preview-react/pill';
190+
const myVar = 'Hello World';
191+
<>
192+
<Pill>
193+
<Pill.Label>{myVar}</Pill.Label>
194+
<Pill.IconButton />
195+
</Pill>
196+
</>
197+
`;
198+
expectTransform(input, expected);
199+
});
200+
it('should wrap text in label if Pill.IconButton is present and the text is rendered as an expression', () => {
201+
const input = stripIndent`
202+
import {Pill} from '@workday/canvas-kit-preview-react/pill';
203+
const myVar = 'Hello World';
204+
<>
205+
<Pill
206+
margin="auto"
207+
marginTop="xxl"
208+
variant="readOnly"
209+
backgroundColor="soap100"
210+
data-automation-id="branding-banner-pill"
211+
>
212+
{getText('Some.Text')}
213+
</Pill>
214+
</>
215+
`;
216+
217+
const expected = stripIndent`
218+
import {Pill} from '@workday/canvas-kit-preview-react/pill';
219+
const myVar = 'Hello World';
220+
<>
221+
<Pill
222+
margin="auto"
223+
marginTop="xxl"
224+
variant="readOnly"
225+
backgroundColor="soap100"
226+
data-automation-id="branding-banner-pill"
227+
>
228+
{getText('Some.Text')}
229+
</Pill>
230+
</>
231+
`;
232+
expectTransform(input, expected);
233+
});
234+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import {runInlineTest} from 'jscodeshift/dist/testUtils';
2+
3+
export const expectTransformFactory =
4+
(fn: Function) => (input: string, expected: string, options?: Record<string, any>) => {
5+
return runInlineTest(fn, options, {source: input}, expected, {parser: 'tsx'});
6+
};

‎modules/docs/mdx/12.0-UPGRADE-GUIDE.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ const MyComboboxInput = createSubcomponent(TextInput)({
326326
**PRs:** [#2865](https://github.com/Workday/canvas-kit/pull/2865),
327327
[#2881](https://github.com/Workday/canvas-kit/pull/2881),
328328
[#2934](https://github.com/Workday/canvas-kit/pull/2934),
329-
[2973](https://github.com/Workday/canvas-kit/pull/2973)
329+
[#2973](https://github.com/Workday/canvas-kit/pull/2973)
330330

331331
We've promoted FormField from [Preview](#preview) to [Main](#main). The following changes has been
332332
made to provide more flexibility and better explicit components when using inputs.

0 commit comments

Comments
 (0)
Please sign in to comment.