Skip to content

Commit

Permalink
feat(tag): implement component
Browse files Browse the repository at this point in the history
  • Loading branch information
dpellier committed Jul 29, 2024
1 parent 49365cd commit 6e50d3b
Show file tree
Hide file tree
Showing 36 changed files with 1,824 additions and 105 deletions.
2 changes: 2 additions & 0 deletions .stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
"rules": {
"alpha-value-notation": "number",
"color-function-notation": "legacy",
"color-hex-length": null,
"color-named": "never",
"hue-degree-notation": "number",
"scss/at-mixin-argumentless-call-parentheses": null,
"scss/at-rule-conditional-no-parentheses": null,
"scss/double-slash-comment-empty-line-before": null,
"scss/no-global-function-names": null,
Expand Down
1 change: 1 addition & 0 deletions packages/ods/react/tests/_app/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const componentNames = [
'skeleton',
'tooltip',
'button',
'tag',
//--generator-anchor--
];

Expand Down
24 changes: 24 additions & 0 deletions packages/ods/react/tests/_app/src/components/ods-tag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react-dom/client';
import { type OdsTagEventRemove } from '@ovhcloud/ods-components';
import { OdsTag } from 'ods-components-react';

const Tag = () => {
function onRemove(event: OdsTagEventRemove) {
console.log(`React tag remove event: ${event.detail.id}`);
}

return (
<>
<OdsTag id="my-tag"
label="My tag"
onOdsRemove={ onRemove } />

<OdsTag id="my-disabled-tag"
isDisabled={ true }
label="My tag"
onOdsRemove={ onRemove } />
</>
);
};

export default Tag;
53 changes: 53 additions & 0 deletions packages/ods/react/tests/e2e/ods-tag.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Page } from 'puppeteer';
import { goToComponentPage, setupBrowser } from '../setup';

describe('ods-tag react', () => {
const setup = setupBrowser();
let page: Page;

beforeAll(async () => {
page = setup().page;
});

beforeEach(async () => {
await goToComponentPage(page, 'ods-tag');
});

it('render the component correctly', async () => {
const elem = await page.$('ods-tag');
const boundingBox = await elem?.boundingBox();

expect(boundingBox?.height).toBeGreaterThan(0);
expect(boundingBox?.width).toBeGreaterThan(0);
});

it('emit an event on click', async () => {
const tagId = 'my-tag';
const elem = await page.$(`#${tagId}`);
let consoleLog = ''
page.on('console', (consoleObj) => {
consoleLog = consoleObj.text()
});

await elem?.click();
// Small delay to ensure page console event has been resolved
await new Promise((resolve) => setTimeout(resolve, 100));

expect(consoleLog).toBe(`React tag remove event: ${tagId}`);
});

it('does not emit an event on click if disabled', async () => {
const tagId = 'my-disabled-tag';
const elem = await page.$(`#${tagId}`);
let consoleLog = ''
page.on('console', (consoleObj) => {
consoleLog = consoleObj.text()
});

await elem?.click();
// Small delay to ensure page console event has been resolved
await new Promise((resolve) => setTimeout(resolve, 100));

expect(consoleLog).toBe('');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,29 @@ describe('ods-button navigation', () => {
it('should trigger on "Enter" when focused', async() => {
await setup('<ods-button label="Dummy button"></ods-button>');
await bindClick();

await page.keyboard.press('Tab');
await page.waitForChanges();

expect(clickSpy).not.toHaveBeenCalled();

await page.keyboard.press('Enter');
await page.waitForChanges();

expect(clickSpy).toHaveBeenCalledTimes(1);
});

it('should trigger on "Space" when focused', async() => {
await setup('<ods-button label="Dummy button"></ods-button>');
await bindClick();

await page.keyboard.press('Tab');
await page.waitForChanges();

expect(clickSpy).not.toHaveBeenCalled();

await page.keyboard.press('Space');
await page.waitForChanges();

expect(clickSpy).toHaveBeenCalledTimes(1);
});
Expand All @@ -86,6 +92,7 @@ describe('ods-button navigation', () => {
expect(clickSpy).not.toHaveBeenCalled();

await el.click();
await page.waitForChanges();

expect(clickSpy).toHaveBeenCalledTimes(1);
});
Expand All @@ -97,6 +104,7 @@ describe('ods-button navigation', () => {
expect(clickSpy).not.toHaveBeenCalled();

await el.click();
await page.waitForChanges();

expect(clickSpy).not.toHaveBeenCalled();
});
Expand Down
2 changes: 2 additions & 0 deletions packages/ods/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ export * from './link/src';
export * from './skeleton/src';
export * from './tooltip/src';
export * from './button/src';

export * from './tag/src';
19 changes: 19 additions & 0 deletions packages/ods/src/components/tag/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@ovhcloud/ods-component-tag",
"version": "17.1.0",
"private": true,
"description": "ODS Tag component",
"main": "dist/index.cjs.js",
"collection": "dist/collection/collection-manifest.json",
"scripts": {
"clean": "rimraf .stencil coverage dist docs-api www",
"doc": "typedoc --pretty --plugin ../../../scripts/typedoc-plugin-decorator.js && node ../../../scripts/generate-typedoc-md.js",
"lint:scss": "stylelint 'src/components/**/*.scss'",
"lint:ts": "eslint '{src,tests}/**/*.{js,ts,tsx}'",
"start": "stencil build --dev --watch --serve",
"test:e2e": "stencil test --e2e --config stencil.config.ts",
"test:e2e:ci": "tsc --noEmit && stencil test --e2e --ci --config stencil.config.ts",
"test:spec": "stencil test --spec --config stencil.config.ts --coverage",
"test:spec:ci": "tsc --noEmit && stencil test --config stencil.config.ts --spec --ci --coverage"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
@import '../../../../../style/tag';

:host(.ods-tag) {
display: inline-block;
}

.ods-tag {
&__tag {
$tag: &;

@include ods-tag();

&:not(&#{$tag}--disabled) {
&#{$tag}--default {
&#{$tag}--critical {
@include ods-tag-variant-default('critical');
}

&#{$tag}--information {
@include ods-tag-variant-default('information');
}

&#{$tag}--neutral {
@include ods-tag-variant-default('neutral');
}

&#{$tag}--success {
@include ods-tag-variant-default('success');
}

&#{$tag}--warning {
@include ods-tag-variant-default('warning');
}
}

&#{$tag}--outline {
&#{$tag}--critical {
@include ods-tag-variant-outline('critical');
}

&#{$tag}--information {
@include ods-tag-variant-outline('information');
}

&#{$tag}--neutral {
@include ods-tag-variant-outline('neutral');
}

&#{$tag}--success {
@include ods-tag-variant-outline('success');
}

&#{$tag}--warning {
@include ods-tag-variant-outline('warning');
}
}
}

&--disabled {
border-color: var(--ods-color-neutral-300);
background-color: var(--ods-color-neutral-300);
cursor: not-allowed;
color: var(--ods-color-neutral-600);
}

&--round {
@include ods-tag-shape('round');
}

&--square {
@include ods-tag-shape('square');
}

&--sm {
@include ods-tag-size('sm');
}

&--md {
@include ods-tag-size('md');
}

&--lg {
@include ods-tag-size('lg');
}
}
}
71 changes: 71 additions & 0 deletions packages/ods/src/components/tag/src/components/ods-tag/ods-tag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Component, Element, Event, type EventEmitter, type FunctionalComponent, Host, Listen, Prop, h } from '@stencil/core';
import { ODS_ICON_NAME, type OdsIconName } from '../../../../icon/src';
import { ODS_TAG_COLOR, type OdsTagColor } from '../../constants/tag-color';
import { ODS_TAG_SHAPE, type OdsTagShape } from '../../constants/tag-shape';
import { ODS_TAG_SIZE, type OdsTagSize } from '../../constants/tag-size';
import { ODS_TAG_VARIANT, type OdsTagVariant } from '../../constants/tag-variant';
import { handleClick, handleKeyDown, handleKeyUp } from '../../controller/ods-tag';
import { type OdsTagEventRemoveDetail } from '../../interfaces/events';

@Component({
shadow: true,
styleUrl: 'ods-tag.scss',
tag: 'ods-tag',
})
export class OdsTag {
@Element() el!: HTMLElement;

@Prop({ reflect: true }) public color: OdsTagColor = ODS_TAG_COLOR.information;
@Prop({ reflect: true }) public icon?: OdsIconName;
@Prop({ reflect: true }) public isDisabled: boolean = false;
@Prop({ reflect: true }) public label!: string;
@Prop({ reflect: true }) public shape: OdsTagShape = ODS_TAG_SHAPE.round;
@Prop({ reflect: true }) public size: OdsTagSize = ODS_TAG_SIZE.md;
@Prop({ reflect: true }) public variant: OdsTagVariant = ODS_TAG_VARIANT.default;

@Event() odsRemove!: EventEmitter<OdsTagEventRemoveDetail>;

@Listen('click')
onClick(): void {
handleClick(this.el, this.isDisabled, this.odsRemove);
}

@Listen('keydown')
onKeyDown(event: KeyboardEvent): void {
handleKeyDown(event);
}

@Listen('keyup')
onKeyUp(event: KeyboardEvent): void {
handleKeyUp(event, this.el, this.isDisabled, this.odsRemove);
}

render(): FunctionalComponent {
return (
<Host class="ods-tag">
<div
class={{
'ods-tag__tag': true,
'ods-tag__tag--disabled': this.isDisabled,
[`ods-tag__tag--${this.color}`]: true,
[`ods-tag__tag--${this.shape}`]: true,
[`ods-tag__tag--${this.size}`]: true,
[`ods-tag__tag--${this.variant}`]: true,
}}
part="tag"
tabindex={ this.isDisabled ? -1 : 0 }>
{
!!this.icon &&
<ods-icon name={ this.icon }>
</ods-icon>
}

{ this.label }

<ods-icon name={ ODS_ICON_NAME.cross }>
</ods-icon>
</div>
</Host>
);
}
}
17 changes: 17 additions & 0 deletions packages/ods/src/components/tag/src/constants/tag-color.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
enum ODS_TAG_COLOR {
critical = 'critical',
information = 'information',
neutral = 'neutral',
success = 'success',
warning = 'warning',
}

type OdsTagColor =`${ODS_TAG_COLOR}`;

const ODS_TAG_COLORS = Object.freeze(Object.values(ODS_TAG_COLOR));

export {
ODS_TAG_COLOR,
ODS_TAG_COLORS,
type OdsTagColor,
};
14 changes: 14 additions & 0 deletions packages/ods/src/components/tag/src/constants/tag-shape.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
enum ODS_TAG_SHAPE {
round = 'round',
square = 'square',
}

type OdsTagShape =`${ODS_TAG_SHAPE}`;

const ODS_TAG_SHAPES = Object.freeze(Object.values(ODS_TAG_SHAPE));

export {
ODS_TAG_SHAPE,
ODS_TAG_SHAPES,
type OdsTagShape,
};
15 changes: 15 additions & 0 deletions packages/ods/src/components/tag/src/constants/tag-size.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
enum ODS_TAG_SIZE {
lg = 'lg',
md = 'md',
sm = 'sm',
}

type OdsTagSize =`${ODS_TAG_SIZE}`;

const ODS_TAG_SIZES = Object.freeze(Object.values(ODS_TAG_SIZE));

export {
ODS_TAG_SIZE,
ODS_TAG_SIZES,
type OdsTagSize,
};
14 changes: 14 additions & 0 deletions packages/ods/src/components/tag/src/constants/tag-variant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
enum ODS_TAG_VARIANT {
default = 'default',
outline = 'outline',
}

type OdsTagVariant =`${ODS_TAG_VARIANT}`;

const ODS_TAG_VARIANTS = Object.freeze(Object.values(ODS_TAG_VARIANT));

export {
ODS_TAG_VARIANT,
ODS_TAG_VARIANTS,
type OdsTagVariant,
};
Loading

0 comments on commit 6e50d3b

Please sign in to comment.