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(input): add file type to the allowed types #1613

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
9 changes: 7 additions & 2 deletions src/components/input/input-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ export abstract class IgcInputBaseComponent extends FormAssociatedRequiredMixin(
};
}

protected renderFileParts(): TemplateResult | typeof nothing {
return nothing;
}

/** Sets the text selection range of the control */
public setSelectionRange(
start: number,
Expand Down Expand Up @@ -175,7 +179,7 @@ export abstract class IgcInputBaseComponent extends FormAssociatedRequiredMixin(
})}
>
<div part="start">${this.renderPrefix()}</div>
${this.renderInput()}
${this.renderInput()} ${this.renderFileParts()}
<div part="notch">${this.renderLabel()}</div>
<div part="filler"></div>
<div part="end">${this.renderSuffix()}</div>
Expand All @@ -187,7 +191,8 @@ export abstract class IgcInputBaseComponent extends FormAssociatedRequiredMixin(
private renderStandard() {
return html`${this.renderLabel()}
<div part=${partNameMap(this.resolvePartNames('container'))}>
${this.renderPrefix()} ${this.renderInput()} ${this.renderSuffix()}
${this.renderPrefix()} ${this.renderFileParts()} ${this.renderInput()}
${this.renderSuffix()}
</div>
${this.renderValidatorContainer()}`;
}
Expand Down
102 changes: 102 additions & 0 deletions src/components/input/input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,66 @@ describe('Input component', () => {
expect(input.value).to.be.empty;
});

it('sets the multiple property when input is of type file', async () => {
await createFixture(html`<igc-input type="file" multiple></igc-input>`);

expect(element.multiple).to.equal(true);
expect(input.multiple).to.equal(true);

element.multiple = false;
await elementUpdated(element);

expect(element.multiple).to.equal(false);
expect(input.multiple).to.equal(false);
});

it('sets the accept property when input is of type file', async () => {
await createFixture(
html`<igc-input type="file" accept="image/*"></igc-input>`
);

expect(element.accept).to.equal('image/*');
expect(input.accept).to.equal('image/*');

element.accept = '';
await elementUpdated(element);

expect(element.accept).to.be.empty;
expect(input.accept).to.be.empty;
});

it('returns the uploaded files when input is of type file', async () => {
await createFixture(html`<igc-input type="file"></igc-input>`);

const eventSpy = spy(element, 'emitEvent');
const file = new File(['test content'], 'test.txt', {
type: 'text/plain',
});
const fileList = {
0: file,
length: 1,
item: (index: number) => (index === 0 ? file : null),
};
const nativeInput = element.shadowRoot!.querySelector(
'input[type="file"]'
) as HTMLInputElement;

Object.defineProperty(nativeInput, 'files', {
value: fileList,
writable: true,
});

nativeInput!.dispatchEvent(new Event('change', { bubbles: true }));

await elementUpdated(element);

expect(eventSpy).calledOnceWith('igcChange');
expect(element.files).to.exist;
expect(element.files!.length).to.equal(1);
expect(element.files![0].name).to.equal('test.txt');
expect(element.files).to.deep.equal(nativeInput.files);
});

it('issue #1026 - passing undefined sets the underlying input value to undefined', async () => {
await createFixture(html`<igc-input value="a"></igc-input>`);

Expand Down Expand Up @@ -712,4 +772,46 @@ describe('Input component', () => {
runValidationContainerTests(IgcInputComponent, testParameters);
});
});

describe('File type layout', () => {
it('renders publicly documented parts when the input is of type file', async () => {
await createFixture(html`<igc-input type="file"></igc-input>`);

expect(
element.shadowRoot!.querySelector('div[part="file-selector-button"]')
).to.exist;
expect(element.shadowRoot!.querySelector('div[part="file-names"]')).to
.exist;

element.type = 'text';
await elementUpdated(element);

expect(
element.shadowRoot!.querySelector('div[part="file-selector-button"]')
).not.to.exist;
expect(element.shadowRoot!.querySelector('div[part="file-names"]')).not.to
.exist;
});

it('renders slotted contents when the input is of type file', async () => {
await createFixture(html`
<igc-input type="file">
<span slot="file-selector-text">Upload</span>
<span slot="file-missing-text">Choose a file</span>
</igc-input>
`);

const selectorSlot = element.shadowRoot!.querySelector(
'slot[name="file-selector-text"]'
) as HTMLSlotElement;
const missingSlot = element.shadowRoot!.querySelector(
'slot[name="file-missing-text"]'
) as HTMLSlotElement;

expect(selectorSlot!.assignedNodes()[0].textContent).to.equal('Upload');
expect(missingSlot!.assignedNodes()[0].textContent).to.equal(
'Choose a file'
);
});
});
});
62 changes: 61 additions & 1 deletion src/components/input/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { property } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';

import IgcButtonComponent from '../button/button.js';
import { registerComponent } from '../common/definitions/register.js';
import {
type FormValue,
Expand All @@ -20,6 +21,8 @@ import { numberValidators, stringValidators } from './validators.js';
* @slot prefix - Renders content before the input.
* @slot suffix - Renders content after input.
* @slot helper-text - Renders content below the input.
* @slot file-selector-text - Renders content for the browse button when input type is file.
* @slot file-missing-text - Renders content when input type is file and no file is chosen.
* @slot value-missing - Renders content when the required validation fails.
* @slot type-mismatch - Renders content when the a type url/email input pattern validation fails.
* @slot pattern-mismatch - Renders content when the pattern validation fails.
Expand All @@ -37,6 +40,8 @@ import { numberValidators, stringValidators } from './validators.js';
* @csspart container - The main wrapper that holds all main input elements.
* @csspart input - The native input element.
* @csspart label - The native label element.
* @csspart file-names - The file names wrapper when input type is 'file'.
* @csspart file-selector-button - The browse button when input type is 'file'.
* @csspart prefix - The prefix wrapper.
* @csspart suffix - The suffix wrapper.
* @csspart helper-text - The helper text wrapper.
Expand All @@ -46,7 +51,11 @@ export default class IgcInputComponent extends IgcInputBaseComponent {

/* blazorSuppress */
public static register() {
registerComponent(IgcInputComponent, IgcValidationContainerComponent);
registerComponent(
IgcInputComponent,
IgcValidationContainerComponent,
IgcButtonComponent
);
}

protected override _formValue: FormValue<string>;
Expand All @@ -62,6 +71,14 @@ export default class IgcInputComponent extends IgcInputBaseComponent {
private _pattern?: string;
private _step?: number;

private get _fileNames(): string | null {
if (!this.files || this.files.length === 0) return null;

return Array.from(this.files)
.map((file) => file.name)
.join(', ');
}

/* @tsTwoWayProperty(true, "igcChange", "detail", false) */
/**
* The value of the control.
Expand Down Expand Up @@ -90,8 +107,25 @@ export default class IgcInputComponent extends IgcInputBaseComponent {
| 'search'
| 'tel'
| 'text'
| 'file'
| 'url' = 'text';

/**
* The multiple attribute of the control.
* Used to indicate that a file input allows the user to select more than one file.
* @attr
*/
@property({ type: Boolean })
public multiple = false;

/**
* The accept attribute of the control.
* Defines the file types as a list of comma-separated values that the file input should accept.
* @attr
*/
@property({ type: String })
public accept = '';

/**
* The input mode attribute of the control.
* See [relevant MDN article](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode)
Expand Down Expand Up @@ -247,6 +281,11 @@ export default class IgcInputComponent extends IgcInputBaseComponent {
this.value = this.input.value;
}

public get files(): FileList | null {
if (this.type !== 'file' || !this.input) return null;
return this.input.files;
}

private handleInput() {
this.value = this.input.value;
this.emitEvent('igcInput', { detail: this.value });
Expand All @@ -265,11 +304,30 @@ export default class IgcInputComponent extends IgcInputBaseComponent {
this._validate();
}

protected override renderFileParts() {
if (this.type !== 'file') return nothing;

return html`
<div part="file-parts">
<div part="file-selector-button">
<igc-button variant="flat" ?disabled=${this.disabled} tabindex="-1">
<slot name="file-selector-text">Browse</slot>
</igc-button>
</div>
<div part="file-names">
${this._fileNames ??
html`<slot name="file-missing-text">No file chosen</slot>`}
</div>
</div>
`;
}

protected renderInput() {
return html`
<input
id=${this.inputId}
part=${partNameMap(this.resolvePartNames('input'))}
class="native-input"
name=${ifDefined(this.name)}
type=${ifDefined(this.type)}
pattern=${ifDefined(this.pattern)}
Expand All @@ -279,6 +337,7 @@ export default class IgcInputComponent extends IgcInputBaseComponent {
?disabled=${this.disabled}
?required=${this.required}
?autofocus=${this.autofocus}
?multiple=${this.type === 'file' && this.multiple}
tabindex=${this.tabIndex}
autocomplete=${ifDefined(this.autocomplete as any)}
inputmode=${ifDefined(this.inputMode)}
Expand All @@ -287,6 +346,7 @@ export default class IgcInputComponent extends IgcInputBaseComponent {
minlength=${ifDefined(this.minLength)}
maxlength=${ifDefined(this.validateOnly ? undefined : this.maxLength)}
step=${ifDefined(this.step)}
accept=${ifDefined(this.type !== 'file' ? undefined : this.accept)}
aria-invalid=${this.invalid ? 'true' : 'false'}
aria-describedby=${ifDefined(
isEmpty(this._helperText) ? nothing : 'helper-text'
Expand Down
Loading