Skip to content

Commit 9b0e1ca

Browse files
committed
Add small (24px) size variant to nimble-chip
Add a 'size' attribute to the chip component with two values: - 'normal' (default, undefined): 32px height (existing behavior) - 'small': 24px height using controlSlimHeight token This is a non-breaking change. Existing chips without a size attribute continue to render at 32px. Changes: - Add ChipSize enum to chip/types.ts - Add size attribute to Chip class - Add sizeBehavior utility (mirrors appearanceBehavior pattern) - Update styles with controlSlimHeight for small variant - Add unit tests for size attribute - Update Storybook stories and matrix with size states - Update Angular directive with size property and tests
1 parent 0d42f34 commit 9b0e1ca

9 files changed

Lines changed: 134 additions & 12 deletions

File tree

packages/angular-workspace/nimble-angular/chip/nimble-chip.directive.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Directive, ElementRef, Input, Renderer2 } from '@angular/core';
22
import { type Chip, chipTag } from '@ni/nimble-components/dist/esm/chip';
3-
import { ChipAppearance } from '@ni/nimble-components/dist/esm/chip/types';
3+
import { ChipAppearance, ChipSize } from '@ni/nimble-components/dist/esm/chip/types';
44
import { type BooleanValueOrAttribute, toBooleanProperty } from '@ni/nimble-angular/internal-utilities';
55

66
export type { Chip };
77
export { chipTag };
8-
export { ChipAppearance };
8+
export { ChipAppearance, ChipSize };
99

1010
/**
1111
* Directive to provide Angular integration for the chip.
@@ -39,5 +39,13 @@ export class NimbleChipDirective {
3939
this.renderer.setProperty(this.elementRef.nativeElement, 'appearance', value);
4040
}
4141

42+
public get size(): ChipSize {
43+
return this.elementRef.nativeElement.size;
44+
}
45+
46+
@Input() public set size(value: ChipSize) {
47+
this.renderer.setProperty(this.elementRef.nativeElement, 'size', value);
48+
}
49+
4250
public constructor(private readonly renderer: Renderer2, private readonly elementRef: ElementRef<Chip>) {}
4351
}

packages/angular-workspace/nimble-angular/chip/tests/nimble-chip.directive.spec.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Component, ElementRef, ViewChild } from '@angular/core';
22
import { ComponentFixture, TestBed } from '@angular/core/testing';
33
import type { BooleanValueOrAttribute } from '@ni/nimble-angular/internal-utilities';
4-
import { type Chip, ChipAppearance, NimbleChipDirective } from '../nimble-chip.directive';
4+
import { type Chip, ChipAppearance, ChipSize, NimbleChipDirective } from '../nimble-chip.directive';
55
import { NimbleChipModule } from '../nimble-chip.module';
66

77
describe('Nimble chip', () => {
@@ -58,6 +58,11 @@ describe('Nimble chip', () => {
5858
expect(directive.appearance).toBe(ChipAppearance.outline);
5959
expect(nativeElement.appearance).toBe(ChipAppearance.outline);
6060
});
61+
62+
it('has expected defaults for size', () => {
63+
expect(directive.size).toBeUndefined();
64+
expect(nativeElement.size).toBeUndefined();
65+
});
6166
});
6267

6368
describe('with template string values', () => {
@@ -66,7 +71,8 @@ describe('Nimble chip', () => {
6671
<nimble-chip #chip
6772
removable
6873
disabled
69-
appearance="block">
74+
appearance="block"
75+
size="small">
7076
</nimble-chip>`,
7177
standalone: false
7278
})
@@ -104,6 +110,11 @@ describe('Nimble chip', () => {
104110
expect(directive.appearance).toBe(ChipAppearance.block);
105111
expect(nativeElement.appearance).toBe(ChipAppearance.block);
106112
});
113+
114+
it('will use template string values for size', () => {
115+
expect(directive.size).toBe(ChipSize.small);
116+
expect(nativeElement.size).toBe(ChipSize.small);
117+
});
107118
});
108119

109120
describe('with property bound values', () => {
@@ -112,7 +123,8 @@ describe('Nimble chip', () => {
112123
<nimble-chip #chip
113124
[removable]="removable"
114125
[disabled]="disabled"
115-
[appearance]="appearance">
126+
[appearance]="appearance"
127+
[size]="size">
116128
</nimble-chip>
117129
`,
118130
standalone: false
@@ -123,6 +135,7 @@ describe('Nimble chip', () => {
123135
public removable = false;
124136
public disabled = false;
125137
public appearance: ChipAppearance = ChipAppearance.outline;
138+
public size: ChipSize = ChipSize.normal;
126139
}
127140

128141
let fixture: ComponentFixture<TestHostComponent>;
@@ -172,6 +185,17 @@ describe('Nimble chip', () => {
172185
expect(directive.appearance).toBe(ChipAppearance.block);
173186
expect(nativeElement.appearance).toBe(ChipAppearance.block);
174187
});
188+
189+
it('can be configured with property binding for size', () => {
190+
expect(directive.size).toBeUndefined();
191+
expect(nativeElement.size).toBeUndefined();
192+
193+
fixture.componentInstance.size = ChipSize.small;
194+
fixture.detectChanges();
195+
196+
expect(directive.size).toBe(ChipSize.small);
197+
expect(nativeElement.size).toBe(ChipSize.small);
198+
});
175199
});
176200

177201
describe('with attribute bound values', () => {
@@ -180,7 +204,8 @@ describe('Nimble chip', () => {
180204
<nimble-chip #chip
181205
[attr.removable]="removable"
182206
[attr.disabled]="disabled"
183-
[attr.appearance]="appearance">
207+
[attr.appearance]="appearance"
208+
[attr.size]="size">
184209
</nimble-chip>
185210
`,
186211
standalone: false
@@ -191,6 +216,7 @@ describe('Nimble chip', () => {
191216
public removable: BooleanValueOrAttribute = null;
192217
public disabled: BooleanValueOrAttribute = null;
193218
public appearance: ChipAppearance = ChipAppearance.outline;
219+
public size: ChipSize = ChipSize.normal;
194220
}
195221

196222
let fixture: ComponentFixture<TestHostComponent>;
@@ -240,5 +266,16 @@ describe('Nimble chip', () => {
240266
expect(directive.appearance).toBe(ChipAppearance.block);
241267
expect(nativeElement.appearance).toBe(ChipAppearance.block);
242268
});
269+
270+
it('can be configured with attribute binding for size', () => {
271+
expect(directive.size).toBeUndefined();
272+
expect(nativeElement.size).toBeUndefined();
273+
274+
fixture.componentInstance.size = ChipSize.small;
275+
fixture.detectChanges();
276+
277+
expect(directive.size).toBe(ChipSize.small);
278+
expect(nativeElement.size).toBe(ChipSize.small);
279+
});
243280
});
244281
});

packages/nimble-components/src/chip/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from '@ni/fast-foundation';
1111
import { styles } from './styles';
1212
import { template } from './template';
13-
import { ChipAppearance } from './types';
13+
import { ChipAppearance, ChipSize } from './types';
1414
import { slotTextContent } from '../utilities/models/slot-text-content';
1515
import { itemRemoveLabel } from '../label-provider/core/label-tokens';
1616

@@ -37,6 +37,9 @@ export class Chip extends FoundationElement {
3737
@attr()
3838
public appearance: ChipAppearance = ChipAppearance.outline;
3939

40+
@attr()
41+
public size: ChipSize;
42+
4043
@attr({ attribute: 'tabindex', converter: nullableNumberConverter })
4144
public override tabIndex!: number;
4245

packages/nimble-components/src/chip/styles.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ import {
77
borderRgbPartialColor,
88
borderWidth,
99
controlHeight,
10+
controlSlimHeight,
1011
iconColor,
1112
iconSize,
1213
mediumPadding,
1314
smallPadding
1415
} from '../theme-provider/design-tokens';
1516
import { display } from '../utilities/style/display';
1617
import { appearanceBehavior } from '../utilities/style/appearance';
17-
import { ChipAppearance } from './types';
18+
import { sizeBehavior } from '../utilities/style/size';
19+
import { ChipAppearance, ChipSize } from './types';
1820

1921
export const styles = css`
2022
${display('inline-flex')}
@@ -79,5 +81,13 @@ export const styles = css`
7981
border-color: transparent;
8082
}
8183
`
84+
),
85+
sizeBehavior(
86+
ChipSize.small,
87+
css`
88+
:host {
89+
height: ${controlSlimHeight};
90+
}
91+
`
8292
)
8393
);

packages/nimble-components/src/chip/tests/chip.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Chip, chipTag } from '..';
33
import { fixture, type Fixture } from '../../utilities/tests/fixture';
44
import { ChipPageObject } from '../testing/chip.pageobject';
55
import { waitForUpdatesAsync } from '../../testing/async-helpers';
6+
import { ChipSize } from '../types';
67

78
async function setup(): Promise<Fixture<Chip>> {
89
return await fixture<Chip>(html`<${chipTag}></${chipTag}>`);
@@ -87,6 +88,26 @@ describe('Chip', () => {
8788
expect(pageObject.getRemoveButtonTabIndex()).toBeNull();
8889
});
8990

91+
it('has expected defaults for size', () => {
92+
expect(element.size).toBeUndefined();
93+
});
94+
95+
it('can set size to small', async () => {
96+
element.size = ChipSize.small;
97+
await waitForUpdatesAsync();
98+
expect(element.size).toBe(ChipSize.small);
99+
expect(element.getAttribute('size')).toBe('small');
100+
});
101+
102+
it('can set size back to normal', async () => {
103+
element.size = ChipSize.small;
104+
await waitForUpdatesAsync();
105+
element.size = ChipSize.normal;
106+
await waitForUpdatesAsync();
107+
expect(element.size).toBeUndefined();
108+
expect(element.hasAttribute('size')).toBeFalse();
109+
});
110+
90111
describe('title overflow', () => {
91112
beforeEach(async () => {
92113
element.style.width = '200px';

packages/nimble-components/src/chip/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,9 @@ export const ChipAppearance = {
33
block: 'block'
44
} as const;
55
export type ChipAppearance = (typeof ChipAppearance)[keyof typeof ChipAppearance];
6+
7+
export const ChipSize = {
8+
normal: undefined,
9+
small: 'small'
10+
} as const;
11+
export type ChipSize = (typeof ChipSize)[keyof typeof ChipSize];
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Behavior, ElementStyles } from '@ni/fast-element';
2+
import { MultivaluePropertyStyleSheetBehavior } from './multivalue-property-stylesheet-behavior';
3+
4+
/**
5+
* Behavior that will conditionally apply a stylesheet based on the element's
6+
* size property
7+
*
8+
* @param value - The value or values of the size property
9+
* @param styles - The styles to be applied when condition matches
10+
*
11+
* @public
12+
*/
13+
export function sizeBehavior<SizeType>(
14+
value: SizeType | SizeType[],
15+
styles: ElementStyles
16+
): Behavior {
17+
return new MultivaluePropertyStyleSheetBehavior('size', value, styles);
18+
}

packages/storybook/src/nimble/chip/chip-matrix.stories.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { StoryFn, Meta } from '@storybook/html-vite';
22
import { html, ViewTemplate, when } from '@ni/fast-element';
33
import { iconKeyTag } from '@ni/nimble-components/dist/esm/icons/key';
4-
import { ChipAppearance } from '@ni/nimble-components/dist/esm/chip/types';
4+
import { ChipAppearance, ChipSize } from '@ni/nimble-components/dist/esm/chip/types';
55
import { chipTag } from '@ni/nimble-components/dist/esm/chip';
66
import {
77
controlLabelFont,
@@ -23,6 +23,12 @@ const appearanceStates = [
2323
] as const;
2424
type AppearanceState = (typeof appearanceStates)[number];
2525

26+
const sizeStates = [
27+
['Normal', ChipSize.normal],
28+
['Small', ChipSize.small]
29+
] as const;
30+
type SizeState = (typeof sizeStates)[number];
31+
2632
const removableStates = [
2733
['Removable', true],
2834
['Not Removable', false]
@@ -64,17 +70,19 @@ export default metadata;
6470
const component = (
6571
[disabledName, disabled]: DisabledState,
6672
[appearanceName, appearance]: AppearanceState,
73+
[sizeName, size]: SizeState,
6774
[removableName, removable]: RemovableStates,
6875
[showStartSlotIconName, showStartSlotIcon]: ShowStartSlotIconState,
6976
[labelName, label]: LabelState,
7077
[widthName, width]: WidthState
7178
): ViewTemplate => html`
7279
<div style="display: flex; flex-direction: column;">
7380
<label style="color: var(${controlLabelFontColor.cssCustomProperty}); font: var(${controlLabelFont.cssCustomProperty})">
74-
${appearanceName}, ${removableName}, ${showStartSlotIconName}, ${labelName}, ${widthName}, ${disabledName ? `(${disabledName})` : ''}
81+
${appearanceName}, ${sizeName}, ${removableName}, ${showStartSlotIconName}, ${labelName}, ${widthName}, ${disabledName ? `(${disabledName})` : ''}
7582
</label>
7683
<${chipTag}
7784
appearance="${() => appearance}"
85+
size="${() => size}"
7886
?removable="${() => removable}"
7987
?disabled=${() => disabled}
8088
style="margin-right: 8px; margin-bottom: 8px; ${() => width};">
@@ -88,6 +96,7 @@ export const themeMatrix: StoryFn = createMatrixThemeStory(
8896
createMatrix(component, [
8997
disabledStates,
9098
appearanceStates,
99+
sizeStates,
91100
removableStates,
92101
showStartSlotIconStates,
93102
labelStates,
@@ -98,6 +107,7 @@ export const themeMatrix: StoryFn = createMatrixThemeStory(
98107
const interactionStates = cartesianProduct([
99108
disabledStates,
100109
appearanceStates,
110+
sizeStates,
101111
removableStates,
102112
[showStartSlotIconStatesOnlyIcon],
103113
[labelStatesOnlyShort],
@@ -107,6 +117,7 @@ const interactionStates = cartesianProduct([
107117
const interactionStatesHover = cartesianProduct([
108118
disabledStates,
109119
appearanceStates,
120+
sizeStates,
110121
removableStates,
111122
[showStartSlotIconStatesOnlyIcon],
112123
[labelStatesOnlyShort],

packages/storybook/src/nimble/chip/chip.stories.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { html, when } from '@ni/fast-element';
22
import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html-vite';
33
import { withActions } from 'storybook/actions/decorator';
44
import { chipTag } from '@ni/nimble-components/dist/esm/chip';
5-
import { ChipAppearance } from '@ni/nimble-components/dist/esm/chip/types';
5+
import { ChipAppearance, ChipSize } from '@ni/nimble-components/dist/esm/chip/types';
66
import {
77
apiCategory,
88
appearanceDescription,
@@ -13,6 +13,7 @@ import {
1313

1414
interface ChipArgs {
1515
appearance: keyof typeof ChipAppearance;
16+
size: keyof typeof ChipSize;
1617
removable: boolean;
1718
content: string;
1819
icon: boolean;
@@ -24,7 +25,7 @@ const metadata: Meta<ChipArgs> = {
2425
title: 'Components/Chip',
2526
render: createUserSelectedThemeStory(html`
2627
${disableStorybookZoomTransform}
27-
<${chipTag} appearance="${x => x.appearance}" ?removable="${x => x.removable}" ?disabled="${x => x.disabled}">
28+
<${chipTag} appearance="${x => x.appearance}" size="${x => ChipSize[x.size]}" ?removable="${x => x.removable}" ?disabled="${x => x.disabled}">
2829
${x => x.content}
2930
${when(x => x.icon, html`
3031
<nimble-icon-check slot="start"></nimble-icon-check>
@@ -38,6 +39,12 @@ const metadata: Meta<ChipArgs> = {
3839
control: { type: 'radio' },
3940
table: { category: apiCategory.attributes }
4041
},
42+
size: {
43+
description: 'Size of the chip. Use `small` for the 24px version.',
44+
options: Object.keys(ChipSize),
45+
control: { type: 'radio' },
46+
table: { category: apiCategory.attributes }
47+
},
4148
removable: {
4249
name: 'removable',
4350
description: 'When the `removable` attribute is set, a remove button is displayed on the chip. When the remove button is pressed a `remove` event is dispatched. The client application is responsible for performing the actual removal.',
@@ -68,6 +75,7 @@ const metadata: Meta<ChipArgs> = {
6875
},
6976
args: {
7077
appearance: 'outline',
78+
size: 'normal',
7179
removable: false,
7280
content: 'Homer Simpson',
7381
icon: false,

0 commit comments

Comments
 (0)