Skip to content

Commit

Permalink
Create Modus Alert
Browse files Browse the repository at this point in the history
Resolves #48

New component modus-alert provides Trimble Modus Alert element as web component. Includes unit and e2e tests.

Increment package.json version to 0.0.4 to publish.

Add icon functional components for use in various components instead of embedding ugly SVG into each one separately.
  • Loading branch information
gabriel-piltzer-trimble committed Jun 8, 2021
1 parent fb69d06 commit a8bd668
Show file tree
Hide file tree
Showing 22 changed files with 541 additions and 25 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This library provides Modus Elements as web components. Web components are reusa
- [Technology](#technology)
- [Components](#components)
- **Implemented**
- Alert (modus-alert)
- Badge (modus-badge)
- Button (modus-button)
- Checkbox (modus-checkbox)
Expand All @@ -28,7 +29,6 @@ This library provides Modus Elements as web components. Web components are reusa
- Text Input (modus-text-input)
- **Not Implemented**
- [Accordion](#accordion)
- [Alert](#alert)
- [Breadcrumb](#breadcrumb)
- [Card](#card)
- [Chip](#chip)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@trimble-oss/modus-web-components",
"version": "0.0.3",
"version": "0.0.4",
"description": "Trimble Modus Web Component Library",
"repository": {
"type": "git",
Expand Down
41 changes: 41 additions & 0 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
export namespace Components {
interface ModusAlert {
/**
* (optional) Whether the alert has a dismiss button
*/
"dismissible": boolean;
/**
* (optional) The alert message
*/
"message": string;
/**
* (optional) The type of alert, sets the color and icon to render
*/
"type": 'error' | 'info' | 'info-gray' | 'info-gray-dark' | 'success' | 'warning';
}
interface ModusBadge {
/**
* (optional) The color of the badge
Expand Down Expand Up @@ -132,6 +146,12 @@ export namespace Components {
}
}
declare global {
interface HTMLModusAlertElement extends Components.ModusAlert, HTMLStencilElement {
}
var HTMLModusAlertElement: {
prototype: HTMLModusAlertElement;
new (): HTMLModusAlertElement;
};
interface HTMLModusBadgeElement extends Components.ModusBadge, HTMLStencilElement {
}
var HTMLModusBadgeElement: {
Expand Down Expand Up @@ -181,6 +201,7 @@ declare global {
new (): HTMLModusTextInputElement;
};
interface HTMLElementTagNameMap {
"modus-alert": HTMLModusAlertElement;
"modus-badge": HTMLModusBadgeElement;
"modus-button": HTMLModusButtonElement;
"modus-checkbox": HTMLModusCheckboxElement;
Expand All @@ -192,6 +213,24 @@ declare global {
}
}
declare namespace LocalJSX {
interface ModusAlert {
/**
* (optional) Whether the alert has a dismiss button
*/
"dismissible"?: boolean;
/**
* (optional) The alert message
*/
"message"?: string;
/**
* An event that fires when the alert is dismissed
*/
"onDismissClick"?: (event: CustomEvent<any>) => void;
/**
* (optional) The type of alert, sets the color and icon to render
*/
"type"?: 'error' | 'info' | 'info-gray' | 'info-gray-dark' | 'success' | 'warning';
}
interface ModusBadge {
/**
* (optional) The color of the badge
Expand Down Expand Up @@ -337,6 +376,7 @@ declare namespace LocalJSX {
"value"?: string;
}
interface IntrinsicElements {
"modus-alert": ModusAlert;
"modus-badge": ModusBadge;
"modus-button": ModusButton;
"modus-checkbox": ModusCheckbox;
Expand All @@ -351,6 +391,7 @@ export { LocalJSX as JSX };
declare module "@stencil/core" {
export namespace JSX {
interface IntrinsicElements {
"modus-alert": LocalJSX.ModusAlert & JSXBase.HTMLAttributes<HTMLModusAlertElement>;
"modus-badge": LocalJSX.ModusBadge & JSXBase.HTMLAttributes<HTMLModusBadgeElement>;
"modus-button": LocalJSX.ModusButton & JSXBase.HTMLAttributes<HTMLModusButtonElement>;
"modus-checkbox": LocalJSX.ModusCheckbox & JSXBase.HTMLAttributes<HTMLModusCheckboxElement>;
Expand Down
14 changes: 14 additions & 0 deletions src/components/icons/icon-check-circle-outline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// eslint-disable-next-line
import { FunctionalComponent, h } from '@stencil/core';

interface IconProps {
color?: string;
onClick?: () => void;
size?: string;
}

export const IconCheckCircleOutline: FunctionalComponent<IconProps> = (props: IconProps) => (
<svg class="icon-check-circle-outline" height={props.size ?? 16} width={props.size ?? 16} onClick={() => props.onClick()} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 4C16.4 4 20 7.6 20 12C20 16.4 16.4 20 12 20C7.6 20 4 16.4 4 12C4 7.6 7.6 4 12 4ZM12 18.36C15.531 18.36 18.4 15.4343 18.4 11.9234C18.4 8.41257 15.531 5.56 12 5.56C8.46897 5.56 5.6 8.48571 5.6 11.9966C5.6 15.5074 8.46897 18.36 12 18.36ZM15.44 10.4L11.28 14.56C11.12 14.72 10.96 14.72 10.8 14.72C10.64 14.72 10.48 14.64 10.32 14.56L8.48 12.72C8.24 12.48 8.24 12.08 8.48 11.84C8.72 11.6 9.12 11.6 9.36 11.84L10.72 13.2L14.48 9.44C14.72 9.2 15.12 9.2 15.36 9.44C15.68 9.68 15.68 10.08 15.44 10.4Z" fill={props.color ?? '#6A6976'} />
</svg>
);
14 changes: 14 additions & 0 deletions src/components/icons/icon-check-circle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// eslint-disable-next-line
import { FunctionalComponent, h } from '@stencil/core';

interface IconProps {
color?: string;
onClick?: () => void;
size?: string;
}

export const IconCheckCircle: FunctionalComponent<IconProps> = (props: IconProps) => (
<svg class="icon-check-circle" height={props.size ?? 16} width={props.size ?? 16} onClick={() => props.onClick()} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 4C7.584 4 4 7.584 4 12C4 16.416 7.584 20 12 20C16.416 20 20 16.416 20 12C20 7.584 16.416 4 12 4ZM10.4 16L6.4 12L7.528 10.872L10.4 13.736L16.472 7.664L17.6 8.8L10.4 16Z" fill={props.color ?? '#6A6976'} />
</svg>
);
14 changes: 14 additions & 0 deletions src/components/icons/icon-check.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// eslint-disable-next-line
import { FunctionalComponent, h } from '@stencil/core';

interface IconProps {
color?: string;
onClick?: () => void;
size?: string;
}

export const IconCheck: FunctionalComponent<IconProps> = (props: IconProps) => (
<svg class="icon-check" height={props.size ?? 16} width={props.size ?? 16} onClick={() => props.onClick()} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.08471 15.4676L5.29164 11.736L4 12.9978L9.08471 18L20 7.26174L18.7175 6L9.08471 15.4676Z" fill={props.color ?? '#6A6976'} />
</svg>
);
21 changes: 21 additions & 0 deletions src/components/icons/icon-close.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// eslint-disable-next-line
import { FunctionalComponent, h } from '@stencil/core';

interface IconProps {
color?: string;
onClick?: () => void;
size?: string;
}

export const IconClose: FunctionalComponent<IconProps> = (props: IconProps) => (
<svg class="icon-close" height={props.size ?? 16} width={props.size ?? 16} onClick={() => props.onClick()} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M19 7.30929L17.6907 6L12.5 11.1907L7.30929 6L6 7.30929L11.1907 12.5L6 17.6907L7.30929 19L12.5 13.8093L17.6907 19L19 17.6907L13.8093 12.5L19 7.30929Z" fill={props.color ?? '#6A6976'} />
</g>
<defs>
<clipPath id="clip0">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>
);
19 changes: 19 additions & 0 deletions src/components/icons/icon-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// eslint-disable-next-line
import { FunctionalComponent, h } from '@stencil/core';

interface IconProps {
color?: string;
onClick?: () => void;
size?: string;
}

export const IconError: FunctionalComponent<IconProps> = (props: IconProps) => (
<svg class="icon-error" height={props.size ?? 16} width={props.size ?? 16} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 12C3 7.032 7.032 3 12 3C16.968 3 21 7.032 21 12C21 16.968 16.968 21 12 21C7.032 21 3 16.968 3 12ZM12.9221 12.6706H11.078L10.7707 7.96292H13.2295L12.9221 12.6706ZM12.0001 16.0989C11.3264 16.0989 10.7823 15.5432 10.7823 14.8811C10.7823 14.2191 11.3264 13.6634 12.0001 13.6634C12.6738 13.6634 13.2178 14.2191 13.2178 14.8811C13.2178 15.5432 12.6738 16.0989 12.0001 16.0989Z" fill={props.color ?? '#6A6976'} />
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="3" y="3" width="18" height="18">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 12C3 7.032 7.032 3 12 3C16.968 3 21 7.032 21 12C21 16.968 16.968 21 12 21C7.032 21 3 16.968 3 12ZM12.9221 12.6706H11.078L10.7707 7.96292H13.2295L12.9221 12.6706ZM12.0001 16.0989C11.3264 16.0989 10.7823 15.5432 10.7823 14.8811C10.7823 14.2191 11.3264 13.6634 12.0001 13.6634C12.6738 13.6634 13.2178 14.2191 13.2178 14.8811C13.2178 15.5432 12.6738 16.0989 12.0001 16.0989Z" fill="white" />
</mask>
<g mask="url(#mask0)"></g>
</svg>

);
18 changes: 18 additions & 0 deletions src/components/icons/icon-info-outline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// eslint-disable-next-line
import { FunctionalComponent, h } from '@stencil/core';

interface IconProps {
color?: string;
onClick?: () => void;
size?: string;
}

export const IconInfoOutline: FunctionalComponent<IconProps> = (props: IconProps) => (
<svg class="icon-info-outline" height={props.size ?? 16} width={props.size ?? 16} onClick={() => props.onClick()} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 12C3 16.9631 7.03773 21 12 21C16.9623 21 21 16.9631 21 12C21 7.03773 16.9623 3 12 3C7.03773 3 3 7.03773 3 12ZM4.81555 12C4.81555 8.03836 8.03836 4.81555 12 4.81555C15.9616 4.81555 19.1845 8.03836 19.1845 12C19.1845 15.9616 15.9616 19.1845 12 19.1845C8.03836 19.1845 4.81555 15.9616 4.81555 12ZM11 17V11H13V17H11ZM11 7V9H13V7H11Z" fill={props.color ?? '#6A6976'} />
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="3" y="3" width="18" height="18">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 12C3 16.9631 7.03773 21 12 21C16.9623 21 21 16.9631 21 12C21 7.03773 16.9623 3 12 3C7.03773 3 3 7.03773 3 12ZM4.81555 12C4.81555 8.03836 8.03836 4.81555 12 4.81555C15.9616 4.81555 19.1845 8.03836 19.1845 12C19.1845 15.9616 15.9616 19.1845 12 19.1845C8.03836 19.1845 4.81555 15.9616 4.81555 12ZM11 17V11H13V17H11ZM11 7V9H13V7H11Z" fill="white" />
</mask>
<g mask="url(#mask0)"></g>
</svg>
);
18 changes: 18 additions & 0 deletions src/components/icons/icon-info.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// eslint-disable-next-line
import { FunctionalComponent, h } from '@stencil/core';

interface IconProps {
color?: string;
onClick?: () => void;
size?: string;
}

export const IconInfo: FunctionalComponent<IconProps> = (props: IconProps) => (
<svg class="icon-info" height={props.size ?? 16} width={props.size ?? 16} onClick={() => props.onClick()} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 21C16.968 21 21 16.968 21 12C21 7.032 16.968 3 12 3C7.032 3 3 7.032 3 12C3 16.968 7.032 21 12 21ZM11 7H13V9H11V7ZM11 11H13L13 17H11L11 11Z" fill={props.color ?? '#6A6976'} />
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="3" y="3" width="18" height="18">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 21C16.968 21 21 16.968 21 12C21 7.032 16.968 3 12 3C7.032 3 3 7.032 3 12C3 16.968 7.032 21 12 21ZM11 7H13V9H11V7ZM11 11H13L13 17H11L11 11Z" fill="white" />
</mask>
<g mask="url(#mask0)"></g>
</svg>
);
19 changes: 19 additions & 0 deletions src/components/icons/icon-warning-outline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// eslint-disable-next-line
import { FunctionalComponent, h } from '@stencil/core';

interface IconProps {
color?: string;
onClick?: () => void;
size?: string;
}

export const IconWarningOutline: FunctionalComponent<IconProps> = (props: IconProps) => (
<svg class="icon-warning-outline" height={props.size ?? 16} width={props.size ?? 16} onClick={() => props.onClick()} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4.68605C11.6138 4.68605 11.268 4.88157 11.0752 5.20881L3.82989 17.5336C3.64191 17.8533 3.63917 18.2368 3.82371 18.5592C4.01512 18.8934 4.36295 19.093 4.75468 19.093H19.2453C19.6371 19.093 19.9849 18.8934 20.1763 18.5592C20.3608 18.2375 20.3581 17.8533 20.1701 17.5336L12.9248 5.20881C12.732 4.88157 12.3869 4.68605 12 4.68605ZM19.2453 19.779H4.75468C4.11391 19.779 3.54312 19.4504 3.22823 18.9002C2.92019 18.3617 2.92431 17.7209 3.23852 17.1865L10.4838 4.86167C10.8008 4.32244 11.3675 4 12 4C12.6325 4 13.1999 4.32244 13.5162 4.86167L20.7615 17.1865C21.0757 17.7209 21.0798 18.3617 20.7718 18.9002C20.4569 19.4504 19.8861 19.779 19.2453 19.779ZM12.0001 17.0989C11.3264 17.0989 10.7823 16.5432 10.7823 15.8811C10.7823 15.2191 11.3264 14.6634 12.0001 14.6634C12.6738 14.6634 13.2178 15.2191 13.2178 15.8811C13.2178 16.5432 12.6738 17.0989 12.0001 17.0989ZM11.078 13.6706H12.9221L13.2295 8.96292H10.7707L11.078 13.6706Z" fill={props.color ?? '#6A6976'} />
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="3" y="4" width="18" height="16">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4.68605C11.6138 4.68605 11.268 4.88157 11.0752 5.20881L3.82989 17.5336C3.64191 17.8533 3.63917 18.2368 3.82371 18.5592C4.01512 18.8934 4.36295 19.093 4.75468 19.093H19.2453C19.6371 19.093 19.9849 18.8934 20.1763 18.5592C20.3608 18.2375 20.3581 17.8533 20.1701 17.5336L12.9248 5.20881C12.732 4.88157 12.3869 4.68605 12 4.68605ZM19.2453 19.779H4.75468C4.11391 19.779 3.54312 19.4504 3.22823 18.9002C2.92019 18.3617 2.92431 17.7209 3.23852 17.1865L10.4838 4.86167C10.8008 4.32244 11.3675 4 12 4C12.6325 4 13.1999 4.32244 13.5162 4.86167L20.7615 17.1865C21.0757 17.7209 21.0798 18.3617 20.7718 18.9002C20.4569 19.4504 19.8861 19.779 19.2453 19.779ZM12.0001 17.0989C11.3264 17.0989 10.7823 16.5432 10.7823 15.8811C10.7823 15.2191 11.3264 14.6634 12.0001 14.6634C12.6738 14.6634 13.2178 15.2191 13.2178 15.8811C13.2178 16.5432 12.6738 17.0989 12.0001 17.0989ZM11.078 13.6706H12.9221L13.2295 8.96292H10.7707L11.078 13.6706Z" fill="white" />
</mask>
<g mask="url(#mask0)"></g>
</svg>

);
14 changes: 14 additions & 0 deletions src/components/icons/icon-warning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// eslint-disable-next-line
import { FunctionalComponent, h } from '@stencil/core';

interface IconProps {
color?: string;
onClick?: () => void;
size?: string;
}

export const IconWarning: FunctionalComponent<IconProps> = (props: IconProps) => (
<svg class="icon-warning" height={props.size ?? 16} width={props.size ?? 16} onClick={() => props.onClick()} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.75468 19.779C4.11391 19.779 3.54312 19.4504 3.22823 18.9002C2.92019 18.3617 2.92431 17.7209 3.23852 17.1865L10.4838 4.86167C10.8008 4.32244 11.3675 4 12 4C12.6325 4 13.1999 4.32244 13.5162 4.86167L20.7615 17.1865C21.0757 17.7209 21.0798 18.3617 20.7718 18.9002C20.4569 19.4504 19.8861 19.779 19.2453 19.779H4.75468ZM12.0001 17.0989C11.3264 17.0989 10.7823 16.5432 10.7823 15.8811C10.7823 15.2191 11.3264 14.6634 12.0001 14.6634C12.6738 14.6634 13.2178 15.2191 13.2178 15.8811C13.2178 16.5432 12.6738 17.0989 12.0001 17.0989ZM11.078 13.6706H12.9221L13.2295 8.96292H10.7707L11.078 13.6706Z" fill={props.color ?? '#6A6976'} />
</svg>
);
87 changes: 87 additions & 0 deletions src/components/modus-alert/modus-alert.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { newE2EPage } from '@stencil/core/testing';

describe('modus-alert', () => {
it('renders', async () => {
const page = await newE2EPage();

await page.setContent('<modus-alert></modus-alert>');
const element = await page.find('modus-alert');
expect(element).toHaveClass('hydrated');
});

it('renders changes to the dismissible prop', async () => {
const page = await newE2EPage();

await page.setContent('<modus-alert dismissible></modus-alert>');
const component = await page.find('modus-alert');
let element = await page.find('modus-alert >>> svg.icon-close');
expect(element).toBeDefined();

component.setProperty('dismissible', false);
await page.waitForChanges();
element = await page.find('modus-alert >>> svg.icon-close');
expect(element).toBeNull();
});

it('renders changes to the message prop', async () => {
const page = await newE2EPage();

await page.setContent('<modus-alert message="Hello"></modus-alert>');
const component = await page.find('modus-alert');
let element = await page.find('modus-alert >>> div.message');
expect(element.innerHTML).toEqual('Hello');

component.setProperty('message', 'Hello world!');
await page.waitForChanges();
expect(element.innerHTML).toEqual('Hello world!');
});

it('renders changes to the type prop', async () => {
const page = await newE2EPage();

await page.setContent('<modus-alert></modus-alert>');
const component = await page.find('modus-alert');
let element = await page.find('modus-alert >>> div.alert');
expect(element).toHaveClass('type-info');

component.setProperty('type', 'error');
await page.waitForChanges();
expect(element).toHaveClass('type-error');
});

it('renders the correct icon by type', async () => {
const page = await newE2EPage();

await page.setContent('<modus-alert></modus-alert>');
const component = await page.find('modus-alert');
let element = await page.find('modus-alert >>> svg.icon-info');
expect(element).toBeDefined();

component.setProperty('type', 'error');
await page.waitForChanges();
element = await page.find('modus-alert >>> svg.icon-error');
expect(element).toBeDefined();

component.setProperty('type', 'success');
await page.waitForChanges();
element = await page.find('modus-alert >>> svg.icon-check-circle');
expect(element).toBeDefined();

component.setProperty('type', 'warning');
await page.waitForChanges();
element = await page.find('modus-alert >>> svg.icon-warning');
expect(element).toBeDefined();
});

it('emits dismissClick event on icon close click', async () => {
const page = await newE2EPage();

await page.setContent('<modus-alert dismissible></modus-alert>');
const dismissClick = await page.spyOnEvent('dismissClick');
const element = await page.find('modus-alert >>> svg.icon-close');

await element.click();
await page.waitForChanges();
expect(dismissClick).toHaveReceivedEvent();
});
});
Loading

0 comments on commit a8bd668

Please sign in to comment.