Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update label to use element internals custom states for styling #31738

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
chrisdholt marked this conversation as resolved.
Show resolved Hide resolved
"type": "prerelease",
"comment": "update label to use element internals custom states for styling",
"packageName": "@fluentui/web-components",
"email": "[email protected]",
"dependentChangeType": "patch"
}
9 changes: 8 additions & 1 deletion packages/web-components/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -2146,9 +2146,14 @@ export const ImageTemplate: ElementViewTemplate<Image_2>;
// @public
export class Label extends FASTElement {
disabled: boolean;
disabledChanged(prev: boolean | undefined, next: boolean | undefined): void;
// @internal
elementInternals: ElementInternals;
required: boolean;
size?: LabelSize;
sizeChanged(prev: LabelSize | undefined, next: LabelSize | undefined): void;
weight?: LabelWeight;
weightChanged(prev: LabelWeight | undefined, next: LabelWeight | undefined): void;
}

// @public
Expand Down Expand Up @@ -2883,9 +2888,11 @@ export const spacingVerticalXXXL = "var(--spacingVerticalXXXL)";
export class Spinner extends FASTElement {
constructor();
appearance?: SpinnerAppearance;
appearanceChanged(prev: SpinnerAppearance | undefined, next: SpinnerAppearance | undefined): void;
// @internal
protected elementInternals: ElementInternals;
elementInternals: ElementInternals;
size?: SpinnerSize;
sizeChanged(prev: SpinnerSize | undefined, next: SpinnerSize | undefined): void;
}

// @public
Expand Down
55 changes: 9 additions & 46 deletions packages/web-components/src/label/label.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,31 @@ test.describe('Label', () => {
await page.close();
});

test('should set default attribute values', async () => {
await expect(element).toHaveAttribute('size', 'medium');
await expect(element).toHaveJSProperty('size', 'medium');

await expect(element).toHaveAttribute('weight', 'regular');
await expect(element).toHaveJSProperty('weight', 'regular');
});

test('should reflect size attribute', async () => {
await element.evaluate((node: Label) => {
node.size = 'small';
});

await expect(element).toHaveAttribute('size', 'small');
await expect(element).toHaveJSProperty('size', 'small');
expect(await element.evaluate((node: Label) => node.elementInternals.states.has('small'))).toBe(true);

await element.evaluate((node: Label) => {
node.size = 'medium';
});

await expect(element).toHaveAttribute('size', 'medium');
await expect(element).toHaveJSProperty('size', 'medium');
expect(await element.evaluate((node: Label) => node.elementInternals.states.has('small'))).toBe(false);
expect(await element.evaluate((node: Label) => node.elementInternals.states.has('medium'))).toBe(true);

await element.evaluate((node: Label) => {
node.size = 'large';
});
await expect(element).toHaveAttribute('size', 'large');
await expect(element).toHaveJSProperty('size', 'large');
expect(await element.evaluate((node: Label) => node.elementInternals.states.has('medium'))).toBe(false);
expect(await element.evaluate((node: Label) => node.elementInternals.states.has('large'))).toBe(true);
});

test('should reflect weight attribute', async () => {
Expand All @@ -57,12 +54,15 @@ test.describe('Label', () => {
});
await expect(element).toHaveAttribute('weight', 'regular');
await expect(element).toHaveJSProperty('weight', 'regular');
expect(await element.evaluate((node: Label) => node.elementInternals.states.has('regular'))).toBe(true);

await element.evaluate((node: Label) => {
node.weight = 'semibold';
});
await expect(element).toHaveAttribute('weight', 'semibold');
await expect(element).toHaveJSProperty('weight', 'semibold');
expect(await element.evaluate((node: Label) => node.elementInternals.states.has('regular'))).toBe(false);
expect(await element.evaluate((node: Label) => node.elementInternals.states.has('semibold'))).toBe(true);
});

test('should reflect disabled attribute', async () => {
Expand All @@ -74,6 +74,7 @@ test.describe('Label', () => {

await expect(element).toHaveAttribute('disabled', '');
await expect(element).toHaveJSProperty('disabled', true);
expect(await element.evaluate((node: Label) => node.elementInternals.states.has('disabled'))).toBe(true);
});

test('should reflect required attribute and show asterisk', async () => {
Expand Down Expand Up @@ -102,42 +103,4 @@ test.describe('Label', () => {
await expect(element).toHaveJSProperty('required', false);
await expect(asterisk).toBeHidden();
});

test('should reflect changes in size, weight, disabled, and required attributes', async () => {
await root.evaluate(node => {
node.innerHTML = /* html */ `
<fluent-label size="medium" weight="regular">Label</fluent-label>
`;
});
await expect(element).toHaveAttribute('size', 'medium');
await expect(element).toHaveJSProperty('size', 'medium');

await expect(element).toHaveAttribute('weight', 'regular');
await expect(element).toHaveJSProperty('weight', 'regular');

await expect(element).not.toHaveAttribute('disabled', '');
await expect(element).toHaveJSProperty('disabled', false);

await expect(element).not.toHaveAttribute('required', '');
await expect(element).toHaveJSProperty('required', false);

await element.evaluate((node: Label) => {
node.size = 'large';
node.weight = 'semibold';
node.disabled = true;
node.required = true;
});

await expect(element).toHaveAttribute('size', 'large');
await expect(element).toHaveJSProperty('size', 'large');

await expect(element).toHaveAttribute('weight', 'semibold');
await expect(element).toHaveJSProperty('weight', 'semibold');

await expect(element).toHaveAttribute('disabled', '');
await expect(element).toHaveJSProperty('disabled', true);

await expect(element).toHaveAttribute('required', '');
await expect(element).toHaveJSProperty('required', true);
});
});
13 changes: 7 additions & 6 deletions packages/web-components/src/label/label.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
lineHeightBase400,
spacingHorizontalXS,
} from '../theme/design-tokens.js';
import { largeState, smallState } from '../styles/states/index.js';

/** Label styles
* @public
Expand All @@ -37,23 +38,23 @@ export const styles = css`
margin-inline-start: ${spacingHorizontalXS};
}

:host([size='small']) {
:host(${smallState}) {
font-size: ${fontSizeBase200};
line-height: ${lineHeightBase200};
}

:host([size='large']) {
:host(${largeState}) {
font-size: ${fontSizeBase400};
line-height: ${lineHeightBase400};
}

:host([size='large']),
:host([weight='semibold']) {
:host(${largeState}),
:host(:is([state--semibold], :state(semibold))) {
font-weight: ${fontWeightSemibold};
}

:host([disabled]),
:host([disabled]) .asterisk {
:host(:is([state--disabled], :state(disabled))),
:host(:is([state--disabled], :state(disabled))) .asterisk {
color: ${colorNeutralForegroundDisabled};
}
`;
45 changes: 45 additions & 0 deletions packages/web-components/src/label/label.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { attr, FASTElement } from '@microsoft/fast-element';
import { toggleState } from '../utils/element-internals.js';
import { LabelSize, LabelWeight } from './label.options.js';

/**
* The base class used for constructing a fluent-label custom element
* @public
*/
export class Label extends FASTElement {
/**
* The internal {@link https://developer.mozilla.org/docs/Web/API/ElementInternals | `ElementInternals`} instance for the component.
*
* @internal
*/
public elementInternals: ElementInternals = this.attachInternals();

/**
* Specifies font size of a label
*
Expand All @@ -16,6 +24,20 @@ export class Label extends FASTElement {
@attr
public size?: LabelSize;

/**
* Handles changes to size attribute custom states
* @param prev - the previous state
* @param next - the next state
*/
public sizeChanged(prev: LabelSize | undefined, next: LabelSize | undefined) {
if (prev) {
toggleState(this.elementInternals, `${prev}`, false);
}
if (next) {
toggleState(this.elementInternals, `${next}`, true);
}
}

/**
* Specifies font weight of a label
*
Expand All @@ -26,6 +48,20 @@ export class Label extends FASTElement {
@attr
public weight?: LabelWeight;

/**
* Handles changes to weight attribute custom states
* @param prev - the previous state
* @param next - the next state
*/
public weightChanged(prev: LabelWeight | undefined, next: LabelWeight | undefined) {
if (prev) {
toggleState(this.elementInternals, `${prev}`, false);
}
if (next) {
toggleState(this.elementInternals, `${next}`, true);
}
}

/**
* Specifies styles for label when associated input is disabled
*
Expand All @@ -36,6 +72,15 @@ export class Label extends FASTElement {
@attr({ mode: 'boolean' })
public disabled: boolean = false;

/**
* Handles changes to disabled attribute custom states
* @param prev - the previous state
* @param next - the next state
*/
public disabledChanged(prev: boolean | undefined, next: boolean | undefined) {
toggleState(this.elementInternals, 'disabled', next);
}

/**
* Specifies styles for label when associated input is a required field
*
Expand Down
Loading