Skip to content
This repository is currently being migrated. It's locked while the migration is in progress.
Draft
Show file tree
Hide file tree
Changes from 4 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
59 changes: 59 additions & 0 deletions packages/storybook/stories/va-loader.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useState } from 'react';
import { getWebComponentDocs, propStructure, StoryDocs } from './wc-helpers';

const loadingIndicatorDocs = getWebComponentDocs('va-loader');

export default {
title: 'Components/Loader',
id: 'components/va-loader',
parameters: {
componentSubtitle: 'va-loader web component',
docs: {
page: () => (
<StoryDocs storyDefault={Default} data={loadingIndicatorDocs} />
),
},
actions: {
handles: ['component-library-analytics'],
},
},
};

const defaultArgs = {
'message': 'Loading your application...',
'label': 'Loading',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

label and message are declared but not being used.

'set-focus': false,
'enable-analytics': false,
};

const Template = ({
'enable-analytics': enableAnalytics,
label,
message,
'set-focus': setFocus,
}) => {
const [isLoading, setIsLoading] = useState(true);
return (
<div>
{enableAnalytics && (
<button
className="vads-u-display--flex vads-u-margin-x--auto"
onClick={() => setIsLoading(false)}
>
Finish loading
</button>
)}
{isLoading && <va-loader />}
</div>
);
};

export const Default = Template.bind(null);
Default.args = { ...defaultArgs };
Default.argTypes = propStructure(loadingIndicatorDocs);

export const SetFocus = Template.bind(null);
SetFocus.args = { ...defaultArgs, 'set-focus': true };

export const EnableAnalytics = Template.bind(null);
EnableAnalytics.args = { ...defaultArgs, 'enable-analytics': true };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggeston (blocking): There is no enable-analytics property on the component so this story should not have been added. This could be an issue with the va-loader story too but even so, we don't want the same problem to be duplicated.

1 change: 1 addition & 0 deletions packages/storybook/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ declare global {
'va-link': WCTypes.VaLink & IsElement;
'va-link-action': WCTypes.VaLinkAction & IsElement;
'va-loading-indicator': WCTypes.VaLoadingIndicator & IsElement;
'va-loader': WCTypes.VaLoader & IsElement;
'va-maintenance-banner': WCTypes.VaMaintenanceBanner & IsElement;
'va-minimal-footer': WCTypes.VaMinimalFooter & IsElement;
'va-need-help': WCTypes.VaNeedHelp & IsElement;
Expand Down
81 changes: 81 additions & 0 deletions packages/web-components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,37 @@ export namespace Components {
*/
"type"?: 'primary' | 'secondary' | 'reverse' | 'primary-entry';
}
/**
* @componentName Loader
* @maturityCategory review
* @maturityLevel development
*/
interface VaLoader {
/**
* Custom left alignment
*/
"alignLeft"?: string;
/**
* Custom top alignment
*/
"alignTop"?: string;
/**
* The ARIA live region setting
*/
"ariaLiveRegion"?: string;
/**
* Whether the loader is currently busy (use 'true' or 'false' as string)
*/
"busy"?: string;
/**
* The text to display in the center of the loader
*/
"centerLabel"?: string;
/**
* The ARIA role for the loader
*/
"loaderRole"?: string;
}
/**
* @componentName Loading indicator
* @maturityCategory use
Expand Down Expand Up @@ -2867,6 +2898,17 @@ declare global {
prototype: HTMLVaLinkActionElement;
new (): HTMLVaLinkActionElement;
};
/**
* @componentName Loader
* @maturityCategory review
* @maturityLevel development
*/
interface HTMLVaLoaderElement extends Components.VaLoader, HTMLStencilElement {
}
var HTMLVaLoaderElement: {
prototype: HTMLVaLoaderElement;
new (): HTMLVaLoaderElement;
};
interface HTMLVaLoadingIndicatorElementEventMap {
"component-library-analytics": any;
}
Expand Down Expand Up @@ -3537,6 +3579,7 @@ declare global {
"va-language-toggle": HTMLVaLanguageToggleElement;
"va-link": HTMLVaLinkElement;
"va-link-action": HTMLVaLinkActionElement;
"va-loader": HTMLVaLoaderElement;
"va-loading-indicator": HTMLVaLoadingIndicatorElement;
"va-maintenance-banner": HTMLVaMaintenanceBannerElement;
"va-memorable-date": HTMLVaMemorableDateElement;
Expand Down Expand Up @@ -4653,6 +4696,37 @@ declare namespace LocalJSX {
*/
"type"?: 'primary' | 'secondary' | 'reverse' | 'primary-entry';
}
/**
* @componentName Loader
* @maturityCategory review
* @maturityLevel development
*/
interface VaLoader {
/**
* Custom left alignment
*/
"alignLeft"?: string;
/**
* Custom top alignment
*/
"alignTop"?: string;
/**
* The ARIA live region setting
*/
"ariaLiveRegion"?: string;
/**
* Whether the loader is currently busy (use 'true' or 'false' as string)
*/
"busy"?: string;
/**
* The text to display in the center of the loader
*/
"centerLabel"?: string;
/**
* The ARIA role for the loader
*/
"loaderRole"?: string;
}
/**
* @componentName Loading indicator
* @maturityCategory use
Expand Down Expand Up @@ -6015,6 +6089,7 @@ declare namespace LocalJSX {
"va-language-toggle": VaLanguageToggle;
"va-link": VaLink;
"va-link-action": VaLinkAction;
"va-loader": VaLoader;
"va-loading-indicator": VaLoadingIndicator;
"va-maintenance-banner": VaMaintenanceBanner;
"va-memorable-date": VaMemorableDate;
Expand Down Expand Up @@ -6241,6 +6316,12 @@ declare module "@stencil/core" {
* @guidanceName Action link
*/
"va-link-action": LocalJSX.VaLinkAction & JSXBase.HTMLAttributes<HTMLVaLinkActionElement>;
/**
* @componentName Loader
* @maturityCategory review
* @maturityLevel development
*/
"va-loader": LocalJSX.VaLoader & JSXBase.HTMLAttributes<HTMLVaLoaderElement>;
/**
* @componentName Loading indicator
* @maturityCategory use
Expand Down
124 changes: 124 additions & 0 deletions packages/web-components/src/components/va-loader/loader-generated.tsx
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (blocking): The name of this file should be name of the component va-loader.tsx which would follow the current component-library pattern.

Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Component, Element, Host, Prop, State, h, Watch } from '@stencil/core';

/**
* @componentName Loader
* @maturityCategory review
* @maturityLevel development
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (blocking): The maturityCategory and maturityLevel should follow these maturity scale values

*/

@Component({
tag: 'va-loader',
styleUrl: 'va-loader.css',
shadow: true,
})
export class VaLoader {
@Element() el!: HTMLElement;
private textRef?: HTMLDivElement;
private rotationInterval: number;

@State() rotation: number = 0;
@State() loaderText: string;

/**
* The text to display in the center of the loader
*/
@Prop() centerLabel?: string = 'Loading';

/**
* The ARIA role for the loader
*/
@Prop() loaderRole?: string = 'status';

/**
* The ARIA live region setting
*/
@Prop() ariaLiveRegion?: string;

/**
* Whether the loader is currently busy (use 'true' or 'false' as string)
*/
@Prop() busy?: string;

/**
* Custom left alignment
*/
@Prop() alignLeft?: string = '0px';

/**
* Custom top alignment
*/
@Prop() alignTop?: string = '0px';

componentWillLoad() {
this.loaderText = this.centerLabel;
}

componentDidLoad() {
this.startRotation();
this.adjustPosition();
}

disconnectedCallback() {
window.clearInterval(this.rotationInterval);
}

@Watch('rotation')
handleRotationChange() {
switch (this.rotation) {
case 0:
this.loaderText = this.centerLabel;
break;
case 90:
this.loaderText = `${this.centerLabel}.`;
break;
case 180:
this.loaderText = `${this.centerLabel}..`;
break;
case 270:
this.loaderText = `${this.centerLabel}...`;
break;
}
this.adjustPosition();
}

private startRotation() {
this.rotationInterval = window.setInterval(() => {
this.rotation = (this.rotation + 90) % 360;
}, 250);
}

private adjustPosition() {
if (this.textRef) {
const containsSpace = this.loaderText.includes(' ');
this.textRef.style.top = containsSpace ? '40%' : '45%';
this.textRef.style.left = containsSpace ? '35%' : '30%';
}
}

render() {
const spanStyle = {
left: this.alignLeft,
top: this.alignTop,
};

return (
<Host>
<div
class="vacds-loader"
role={this.loaderRole}
aria-live={this.ariaLiveRegion}
aria-busy={this.busy}
tabindex={0}
>
<div class="vacds-loader-border">
<span class="edge edge-left"></span>
<span class="edge edge-right"></span>
</div>
<div class="vacds-loader-text" ref={el => (this.textRef = el)}>
<span style={spanStyle}>{this.loaderText}</span>
</div>
</div>
</Host>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { newE2EPage } from '@stencil/core/testing';
import { axeCheck } from '../../../testing/test-helpers';

describe('va-loader', () => {
it('renders', async () => {
const page = await newE2EPage();
await page.setContent('<va-loader></va-loader>');

const element = await page.find('va-loader');
expect(element).not.toBeNull();

const loaderDiv = await page.find('va-loader >>> .vacds-loader');
expect(loaderDiv).not.toBeNull();
expect(await loaderDiv.getAttribute('role')).toBe('status');

const textDiv = await page.find('va-loader >>> .vacds-loader-text');
const text = await textDiv.getProperty('textContent');
expect(text).toBe('Loading');
});

it('renders with custom label', async () => {
const customLabel = 'Loading spinner';
const page = await newE2EPage();
await page.setContent(
`<va-loader center-label="${customLabel}"></va-loader>`,
);

const textDiv = await page.find('va-loader >>> .vacds-loader-text');
const text = await textDiv.getProperty('textContent');
expect(text).toBe(customLabel);
});

it('updates text based on rotation state', async () => {
const page = await newE2EPage();
await page.setContent('<va-loader></va-loader>');

// Initial state
let textDiv = await page.find('va-loader >>> .vacds-loader-text');
let text = await textDiv.getProperty('textContent');
expect(text).toBe('Loading');

// Wait for first rotation (250ms)
await page.waitForTimeout(250);
textDiv = await page.find('va-loader >>> .vacds-loader-text');
text = await textDiv.getProperty('textContent');
expect(text).toBe('Loading.');

// Wait for second rotation (250ms)
await page.waitForTimeout(250);
textDiv = await page.find('va-loader >>> .vacds-loader-text');
text = await textDiv.getProperty('textContent');
expect(text).toBe('Loading..');

// Wait for third rotation (250ms)
await page.waitForTimeout(250);
textDiv = await page.find('va-loader >>> .vacds-loader-text');
text = await textDiv.getProperty('textContent');
expect(text).toBe('Loading...');

// Wait for fourth rotation (250ms) - back to initial state
await page.waitForTimeout(250);
textDiv = await page.find('va-loader >>> .vacds-loader-text');
text = await textDiv.getProperty('textContent');
expect(text).toBe('Loading');
});

it('passes an axe check', async () => {
const page = await newE2EPage();
await page.setContent('<va-loader></va-loader>');

await axeCheck(page);
});
});
Loading
Loading