@zywave/zui-formfield 4.2.2-pre.0 → 4.3.0-pre.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.
@@ -70,6 +70,15 @@
70
70
  "text": "number | undefined | undefined"
71
71
  }
72
72
  },
73
+ {
74
+ "kind": "field",
75
+ "name": "#isRequired",
76
+ "privacy": "private",
77
+ "type": {
78
+ "text": "boolean"
79
+ },
80
+ "default": "false"
81
+ },
73
82
  {
74
83
  "kind": "field",
75
84
  "name": "#validationMessage",
@@ -81,6 +90,11 @@
81
90
  "privacy": "private",
82
91
  "readonly": true
83
92
  },
93
+ {
94
+ "kind": "method",
95
+ "name": "#checkRequired",
96
+ "privacy": "private"
97
+ },
84
98
  {
85
99
  "kind": "method",
86
100
  "name": "#onLabelClick",
@@ -1,3 +1,3 @@
1
1
  import { css } from 'lit';
2
- export const style = css `:host .container label,:host .container ::slotted(label){display:inline-flex;margin:0;font-weight:600;color:var(--zui-gray-800)}:host{contain:none}:host .container{display:flex;flex-direction:column;margin-bottom:1.25rem}:host .container ::slotted(*:not(zui-toggle):not(zui-checkbox):not(zui-radio)){margin-top:.25rem !important}:host .container ::slotted(zui-toggle){margin:.9375rem 0}:host .validation-message:empty{display:none}:host .validation-message{color:var(--zui-red-500)}`;
2
+ export const style = css `:host .container label,:host .container ::slotted(label){display:inline-flex;margin:0;font-weight:600;color:var(--zui-gray-800)}:host{contain:none}:host .container{display:flex;flex-direction:column;margin-bottom:1.25rem}:host .container ::slotted(*:not(zui-toggle):not(zui-checkbox):not(zui-radio)){margin-top:.25rem !important}:host .container ::slotted(zui-toggle){margin:.9375rem 0}:host .validation-message:empty{display:none}:host .validation-message{color:var(--zui-red-500)}:host label.required::after{content:"*";margin-left:.5ch;color:var(--zui-red-500)}`;
3
3
  //# sourceMappingURL=zui-formfield-css.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"zui-formfield-css.js","sourceRoot":"","sources":["../src/zui-formfield-css.js"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B,MAAM,CAAC,MAAM,KAAK,GAAG,GAAG,CAAA,oeAAoe,CAAC","sourcesContent":["import { css } from 'lit';\n\nexport const style = css`:host .container label,:host .container ::slotted(label){display:inline-flex;margin:0;font-weight:600;color:var(--zui-gray-800)}:host{contain:none}:host .container{display:flex;flex-direction:column;margin-bottom:1.25rem}:host .container ::slotted(*:not(zui-toggle):not(zui-checkbox):not(zui-radio)){margin-top:.25rem !important}:host .container ::slotted(zui-toggle){margin:.9375rem 0}:host .validation-message:empty{display:none}:host .validation-message{color:var(--zui-red-500)}`;\n"]}
1
+ {"version":3,"file":"zui-formfield-css.js","sourceRoot":"","sources":["../src/zui-formfield-css.js"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B,MAAM,CAAC,MAAM,KAAK,GAAG,GAAG,CAAA,sjBAAsjB,CAAC","sourcesContent":["import { css } from 'lit';\n\nexport const style = css`:host .container label,:host .container ::slotted(label){display:inline-flex;margin:0;font-weight:600;color:var(--zui-gray-800)}:host{contain:none}:host .container{display:flex;flex-direction:column;margin-bottom:1.25rem}:host .container ::slotted(*:not(zui-toggle):not(zui-checkbox):not(zui-radio)){margin-top:.25rem !important}:host .container ::slotted(zui-toggle){margin:.9375rem 0}:host .validation-message:empty{display:none}:host .validation-message{color:var(--zui-red-500)}:host label.required::after{content:\"*\";margin-left:.5ch;color:var(--zui-red-500)}`;\n"]}
@@ -28,9 +28,11 @@ export class ZuiFormField extends ZuiBaseElement {
28
28
  * (optional): Provide a valid CSS selector to help `zui-formfield` find the correct form control. Defaults to the first child element.
29
29
  */
30
30
  this.controlSelector = '*';
31
+ this.#isRequired = false;
31
32
  }
32
33
  #validationMessageField;
33
34
  #validationIntervalId;
35
+ #isRequired;
34
36
  get #validationMessage() {
35
37
  return this.#validationMessageField;
36
38
  }
@@ -44,12 +46,21 @@ export class ZuiFormField extends ZuiBaseElement {
44
46
  get #control() {
45
47
  return findAssignedElement(this._slotEl, this.controlSelector);
46
48
  }
49
+ #checkRequired() {
50
+ const control = this.#control;
51
+ const wasRequired = this.#isRequired;
52
+ this.#isRequired = control?.hasAttribute('required') ?? false;
53
+ if (wasRequired !== this.#isRequired) {
54
+ this.requestUpdate();
55
+ }
56
+ }
47
57
  static get styles() {
48
58
  return [super.styles, style];
49
59
  }
50
60
  async firstUpdated(changedProps) {
51
61
  super.firstUpdated(changedProps);
52
62
  if (this.#control) {
63
+ this.#checkRequired();
53
64
  this.#control.tagName.includes('-')
54
65
  ? customElements
55
66
  .whenDefined(this.#control.tagName.toLowerCase())
@@ -67,7 +78,13 @@ export class ZuiFormField extends ZuiBaseElement {
67
78
  render() {
68
79
  return html `
69
80
  <div class="container" part="container">
70
- ${this.label ? html ` <label @click="${this.#onLabelClick}" part="label">${this.label}</label> ` : nothing}
81
+ ${this.label
82
+ ? html `
83
+ <label @click="${this.#onLabelClick}" part="label" class="${this.#isRequired ? 'required' : ''}">
84
+ ${this.label}
85
+ </label>
86
+ `
87
+ : nothing}
71
88
  <slot @slotchange="${this.#onSlotChange}"></slot>
72
89
  <div class="validation-message">${this.#validationMessage ?? ''}</div>
73
90
  </div>
@@ -79,6 +96,7 @@ export class ZuiFormField extends ZuiBaseElement {
79
96
  }
80
97
  #onSlotChange() {
81
98
  if (this.#control) {
99
+ this.#checkRequired();
82
100
  this.#configureValidation(this.#control);
83
101
  }
84
102
  }
@@ -1 +1 @@
1
- {"version":3,"file":"zui-formfield.js","sourceRoot":"","sources":["../src/zui-formfield.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAC3F,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAG/C;;;;;;;GAOG;AACH,MAAM,OAAO,YAAa,SAAQ,cAAc;IAAhD;;QACE;;WAEG;QAEH,UAAK,GAAkB,IAAI,CAAC;QAE5B;;WAEG;QAEH,oBAAe,GAAG,GAAG,CAAC;IA6GxB,CAAC;IAxGC,uBAAuB,CAA6B;IAEpD,qBAAqB,CAAsB;IAE3C,IAAI,kBAAkB;QACpB,OAAO,IAAI,CAAC,uBAAuB,CAAC;IACtC,CAAC;IAED,IAAI,kBAAkB,CAAC,GAA8B;QACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,uBAAuB,CAAC;QAC9C,IAAI,QAAQ,KAAK,GAAG,EAAE,CAAC;YACrB,IAAI,CAAC,uBAAuB,GAAG,GAAG,EAAE,IAAI,EAAE,CAAC;YAC3C,IAAI,CAAC,aAAa,EAAE,CAAC;QACvB,CAAC;IACH,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,mBAAmB,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC;IACjE,CAAC;IAED,MAAM,KAAK,MAAM;QACf,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,YAA4B;QAC7C,KAAK,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;QAEjC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC;gBACjC,CAAC,CAAC,cAAc;qBACX,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;qBAChD,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAiC,CAAC,CAAC;gBAClF,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAiC,CAAC,CAAC;QACxE,CAAC;IACH,CAAC;IAED,oBAAoB;QAClB,KAAK,CAAC,oBAAoB,EAAE,CAAC;QAE7B,IAAI,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAC/B,aAAa,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;YAC1C,IAAI,CAAC,qBAAqB,GAAG,SAAS,CAAC;QACzC,CAAC;IACH,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAA;;UAEL,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA,mBAAmB,IAAI,CAAC,aAAa,kBAAkB,IAAI,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO;6BACpF,IAAI,CAAC,aAAa;0CACL,IAAI,CAAC,kBAAkB,IAAI,EAAE;;KAElE,CAAC;IACJ,CAAC;IAED,aAAa;QACX,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC;QACvB,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC;IACzB,CAAC;IAED,aAAa;QACX,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAiC,CAAC,CAAC;QACpE,CAAC;IACH,CAAC;IAED,oBAAoB,CAAC,OAA8B;QACjD,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,CAAC,aAAa,KAAK,UAAU,EAAE,CAAC;YAC5D,OAAO;QACT,CAAC;QAED,OAAO,CAAC,gBAAgB,CACtB,SAAS,EACT,KAAK,EAAE,KAAK,EAAE,EAAE;YACd,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,MAAM,IAAI,CAAC,yBAAyB,CAAC,OAAO,CAAC,CAAC;QAChD,CAAC,EACD,EAAE,OAAO,EAAE,IAAI,EAAE,CAClB,CAAC;QAEF,OAAO,CAAC,gBAAgB,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,IAAI,CAAC,yBAAyB,CAAC,OAAO,CAAC,CAAC,CAAC;QAC5F,OAAO,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,IAAI,CAAC,yBAAyB,CAAC,OAAO,CAAC,CAAC,CAAC;QAC7F,OAAO,CAAC,gBAAgB,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,IAAI,CAAC,yBAAyB,CAAC,OAAO,CAAC,CAAC,CAAC;QAC9F,OAAO,CAAC,gBAAgB,CAAC,qBAAqB,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,IAAI,CAAC,yBAAyB,CAAC,OAAO,CAAC,CAAC,CAAC;QAE3G,sIAAsI;QACtI,iDAAiD;QACjD,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACnC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;YAEjD,IAAI,CAAC,qBAAqB,GAAG,MAAM,CAAC,WAAW,CAAC,KAAK,IAAI,EAAE;gBACzD,IAAI,OAAO,CAAC,OAAO,CAAC,2BAA2B,CAAC,EAAE,CAAC;oBACjD,MAAM,IAAI,CAAC,yBAAyB,CAAC,OAAO,CAAC,CAAC;gBAChD,CAAC;YACH,CAAC,EAAE,GAAG,CAAC,CAAC;QACV,CAAC;IACH,CAAC;IAED,KAAK,CAAC,yBAAyB,CAAC,OAA8B;QAC5D,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;YAC3B,MAAM,OAAO,CAAC,cAAc,CAAC;QAC/B,CAAC;QACD,IAAI,CAAC,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IACtD,CAAC;CACF;AAnHC;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;2CACC;AAM5B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC;qDACpC;AAGtB;IADC,KAAK,CAAC,MAAM,CAAC;6CACW;AA4G3B,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC","sourcesContent":["import { ZuiBaseElement } from '@zywave/zui-base';\nimport { findAssignedElement } from '@zywave/zui-base/dist/utils/find-assigned-element.js';\nimport { html, nothing } from 'lit';\nimport { property, query } from 'lit/decorators.js';\nimport { style } from './zui-formfield-css.js';\nimport type { PropertyValues } from 'lit';\n\n/**\n * `<zui-formfield>` provides a standardized way of labeling and styling form controls.\n *\n * @slot - Default, unnamed slot; for inserting form controls, such as `<select>`, `<input>`, `<zui-input>`, `<zui-select>`, etc., into `<zui-formfield>`\n *\n * @csspart container - The container for form fields inserted into `zui-formfield`; this is exposed as a CSS shadow part and can be accessed with `::part(container)`.\n * @csspart label - The label for `zui-formfield`; this is exposed as a CSS shadow part and can be accessed with `::part(label)`.\n */\nexport class ZuiFormField extends ZuiBaseElement {\n /**\n * (optional): Label text, for the form control. Alternatively can slot in label text instead. If necessary, can be styled with `::part(label)`.\n */\n @property({ type: String })\n label: string | null = null;\n\n /**\n * (optional): Provide a valid CSS selector to help `zui-formfield` find the correct form control. Defaults to the first child element.\n */\n @property({ type: String, attribute: 'control-selector' })\n controlSelector = '*';\n\n @query('slot')\n _slotEl: HTMLSlotElement;\n\n #validationMessageField?: string | null | undefined;\n\n #validationIntervalId?: number | undefined;\n\n get #validationMessage() {\n return this.#validationMessageField;\n }\n\n set #validationMessage(msg: string | null | undefined) {\n const oldValue = this.#validationMessageField;\n if (oldValue !== msg) {\n this.#validationMessageField = msg?.trim();\n this.requestUpdate();\n }\n }\n\n get #control() {\n return findAssignedElement(this._slotEl, this.controlSelector);\n }\n\n static get styles() {\n return [super.styles, style];\n }\n\n async firstUpdated(changedProps: PropertyValues) {\n super.firstUpdated(changedProps);\n\n if (this.#control) {\n this.#control.tagName.includes('-')\n ? customElements\n .whenDefined(this.#control.tagName.toLowerCase())\n .then(() => this.#configureValidation(this.#control as ValidatableLitElement))\n : this.#configureValidation(this.#control as ValidatableLitElement);\n }\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n\n if (this.#validationIntervalId) {\n clearInterval(this.#validationIntervalId);\n this.#validationIntervalId = undefined;\n }\n }\n\n render() {\n return html`\n <div class=\"container\" part=\"container\">\n ${this.label ? html` <label @click=\"${this.#onLabelClick}\" part=\"label\">${this.label}</label> ` : nothing}\n <slot @slotchange=\"${this.#onSlotChange}\"></slot>\n <div class=\"validation-message\">${this.#validationMessage ?? ''}</div>\n </div>\n `;\n }\n\n #onLabelClick() {\n this.#control?.focus();\n this.#control?.click();\n }\n\n #onSlotChange() {\n if (this.#control) {\n this.#configureValidation(this.#control as ValidatableLitElement);\n }\n }\n\n #configureValidation(control: ValidatableLitElement) {\n if (!control || typeof control.checkValidity !== 'function') {\n return;\n }\n\n control.addEventListener(\n 'invalid',\n async (event) => {\n event.preventDefault();\n await this.#extractValidationMessage(control);\n },\n { capture: true }\n );\n\n control.addEventListener('blur', async () => await this.#extractValidationMessage(control));\n control.addEventListener('input', async () => await this.#extractValidationMessage(control));\n control.addEventListener('change', async () => await this.#extractValidationMessage(control));\n control.addEventListener('validitystatechange', async () => await this.#extractValidationMessage(control));\n\n // workaround native elements not necessarily raising events for direct manipulation (i.e. input.value = 'foo' doesn't trigger events)\n // see https://github.com/whatwg/html/issues/9878\n if (!control.tagName.includes('-')) {\n window.clearInterval(this.#validationIntervalId);\n\n this.#validationIntervalId = window.setInterval(async () => {\n if (control.matches(':user-invalid,:user-valid')) {\n await this.#extractValidationMessage(control);\n }\n }, 100);\n }\n }\n\n async #extractValidationMessage(control: ValidatableLitElement) {\n if (control.updateComplete) {\n await control.updateComplete;\n }\n this.#validationMessage = control.validationMessage;\n }\n}\n\nwindow.customElements.define('zui-formfield', ZuiFormField);\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'zui-formfield': ZuiFormField;\n }\n}\n\ntype ValidatableLitElement = ValidatableHTMLElement & {\n updateComplete?: Promise<void>;\n};\n\ntype ValidatableHTMLElement = HTMLElement & {\n setCustomValidity(message: string): void;\n checkValidity(): boolean;\n reportValidity(): boolean;\n validationMessage: string;\n validity: ValidityState;\n};\n"]}
1
+ {"version":3,"file":"zui-formfield.js","sourceRoot":"","sources":["../src/zui-formfield.ts"],"names":[],"mappings":"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","sourcesContent":["import { ZuiBaseElement } from '@zywave/zui-base';\nimport { findAssignedElement } from '@zywave/zui-base/dist/utils/find-assigned-element.js';\nimport { html, nothing } from 'lit';\nimport { property, query } from 'lit/decorators.js';\nimport { style } from './zui-formfield-css.js';\nimport type { PropertyValues } from 'lit';\n\n/**\n * `<zui-formfield>` provides a standardized way of labeling and styling form controls.\n *\n * @slot - Default, unnamed slot; for inserting form controls, such as `<select>`, `<input>`, `<zui-input>`, `<zui-select>`, etc., into `<zui-formfield>`\n *\n * @csspart container - The container for form fields inserted into `zui-formfield`; this is exposed as a CSS shadow part and can be accessed with `::part(container)`.\n * @csspart label - The label for `zui-formfield`; this is exposed as a CSS shadow part and can be accessed with `::part(label)`.\n */\nexport class ZuiFormField extends ZuiBaseElement {\n /**\n * (optional): Label text, for the form control. Alternatively can slot in label text instead. If necessary, can be styled with `::part(label)`.\n */\n @property({ type: String })\n label: string | null = null;\n\n /**\n * (optional): Provide a valid CSS selector to help `zui-formfield` find the correct form control. Defaults to the first child element.\n */\n @property({ type: String, attribute: 'control-selector' })\n controlSelector = '*';\n\n @query('slot')\n _slotEl: HTMLSlotElement;\n\n #validationMessageField?: string | null | undefined;\n\n #validationIntervalId?: number | undefined;\n\n #isRequired = false;\n\n get #validationMessage() {\n return this.#validationMessageField;\n }\n\n set #validationMessage(msg: string | null | undefined) {\n const oldValue = this.#validationMessageField;\n if (oldValue !== msg) {\n this.#validationMessageField = msg?.trim();\n this.requestUpdate();\n }\n }\n\n get #control() {\n return findAssignedElement(this._slotEl, this.controlSelector);\n }\n\n #checkRequired() {\n const control = this.#control;\n const wasRequired = this.#isRequired;\n this.#isRequired = control?.hasAttribute('required') ?? false;\n\n if (wasRequired !== this.#isRequired) {\n this.requestUpdate();\n }\n }\n\n static get styles() {\n return [super.styles, style];\n }\n\n async firstUpdated(changedProps: PropertyValues) {\n super.firstUpdated(changedProps);\n\n if (this.#control) {\n this.#checkRequired();\n this.#control.tagName.includes('-')\n ? customElements\n .whenDefined(this.#control.tagName.toLowerCase())\n .then(() => this.#configureValidation(this.#control as ValidatableLitElement))\n : this.#configureValidation(this.#control as ValidatableLitElement);\n }\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n\n if (this.#validationIntervalId) {\n clearInterval(this.#validationIntervalId);\n this.#validationIntervalId = undefined;\n }\n }\n\n render() {\n return html`\n <div class=\"container\" part=\"container\">\n ${this.label\n ? html`\n <label @click=\"${this.#onLabelClick}\" part=\"label\" class=\"${this.#isRequired ? 'required' : ''}\">\n ${this.label}\n </label>\n `\n : nothing}\n <slot @slotchange=\"${this.#onSlotChange}\"></slot>\n <div class=\"validation-message\">${this.#validationMessage ?? ''}</div>\n </div>\n `;\n }\n\n #onLabelClick() {\n this.#control?.focus();\n this.#control?.click();\n }\n\n #onSlotChange() {\n if (this.#control) {\n this.#checkRequired();\n this.#configureValidation(this.#control as ValidatableLitElement);\n }\n }\n\n #configureValidation(control: ValidatableLitElement) {\n if (!control || typeof control.checkValidity !== 'function') {\n return;\n }\n\n control.addEventListener(\n 'invalid',\n async (event) => {\n event.preventDefault();\n await this.#extractValidationMessage(control);\n },\n { capture: true }\n );\n\n control.addEventListener('blur', async () => await this.#extractValidationMessage(control));\n control.addEventListener('input', async () => await this.#extractValidationMessage(control));\n control.addEventListener('change', async () => await this.#extractValidationMessage(control));\n control.addEventListener('validitystatechange', async () => await this.#extractValidationMessage(control));\n\n // workaround native elements not necessarily raising events for direct manipulation (i.e. input.value = 'foo' doesn't trigger events)\n // see https://github.com/whatwg/html/issues/9878\n if (!control.tagName.includes('-')) {\n window.clearInterval(this.#validationIntervalId);\n\n this.#validationIntervalId = window.setInterval(async () => {\n if (control.matches(':user-invalid,:user-valid')) {\n await this.#extractValidationMessage(control);\n }\n }, 100);\n }\n }\n\n async #extractValidationMessage(control: ValidatableLitElement) {\n if (control.updateComplete) {\n await control.updateComplete;\n }\n this.#validationMessage = control.validationMessage;\n }\n}\n\nwindow.customElements.define('zui-formfield', ZuiFormField);\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'zui-formfield': ZuiFormField;\n }\n}\n\ntype ValidatableLitElement = ValidatableHTMLElement & {\n updateComplete?: Promise<void>;\n};\n\ntype ValidatableHTMLElement = HTMLElement & {\n setCustomValidity(message: string): void;\n checkValidity(): boolean;\n reportValidity(): boolean;\n validationMessage: string;\n validity: ValidityState;\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zywave/zui-formfield",
3
- "version": "4.2.2-pre.0",
3
+ "version": "4.3.0-pre.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "license": "UNLICENSED",
@@ -24,5 +24,5 @@
24
24
  "access": "public"
25
25
  },
26
26
  "customElements": "dist/custom-elements.json",
27
- "gitHead": "3749b9b5bd4d2c0fa1b7e4ed68b695c960f99b4e"
27
+ "gitHead": "cdf7b9622b4f662734912310f3582350ab1afd75"
28
28
  }
@@ -1,3 +1,3 @@
1
1
  import { css } from 'lit';
2
2
 
3
- export const style = css`:host .container label,:host .container ::slotted(label){display:inline-flex;margin:0;font-weight:600;color:var(--zui-gray-800)}:host{contain:none}:host .container{display:flex;flex-direction:column;margin-bottom:1.25rem}:host .container ::slotted(*:not(zui-toggle):not(zui-checkbox):not(zui-radio)){margin-top:.25rem !important}:host .container ::slotted(zui-toggle){margin:.9375rem 0}:host .validation-message:empty{display:none}:host .validation-message{color:var(--zui-red-500)}`;
3
+ export const style = css`:host .container label,:host .container ::slotted(label){display:inline-flex;margin:0;font-weight:600;color:var(--zui-gray-800)}:host{contain:none}:host .container{display:flex;flex-direction:column;margin-bottom:1.25rem}:host .container ::slotted(*:not(zui-toggle):not(zui-checkbox):not(zui-radio)){margin-top:.25rem !important}:host .container ::slotted(zui-toggle){margin:.9375rem 0}:host .validation-message:empty{display:none}:host .validation-message{color:var(--zui-red-500)}:host label.required::after{content:"*";margin-left:.5ch;color:var(--zui-red-500)}`;
@@ -29,4 +29,10 @@
29
29
  .validation-message {
30
30
  color: var(--zui-red-500);
31
31
  }
32
+
33
+ label.required::after {
34
+ content: '*';
35
+ margin-left: 0.5ch;
36
+ color: var(--zui-red-500);
37
+ }
32
38
  }
@@ -33,6 +33,8 @@ export class ZuiFormField extends ZuiBaseElement {
33
33
 
34
34
  #validationIntervalId?: number | undefined;
35
35
 
36
+ #isRequired = false;
37
+
36
38
  get #validationMessage() {
37
39
  return this.#validationMessageField;
38
40
  }
@@ -49,6 +51,16 @@ export class ZuiFormField extends ZuiBaseElement {
49
51
  return findAssignedElement(this._slotEl, this.controlSelector);
50
52
  }
51
53
 
54
+ #checkRequired() {
55
+ const control = this.#control;
56
+ const wasRequired = this.#isRequired;
57
+ this.#isRequired = control?.hasAttribute('required') ?? false;
58
+
59
+ if (wasRequired !== this.#isRequired) {
60
+ this.requestUpdate();
61
+ }
62
+ }
63
+
52
64
  static get styles() {
53
65
  return [super.styles, style];
54
66
  }
@@ -57,6 +69,7 @@ export class ZuiFormField extends ZuiBaseElement {
57
69
  super.firstUpdated(changedProps);
58
70
 
59
71
  if (this.#control) {
72
+ this.#checkRequired();
60
73
  this.#control.tagName.includes('-')
61
74
  ? customElements
62
75
  .whenDefined(this.#control.tagName.toLowerCase())
@@ -77,7 +90,13 @@ export class ZuiFormField extends ZuiBaseElement {
77
90
  render() {
78
91
  return html`
79
92
  <div class="container" part="container">
80
- ${this.label ? html` <label @click="${this.#onLabelClick}" part="label">${this.label}</label> ` : nothing}
93
+ ${this.label
94
+ ? html`
95
+ <label @click="${this.#onLabelClick}" part="label" class="${this.#isRequired ? 'required' : ''}">
96
+ ${this.label}
97
+ </label>
98
+ `
99
+ : nothing}
81
100
  <slot @slotchange="${this.#onSlotChange}"></slot>
82
101
  <div class="validation-message">${this.#validationMessage ?? ''}</div>
83
102
  </div>
@@ -91,6 +110,7 @@ export class ZuiFormField extends ZuiBaseElement {
91
110
 
92
111
  #onSlotChange() {
93
112
  if (this.#control) {
113
+ this.#checkRequired();
94
114
  this.#configureValidation(this.#control as ValidatableLitElement);
95
115
  }
96
116
  }
@@ -151,4 +151,79 @@ suite('zui-formfield', () => {
151
151
 
152
152
  newForm.remove();
153
153
  });
154
+
155
+ suite('required indicator', () => {
156
+ test('adds required class to label when control has required attribute', async () => {
157
+ formFieldInput.setAttribute('required', '');
158
+
159
+ // Manually trigger the slot change handler
160
+ const slot = formField.shadowRoot.querySelector('slot') as HTMLSlotElement;
161
+ slot.dispatchEvent(new Event('slotchange'));
162
+
163
+ await formField.updateComplete;
164
+
165
+ const label = formField.shadowRoot.querySelector('label') as HTMLElement;
166
+ assert.isTrue(
167
+ label?.classList.contains('required'),
168
+ 'label should have required class when control has required attribute'
169
+ );
170
+ });
171
+
172
+ test('does not add required class to label when control does not have required attribute', async () => {
173
+ await formField.updateComplete;
174
+
175
+ const label = formField.shadowRoot.querySelector('label') as HTMLElement;
176
+ assert.isFalse(
177
+ label?.classList.contains('required'),
178
+ 'label should not have required class when control does not have required attribute'
179
+ );
180
+ });
181
+
182
+ test('updates required class when required attribute is added dynamically', async () => {
183
+ await formField.updateComplete;
184
+
185
+ let label = formField.shadowRoot.querySelector('label') as HTMLElement;
186
+ assert.isFalse(label?.classList.contains('required'), 'label should not have required class initially');
187
+
188
+ formFieldInput.setAttribute('required', '');
189
+
190
+ // Manually trigger the slot change handler
191
+ const slot = formField.shadowRoot.querySelector('slot') as HTMLSlotElement;
192
+ slot.dispatchEvent(new Event('slotchange'));
193
+
194
+ await formField.updateComplete;
195
+
196
+ label = formField.shadowRoot.querySelector('label') as HTMLElement;
197
+ assert.isTrue(
198
+ label?.classList.contains('required'),
199
+ 'label should have required class after required attribute is added'
200
+ );
201
+ });
202
+
203
+ test('updates required class when required attribute is removed dynamically', async () => {
204
+ formFieldInput.setAttribute('required', '');
205
+
206
+ // Manually trigger the slot change handler to apply required class
207
+ const slot = formField.shadowRoot.querySelector('slot') as HTMLSlotElement;
208
+ slot.dispatchEvent(new Event('slotchange'));
209
+
210
+ await formField.updateComplete;
211
+
212
+ let label = formField.shadowRoot.querySelector('label') as HTMLElement;
213
+ assert.isTrue(label?.classList.contains('required'), 'label should have required class initially');
214
+
215
+ formFieldInput.removeAttribute('required');
216
+
217
+ // Manually trigger the slot change handler again to remove required class
218
+ slot.dispatchEvent(new Event('slotchange'));
219
+
220
+ await formField.updateComplete;
221
+
222
+ label = formField.shadowRoot.querySelector('label') as HTMLElement;
223
+ assert.isFalse(
224
+ label?.classList.contains('required'),
225
+ 'label should not have required class after required attribute is removed'
226
+ );
227
+ });
228
+ });
154
229
  });