|
| 1 | +import { html, nothing } from 'lit'; |
| 2 | +import { property } from 'lit/decorators.js'; |
| 3 | +import { ifDefined } from 'lit/directives/if-defined.js'; |
| 4 | +import { live } from 'lit/directives/live.js'; |
| 5 | + |
| 6 | +import { themes } from '../../theming/theming-decorator.js'; |
| 7 | +import IgcButtonComponent from '../button/button.js'; |
| 8 | +import { registerComponent } from '../common/definitions/register.js'; |
| 9 | +import { |
| 10 | + type FormValue, |
| 11 | + createFormValueState, |
| 12 | +} from '../common/mixins/forms/form-value.js'; |
| 13 | +import { isEmpty, partNameMap } from '../common/util.js'; |
| 14 | +import { IgcInputBaseComponent } from '../input/input-base.js'; |
| 15 | +import IgcValidationContainerComponent from '../validation-container/validation-container.js'; |
| 16 | +import { styles } from './themes/base.css.js'; |
| 17 | +import { all } from './themes/themes.js'; |
| 18 | +import { fileValidators } from './validators.js'; |
| 19 | + |
| 20 | +/** |
| 21 | + * @element igc-file-input |
| 22 | + * |
| 23 | + * @slot prefix - Renders content before the input. |
| 24 | + * @slot suffix - Renders content after input. |
| 25 | + * @slot helper-text - Renders content below the input. |
| 26 | + * @slot file-selector-text - Renders content for the browse button when input type is file. |
| 27 | + * @slot file-missing-text - Renders content when input type is file and no file is chosen. |
| 28 | + * @slot value-missing - Renders content when the required validation fails. |
| 29 | + * @slot custom-error - Renders content when setCustomValidity(message) is set. |
| 30 | + * @slot invalid - Renders content when the component is in invalid state (validity.valid = false). |
| 31 | + * |
| 32 | + * @fires igcInput - Emitted when the control input receives user input. |
| 33 | + * @fires igcChange - Emitted when the control's checked state changes. |
| 34 | + * @fires igcCancel - Emitted when the control's file picker dialog is canceled. |
| 35 | + * |
| 36 | + * @csspart container - The main wrapper that holds all main input elements. |
| 37 | + * @csspart input - The native input element. |
| 38 | + * @csspart label - The native label element. |
| 39 | + * @csspart file-names - The file names wrapper when input type is 'file'. |
| 40 | + * @csspart file-selector-button - The browse button when input type is 'file'. |
| 41 | + * @csspart prefix - The prefix wrapper. |
| 42 | + * @csspart suffix - The suffix wrapper. |
| 43 | + * @csspart helper-text - The helper text wrapper. |
| 44 | + */ |
| 45 | +@themes(all, { exposeController: true }) |
| 46 | +export default class IgcFileInputComponent extends IgcInputBaseComponent { |
| 47 | + public static readonly tagName = 'igc-file-input'; |
| 48 | + public static override styles = [...super.styles, styles]; |
| 49 | + |
| 50 | + /* blazorSuppress */ |
| 51 | + public static register() { |
| 52 | + registerComponent( |
| 53 | + IgcFileInputComponent, |
| 54 | + IgcValidationContainerComponent, |
| 55 | + IgcButtonComponent |
| 56 | + ); |
| 57 | + } |
| 58 | + |
| 59 | + protected override get __validators() { |
| 60 | + return fileValidators; |
| 61 | + } |
| 62 | + |
| 63 | + protected override _formValue: FormValue<string>; |
| 64 | + |
| 65 | + private get _fileNames(): string | null { |
| 66 | + if (!this.files || this.files.length === 0) return null; |
| 67 | + |
| 68 | + return Array.from(this.files) |
| 69 | + .map((file) => file.name) |
| 70 | + .join(', '); |
| 71 | + } |
| 72 | + |
| 73 | + /* @tsTwoWayProperty(true, "igcChange", "detail", false) */ |
| 74 | + /** |
| 75 | + * The value of the control. |
| 76 | + * @attr |
| 77 | + */ |
| 78 | + @property() |
| 79 | + public set value(value: string) { |
| 80 | + this._formValue.setValueAndFormState(value); |
| 81 | + this._validate(); |
| 82 | + } |
| 83 | + |
| 84 | + public get value(): string { |
| 85 | + return this._formValue.value; |
| 86 | + } |
| 87 | + |
| 88 | + /** |
| 89 | + * The multiple attribute of the control. |
| 90 | + * Used to indicate that a file input allows the user to select more than one file. |
| 91 | + * @attr |
| 92 | + */ |
| 93 | + @property({ type: Boolean }) |
| 94 | + public multiple = false; |
| 95 | + |
| 96 | + /** |
| 97 | + * The accept attribute of the control. |
| 98 | + * Defines the file types as a list of comma-separated values that the file input should accept. |
| 99 | + * @attr |
| 100 | + */ |
| 101 | + @property({ type: String }) |
| 102 | + public accept = ''; |
| 103 | + |
| 104 | + /** |
| 105 | + * The autofocus attribute of the control. |
| 106 | + * @attr |
| 107 | + */ |
| 108 | + @property({ type: Boolean }) |
| 109 | + public override autofocus!: boolean; |
| 110 | + |
| 111 | + /** |
| 112 | + * @internal |
| 113 | + */ |
| 114 | + @property({ type: Number }) |
| 115 | + public override tabIndex = 0; |
| 116 | + |
| 117 | + constructor() { |
| 118 | + super(); |
| 119 | + this._formValue = createFormValueState(this, { initialValue: '' }); |
| 120 | + } |
| 121 | + |
| 122 | + /** Returns the selected files when input type is 'file', otherwise returns null. */ |
| 123 | + public get files(): FileList | null { |
| 124 | + if (!this.input) return null; |
| 125 | + return this.input.files; |
| 126 | + } |
| 127 | + |
| 128 | + private handleInput() { |
| 129 | + this.value = this.input.value; |
| 130 | + this.emitEvent('igcInput', { detail: this.value }); |
| 131 | + } |
| 132 | + |
| 133 | + private handleChange() { |
| 134 | + this.value = this.input.value; |
| 135 | + this.emitEvent('igcChange', { detail: this.value }); |
| 136 | + } |
| 137 | + |
| 138 | + private handleCancel() { |
| 139 | + this.emitEvent('igcCancel', { |
| 140 | + detail: { |
| 141 | + message: 'User canceled the file selection dialog', |
| 142 | + value: this.value, |
| 143 | + }, |
| 144 | + }); |
| 145 | + } |
| 146 | + |
| 147 | + protected handleFocus(): void { |
| 148 | + this._dirty = true; |
| 149 | + } |
| 150 | + |
| 151 | + protected handleBlur(): void { |
| 152 | + this._validate(); |
| 153 | + } |
| 154 | + |
| 155 | + protected override renderFileParts() { |
| 156 | + const emptyText = this.placeholder ?? 'No file chosen'; |
| 157 | + |
| 158 | + return html` |
| 159 | + <div part="file-parts"> |
| 160 | + <div part="file-selector-button"> |
| 161 | + <igc-button variant="flat" ?disabled=${this.disabled} tabindex="-1"> |
| 162 | + <slot name="file-selector-text">Browse</slot> |
| 163 | + </igc-button> |
| 164 | + </div> |
| 165 | + <div part="file-names"> |
| 166 | + ${this._fileNames ?? |
| 167 | + html`<slot name="file-missing-text">${emptyText}</slot>`} |
| 168 | + </div> |
| 169 | + </div> |
| 170 | + `; |
| 171 | + } |
| 172 | + |
| 173 | + protected renderInput() { |
| 174 | + return html` |
| 175 | + <input |
| 176 | + id=${this.inputId} |
| 177 | + part=${partNameMap(this.resolvePartNames('input'))} |
| 178 | + name=${ifDefined(this.name)} |
| 179 | + type="file" |
| 180 | + .value=${live(this.value)} |
| 181 | + ?readonly=${this.readOnly} |
| 182 | + ?disabled=${this.disabled} |
| 183 | + ?required=${this.required} |
| 184 | + ?autofocus=${this.autofocus} |
| 185 | + ?multiple=${this.multiple} |
| 186 | + tabindex=${this.tabIndex} |
| 187 | + accept=${ifDefined(this.accept === '' ? undefined : this.accept)} |
| 188 | + aria-invalid=${this.invalid ? 'true' : 'false'} |
| 189 | + aria-describedby=${ifDefined( |
| 190 | + isEmpty(this._helperText) ? nothing : 'helper-text' |
| 191 | + )} |
| 192 | + @change=${this.handleChange} |
| 193 | + @input=${this.handleInput} |
| 194 | + @cancel=${this.handleCancel} |
| 195 | + @focus=${this.handleFocus} |
| 196 | + @blur=${this.handleBlur} |
| 197 | + /> |
| 198 | + `; |
| 199 | + } |
| 200 | +} |
| 201 | + |
| 202 | +declare global { |
| 203 | + interface HTMLElementTagNameMap { |
| 204 | + 'igc-file-input': IgcFileInputComponent; |
| 205 | + } |
| 206 | +} |
0 commit comments