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

va-language-toggle: add component #1386

Merged
merged 17 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
65 changes: 65 additions & 0 deletions packages/storybook/stories/va-language-toggle.stories.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { Fragment } from 'react';
import { getWebComponentDocs, StoryDocs, propStructure } from './wc-helpers';
import { VaLanguageToggle } from '@department-of-veterans-affairs/web-components/react-bindings';

const languageToggleDocs = getWebComponentDocs('va-language-toggle');

export default {
title: 'Components/Language Toggle',
id: 'components/va-language-toggle',
parameters: {
componentSubtitle: 'va-language-toggle web component',
docs: {
page: () => <StoryDocs storyDefault={Default} data={languageToggleDocs} />,
},
},
};

const url = new URL(window.parent.location.href);
url.searchParams.set('path', '/docs/components-va-language-toggle--docs');

const defaultArgs = {
urls: [
{ "href": url.href, "lang": "en", "label": "English" },
{ "href": url.href, "lang": "es", "label": "Español" },
{ "href": url.href, "lang": "tl", "label": "Tagalog" }
],
}

const Template = ({ urls }) => {
let lang = sessionStorage.getItem('va-language-toggle-lang') ?? 'en';
function handleLanguageToggle(e) {
const { language } = e.detail;
sessionStorage.setItem('va-language-toggle-lang', language)
}

return (
<VaLanguageToggle language={lang} urls={urls} onVaLanguageToggle={handleLanguageToggle}/>
);
};

const WithRouterLinksTemplate = ({urls}) => {

function handleLanguageToggle(e) {
console.log(`the language has been toggled to ${e.detail.language}`);
}

return (
<Fragment>
<div>This example illustrates how to use the component with a router. When <code>router-links</code> is
set to <code>true</code>, clicking on a link will not navigate to a new page (i.e. <code>event.preventDefault()</code> is called).
By capturing the <code>language-toggle</code> event page content can be updated as needed to reflect the selected language.
</div>
<VaLanguageToggle urls={urls} routerLinks={true} onVaLanguageToggle={handleLanguageToggle}/>
</Fragment>
)
}

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

export const WithRouterLinks = WithRouterLinksTemplate.bind(null);
WithRouterLinks.args = { ...defaultArgs }
78 changes: 78 additions & 0 deletions packages/web-components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { Breadcrumb } from "./components/va-breadcrumbs/va-breadcrumbs";
import { LangUrl } from "./components/va-language-toggle/va-language-toggle";
export { Breadcrumb } from "./components/va-breadcrumbs/va-breadcrumbs";
export { LangUrl } from "./components/va-language-toggle/va-language-toggle";
export namespace Components {
/**
* @componentName Accordion
Expand Down Expand Up @@ -636,6 +638,25 @@ export namespace Components {
*/
"srtext"?: string;
}
/**
* @componentName Language Toggle
* @maturityCategory caution
* @maturityLevel candidate
*/
interface VaLanguageToggle {
/**
* The ISO language code for the page. Default is 'en'.
*/
"language": string;
/**
* If true, specifies that the toggle is being used on a page with a router and clicking on a link will not result in page navigation.
*/
"routerLinks"?: boolean;
/**
* A JSON array of objects with link data. Each object should have an href, lang (ISO language code), and label properties. If using the pure web component provide as a string. Example: `[{"href": "/one", "lang": "en", "label": "English"}, ...]`.
*/
"urls": LangUrl[] | string;
}
/**
* @componentName Link
* @maturityCategory caution
Expand Down Expand Up @@ -1821,6 +1842,10 @@ export interface VaFileInputMultipleCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLVaFileInputMultipleElement;
}
export interface VaLanguageToggleCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLVaLanguageToggleElement;
}
export interface VaLinkCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLVaLinkElement;
Expand Down Expand Up @@ -2326,6 +2351,28 @@ declare global {
prototype: HTMLVaIconElement;
new (): HTMLVaIconElement;
};
interface HTMLVaLanguageToggleElementEventMap {
"vaLanguageToggle": any;
}
/**
* @componentName Language Toggle
* @maturityCategory caution
* @maturityLevel candidate
*/
interface HTMLVaLanguageToggleElement extends Components.VaLanguageToggle, HTMLStencilElement {
addEventListener<K extends keyof HTMLVaLanguageToggleElementEventMap>(type: K, listener: (this: HTMLVaLanguageToggleElement, ev: VaLanguageToggleCustomEvent<HTMLVaLanguageToggleElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLVaLanguageToggleElementEventMap>(type: K, listener: (this: HTMLVaLanguageToggleElement, ev: VaLanguageToggleCustomEvent<HTMLVaLanguageToggleElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLVaLanguageToggleElement: {
prototype: HTMLVaLanguageToggleElement;
new (): HTMLVaLanguageToggleElement;
};
interface HTMLVaLinkElementEventMap {
"component-library-analytics": any;
}
Expand Down Expand Up @@ -2968,6 +3015,7 @@ declare global {
"va-file-input-multiple": HTMLVaFileInputMultipleElement;
"va-header-minimal": HTMLVaHeaderMinimalElement;
"va-icon": HTMLVaIconElement;
"va-language-toggle": HTMLVaLanguageToggleElement;
"va-link": HTMLVaLinkElement;
"va-link-action": HTMLVaLinkActionElement;
"va-loading-indicator": HTMLVaLoadingIndicatorElement;
Expand Down Expand Up @@ -3726,6 +3774,29 @@ declare namespace LocalJSX {
*/
"srtext"?: string;
}
/**
* @componentName Language Toggle
* @maturityCategory caution
* @maturityLevel candidate
*/
interface VaLanguageToggle {
/**
* The ISO language code for the page. Default is 'en'.
*/
"language"?: string;
/**
* Event fired when a link is clicked. Includes the selected language's ISO code.
*/
"onVaLanguageToggle"?: (event: VaLanguageToggleCustomEvent<any>) => void;
/**
* If true, specifies that the toggle is being used on a page with a router and clicking on a link will not result in page navigation.
*/
"routerLinks"?: boolean;
/**
* A JSON array of objects with link data. Each object should have an href, lang (ISO language code), and label properties. If using the pure web component provide as a string. Example: `[{"href": "/one", "lang": "en", "label": "English"}, ...]`.
*/
"urls"?: LangUrl[] | string;
}
/**
* @componentName Link
* @maturityCategory caution
Expand Down Expand Up @@ -5023,6 +5094,7 @@ declare namespace LocalJSX {
"va-file-input-multiple": VaFileInputMultiple;
"va-header-minimal": VaHeaderMinimal;
"va-icon": VaIcon;
"va-language-toggle": VaLanguageToggle;
"va-link": VaLink;
"va-link-action": VaLinkAction;
"va-loading-indicator": VaLoadingIndicator;
Expand Down Expand Up @@ -5198,6 +5270,12 @@ declare module "@stencil/core" {
* @maturityLevel candidate
*/
"va-icon": LocalJSX.VaIcon & JSXBase.HTMLAttributes<HTMLVaIconElement>;
/**
* @componentName Language Toggle
* @maturityCategory caution
* @maturityLevel candidate
*/
"va-language-toggle": LocalJSX.VaLanguageToggle & JSXBase.HTMLAttributes<HTMLVaLanguageToggleElement>;
/**
* @componentName Link
* @maturityCategory caution
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { newE2EPage } from "@stencil/core/testing";
import { axeCheck } from "../../../testing/test-helpers";

describe('va-language-toggle', () => {
const urls = '[{"href":"/resources/the-pact-act-and-your-va-benefits/","lang":"en","label":"English"},{"href":"/resources/the-pact-act-and-your-va-benefits-esp/","lang":"es","label":"Español"},{"href":"/resources/the-pact-act-and-your-va-benefits-tl/","lang":"tg","label":"Tagalog"}]';

it('renders', async () => {
const page = await newE2EPage();
await page.setContent(`<va-language-toggle urls=${urls} />`);
const element = await page.find('va-language-toggle');
expect(element).toHaveClass('hydrated');
});

it('English is the default language', async () => {
const page = await newE2EPage();
await page.setContent(`<va-language-toggle urls=${urls} />`);
const anchor = await page.find('va-language-toggle >>> a');
expect(anchor).toHaveClass('is-current-lang');
});

it('if language prop is set the matching language is bolded', async () => {
const page = await newE2EPage();
await page.setContent(`<va-language-toggle language="es" urls=${urls} />`);
const [_, anchor] = await page.findAll('va-language-toggle >>> a');
expect(anchor).toHaveClass('is-current-lang');
});

it('if router-links is set, clicking an anchor tag does not result in page navigation', async () => {
const page = await newE2EPage();
await page.setContent(`<va-language-toggle router-links="true" urls=${urls} />`);
const [startUrl] = page.url().split('?');
const [_, anchor] = await page.findAll('va-language-toggle >>> a');
await anchor.click();
const [endUrl] = page.url().split('?');
expect(startUrl).toEqual(endUrl);
});

it('fires a language-toggle event when a link is clicked', async () => {
const page = await newE2EPage();
await page.setContent(`<va-language-toggle urls=${urls} />`);
const toggleSpy = await page.spyOnEvent('vaLanguageToggle');
const [_, anchor] = await page.findAll('va-language-toggle >>> a');
await anchor.click();
expect(toggleSpy).toHaveReceivedEvent();
});

it('passes an aXe check', async () => {
const page = await newE2EPage();
await page.setContent(`<va-language-toggle urls=${urls} />`);
await axeCheck(page);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@import '../../mixins/focusable.css';

:host {
display: inline-block;
margin: 32px 0 24px 0;
border-bottom: 1px solid var(--vads-color-base-dark);

span {
margin: 0 4px 0 4px;
color: var(--vads-color-base-dark);
}

a {
&.is-current-lang {
font-weight: var(--font-weight-bold);
text-decoration: none;
color: var(--vads-color-base);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have a Figma link that can be added to description of the PR? I think it would be worthwhile to get a review from one of the designers as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jamigibbs I'm reaching out to Barb on this

}
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Component, Host, h, State, Event, EventEmitter, Prop, Fragment } from '@stencil/core';
import classNames from 'classnames';

export type LangUrl = {
label: string;
href: string;
lang: string;
};

/**
* @componentName Language Toggle
* @maturityCategory caution
* @maturityLevel candidate
*/

@Component({
tag: 'va-language-toggle',
styleUrl: 'va-language-toggle.scss',
shadow: true,
})
export class VaLanguageToggle {
/**
* The ISO language code for the page. Default is 'en'.
*/
@Prop() language: string = 'en';

/**
* A JSON array of objects with link data. Each object should have an href, lang (ISO language code), and label properties. If using the pure web component provide as a string. Example: `[{"href": "/one", "lang": "en", "label": "English"}, ...]`.
*/
@Prop() urls: LangUrl[] | string;
Copy link
Contributor

@jamigibbs jamigibbs Nov 5, 2024

Choose a reason for hiding this comment

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

I don't know if we want to add this type of flexibility. Our guidelines say that we only support 3 languages; English, Espanol, Tagalog

Other language options. VA.gov supports the 3 languages identified in the toggle. Do not add or remove from those 3 languages.

https://design.va.gov/components/language-toggle#usage

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jamigibbs - good point - I updated the component to only allow en, es, or tl.


/**
* If true, specifies that the toggle is being used on a page with a router and clicking on a link will not result in page navigation.
*/
@Prop() routerLinks?: boolean = false;

/**
* The urls as a Javascript array of objects, each with href, lang, and label properties.
*/
@State() formattedUrls: LangUrl[] = [];

/**
* Event fired when a link is clicked. Includes the selected language's ISO code.
*/
@Event()
vaLanguageToggle: EventEmitter;

// get the current page's url with language as a query param.
// allows for marking "pages" as visited
getUrl(langCode: string): string {
const url = new URL(window.location.href);
url.searchParams.set('lang', langCode);
return url.href;
}

// This method is fired whenever a link is clicked
handleToggle(e: Event, langCode: string) {
// don't navigate from current page but set new language
if (this.routerLinks) {
e.preventDefault();
// change browser url so that :visited styles apply to links
window.history.replaceState(null, null, this.getUrl(langCode));
this.language = langCode;
}

this.vaLanguageToggle.emit({ language: langCode });
}

componentWillLoad() {
// parse urls if needed
this.formattedUrls = (typeof this.urls === 'string' || this.urls instanceof String)
? JSON.parse(this.urls as string)
: this.urls;
}

render() {
const { language, formattedUrls } = this;
return (
<Host>
{this.formattedUrls.map(({href, lang, label}, i) => {
const anchorClass = classNames({
'is-current-lang': lang === language
});
return (
<Fragment>
<a
class={anchorClass}
href={href}
lang={lang}
hrefLang={lang}
onClick={(e) => this.handleToggle(e, lang)}
>
{label}
</a>
{ (i < formattedUrls.length - 1) && <span>|</span> }
</Fragment>
)
})}
</Host>
);
}
}
Loading