Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
run: cargo test

typescript_tests:
name: Typescript tests
name: TypeScript tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand Down
19 changes: 16 additions & 3 deletions lib/angular-ui/src/lib/atoms/badge/badge.component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, input } from '@angular/core';

import { defineCustomElement } from '@cg/ui/dist/components/cg-badge';
import { DefineCustomElement } from '../../define-custom-element';
import { defineCustomElementComponent } from '../../custom-element-component';

type CustomElement = HTMLCgBadgeElement;

@DefineCustomElement(defineCustomElement)
@Component({
selector: 'cg-badge',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<ng-content></ng-content>`,
template: `<ng-content />`,
})
export class BadgeComponent {}
export class BadgeComponent extends defineCustomElementComponent<CustomElement>(
defineCustomElement,
) {
public readonly theme = input<CustomElement['theme']>('primary');

constructor() {
super();

this.elemProxyEffect(this.theme, 'theme');
}
}
9 changes: 6 additions & 3 deletions lib/angular-ui/src/lib/atoms/card/card.component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { defineCustomElement } from '@cg/ui/dist/components/cg-card';
import { DefineCustomElement } from '../../define-custom-element';
import { defineCustomElementComponent } from '../../custom-element-component';

type CustomElement = HTMLCgCardElement;

@DefineCustomElement(defineCustomElement)
@Component({
selector: 'cg-card',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<ng-content />`,
})
export class CardComponent {}
export class CardComponent extends defineCustomElementComponent<CustomElement>(
defineCustomElement,
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('IconBtnComponent', () => {

fixture = TestBed.createComponent(IconBtnComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('ariaLabel', 'test');
fixture.detectChanges();
});

Expand Down
29 changes: 25 additions & 4 deletions lib/angular-ui/src/lib/atoms/icon-btn/icon-btn.component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, input } from '@angular/core';

import { defineCustomElement } from '@cg/ui/dist/components/cg-icon-btn';
import { DefineCustomElement } from '../../define-custom-element';
import { defineCustomElementComponent } from '../../custom-element-component';

type CustomElement = HTMLCgIconBtnElement;

@DefineCustomElement(defineCustomElement)
@Component({
selector: 'cg-icon-btn',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<ng-content />`,
})
export class IconBtnComponent {}
export class IconBtnComponent extends defineCustomElementComponent<CustomElement>(
defineCustomElement,
) {
public readonly type = input<CustomElement['type']>('button');
public readonly disabled = input<CustomElement['disabled']>(false);
public readonly ariaLabel = input.required<CustomElement['ariaLabel']>();
public readonly ariaHasPopup = input<CustomElement['ariaHasPopup']>();
public readonly ariaExpanded = input<CustomElement['ariaExpanded']>();
public readonly ariaControls = input<CustomElement['ariaControls']>();

constructor() {
super();

this.elemProxyEffect(this.type, 'type');
this.elemProxyEffect(this.disabled, 'disabled');
this.elemProxyEffect(this.ariaLabel, 'ariaLabel');
this.elemProxyEffect(this.ariaHasPopup, 'ariaHasPopup');
this.elemProxyEffect(this.ariaExpanded, 'ariaExpanded');
this.elemProxyEffect(this.ariaControls, 'ariaControls');
}
}
1 change: 1 addition & 0 deletions lib/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
},
"dependencies": {
"@cg/styles": "workspace:*",
"@cg/utils": "workspace:*",
"@stencil/core": "^4.23.1"
},
"devDependencies": {
Expand Down
26 changes: 0 additions & 26 deletions lib/ui/src/coercion/coerce-boolean.ts

This file was deleted.

2 changes: 0 additions & 2 deletions lib/ui/src/coercion/index.ts

This file was deleted.

24 changes: 10 additions & 14 deletions lib/ui/src/components/atoms/badge/badge.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from '@playwright/test';
import { test } from '@stencil/playwright';
import { waitForError } from '../../../e2e-utils';

test.describe('cg-badge', () => {
const content = 'Badge content';
Expand Down Expand Up @@ -32,19 +33,14 @@ test.describe('cg-badge', () => {
});

test('should throw error for an invalid theme', async ({ page }) => {
let errorThrown = false;
page.on('pageerror', error => {
if (error.message === 'Invalid theme: "garbage"') {
errorThrown = true;
}
});

await page.setContent(`
<cg-badge theme="garbage">
${content}
</cg-badge>
`);

expect(errorThrown).toBe(true);
await waitForError(
page,
'Invalid theme provided: "garbage"',
page.setContent(`
<cg-badge theme="garbage">
${content}
</cg-badge>
`),
);
});
});
2 changes: 1 addition & 1 deletion lib/ui/src/components/atoms/badge/badge.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, ComponentInterface, Host, Prop, h } from '@stencil/core';
import { Theme } from '../../../types';
import { coerceTheme } from '../../../coercion';
import { coerceTheme } from '../../../utils';

@Component({
tag: 'cg-badge',
Expand Down
2 changes: 1 addition & 1 deletion lib/ui/src/components/atoms/focus-ring/focus-ring.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ test.describe('cg-focus-ring', () => {
test('should throw error for an invalid theme', async ({ page }) => {
let errorThrown = false;
page.on('pageerror', error => {
if (error.message === 'Invalid theme: "garbage"') {
if (error.message === 'Invalid theme provided: "garbage"') {
errorThrown = true;
}
});
Expand Down
2 changes: 1 addition & 1 deletion lib/ui/src/components/atoms/focus-ring/focus-ring.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, ComponentInterface, Host, Prop, h } from '@stencil/core';
import { Theme } from '../../../types';
import { coerceTheme } from '../../../coercion';
import { coerceTheme } from '../../../utils';

@Component({
tag: 'cg-focus-ring',
Expand Down
85 changes: 85 additions & 0 deletions lib/ui/src/components/atoms/icon-btn/icon-btn.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { test } from '@stencil/playwright';
import { expect } from '@playwright/test';
import { waitForError } from '../../../e2e-utils';

test.describe('cg-icon-btn', () => {
const compLoc = 'cg-icon-btn';
const iconLoc = 'cg-profile-icon';
const focusRingLoc = 'cg-focus-ring';
const buttonLoc = 'button';

test('should render button with default attributes', async ({ page }) => {
await page.setContent(`
<cg-icon-btn aria-label="test">
<cg-profile-icon />
</cg-icon-btn>
`);

const component = page.locator(compLoc);

await expect(component).toHaveAttribute('type', 'button');
await expect(component).toHaveAttribute('aria-label', 'test');
await expect(component).not.toHaveAttribute('disabled');
await expect(component).not.toHaveAttribute('aria-haspopup');
await expect(component).not.toHaveAttribute('aria-expanded');
await expect(component).not.toHaveAttribute('aria-controls');

await expect(component.locator(iconLoc)).toBeVisible();

const focusRing = component.locator(focusRingLoc);
const innerButton = component.locator(buttonLoc);

await expect(focusRing).not.toHaveAttribute('is-focused');
await innerButton.focus();
await expect(focusRing).toHaveAttribute('is-focused');
await innerButton.blur();
await expect(focusRing).not.toHaveAttribute('is-focused');
});

test('should render button with custom attributes', async ({ page }) => {
await page.setContent(`
<cg-icon-btn
type="submit"
disabled
aria-label="test"
aria-haspopup="menu"
aria-expanded
aria-controls="menu-id"
>
<cg-profile-icon />
</cg-icon-btn>
`);

const component = page.locator(compLoc);

await expect(component).toHaveAttribute('type', 'submit');
await expect(component).toHaveAttribute('aria-label', 'test');
await expect(component).toHaveAttribute('disabled');
await expect(component).toHaveAttribute('aria-haspopup', 'menu');
await expect(component).toHaveAttribute('aria-expanded');
await expect(component).toHaveAttribute('aria-controls', 'menu-id');

await expect(component.locator(iconLoc)).toBeVisible();

const focusRing = component.locator(focusRingLoc);
const innerButton = component.locator(buttonLoc);

await expect(focusRing).not.toHaveAttribute('is-focused');
await innerButton.focus();
await expect(focusRing).not.toHaveAttribute('is-focused');
await innerButton.blur();
await expect(focusRing).not.toHaveAttribute('is-focused');
});

test.fixme('should throw error for missing aria-label', async ({ page }) => {
await waitForError(
page,
'Error: Empty string provided where a non-empty string was expected',
page.setContent(`
<cg-icon-btn>
<cg-profile-icon />
</cg-icon-btn>
`),
);
});
});
4 changes: 2 additions & 2 deletions lib/ui/src/components/atoms/icon-btn/icon-btn.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const meta: Meta = {
disabled: false,
},
render: args => `
<cg-icon-btn label="${args.ariaLabel}" disabled="${args.disabled}">
<cg-profile-icon></cg-profile-icon>
<cg-icon-btn aria-label="${args.ariaLabel}" disabled="${args.disabled}">
<cg-profile-icon />
</cg-icon-btn>
`,
};
Expand Down
15 changes: 13 additions & 2 deletions lib/ui/src/components/atoms/icon-btn/icon-btn.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, ComponentInterface, Prop, State, h } from '@stencil/core';
import { AriaHasPopup, ButtonType } from '../../../types';
import { assertStringNonEmpty } from '../../../utils';

@Component({
tag: 'cg-icon-btn',
Expand All @@ -11,10 +12,17 @@ export class IconBtnComponent implements ComponentInterface {
public type: ButtonType = 'button';

@Prop({ reflect: true })
public disabled?: boolean;
public disabled: boolean = false;

@Prop({ reflect: true, attribute: 'aria-label' })
public ariaLabel!: string;
public get ariaLabel(): string {
return this.#ariaLabel;
}
public set ariaLabel(value: string) {
assertStringNonEmpty(value);
this.#ariaLabel = value;
}
#ariaLabel!: string;

@Prop({ reflect: true, attribute: 'aria-haspopup' })
public ariaHasPopup?: AriaHasPopup;
Expand Down Expand Up @@ -47,6 +55,9 @@ export class IconBtnComponent implements ComponentInterface {
</button>
);
}
public componentWillLoad(): void {
assertStringNonEmpty(this.ariaLabel);
}

private onFocused(): void {
this.isFocused = true;
Expand Down
1 change: 1 addition & 0 deletions lib/ui/src/e2e-utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './wait-for-error';
14 changes: 14 additions & 0 deletions lib/ui/src/e2e-utils/wait-for-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Page } from '@playwright/test';

export async function waitForError<T extends Promise<void>>(
page: Page,
errorMessage: string,
doFn: T,
): Promise<Error> {
const [error] = await Promise.all([
page.waitForEvent('pageerror', error => error.message === errorMessage),
doFn,
]);

return error;
}
11 changes: 11 additions & 0 deletions lib/ui/src/utils/assert-string-has-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { isNil } from '@cg/utils';

export function assertStringNonEmpty(value: string | undefined | null): string {
if (isNil(value) || value.trim() === '') {
throw new Error(
'Empty string provided where a non-empty string was expected',
);
}

return value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Theme } from '../types';

export function coerceTheme(theme: unknown): Theme {
if (!isTheme(theme)) {
throw new Error(`Invalid theme: "${theme}"`);
throw new Error(`Invalid theme provided: "${theme}"`);
}

return theme;
Expand Down
2 changes: 2 additions & 0 deletions lib/ui/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './assert-string-has-value';
export * from './coerce-theme';
10 changes: 10 additions & 0 deletions lib/utils/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@cg/utils",
"private": true,
"scripts": {
"build": "tsc"
},
"module": "dist/index.js",
"types": "dist/index.d.ts",
"main": "dist/index.js"
}
1 change: 1 addition & 0 deletions lib/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './nil';
9 changes: 9 additions & 0 deletions lib/utils/src/nil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function isNil<T>(
value: T | null | undefined,
): value is null | undefined {
return value === null || value === undefined;
}

export function isNotNil<T>(value: T | null | undefined): value is T {
return !isNil(value);
}
Loading
Loading