@pairbo/ui-kit 0.0.1

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.
Files changed (94) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.prettierignore +16 -0
  3. package/.prettierrc.json +17 -0
  4. package/README.md +61 -0
  5. package/cspell.json +9 -0
  6. package/dev.html +101 -0
  7. package/docs/README.md +1 -0
  8. package/docs/_includes/component.njk +16 -0
  9. package/docs/_includes/default.njk +39 -0
  10. package/docs/_includes/sidebar.njk +16 -0
  11. package/docs/eleventy.config.mjs +72 -0
  12. package/docs/pages/components/message-selector.md +17 -0
  13. package/docs/pages/fabric-example.html +46 -0
  14. package/docs/pages/fabric-example.js +28 -0
  15. package/docs/pages/index.md +76 -0
  16. package/eslint.config.mjs +32 -0
  17. package/ignote_temp +3 -0
  18. package/index.html +162 -0
  19. package/lint-stage.confg.js +6 -0
  20. package/package.json +66 -0
  21. package/pages/card-selection.html +65 -0
  22. package/pages/drawer.html +47 -0
  23. package/pages/editor.html +45 -0
  24. package/pages/page-mgn.html +51 -0
  25. package/pages/test_build.html +47 -0
  26. package/public/Greeting Card from Pairbo.png +0 -0
  27. package/scripts/plop/plopfile.js +51 -0
  28. package/scripts/plop/templates/components/component.hbs +34 -0
  29. package/scripts/plop/templates/components/define.hbs +10 -0
  30. package/scripts/plop/templates/components/styles.hbs +7 -0
  31. package/src/components/button/button.component.ts +93 -0
  32. package/src/components/button/button.styles.ts +273 -0
  33. package/src/components/button/button.ts +10 -0
  34. package/src/components/button-group/button-group.component.ts +36 -0
  35. package/src/components/button-group/button-group.styles.ts +7 -0
  36. package/src/components/button-group/button-group.ts +10 -0
  37. package/src/components/card-selection/card-selection.component.ts +43 -0
  38. package/src/components/card-selection/card-selection.styles.ts +7 -0
  39. package/src/components/card-selection/card-selection.ts +10 -0
  40. package/src/components/category/category.component.ts +91 -0
  41. package/src/components/category/category.styles.ts +27 -0
  42. package/src/components/category/category.ts +10 -0
  43. package/src/components/category-image/category-image.component.ts +38 -0
  44. package/src/components/category-image/category-image.styles.ts +11 -0
  45. package/src/components/category-image/category-image.ts +10 -0
  46. package/src/components/drawer/drawer.component.ts +82 -0
  47. package/src/components/drawer/drawer.styles.ts +54 -0
  48. package/src/components/drawer/drawer.ts +10 -0
  49. package/src/components/editor/editor.component.ts +135 -0
  50. package/src/components/editor/editor.styles.ts +13 -0
  51. package/src/components/editor/editor.ts +10 -0
  52. package/src/components/fabric-example/fabric-example.component.ts +268 -0
  53. package/src/components/fabric-example/fabric-example.styles.ts +23 -0
  54. package/src/components/fabric-example/fabric-example.test.ts +0 -0
  55. package/src/components/fabric-example/fabric-example.ts +12 -0
  56. package/src/components/image-slider/editor-card-slider.component.ts +136 -0
  57. package/src/components/image-slider/editor-card-slider.styles.ts +46 -0
  58. package/src/components/image-slider/editor-card-slider.ts +9 -0
  59. package/src/components/main.ts +17 -0
  60. package/src/components/message-selector/message-selector.component.ts +154 -0
  61. package/src/components/message-selector/message-selector.styles.ts +16 -0
  62. package/src/components/message-selector/message-selector.test.ts +64 -0
  63. package/src/components/message-selector/message-selector.ts +13 -0
  64. package/src/components/page-manager/page-manager.component.ts +228 -0
  65. package/src/components/page-manager/page-manager.styles.ts +9 -0
  66. package/src/components/page-manager/page-manager.ts +10 -0
  67. package/src/components/radio-button/radio-button.component.ts +118 -0
  68. package/src/components/radio-button/radio-button.styles.ts +13 -0
  69. package/src/components/radio-button/radio-button.ts +10 -0
  70. package/src/components/radio-group/radio-group.component.ts +203 -0
  71. package/src/components/radio-group/radio-group.styles.ts +19 -0
  72. package/src/components/radio-group/radio-group.ts +10 -0
  73. package/src/components/selector/selector.component.ts +115 -0
  74. package/src/components/selector/selector.styles.ts +9 -0
  75. package/src/components/selector/selector.ts +10 -0
  76. package/src/components/textarea/textarea.component.ts +234 -0
  77. package/src/components/textarea/textarea.styles.ts +178 -0
  78. package/src/components/textarea/textarea.ts +10 -0
  79. package/src/components/type-form/type-form.component.ts +121 -0
  80. package/src/components/type-form/type-form.styles.ts +7 -0
  81. package/src/components/type-form/type-form.ts +10 -0
  82. package/src/declaration.d.ts +44 -0
  83. package/src/events/events.ts +1 -0
  84. package/src/events/pbo-category-card-select.ts +7 -0
  85. package/src/internal/form.ts +376 -0
  86. package/src/internal/pairbo-element.ts +85 -0
  87. package/src/internal/slots.ts +54 -0
  88. package/src/internal/watch.ts +79 -0
  89. package/src/styles/component.styles.ts +17 -0
  90. package/src/styles/form-control.styles.ts +59 -0
  91. package/src/themes/default.css +414 -0
  92. package/temp +20 -0
  93. package/tsconfig.json +28 -0
  94. package/vite.config.ts +26 -0
@@ -0,0 +1,10 @@
1
+ import PboTypeForm from "./type-form.component.js";
2
+
3
+ export * from "./type-form.component.js";
4
+ export default PboTypeForm;
5
+
6
+ declare global {
7
+ interface HTMLElementTagNameMap {
8
+ "pbo-typing-form": PboTypeForm;
9
+ }
10
+ }
@@ -0,0 +1,44 @@
1
+ declare module "*.css" {
2
+ const styles: string;
3
+ export default styles;
4
+ }
5
+
6
+ // Declare that CSS files can be imported as a string
7
+ declare module "*.css?inline" {
8
+ const content: string;
9
+ export default content;
10
+ }
11
+
12
+ declare interface Card {
13
+ id: string;
14
+ name: string;
15
+ category: string | string[];
16
+ medias: {
17
+ cover: {
18
+ url: string;
19
+ alt: string;
20
+ };
21
+ back: {
22
+ url: string;
23
+ alt: string;
24
+ };
25
+ inner: {
26
+ url: string;
27
+ alt: string;
28
+ };
29
+ render_1: {
30
+ url: string;
31
+ alt: string;
32
+ };
33
+ render_2: {
34
+ url: string;
35
+ alt: string;
36
+ };
37
+ };
38
+ }
39
+
40
+ declare interface Category {
41
+ id: string;
42
+ name: string;
43
+ cards: Card[];
44
+ }
@@ -0,0 +1 @@
1
+ export type { PboCategoryCardSelectEvent } from "./pbo-category-card-select.js";
@@ -0,0 +1,7 @@
1
+ export type PboCategoryCardSelectEvent = CustomEvent<{ cardId: string }>;
2
+
3
+ declare global {
4
+ interface GlobalEventHandlersEventMap {
5
+ "pbo-category-card-selected": PboCategoryCardSelectEvent;
6
+ }
7
+ }
@@ -0,0 +1,376 @@
1
+ import { ReactiveController, ReactiveControllerHost } from "lit";
2
+ import { PairboFormControl } from "./pairbo-element";
3
+ import { PboButton } from "../components/main";
4
+
5
+ export const formCollections: WeakMap<HTMLFormElement, Set<PairboFormControl>> = new WeakMap();
6
+
7
+ // We store a WeakMap of reportValidity() overloads so we can override it when form controls connect to the DOM and
8
+ // restore the original behavior when they disconnect.
9
+ //
10
+ const reportValidityOverloads: WeakMap<HTMLFormElement, () => boolean> = new WeakMap();
11
+ const checkValidityOverloads: WeakMap<HTMLFormElement, () => boolean> = new WeakMap();
12
+
13
+ //
14
+ // We store a Set of controls that users have interacted with. This allows us to determine the interaction state
15
+ // without littering the DOM with additional data attributes.
16
+ //
17
+ const userInteractedControls: WeakSet<PairboFormControl> = new WeakSet();
18
+
19
+ //
20
+ // We store a WeakMap of interactions for each form control so we can track when all conditions are met for validation.
21
+ //
22
+ const interactions = new WeakMap<PairboFormControl, string[]>();
23
+
24
+ export interface FormControlControllerOptions {
25
+ // The form element that the control is associated with.
26
+ form: (input: PairboFormControl) => HTMLFormElement | null;
27
+ // The name of the control.
28
+ name: (input: PairboFormControl) => string;
29
+ // The default value of the control.
30
+ defaultValue: (input: PairboFormControl) => unknown;
31
+ // The value of the control.
32
+ value: (input: PairboFormControl) => unknown | unknown[];
33
+ // Whether the control is disabled.
34
+ disabled: (input: PairboFormControl) => boolean;
35
+ // When the control is invalid, the error message that should be shown to the user.
36
+ reportValidity: (input: PairboFormControl) => boolean;
37
+ //check if the control is valid
38
+ checkValidity: (input: PairboFormControl) => boolean;
39
+ // set value of the control
40
+ setValue: (input: PairboFormControl, value: unknown) => void;
41
+ // An array of event names to listen to.
42
+ assumeInteractionOn: string[];
43
+ }
44
+
45
+ export class FormControlController implements ReactiveController {
46
+ host: PairboFormControl & ReactiveControllerHost;
47
+ form?: HTMLFormElement | null;
48
+ options: FormControlControllerOptions;
49
+
50
+ constructor(host: PairboFormControl & ReactiveControllerHost, options?: Partial<FormControlControllerOptions>) {
51
+ console.log({ host });
52
+ (this.host = host).addController(this);
53
+ this.options = {
54
+ form: input => {
55
+ const formId = input.form;
56
+
57
+ if (formId) {
58
+ const root = input.getRootNode() as Document | ShadowRoot | HTMLElement;
59
+ const form = root.querySelector(`#${formId}`);
60
+
61
+ if (form) {
62
+ return form as HTMLFormElement;
63
+ }
64
+ }
65
+ return input.closest("form");
66
+ },
67
+ name: input => input.name,
68
+ value: input => input.value,
69
+ defaultValue: input => input.defaultValue,
70
+ disabled: input => input.disabled || false,
71
+ reportValidity: input => (typeof input.reportValidity === "function" ? input.reportValidity() : true),
72
+ checkValidity: input => (typeof input.checkValidity === "function" ? input.checkValidity() : true),
73
+ setValue: (input, value: string) => (input.value = value),
74
+ assumeInteractionOn: ["pbo-input"],
75
+ ...options,
76
+ };
77
+ }
78
+
79
+ hostConnected() {
80
+ const form = this.options.form(this.host);
81
+
82
+ if (form) {
83
+ this.attachForm(form);
84
+ }
85
+ // Listen for interactions
86
+ interactions.set(this.host, []);
87
+ console.log({ interactions });
88
+
89
+ this.options.assumeInteractionOn.forEach(event => {
90
+ this.host.addEventListener(event, this.handleInteraction);
91
+ });
92
+ }
93
+ hostDisconnected() {
94
+ this.detachForm();
95
+
96
+ // Clean up interactions
97
+ interactions.delete(this.host);
98
+ this.options.assumeInteractionOn.forEach(event => {
99
+ this.host.removeEventListener(event, this.handleInteraction);
100
+ });
101
+ }
102
+
103
+ hostUpdated() {
104
+ const form = this.options.form(this.host);
105
+
106
+ // Detach if the form no longer exists
107
+ if (!form) {
108
+ this.detachForm();
109
+ }
110
+
111
+ // If the form has changed, reattach it
112
+ if (form && this.form !== form) {
113
+ this.detachForm();
114
+ this.attachForm(form);
115
+ }
116
+
117
+ if (this.host.hasUpdated) {
118
+ this.setValidity(this.host.validity.valid);
119
+ }
120
+ }
121
+
122
+ private attachForm(form?: HTMLFormElement) {
123
+ if (form) {
124
+ this.form = form;
125
+
126
+ // Add this element to the form's collection
127
+ if (formCollections.has(this.form)) {
128
+ formCollections.get(this.form)!.add(this.host);
129
+ } else {
130
+ formCollections.set(this.form, new Set<PairboFormControl>([this.host]));
131
+ }
132
+
133
+ this.form.addEventListener("formdata", this.handleFormData);
134
+ this.form.addEventListener("submit", this.handleFormSubmit);
135
+ this.form.addEventListener("reset", this.handleFormReset);
136
+
137
+ // Overload the form's reportValidity() method so it looks at Shoelace form controls
138
+ if (!reportValidityOverloads.has(this.form)) {
139
+ reportValidityOverloads.set(this.form, this.form.reportValidity);
140
+ this.form.reportValidity = () => this.reportFormValidity();
141
+ }
142
+
143
+ // Overload the form's checkValidity() method so it looks at Shoelace form controls
144
+ if (!checkValidityOverloads.has(this.form)) {
145
+ checkValidityOverloads.set(this.form, this.form.checkValidity);
146
+ this.form.checkValidity = () => this.checkFormValidity();
147
+ }
148
+ } else {
149
+ this.form = undefined;
150
+ }
151
+ }
152
+ private detachForm() {
153
+ if (!this.form) return;
154
+
155
+ const formCollection = formCollections.get(this.form);
156
+
157
+ if (!formCollection) {
158
+ return;
159
+ }
160
+
161
+ // Remove this host from the form's collection
162
+ formCollection.delete(this.host);
163
+
164
+ // Check to make sure there's no other form controls in the collection. If we do this
165
+ // without checking if any other controls are still in the collection, then we will wipe out the
166
+ // validity checks for all other elements.
167
+ // see: https://github.com/shoelace-style/shoelace/issues/1703
168
+ if (formCollection.size <= 0) {
169
+ this.form.removeEventListener("formdata", this.handleFormData);
170
+ this.form.removeEventListener("submit", this.handleFormSubmit);
171
+ this.form.removeEventListener("reset", this.handleFormReset);
172
+
173
+ // Remove the overload and restore the original method
174
+ if (reportValidityOverloads.has(this.form)) {
175
+ this.form.reportValidity = reportValidityOverloads.get(this.form)!;
176
+ reportValidityOverloads.delete(this.form);
177
+ }
178
+
179
+ if (checkValidityOverloads.has(this.form)) {
180
+ this.form.checkValidity = checkValidityOverloads.get(this.form)!;
181
+ checkValidityOverloads.delete(this.form);
182
+ }
183
+
184
+ // So it looks weird here to not always set the form to undefined. But I _think_ if we unattach this.form here,
185
+ // we end up in this fun spot where future validity checks don't have a reference to the form validity handler.
186
+ // First form element in sets the validity handler. So we can't clean up `this.form` until there are no other form elements in the form.
187
+ this.form = undefined;
188
+ }
189
+ }
190
+
191
+ // Append the control's name and value to the given FormData object.
192
+ private handleFormData = (event: FormDataEvent) => {
193
+ const disabled = this.options.disabled(this.host);
194
+ const name = this.options.name(this.host);
195
+ const value = this.options.value(this.host);
196
+ if (
197
+ this.host.isConnected &&
198
+ !disabled &&
199
+ typeof name === "string" &&
200
+ name.length > 0 &&
201
+ typeof value !== "undefined"
202
+ ) {
203
+ if (Array.isArray(value)) {
204
+ (value as unknown[]).forEach(val => {
205
+ event.formData.append(name, (val as string | number | boolean).toString());
206
+ });
207
+ } else {
208
+ event.formData.append(name, (value as string | number | boolean).toString());
209
+ }
210
+ }
211
+ };
212
+
213
+ private handleFormSubmit = (event: Event) => {
214
+ const disabled = this.options.disabled(this.host);
215
+ const reportValidity = this.options.reportValidity;
216
+ if (this.form && !this.form.noValidate) {
217
+ formCollections.get(this.form)?.forEach(control => {
218
+ this.setUserInteracted(control, true);
219
+ });
220
+ }
221
+
222
+ if (this.form && !this.form.noValidate && !disabled && !reportValidity(this.host)) {
223
+ event?.preventDefault();
224
+ event?.stopImmediatePropagation();
225
+ }
226
+ };
227
+
228
+ private handleFormReset = () => {
229
+ this.options.setValue(this.host, this.options.defaultValue(this.host));
230
+ this.setUserInteracted(this.host, false);
231
+ interactions.set(this.host, []);
232
+ };
233
+
234
+ private handleInteraction = (event: Event) => {
235
+ const emittedEvents = interactions.get(this.host)!;
236
+ if (!emittedEvents.includes(event.type)) {
237
+ emittedEvents.push(event.type);
238
+ }
239
+ if (emittedEvents.length === this.options.assumeInteractionOn.length) {
240
+ this.setUserInteracted(this.host, true);
241
+ }
242
+ };
243
+
244
+ private checkFormValidity = () => {
245
+ if (this.form && !this.form.noValidate) {
246
+ const elements = this.form.querySelectorAll<HTMLInputElement>("*");
247
+ for (const element of elements) {
248
+ if (typeof element.checkValidity === "function") {
249
+ if (!element.checkValidity()) {
250
+ return false;
251
+ }
252
+ }
253
+ }
254
+ }
255
+ return true;
256
+ };
257
+ private reportFormValidity() {
258
+ if (this.form && !this.form.noValidate) {
259
+ const elements = this.form.querySelectorAll<HTMLInputElement>("*");
260
+ for (const element of elements) {
261
+ if (typeof element.reportValidity === "function") {
262
+ if (!element.reportValidity()) {
263
+ return false;
264
+ }
265
+ }
266
+ }
267
+ }
268
+ return true;
269
+ }
270
+
271
+ private setUserInteracted(el: PairboFormControl, hasInteracted: boolean) {
272
+ if (hasInteracted) {
273
+ userInteractedControls.add(el);
274
+ } else {
275
+ userInteractedControls.delete(el);
276
+ }
277
+
278
+ el.requestUpdate();
279
+ }
280
+ private doAction = (type: "submit" | "reset", submitter?: HTMLInputElement | PboButton) => {
281
+ if (this.form) {
282
+ const button = document.createElement("button");
283
+ button.type = type;
284
+ button.style.position = "absolute";
285
+ button.style.width = "0";
286
+ button.style.height = "0";
287
+ button.style.clipPath = "inset(50%)";
288
+ button.style.overflow = "hidden";
289
+ button.style.whiteSpace = "nowrap";
290
+ if (submitter) {
291
+ button.name = submitter.name;
292
+ button.value = submitter.value;
293
+
294
+ ["formation", "formenctype", "formmethod", "formnovalidate", "formtarget"].forEach(attr => {
295
+ if (submitter.hasAttribute(attr)) {
296
+ button.setAttribute(attr, submitter.getAttribute(attr)!);
297
+ }
298
+ });
299
+ }
300
+
301
+ this.form.append(button);
302
+ button.click();
303
+ button.remove();
304
+ }
305
+ };
306
+
307
+ getForm() {
308
+ return this.form ?? null;
309
+ }
310
+ reset(submitter?: HTMLInputElement | PboButton) {
311
+ this.doAction("reset", submitter);
312
+ }
313
+
314
+ submit(submitter?: HTMLInputElement | PboButton) {
315
+ this.doAction("submit", submitter);
316
+ }
317
+ setValidity(isValid: boolean) {
318
+ const host = this.host;
319
+ const hasInteracted = Boolean(userInteractedControls.has(host));
320
+ const required = Boolean(host.required);
321
+
322
+ host.toggleAttribute("data-required", required);
323
+ host.toggleAttribute("data-optional", !required);
324
+ host.toggleAttribute("data-invalid", !isValid);
325
+ host.toggleAttribute("data-valid", isValid);
326
+ host.toggleAttribute("data-user-invalid", hasInteracted && !isValid);
327
+ host.toggleAttribute("data-user-valid", hasInteracted && isValid);
328
+ }
329
+ updateValidity() {
330
+ const host = this.host;
331
+ this.setValidity(host.validity.valid);
332
+ }
333
+ emitInvalidEvent(originalInvalidEvent?: Event) {
334
+ const pboInvalidEvent = new CustomEvent<Record<PropertyKey, never>>("pbo-invalid", {
335
+ bubbles: false,
336
+ composed: false,
337
+ cancelable: true,
338
+ detail: {},
339
+ });
340
+
341
+ if (!originalInvalidEvent) {
342
+ pboInvalidEvent.preventDefault();
343
+ }
344
+
345
+ if (!this.host.dispatchEvent(pboInvalidEvent)) {
346
+ originalInvalidEvent?.preventDefault();
347
+ }
348
+ }
349
+ }
350
+
351
+ export const validValidityState: ValidityState = Object.freeze({
352
+ badInput: false,
353
+ customError: false,
354
+ patternMismatch: false,
355
+ rangeOverflow: false,
356
+ rangeUnderflow: false,
357
+ stepMismatch: false,
358
+ tooLong: false,
359
+ tooShort: false,
360
+ typeMismatch: false,
361
+ valid: true,
362
+ valueMissing: false,
363
+ });
364
+
365
+ // A validity state object that represents `value missing`
366
+ export const valueMissingValidityState: ValidityState = Object.freeze({
367
+ ...validValidityState,
368
+ valid: false,
369
+ valueMissing: false,
370
+ });
371
+
372
+ export const customErrorValidityState: ValidityState = Object.freeze({
373
+ ...validValidityState,
374
+ valid: false,
375
+ customError: true,
376
+ });
@@ -0,0 +1,85 @@
1
+ import { LitElement } from "lit";
2
+
3
+ /**
4
+ * Interface representing a Pairbo form control element.
5
+ * Extends LitElement to provide form control functionality.
6
+ *
7
+ * @interface PairboFormControl
8
+ * @extends {LitElement}
9
+ *
10
+ * @property {string} name - The name of the form control
11
+ * @property {unknown} value - The current value of the form control
12
+ * @property {boolean} [disabled] - Whether the form control is disabled
13
+ * @property {unknown} [defaultValue] - The default value of the form control
14
+ * @property {boolean} [defaultChecked] - The default checked state for checkable controls
15
+ * @property {string} [form] - The id of the form this control belongs to
16
+ * @property {string} [pattern] - Regular expression pattern for validation
17
+ * @property {number|string|Date} [min] - Minimum allowed value
18
+ * @property {number|string|Date} [max] - Maximum allowed value
19
+ * @property {number|"any"} [step] - Step increment value
20
+ * @property {boolean} [required] - Whether the field is required
21
+ * @property {number} [minLength] - Minimum length for text input
22
+ * @property {number} [maxLength] - Maximum length for text input
23
+ * @property {ValidityState} validity - The ValidityState object representing validation state
24
+ * @property {string} validationMessage - The validation message
25
+ *
26
+ * @method checkValidity - Checks if the element's value satisfies validation constraints
27
+ * @returns {boolean} True if the element's value is valid, false otherwise
28
+ *
29
+ * @method getForm - Gets the form element that contains this control
30
+ * @returns {HTMLFormElement|null} The parent form element or null if not found
31
+ *
32
+ * @method reportValidity - Reports validity to the user through UI feedback
33
+ * @returns {boolean} True if the element's value is valid, false otherwise
34
+ *
35
+ * @method setCustomValidity - Sets a custom validation message
36
+ * @param {string} error - The custom error message to display
37
+ */
38
+ export interface PairboFormControl extends LitElement {
39
+ name: string;
40
+ value: unknown;
41
+ disabled?: boolean;
42
+ defaultValue?: unknown;
43
+ defaultChecked?: boolean;
44
+ form?: string;
45
+
46
+ pattern?: string;
47
+ min?: number | string | Date;
48
+ max?: number | string | Date;
49
+ step?: number | "any";
50
+ required?: boolean;
51
+ minlength?: number;
52
+ maxlength?: number;
53
+
54
+ readonly validity: ValidityState;
55
+ readonly validationMessage: string;
56
+
57
+ checkValidity: () => boolean;
58
+ getForm: () => HTMLFormElement | null;
59
+ reportValidity: () => boolean;
60
+ setCustomValidity: (error: string) => void;
61
+ }
62
+
63
+ export default class PairboElement extends LitElement {
64
+ /* --------------------------- Emits custom events -------------------------- */
65
+ emit(name: string, options?: CustomEventInit<unknown> | undefined) {
66
+ /**
67
+ * Creates a new CustomEvent instance with specified event name and options.
68
+ * @property {boolean} bubbles - Whether the event can bubble up through the Dom
69
+ * @property {boolean} cancelable - Indicates whether the event is cancelable and therefore prevented as if the event never happened. To Cancel the event, call preventDefault() on the event.
70
+ * @property {boolean} composed - Indicates whether or not the event will propagate across the shadow Dom boundary into the standard DOM.
71
+ * @property {Object} detail - Returns any custom data event was created with. Typically used for synthetic events. For the custom event.
72
+ */
73
+ const event = new CustomEvent(name, {
74
+ bubbles: true,
75
+ cancelable: true,
76
+ composed: true,
77
+ detail: {},
78
+ ...options,
79
+ });
80
+
81
+ this.dispatchEvent(event);
82
+
83
+ return event;
84
+ }
85
+ }
@@ -0,0 +1,54 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from "lit";
2
+
3
+ export class HasSlotController implements ReactiveController {
4
+ host: ReactiveControllerHost & Element;
5
+ slotNames: string[] = [];
6
+ constructor(host: ReactiveControllerHost & Element, ...slotNames: string[]) {
7
+ (this.host = host).addController(this);
8
+ this.slotNames = slotNames;
9
+ }
10
+
11
+ private hasDefaultSlot() {
12
+ return [...this.host.childNodes].some(node => {
13
+ // The node is a text node and it's not empty
14
+ if (node.nodeType === node.TEXT_NODE && node.textContent?.trim() !== "") {
15
+ return true;
16
+ }
17
+
18
+ // The node is an element node
19
+ if (node.nodeType === node.ELEMENT_NODE) {
20
+ const element = node as Element;
21
+ const tagName = element.tagName.toLowerCase();
22
+ // If the tag is visually hidden, ignore it
23
+ if (tagName === "pbo-visually-hidden") {
24
+ return false;
25
+ }
26
+ // So if the element has no slot attribute, it's in the default slot
27
+ if (!element.hasAttribute("slot")) {
28
+ return true;
29
+ }
30
+ }
31
+ return false;
32
+ });
33
+ }
34
+
35
+ private hasNamedSlot(name: string) {
36
+ return this.host.querySelector(`:scope > [slot="${name}]`) !== null;
37
+ }
38
+ test(slotName: string) {
39
+ return slotName === "[default]" ? this.hasDefaultSlot() : this.hasNamedSlot(slotName);
40
+ }
41
+ hostConnected() {
42
+ this.host.shadowRoot!.addEventListener("slotchange", this.handleSlotChange);
43
+ }
44
+ hostDisconnected() {
45
+ this.host.shadowRoot!.removeEventListener("slotchange", this.handleSlotChange);
46
+ }
47
+
48
+ private handleSlotChange = (event: Event) => {
49
+ const slot = event.target as HTMLSlotElement;
50
+ if ((this.slotNames.includes("[default]") && !slot.name) || (slot.name && this.slotNames.includes(slot.name))) {
51
+ this.host.requestUpdate();
52
+ }
53
+ };
54
+ }
@@ -0,0 +1,79 @@
1
+ import { waitUntil } from "@open-wc/testing";
2
+ import { LitElement } from "lit";
3
+ // Define the handler function type
4
+ type UpdateHandler = (prev?: unknown, next?: unknown) => void;
5
+
6
+ // Check the generic type, remove undefined
7
+ type NonUndefined<A> = A extends undefined ? never : A;
8
+
9
+ /**
10
+ * Get the keys of the updates handler inside one class.
11
+ *
12
+ * keyof will only extract the keys which are non-never
13
+ *
14
+ * The modified object
15
+ * { [key in keyof T] -?: NonUndefined<T[key]> extends UpdateHandler ? key : never; }
16
+ *
17
+ * Get the keys in the object: [key in keyof T]
18
+ *
19
+ * Remove the undefined keys: -?
20
+ *
21
+ * Ternary operator A ? B : C - Check if the type is UpdateHandler: NonUndefined<T[Key]> extends UpdateHandler
22
+ * If true, set the value of current key to the key name
23
+ * Else set the value of current key to never
24
+ * After that, we use the mapped type to get the keys of the object ExampleObject[keyof ExampleObject] will return an union of they key values, and the never value will be removed
25
+ **/
26
+ type UpdateHandlerKeys<T extends Object> = {
27
+ [key in keyof T]-?: NonUndefined<T[key]> extends UpdateHandler ? key : never;
28
+ }[keyof T];
29
+
30
+ type Func = (first: string, second: number, last?: boolean) => void;
31
+ type FunctionKeys = keyof (Func extends Object ? Func : never);
32
+ type Option = {
33
+ waitUntilFirstUpdate: boolean;
34
+ };
35
+
36
+ export function watch(propertyName: string | string[], options?: Option) {
37
+ const resolvedOptions: Required<Option> = {
38
+ waitUntilFirstUpdate: false,
39
+ ...options,
40
+ };
41
+
42
+ /**
43
+ * target: The class to be decorated
44
+ * decoratedFnName: The name of the function to be decorated
45
+ */
46
+ return function <EleClass extends LitElement>(target: EleClass, decoratedFnName: UpdateHandlerKeys<EleClass>) {
47
+ // If the property name is string, convert it into a string array
48
+ const watchedProperty = Array.isArray(propertyName) ? propertyName : [propertyName];
49
+
50
+ // @ts-ignore update is protected in the LitElement
51
+ // Store the original update function
52
+ const { update } = target;
53
+
54
+ // This will create a new update function, but it will
55
+ // wrap the original update function, so it will not be overridden
56
+
57
+ // @ts-ignore update is protected in the LitElement
58
+ target.update = function (this: EleClass, changeProps: Map<keyof EleClass, EleClass[keyof ElemClass]>) {
59
+ // Loop through the watched properties
60
+ watchedProperty.forEach(property => {
61
+ const key = property as keyof EleClass;
62
+ if (changeProps.has(key)) {
63
+ const oldValue = changeProps.get(key);
64
+ const newValue = this[key];
65
+
66
+ if (oldValue !== newValue) {
67
+ if (!resolvedOptions.waitUntilFirstUpdate || this.hasUpdated) {
68
+ (this[decoratedFnName] as unknown as UpdateHandler)(oldValue, newValue);
69
+ }
70
+ }
71
+ }
72
+ });
73
+
74
+ // @ts-ignore update is protected in the LitElement
75
+ // Call the original update function
76
+ update.call(this, changeProps);
77
+ };
78
+ };
79
+ }
@@ -0,0 +1,17 @@
1
+ import { css } from "lit";
2
+
3
+ export default css`
4
+ :host {
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ :host *,
9
+ :host *::before,
10
+ :host *::after {
11
+ box-sizing: inherit;
12
+ }
13
+
14
+ [hidden] {
15
+ display: none !important;
16
+ }
17
+ `;