@justeattakeaway/pie-webc-core 0.0.0-snapshot-release-20240104162246
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/.eslintignore +5 -0
- package/.turbo/turbo-build.log +7 -0
- package/CHANGELOG.md +119 -0
- package/README.md +74 -0
- package/declaration.d.ts +9 -0
- package/dist/index.js +65 -0
- package/index.ts +1 -0
- package/package.json +28 -0
- package/playwright/index.html +56 -0
- package/playwright/index.ts +1 -0
- package/playwright-lit.config.ts +18 -0
- package/src/decorators/index.ts +3 -0
- package/src/decorators/required-property.ts +21 -0
- package/src/decorators/valid-property-values.ts +29 -0
- package/src/functions/defineCustomElement.ts +47 -0
- package/src/functions/index.ts +1 -0
- package/src/index.ts +3 -0
- package/src/mixins/formControl/formControlMixin.ts +58 -0
- package/src/mixins/index.ts +4 -0
- package/src/mixins/rtl/rtlMixin.ts +91 -0
- package/src/mixins/types/GenericConstructor.ts +7 -0
- package/src/test/decorators/required-property.spec.ts +73 -0
- package/src/test/decorators/valid-property-values.spec.ts +68 -0
- package/src/test/functions/defineCustomElement.spec.ts +81 -0
- package/src/test/mixins/formControlMixin/MockComponent.ts +19 -0
- package/src/test/mixins/formControlMixin/formControlMixin.browser.spec.ts +76 -0
- package/src/test/mixins/rtlMixin/rtlMixin.client.spec.ts +92 -0
- package/src/test/mixins/rtlMixin/rtlMixin.server.spec.ts +62 -0
- package/tsconfig.json +8 -0
- package/vite.config.js +19 -0
package/.eslintignore
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.0.0-snapshot-release-20240104162246
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [Removed] - unused LitElementMixin type as we do not need it anywhere ([#1145](https://github.com/justeattakeaway/pie/pull/1145)) by [@jamieomaguire](https://github.com/jamieomaguire)
|
|
8
|
+
|
|
9
|
+
- [Changed] - Updated LitElement imports where it is only used as a type to include the 'type' keyword (fixes a TS error in some consumers) ([#1143](https://github.com/justeattakeaway/pie/pull/1143)) by [@jamieomaguire](https://github.com/jamieomaguire)
|
|
10
|
+
|
|
11
|
+
- [Changed] - Use proper interface keyword for RTLmixin interface. Was using a class before which is incorrect. ([#1145](https://github.com/justeattakeaway/pie/pull/1145)) by [@jamieomaguire](https://github.com/jamieomaguire)
|
|
12
|
+
|
|
13
|
+
## 0.14.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- [Added] - Form Control Mixin for adding native form association behaviour to web components ([#1142](https://github.com/justeattakeaway/pie/pull/1142)) by [@jamieomaguire](https://github.com/jamieomaguire)
|
|
18
|
+
|
|
19
|
+
- [Changed] - Re organised test structure and added playwright testing ([#1142](https://github.com/justeattakeaway/pie/pull/1142)) by [@jamieomaguire](https://github.com/jamieomaguire)
|
|
20
|
+
|
|
21
|
+
## 0.13.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- [Added] - Build webc-core using vite ([#1037](https://github.com/justeattakeaway/pie/pull/1037)) by [@xander-marjoram](https://github.com/xander-marjoram)
|
|
26
|
+
|
|
27
|
+
- [Changed] - Move lit dependency from storybook to webc-core ([#1037](https://github.com/justeattakeaway/pie/pull/1037)) by [@xander-marjoram](https://github.com/xander-marjoram)
|
|
28
|
+
|
|
29
|
+
## 0.12.0
|
|
30
|
+
|
|
31
|
+
### Minor Changes
|
|
32
|
+
|
|
33
|
+
- [Changed] - `@validPropertyValues` and `@requiredProperty` decorators to support Lit 3. ([#1027](https://github.com/justeattakeaway/pie/pull/1027)) by [@raoufswe](https://github.com/raoufswe)
|
|
34
|
+
|
|
35
|
+
## 0.12.0-next.0
|
|
36
|
+
|
|
37
|
+
### Minor Changes
|
|
38
|
+
|
|
39
|
+
- [Changed] - `@validPropertyValues` and `@requiredProperty` decorators to support Lit 3. ([#1025](https://github.com/justeattakeaway/pie/pull/1025)) by [@raoufswe](https://github.com/raoufswe)
|
|
40
|
+
|
|
41
|
+
## 0.11.0
|
|
42
|
+
|
|
43
|
+
### Minor Changes
|
|
44
|
+
|
|
45
|
+
- [Added] - defineCustomElement helper function to protect against duplicate component registration errors ([#905](https://github.com/justeattakeaway/pie/pull/905)) by [@jamieomaguire](https://github.com/jamieomaguire)
|
|
46
|
+
|
|
47
|
+
## 0.10.0
|
|
48
|
+
|
|
49
|
+
### Minor Changes
|
|
50
|
+
|
|
51
|
+
- [Fixed] - License removed from packages ([#869](https://github.com/justeattakeaway/pie/pull/869)) by [@kevinrodrigues](https://github.com/kevinrodrigues)
|
|
52
|
+
|
|
53
|
+
## 0.9.1
|
|
54
|
+
|
|
55
|
+
### Patch Changes
|
|
56
|
+
|
|
57
|
+
- [Changed] - Align author field for all packages ([#852](https://github.com/justeattakeaway/pie/pull/852)) by [@xander-marjoram](https://github.com/xander-marjoram)
|
|
58
|
+
|
|
59
|
+
## 0.9.0
|
|
60
|
+
|
|
61
|
+
### Minor Changes
|
|
62
|
+
|
|
63
|
+
- [Changed] - Rewrite RTL mixin to remove the dir property. The LitElement base class is a subclass of HTMLElement, so a Lit component inherits all of the standard HTMLElement properties and methods. This means that the dir property is already available on the component and we don't need to add it again. [Reference](https://lit.dev/docs/components/defining/#a-lit-component-is-an-html-element). During SSR, if no dir is provided, it will be inferred from the document.documentElement once it's instantiated on the client. ([#818](https://github.com/justeattakeaway/pie/pull/818)) by [@jamieomaguire](https://github.com/jamieomaguire)
|
|
64
|
+
|
|
65
|
+
- [Changed] - Removed the DependantMap type and replaced all references with Lit's own PropertyValues helper. This provides exactly the same strongly typed map for a component's properties which makes our own type a little redundant. [Reference](https://lit.dev/docs/components/lifecycle/#typescript-types-for-changedproperties) ([#818](https://github.com/justeattakeaway/pie/pull/818)) by [@jamieomaguire](https://github.com/jamieomaguire)
|
|
66
|
+
|
|
67
|
+
## 0.8.0
|
|
68
|
+
|
|
69
|
+
### Minor Changes
|
|
70
|
+
|
|
71
|
+
- [Changed] - Update RTL mixin to infer dir by default and document ([#745](https://github.com/justeattakeaway/pie/pull/745)) by [@raoufswe](https://github.com/raoufswe)
|
|
72
|
+
|
|
73
|
+
## 0.7.0
|
|
74
|
+
|
|
75
|
+
### Minor Changes
|
|
76
|
+
|
|
77
|
+
- [Changed] - Add js/ts linting and fix errors ([#653](https://github.com/justeattakeaway/pie/pull/653)) by [@jamieomaguire](https://github.com/jamieomaguire)
|
|
78
|
+
|
|
79
|
+
## 0.6.0
|
|
80
|
+
|
|
81
|
+
### Minor Changes
|
|
82
|
+
|
|
83
|
+
- [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)
|
|
84
|
+
|
|
85
|
+
## 0.5.0
|
|
86
|
+
|
|
87
|
+
### Minor Changes
|
|
88
|
+
|
|
89
|
+
- [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)
|
|
90
|
+
|
|
91
|
+
## 0.4.0
|
|
92
|
+
|
|
93
|
+
### Minor Changes
|
|
94
|
+
|
|
95
|
+
- [Added] - New function to fix CSS loading in Safari Percy tests ([#534](https://github.com/justeattakeaway/pie/pull/534)) by [@siggerzz](https://github.com/siggerzz)
|
|
96
|
+
|
|
97
|
+
## 0.3.0
|
|
98
|
+
|
|
99
|
+
### Minor Changes
|
|
100
|
+
|
|
101
|
+
- [Added] - new WebComponentTestWrapper test component for adding labels to visual tests ([#519](https://github.com/justeattakeaway/pie/pull/519)) by [@jamieomaguire](https://github.com/jamieomaguire)
|
|
102
|
+
|
|
103
|
+
- [Added] - a helper module to generate all prop variants of a given component ([#499](https://github.com/justeattakeaway/pie/pull/499)) by [@jamieomaguire](https://github.com/jamieomaguire)
|
|
104
|
+
|
|
105
|
+
- [Added] - `required` property decorator ([#530](https://github.com/justeattakeaway/pie/pull/530)) by [@raoufswe](https://github.com/raoufswe)
|
|
106
|
+
|
|
107
|
+
- [Changed] - Make pie-webc-core public ([#523](https://github.com/justeattakeaway/pie/pull/523)) by [@jamieomaguire](https://github.com/jamieomaguire)
|
|
108
|
+
|
|
109
|
+
## 0.2.0
|
|
110
|
+
|
|
111
|
+
### Minor Changes
|
|
112
|
+
|
|
113
|
+
- [Added] - Property validation decorator ([#491](https://github.com/justeattakeaway/pie/pull/491)) by [@jamieomaguire](https://github.com/jamieomaguire)
|
|
114
|
+
|
|
115
|
+
## 0.1.0
|
|
116
|
+
|
|
117
|
+
### Minor Changes
|
|
118
|
+
|
|
119
|
+
- [Added] - RTL Mixin and base project ([#478](https://github.com/justeattakeaway/pie/pull/478)) by [@jamieomaguire](https://github.com/jamieomaguire)
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img align="center" src="../../../readme_image.png" height="200" alt="">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/@justeattakeaway/pie-webc-core">
|
|
7
|
+
<img alt="GitHub Workflow Status" src="https://img.shields.io/npm/v/@justeattakeaway/pie-webc-core.svg">
|
|
8
|
+
</a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
# pie-webc-core
|
|
12
|
+
|
|
13
|
+
1. [Introduction](#introduction)
|
|
14
|
+
2. [Installation](#installation)
|
|
15
|
+
3. [Importing the package](#importing-the-package)
|
|
16
|
+
4. [Dependencies](#dependencies)
|
|
17
|
+
5. [Contributing](#contributing)
|
|
18
|
+
6. [Testing](#testing)
|
|
19
|
+
|
|
20
|
+
## Introduction
|
|
21
|
+
|
|
22
|
+
A core library for PIE web components which contains classes, mixins, utilities and anything other functionality that can be shared across web components.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
To add `pie-webc-core` to your component, run the following on your command line:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
$ yarn add @justeattakeaway/pie-webc-core
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Importing the package
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
import { RequiredProperty, RtlMixin } from '@justeattakeaway/pie-webc-core';
|
|
37
|
+
|
|
38
|
+
export class MyComponent extends RtlMixin(LitElement) implements MyComponentProps {
|
|
39
|
+
// ...
|
|
40
|
+
|
|
41
|
+
@property({ type: String })
|
|
42
|
+
@requiredProperty('my-component')
|
|
43
|
+
public value!: string;
|
|
44
|
+
|
|
45
|
+
// ...
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Dependencies
|
|
50
|
+
|
|
51
|
+
This package provides `lit` as a dependency which means the package that depends on `@justeattakeaway/pie-webc-core` will automatically install `lit` into the `node_modules` folder of its consuming application.
|
|
52
|
+
|
|
53
|
+
Other dependencies may be added in future to make it easier for people to install and use our components.
|
|
54
|
+
|
|
55
|
+
## Contributing
|
|
56
|
+
|
|
57
|
+
Check out our [contributing guide](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide) for more information on [local development](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide#local-development).
|
|
58
|
+
|
|
59
|
+
To run the unit tests, simply run `yarn test --filter=pie-webc-core` from the root of the monorepo.
|
|
60
|
+
|
|
61
|
+
## Testing
|
|
62
|
+
|
|
63
|
+
- Currently we have two methods of testing our core utilities:
|
|
64
|
+
- Unit tests using Vitest
|
|
65
|
+
- Browser tests using Playwright
|
|
66
|
+
|
|
67
|
+
### Unit tests
|
|
68
|
+
We write unit tests for small pieces of functionality that can be tested in isolation. These could be things such as class decorators or utility functions. However, sometimes testing in an isolated node environment is not enough. This is where Browser tests come in.
|
|
69
|
+
|
|
70
|
+
### Browser tests
|
|
71
|
+
We write browser tests for functionality that requires a browser environment to test. This could be things such as component class mixins. For these, we run our tests using Playwright. This allows us to run our tests in a real browser environment and test things such as DOM manipulation and events. A useful pattern for this kind of testing is to write a mock component that uses the mixin you want to test. This allows you to test the mixin in isolation without having to worry about the implementation of the component itself.
|
|
72
|
+
|
|
73
|
+
### Naming and running tests
|
|
74
|
+
Currently, for writing unit tests we simply name the file `**/*.spec.ts`. To write browser tests, we name the file `**/*.browser.spec.ts`. This allows us to run all unit tests using `yarn test --filter=pie-webc-core` and all browser tests using `yarn test:browsers --filter=pie-webc-core`.
|
package/declaration.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { isServer as u } from "lit";
|
|
2
|
+
const l = (e) => {
|
|
3
|
+
class t extends e {
|
|
4
|
+
/**
|
|
5
|
+
* A getter to determine whether the text direction is right-to-left (RTL).
|
|
6
|
+
* If the `dir` property is present on the component, it will be used to determine the text direction.
|
|
7
|
+
* If running on the client-side (not SSR) and the `dir` property is not present, the text direction will be inferred
|
|
8
|
+
* from the document's root element. This inference is not available during SSR.
|
|
9
|
+
* In all other cases, it will return `false`, indicating a left-to-right (LTR) text direction.
|
|
10
|
+
*
|
|
11
|
+
* @returns {boolean} - Returns `true` if the text direction is RTL, otherwise `false`.
|
|
12
|
+
*/
|
|
13
|
+
get isRTL() {
|
|
14
|
+
return this.dir ? this.dir === "rtl" : !u && !this.dir ? document.documentElement.getAttribute("dir") === "rtl" : !1;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return t;
|
|
18
|
+
}, a = (e) => {
|
|
19
|
+
class t extends e {
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
constructor(...i) {
|
|
22
|
+
super(...i), this._internals = this.attachInternals();
|
|
23
|
+
}
|
|
24
|
+
get form() {
|
|
25
|
+
return this._internals.form;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return t.formAssociated = !0, t;
|
|
29
|
+
}, f = (e, t, n) => function(o, r) {
|
|
30
|
+
const s = `#${r}`;
|
|
31
|
+
Object.defineProperty(o, r, {
|
|
32
|
+
get() {
|
|
33
|
+
return this[s];
|
|
34
|
+
},
|
|
35
|
+
set(c) {
|
|
36
|
+
t.includes(c) ? this[s] = c : (console.error(
|
|
37
|
+
`<${e}> Invalid value "${c}" provided for property "${r}".`,
|
|
38
|
+
`Must be one of: ${t.join(" | ")}.`,
|
|
39
|
+
`Falling back to default value: "${n}"`
|
|
40
|
+
), this[s] = n);
|
|
41
|
+
},
|
|
42
|
+
configurable: !0
|
|
43
|
+
});
|
|
44
|
+
}, m = (e) => function(n, i) {
|
|
45
|
+
const o = `#${i}`;
|
|
46
|
+
Object.defineProperty(n, i, {
|
|
47
|
+
get() {
|
|
48
|
+
return this[o];
|
|
49
|
+
},
|
|
50
|
+
set(r) {
|
|
51
|
+
(r == null || typeof r == "string" && r.trim() === "") && console.error(`<${e}> Missing required attribute "${i}"`), this[o] = r;
|
|
52
|
+
},
|
|
53
|
+
configurable: !0
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
function h(e, t) {
|
|
57
|
+
customElements.get(e) ? console.warn(`PIE Web Component: "${e}" has already been defined. Please ensure the component is only being defined once in your application.`) : customElements.define(e, t);
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
a as FormControlMixin,
|
|
61
|
+
l as RtlMixin,
|
|
62
|
+
h as defineCustomElement,
|
|
63
|
+
m as requiredProperty,
|
|
64
|
+
f as validPropertyValues
|
|
65
|
+
};
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './src';
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@justeattakeaway/pie-webc-core",
|
|
3
|
+
"version": "0.0.0-snapshot-release-20240104162246",
|
|
4
|
+
"description": "PIE design system base classes, mixins and utilities for web components",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"author": "Just Eat Takeaway.com - Design System Team",
|
|
8
|
+
"license": "Apache-2.0",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "run -T vite build",
|
|
11
|
+
"lint:scripts": "run -T eslint .",
|
|
12
|
+
"lint:scripts:fix": "run -T eslint . --fix",
|
|
13
|
+
"test": "run -T vitest run",
|
|
14
|
+
"test:ci": "yarn test",
|
|
15
|
+
"test:browsers": "npx playwright test -c ./playwright-lit.config.ts",
|
|
16
|
+
"test:browsers:ci": "yarn test:browsers",
|
|
17
|
+
"test:watch": "run -T vitest"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"lit": "3.1.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@justeattakeaway/pie-components-config": "0.7.0"
|
|
24
|
+
},
|
|
25
|
+
"volta": {
|
|
26
|
+
"extends": "../../../package.json"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="preload" href="https://d30v2pzvrfyzpo.cloudfront.net/fonts/JETSansDigital-Regular-optimised.woff2" as="font" type="font/woff2" crossorigin>
|
|
7
|
+
<link rel="preload" href="https://d30v2pzvrfyzpo.cloudfront.net/fonts/JETSansDigital-Bold-optimised.woff2" as="font" type="font/woff2" crossorigin>
|
|
8
|
+
<link rel="preload" href="https://d30v2pzvrfyzpo.cloudfront.net/fonts/JETSansDigital-ExtraBold-optimised.woff2" as="font" type="font/woff2" crossorigin>
|
|
9
|
+
<style>
|
|
10
|
+
@font-face {
|
|
11
|
+
font-family: JETSansDigital;
|
|
12
|
+
src: url('https://d30v2pzvrfyzpo.cloudfront.net/fonts/JETSansDigital-Regular-optimised.woff2') format("woff2"),
|
|
13
|
+
url('https://d30v2pzvrfyzpo.cloudfront.net/fonts/JETSansDigital-Regular-optimised.woff') format("woff");
|
|
14
|
+
font-weight: 400;
|
|
15
|
+
font-display: swap;
|
|
16
|
+
}
|
|
17
|
+
@font-face {
|
|
18
|
+
font-family: JETSansDigital;
|
|
19
|
+
src: url('https://d30v2pzvrfyzpo.cloudfront.net/fonts/JETSansDigital-Bold-optimised.woff2') format("woff2"),
|
|
20
|
+
url('https://d30v2pzvrfyzpo.cloudfront.net/fonts/JETSansDigital-Bold-optimised.woff') format("woff");
|
|
21
|
+
font-weight: 700;
|
|
22
|
+
font-display: swap;
|
|
23
|
+
}
|
|
24
|
+
@font-face {
|
|
25
|
+
font-family: JETSansDigital;
|
|
26
|
+
src: url('https://d30v2pzvrfyzpo.cloudfront.net/fonts/JETSansDigital-ExtraBold-optimised.woff2') format("woff2"),
|
|
27
|
+
url('https://d30v2pzvrfyzpo.cloudfront.net/fonts/JETSansDigital-ExtraBold-optimised.woff') format("woff");
|
|
28
|
+
font-weight: 800;
|
|
29
|
+
font-display: swap;
|
|
30
|
+
}
|
|
31
|
+
body {
|
|
32
|
+
font-feature-settings: "tnum"; /* Enable tabular numbers */
|
|
33
|
+
}
|
|
34
|
+
/* basic styles to center align components and give them some spacing */
|
|
35
|
+
#root {
|
|
36
|
+
padding: 1em;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#root > * {
|
|
40
|
+
display: block;
|
|
41
|
+
margin-inline: auto;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#root > * + * {
|
|
45
|
+
margin-top: 1em;
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
48
|
+
<title>Testing Page</title>
|
|
49
|
+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/@justeat/pie-design-tokens/dist/jet.css" />
|
|
50
|
+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/@justeat/pie-design-tokens/dist/jet-hsl-colors.css" />
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<div id="root"></div>
|
|
54
|
+
<script type="module" src="./index.ts"></script>
|
|
55
|
+
</body>
|
|
56
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Import common styles here
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@sand4rt/experimental-ct-web';
|
|
2
|
+
import { getPlaywrightConfig as getPlaywrightConfigOG } from '@justeattakeaway/pie-components-config';
|
|
3
|
+
import type { PlaywrightTestConfig } from '@playwright/test';
|
|
4
|
+
|
|
5
|
+
const getPlaywrightConfig = () : PlaywrightTestConfig => ({
|
|
6
|
+
...getPlaywrightConfigOG(),
|
|
7
|
+
projects: [
|
|
8
|
+
{
|
|
9
|
+
name: 'component:chrome',
|
|
10
|
+
use: {
|
|
11
|
+
...devices['Desktop Chrome'],
|
|
12
|
+
},
|
|
13
|
+
testMatch: ['**/test/**/*.browser.spec.ts'],
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export default defineConfig(getPlaywrightConfig());
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A decorator for marking a property as required.
|
|
3
|
+
* If the property's value is `undefined`, `null` or empty string, an error is logged.
|
|
4
|
+
* @returns {Function} - The decorator function.
|
|
5
|
+
*/
|
|
6
|
+
export const requiredProperty = <T>(componentName: string) => function validateRequiredProperty (target: object, propertyKey: string): void {
|
|
7
|
+
const privatePropertyKey = `#${propertyKey}`;
|
|
8
|
+
|
|
9
|
+
Object.defineProperty(target, propertyKey, {
|
|
10
|
+
get (): T {
|
|
11
|
+
return this[privatePropertyKey];
|
|
12
|
+
},
|
|
13
|
+
set (value: T): void {
|
|
14
|
+
if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
|
|
15
|
+
console.error(`<${componentName}> Missing required attribute "${propertyKey}"`);
|
|
16
|
+
}
|
|
17
|
+
this[privatePropertyKey] = value;
|
|
18
|
+
},
|
|
19
|
+
configurable: true,
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A decorator for specifying a list of valid values for a property.
|
|
3
|
+
* If this property's setter is called with an invalid value, an error is logged and the default value will be assigned instead.
|
|
4
|
+
* @param validValues - The array of valid values
|
|
5
|
+
* @param defaultValue - The value to fall back on
|
|
6
|
+
* @returns - The decorator function
|
|
7
|
+
*/
|
|
8
|
+
export const validPropertyValues = <T>(componentName: string, validValues: readonly T[], defaultValue: T) => function validatePropertyValues (target: object, propertyKey: string) : void {
|
|
9
|
+
const privatePropertyKey = `#${propertyKey}`;
|
|
10
|
+
|
|
11
|
+
Object.defineProperty(target, propertyKey, {
|
|
12
|
+
get (): T {
|
|
13
|
+
return this[privatePropertyKey];
|
|
14
|
+
},
|
|
15
|
+
set (value: T): void {
|
|
16
|
+
if (!validValues.includes(value)) {
|
|
17
|
+
console.error(
|
|
18
|
+
`<${componentName}> Invalid value "${value}" provided for property "${propertyKey}".`,
|
|
19
|
+
`Must be one of: ${validValues.join(' | ')}.`,
|
|
20
|
+
`Falling back to default value: "${defaultValue}"`,
|
|
21
|
+
);
|
|
22
|
+
this[privatePropertyKey] = defaultValue;
|
|
23
|
+
} else {
|
|
24
|
+
this[privatePropertyKey] = value;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
configurable: true,
|
|
28
|
+
});
|
|
29
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { LitElement } from 'lit';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Defines a web component, ensuring that the component is only defined once in the Custom Element Registry.
|
|
5
|
+
*
|
|
6
|
+
* If the component has already been defined with the same name, a warning is logged to the console.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} elementSelector - The selector of the custom element. Must be a valid custom element selector, containing a dash. I.e. 'my-component'
|
|
9
|
+
* @param {typeof LitElement} elementClass - The class that defines the custom element, extending LitElement.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
*
|
|
13
|
+
* ```javascript
|
|
14
|
+
* import { css, html, LitElement } from 'lit';
|
|
15
|
+
*
|
|
16
|
+
* // IMPORTANT: Add the following JSDoc comment above your class setting the `@tagname` to the selector you want to use. - This is mandatory.
|
|
17
|
+
* // "@tagname my-component"
|
|
18
|
+
* class MyComponent extends LitElement {
|
|
19
|
+
* static styles = css`
|
|
20
|
+
* :host {
|
|
21
|
+
* display: block;
|
|
22
|
+
* padding: 16px;
|
|
23
|
+
* color: var(--my-component-text-color, black);
|
|
24
|
+
* }
|
|
25
|
+
* `;
|
|
26
|
+
*
|
|
27
|
+
* render() {
|
|
28
|
+
* return html`
|
|
29
|
+
* <p>My custom element content goes here!</p>
|
|
30
|
+
* `;
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* defineCustomElement('my-component', MyComponent);
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* NOTE: The inclusion of the `@tagname` JSDoc comment above your class is essential for correct functionality. Ensure it matches the tag you're registering.
|
|
38
|
+
*
|
|
39
|
+
* @returns {void}
|
|
40
|
+
*/
|
|
41
|
+
export function defineCustomElement (elementSelector: string, elementClass: typeof LitElement): void {
|
|
42
|
+
if (!customElements.get(elementSelector)) {
|
|
43
|
+
customElements.define(elementSelector, elementClass);
|
|
44
|
+
} else {
|
|
45
|
+
console.warn(`PIE Web Component: "${elementSelector}" has already been defined. Please ensure the component is only being defined once in your application.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './defineCustomElement';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { LitElement } from 'lit';
|
|
2
|
+
import type { GenericConstructor } from '../types/GenericConstructor';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Interface for FormControl behavior.
|
|
6
|
+
*/
|
|
7
|
+
export interface FormControlInterface {
|
|
8
|
+
_internals: ElementInternals;
|
|
9
|
+
get form(): HTMLFormElement | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Mixin to add form control behaviors to a LitElement.
|
|
14
|
+
* This mixin adds properties and methods to handle form-associated custom elements.
|
|
15
|
+
*
|
|
16
|
+
* @param superClass - The LitElement class to extend with form control functionality.
|
|
17
|
+
* @returns A class extending both the provided LitElement and _FormControlInterface.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { LitElement, html } from 'lit';
|
|
22
|
+
* import { FormControlMixin } from './path-to-FormControlMixin'; // Update the import path
|
|
23
|
+
*
|
|
24
|
+
* // Create a new component using the FormControlMixin
|
|
25
|
+
* class MyFormElement extends FormControlMixin(LitElement) {
|
|
26
|
+
* render() {
|
|
27
|
+
* return html`<div>Form-associated element with form: ${this.form ? 'attached' : 'detached'}</div>`;
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* customElements.define('my-form-element', MyFormElement);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export const FormControlMixin =
|
|
35
|
+
<T extends GenericConstructor<LitElement>>(superClass: T) => {
|
|
36
|
+
/**
|
|
37
|
+
* Class representing a LitElement that behaves like a native HTML form control element.
|
|
38
|
+
* @extends {LitElement}
|
|
39
|
+
* @implements {FormControlInterface}
|
|
40
|
+
*/
|
|
41
|
+
class FormControlElement extends superClass implements FormControlInterface {
|
|
42
|
+
static formAssociated = true;
|
|
43
|
+
|
|
44
|
+
_internals: ElementInternals;
|
|
45
|
+
|
|
46
|
+
get form () {
|
|
47
|
+
return this._internals.form;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
constructor (...args: any[]) {
|
|
52
|
+
super(...args);
|
|
53
|
+
this._internals = this.attachInternals();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return FormControlElement as GenericConstructor<FormControlInterface> & T;
|
|
58
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* eslint-disable max-classes-per-file */
|
|
2
|
+
import { LitElement, isServer } from 'lit';
|
|
3
|
+
import type { GenericConstructor } from '../types/GenericConstructor';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An interface representing the structure of RTL related class.
|
|
7
|
+
* @interface
|
|
8
|
+
*/
|
|
9
|
+
export interface RTLInterface {
|
|
10
|
+
/** A boolean indicating whether the text direction is right-to-left. */
|
|
11
|
+
isRTL: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A mixin to extend LitElement with right-to-left (RTL) text direction handling.
|
|
16
|
+
* This mixin adds a read-only `isRTL` property to the class it's applied to,
|
|
17
|
+
* allowing you to easily determine the text direction within your component.
|
|
18
|
+
*
|
|
19
|
+
* @function
|
|
20
|
+
* @param {GenericConstructor<LitElement>} superClass - The LitElement class to extend with RTL functionality.
|
|
21
|
+
* @returns {GenericConstructor<RTLInterface> & T} - A new class extending both LitElement and RTLInterface.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* import { LitElement, html } from 'lit';
|
|
26
|
+
* import { RtlMixin } from './RtlMixin'; // Adjust the import path as needed
|
|
27
|
+
*
|
|
28
|
+
* class MyElement extends RtlMixin(LitElement) {
|
|
29
|
+
* render() {
|
|
30
|
+
* return html`<p>Text direction is ${this.isRTL ? 'right-to-left' : 'left-to-right'}</p>`;
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* customElements.define('my-element', MyElement);
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* import { LitElement, html } from 'lit';
|
|
40
|
+
* import { RtlMixin } from './RtlMixin'; // Adjust the import path as needed
|
|
41
|
+
*
|
|
42
|
+
* class MyStyledElement extends RtlMixin(LitElement) {
|
|
43
|
+
* render() {
|
|
44
|
+
* return html`<div class="foo" ?isRTL=${this.isRTL}>Content</div>`;
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
*
|
|
48
|
+
* customElements.define('my-styled-element', MyStyledElement);
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* The corresponding SCSS to leverage the `isRTL` attribute:
|
|
52
|
+
* ```scss
|
|
53
|
+
* .foo[isRTL] {
|
|
54
|
+
* background-color: red;
|
|
55
|
+
* text-align: right;
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export const RtlMixin =
|
|
60
|
+
<T extends GenericConstructor<LitElement>>(superClass: T) => {
|
|
61
|
+
/**
|
|
62
|
+
* Class representing a LitElement with RTL handling.
|
|
63
|
+
* @extends {LitElement}
|
|
64
|
+
* @implements {RTLInterface}
|
|
65
|
+
*/
|
|
66
|
+
class RTLElement extends superClass implements RTLInterface {
|
|
67
|
+
/**
|
|
68
|
+
* A getter to determine whether the text direction is right-to-left (RTL).
|
|
69
|
+
* If the `dir` property is present on the component, it will be used to determine the text direction.
|
|
70
|
+
* If running on the client-side (not SSR) and the `dir` property is not present, the text direction will be inferred
|
|
71
|
+
* from the document's root element. This inference is not available during SSR.
|
|
72
|
+
* In all other cases, it will return `false`, indicating a left-to-right (LTR) text direction.
|
|
73
|
+
*
|
|
74
|
+
* @returns {boolean} - Returns `true` if the text direction is RTL, otherwise `false`.
|
|
75
|
+
*/
|
|
76
|
+
get isRTL (): boolean {
|
|
77
|
+
if (this.dir) {
|
|
78
|
+
return this.dir === 'rtl';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// If running on client-side and `dir` is not present, infer from the document's root element.
|
|
82
|
+
if (!isServer && !this.dir) {
|
|
83
|
+
return document.documentElement.getAttribute('dir') === 'rtl';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return RTLElement as GenericConstructor<RTLInterface> & T;
|
|
91
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
beforeEach,
|
|
3
|
+
afterEach,
|
|
4
|
+
describe,
|
|
5
|
+
it,
|
|
6
|
+
expect,
|
|
7
|
+
vi,
|
|
8
|
+
} from 'vitest';
|
|
9
|
+
|
|
10
|
+
import { requiredProperty } from '../../decorators/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
|
+
|
|
30
|
+
it('should log an error if the property is undefined', () => {
|
|
31
|
+
// Arrange
|
|
32
|
+
const mockComponent = new MockComponent();
|
|
33
|
+
|
|
34
|
+
// Act
|
|
35
|
+
mockComponent.color = undefined;
|
|
36
|
+
|
|
37
|
+
// Assert
|
|
38
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should log an error if the property is null', () => {
|
|
42
|
+
// Arrange
|
|
43
|
+
const mockComponent = new MockComponent();
|
|
44
|
+
|
|
45
|
+
// Act
|
|
46
|
+
mockComponent.color = null;
|
|
47
|
+
|
|
48
|
+
// Assert
|
|
49
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should log an error if the property is an empty string', () => {
|
|
53
|
+
// Arrange
|
|
54
|
+
const mockComponent = new MockComponent();
|
|
55
|
+
|
|
56
|
+
// Act
|
|
57
|
+
mockComponent.color = '';
|
|
58
|
+
|
|
59
|
+
// Assert
|
|
60
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should not log an error if the property is a non-empty string', () => {
|
|
64
|
+
// Arrange
|
|
65
|
+
const mockComponent = new MockComponent();
|
|
66
|
+
|
|
67
|
+
// Act
|
|
68
|
+
mockComponent.color = 'blue';
|
|
69
|
+
|
|
70
|
+
// Assert
|
|
71
|
+
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
beforeEach,
|
|
3
|
+
afterEach,
|
|
4
|
+
describe,
|
|
5
|
+
it,
|
|
6
|
+
expect,
|
|
7
|
+
vi,
|
|
8
|
+
} from 'vitest';
|
|
9
|
+
|
|
10
|
+
import { validPropertyValues } from '../../decorators/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
|
+
|
|
30
|
+
it('should allow value to be updated with a valid value', () => {
|
|
31
|
+
// Arrange
|
|
32
|
+
const mockComponent = new MockComponent();
|
|
33
|
+
|
|
34
|
+
// Act
|
|
35
|
+
mockComponent.color = 'green';
|
|
36
|
+
|
|
37
|
+
// Assert
|
|
38
|
+
expect(mockComponent.color).toBe('green');
|
|
39
|
+
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should fallback to the default value if an invalid value is assigned', () => {
|
|
43
|
+
// Arrange
|
|
44
|
+
const mockComponent = new MockComponent();
|
|
45
|
+
|
|
46
|
+
// Act
|
|
47
|
+
mockComponent.color = 'yellow';
|
|
48
|
+
|
|
49
|
+
// Assert
|
|
50
|
+
expect(mockComponent.color).toBe('red');
|
|
51
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should log an error message if an invalid value is assigned', () => {
|
|
55
|
+
// Arrange
|
|
56
|
+
const mockComponent = new MockComponent();
|
|
57
|
+
|
|
58
|
+
// Act
|
|
59
|
+
mockComponent.color = 'yellow';
|
|
60
|
+
|
|
61
|
+
// Assert
|
|
62
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
63
|
+
'<mock-component> Invalid value "yellow" provided for property "color".',
|
|
64
|
+
'Must be one of: red | green | blue.',
|
|
65
|
+
'Falling back to default value: "red"',
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/* eslint-disable max-classes-per-file */
|
|
2
|
+
import { LitElement } from 'lit';
|
|
3
|
+
import {
|
|
4
|
+
vi, expect, it,
|
|
5
|
+
} from 'vitest';
|
|
6
|
+
import { defineCustomElement } from '../../functions/defineCustomElement';
|
|
7
|
+
|
|
8
|
+
it('should call console.warn when a component is defined twice', () => {
|
|
9
|
+
// Arrange
|
|
10
|
+
class MockComponentA extends LitElement {}
|
|
11
|
+
|
|
12
|
+
const warnSpy = vi.spyOn(console, 'warn');
|
|
13
|
+
const errorSpy = vi.spyOn(console, 'error');
|
|
14
|
+
|
|
15
|
+
// Act
|
|
16
|
+
defineCustomElement('mock-component-a', MockComponentA);
|
|
17
|
+
defineCustomElement('mock-component-a', MockComponentA);
|
|
18
|
+
|
|
19
|
+
// Assert
|
|
20
|
+
const [[warning]] = warnSpy.mock.calls;
|
|
21
|
+
|
|
22
|
+
expect(warnSpy).toHaveBeenCalledOnce();
|
|
23
|
+
expect(warning).toMatch('PIE Web Component: "mock-component-a" has already been defined. Please ensure the component is only being defined once in your application.');
|
|
24
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
25
|
+
expect(customElements.get('mock-component-a')).toBe(MockComponentA);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should define the component without warnings when called once', () => {
|
|
29
|
+
// Arrange
|
|
30
|
+
class MockComponentB extends LitElement {}
|
|
31
|
+
|
|
32
|
+
const warnSpy = vi.spyOn(console, 'warn');
|
|
33
|
+
const errorSpy = vi.spyOn(console, 'error');
|
|
34
|
+
|
|
35
|
+
// Act
|
|
36
|
+
defineCustomElement('mock-component-b', MockComponentB);
|
|
37
|
+
|
|
38
|
+
// Assert
|
|
39
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
40
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
41
|
+
expect(customElements.get('mock-component-b')).toBe(MockComponentB);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should warn when defining the same component name with a different class', () => {
|
|
45
|
+
// Arrange
|
|
46
|
+
class MockComponentC extends LitElement {}
|
|
47
|
+
|
|
48
|
+
class MockComponentD extends LitElement {}
|
|
49
|
+
|
|
50
|
+
const warnSpy = vi.spyOn(console, 'warn');
|
|
51
|
+
const errorSpy = vi.spyOn(console, 'error');
|
|
52
|
+
|
|
53
|
+
// Act
|
|
54
|
+
defineCustomElement('mock-component-c', MockComponentC);
|
|
55
|
+
defineCustomElement('mock-component-c', MockComponentD);
|
|
56
|
+
|
|
57
|
+
// Assert
|
|
58
|
+
const [[warning]] = warnSpy.mock.calls;
|
|
59
|
+
|
|
60
|
+
expect(warnSpy).toHaveBeenCalledOnce();
|
|
61
|
+
expect(warning).toMatch('PIE Web Component: "mock-component-c" has already been defined. Please ensure the component is only being defined once in your application.');
|
|
62
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
63
|
+
expect(customElements.get('mock-component-c')).toBe(MockComponentC);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('Should correctly accept component classes that use multiple levels of inheritance', () => {
|
|
67
|
+
// Arrange
|
|
68
|
+
class MockComponentE extends LitElement {}
|
|
69
|
+
class MockComponentF extends MockComponentE {}
|
|
70
|
+
|
|
71
|
+
const warnSpy = vi.spyOn(console, 'warn');
|
|
72
|
+
const errorSpy = vi.spyOn(console, 'error');
|
|
73
|
+
|
|
74
|
+
// Act
|
|
75
|
+
defineCustomElement('mock-component-f', MockComponentF);
|
|
76
|
+
|
|
77
|
+
// Assert
|
|
78
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
79
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
80
|
+
expect(customElements.get('mock-component-f')).toBe(MockComponentF);
|
|
81
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { LitElement, html } from 'lit';
|
|
2
|
+
import { FormControlMixin } from '../../../mixins/formControl/formControlMixin.ts';
|
|
3
|
+
import { defineCustomElement } from '../../../functions/defineCustomElement.ts';
|
|
4
|
+
|
|
5
|
+
const componentSelector = 'form-control-mixin-mock';
|
|
6
|
+
|
|
7
|
+
export class MockComponent extends FormControlMixin(LitElement) {
|
|
8
|
+
render () {
|
|
9
|
+
return html`<div>Form Control Component</div>`;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
declare global {
|
|
14
|
+
interface HTMLElementTagNameMap {
|
|
15
|
+
[componentSelector]: MockComponent;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
defineCustomElement(componentSelector, MockComponent);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { test, expect } from '@sand4rt/experimental-ct-web';
|
|
2
|
+
import { MockComponent } from './MockComponent.ts';
|
|
3
|
+
|
|
4
|
+
test.describe('FormControlMixin', () => {
|
|
5
|
+
// IMPORTANT: Mounting and Unmounting the component before each test ensures that any tests that do not explicitly
|
|
6
|
+
// mount the component will still have it available in Playwright's cache (loaded and registered in the test browser)
|
|
7
|
+
test.beforeEach(async ({ mount }) => {
|
|
8
|
+
const component = await mount(MockComponent);
|
|
9
|
+
await component.unmount();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test.describe('form property', () => {
|
|
13
|
+
test.describe('when no form exists', () => {
|
|
14
|
+
test('should not have an associated form', async ({ page, mount }) => {
|
|
15
|
+
// Arrange
|
|
16
|
+
await mount(MockComponent);
|
|
17
|
+
|
|
18
|
+
const isFormAssociated = await page.evaluate(() => {
|
|
19
|
+
const component = document.querySelector('form-control-mixin-mock');
|
|
20
|
+
const form = component?.form;
|
|
21
|
+
|
|
22
|
+
return !!form;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Assert
|
|
26
|
+
expect(isFormAssociated).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test.describe('when inside of a form', () => {
|
|
31
|
+
test('should return the associated form', async ({ page }) => {
|
|
32
|
+
// Arrange
|
|
33
|
+
await page.setContent(`
|
|
34
|
+
<form id="testForm" action="/foo" method="POST">
|
|
35
|
+
<input type="text" id="username" name="username" required>
|
|
36
|
+
<input type="password" id="password" name="password" required>
|
|
37
|
+
<form-control-mixin-mock></form-control-mixin-mock>
|
|
38
|
+
</form>
|
|
39
|
+
`);
|
|
40
|
+
|
|
41
|
+
const formId = await page.evaluate(() => {
|
|
42
|
+
const component = document.querySelector('form-control-mixin-mock');
|
|
43
|
+
const form = component?.form;
|
|
44
|
+
|
|
45
|
+
return form?.id;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Assert
|
|
49
|
+
expect(formId).toContain('testForm');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test.describe('when not inside of an existing form', () => {
|
|
54
|
+
test('should not have an associated form', async ({ page }) => {
|
|
55
|
+
// Arrange
|
|
56
|
+
await page.setContent(`
|
|
57
|
+
<form id="siblingForm" action="/foo" method="POST">
|
|
58
|
+
<input type="text" id="username" name="username" required>
|
|
59
|
+
<input type="password" id="password" name="password" required>
|
|
60
|
+
</form>
|
|
61
|
+
<form-control-mixin-mock></form-control-mixin-mock>
|
|
62
|
+
`);
|
|
63
|
+
|
|
64
|
+
const isFormAssociated = await page.evaluate(() => {
|
|
65
|
+
const component = document.querySelector('form-control-mixin-mock');
|
|
66
|
+
const form = component?.form;
|
|
67
|
+
|
|
68
|
+
return !!form;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Assert
|
|
72
|
+
expect(isFormAssociated).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LitElement, html, nothing,
|
|
3
|
+
} from 'lit';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
describe,
|
|
7
|
+
it,
|
|
8
|
+
expect,
|
|
9
|
+
vi,
|
|
10
|
+
} from 'vitest';
|
|
11
|
+
|
|
12
|
+
import { RtlMixin } from '../../../index';
|
|
13
|
+
|
|
14
|
+
const scenarios = [
|
|
15
|
+
{ dir: 'ltr', isRTL: false },
|
|
16
|
+
{ dir: 'rtl', isRTL: true },
|
|
17
|
+
{ dir: 'auto', isRTL: false }
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
vi.mock('lit', async () => ({
|
|
21
|
+
...((await vi.importActual('lit')) as Array<unknown>),
|
|
22
|
+
isServer: false,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
describe('RtlMixin', () => {
|
|
26
|
+
const componentSelector = 'rtl-mixin-mock';
|
|
27
|
+
|
|
28
|
+
class MockComponent extends RtlMixin(LitElement) {
|
|
29
|
+
render () {
|
|
30
|
+
const { isRTL, dir } = this;
|
|
31
|
+
return html`<div dir="${dir || nothing}">${isRTL ? 'RTL Mode' : 'LTR Mode'}</div>`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
customElements.define(componentSelector, MockComponent);
|
|
36
|
+
|
|
37
|
+
function getMockInstance (): MockComponent {
|
|
38
|
+
return document.body.querySelector(componentSelector) as MockComponent;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('when running in the browser', () => {
|
|
42
|
+
describe('when the dir attribute is set via the component', () => {
|
|
43
|
+
scenarios.forEach(({ dir, isRTL }) => {
|
|
44
|
+
it(`should reflect ${isRTL ? 'RTL' : 'LTR'} if the component dir attribute is set to ${dir}`, () => {
|
|
45
|
+
// Arrange
|
|
46
|
+
document.body.innerHTML = `<rtl-mixin-mock dir="${dir}"></rtl-mixin-mock>`;
|
|
47
|
+
const component = getMockInstance();
|
|
48
|
+
|
|
49
|
+
// Assert
|
|
50
|
+
expect(component.isRTL).toBe(isRTL);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('when the dir attribute is set via the root document and no attribute provided to the component', () => {
|
|
56
|
+
scenarios.forEach(({ dir, isRTL }) => {
|
|
57
|
+
it(`should reflect ${isRTL ? 'RTL' : 'LTR'} if the root document dir attribute is set to ${dir}`, () => {
|
|
58
|
+
// Arrange
|
|
59
|
+
document.documentElement.setAttribute('dir', dir);
|
|
60
|
+
document.body.innerHTML = '<rtl-mixin-mock></rtl-mixin-mock>'; // component doesn't set dir
|
|
61
|
+
const component = getMockInstance();
|
|
62
|
+
|
|
63
|
+
// Assert
|
|
64
|
+
expect(component.isRTL).toBe(isRTL);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should prefer the dir value of the component over the root document when both are set', () => {
|
|
70
|
+
// Arrange
|
|
71
|
+
const rootDir = 'ltr';
|
|
72
|
+
const componentDir = 'rtl';
|
|
73
|
+
document.documentElement.setAttribute('dir', rootDir);
|
|
74
|
+
document.body.innerHTML = `<rtl-mixin-mock dir="${componentDir}"></rtl-mixin-mock>`;
|
|
75
|
+
const component = getMockInstance();
|
|
76
|
+
|
|
77
|
+
// Assert
|
|
78
|
+
expect(component.isRTL).toBeTruthy();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should reflect the root document dir attribute if `auto` is provided to the component', () => {
|
|
82
|
+
// Arrange
|
|
83
|
+
const rootDir = 'ltr';
|
|
84
|
+
document.documentElement.setAttribute('dir', rootDir);
|
|
85
|
+
document.body.innerHTML = '<rtl-mixin-mock dir="auto"></rtl-mixin-mock>';
|
|
86
|
+
const component = getMockInstance();
|
|
87
|
+
|
|
88
|
+
// Assert
|
|
89
|
+
expect(component.isRTL).toBeFalsy();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LitElement, html, nothing,
|
|
3
|
+
} from 'lit';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
describe,
|
|
7
|
+
it,
|
|
8
|
+
expect,
|
|
9
|
+
vi,
|
|
10
|
+
} from 'vitest';
|
|
11
|
+
|
|
12
|
+
import { RtlMixin } from '../../../index';
|
|
13
|
+
|
|
14
|
+
const scenarios = [
|
|
15
|
+
{ dir: 'ltr', isRTL: false },
|
|
16
|
+
{ dir: 'rtl', isRTL: true },
|
|
17
|
+
{ dir: 'auto', isRTL: false }
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
vi.mock('lit', async () => ({
|
|
21
|
+
...((await vi.importActual('lit')) as Array<unknown>),
|
|
22
|
+
isServer: true,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
describe('RtlMixin', () => {
|
|
26
|
+
const componentSelector = 'rtl-mixin-mock';
|
|
27
|
+
|
|
28
|
+
class MockComponent extends RtlMixin(LitElement) {
|
|
29
|
+
render () {
|
|
30
|
+
const { isRTL, dir } = this;
|
|
31
|
+
return html`<div dir="${dir || nothing}">${isRTL ? 'RTL Mode' : 'LTR Mode'}</div>`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
customElements.define(componentSelector, MockComponent);
|
|
36
|
+
|
|
37
|
+
function getMockInstance (): MockComponent {
|
|
38
|
+
return document.body.querySelector(componentSelector) as MockComponent;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('when running on the server', () => {
|
|
42
|
+
it('should return false for isRTL when dir is not set', () => {
|
|
43
|
+
// Arrange
|
|
44
|
+
document.body.innerHTML = '<rtl-mixin-mock></rtl-mixin-mock>';
|
|
45
|
+
const component = getMockInstance();
|
|
46
|
+
|
|
47
|
+
// Assert
|
|
48
|
+
expect(component.isRTL).toBeFalsy();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
scenarios.forEach(({ dir, isRTL }) => {
|
|
52
|
+
it(`should reflect ${isRTL ? 'RTL' : 'LTR'} if the component dir attribute is set to ${dir} when running on the server`, () => {
|
|
53
|
+
// Arrange
|
|
54
|
+
document.body.innerHTML = `<rtl-mixin-mock dir="${dir}"></rtl-mixin-mock>`;
|
|
55
|
+
const component = getMockInstance();
|
|
56
|
+
|
|
57
|
+
// Assert
|
|
58
|
+
expect(component.isRTL).toBe(isRTL);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
package/tsconfig.json
ADDED
package/vite.config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
build: {
|
|
5
|
+
lib: {
|
|
6
|
+
entry: {
|
|
7
|
+
index: 'src/index.ts',
|
|
8
|
+
},
|
|
9
|
+
formats: ['es'],
|
|
10
|
+
},
|
|
11
|
+
rollupOptions: {
|
|
12
|
+
external: (id) => /^lit/.test(id),
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
test: {
|
|
16
|
+
environment: 'jsdom',
|
|
17
|
+
exclude: ['**/*.browser.spec.ts'],
|
|
18
|
+
},
|
|
19
|
+
});
|