Skip to content

Commit

Permalink
feat(chip-field): add new addOnBlur property/attribute (#408)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickonometry authored Oct 27, 2023
1 parent 03b92ab commit f2835dd
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 16 deletions.
1 change: 1 addition & 0 deletions src/dev/pages/chip-field/chip-field.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
{ type: 'switch', label: 'Invalid', id: 'opt-invalid' },
{ type: 'switch', label: 'Disabled', id: 'opt-disabled' },
{ type: 'switch', label: 'Dense', id: 'opt-dense' },
{ type: 'switch', label: 'Add on blur', id: 'opt-add-on-blur' },
{ type: 'button', label: 'Populate chips', id: 'opt-btn-populate' },
{ type: 'button', label: 'Clear chips', id: 'opt-btn-clear' },
]
Expand Down
6 changes: 6 additions & 0 deletions src/dev/pages/chip-field/chip-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ const requiredToggle = document.querySelector('#opt-required') as ISwitchCompone
const invalidToggle = document.querySelector('#opt-invalid') as ISwitchComponent;
const disabledToggle = document.querySelector('#opt-disabled') as ISwitchComponent;
const denseToggle = document.querySelector('#opt-dense') as ISwitchComponent;
const onBlurToggle = document.querySelector('#opt-add-on-blur') as ISwitchComponent;
const populateButton = document.querySelector('#opt-btn-populate') as HTMLButtonElement;
const clearButton = document.querySelector('#opt-btn-clear') as HTMLButtonElement;

requiredToggle.addEventListener('forge-switch-select', updateRequiredState);
invalidToggle.addEventListener('forge-switch-select', updateInvalidState);
disabledToggle.addEventListener('forge-switch-select', updateDisabledState);
denseToggle.addEventListener('forge-switch-select', updateDenseState);
onBlurToggle.addEventListener('forge-switch-select', updateOnBlurProperty);
populateButton.addEventListener('click', () => populateMembers(45));
clearButton.addEventListener('click', removeAllMembers);

Expand Down Expand Up @@ -180,6 +182,10 @@ function updateDenseState({ detail: isDense }: CustomEvent<boolean>): void {
chips.forEach(({ dense }) => dense = isDense);
}

function updateOnBlurProperty({ detail: addOnBlur }: CustomEvent<boolean>): void {
simpleChipField.addOnBlur = addOnBlur;
}

function setChipsDisabledState(isDisabled: boolean): void {
const chips = autocompleteChipField.querySelectorAll('forge-chip');
chips.forEach(({ disabled }) => disabled = isDisabled);
Expand Down
5 changes: 5 additions & 0 deletions src/lib/chip-field/chip-field-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ const events = {
MEMBER_REMOVED: `${elementName}-member-removed`
};

const attributes = {
ADD_ON_BLUR: 'add-on-blur'
};

export const CHIP_FIELD_CONSTANTS = {
attributes,
elementName,
classes,
slots,
Expand Down
29 changes: 26 additions & 3 deletions src/lib/chip-field/chip-field-foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { IChipFieldAdapter } from './chip-field-adapter';
import { CHIP_FIELD_CONSTANTS } from './chip-field-constants';
import { IFieldFoundation, FieldFoundation } from '../field/field-foundation';

export interface IChipFieldFoundation extends IFieldFoundation {}
export interface IChipFieldFoundation extends IFieldFoundation {
addOnBlur: boolean;
}

export class ChipFieldFoundation extends FieldFoundation implements IChipFieldFoundation {
private _addOnBlur = false;
private _memberSlotListener: () => void;
private _inputContainerMouseDownListener: (evt: MouseEvent) => void;
private _handleRootKeyDown: (event: KeyboardEvent) => void;
Expand Down Expand Up @@ -35,6 +38,18 @@ export class ChipFieldFoundation extends FieldFoundation implements IChipFieldFo
this._adapter.removeInputListener('keydown', this._handleKeyDown);
}

/** Controls adding a member of entered text on blur. */
public get addOnBlur(): boolean {
return this._addOnBlur;
}
public set addOnBlur(value: boolean) {
value = Boolean(value);
if (this._addOnBlur !== value) {
this._addOnBlur = value;
this._adapter.toggleHostAttribute(CHIP_FIELD_CONSTANTS.attributes.ADD_ON_BLUR, this._addOnBlur);
}
}

private _onInputContainerMouseDown(evt: MouseEvent): void {
evt.preventDefault();
this._adapter.focusInput();
Expand All @@ -43,6 +58,11 @@ export class ChipFieldFoundation extends FieldFoundation implements IChipFieldFo

protected _onBlur(event: FocusEvent): void {
const input = event.target as HTMLInputElement;

if (this._addOnBlur) {
this._addMember(input);
}

input.value = '';
super._onBlur(event);
}
Expand Down Expand Up @@ -79,9 +99,13 @@ export class ChipFieldFoundation extends FieldFoundation implements IChipFieldFo
break;
case 'Esc':
case 'Escape':
case 'Tab':
input.value = '';
break;
case 'Tab':
if (!this._addOnBlur) {
input.value = '';
}
break;
default:
break;
}
Expand Down Expand Up @@ -152,7 +176,6 @@ export class ChipFieldFoundation extends FieldFoundation implements IChipFieldFo
if (cleanInputValue && cleanInputValue.length > 0) {
this._adapter.emitHostEvent(CHIP_FIELD_CONSTANTS.events.MEMBER_ADDED, cleanInputValue);
}

input.value = '';
}

Expand Down
27 changes: 25 additions & 2 deletions src/lib/chip-field/chip-field.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CustomElement, attachShadowTemplate } from '@tylertech/forge-core';
import { CustomElement, FoundationProperty, attachShadowTemplate, coerceBoolean } from '@tylertech/forge-core';
import { ChipFieldAdapter } from './chip-field-adapter';
import { ChipFieldFoundation } from './chip-field-foundation';
import { CHIP_FIELD_CONSTANTS } from './chip-field-constants';
Expand All @@ -7,8 +7,11 @@ import { FieldComponent, IFieldComponent } from '../field/field';

import template from './chip-field.html';
import styles from './chip-field.scss';
import { FIELD_CONSTANTS } from '../field/field-constants';

export interface IChipFieldComponent extends IFieldComponent { }
export interface IChipFieldComponent extends IFieldComponent {
addOnBlur: boolean;
}

declare global {
interface HTMLElementTagNameMap {
Expand All @@ -31,9 +34,29 @@ declare global {
dependencies: [ChipComponent]
})
export class ChipFieldComponent extends FieldComponent<ChipFieldFoundation> implements IChipFieldComponent {
public static get observedAttributes(): string[] {
return [
...Object.values(FIELD_CONSTANTS.attributes),
CHIP_FIELD_CONSTANTS.attributes.ADD_ON_BLUR
];
}

constructor() {
super();
attachShadowTemplate(this, template, styles);
this._foundation = new ChipFieldFoundation(new ChipFieldAdapter(this));
}

public attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
switch (name) {
case CHIP_FIELD_CONSTANTS.attributes.ADD_ON_BLUR:
this.addOnBlur = coerceBoolean(newValue);
return;
}
super.attributeChangedCallback(name, oldValue, newValue);
}

/** Controls whether or not the value should be set onBlur */
@FoundationProperty()
public declare addOnBlur: boolean;
}
23 changes: 14 additions & 9 deletions src/lib/field/field-constants.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
const attributes = {
DENSITY: 'density',
FLOAT_LABEL_TYPE: 'float-label-type',
SHAPE: 'shape',
INVALID: 'invalid',
REQUIRED: 'required',
HOST_LABEL_FLOATING: `forge-label-floating`
};

const selectors = {
INPUT: 'input,textarea'
};
Expand All @@ -31,8 +22,22 @@ const classes = {
LABEL: 'forge-field--label'
};

const observedAttributes = {
DENSITY: 'density',
FLOAT_LABEL_TYPE: 'float-label-type',
SHAPE: 'shape',
INVALID: 'invalid',
REQUIRED: 'required',
HOST_LABEL_FLOATING: `forge-label-floating`
};

const attributes = {
...observedAttributes
};

export const FIELD_CONSTANTS = {
attributes,
observedAttributes,
observedInputAttributes,
selectors,
classes
Expand Down
10 changes: 9 additions & 1 deletion src/stories/src/components/chip-field/chip-field-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface IChipFieldProps {
hasLeading: boolean;
hasTrailing: boolean;
hasAddonEnd: boolean;
addOnBlur: boolean;
}

export const argTypes = {
Expand Down Expand Up @@ -129,5 +130,12 @@ export const argTypes = {
table: {
category: 'Slots',
},
}
},
addOnBlur: {
control: 'boolean',
description: '',
table: {
category: 'Properties',
},
},
};
8 changes: 8 additions & 0 deletions src/stories/src/components/chip-field/chip-field.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ Valid values: `auto` (default), `always`.

</PropertyDef>

<PropertyDef name="addOnBlur" type="boolean" defaultValue="false">

Controls the behavior of the blur event. When set to true, pressing tab or clicking away from the field will emit the add member event with the current text value.

Note: This property should only be used with a simple chip field, not with an autocomplete.

</PropertyDef>

</PageSection>

---
Expand Down
3 changes: 3 additions & 0 deletions src/stories/src/components/chip-field/chip-field.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const Default: Story<IChipFieldProps> = ({
hasTrailing = false,
hasHelperText = false,
hasAddonEnd = false,
addOnBlur = false
}) => {
const chipFieldRef = useRef<IChipFieldComponent>();
const addMember = (evt: CustomEvent) => {
Expand Down Expand Up @@ -68,6 +69,7 @@ export const Default: Story<IChipFieldProps> = ({
floatLabelType={floatLabelType}
shape={shape}
invalid={invalid}
addOnBlur={addOnBlur}
required={required}
style={{width: '559px'}}>

Expand Down Expand Up @@ -109,6 +111,7 @@ Default.args = {
hasTrailing: false,
hasHelperText: false,
hasAddonEnd: false,
addOnBlur: false
} as IChipFieldProps;

export const WithAutocomplete: Story<{}> = () => {
Expand Down
58 changes: 57 additions & 1 deletion src/test/spec/chip-field/chip-field.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ describe('ChipFieldComponent', function(this: ITestContext) {
expect(this.context.foundation['_isInitialized']).toBeTrue();
});

it('should set the addOnBlur property to false when the attribute is not applied', function(this: ITestContext) {
this.context = setupTestContext();

expect(this.context.component.addOnBlur).toBeFalse();
});

it('should set the addOnBlur property to true when the attribute is set to true', function(this: ITestContext) {
this.context = setupTestContext();
this.context.component.setAttribute(CHIP_FIELD_CONSTANTS.attributes.ADD_ON_BLUR, '');

expect(this.context.component.addOnBlur).toBe(true);
});

it('should float label if value is set before adding to DOM', async function(this: ITestContext) {
this.context = setupTestContext(false);
Expand Down Expand Up @@ -1192,8 +1204,52 @@ describe('ChipFieldComponent', function(this: ITestContext) {
const inputIsActive = getActiveElement() === getNativeInput(this.context.component);
expect(inputIsActive).toBeTrue();
});
});

it('should set the addOnBlur property to true when the attribute is set to true', function(this: ITestContext) {
this.context = setupTestContext();
this.context.component.setAttribute(CHIP_FIELD_CONSTANTS.attributes.ADD_ON_BLUR, '');

expect(this.context.component.addOnBlur).toBe(true);
});

it('should set the addOnBlur property to false when the attribute is set to false', function(this: ITestContext) {
this.context = setupTestContext();

this.context.component.addOnBlur = true;
expect(this.context.component.addOnBlur).toBeTrue();

this.context.component.setAttribute(CHIP_FIELD_CONSTANTS.attributes.ADD_ON_BLUR, 'false');
expect(this.context.component.addOnBlur).toBe(false);
});

it('chips should not be added when addOnBlur is set to false and the "Tab" key is pressed', function(this: ITestContext) {
this.context = setupTestContext();

expect(this.context.component.addOnBlur).toBeFalse();

const listener = jasmine.createSpy('add member listener');
this.context.component.addEventListener(CHIP_FIELD_CONSTANTS.events.MEMBER_ADDED, listener);

const inputEl = getNativeInput(this.context.component);
inputEl.focus();
inputEl.value = 'test';
dispatchKeydownEvent(inputEl, 'Tab');

expect(listener).toHaveBeenCalledTimes(0);
expect(inputEl.value).withContext('the input value should have been cleared').toBe('');
});

it('chips should be added when addOnBlur is set to true and the mouse is clicked outside of the input', function(this: ITestContext) {
this.context = setupTestContext();
this.context.component.setAttribute(CHIP_FIELD_CONSTANTS.attributes.ADD_ON_BLUR, 'true');
const listener = jasmine.createSpy('add member listener');
this.context.component.addEventListener(CHIP_FIELD_CONSTANTS.events.MEMBER_ADDED, listener);
getNativeInput(this.context.component).value = 'test';
getNativeInput(this.context.component).focus();
getNativeInput(this.context.component).blur();
expect(listener).toHaveBeenCalledTimes(1)
});
});
});

describe('With no label', function(this: ITestContext) {
Expand Down

0 comments on commit f2835dd

Please sign in to comment.