@kubex/zinc 1.1.104 → 1.1.106

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.
@@ -378,6 +378,8 @@ You can also prevent close from specific sources by checking the `event.detail.s
378
378
 
379
379
  Add the `dialog-closer` attribute to any element inside the dialog to make it close the dialog when clicked. This is commonly used with buttons in the footer.
380
380
 
381
+ The `modal-closer` attribute works the same way but closes the nearest containing modal of either kind — dialog or slideout — and, unlike `dialog-closer`, works from inside a nested component's shadow root. `<zn-form-actions>` uses it on its cancel button.
382
+
381
383
  ```html:preview
382
384
  <zn-button id="dialog-closer-trigger">Open Dialog</zn-button>
383
385
  <zn-dialog trigger="dialog-closer-trigger" label="Dialog Closer">
@@ -0,0 +1,71 @@
1
+ ---
2
+ meta:
3
+ title: Form Actions
4
+ description: A standard action row for the bottom of a form, built up from a lone submit button.
5
+ layout: component
6
+ ---
7
+
8
+ Drops the standard action row into a form. The default is just a primary submit button — build it up with `with-cancel` and `with-reset` as the form needs. Place it inside the form it should act on.
9
+
10
+ ```html:preview
11
+ <form>
12
+ <zn-form-actions></zn-form-actions>
13
+ </form>
14
+ ```
15
+
16
+ ## Examples
17
+
18
+ ### With a Cancel Button
19
+
20
+ Use the `with-cancel` attribute to add a cancel button. When the form lives inside a dialog or slideout, cancelling closes it; it also emits a `zn-cancel` event for the container to handle.
21
+
22
+ ```html:preview
23
+ <zn-dialog trigger="form-actions-dialog" label="New Ticket">
24
+ <form>
25
+ <zn-form-actions with-cancel></zn-form-actions>
26
+ </form>
27
+ </zn-dialog>
28
+ <zn-button id="form-actions-dialog">Open Dialog</zn-button>
29
+ ```
30
+
31
+ ### With a Reset Button
32
+
33
+ Use the `with-reset` attribute to add a button that resets the form's fields.
34
+
35
+ ```html:preview
36
+ <form>
37
+ <input type="text" name="example" value="Change me, then reset">
38
+ <zn-form-actions with-reset></zn-form-actions>
39
+ </form>
40
+ ```
41
+
42
+ ### Custom Text and Icons
43
+
44
+ Each button's text and icon can be overridden with `submit-text` / `submit-icon`, `cancel-text` / `cancel-icon` and `reset-text` / `reset-icon`.
45
+
46
+ ```html:preview
47
+ <form>
48
+ <zn-form-actions with-cancel submit-text="Create Ticket" submit-icon="plus@lu" cancel-text="Discard" cancel-icon="trash-2@lu"></zn-form-actions>
49
+ </form>
50
+ ```
51
+
52
+ ### Extra Actions
53
+
54
+ Extra buttons placed in the default slot appear before the standard buttons.
55
+
56
+ ```html:preview
57
+ <form>
58
+ <zn-form-actions>
59
+ <zn-button color="transparent">Preview</zn-button>
60
+ </zn-form-actions>
61
+ </form>
62
+ ```
63
+
64
+ ### Targeting a Form by Id
65
+
66
+ When the component can't live inside the form, point it at one with the `form` attribute.
67
+
68
+ ```html:preview
69
+ <form id="standalone-form"></form>
70
+ <zn-form-actions form="standalone-form"></zn-form-actions>
71
+ ```
@@ -248,6 +248,8 @@ You can also prevent close from specific sources by checking the `event.detail.s
248
248
 
249
249
  Add the `slideout-closer` attribute to any element inside the slideout to make it close the slideout when clicked. This is commonly used with buttons in the footer.
250
250
 
251
+ The `modal-closer` attribute works the same way but closes the nearest containing modal of either kind — dialog or slideout — and, unlike `slideout-closer`, works from inside a nested component's shadow root. `<zn-form-actions>` uses it on its cancel button.
252
+
251
253
  ```html:preview
252
254
  <zn-button id="slideout-closer-trigger">Open Slideout</zn-button>
253
255
  <zn-slideout trigger="slideout-closer-trigger" label="Slideout Closer">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.104",
3
+ "version": "1.1.106",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -57,6 +57,10 @@
57
57
  &__label--hidden {
58
58
  visibility: hidden;
59
59
  }
60
+
61
+ zn-icon {
62
+ --icon-color: currentColor;
63
+ }
60
64
  }
61
65
 
62
66
  .button.button--muted-notification {
@@ -184,6 +184,20 @@ export default class ZnDialog extends ZincElement {
184
184
  private closeClickHandler = (event: MouseEvent) => {
185
185
  if (event.target instanceof HTMLElement && event.target.hasAttribute('dialog-closer')) {
186
186
  this.hide();
187
+ return;
188
+ }
189
+
190
+ // `modal-closer` works from nested shadow roots (e.g. zn-form-actions' cancel button),
191
+ // where retargeting hides the source from event.target. Walk outward from the click:
192
+ // hitting another modal first means the closer belongs to it, not this dialog.
193
+ for (const el of event.composedPath()) {
194
+ if (el === this) return;
195
+ if (!(el instanceof HTMLElement)) continue;
196
+ if (el.hasAttribute('modal-closer')) {
197
+ this.hide();
198
+ return;
199
+ }
200
+ if (el.localName === 'zn-dialog' || el.localName === 'zn-slideout') return;
187
201
  }
188
202
  }
189
203
 
@@ -0,0 +1,96 @@
1
+ import {type CSSResultGroup, html, unsafeCSS} from 'lit';
2
+ import {idSelector} from "../../utilities/query";
3
+ import {property} from 'lit/decorators.js';
4
+ import ZincElement from '../../internal/zinc-element';
5
+ import ZnButton from "../button";
6
+
7
+ import styles from './form-actions.scss';
8
+
9
+ /**
10
+ * @summary Standard action row for the bottom of a form. Renders a submit button by default; opt in to more
11
+ * with `with-cancel` and `with-reset`.
12
+ * @documentation https://zinc.style/components/form-actions
13
+ * @status experimental
14
+ * @since 1.0
15
+ *
16
+ * @dependency zn-button
17
+ *
18
+ * @event zn-cancel - Emitted when the cancel button is clicked.
19
+ *
20
+ * @slot - Extra actions, placed before the buttons.
21
+ *
22
+ * @csspart cancel-button - The cancel button.
23
+ * @csspart reset-button - The reset button.
24
+ * @csspart submit-button - The submit button.
25
+ */
26
+ export default class ZnFormActions extends ZincElement {
27
+ static styles: CSSResultGroup = unsafeCSS(styles);
28
+ static dependencies = {
29
+ 'zn-button': ZnButton
30
+ };
31
+
32
+ /** The submit button's text. */
33
+ @property({attribute: 'submit-text'}) submitText = 'Save';
34
+
35
+ /** The submit button's icon. */
36
+ @property({attribute: 'submit-icon'}) submitIcon = 'check@lu';
37
+
38
+ /** Adds a cancel button. Closes the containing dialog or slideout, if any, and emits `zn-cancel`. */
39
+ @property({attribute: 'with-cancel', type: Boolean}) withCancel = false;
40
+
41
+ /** The cancel button's text. */
42
+ @property({attribute: 'cancel-text'}) cancelText = 'Cancel';
43
+
44
+ /** The cancel button's icon. */
45
+ @property({attribute: 'cancel-icon'}) cancelIcon = 'x@lu';
46
+
47
+ /** Adds a reset button that resets the form. */
48
+ @property({attribute: 'with-reset', type: Boolean}) withReset = false;
49
+
50
+ /** The reset button's text. */
51
+ @property({attribute: 'reset-text'}) resetText = 'Reset';
52
+
53
+ /** The reset button's icon. */
54
+ @property({attribute: 'reset-icon'}) resetIcon = 'rotate-ccw@lu';
55
+
56
+ /** The id of the form to act on. If omitted, the closest containing form is used. */
57
+ @property() form: string;
58
+
59
+ // The buttons live in this component's shadow root, so zn-button's own
60
+ // submit/reset handling can't find the form — resolve it from the host.
61
+ private getForm(): HTMLFormElement | null {
62
+ if (this.form) {
63
+ const root = this.getRootNode() as Document | ShadowRoot;
64
+ return root.querySelector<HTMLFormElement>(idSelector(this.form));
65
+ }
66
+ return this.closest('form');
67
+ }
68
+
69
+ private handleCancel = () => {
70
+ this.emit('zn-cancel');
71
+ };
72
+
73
+ private handleReset = () => {
74
+ this.getForm()?.reset();
75
+ };
76
+
77
+ private handleSubmit = () => {
78
+ this.getForm()?.requestSubmit();
79
+ };
80
+
81
+ render() {
82
+ return html`
83
+ <slot></slot>
84
+ ${this.withCancel ? html`
85
+ <zn-button part="cancel-button" panel-bg modal-closer icon="${this.cancelIcon}" @click="${this.handleCancel}">
86
+ ${this.cancelText}
87
+ </zn-button>` : ''}
88
+ ${this.withReset ? html`
89
+ <zn-button part="reset-button" panel-bg icon="${this.resetIcon}" @click="${this.handleReset}">
90
+ ${this.resetText}
91
+ </zn-button>` : ''}
92
+ <zn-button part="submit-button" color="primary" icon="${this.submitIcon}" @click="${this.handleSubmit}">
93
+ ${this.submitText}
94
+ </zn-button>`;
95
+ }
96
+ }
@@ -0,0 +1,9 @@
1
+ @use "../../wc";
2
+
3
+ // Keep in sync with the global .form-actions layout in scss/_global-spacing.scss.
4
+ :host {
5
+ display: flex;
6
+ justify-content: flex-end;
7
+ align-items: center;
8
+ gap: var(--zn-spacing-small);
9
+ }
@@ -0,0 +1,40 @@
1
+ import '../../../dist/zn.min.js';
2
+ import { expect, fixture, html, oneEvent } from '@open-wc/testing';
3
+
4
+ // ZnButton overrides click() without dispatching a DOM event, so fire a real one.
5
+ const clickButton = (button: HTMLElement) =>
6
+ button.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true, cancelable: true }));
7
+
8
+ describe('<zn-form-actions>', () => {
9
+ it('should render a component', async () => {
10
+ const el = await fixture(html` <zn-form-actions></zn-form-actions> `);
11
+
12
+ expect(el).to.exist;
13
+ });
14
+
15
+ it('should emit zn-cancel when the cancel button is clicked', async () => {
16
+ const el = await fixture<HTMLElement>(html` <zn-form-actions with-cancel></zn-form-actions> `);
17
+ const cancel = el.shadowRoot!.querySelector<HTMLElement>('[part="cancel-button"]')!;
18
+
19
+ const listener = oneEvent(el, 'zn-cancel');
20
+ clickButton(cancel);
21
+
22
+ expect(await listener).to.exist;
23
+ });
24
+
25
+ it('should close a containing dialog when the cancel button is clicked', async () => {
26
+ const dialog = await fixture<HTMLElement & { open: boolean }>(html`
27
+ <zn-dialog open label="Test">
28
+ <form>
29
+ <zn-form-actions with-cancel></zn-form-actions>
30
+ </form>
31
+ </zn-dialog>
32
+ `);
33
+ const actions = dialog.querySelector('zn-form-actions')!;
34
+ const cancel = actions.shadowRoot!.querySelector<HTMLElement>('[part="cancel-button"]')!;
35
+
36
+ clickButton(cancel);
37
+
38
+ expect(dialog.open).to.be.false;
39
+ });
40
+ });
@@ -0,0 +1,12 @@
1
+ import ZnFormActions from './form-actions.component';
2
+
3
+ export * from './form-actions.component';
4
+ export default ZnFormActions;
5
+
6
+ ZnFormActions.define('zn-form-actions');
7
+
8
+ declare global {
9
+ interface HTMLElementTagNameMap {
10
+ 'zn-form-actions': ZnFormActions;
11
+ }
12
+ }
@@ -1,5 +1,5 @@
1
1
  import {classMap} from 'lit/directives/class-map.js';
2
- import {type CSSResultGroup, html, nothing, unsafeCSS} from 'lit';
2
+ import {type CSSResultGroup, html, nothing, type PropertyValues, unsafeCSS} from 'lit';
3
3
  import {defaultValue} from '../../internal/default-value';
4
4
  import {FormControlController} from '../../internal/form';
5
5
  import {property, query, state} from 'lit/decorators.js';
@@ -244,6 +244,36 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
244
244
  }
245
245
  }
246
246
 
247
+ // The [icon]/[library]/[color] hidden inputs must live in the light DOM —
248
+ // shadow DOM inputs are invisible to form submission.
249
+ private syncHiddenInputs() {
250
+ const fields: [string, string, boolean][] = [
251
+ ['icon', this.icon, true],
252
+ ['library', this.library, !this.noLibrary],
253
+ ['color', this.color, !this.noColor],
254
+ ];
255
+ for (const [key, value, enabled] of fields) {
256
+ const name = `${this.name}[${key}]`;
257
+ let input = this.querySelector<HTMLInputElement>(`input[type="hidden"][name="${name}"]`);
258
+ if (!this.name || !enabled) {
259
+ input?.remove();
260
+ continue;
261
+ }
262
+ if (!input) {
263
+ input = document.createElement('input');
264
+ input.type = 'hidden';
265
+ input.name = name;
266
+ this.appendChild(input);
267
+ }
268
+ input.value = value ?? '';
269
+ }
270
+ }
271
+
272
+ protected updated(changedProperties: PropertyValues) {
273
+ super.updated(changedProperties);
274
+ this.syncHiddenInputs();
275
+ }
276
+
247
277
  private handleSearchInput(e: Event) {
248
278
  const input = e.target as HTMLInputElement;
249
279
  this._searchQuery = input.value.toLowerCase();
@@ -381,12 +411,6 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
381
411
  `}
382
412
  </div>
383
413
 
384
- ${this.name ? html`
385
- <input type="hidden" name="${this.name}[icon]" .value=${this.icon}>
386
- ${!this.noLibrary ? html`<input type="hidden" name="${this.name}[library]" .value=${this.library}>` : nothing}
387
- ${!this.noColor ? html`<input type="hidden" name="${this.name}[color]" .value=${this.color}>` : nothing}
388
- ` : nothing}
389
-
390
414
  <div
391
415
  part="form-control-help-text"
392
416
  class="form-control__help-text"
@@ -157,6 +157,20 @@ export default class ZnSlideout extends ZincElement {
157
157
  private closeClickHandler = (event: MouseEvent) => {
158
158
  if (event.target instanceof HTMLElement && event.target.hasAttribute('slideout-closer')) {
159
159
  this.hide();
160
+ return;
161
+ }
162
+
163
+ // `modal-closer` works from nested shadow roots (e.g. zn-form-actions' cancel button),
164
+ // where retargeting hides the source from event.target. Walk outward from the click:
165
+ // hitting another modal first means the closer belongs to it, not this slideout.
166
+ for (const el of event.composedPath()) {
167
+ if (el === this) return;
168
+ if (!(el instanceof HTMLElement)) continue;
169
+ if (el.hasAttribute('modal-closer')) {
170
+ void this.hide();
171
+ return;
172
+ }
173
+ if (el.localName === 'zn-dialog' || el.localName === 'zn-slideout') return;
160
174
  }
161
175
  }
162
176
 
@@ -142,6 +142,7 @@ a {
142
142
  align-items: center;
143
143
  column-gap: var(--zn-spacing-large);
144
144
  min-width: 0;
145
+ flex-shrink: 0;
145
146
  overflow: hidden;
146
147
 
147
148
  ::slotted(*) {
package/src/zinc.ts CHANGED
@@ -63,6 +63,7 @@ export { default as Option } from './components/option';
63
63
  export { default as Textarea } from './components/textarea';
64
64
  export { default as Checkbox } from './components/checkbox';
65
65
  export { default as Datepicker } from './components/datepicker';
66
+ export { default as FormActions } from './components/form-actions';
66
67
  export { default as FormGroup } from './components/form-group';
67
68
  export { default as InputGroup } from './components/input-group';
68
69
  export { default as LinkedSelect } from './components/linked-select';