Skip to content

Commit

Permalink
Merge pull request #527 from funidata/DS-248-spinner-v1
Browse files Browse the repository at this point in the history
[Loading Spinner]: New Component
  • Loading branch information
RiinaKuu authored Jan 13, 2025
2 parents dcb07b8 + e00bb0f commit 55681df
Show file tree
Hide file tree
Showing 117 changed files with 902 additions and 139 deletions.
1 change: 1 addition & 0 deletions ngx-fudis/.storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const preview = {
"Icon",
"Language Badge Group",
"Link",
"Loading Spinner",
"Notification",
"Section",
"Typography",
Expand Down
6 changes: 4 additions & 2 deletions ngx-fudis/projects/dev/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@
<fudis-grid-item [columns]="'3 / 4'" [alignSelfX]="'end'" [alignSelfY]="'end'">
<fudis-body-text>1 rem = {{ visibleRemValue }}px</fudis-body-text>
<fudis-body-text>Font size is {{ fontSize }}</fudis-body-text>
<fudis-loading-spinner class="fudis-mt-md" />
</fudis-grid-item>
<fudis-grid-item [columns]="'4 / -1'" [alignSelfX]="'end'">
<fudis-button [label]="'Change rem base'" (handleClick)="changeRemBase()"></fudis-button>
</fudis-grid-item>
</fudis-grid>
<fudis-grid [columns]="1" [classes]="'fudis-mt-xl'">
<fudis-heading [level]="1">Welcome to Fudis sandbox </fudis-heading>
<fudis-heading [level]="1">Welcome to Fudis sandbox</fudis-heading>
<fudis-expandable [title]="'Random stuff'" [level]="2">
<ng-template fudisExpandableContent>
<fudis-grid [classes]="'fudis-mt-md'">
<fudis-notification [variant]="'danger'"
><fudis-body-text
>Whoops! Here is danger notification<a
>Whoops! Here is danger notification
<a
fudisLink
[title]="t('notificationExternalLink')"
[external]="true"
Expand Down
3 changes: 2 additions & 1 deletion ngx-fudis/projects/dev/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgxFudisModule } from 'ngx-fudis';
import { NgxFudisModule, LoadingSpinnerComponent } from 'ngx-fudis';

import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { ScrollingModule } from '@angular/cdk/scrolling';
Expand Down Expand Up @@ -31,6 +31,7 @@ import { DialogTestFormComponent } from './dialog-test/dialog-test-content/dialo
NgxFudisModule,
ScrollingModule,
TranslocoRootModule,
LoadingSpinnerComponent,
RouterModule.forRoot([]),
],
providers: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@let loading = loadingState();
<div class="fudis-loading-spinner-demo fudis-p-md">
<div
class="fudis-loading-spinner-demo__backdrop"
[class.fudis-loading-spinner-demo__backdrop--visible]="loading"
[class.fudis-loading-spinner-demo__backdrop--hidden]="!loading"
>
<fudis-loading-spinner [variant]="'lg'" [visible]="loading"
/></div>
<fudis-grid [alignItemsX]="'center'" *ngIf="!loading">
<fudis-heading #headingRef tabindex="-1" [align]="'center'" [level]="1"
>Welcome to Loading Spinner Demo Page</fudis-heading
>
<fudis-body-text
>Click Button below to turn loading state on for 3 seconds. This will trigger also status
message for screen readers.</fudis-body-text
>
<fudis-body-text
>After 3s, focus handled by this demo component, will move shortly to Heading component. There
is small delay in focus, so that screen readers have some time to read out Loading Spinner's
changed status message.
</fudis-body-text>
<fudis-button [label]="'Set loading on!'" (handleClick)="toggleLoading()" />
</fudis-grid>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* stylelint-disable unit-disallowed-list */
@use '../../../foundations/borders/mixins.scss' as borders;
@use '../../../foundations/colors/mixins.scss' as colors;

.fudis-loading-spinner-demo {
@include borders.outline('3px', 'solid', 'primary');

display: flex;
position: relative;
align-items: center;
justify-content: center;
max-width: 30rem;
min-height: 15rem;

&__backdrop {
@include colors.bg-color('white');
@include borders.outline('3px', 'solid', 'green');

display: flex;
position: relative;
position: absolute;
align-items: center;
justify-content: center;
transition: 1s;
width: 100%;
height: 100%;

&--visible {
opacity: 1;
}

&--hidden {
opacity: 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Component, signal, ViewChild } from '@angular/core';

import { CommonModule } from '@angular/common';
import { LoadingSpinnerComponent } from '../loading-spinner.component';
import { NgxFudisModule } from '../../../ngx-fudis.module';
import { HeadingComponent } from '../../typography/heading/heading.component';

@Component({
standalone: true,
imports: [CommonModule, LoadingSpinnerComponent, NgxFudisModule],
selector: 'example-loading-spinner-demo',
styleUrl: './loading-spinner-example.component.scss',
templateUrl: './loading-spinner-example.component.html',
})
export class StorybookExampleLoadingSpinnerComponent {
loadingState = signal<boolean>(false);

@ViewChild('headingRef') public headingRef: HeadingComponent;

public toggleLoading(): void {
this.loadingState.set(true);

setTimeout(() => {
this.loadingState.set(false);

// Enough delay, so that screen reader has time to announce from Loading Spinner that page load is finished
setTimeout(() => {
this.headingRef.headingRef.nativeElement.focus();
}, 500);
}, 3000);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@let translations = _translationService.getTranslations()();

<div class="fudis-loading-spinner">
<p
*ngIf="variant === 'lg'"
class="fudis-loading-spinner__status fudis-visually-hidden"
role="status"
>{{
statusMessage ||
(visible
? translations.LOADING_SPINNER.PAGE_LOADING
: translations.LOADING_SPINNER.PAGE_LOAD_FINISHED)
}}</p
>
<div *ngIf="visible" class="fudis-loading-spinner__ui-content">
<svg
class="fudis-loading-spinner__svg fudis-loading-spinner__variant__{{ variant }}"
aria-hidden="true"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M15.2256 0.441641C12.7017 -0.262704 10.0165 -0.121557 7.58041 0.843527C5.14424 1.80861 3.09092 3.54458 1.73407 5.78628C0.377218 8.02798 -0.208579 10.6522 0.066151 13.2581C0.34088 15.864 1.46103 18.3084 3.25553 20.2179C5.05002 22.1274 7.4202 23.397 10.004 23.8328C12.5879 24.2687 15.2434 23.8468 17.565 22.6316L16.1133 19.8581C14.3974 20.7563 12.4346 21.0681 10.5247 20.746C8.61493 20.4239 6.86307 19.4855 5.5367 18.0741C4.21034 16.6628 3.3824 14.856 3.17934 12.9299C2.97627 11.0038 3.40925 9.06416 4.41214 7.40725C5.41504 5.75033 6.93271 4.46723 8.73335 3.75391C10.534 3.04059 12.5186 2.93626 14.3841 3.45686C16.2497 3.97747 17.8935 5.09438 19.0645 6.63702C20.2356 8.17965 20.8696 10.0632 20.8696 12H24C24 9.37963 23.1424 6.83129 21.5579 4.7442C19.9735 2.65711 17.7496 1.14599 15.2256 0.441641Z"
/>
</svg>

<fudis-body-text
[align]="'center'"
[variant]="variant === 'sm' ? 'md-regular' : 'lg-regular'"
>{{ label || translations.LOADING_SPINNER.VISIBLE_LABEL }}</fudis-body-text
>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
@use '../../foundations/colors/tokens.scss' as colors;
@use '../../foundations/spacing/tokens.scss' as spacing;

/* stylelint-disable-next-line unit-disallowed-list */
$lg-size: calc(3rem / var(--fudis-rem-multiplier)); // 48px
/* stylelint-disable-next-line unit-disallowed-list */
$max-width: calc(16rem / var(--fudis-rem-multiplier)); // 256px

.fudis-loading-spinner {
display: inline-block;
max-width: $max-width;

&__ui-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

&__variant {
&__sm {
margin-top: spacing.$spacing-xxs;
margin-bottom: spacing.$spacing-xs;
width: spacing.$spacing-md;
height: spacing.$spacing-md;
}

&__lg {
margin-bottom: spacing.$spacing-sm;
width: $lg-size;
height: $lg-size;
}
}

&__svg {
animation: spin 1.5s linear infinite;
fill: colors.$color-primary;
}
}

@keyframes spin {
0% {
transform: rotate(0deg);
}

100% {
transform: rotate(360deg);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoadingSpinnerComponent } from './loading-spinner.component';
import { getElement } from '../../utilities/tests/utilities';
import { NgxFudisModule } from '../../ngx-fudis.module';

describe('LoadingSpinnerComponent', () => {
let component: LoadingSpinnerComponent;
let fixture: ComponentFixture<LoadingSpinnerComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LoadingSpinnerComponent, NgxFudisModule],
}).compileComponents();

fixture = TestBed.createComponent(LoadingSpinnerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

const getParagraphLabel = (variant: string): HTMLParagraphElement => {
return getElement(
fixture,
`.fudis-loading-spinner .fudis-loading-spinner__ui-content .fudis-body-text__${variant}-regular`,
) as HTMLParagraphElement;
};

const getSvgIcon = (variant: string): HTMLOrSVGElement => {
return getElement(
fixture,
`svg.fudis-loading-spinner__svg.fudis-loading-spinner__variant__${variant}[aria-hidden="true"]`,
) as HTMLOrSVGElement;
};

const getStatusMessage = (): string | null => {
return (
getElement(
fixture,
'.fudis-loading-spinner__status.fudis-visually-hidden[role="status"]',
) as HTMLParagraphElement
)?.textContent;
};

describe('visual properties', () => {
const variants = [
{
variant: 'sm',
name: 'small (default)',
bodyTextVariant: 'md',
},
{
variant: 'lg',
name: 'large',
bodyTextVariant: 'lg',
},
];

variants.forEach((variant) => {
describe(`${variant.name} variant`, () => {
beforeEach(() => {
fixture.componentRef.setInput('variant', variant.variant);
fixture.detectChanges();
});

it('should have default Loading text if label is not provided', () => {
const labelText = getParagraphLabel(variant.bodyTextVariant);

expect(labelText.textContent).toEqual('Loading');
});

it('should have app provided label', async () => {
const appLabel = 'App provided label';

fixture.componentRef.setInput('label', appLabel);

fixture.detectChanges();

const labelText = getParagraphLabel(variant.bodyTextVariant);

expect(labelText.textContent).toEqual(appLabel);
});

it('should have correct svg icon', () => {
const svgElement = getSvgIcon(variant.variant);

expect(svgElement).toBeTruthy();
});

it('should not have visible elements, if visible is false', () => {
fixture.componentRef.setInput('visible', false);

fixture.detectChanges();

const uiContent = getElement(fixture, '.fudis-loading-spinner__ui-content');

expect(uiContent).toBeNull();
});
});
});
});

describe('screen reader elements', () => {
beforeEach(() => {
fixture.componentRef.setInput('variant', 'lg');
fixture.detectChanges();
});

it('should not be present with small variant', () => {
fixture.componentRef.setInput('variant', 'sm');
fixture.detectChanges();

const statusElement = getElement(fixture, '.fudis-loading-spinner__status');

expect(statusElement).toBeNull();
});

it('should be present with large variant', () => {
const statusElement = getElement(
fixture,
'.fudis-loading-spinner__status.fudis-visually-hidden[role="status"]',
);

expect(statusElement).toBeTruthy();
});

it('should have correct DEFAULT status message when visible is TRUE', () => {
expect(getStatusMessage()).toEqual('Page is loading');
});

it('should have correct DEFAULT status message when visible is FALSE', () => {
fixture.componentRef.setInput('visible', false);
fixture.detectChanges();
expect(getStatusMessage()).toEqual('Page load finished');
});

it('should have correct APP PROVIDED status message when visible is TRUE', async () => {
const appMessage = 'We need more loading!';

fixture.componentRef.setInput('statusMessage', appMessage);
fixture.detectChanges();

await fixture.whenStable();

fixture.detectChanges();

expect(getStatusMessage()).toEqual(appMessage);
});

it('should have correct APP PROVIDED status message when visible is FALSE', () => {
const appMessage = 'Enough is enough!';

fixture.componentRef.setInput('statusMessage', appMessage);
fixture.componentRef.setInput('visible', false);
fixture.detectChanges();
expect(getStatusMessage()).toEqual(appMessage);
});
});
});
Loading

0 comments on commit 55681df

Please sign in to comment.