diff --git a/api-generator/api-generator.js b/api-generator/api-generator.js index 827f2306..24e6e329 100644 --- a/api-generator/api-generator.js +++ b/api-generator/api-generator.js @@ -136,26 +136,30 @@ async function main() { }; component_props_group.children.forEach((prop) => { + const rawType = + prop.getSignature && prop.getSignature.type + ? prop.getSignature.type.toString() + : prop.type + ? prop.type.toString() + : null; + const isSignalInput = rawType?.startsWith('InputSignal<'); props.values.push({ name: prop.name, optional: prop.flags.isOptional, readonly: prop.flags.isReadonly, - type: - prop.getSignature && prop.getSignature.type - ? prop.getSignature.type.toString() - : prop.type - ? prop.type.toString() - : null, + type: unwrapSignalType(rawType), default: (prop.type && prop.type.name === 'boolean' && !prop.defaultValue ? 'false' - : prop.defaultValue + : prop.defaultValue && + !(isSignalInput && prop.defaultValue === '...') ? prop.defaultValue.replace(/^'|'$/g, '') : undefined) ?? getDefaultValue(prop.setSignature) ?? - getDefaultValue(prop.getSignature), + getDefaultValue(prop.getSignature) ?? + getDefaultValue(prop), description: ( prop.getSignature?.comment?.summary || prop.setSignature?.comment?.summary || @@ -622,6 +626,12 @@ function extractParameter(emitter) { } } +const unwrapSignalType = (type) => { + if (!type) return type; + const match = type.match(/^InputSignal<(.+)>$/); + return match ? match[1] : type; +}; + const isProcessable = (value) => { return value && value.children && value.children.length; }; diff --git a/playwright/cps-accessibility.spec.ts b/playwright/cps-accessibility.spec.ts index 82f78fb6..06cfad90 100644 --- a/playwright/cps-accessibility.spec.ts +++ b/playwright/cps-accessibility.spec.ts @@ -56,7 +56,7 @@ const components: ComponentEntry[] = [ // name: 'Expansion panel', // selector: 'cps-expansion-panel' // }, - // { route: '/file-upload', name: 'File upload', selector: 'cps-file-upload' }, + { route: '/file-upload', name: 'File upload', selector: 'cps-file-upload' }, // { route: '/icon', name: 'Icon', selector: 'cps-icon' }, // { route: '/info-circle', name: 'Info circle', selector: 'cps-info-circle' }, // { route: '/input', name: 'Input', selector: 'cps-input' }, diff --git a/projects/composition/src/app/api-data/cps-divider.json b/projects/composition/src/app/api-data/cps-divider.json index 99d895fa..5c6ca081 100644 --- a/projects/composition/src/app/api-data/cps-divider.json +++ b/projects/composition/src/app/api-data/cps-divider.json @@ -9,32 +9,32 @@ "name": "vertical", "optional": false, "readonly": false, - "type": "InputSignal", - "default": "...", + "type": "boolean", + "default": "false", "description": "Determines whether the divider is vertically aligned." }, { "name": "color", "optional": false, "readonly": false, - "type": "InputSignal", - "default": "...", + "type": "string", + "default": "line-mid", "description": "Color of the divider." }, { "name": "type", "optional": false, "readonly": false, - "type": "InputSignal", - "default": "...", + "type": "CpsDividerType", + "default": "solid", "description": "Type of the divider." }, { "name": "thickness", "optional": false, "readonly": false, - "type": "InputSignal", - "default": "...", + "type": "string | number", + "default": "1px", "description": "Thickness of the divider, a number denoting pixels or a string." } ] diff --git a/projects/composition/src/app/api-data/cps-file-upload.json b/projects/composition/src/app/api-data/cps-file-upload.json index c0bbedd5..d442e426 100644 --- a/projects/composition/src/app/api-data/cps-file-upload.json +++ b/projects/composition/src/app/api-data/cps-file-upload.json @@ -22,12 +22,12 @@ "description": "Expected file description. E.g. 'Word document'." }, { - "name": "width", + "name": "ariaLabel", "optional": false, "readonly": false, - "type": "string | number", - "default": "100%", - "description": "Width of the component, a number denoting pixels or a string." + "type": "string", + "default": "Upload file", + "description": "Aria label for the component, used for accessibility." }, { "name": "fileInfo", @@ -37,6 +37,14 @@ "default": "", "description": "Expected file info block, explaining some extra stuff about file." }, + { + "name": "disabled", + "optional": false, + "readonly": false, + "type": "boolean", + "default": "false", + "description": "Whether the component is disabled." + }, { "name": "fileProcessingCallback", "optional": true, @@ -60,6 +68,14 @@ "type": "number", "default": "12", "description": "File name tooltip offset for styling." + }, + { + "name": "width", + "optional": false, + "readonly": false, + "type": "string | number", + "default": "100%", + "description": "Width of the component, a number denoting pixels or a string." } ] }, @@ -86,6 +102,36 @@ ], "description": "Callback to invoke when file upload fails." }, + { + "name": "fileProcessed", + "parameters": [ + { + "name": "value", + "type": "File" + } + ], + "description": "Callback to invoke when file is processed." + }, + { + "name": "fileProcessingFailed", + "parameters": [ + { + "name": "value", + "type": "string" + } + ], + "description": "Callback to invoke when file processing fails." + }, + { + "name": "fileProcessingCancelled", + "parameters": [ + { + "name": "value", + "type": "string" + } + ], + "description": "Callback to invoke when file processing is cancelled." + }, { "name": "uploadedFileRemoved", "parameters": [ diff --git a/projects/composition/src/app/pages/divider-page/divider-page.component.scss b/projects/composition/src/app/pages/divider-page/divider-page.component.scss index a5577ad9..aac7a0ca 100644 --- a/projects/composition/src/app/pages/divider-page/divider-page.component.scss +++ b/projects/composition/src/app/pages/divider-page/divider-page.component.scss @@ -1,6 +1,6 @@ :host { .section-title { - color: var(--cps-color-calm); + color: var(--cps-color-depth); margin-top: 0; } diff --git a/projects/composition/src/app/pages/file-upload-page/file-upload-page.component.html b/projects/composition/src/app/pages/file-upload-page/file-upload-page.component.html index c005f6cd..fecb34c5 100644 --- a/projects/composition/src/app/pages/file-upload-page/file-upload-page.component.html +++ b/projects/composition/src/app/pages/file-upload-page/file-upload-page.component.html @@ -3,7 +3,7 @@

File upload component with the dependency on selected file extension

-
+


+
- -

File upload component with extra info

-
+
+

+ + +

+ +
+ +

Disabled file upload component

+

+ +
diff --git a/projects/composition/src/app/pages/file-upload-page/file-upload-page.component.scss b/projects/composition/src/app/pages/file-upload-page/file-upload-page.component.scss index e69de29b..09d9af55 100644 --- a/projects/composition/src/app/pages/file-upload-page/file-upload-page.component.scss +++ b/projects/composition/src/app/pages/file-upload-page/file-upload-page.component.scss @@ -0,0 +1,15 @@ +:host { + .section-title { + color: var(--cps-color-depth); + } + .section-title:first-child { + margin-top: 0; + } + .section-title, + .section-body { + margin-left: 0.5rem; + } + .section-body:last-child { + margin-bottom: 0.5rem; + } +} diff --git a/projects/composition/src/app/pages/file-upload-page/file-upload-page.component.ts b/projects/composition/src/app/pages/file-upload-page/file-upload-page.component.ts index d7fb9387..27cf5a73 100644 --- a/projects/composition/src/app/pages/file-upload-page/file-upload-page.component.ts +++ b/projects/composition/src/app/pages/file-upload-page/file-upload-page.component.ts @@ -1,11 +1,12 @@ -import { Component } from '@angular/core'; +import { Component, ViewChild } from '@angular/core'; import { CpsFileUploadComponent, CpsButtonToggleComponent, CpsButtonToggleOption, - CpsDividerComponent + CpsDividerComponent, + CpsButtonComponent } from 'cps-ui-kit'; -import { Observable, catchError, from, map, of } from 'rxjs'; +import { Observable, catchError, delay, from, map, of } from 'rxjs'; import ComponentData from '../../api-data/cps-file-upload.json'; import { ComponentDocsViewerComponent } from '../../components/component-docs-viewer/component-docs-viewer.component'; @@ -14,6 +15,7 @@ import { ComponentDocsViewerComponent } from '../../components/component-docs-vi selector: 'app-file-upload-page', imports: [ CpsButtonToggleComponent, + CpsButtonComponent, CpsFileUploadComponent, ComponentDocsViewerComponent, CpsDividerComponent @@ -23,6 +25,8 @@ import { ComponentDocsViewerComponent } from '../../components/component-docs-vi host: { class: 'composition-page' } }) export class FileUploadPageComponent { + @ViewChild('fileUpload') fileUpload?: CpsFileUploadComponent; + componentData = ComponentData; fileUploadOptions: CpsButtonToggleOption[] = [ @@ -33,11 +37,14 @@ export class FileUploadPageComponent { selectedFileUploadType: CpsButtonToggleOption = this.fileUploadOptions[0]; + isDisabled = true; + fileInfo: string = 'The file should be a small sample file to infer the schema, which will be shown in the next step'; processUploadedFile(file: File): Observable { return from(file.text()).pipe( + delay(3000), map((fileContentsAsText) => { console.log(fileContentsAsText); return true; @@ -57,11 +64,24 @@ export class FileUploadPageComponent { console.log('File upload failed', fileName); } + onFileProcessed(file: File) { + console.log('File processed', file?.name); + } + + onFileProcessingFailed(fileName: string) { + console.log('File processing failed', fileName); + } + + onFileProcessingCancelled(fileName: string) { + console.log('File processing cancelled', fileName); + } + onUploadedFileRemoved(fileName: string) { console.log('File removed: ', fileName); } onFileExtensionChanged(event: string) { + this.fileUpload?.resetState(); const foundSelectedItem = this.fileUploadOptions.find( (item) => item.value === event ); @@ -69,4 +89,8 @@ export class FileUploadPageComponent { this.selectedFileUploadType = foundSelectedItem; } } + + toggleDisabled() { + this.isDisabled = !this.isDisabled; + } } diff --git a/projects/cps-ui-kit/src/lib/components/cps-divider/cps-divider.component.ts b/projects/cps-ui-kit/src/lib/components/cps-divider/cps-divider.component.ts index 184f1076..dfc6135b 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-divider/cps-divider.component.ts +++ b/projects/cps-ui-kit/src/lib/components/cps-divider/cps-divider.component.ts @@ -35,24 +35,28 @@ export class CpsDividerComponent { /** * Determines whether the divider is vertically aligned. * @group Props + * @default false */ vertical = input(false); /** * Color of the divider. * @group Props + * @default line-mid */ color = input('line-mid'); /** * Type of the divider. * @group Props + * @default solid */ type = input('solid'); /** * Thickness of the divider, a number denoting pixels or a string. * @group Props + * @default 1px */ thickness = input('1px'); diff --git a/projects/cps-ui-kit/src/lib/components/cps-file-upload/cps-file-upload.component.html b/projects/cps-ui-kit/src/lib/components/cps-file-upload/cps-file-upload.component.html index 42974e52..ff9c8b93 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-file-upload/cps-file-upload.component.html +++ b/projects/cps-ui-kit/src/lib/components/cps-file-upload/cps-file-upload.component.html @@ -1,23 +1,45 @@ -
-
+
+ @if (errorMessage) { + {{ errorMessage }} + } @else if (uploadedFile) { + @if (isProcessingFile) { + File is being processed + } @else { + File successfully uploaded + } + } +
+
+ + @if (errorMessage) { +
+ + {{ errorMessage }} +
+ } @if (uploadedFile) {
@@ -52,7 +86,9 @@ + [color]=" + disabled ? 'text-light' : isProcessingFile ? 'warn' : 'success' + ">
- @if (!isProcessingFile) { - - + @if (!disabled) { + @if (isProcessingFile) { + + + } @else { + + + } }
diff --git a/projects/cps-ui-kit/src/lib/components/cps-file-upload/cps-file-upload.component.scss b/projects/cps-ui-kit/src/lib/components/cps-file-upload/cps-file-upload.component.scss index 7a10425b..95a1f17c 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-file-upload/cps-file-upload.component.scss +++ b/projects/cps-ui-kit/src/lib/components/cps-file-upload/cps-file-upload.component.scss @@ -1,30 +1,58 @@ +@use '../../../../styles/mixins' as *; + $color-calm: var(--cps-color-calm); -$border-dashed-2px: 2px dashed $color-calm; -$border-dashed-1px: 1px dashed $color-calm; +$border-dashed-2px: 0.125rem dashed $color-calm; +$border-dashed-1px: 0.0625rem dashed $color-calm; $text-color-dark: var(--cps-color-text-dark); $text-color-darkest: var(--cps-color-text-darkest); +$highlight-hover: var(--cps-color-highlight-hover); +$highlight-selected: var(--cps-color-highlight-selected); +$color-error: var(--cps-color-error); +$color-error-darken1: var(--cps-color-error-darken1); +$text-color-mild: var(--cps-color-text-mild); +$border-color-disabled: var(--cps-color-line-darkest); +$bg-disabled: var(--cps-color-bg-disabled); :host { display: flex; .cps-file-upload { + min-width: 0; + position: relative; .cps-file-upload-dropzone { + background: transparent; font-family: 'Source Sans Pro', sans-serif; - min-height: 20px; + min-height: 1.25rem; padding: 2rem; text-align: center; border: $border-dashed-2px; position: relative; border-radius: 1rem; + display: block; + width: 100%; + box-sizing: border-box; + cursor: pointer; + user-select: none; &:hover { - background-color: var(--cps-color-highlight-hover); + background-color: $highlight-hover; + } + &:focus-visible { + @include focus-ring(0.25rem, 0.375rem, inherit); // already rem-based + } + &:focus-visible:not(.processing) { + @include focus-ring(0.25rem, 0.375rem, inherit); + background-color: $highlight-hover; + } + &:active:not(.processing) { + background-color: $highlight-selected; } - &:active { - background-color: var(--cps-color-highlight-selected); + &.processing { + pointer-events: none; + cursor: default; } &.dragged-over { - background-color: var(--cps-color-highlight-selected); + background-color: $highlight-selected; } - &.with-uploads { + &.with-bottom-section { border-bottom-right-radius: 0; border-bottom-left-radius: 0; border-bottom: $border-dashed-1px; @@ -34,32 +62,32 @@ $text-color-darkest: var(--cps-color-text-darkest); cursor: pointer; opacity: 0; position: absolute; + inset: 0; width: 100%; height: 100%; - top: 0; - left: 0; } .cps-file-upload-dropzone-title { - font-size: 16px; - margin: 8px 0; + font-size: 1rem; + margin: 0.5rem 0; color: $color-calm; font-weight: 600; } .cps-file-upload-dropzone-file-desc { - font-size: 18px; + font-size: 1.125rem; color: $text-color-dark; } .cps-file-upload-dropzone-content { - margin-top: 16px; + margin-top: 1rem; font-size: 1rem; + color: $text-color-dark; } .cps-file-upload-progress-bar { position: absolute; - bottom: -2px; + bottom: -0.125rem; left: 0; } } @@ -67,16 +95,16 @@ $text-color-darkest: var(--cps-color-text-darkest); border-bottom: $border-dashed-2px; border-left: $border-dashed-2px; border-right: $border-dashed-2px; - border-bottom-right-radius: 16px; - border-bottom-left-radius: 16px; - background-color: var(--cps-color-highlight-hover); + border-bottom-right-radius: 1rem; + border-bottom-left-radius: 1rem; + background-color: $highlight-hover; .cps-file-upload-uploaded-file { cursor: default; display: flex; align-items: center; justify-content: space-between; - padding: 8px 12px 8px 12px; + padding: 0.5rem 0.75rem; .cps-file-upload-uploaded-file-title { display: flex; align-items: center; @@ -84,25 +112,101 @@ $text-color-darkest: var(--cps-color-text-darkest); min-width: 0; .cps-file-upload-uploaded-file-name { color: $text-color-darkest; - font-size: 20px; - margin: 0 8px; + font-size: 1.25rem; + margin: 0 0.5rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - max-width: calc(100% - 60px); + max-width: calc(100% - 3.75rem); } } - .cps-file-upload-uploaded-file-remove-icon { + .cps-file-upload-uploaded-file-remove-icon, + .cps-file-upload-uploaded-file-cancel-process-icon { cursor: pointer; flex-shrink: 0; - margin-left: 8px; + margin-left: 0.5rem; + &:focus { + outline: none; + } + &:focus-visible { + @include focus-ring(); + } &:hover { ::ng-deep .cps-icon { - color: var(--cps-color-error-darken1) !important; + color: $color-error-darken1 !important; + } + } + } + } + } + + .cps-file-upload-error { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + overflow: hidden; + border-bottom: $border-dashed-2px; + border-left: $border-dashed-2px; + border-right: $border-dashed-2px; + border-bottom-right-radius: 1rem; + border-bottom-left-radius: 1rem; + background-color: $highlight-hover; + color: $color-error; + font-size: 1.25rem; + + span { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + &.disabled { + pointer-events: none; + .cps-file-upload-dropzone { + border-color: $border-color-disabled; + cursor: not-allowed; + .cps-file-upload-dropzone-title { + color: $text-color-mild; + } + .cps-file-upload-dropzone-file-desc { + color: $text-color-mild; + } + .cps-file-upload-dropzone-content { + color: $text-color-mild; + } + } + .cps-file-upload-uploaded-files { + border-color: $border-color-disabled; + background-color: $bg-disabled; + .cps-file-upload-uploaded-file { + .cps-file-upload-uploaded-file-title { + .cps-file-upload-uploaded-file-name { + color: $text-color-mild; } } } } + .cps-file-upload-error { + border-color: $border-color-disabled; + background-color: $bg-disabled; + color: $text-color-mild; + } + } + + .sr-only { + position: absolute; + width: 0.0625rem; + height: 0.0625rem; + padding: 0; + margin: -0.0625rem; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; } } } diff --git a/projects/cps-ui-kit/src/lib/components/cps-file-upload/cps-file-upload.component.ts b/projects/cps-ui-kit/src/lib/components/cps-file-upload/cps-file-upload.component.ts index f6520957..f5d8be56 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-file-upload/cps-file-upload.component.ts +++ b/projects/cps-ui-kit/src/lib/components/cps-file-upload/cps-file-upload.component.ts @@ -1,18 +1,23 @@ import { CommonModule } from '@angular/common'; import { + booleanAttribute, Component, + computed, ElementRef, EventEmitter, Input, + input, numberAttribute, OnChanges, + OnDestroy, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; -import { catchError, Observable, of, take } from 'rxjs'; +import { catchError, Observable, of, Subject, take, takeUntil } from 'rxjs'; import { convertSize } from '../../utils/internal/size-utils'; +import { focusElement } from '../../utils/internal/accessibility-utils'; import { CpsIconComponent } from '../cps-icon/cps-icon.component'; import { CpsTooltipDirective, @@ -35,7 +40,7 @@ import { CpsProgressLinearComponent } from '../cps-progress-linear/cps-progress- templateUrl: './cps-file-upload.component.html', styleUrls: ['./cps-file-upload.component.scss'] }) -export class CpsFileUploadComponent implements OnInit, OnChanges { +export class CpsFileUploadComponent implements OnInit, OnChanges, OnDestroy { /** * Expected extensions of a file to be uploaded. E.g. 'doc or .doc'. * @group Props @@ -49,10 +54,10 @@ export class CpsFileUploadComponent implements OnInit, OnChanges { @Input() fileDesc = 'Any file'; /** - * Width of the component, a number denoting pixels or a string. + * Aria label for the component, used for accessibility. * @group Props */ - @Input() width: number | string = '100%'; + @Input() ariaLabel = 'Upload file'; /** * Expected file info block, explaining some extra stuff about file. @@ -60,6 +65,12 @@ export class CpsFileUploadComponent implements OnInit, OnChanges { */ @Input() fileInfo: string = ''; + /** + * Whether the component is disabled. + * @group Props + */ + @Input({ transform: booleanAttribute }) disabled = false; + /** * Callback for uploaded file processing. * @group Props @@ -79,6 +90,13 @@ export class CpsFileUploadComponent implements OnInit, OnChanges { */ @Input({ transform: numberAttribute }) fileNameTooltipOffset: number = 12; + /** + * Width of the component, a number denoting pixels or a string. + * @group Props + * @default 100% + */ + width = input('100%'); + /** * Callback to invoke when file is uploaded. * @param {File} File @@ -93,6 +111,27 @@ export class CpsFileUploadComponent implements OnInit, OnChanges { */ @Output() fileUploadFailed = new EventEmitter(); + /** + * Callback to invoke when file is processed. + * @param {File} File + * @group Emits + */ + @Output() fileProcessed = new EventEmitter(); + + /** + * Callback to invoke when file processing fails. + * @param {string} - file name + * @group Emits + */ + @Output() fileProcessingFailed = new EventEmitter(); + + /** + * Callback to invoke when file processing is cancelled. + * @param {string} - file name + * @group Emits + */ + @Output() fileProcessingCancelled = new EventEmitter(); + /** * Callback to invoke when uploaded file is removed. * @param {string} - file name @@ -101,18 +140,27 @@ export class CpsFileUploadComponent implements OnInit, OnChanges { @Output() uploadedFileRemoved = new EventEmitter(); @ViewChild('fileInput') fileInput?: ElementRef; + @ViewChild('dropzoneButton') dropzoneButton?: ElementRef; isDragoverFile = false; uploadedFile?: File; extensionsString = ''; extensionsStringAsterisks = ''; - cvtWidth = ''; + cvtWidth = computed(() => convertSize(this.width())); isProcessingFile = false; + errorMessage = ''; + + private dragCounter = 0; + private readonly cancelProcessing$ = new Subject(); ngOnInit(): void { this.updateExtensionsString(); - this.cvtWidth = convertSize(this.width); + } + + ngOnDestroy(): void { + this.cancelProcessing$.next(); + this.cancelProcessing$.complete(); } ngOnChanges(changes: SimpleChanges): void { @@ -121,6 +169,48 @@ export class CpsFileUploadComponent implements OnInit, OnChanges { } } + resetState() { + this.cancelFileProcessing(); + this.errorMessage = ''; + this.dragCounter = 0; + this.isDragoverFile = false; + } + + openFilePicker(): void { + if (this.isProcessingFile) return; + this.fileInput?.nativeElement.click(); + } + + onDragEnter() { + this.dragCounter++; + this.isDragoverFile = true; + } + + onDragLeave() { + this.dragCounter--; + if (this.dragCounter <= 0) { + this.isDragoverFile = false; + this.dragCounter = 0; + } + } + + onDragEnd() { + this.dragCounter = 0; + this.isDragoverFile = false; + } + + onDragOver(event: DragEvent) { + event.preventDefault(); + this.isDragoverFile = true; + } + + onDrop(event: Event) { + event.preventDefault(); + this.dragCounter = 0; + this.isDragoverFile = false; + this.tryUploadFile(event); + } + updateExtensionsString(): void { this.extensions = this.extensions.map((ext) => ext.startsWith('.') ? ext.toLowerCase() : '.' + ext.toLowerCase() @@ -135,7 +225,10 @@ export class CpsFileUploadComponent implements OnInit, OnChanges { event.preventDefault(); event.stopPropagation(); + if (this.isProcessingFile) return; + this.isDragoverFile = false; + this.errorMessage = ''; let file: File | undefined; if (event.type === 'drop') { @@ -147,6 +240,7 @@ export class CpsFileUploadComponent implements OnInit, OnChanges { } if (!this._isFileExtensionValid(file)) { + this.errorMessage = 'Unsupported file type'; this.fileUploadFailed.emit(file?.name ?? ''); return; } @@ -159,25 +253,59 @@ export class CpsFileUploadComponent implements OnInit, OnChanges { this.fileProcessingCallback(this.uploadedFile) .pipe( take(1), + takeUntil(this.cancelProcessing$), catchError(() => { return of(false); }) ) .subscribe((res) => { - if (!res) this.removeUploadedFile(); this.isProcessingFile = false; + if (res) { + this.fileProcessed.emit(this.uploadedFile); + } else { + this.errorMessage = 'File processing failed'; + this.fileProcessingFailed.emit(this.uploadedFile?.name ?? ''); + this.removeUploadedFile(); + } }); } } } + onRemoveUploadedFile(event: Event) { + event.preventDefault(); + event.stopPropagation(); + this.removeUploadedFile(); + focusElement(this.dropzoneButton?.nativeElement); + } + + onCancelFileProcessing(event: Event) { + event.preventDefault(); + event.stopPropagation(); + this.cancelFileProcessing(); + focusElement(this.dropzoneButton?.nativeElement); + } + + cancelFileProcessing() { + this.cancelProcessing$.next(); + this.isProcessingFile = false; + const name = this.uploadedFile?.name; + if (name) { + this.fileProcessingCancelled.emit(name); + } + this.removeUploadedFile(); + } + removeUploadedFile() { - const name = this.uploadedFile?.name ?? ''; + const name = this.uploadedFile?.name; this.uploadedFile = undefined; - this.uploadedFileRemoved.emit(name); + if (name) { + this.uploadedFileRemoved.emit(name); + } - if (this.fileInput) { - this.fileInput.nativeElement.value = ''; + const inputEl = this.fileInput?.nativeElement; + if (inputEl) { + inputEl.value = ''; } } @@ -186,10 +314,7 @@ export class CpsFileUploadComponent implements OnInit, OnChanges { if (this.extensions.length < 1) return true; const fileNameLowerCase = file.name.toLowerCase(); - for (const ext of this.extensions) { - if (fileNameLowerCase.endsWith(ext)) return true; - } - return false; + return this.extensions.some((ext) => fileNameLowerCase.endsWith(ext)); } } diff --git a/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.html b/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.html index 2e9a96b1..e2cae6e7 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.html +++ b/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.html @@ -1,17 +1,15 @@
+ [style.max-width]="cvtWidth()" + [style.height]="cvtHeight()" + [style.border-radius]="cvtRadius()" + [style.background]="cssBgColor()">
+ [style.background]="cssColor()" + [style.opacity]="opacity()">
+ [style.background]="cssColor()" + [style.opacity]="opacity()">
diff --git a/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.spec.ts b/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.spec.ts index c4949746..4e3aa770 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.spec.ts +++ b/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.spec.ts @@ -20,66 +20,66 @@ describe('CpsProgressLinearComponent', () => { }); it('should have default values', () => { - expect(component.width).toBe('100%'); - expect(component.height).toBe('0.5rem'); - expect(component.color).toBeTruthy(); - expect(component.bgColor).toBeTruthy(); - expect(component.opacity).toBe(1); - expect(component.radius).toBe('0px'); + expect(component.width()).toBe('100%'); + expect(component.height()).toBe('0.5rem'); + expect(component.color()).toBeTruthy(); + expect(component.bgColor()).toBeTruthy(); + expect(component.opacity()).toBe(1); + expect(component.cvtRadius()).toBe('0px'); }); - it('should convert width on init', () => { - component.width = 200; - component.ngOnInit(); - expect(component.width).toBe('200px'); + it('should convert width to px', () => { + fixture.componentRef.setInput('width', 200); + fixture.detectChanges(); + expect(component.cvtWidth()).toBe('200px'); }); it('should keep width as string if already string', () => { fixture.componentRef.setInput('width', '50%'); fixture.detectChanges(); - expect(component.width).toBe('50%'); + expect(component.cvtWidth()).toBe('50%'); }); - it('should convert height on init', () => { - component.height = 10; - component.ngOnInit(); - expect(component.height).toBe('10px'); + it('should convert height to px', () => { + fixture.componentRef.setInput('height', 10); + fixture.detectChanges(); + expect(component.cvtHeight()).toBe('10px'); }); it('should keep height as string if already string', () => { fixture.componentRef.setInput('height', '1rem'); fixture.detectChanges(); - expect(component.height).toBe('1rem'); + expect(component.cvtHeight()).toBe('1rem'); }); - it('should convert radius on init', () => { - component.radius = 4; - component.ngOnInit(); - expect(component.radius).toBe('4px'); + it('should convert radius to px', () => { + fixture.componentRef.setInput('radius', 4); + fixture.detectChanges(); + expect(component.cvtRadius()).toBe('4px'); }); it('should keep radius as string if already string', () => { fixture.componentRef.setInput('radius', '0.25rem'); fixture.detectChanges(); - expect(component.radius).toBe('0.25rem'); + expect(component.cvtRadius()).toBe('0.25rem'); }); it('should set custom color', () => { fixture.componentRef.setInput('color', 'primary'); fixture.detectChanges(); - expect(component.color).toBeTruthy(); + expect(component.cssColor()).toBeTruthy(); }); it('should set custom background color', () => { fixture.componentRef.setInput('bgColor', 'line-light'); fixture.detectChanges(); - expect(component.bgColor).toBeTruthy(); + expect(component.cssBgColor()).toBeTruthy(); }); it('should set custom opacity', () => { fixture.componentRef.setInput('opacity', 0.5); fixture.detectChanges(); - expect(component.opacity).toBe(0.5); + expect(component.opacity()).toBe(0.5); }); it('should display progress bar', () => { diff --git a/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.ts b/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.ts index af4fe6d4..16b1de9f 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.ts +++ b/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.ts @@ -1,5 +1,5 @@ -import { CommonModule, DOCUMENT } from '@angular/common'; -import { Component, Inject, Input, OnInit } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Component, computed, inject, input } from '@angular/core'; import { getCSSColor } from '../../utils/colors-utils'; import { convertSize } from '../../utils/internal/size-utils'; @@ -8,57 +8,58 @@ import { convertSize } from '../../utils/internal/size-utils'; * @group Components */ @Component({ - imports: [CommonModule], selector: 'cps-progress-linear', templateUrl: './cps-progress-linear.component.html', styleUrls: ['./cps-progress-linear.component.scss'] }) -export class CpsProgressLinearComponent implements OnInit { +export class CpsProgressLinearComponent { /** * Width of the progress bar, of type number denoting pixels or string. * @group Props + * @default 100% */ - @Input() width: number | string = '100%'; + width = input('100%'); /** * Height of the progress bar, of type number denoting pixels or string. * @group Props + * @default 0.5rem */ - @Input() height: number | string = '0.5rem'; + height = input('0.5rem'); /** * Color of the progress bar. * @group Props + * @default var(--cps-accent-primary) */ - @Input() color = 'var(--cps-accent-primary)'; + color = input('var(--cps-accent-primary)'); /** * Background color of the progress bar. * @group Props + * @default white */ - @Input() bgColor = 'white'; + bgColor = input('white'); /** * Option to control the opacity of the progress bar. * @group Props + * @default 1 */ - @Input() opacity: number | string = 1; + opacity = input(1); /** * Border radius of the progress bar, of type number denoting pixels or string. * @group Props + * @default 0 */ - @Input() radius: number | string = 0; + radius = input(0); - // eslint-disable-next-line no-useless-constructor - constructor(@Inject(DOCUMENT) private document: Document) {} + private readonly document = inject(DOCUMENT); - ngOnInit(): void { - this.width = convertSize(this.width); - this.height = convertSize(this.height); - this.radius = convertSize(this.radius); - - this.color = getCSSColor(this.color, this.document); - this.bgColor = getCSSColor(this.bgColor, this.document); - } + cvtWidth = computed(() => convertSize(this.width())); + cvtHeight = computed(() => convertSize(this.height())); + cvtRadius = computed(() => convertSize(this.radius())); + cssColor = computed(() => getCSSColor(this.color(), this.document)); + cssBgColor = computed(() => getCSSColor(this.bgColor(), this.document)); } diff --git a/projects/cps-ui-kit/src/lib/utils/internal/accessibility-utils.ts b/projects/cps-ui-kit/src/lib/utils/internal/accessibility-utils.ts index fafe4f0c..d6d6e327 100644 --- a/projects/cps-ui-kit/src/lib/utils/internal/accessibility-utils.ts +++ b/projects/cps-ui-kit/src/lib/utils/internal/accessibility-utils.ts @@ -26,3 +26,9 @@ export const getComputedLabel = (context: { return parts.length > 0 ? parts.join(' ') : null; }; + +export const focusElement = (element?: HTMLElement): void => { + if (element) { + setTimeout(() => element.focus()); + } +};