Skip to content

Commit

Permalink
fix(field): only float when a label is present
Browse files Browse the repository at this point in the history
  • Loading branch information
samrichardsontylertech committed Feb 14, 2024
1 parent 25b9459 commit ccc1a44
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 22 deletions.
17 changes: 17 additions & 0 deletions src/lib/field-next/field-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FieldLabelPosition, FieldSlot, FIELD_CONSTANTS } from './field-constant

export interface IFieldAdapter extends IBaseAdapter {
addSlotChangeListener(slotName: FieldSlot, listener: () => void): void;
removeSlotChangeListener(slotName: FieldSlot, listener: () => void): void;
addPopoverIconClickListener(listener: () => void): void;
removePopoverIconClickListener(listener: () => void): void;
attachResizeContainer(): void;
Expand All @@ -20,6 +21,7 @@ export class FieldAdapter extends BaseAdapter<IFieldComponent> implements IField
private readonly _rootElement: HTMLElement;
private readonly _containerElement: HTMLElement;
private readonly _labelElement: HTMLElement;
private readonly _labelSlotElement: HTMLSlotElement;
private readonly _startSlotElement: HTMLSlotElement;
private readonly _endSlotElement: HTMLSlotElement;
private readonly _accessorySlotElement: HTMLSlotElement;
Expand All @@ -37,6 +39,7 @@ export class FieldAdapter extends BaseAdapter<IFieldComponent> implements IField
this._rootElement = getShadowElement(component, FIELD_CONSTANTS.selectors.ROOT);
this._containerElement = getShadowElement(component, FIELD_CONSTANTS.selectors.CONTAINER);
this._labelElement = getShadowElement(component, FIELD_CONSTANTS.selectors.LABEL);
this._labelSlotElement = getShadowElement(component, FIELD_CONSTANTS.selectors.LABEL_SLOT) as HTMLSlotElement;
this._startSlotElement = getShadowElement(component, FIELD_CONSTANTS.selectors.START_SLOT) as HTMLSlotElement;
this._endSlotElement = getShadowElement(component, FIELD_CONSTANTS.selectors.END_SLOT) as HTMLSlotElement;
this._accessorySlotElement = getShadowElement(component, FIELD_CONSTANTS.selectors.ACCESSORY_SLOT) as HTMLSlotElement;
Expand All @@ -47,6 +50,9 @@ export class FieldAdapter extends BaseAdapter<IFieldComponent> implements IField

public addSlotChangeListener(slotName: FieldSlot, listener: (evt: Event) => void): void {
switch (slotName) {
case 'label':
this._labelSlotElement.addEventListener('slotchange', listener);
break;
case 'start':
this._startSlotElement.addEventListener('slotchange', listener);
break;
Expand All @@ -65,6 +71,14 @@ export class FieldAdapter extends BaseAdapter<IFieldComponent> implements IField
}
}

public removeSlotChangeListener(slotName: FieldSlot, listener: (evt: Event) => void): void {
switch (slotName) {
case 'label':
this._labelSlotElement.removeEventListener('slotchange', listener);
break;
}
}

public addPopoverIconClickListener(listener: () => void): void {
this._popoverIconElement.addEventListener('click', listener);
}
Expand Down Expand Up @@ -149,6 +163,9 @@ export class FieldAdapter extends BaseAdapter<IFieldComponent> implements IField
*/
public handleSlotChange(slotName: FieldSlot): void {
switch (slotName) {
case 'label':
toggleClass(this._rootElement, !!this._labelSlotElement.assignedNodes().length, FIELD_CONSTANTS.classes.HAS_LABEL);
break;
case 'start':
toggleClass(this._rootElement, !!this._startSlotElement.assignedNodes().length, FIELD_CONSTANTS.classes.HAS_START);
break;
Expand Down
11 changes: 9 additions & 2 deletions src/lib/field-next/field-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const attributes = {
const classes = {
FLOATING_IN: 'forge-field--floating-in',
FLOATING_OUT: 'forge-field--floating-out',
HAS_LABEL: 'forge-field--has-label',
HAS_START: 'forge-field--has-start',
HAS_END: 'forge-field--has-end',
HAS_ACCESSORY: 'forge-field--has-accessory',
Expand All @@ -40,6 +41,7 @@ const selectors = {
ROOT: '#root',
CONTAINER: '#container',
LABEL: '#label',
LABEL_SLOT: 'slot[name=label]',
START_SLOT: 'slot[name=start]',
END_SLOT: 'slot[name=end]',
ACCESSORY_SLOT: 'slot[name=accessory]',
Expand Down Expand Up @@ -68,6 +70,10 @@ const animations = {
FLOAT_OUT_LABEL: 'float-out-label-animation'
};

const values = {
ANIMATION_TIMEOUT_DURATION: 1000
};

export const FIELD_CONSTANTS = {
elementName,
observedAttributes,
Expand All @@ -76,7 +82,8 @@ export const FIELD_CONSTANTS = {
selectors,
events,
defaults,
animations
animations,
values
};

export type FieldVariant = 'plain' | 'outlined' | 'tonal' | 'filled' | 'raised';
Expand All @@ -86,4 +93,4 @@ export type FieldDensity = Density | 'extra-small' | 'extra-large';
export type FieldLabelPosition = 'inline-start' | 'inline-end' | 'block-start' | 'inset' | 'none';
export type FieldLabelAlignment = 'default' | 'center' | 'baseline' | 'start' | 'end';
export type FieldSupportTextInset = 'start' | 'end' | 'both' | 'none';
export type FieldSlot = 'start' | 'end' | 'accessory' | 'support-start' | 'support-end';
export type FieldSlot = 'label' | 'start' | 'end' | 'accessory' | 'support-start' | 'support-end';
19 changes: 17 additions & 2 deletions src/lib/field-next/field-foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class FieldFoundation implements IFieldFoundation {
private _multiline = false;
private _supportTextInset: FieldSupportTextInset = FIELD_CONSTANTS.defaults.DEFAULT_SUPPORT_TEXT_INSET;

private _labelSlotListener: () => void;
private _startSlotListener: () => void;
private _endSlotListener: () => void;
private _accessorySlotListener: () => void;
Expand All @@ -49,6 +50,7 @@ export class FieldFoundation implements IFieldFoundation {
private _popoverIconClickListener: () => void;

constructor(private _adapter: IFieldAdapter) {
this._labelSlotListener = () => this._onSlotChange('label');
this._startSlotListener = () => this._onSlotChange('start');
this._endSlotListener = () => this._onSlotChange('end');
this._accessorySlotListener = () => this._onSlotChange('accessory');
Expand All @@ -64,6 +66,9 @@ export class FieldFoundation implements IFieldFoundation {
this._adapter.addSlotChangeListener('support-start', this._supportStartSlotListener);
this._adapter.addSlotChangeListener('support-end', this._supportEndSlotListener);
this._adapter.setLabelPosition(this._labelPosition);
if (this._labelPosition === 'inset') {
this._adapter.addSlotChangeListener('label', this._labelSlotListener);
}
if (this._popoverIcon) {
this._adapter.addPopoverIconClickListener(this._popoverIconClickListener);
}
Expand Down Expand Up @@ -95,9 +100,19 @@ export class FieldFoundation implements IFieldFoundation {
if (this._labelPosition !== value) {
this._labelPosition = value;
this._adapter.setHostAttribute(FIELD_CONSTANTS.attributes.LABEL_POSITION, this._labelPosition);
if (this._adapter.isConnected) {
this._adapter.setLabelPosition(this._labelPosition);

if (!this._adapter.isConnected) {
return;
}

this._adapter.setLabelPosition(this._labelPosition);

if (this._labelPosition === 'inset') {
this._adapter.addSlotChangeListener('label', this._labelSlotListener);
} else {
this._adapter.removeSlotChangeListener('label', this._labelSlotListener);
}
this._labelSlotListener();
}
}

Expand Down
24 changes: 12 additions & 12 deletions src/lib/field-next/field.scss
Original file line number Diff line number Diff line change
Expand Up @@ -322,16 +322,16 @@ $variants: (
//

:host(#{map.get($label-positions, inset)}[float-label]) {
.label {
@include core.floating-label;
}

.input {
@include core.floating-input;
}

.forge-field {
&--floating-in {
.forge-field--has-label {
.label {
@include core.floating-label;
}

.input {
@include core.floating-input;
}

&.forge-field--floating-in {
.label {
@include core.float-in-label;
}
Expand All @@ -352,7 +352,7 @@ $variants: (
}

:host(#{map.get($label-positions, inset)}:not([float-label])) {
.forge-field--floating-out {
.forge-field--has-label.forge-field--floating-out {
.label {
@include core.float-out-label;
}
Expand All @@ -364,7 +364,7 @@ $variants: (
}

:host(#{map.get($label-positions, inset)}[float-label][multiline]) {
.forge-field:not(.forge-field--floating-in) {
.forge-field--has-label:not(.forge-field--floating-in) {
::slotted(textarea) {
@include core.resize-container-slotted-floating-input;
}
Expand Down
121 changes: 115 additions & 6 deletions src/lib/field-next/field.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,38 +320,136 @@ describe('Field', () => {
describe('slots', () => {
it('should add class when start slot has content', async () => {
const harness = await createDefaultFixture();
harness.slotContent('start');
harness.addSlottedContent('start');
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_START)).to.be.true;
});

it('should not add class when start slot has no content', async () => {
const harness = await createDefaultFixture();
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_START)).to.be.false;
});

it('should remove class when start slot content is removed', async () => {
const harness = await createDefaultFixture();
harness.addSlottedContent('start');
await tick();
harness.removeSlottedContent('start');
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_START)).to.be.false;
});

it('should add class when end slot has content', async () => {
const harness = await createDefaultFixture();
harness.slotContent('end');
harness.addSlottedContent('end');
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_END)).to.be.true;
});

it('should not add class when end slot has no content', async () => {
const harness = await createDefaultFixture();
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_END)).to.be.false;
});

it('should remove class when end slot content is removed', async () => {
const harness = await createDefaultFixture();
harness.addSlottedContent('end');
await tick();
harness.removeSlottedContent('end');
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_END)).to.be.false;
});

it('should add class when accessory slot has content', async () => {
const harness = await createDefaultFixture();
harness.slotContent('accessory');
harness.addSlottedContent('accessory');
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_ACCESSORY)).to.be.true;
});

it('should not add class when accessory slot has no content', async () => {
const harness = await createDefaultFixture();
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_ACCESSORY)).to.be.false;
});

it('should remove class when accessory slot content is removed', async () => {
const harness = await createDefaultFixture();
harness.addSlottedContent('accessory');
await tick();
harness.removeSlottedContent('accessory');
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_ACCESSORY)).to.be.false;
});

it('should add class when support text start slot has content', async () => {
const harness = await createDefaultFixture();
harness.slotContent('support-text-start');
harness.addSlottedContent('support-text-start');
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_SUPPORT_START)).to.be.true;
});

it('should not add class when support text start slot has no content', async () => {
const harness = await createDefaultFixture();
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_SUPPORT_START)).to.be.false;
});

it('should remove class when support text start slot content is removed', async () => {
const harness = await createDefaultFixture();
harness.addSlottedContent('support-text-start');
await tick();
harness.removeSlottedContent('support-text-start');
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_SUPPORT_START)).to.be.false;
});

it('should add class when support text end slot has content', async () => {
const harness = await createDefaultFixture();
harness.slotContent('support-text-end');
harness.addSlottedContent('support-text-end');
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_SUPPORT_END)).to.be.true;
});

it('should not add class when support text end slot has no content', async () => {
const harness = await createDefaultFixture();
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_SUPPORT_END)).to.be.false;
});

it('should remove class when support text end slot content is removed', async () => {
const harness = await createDefaultFixture();
harness.addSlottedContent('support-text-end');
await tick();
harness.removeSlottedContent('support-text-end');
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_SUPPORT_END)).to.be.false;
});

it('should add class when label slot has content and label position is inset', async () => {
const harness = await createFixture({ labelPosition: 'inset' });
harness.addSlottedContent('label');
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_LABEL)).to.be.true;
});

it('should not add class when label slot has content and label position is not inset', async () => {
const harness = await createFixture({ labelPosition: 'inline-start' });
harness.addSlottedContent('label');
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_LABEL)).to.be.false;
});

it('should remove class when label slot content is removed and label position is inset', async () => {
const harness = await createFixture({ labelPosition: 'inset' });
harness.addSlottedContent('label');
await tick();
harness.removeSlottedContent('label');
await tick();
expect(harness.rootElement.classList.contains(FIELD_CONSTANTS.classes.HAS_LABEL)).to.be.false;
});
});

describe('label position', () => {
Expand Down Expand Up @@ -412,6 +510,7 @@ describe('Field', () => {
const harness = await createFixture({ labelPosition: 'inset' });
const animationSpy = spy();
harness.rootElement.addEventListener('animationstart', animationSpy);
harness.addSlottedContent('label');
harness.element.floatLabel = true;

await tick();
Expand All @@ -423,6 +522,7 @@ describe('Field', () => {
const harness = await createFixture({ labelPosition: 'inset', floatLabel: true });
const animationSpy = spy();
harness.rootElement.addEventListener('animationstart', animationSpy);
harness.addSlottedContent('label');
harness.element.floatLabel = false;

await tick();
Expand All @@ -434,6 +534,7 @@ describe('Field', () => {
const harness = await createFixture({ labelPosition: 'inset' });
const animationSpy = spy();
harness.rootElement.addEventListener('animationstart', animationSpy);
harness.addSlottedContent('label');
harness.element.floatLabelWithoutAnimation(true);

await tick();
Expand All @@ -445,6 +546,7 @@ describe('Field', () => {
const harness = await createFixture({ labelPosition: 'inset', floatLabel: true });
const animationSpy = spy();
harness.rootElement.addEventListener('animationstart', animationSpy);
harness.addSlottedContent('label');
harness.element.floatLabelWithoutAnimation(false);

await tick();
Expand Down Expand Up @@ -482,11 +584,18 @@ class FieldHarness extends TestHarness<IFieldComponent> {
]});
}

public slotContent(slotName: string): void {
public addSlottedContent(slotName: string): void {
const div = document.createElement('div');
div.slot = slotName;
this.element.appendChild(div);
}

public removeSlottedContent(slotName: string): void {
const el = this.element.querySelector(`[slot="${slotName}"]`);
if (el) {
el.remove();
}
}
}

interface FieldFixtureConfig {
Expand Down

0 comments on commit ccc1a44

Please sign in to comment.