= [
{ sizeName: 'xsmall', responsiveSize: '--btn-height--small' },
{ sizeName: 'small-expressive', responsiveSize: '--btn-height--medium' },
diff --git a/packages/components/pie-tag/README.md b/packages/components/pie-tag/README.md
index 7db439beae..a771b99c56 100644
--- a/packages/components/pie-tag/README.md
+++ b/packages/components/pie-tag/README.md
@@ -19,7 +19,7 @@
## pie-tag
-`pie-tag` is a Web Component built using the Lit library.
+`pie-tag` is a Web Component built using the Lit library. A tag is a small visual element used to represent and categorize information within a user interface.
This component can be easily integrated into various frontend frameworks and customized through a set of properties.
@@ -74,16 +74,38 @@ import { PieTag } from '@justeattakeaway/pie-tag/dist/react';
| Property | Type | Default | Description |
|---|---|---|---|
-| - | - | - | - |
+| size | `String` | `large` | Size of the tag. Can be either `large` or `small` |
+| variant | `String` | `neutral` | Variant of the tag, one of `variants` - `neutral-alternative`, `neutral`, `outline`, `ghost`, `blue`, `green`, `yellow`, `red`, `brand` |
+| isStrong | `Boolean` | `false` | If `true`, displays strong tag styles for `green`, `yellow`, `red`, `blue` and `neutral` variants'|
In your markup or JSX, you can then use these to set the properties for the `pie-tag` component:
```html
-
+Label
-
+Label
+```
+## Slots
+
+| Slot | Description |
+| Default slot | Used to pass text into the tag component. |
+| icon | Used to pass in an icon to the tag component. We recommend using `pie-icons-webc` for defining this icon, but this can also accept an SVG icon. |
+
+### Using `pie-icons-webc` with `pie-tag` icon slot
+
+We recommend using `pie-icons-webc` when using the `icon` slot. Here is an example of how you would do this:
+
+```html
+
+
+
+ Vegan
+
```
## Contributing
diff --git a/packages/components/pie-tag/src/defs.ts b/packages/components/pie-tag/src/defs.ts
index d95303e94e..42e2282eb7 100644
--- a/packages/components/pie-tag/src/defs.ts
+++ b/packages/components/pie-tag/src/defs.ts
@@ -1,3 +1,19 @@
-// TODO - please remove the eslint disable comment below when you add props to this interface
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface TagProps {}
+export const variants = ['neutral-alternative', 'neutral', 'outline', 'ghost', 'blue', 'green', 'yellow', 'red', 'brand'] as const;
+export const sizes = ['small', 'large'] as const;
+
+export interface TagProps {
+ /**
+ * What style variant the tag should be such as neutral/ghost etc.
+ */
+ variant: typeof variants[number];
+
+ /**
+ * When true, the 'green', "yellow", "red", "blue" and "neutral" variants change their styles and become bolder
+ */
+ isStrong: boolean;
+
+ /**
+ * What size the tag should be.
+ */
+ size: typeof sizes[number];
+}
diff --git a/packages/components/pie-tag/src/index.ts b/packages/components/pie-tag/src/index.ts
index 4024408c2f..097bff6c64 100644
--- a/packages/components/pie-tag/src/index.ts
+++ b/packages/components/pie-tag/src/index.ts
@@ -1,8 +1,10 @@
-import { LitElement, html, unsafeCSS } from 'lit';
-
-import { defineCustomElement } from '@justeattakeaway/pie-webc-core';
+import {
+ LitElement, html, unsafeCSS, nothing,
+} from 'lit';
+import { property } from 'lit/decorators.js';
+import { validPropertyValues, defineCustomElement } from '@justeattakeaway/pie-webc-core';
import styles from './tag.scss?inline';
-import { TagProps } from './defs';
+import { TagProps, variants, sizes } from './defs';
// Valid values available to consumers
export * from './defs';
@@ -11,10 +13,38 @@ const componentSelector = 'pie-tag';
/**
* @tagname pie-tag
+ * @slot icon - The icon slot
+ * @slot - Default slot
*/
export class PieTag extends LitElement implements TagProps {
+ @property({ type: String })
+ @validPropertyValues(componentSelector, variants, 'neutral')
+ public variant: TagProps['variant'] = 'neutral';
+
+ @property({ type: String })
+ @validPropertyValues(componentSelector, sizes, 'large')
+ public size : TagProps['size'] = 'large';
+
+ @property({ type: Boolean })
+ public isStrong = false;
+
render () {
- return html`Hello world!
`;
+ const {
+ variant,
+ size,
+ isStrong,
+ } = this;
+ return html`
+
+ ${size === 'large' ? html`` : nothing}
+
+
`;
}
// Renders a `CSSResult` generated from SCSS by Vite
diff --git a/packages/components/pie-tag/src/tag.scss b/packages/components/pie-tag/src/tag.scss
index 6ffaedad64..c3c51603df 100644
--- a/packages/components/pie-tag/src/tag.scss
+++ b/packages/components/pie-tag/src/tag.scss
@@ -1 +1,149 @@
@use '@justeattakeaway/pie-css/scss' as p;
+
+*,
+*:before,
+*:after {
+ box-sizing: border-box;
+}
+
+// Base tag styles
+.c-tag {
+ // Custom Property Declarations
+ // These are defined here instead of :host to encapsulate them inside Shadow DOM
+ --tag-font-family: var(--dt-font-body-s-family);
+ --tag-font-weight: var(--dt-font-body-s-weight);
+ --tag-icon-size: 16px;
+
+ // The following values set to default background and color
+ // currently this sets the neutral large tag styles
+ --tag-bg-color: var(--dt-color-container-inverse);
+ --tag-color: var(--dt-color-content-inverse);
+
+ // transparent to variable to function properly in component tests
+ --tag-transparent-bg-color: transparent;
+
+ // Heights for the different tag sizes
+ --tag-height-large: 24px;
+ --tag-height-small: 16px;
+
+ display: inline-flex;
+ vertical-align: middle;
+ align-items: center;
+ justify-content: center;
+ gap: var(--dt-spacing-a);
+ height: var(--tag-height);
+ padding: var(--tag-padding);
+ border: none;
+ border-radius: var(--tag-border-radius);
+ background-color: var(--tag-bg-color);
+ color: var(--tag-color);
+ font-family: var(--tag-font-family);
+ font-weight: var(--tag-font-weight);
+ font-size: var(--tag-font-size);
+ line-height: var(--tag-line-height);
+ opacity: var(--tag-opacity, 1); // we don't specify --tag-opacity variable here to let consumers override a default value that we set
+
+ // Size
+ &[size='small'] {
+ --tag-height: var(--tag-height-small);
+ --tag-padding: 0 var(--dt-spacing-a);
+ --tag-border-radius: var(--dt-radius-rounded-a);
+ --tag-font-size: #{p.font-size(--dt-font-caption-size)};
+ --tag-line-height: #{p.line-height(--dt-font-caption-line-height)};
+ }
+
+ &[size='large'] {
+ --tag-height: var(--tag-height-large);
+ --tag-padding: 2px var(--dt-spacing-b);
+ --tag-border-radius: var(--dt-radius-rounded-b);
+ --tag-font-size: #{p.font-size(--dt-font-body-s-size)};
+ --tag-line-height: #{p.line-height(--dt-font-body-s-line-height)};
+ }
+
+ // Variant
+ &[variant='neutral'] {
+ --tag-bg-color: var(--dt-color-container-strong);
+ --tag-color: var(--dt-color-content-default);
+
+ &[isStrong] {
+ --tag-bg-color: var(--dt-color-container-inverse);
+ --tag-color: var(--dt-color-content-inverse);
+ }
+ }
+
+ &[variant='blue'] {
+ --tag-bg-color: var(--dt-color-support-info-02);
+ --tag-color: var(--dt-color-content-default);
+
+ &[isStrong] {
+ --tag-bg-color: var(--dt-color-support-info);
+ --tag-color: var(--dt-color-content-light);
+ }
+ }
+
+ &[variant='green'] {
+ --tag-bg-color: var(--dt-color-support-positive-02);
+ --tag-color: var(--dt-color-content-default);
+
+ &[isStrong] {
+ --tag-bg-color: var(--dt-color-support-positive);
+ --tag-color: var(--dt-color-content-light);
+ }
+ }
+
+ &[variant='yellow'] {
+ --tag-bg-color: var(--dt-color-support-warning-02);
+ --tag-color: var(--dt-color-content-dark);
+
+ &[isStrong] {
+ --tag-bg-color: var(--dt-color-support-warning);
+ --tag-color: var(--dt-color-content-dark);
+ }
+ }
+
+ &[variant='red'] {
+ --tag-bg-color: var(--dt-color-support-error-02);
+ --tag-color: var(--dt-color-content-default);
+
+ &[isStrong] {
+ --tag-bg-color: var(--dt-color-support-error);
+ --tag-color: var(--dt-color-content-light);
+ }
+ }
+
+ &[variant='brand'] {
+ --tag-bg-color: var(--dt-color-support-brand-02);
+ --tag-color: var(--dt-color-content-default);
+ }
+
+ &[variant='neutral-alternative'] {
+ --tag-bg-color: var(--dt-color-container-default);
+ --tag-color: var(--dt-color-content-default);
+ }
+
+ &[variant='outline'] {
+ --tag-bg-color: var(--tag-transparent-bg-color);
+ --tag-color: var(--dt-color-content-default);
+ border: 1px solid var(--dt-color-border-strong);
+
+ &[size='small'] {
+ --tag-padding: 0 3px; // small tag padding minus 1px of the border
+ }
+
+ &[size='large'] {
+ --tag-padding: 1px 7px; // large tag padding minus 1px of the border
+ }
+ }
+
+ &[variant='ghost'] {
+ --tag-bg-color: var(--tag-transparent-bg-color);
+ --tag-color: var(--dt-color-content-default);
+ }
+}
+
+// Used to size an SVG if one is passed in (when not using the component icons)
+::slotted(svg) {
+ display: block;
+ height: var(--tag-icon-size);
+ width: var(--tag-icon-size);
+}
diff --git a/packages/components/pie-tag/test/accessibility/pie-tag.spec.ts b/packages/components/pie-tag/test/accessibility/pie-tag.spec.ts
index fdaef1c0f1..1096ac692b 100644
--- a/packages/components/pie-tag/test/accessibility/pie-tag.spec.ts
+++ b/packages/components/pie-tag/test/accessibility/pie-tag.spec.ts
@@ -1,18 +1,34 @@
import { test, expect } from '@justeattakeaway/pie-webc-testing/src/playwright/fixtures.ts';
-import { PieTag, TagProps } from '@/index';
+import { getAllPropCombinations, splitCombinationsByPropertyValue } from '@justeattakeaway/pie-webc-testing/src/helpers/get-all-prop-combos.ts';
+import { PropObject, WebComponentPropValues } from '@justeattakeaway/pie-webc-testing/src/helpers/defs.ts';
+import { PieTag } from '@/index';
+import { sizes, variants } from '@/defs';
-test.describe('PieTag - Accessibility tests', () => {
- test('a11y - should test the PieTag component WCAG compliance', async ({ makeAxeBuilder, mount }) => {
+const props: PropObject = {
+ variant: variants,
+ size: sizes,
+ isStrong: [true, false],
+};
+
+const componentPropsMatrix : WebComponentPropValues[] = getAllPropCombinations(props);
+const componentPropsMatrixByVariant: Record = splitCombinationsByPropertyValue(componentPropsMatrix, 'variant');
+const componentVariants: string[] = Object.keys(componentPropsMatrixByVariant);
+
+componentVariants.forEach((variant) => test(`should render all prop variations for Variant: ${variant}`, async ({ makeAxeBuilder, mount }) => {
+ await Promise.all(componentPropsMatrixByVariant[variant].map(async (combo: WebComponentPropValues) => {
await mount(
PieTag,
{
- props: {} as TagProps,
+ props: { ...combo },
+ slots: {
+ default: 'Hello world',
+ },
},
);
+ }));
- const results = await makeAxeBuilder().analyze();
+ const results = await makeAxeBuilder().analyze();
- expect(results.violations).toEqual([]);
- });
-});
+ expect(results.violations).toEqual([]);
+}));
diff --git a/packages/components/pie-tag/test/component/pie-tag.spec.ts b/packages/components/pie-tag/test/component/pie-tag.spec.ts
index 0d401df369..d082a6f6fb 100644
--- a/packages/components/pie-tag/test/component/pie-tag.spec.ts
+++ b/packages/components/pie-tag/test/component/pie-tag.spec.ts
@@ -1,14 +1,43 @@
-
+import { getShadowElementStylePropValues } from '@justeattakeaway/pie-webc-testing/src/helpers/get-shadow-element-style-prop-values.ts';
import { test, expect } from '@sand4rt/experimental-ct-web';
import { PieTag, TagProps } from '@/index';
const componentSelector = '[data-test-id="pie-tag"]';
+const tagIconSelector = '[data-test-id="tag-icon"]';
+
+const props: Partial = {
+ size: 'large',
+ variant: 'neutral',
+ isStrong: false,
+};
+
+type VariantToBgStyle = {
+ variantName: TagProps['variant'];
+ bgStyle: string;
+};
+
+const variantsToIsStrongStyle:Array = [
+ { variantName: 'neutral', bgStyle: '--dt-color-container-inverse' },
+ { variantName: 'green', bgStyle: '--dt-color-support-positive' },
+ { variantName: 'red', bgStyle: '--dt-color-support-error' },
+ { variantName: 'yellow', bgStyle: '--dt-color-support-warning' },
+ { variantName: 'blue', bgStyle: '--dt-color-support-info' },
+ { variantName: 'neutral-alternative', bgStyle: '--dt-color-container-default' },
+ { variantName: 'brand', bgStyle: '--dt-color-support-brand-02' },
+ { variantName: 'ghost', bgStyle: '--tag-transparent-bg-color' },
+ { variantName: 'outline', bgStyle: '--tag-transparent-bg-color' },
+];
+
+const icon = '';
test.describe('PieTag - Component tests', () => {
test('should render successfully', async ({ mount, page }) => {
// Arrange
await mount(PieTag, {
- props: {} as TagProps,
+ props,
+ slots: {
+ default: 'Label',
+ },
});
// Act
@@ -17,4 +46,68 @@ test.describe('PieTag - Component tests', () => {
// Assert
expect(tag).toBeVisible();
});
+
+ test.describe('icon slot', () => {
+ test.describe('when passed', () => {
+ test.describe('if the size is large', () => {
+ test('should render the icon', async ({ mount, page }) => {
+ // Arrange
+ await mount(PieTag, {
+ props,
+ slots: {
+ default: 'Label',
+ icon,
+ },
+ });
+
+ // Act
+ const tagIcon = page.locator(tagIconSelector);
+
+ // Assert
+ expect(tagIcon).toBeVisible();
+ });
+ });
+ });
+
+ test.describe('if the size is small', () => {
+ test('should NOT render the icon', async ({ mount, page }) => {
+ // Arrange
+ await mount(PieTag, {
+ props: {
+ ...props,
+ size: 'small',
+ },
+ slots: {
+ default: 'Label',
+ icon,
+ },
+ });
+
+ // Act
+ const tagIcon = page.locator(tagIconSelector);
+
+ // Assert
+ await expect(tagIcon).not.toBeVisible();
+ });
+ });
+ });
+
+ variantsToIsStrongStyle.forEach(({ variantName, bgStyle }) => {
+ test(`a "${variantName}" tag variant bg colour should be equivalent to "${bgStyle}"`, async ({ mount }) => {
+ const component = await mount(PieTag, {
+ props: {
+ ...props,
+ variant: variantName,
+ isStrong: true,
+ },
+ slots: {
+ default: 'Label',
+ },
+ });
+
+ const [currentBgStyle, expectedBgStyle] = await getShadowElementStylePropValues(component, componentSelector, ['--tag-bg-color', bgStyle]);
+
+ await expect(currentBgStyle).toBe(expectedBgStyle);
+ });
+ });
});
diff --git a/packages/components/pie-tag/test/visual/pie-tag.spec.ts b/packages/components/pie-tag/test/visual/pie-tag.spec.ts
index 3e8b9cc0eb..7bd1ee5aa8 100644
--- a/packages/components/pie-tag/test/visual/pie-tag.spec.ts
+++ b/packages/components/pie-tag/test/visual/pie-tag.spec.ts
@@ -1,14 +1,69 @@
import { test } from '@sand4rt/experimental-ct-web';
import percySnapshot from '@percy/playwright';
-import { PieTag, TagProps } from '@/index';
+import type {
+ PropObject, WebComponentPropValues, WebComponentTestInput,
+} from '@justeattakeaway/pie-webc-testing/src/helpers/defs.ts';
+import {
+ getAllPropCombinations, splitCombinationsByPropertyValue,
+} from '@justeattakeaway/pie-webc-testing/src/helpers/get-all-prop-combos.ts';
+import {
+ createTestWebComponent,
+} from '@justeattakeaway/pie-webc-testing/src/helpers/rendering.ts';
+import {
+ WebComponentTestWrapper,
+} from '@justeattakeaway/pie-webc-testing/src/helpers/components/web-component-test-wrapper/WebComponentTestWrapper.ts';
+import { percyWidths } from '@justeattakeaway/pie-webc-testing/src/percy/breakpoints.ts';
+import { sizes, variants } from '@/defs';
-test.describe('PieTag - Visual tests`', () => {
- test('should display the PieTag component successfully', async ({ page, mount }) => {
- await mount(PieTag, {
- props: {} as TagProps,
- });
+// TODO: Currently setting the slot to use a straight up SVG
+// This should be updated to use pie-icons-webc, but after some investigation, we think that we'll
+// need to convert the webc icons to use Lit, as currently the components don't work well in a Node env like Playwright
+// Atm, importing them like `import '@justeattakeaway/pie-icons-webc/icons/IconClose.js';` results in an `HTMLElement is not defined` error
+const icon = '';
- await percySnapshot(page, 'PieTag - Visual Test');
- });
+const props: PropObject = {
+ variant: variants,
+ size: sizes,
+ isStrong: [true, false],
+ iconSlot: ['', icon],
+};
+
+// Renders a HTML string with the given prop values
+const renderTestPieTag = (propVals: WebComponentPropValues) => `${propVals.iconSlot} Hello world`;
+
+const componentPropsMatrix: WebComponentPropValues[] = getAllPropCombinations(props);
+const componentPropsMatrixByVariant: Record = splitCombinationsByPropertyValue(componentPropsMatrix, 'variant');
+const componentVariants: string[] = Object.keys(componentPropsMatrixByVariant);
+
+// eslint-disable-next-line no-empty-pattern
+test.beforeEach(async ({ }, testInfo) => {
+ testInfo.setTimeout(testInfo.timeout + 40000);
});
+
+componentVariants.forEach((variant) => test(`should render all prop variations for Variant: ${variant}`, async ({ page, mount }) => {
+ await Promise.all(componentPropsMatrixByVariant[variant].map(async (combo: WebComponentPropValues) => {
+ const testComponent: WebComponentTestInput = createTestWebComponent(combo, renderTestPieTag);
+ const propKeyValues = `
+ size: ${testComponent.propValues.size},
+ variant: ${testComponent.propValues.variant},
+ isStrong: ${testComponent.propValues.isStrong},
+ iconSlot: ${testComponent.propValues.iconSlot ? 'with icon' : 'no icon'}`;
+ const darkMode = ['neutral-alternative'].includes(variant);
+
+ await mount(
+ WebComponentTestWrapper,
+ {
+ props: { propKeyValues, darkMode },
+ slots: {
+ component: testComponent.renderedString.trim(),
+ },
+ },
+ );
+ }));
+
+ // Follow up to remove in Jan
+ await page.waitForTimeout(5000);
+
+ await percySnapshot(page, `PIE Tag - Variant: ${variant}`, percyWidths);
+}));
diff --git a/packages/components/pie-webc-testing/src/helpers/get-shadow-element-style-prop-values.ts b/packages/components/pie-webc-testing/src/helpers/get-shadow-element-style-prop-values.ts
new file mode 100644
index 0000000000..2d1b68ba95
--- /dev/null
+++ b/packages/components/pie-webc-testing/src/helpers/get-shadow-element-style-prop-values.ts
@@ -0,0 +1,32 @@
+import type { Locator } from '@playwright/test';
+
+/**
+ * Gets the value of the given style properties from the shadow element
+ * @param element The custom element instance
+ * @param selector The selector of the element in the shadow
+ * @param props The style properties to get the values from
+ * @returns The values of the given style properties
+ */
+export const getShadowElementStylePropValues = async (element:Locator, selector:string, props:Array):Promise> => {
+ const data = { selector, props };
+
+ const evaluated = await element.evaluate((el, data) => {
+ const { selector, props } = data;
+
+ if (!el || !el.shadowRoot) {
+ throw new Error('getShadowElementStylePropValues: evaluate didn\'t return an element');
+ }
+
+ const shadowEl = el.shadowRoot.querySelector(selector);
+
+ if (!shadowEl) {
+ throw new Error('getShadowElementStylePropValues: no shadow element was found');
+ }
+
+ const shadowElStyle = getComputedStyle(shadowEl);
+
+ return props.map((prop) => shadowElStyle.getPropertyValue(prop).trim());
+ }, data);
+
+ return evaluated;
+};
diff --git a/packages/components/pie-webc-testing/src/helpers/index.ts b/packages/components/pie-webc-testing/src/helpers/index.ts
index e0c45fad77..c5f98cda21 100644
--- a/packages/components/pie-webc-testing/src/helpers/index.ts
+++ b/packages/components/pie-webc-testing/src/helpers/index.ts
@@ -1,3 +1,4 @@
+export * from './get-shadow-element-style-prop-values';
export * from './get-all-prop-combos';
export * from './defs';
export * from './rendering';
diff --git a/packages/tools/pie-icons-webc/build.js b/packages/tools/pie-icons-webc/build.js
index 6d66cc5b22..1400780c5f 100644
--- a/packages/tools/pie-icons-webc/build.js
+++ b/packages/tools/pie-icons-webc/build.js
@@ -44,6 +44,12 @@ export class ${name} extends LitElement implements IconProps {
width: var(--btn-icon-size);
height: var(--btn-icon-size);
}
+
+ :host-context(pie-tag) svg {
+ display: block;
+ width: var(--tag-icon-size);
+ height: var(--tag-icon-size);
+ }
\`;
@property({ type: String, reflect: true })