@justeattakeaway/pie-webc-core 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [Changed] - Improve TS typings in RTL mixin and provde extendable prop interface for components ([#623](https://github.com/justeattakeaway/pie/pull/623)) by [@jamieomaguire](https://github.com/jamieomaguire)
8
+
9
+ ## 0.5.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [Removed] - Function for CSS loading in Safari visual tests due to fix on Percy side. ([#575](https://github.com/justeattakeaway/pie/pull/575)) by [@JoshuaNg2332](https://github.com/JoshuaNg2332)
14
+
3
15
  ## 0.4.0
4
16
 
5
17
  ### Minor Changes
@@ -0,0 +1,9 @@
1
+ declare module '*.scss' {
2
+ const content: Record<string, string>;
3
+ export default content;
4
+ }
5
+
6
+ declare module '*.scss?inline' {
7
+ const content: Record<string, string>;
8
+ export default content;
9
+ }
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@justeattakeaway/pie-webc-core",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "PIE design system base classes, mixins and utilities for web components",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "author": "JustEatTakeaway - Design System Web Team",
8
8
  "license": "Apache-2.0",
9
+ "scripts": {
10
+ "test": "run -T vitest run"
11
+ },
9
12
  "volta": {
10
13
  "extends": "../../../package.json"
11
14
  }
@@ -3,17 +3,17 @@
3
3
  * If the property's value is `undefined`, `null` or empty string, an error is logged.
4
4
  * @returns {Function} - The decorator function.
5
5
  */
6
- export const requiredProperty = (componentName: string) => function (target: any, propertyKey: string) : void {
6
+ export const requiredProperty = <T>(componentName: string) => function validateRequiredProperty (target: object, propertyKey: string): void {
7
7
  const privatePropertyKey = `#${propertyKey}`;
8
8
 
9
9
  Object.defineProperty(target, propertyKey, {
10
- get () : any {
10
+ get (): T {
11
11
  return this[privatePropertyKey];
12
12
  },
13
- set (value: any) : void {
13
+ set (value: T): void {
14
14
  const oldValue = this[privatePropertyKey];
15
15
 
16
- if (value === undefined || value === null || value === '') {
16
+ if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
17
17
  console.error(`<${componentName}> Missing required attribute "${propertyKey}"`);
18
18
  }
19
19
  this[privatePropertyKey] = value;
@@ -22,3 +22,4 @@ export const requiredProperty = (componentName: string) => function (target: any
22
22
  },
23
23
  });
24
24
  };
25
+
@@ -0,0 +1,94 @@
1
+ import {
2
+ beforeEach,
3
+ afterEach,
4
+ describe,
5
+ it,
6
+ expect,
7
+ vi,
8
+ } from 'vitest';
9
+
10
+ import { requiredProperty } from '../required-property';
11
+
12
+ describe('requiredProperty', () => {
13
+ let consoleErrorSpy: unknown;
14
+
15
+ beforeEach(() => {
16
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
17
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
18
+ });
19
+
20
+ afterEach(() => {
21
+ vi.restoreAllMocks();
22
+ });
23
+
24
+ // Mock class to test the decorator
25
+ class MockComponent {
26
+ @requiredProperty('mock-component')
27
+ color?: string | null;
28
+
29
+ private _requestUpdateArgs = {};
30
+
31
+ requestUpdate (propertyKey: string, oldValue: unknown) {
32
+ this._requestUpdateArgs = { propertyKey, oldValue };
33
+ }
34
+
35
+ requestUpdateCalledWith () {
36
+ return this._requestUpdateArgs;
37
+ }
38
+ }
39
+
40
+ it('should log an error if the property is undefined', () => {
41
+ // Arrange
42
+ const mockComponent = new MockComponent();
43
+
44
+ // Act
45
+ mockComponent.color = undefined;
46
+
47
+ // Assert
48
+ expect(consoleErrorSpy).toHaveBeenCalled();
49
+ });
50
+
51
+ it('should log an error if the property is null', () => {
52
+ // Arrange
53
+ const mockComponent = new MockComponent();
54
+
55
+ // Act
56
+ mockComponent.color = null;
57
+
58
+ // Assert
59
+ expect(consoleErrorSpy).toHaveBeenCalled();
60
+ });
61
+
62
+ it('should log an error if the property is an empty string', () => {
63
+ // Arrange
64
+ const mockComponent = new MockComponent();
65
+
66
+ // Act
67
+ mockComponent.color = '';
68
+
69
+ // Assert
70
+ expect(consoleErrorSpy).toHaveBeenCalled();
71
+ });
72
+
73
+ it('should not log an error if the property is a non-empty string', () => {
74
+ // Arrange
75
+ const mockComponent = new MockComponent();
76
+
77
+ // Act
78
+ mockComponent.color = 'blue';
79
+
80
+ // Assert
81
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it('should call requestUpdate when the property is set', () => {
85
+ // Arrange
86
+ const mockComponent = new MockComponent();
87
+
88
+ // Act
89
+ mockComponent.color = 'blue';
90
+
91
+ // Assert
92
+ expect(mockComponent.requestUpdateCalledWith()).toStrictEqual({ propertyKey: 'color', oldValue: undefined });
93
+ });
94
+ });
@@ -0,0 +1,90 @@
1
+ import {
2
+ beforeEach,
3
+ afterEach,
4
+ describe,
5
+ it,
6
+ expect,
7
+ vi,
8
+ } from 'vitest';
9
+
10
+ import { validPropertyValues } from '../valid-property-values';
11
+
12
+ describe('validPropertyValues', () => {
13
+ let consoleErrorSpy: unknown;
14
+
15
+ beforeEach(() => {
16
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
17
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
18
+ });
19
+
20
+ afterEach(() => {
21
+ vi.restoreAllMocks();
22
+ });
23
+
24
+ // Mock class to test the decorator
25
+ class MockComponent {
26
+ @validPropertyValues('mock-component', ['red', 'green', 'blue'], 'red')
27
+ color = 'red';
28
+
29
+ private _requestUpdateArgs = {};
30
+
31
+ requestUpdate (propertyKey: string, oldValue: unknown) {
32
+ this._requestUpdateArgs = { propertyKey, oldValue };
33
+ }
34
+
35
+ requestUpdateCalledWith () {
36
+ return this._requestUpdateArgs;
37
+ }
38
+ }
39
+
40
+ it('should allow value to be updated with a valid value', () => {
41
+ // Arrange
42
+ const mockComponent = new MockComponent();
43
+
44
+ // Act
45
+ mockComponent.color = 'green';
46
+
47
+ // Assert
48
+ expect(mockComponent.color).toBe('green');
49
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
50
+ });
51
+
52
+ it('should fallback to the default value if an invalid value is assigned', () => {
53
+ // Arrange
54
+ const mockComponent = new MockComponent();
55
+
56
+ // Act
57
+ mockComponent.color = 'yellow';
58
+
59
+ // Assert
60
+ expect(mockComponent.color).toBe('red');
61
+ expect(consoleErrorSpy).toHaveBeenCalled();
62
+ });
63
+
64
+ it('should log an error message if an invalid value is assigned', () => {
65
+ // Arrange
66
+ const mockComponent = new MockComponent();
67
+
68
+ // Act
69
+ mockComponent.color = 'yellow';
70
+
71
+ // Assert
72
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
73
+ '<mock-component> Invalid value "yellow" provided for property "color".',
74
+ 'Must be one of: red | green | blue.',
75
+ 'Falling back to default value: "red"',
76
+ );
77
+ });
78
+
79
+ it('should call requestUpdate when the property is set', () => {
80
+ // Arrange
81
+ const mockComponent = new MockComponent();
82
+
83
+ // Act
84
+ mockComponent.color = 'yellow';
85
+
86
+ // Assert
87
+ expect(mockComponent.color).toBe('red');
88
+ expect(mockComponent.requestUpdateCalledWith()).toStrictEqual({ propertyKey: 'color', oldValue: 'red' });
89
+ });
90
+ });
@@ -5,14 +5,14 @@
5
5
  * @param defaultValue - The value to fall back on
6
6
  * @returns - The decorator function
7
7
  */
8
- export const validPropertyValues = (componentName: string, validValues: any[], defaultValue: any) => function (target: any, propertyKey: string) : void {
8
+ export const validPropertyValues = <T>(componentName: string, validValues: readonly T[], defaultValue: T) => function validatePropertyValues (target: object, propertyKey: string): void {
9
9
  const privatePropertyKey = `#${propertyKey}`;
10
10
 
11
11
  Object.defineProperty(target, propertyKey, {
12
- get () : any {
12
+ get (): T {
13
13
  return this[privatePropertyKey];
14
14
  },
15
- set (value: any) : void {
15
+ set (value: T): void {
16
16
  const oldValue = this[privatePropertyKey];
17
17
 
18
18
  if (!validValues.includes(value)) {
package/src/index.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export * from './mixins';
2
2
  export * from './decorators';
3
- export * from './test-helpers';
3
+ export * from './type-helpers';
@@ -2,18 +2,29 @@
2
2
  import { LitElement } from 'lit';
3
3
  import { property } from 'lit/decorators/property.js';
4
4
 
5
+ // According to TS, "A mixin class must have a constructor with a single rest parameter of type 'any[]'."
5
6
  type Constructor<T> = new (...args: any[]) => T;
6
7
 
7
- declare class RTLInterface {
8
- dir: string;
8
+ type htmlDirAttribute = 'ltr' | 'rtl' | 'auto';
9
+
10
+ /**
11
+ * Any component property interface that implements RTL should extend this interface. See the ModalProps interface for an example of this.
12
+ */
13
+ export interface RTLComponentProps {
14
+ dir: htmlDirAttribute
15
+ }
16
+
17
+ // This is just used by the dynamically constructed class below and does not need to be imported or referenced anywhere else
18
+ declare class _RTLInterface {
19
+ dir: htmlDirAttribute;
9
20
  isRTL: boolean;
10
21
  }
11
22
 
12
23
  export const RtlMixin =
13
24
  <T extends Constructor<LitElement>>(superClass: T) => {
14
- class RTLElement extends superClass {
15
- @property({ type: String })
16
- dir = '';
25
+ class RTLElement extends superClass implements _RTLInterface {
26
+ @property({ type: String, reflect: true })
27
+ dir : htmlDirAttribute = 'ltr';
17
28
 
18
29
  /**
19
30
  * Returns true if the element is in Right to Left mode.
@@ -37,5 +48,5 @@ export const RtlMixin =
37
48
  }
38
49
  }
39
50
 
40
- return RTLElement as Constructor<RTLInterface> & T;
51
+ return RTLElement as Constructor<_RTLInterface> & T;
41
52
  };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * A type alias that represents a specialized version of the built-in Map type, but with a custom get method that enforces stricter type checking
3
+ */
4
+ export type DependentMap<T> = Omit<Map<keyof T, T[keyof T]>, 'get'> & {
5
+ get<K extends keyof T>(key: K): T[K] | undefined;
6
+ }
@@ -0,0 +1 @@
1
+ export * from './DependentMap';
@@ -1 +0,0 @@
1
- export * from './web-component-test-wrapper/WebComponentTestWrapper';
@@ -1,58 +0,0 @@
1
- import { LitElement, html, unsafeCSS } from 'lit'; // eslint-disable-line import/no-extraneous-dependencies
2
- import { property } from 'lit/decorators.js';
3
- import styles from './webComponentTestWrapper.scss?inline';
4
-
5
- /**
6
- * This is a Web Component used for visual testing purposes.
7
- * It allows us to wrap a component we'd like to test in a container
8
- * that displays the component's props as a label.
9
- *
10
- * Components can be tested without this, but it's useful if your tests
11
- * require additional markup when testing a component.
12
- */
13
- export class WebComponentTestWrapper extends LitElement {
14
- static styles = unsafeCSS(styles);
15
-
16
- /**
17
- * The prop key and values to display above the component.
18
- * This should be a single string representing a comma separated list of prop key/value pairs.
19
- * Such as: 'size: small, isFullWidth: true'
20
- */
21
- @property({ type: String })
22
- propKeyValues = '';
23
-
24
- // Renders a string such as 'size: small, isFullWidth: true'
25
- // as HTML such as:
26
- // <p class="c-webComponentTestWrapper-label"><b>size</b>: <code>small</code></p>
27
- // <p class="c-webComponentTestWrapper-label"><b>isFullWidth</b>: <code>true</code></p>
28
- _renderPropKeyValues () {
29
- return this.propKeyValues.split(',').map((propKeyValueString) => {
30
- const [key, value] = propKeyValueString.split(':');
31
-
32
- return html`<p class="c-webComponentTestWrapper-label"><b>${key}</b>: <code>${value}</code></p>`;
33
- });
34
- }
35
-
36
- // eslint-disable-next-line class-methods-use-this
37
- render () {
38
- return html`
39
- <div class="c-webComponentTestWrapper">
40
- ${this._renderPropKeyValues()}
41
- <div class="c-webComponentTestWrapper-slot">
42
- <slot></slot>
43
- </div>
44
- </div>`;
45
- }
46
- }
47
-
48
- const componentSelector = 'web-component-test-wrapper';
49
-
50
- if (!customElements.get(componentSelector)) {
51
- customElements.define(componentSelector, WebComponentTestWrapper);
52
- }
53
-
54
- declare global {
55
- interface HTMLElementTagNameMap {
56
- [componentSelector]: WebComponentTestWrapper;
57
- }
58
- }
@@ -1,20 +0,0 @@
1
- .c-webComponentTestWrapper {
2
- padding-block: var(--dt-spacing-c);
3
- padding-inline: var(--dt-spacing-e);
4
- font-family: var(--dt-font-interactive-m-family);
5
- font-size: calc(var(--dt-font-size-20) * 1px);
6
- border: 1px solid var(--dt-color-background-dark);
7
- display: grid;
8
- grid-template-columns: 1fr 1fr;
9
- }
10
-
11
- .c-webComponentTestWrapper-label {
12
- margin-block: var(--dt-spacing-c);
13
- }
14
-
15
- .c-webComponentTestWrapper-slot {
16
- padding: var(--dt-spacing-c);
17
- border: 1px dashed var(--dt-color-background-dark);
18
- grid-column: 1 / 3;
19
- margin-block-start: var(--dt-spacing-c);
20
- }
@@ -1,15 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- export type PropObject = {
3
- [key: string]: any;
4
- };
5
-
6
- export type WebComponentPropValues = {
7
- [key: string]: any;
8
- };
9
-
10
- export type WebComponentTestInput = {
11
- propValues: WebComponentPropValues;
12
- renderedString: string;
13
- };
14
-
15
- export type WebComponentRenderFn = (propVals: WebComponentPropValues) => string;
@@ -1,83 +0,0 @@
1
- import { PropObject, WebComponentPropValues } from './defs';
2
-
3
- /**
4
- * Generate all possible combinations of properties for a given object.
5
- *
6
- * @param {PropObject} obj - The object containing properties for which combinations are to be generated.
7
- * Each property value can any data type.
8
- *
9
- * @returns {WebComponentPropValues[]} An array of objects, where each object is a unique combination of web component property values.
10
- */
11
- export const getAllPropCombinations = (obj: PropObject): WebComponentPropValues[] => {
12
- const propertyKeys = Object.keys(obj);
13
- const combinationsOfPropValues: WebComponentPropValues[] = [];
14
-
15
- // This function generates combinations of properties from a given object.
16
- // It does this by recursively concatenating each property value to an 'accumulatedPropertyValues' array,
17
- // and adding the resulting combination to a 'WebComponentPropValues' array when it reaches the end of the keys.
18
- function generatePropCombinations (accumulatedPropertyValues: any[], i: number): void {
19
- // When 'i' equals the length of 'keys', we've reached the end of the keys.
20
- // This means we've formed a complete combination.
21
- if (i === propertyKeys.length) {
22
- // Create an empty object to hold the current WebComponentPropValues.
23
- const combo: WebComponentPropValues = {};
24
- // Loop over the 'accumulatedPropertyValues' array, which contains the property values for this combination.
25
- for (let j = 0; j < accumulatedPropertyValues.length; j++) {
26
- // Assign each value to the corresponding key in the 'combo' object.
27
- combo[propertyKeys[j]] = accumulatedPropertyValues[j];
28
- }
29
- // Add this combination to the 'combinationsOfPropValues' array.
30
- combinationsOfPropValues.push(combo);
31
- // End the recursion for this branch.
32
- return;
33
- }
34
-
35
- // Get the current key and its values from the input object.
36
- const propertyKey = propertyKeys[i];
37
- const propertyValues = obj[propertyKey];
38
-
39
- if (typeof propertyValues === 'boolean') {
40
- // If the values for this key are a boolean, we generate two combinations:
41
- // one with the value 'true', and one with the value 'false'.
42
- generatePropCombinations([...accumulatedPropertyValues, true], i + 1);
43
- generatePropCombinations([...accumulatedPropertyValues, false], i + 1);
44
- } else if (Array.isArray(propertyValues)) {
45
- // If the values for this key are an array, we generate a combination for each value in the array.
46
- for (let j = 0; j < propertyValues.length; j++) {
47
- generatePropCombinations([...accumulatedPropertyValues, propertyValues[j]], i + 1);
48
- }
49
- } else {
50
- // If the values for this key are neither a boolean nor an array,
51
- // we simply generate a single combination with the value as is.
52
- generatePropCombinations([...accumulatedPropertyValues, propertyValues], i + 1);
53
- }
54
- }
55
-
56
- generatePropCombinations([], 0);
57
-
58
- return combinationsOfPropValues;
59
- };
60
-
61
- /**
62
- * Splits an array of component prop combinations by a particular property value.
63
- *
64
- * @param {WebComponentPropValues[]} propValueCombinations - The array of combinations to split.
65
- * @param {string} property - The property to split by.
66
- *
67
- * @returns {Record<string, WebComponentPropValues[]>} An object mapping each unique property value to the combinations that have it.
68
- */
69
- export const splitCombinationsByPropertyValue = (propValueCombinations: WebComponentPropValues[], property: string): Record<string, WebComponentPropValues[]> => propValueCombinations
70
- .reduce((splitCombinations: Record<string, WebComponentPropValues[]>, combination: WebComponentPropValues) => {
71
- const propertyValue = combination[property];
72
- const propertyValueKey = String(propertyValue);
73
-
74
- if (!(propertyValueKey in splitCombinations)) {
75
- splitCombinations[propertyValueKey] = [];
76
- }
77
-
78
- // Add the current combination to the array for its property value
79
- splitCombinations[propertyValueKey].push(combination);
80
-
81
- return splitCombinations;
82
- }, {});
83
-
@@ -1,5 +0,0 @@
1
- export * from './get-all-prop-combos';
2
- export * from './defs';
3
- export * from './rendering';
4
- export * from './components';
5
- export * from './percy-lit-options';
@@ -1,37 +0,0 @@
1
- ///*
2
- ///* This code snippet was provided by Percy as a way to get our Lit components working correctly when running against the Safari browser.
3
- ///* Safari blocks requests coming from localhost, resulting in our CSS not loading when requested.
4
- ///* This function is passed to Percy and executed against the serialised DOM to rewrite all 'localhost' urls to 'render.percy.local'.
5
- ///* This is a temporary workaround, and a long-term solution will be implemented by Percy at a later date.
6
- ///*
7
- const percyOptions = {
8
- domTransformation: (documentElement: any) => {
9
- function updateLinks(root : any) {
10
- root.querySelectorAll('[data-percy-adopted-stylesheets-serialized]').forEach((link : any) => {
11
- console.log(link);
12
- let href = link.getAttribute('data-percy-serialized-attribute-href');
13
- href = href.replace(/localhost[:\d+]*/, 'render.percy.local');
14
- link.setAttribute('data-percy-serialized-attribute-href', href);
15
- });
16
-
17
- root.querySelectorAll('[data-percy-shadow-host]')
18
- .forEach((shadowHost : any) => {
19
- console.log(shadowHost);
20
- if (shadowHost?.shadowRoot)
21
- updateLinks(shadowHost.shadowRoot);
22
- }
23
- );
24
- }
25
- updateLinks(documentElement);
26
- }
27
- }
28
-
29
- const getLitPercyOptions = () => {
30
- const options = {
31
- domTransformation: percyOptions.domTransformation.toString()
32
- }
33
-
34
- return options;
35
- }
36
-
37
- export { getLitPercyOptions }
@@ -1,10 +0,0 @@
1
- import { WebComponentPropValues, WebComponentTestInput, WebComponentRenderFn } from './defs';
2
-
3
- export function createTestWebComponent (propVals: WebComponentPropValues, componentRenderFn: WebComponentRenderFn): WebComponentTestInput {
4
- const testComponent: WebComponentTestInput = {
5
- propValues: propVals,
6
- renderedString: componentRenderFn(propVals),
7
- };
8
-
9
- return testComponent;
10
- }