@nectary/components 4.9.0 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/avatar/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { defineCustomElement, getAttribute, getLiteralAttribute, NectaryElement, updateAttribute, updateLiteralAttribute } from '../utils';
2
2
  import { DEFAULT_SIZE, sizeValues } from '../utils/size';
3
- const templateHTML = '<style>:host{display:inline-block;vertical-align:middle;outline:0}#wrapper{position:relative;width:var(--sinch-local-size);height:var(--sinch-local-size);background-color:var(--sinch-comp-avatar-border-color-default-initial);border-radius:var(--sinch-comp-avatar-shape-radius);--sinch-local-size:var(--sinch-comp-avatar-size-m)}:host([size="l"]) #wrapper{--sinch-local-size:var(--sinch-comp-avatar-size-l)}:host([size="s"]) #wrapper{--sinch-local-size:var(--sinch-comp-avatar-size-s)}#text{display:block;width:100%;height:100%;font:var(--sinch-comp-avatar-container-font-size-m-text);line-height:calc(var(--sinch-local-size) - 2px);text-transform:uppercase;text-align:center}:host([size="l"]) #text{font:var(--sinch-comp-avatar-container-font-size-l-text);line-height:calc(var(--sinch-local-size) - 2px)}:host([size="s"]) #text{font:var(--sinch-comp-avatar-container-font-size-s-text);line-height:calc(var(--sinch-local-size) - 2px)}#circle{position:relative;width:calc(100% - 2px);height:calc(100% - 2px);left:1px;top:1px;border-radius:50%;-webkit-mask:linear-gradient(#fff,#000);mask:linear-gradient(#fff,#000);background-color:var(--sinch-comp-avatar-container-color-default-background);color:var(--sinch-comp-avatar-container-color-default-foreground)}#image{display:none;position:absolute;left:0;top:0;width:100%;height:100%;object-fit:contain}:host([src]:not([src=""])) #image{display:block}#person{display:none;position:absolute;left:0;top:0;width:100%;height:100%;fill:var(--sinch-comp-avatar-container-color-default-foreground);opacity:.15}:host(:not([src]):is(:not([alt]),[alt=""])) #person{display:block}#status-wrapper{position:absolute;left:calc(85% - 5px);top:calc(85% - 5px);width:10px;height:10px;padding:1px;box-sizing:border-box;border-radius:50%;background-color:var(--sinch-comp-avatar-border-color-default-initial);display:none;pointer-events:none}#status{width:8px;height:8px;border-radius:50%}:host([status=away]) #status-wrapper,:host([status=busy]) #status-wrapper,:host([status=offline]) #status-wrapper,:host([status=online]) #status-wrapper{display:block}:host([status=online]) #status{background-color:var(--sinch-comp-avatar-status-color-online-default-background)}:host([status=away]) #status{background-color:var(--sinch-comp-avatar-status-color-away-default-background)}:host([status=busy]) #status{background-color:var(--sinch-comp-avatar-status-color-busy-default-background)}:host([status=offline]) #status{background-color:var(--sinch-comp-avatar-status-color-offline-default-background)}</style><div id="wrapper"><div id="circle"><span id="text"></span><img id="image" alt=""/><svg id="person" viewBox="0 0 40 40" fill="none"><path d="M29.451 15.785a9.451 9.451 0 1 1-18.902 0 9.452 9.452 0 0 1 18.902 0ZM4.734 40.5c.119-7.085 5.899-12.792 13.012-12.792h4.508c7.113 0 12.893 5.707 13.012 12.792H4.734Z"/></svg></div><div id="status-wrapper"><div id="status"></div></div></div>';
3
+ const templateHTML = '<style>:host{display:inline-block;vertical-align:middle;outline:0}#wrapper{position:relative;width:var(--sinch-local-size);height:var(--sinch-local-size);border-radius:var(--sinch-comp-avatar-shape-radius);--sinch-local-size:var(--sinch-comp-avatar-size-m)}:host([size="l"]) #wrapper{--sinch-local-size:var(--sinch-comp-avatar-size-l)}:host([size="s"]) #wrapper{--sinch-local-size:var(--sinch-comp-avatar-size-s)}#text{display:block;width:100%;height:100%;font:var(--sinch-comp-avatar-container-font-size-m-text);line-height:calc(var(--sinch-local-size) - 2px);text-transform:uppercase;text-align:center}:host([size="l"]) #text{font:var(--sinch-comp-avatar-container-font-size-l-text);line-height:calc(var(--sinch-local-size) - 2px)}:host([size="s"]) #text{font:var(--sinch-comp-avatar-container-font-size-s-text);line-height:calc(var(--sinch-local-size) - 2px)}#circle{position:relative;width:calc(100% - 2px);height:calc(100% - 2px);left:1px;top:1px;border-radius:50%;-webkit-mask:linear-gradient(#fff,#000);mask:linear-gradient(#fff,#000);background-color:var(--sinch-comp-avatar-container-color-default-background);color:var(--sinch-comp-avatar-container-color-default-foreground)}#image{display:none;position:absolute;left:0;top:0;width:100%;height:100%;object-fit:contain}:host([src]:not([src=""])) #image{display:block}#person{display:none;position:absolute;left:0;top:0;width:100%;height:100%;fill:var(--sinch-comp-avatar-container-color-default-foreground);opacity:.15}:host(:not([src]):is(:not([alt]),[alt=""])) #person{display:block}#status-wrapper{position:absolute;left:calc(85% - 5px);top:calc(85% - 5px);width:10px;height:10px;padding:1px;box-sizing:border-box;border-radius:50%;background-color:var(--sinch-comp-avatar-status-color-border);display:none;pointer-events:none}#status{width:8px;height:8px;border-radius:50%}:host([status=away]) #status-wrapper,:host([status=busy]) #status-wrapper,:host([status=offline]) #status-wrapper,:host([status=online]) #status-wrapper{display:block}:host([status=online]) #status{background-color:var(--sinch-comp-avatar-status-color-online-default-background)}:host([status=away]) #status{background-color:var(--sinch-comp-avatar-status-color-away-default-background)}:host([status=busy]) #status{background-color:var(--sinch-comp-avatar-status-color-busy-default-background)}:host([status=offline]) #status{background-color:var(--sinch-comp-avatar-status-color-offline-default-background)}</style><div id="wrapper"><div id="circle"><span id="text"></span><img id="image" alt=""/><svg id="person" viewBox="0 0 40 40" fill="none"><path d="M29.451 15.785a9.451 9.451 0 1 1-18.902 0 9.452 9.452 0 0 1 18.902 0ZM4.734 40.5c.119-7.085 5.899-12.792 13.012-12.792h4.508c7.113 0 12.893 5.707 13.012 12.792H4.734Z"/></svg></div><div id="status-wrapper"><div id="status"></div></div></div>';
4
4
  import { getAvatarColorBg, getAvatarColorFg, statusValues } from './utils';
5
5
  const template = document.createElement('template');
6
6
  template.innerHTML = templateHTML;
package/avatar/types.d.ts CHANGED
@@ -14,11 +14,29 @@ export type TSinchAvatarProps = {
14
14
  size?: TSinchSize;
15
15
  /** Status */
16
16
  status?: TSinchAvatarStatus;
17
+ } & {
18
+ style?: {
19
+ '--sinch-comp-avatar-container-color-default-background'?: string;
20
+ '--sinch-comp-avatar-container-color-default-foreground'?: string;
21
+ '--sinch-comp-avatar-status-color-border'?: string;
22
+ '--sinch-comp-avatar-status-color-online-default-background'?: string;
23
+ '--sinch-comp-avatar-status-color-away-default-background'?: string;
24
+ '--sinch-comp-avatar-status-color-busy-default-background'?: string;
25
+ '--sinch-comp-avatar-status-color-offline-default-background'?: string;
26
+ '--sinch-comp-avatar-shape-radius'?: string;
27
+ '--sinch-comp-avatar-size-s'?: string;
28
+ '--sinch-comp-avatar-size-m'?: string;
29
+ '--sinch-comp-avatar-size-l'?: string;
30
+ '--sinch-comp-avatar-container-font-size-s-text'?: string;
31
+ '--sinch-comp-avatar-container-font-size-m-text'?: string;
32
+ '--sinch-comp-avatar-container-font-size-l-text'?: string;
33
+ '--sinch-local-size'?: string;
34
+ };
17
35
  };
18
36
  export type TSinchAvatarStyle = {
19
37
  '--sinch-comp-avatar-container-color-default-background'?: string;
20
38
  '--sinch-comp-avatar-container-color-default-foreground'?: string;
21
- '--sinch-comp-avatar-border-color-default-initial'?: string;
39
+ '--sinch-comp-avatar-status-color-border'?: string;
22
40
  '--sinch-comp-avatar-status-color-online-default-background'?: string;
23
41
  '--sinch-comp-avatar-status-color-away-default-background'?: string;
24
42
  '--sinch-comp-avatar-status-color-busy-default-background'?: string;
package/button/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { defineCustomElement, getBooleanAttribute, getAttribute, isAttrTrue, updateBooleanAttribute, updateAttribute, NectaryElement, getReactEventHandler, getLiteralAttribute, updateLiteralAttribute, Context, subscribeContext, isAttrEqual } from '../utils';
2
+ import { requestSubmitForm } from '../utils/form';
2
3
  import { DEFAULT_SIZE, sizeExValues } from '../utils/size';
3
4
  const templateHTML = '<style>:host{display:inline-block;vertical-align:middle;outline:0;cursor:pointer;user-select:none;--sinch-button-shape-radius-base:var(--sinch-comp-button-shape-radius-size-m);--sinch-local-shadow:var(--sinch-comp-button-shadow-secondary-initial);--sinch-button-border:1px solid;--sinch-button-border-top:var(--sinch-button-border);--sinch-button-border-bottom:var(--sinch-button-border);--sinch-button-border-left:var(--sinch-button-border);--sinch-button-border-right:var(--sinch-button-border);--sinch-button-shape-radius-top-right:unset;--sinch-button-shape-radius-top-left:unset;--sinch-button-shape-radius-bottom-right:unset;--sinch-button-shape-radius-bottom-left:unset}:host([disabled]){cursor:initial}#button{all:initial;display:block;position:relative;width:100%;height:var(--sinch-local-size);user-select:none;--sinch-local-size:var(--sinch-comp-button-size-container-m);--sinch-local-padding:16px;--sinch-local-font:var(--sinch-comp-button-font-size-m-text);--sinch-global-size-icon:var(--sinch-comp-button-size-icon-m);--sinch-global-color-icon:var(--sinch-local-color-icon)}:host([data-size="l"])>#button{--sinch-local-size:var(--sinch-comp-button-size-container-l);--sinch-button-set-size-shape-radius:var(--sinch-comp-button-shape-radius-size-l);--sinch-local-font:var(--sinch-comp-button-font-size-l-text);--sinch-global-size-icon:var(--sinch-comp-button-size-icon-l)}:host([data-size="m"])>#button{--sinch-local-size:var(--sinch-comp-button-size-container-m);--sinch-button-set-size-shape-radius:var(--sinch-comp-button-shape-radius-size-m);--sinch-local-font:var(--sinch-comp-button-font-size-m-text);--sinch-global-size-icon:var(--sinch-comp-button-size-icon-m)}:host([data-size="s"])>#button{--sinch-local-size:var(--sinch-comp-button-size-container-s);--sinch-button-set-size-shape-radius:var(--sinch-comp-button-shape-radius-size-s);--sinch-local-font:var(--sinch-comp-button-font-size-s-text);--sinch-global-size-icon:var(--sinch-comp-button-size-icon-s)}:host([data-size=xs])>#button{--sinch-local-size:var(--sinch-comp-button-size-container-xs);--sinch-local-padding:8px;--sinch-button-set-size-shape-radius:var(--sinch-comp-button-shape-radius-size-xs);--sinch-local-font:var(--sinch-comp-button-font-size-s-text);--sinch-global-size-icon:var(--sinch-comp-button-size-icon-xs)}:host(:is([text=""],:not([text])))>#button{--sinch-local-padding:8px}:host([data-size="s"]:is([text=""],:not([text])))>#button{--sinch-local-padding:4px}:host([data-size=xs]:is([text=""],:not([text])))>#button{--sinch-local-padding:4px}:host([type=primary])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-primary-default-background-initial);--sinch-local-color-border:var(--sinch-comp-button-color-primary-default-border-initial);--sinch-local-color-text:var(--sinch-comp-button-color-primary-default-text-initial);--sinch-local-color-icon:var(--sinch-comp-button-color-primary-default-icon-initial);--sinch-local-color-outline-focus:var(--sinch-comp-button-color-primary-default-outline-focus);--sinch-local-shadow:var(--sinch-comp-button-shadow-primary-initial)}:host(:not([type]))>#button,:host([type=secondary])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-secondary-default-background-initial);--sinch-local-color-border:var(--sinch-comp-button-color-secondary-default-border-initial);--sinch-local-color-outline-focus:var(--sinch-comp-button-color-secondary-default-outline-focus);--sinch-local-color-text:var(--sinch-comp-button-color-secondary-default-text-initial);--sinch-local-color-icon:var(--sinch-comp-button-color-secondary-default-icon-initial)}:host([type=subtle-primary])>#button,:host([type=tertiary])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-subtle-primary-default-background-initial);--sinch-local-color-border:var(--sinch-comp-button-color-subtle-primary-default-border-initial);--sinch-local-color-outline-focus:var(--sinch-comp-button-color-subtle-primary-default-outline-focus);--sinch-local-color-text:var(--sinch-comp-button-color-subtle-primary-default-text-initial);--sinch-local-color-icon:var(--sinch-comp-button-color-subtle-primary-default-icon-initial);--sinch-local-shadow:var(--sinch-comp-button-shadow-subtle-initial)}:host(:not([type]):is([text=""],:not([text])))>#button,:host([type=subtle-secondary])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-subtle-secondary-default-background-initial);--sinch-local-color-border:var(--sinch-comp-button-color-subtle-secondary-default-border-initial);--sinch-local-color-outline-focus:var(--sinch-comp-button-color-subtle-secondary-default-outline-focus);--sinch-local-color-text:var(--sinch-comp-button-color-subtle-secondary-default-text-initial);--sinch-local-color-icon:var(--sinch-comp-button-color-subtle-secondary-default-icon-initial);--sinch-local-shadow:var(--sinch-comp-button-shadow-subtle-initial)}:host([type=cta-primary])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-cta-primary-default-background-initial);--sinch-local-color-border:var(--sinch-comp-button-color-cta-primary-default-border-initial);--sinch-local-color-outline-focus:var(--sinch-comp-button-color-cta-primary-default-outline-focus);--sinch-local-color-text:var(--sinch-comp-button-color-cta-primary-default-text-initial);--sinch-local-color-icon:var(--sinch-comp-button-color-cta-primary-default-icon-initial);--sinch-local-shadow:var(--sinch-comp-button-shadow-cta-primary-initial)}:host([type=cta-secondary])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-cta-secondary-default-background-initial);--sinch-local-color-border:var(--sinch-comp-button-color-cta-secondary-default-border-initial);--sinch-local-color-outline-focus:var(--sinch-comp-button-color-cta-secondary-default-outline-focus);--sinch-local-color-text:var(--sinch-comp-button-color-cta-secondary-default-text-initial);--sinch-local-color-icon:var(--sinch-comp-button-color-cta-secondary-default-icon-initial);--sinch-local-shadow:var(--sinch-comp-button-shadow-cta-secondary-initial)}:host([type=destructive])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-danger-default-background-initial);--sinch-local-color-border:var(--sinch-comp-button-color-danger-default-border-initial);--sinch-local-color-text:var(--sinch-comp-button-color-danger-default-text-initial);--sinch-local-shadow:var(--sinch-comp-button-shadow-danger-initial);--sinch-global-color-icon:var(--sinch-comp-button-color-danger-default-icon-initial)}:host([type=primary]:focus-visible)>#button{--sinch-local-shadow:var(--sinch-comp-button-shadow-primary-focus)}:host(:not([type]):focus-visible)>#button,:host([type=secondary]:focus-visible)>#button{--sinch-local-shadow:var(--sinch-comp-button-shadow-secondary-focus)}:host([type=subtle-primary]:focus-visible)>#button,:host([type=tertiary]:focus-visible)>#button{--sinch-local-shadow:var(--sinch-comp-button-shadow-subtle-focus)}:host(:not([type]):is([text=""],:not([text])):focus-visible)>#button,:host([type=subtle-secondary]:focus-visible)>#button{--sinch-local-shadow:var(--sinch-comp-button-shadow-subtle-focus)}:host([type=cta-primary]:focus-visible)>#button{--sinch-local-shadow:var(--sinch-comp-button-shadow-cta-primary-focus)}:host([type=cta-secondary]:focus-visible)>#button{--sinch-local-shadow:var(--sinch-comp-button-shadow-cta-secondary-focus)}:host([type=destructive]:focus-visible)>#button{--sinch-local-shadow:var(--sinch-comp-button-shadow-danger-focus)}:host([toggled]:not([disabled])[type=subtle-primary])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-subtle-primary-toggled-background-initial)}:host([toggled]:not([disabled]):not([type]):is([text=""],:not([text])))>#button,:host([toggled]:not([disabled])[type=subtle-secondary])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-subtle-secondary-toggled-background-initial)}:host([type=primary]:hover)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-primary-default-background-hover);--sinch-local-shadow:var(--sinch-comp-button-shadow-primary-hover)}:host([type=primary]:active)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-primary-default-background-active);--sinch-local-shadow:var(--sinch-comp-button-shadow-primary-active)}:host(:not([type]):hover)>#button,:host([type=secondary]:hover)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-secondary-default-background-hover);--sinch-local-shadow:var(--sinch-comp-button-shadow-secondary-hover)}:host(:not([type]):active)>#button,:host([type=secondary]:active)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-secondary-default-background-active)}:host([type=subtle-primary]:hover)>#button,:host([type=tertiary]:hover)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-subtle-primary-default-background-hover);--sinch-local-shadow:var(--sinch-comp-button-shadow-subtle-hover)}:host([type=subtle-primary]:active)>#button,:host([type=tertiary]:active)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-subtle-primary-default-background-active);--sinch-local-shadow:var(--sinch-comp-button-shadow-subtle-active)}:host(:not([type]):is([text=""],:not([text])):hover)>#button,:host([type=subtle-secondary]:hover)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-subtle-secondary-default-background-hover);--sinch-local-shadow:var(--sinch-comp-button-shadow-subtle-hover)}:host(:not([type]):is([text=""],:not([text])):active)>#button,:host([type=subtle-secondary]:active)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-subtle-secondary-default-background-active);--sinch-local-shadow:var(--sinch-comp-button-shadow-subtle-active)}:host([type=cta-primary]:hover)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-cta-primary-default-background-hover);--sinch-local-shadow:var(--sinch-comp-button-shadow-cta-primary-hover)}:host([type=cta-primary]:active)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-cta-primary-default-background-active);--sinch-local-shadow:var(--sinch-comp-button-shadow-cta-primary-active)}:host([type=cta-secondary]:hover)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-cta-secondary-default-background-hover);--sinch-local-shadow:var(--sinch-comp-button-shadow-cta-secondary-hover)}:host([type=cta-secondary]:active)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-cta-secondary-default-background-active);--sinch-local-shadow:var(--sinch-comp-button-shadow-cta-secondary-active)}:host([type=destructive]:hover)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-danger-default-background-hover);--sinch-local-shadow:var(--sinch-comp-button-shadow-danger-hover)}:host([type=destructive]:active)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-danger-default-background-active);--sinch-local-shadow:var(--sinch-comp-button-shadow-danger-active)}:host([toggled]:not([disabled])[type=subtle-primary]:hover)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-subtle-primary-default-background-hover);--sinch-local-shadow:var(--sinch-comp-button-shadow-subtle-hover)}:host([toggled]:not([disabled])[type=subtle-primary]:active)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-subtle-primary-default-background-active);--sinch-local-shadow:var(--sinch-comp-button-shadow-subtle-active)}:host([toggled]:not([disabled]):not([type]):is([text=""],:not([text])):hover)>#button,:host([toggled]:not([disabled])[type=subtle-secondary]:hover)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-subtle-secondary-default-background-hover);--sinch-local-shadow:var(--sinch-comp-button-shadow-subtle-hover)}:host([toggled]:not([disabled]):not([type]):is([text=""],:not([text])):active)>#button,:host([toggled]:not([disabled])[type=subtle-secondary]:active)>#button{--sinch-local-color-background:var(--sinch-comp-button-color-subtle-secondary-default-background-active);--sinch-local-shadow:var(--sinch-comp-button-shadow-subtle-active)}:host([type=primary][disabled])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-primary-disabled-background-initial);--sinch-local-color-border:var(--sinch-comp-button-color-primary-disabled-border-initial);--sinch-local-color-text:var(--sinch-comp-button-color-primary-disabled-text-initial);--sinch-local-shadow:var(--sinch-comp-button-shadow-primary-disabled);--sinch-global-color-icon:var(--sinch-comp-button-color-primary-disabled-icon-initial)}:host(:not([type])[disabled])>#button,:host([type=secondary][disabled])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-secondary-disabled-background-initial);--sinch-local-color-border:var(--sinch-comp-button-color-secondary-disabled-border-initial);--sinch-local-color-text:var(--sinch-comp-button-color-secondary-disabled-text-initial);--sinch-local-shadow:var(--sinch-comp-button-shadow-secondary-disabled);--sinch-global-color-icon:var(--sinch-comp-button-color-secondary-disabled-icon-initial)}:host([type=subtle-primary][disabled])>#button,:host([type=tertiary][disabled])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-subtle-primary-disabled-background-initial);--sinch-local-color-border:var(--sinch-comp-button-color-subtle-primary-disabled-border-initial);--sinch-local-color-text:var(--sinch-comp-button-color-subtle-primary-disabled-text-initial);--sinch-local-shadow:var(--sinch-comp-button-shadow-subtle-disabled);--sinch-global-color-icon:var(--sinch-comp-button-color-subtle-primary-disabled-icon-initial)}:host(:not([type]):is([text=""],:not([text]))[disabled])>#button,:host([type=subtle-secondary][disabled])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-subtle-secondary-disabled-background-initial);--sinch-local-color-border:var(--sinch-comp-button-color-subtle-secondary-disabled-border-initial);--sinch-local-color-text:var(--sinch-comp-button-color-subtle-secondary-disabled-text-initial);--sinch-local-shadow:var(--sinch-comp-button-shadow-subtle-disabled);--sinch-global-color-icon:var(--sinch-comp-button-color-subtle-secondary-disabled-icon-initial)}:host([type=cta-primary][disabled])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-cta-primary-disabled-background-initial);--sinch-local-color-border:var(--sinch-comp-button-color-cta-primary-disabled-border-initial);--sinch-local-color-text:var(--sinch-comp-button-color-cta-primary-disabled-text-initial);--sinch-local-shadow:var(--sinch-comp-button-shadow-cta-primary-disabled);--sinch-global-color-icon:var(--sinch-comp-button-color-cta-primary-disabled-icon-initial)}:host([type=cta-secondary][disabled])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-cta-secondary-disabled-background-initial);--sinch-local-color-border:var(--sinch-comp-button-color-cta-secondary-disabled-border-initial);--sinch-local-color-text:var(--sinch-comp-button-color-cta-secondary-disabled-text-initial);--sinch-local-shadow:var(--sinch-comp-button-shadow-cta-secondary-disabled);--sinch-global-color-icon:var(--sinch-comp-button-color-cta-secondary-disabled-icon-initial)}:host([type=destructive][disabled])>#button{--sinch-local-color-background:var(--sinch-comp-button-color-danger-disabled-background-initial);--sinch-local-color-border:var(--sinch-comp-button-color-danger-disabled-border-initial);--sinch-local-color-text:var(--sinch-comp-button-color-danger-disabled-text-initial);--sinch-global-color-icon:var(--sinch-comp-button-color-danger-disabled-icon-initial)}#button::before{content:"";position:absolute;inset:0;background-color:var(--sinch-local-color-background);border-top:var(--sinch-button-border-top);border-bottom:var(--sinch-button-border-bottom);border-right:var(--sinch-button-border-right);border-left:var(--sinch-button-border-left);border-color:var(--sinch-local-color-border);border-top-right-radius:var(--sinch-button-shape-radius-top-right,var(--sinch-button-set-size-shape-radius,var(--sinch-button-shape-radius-base)));border-top-left-radius:var(--sinch-button-shape-radius-top-left,var(--sinch-button-set-size-shape-radius,var(--sinch-button-shape-radius-base)));border-bottom-right-radius:var(--sinch-button-shape-radius-bottom-right,var(--sinch-button-set-size-shape-radius,var(--sinch-button-shape-radius-base)));border-bottom-left-radius:var(--sinch-button-shape-radius-bottom-left,var(--sinch-button-set-size-shape-radius,var(--sinch-button-shape-radius-base)));box-shadow:var(--sinch-local-shadow);pointer-events:none}:host(:not([disabled]):active) #button::before{border-width:2px}:host(:focus-visible) #button::after{position:absolute;content:"";inset:-3px;border:2px solid var(--sinch-local-color-outline-focus);border-top-right-radius:calc(var(--sinch-button-shape-radius-top-right,var(--sinch-button-set-size-shape-radius,var(--sinch-button-shape-radius-base))) + 3px);border-top-left-radius:calc(var(--sinch-button-shape-radius-top-left,var(--sinch-button-set-size-shape-radius,var(--sinch-button-shape-radius-base))) + 3px);border-bottom-right-radius:calc(var(--sinch-button-shape-radius-bottom-right,var(--sinch-button-set-size-shape-radius,var(--sinch-button-shape-radius-base))) + 3px);border-bottom-left-radius:calc(var(--sinch-button-shape-radius-bottom-left,var(--sinch-button-set-size-shape-radius,var(--sinch-button-shape-radius-base))) + 3px);pointer-events:none}#content{position:relative;display:flex;align-items:center;justify-content:center;gap:12px;width:100%;height:100%;padding:0 var(--sinch-local-padding);box-sizing:border-box;pointer-events:none;overflow:hidden}#text{font:var(--sinch-local-font);color:var(--sinch-local-color-text);overflow:hidden;white-space:nowrap;text-overflow:ellipsis;flex-shrink:1;min-width:0}:host(:is([text=""],:not([text]))) :is(#left-icon,#right-icon,#text){display:none}::slotted(*){display:block}</style><div id="button" inert><div id="content"><slot id="left-icon" name="left-icon"></slot><slot id="icon" name="icon"></slot><span id="text"></span><slot id="right-icon" name="right-icon"></slot></div></div>';
4
- import { typeValues } from './utils';
5
+ import { typeValues, formTypeValues } from './utils';
5
6
  const template = document.createElement('template');
6
7
  template.innerHTML = templateHTML;
7
8
  defineCustomElement('sinch-button', class extends NectaryElement {
@@ -9,10 +10,13 @@ defineCustomElement('sinch-button', class extends NectaryElement {
9
10
  #$text;
10
11
  #controller = null;
11
12
  #sizeContext;
13
+ #internals;
14
+ static formAssociated = true;
12
15
  constructor() {
13
16
  super();
14
17
  const shadowRoot = this.attachShadow();
15
18
  shadowRoot.appendChild(template.content.cloneNode(true));
19
+ this.#internals = this.attachInternals();
16
20
  this.#$button = shadowRoot.querySelector('#button');
17
21
  this.#$text = shadowRoot.querySelector('#text');
18
22
  this.#sizeContext = new Context(this.#$button, 'size');
@@ -24,6 +28,7 @@ defineCustomElement('sinch-button', class extends NectaryElement {
24
28
  signal
25
29
  } = this.#controller;
26
30
  this.setAttribute('role', 'button');
31
+ this.#internals.role = 'button';
27
32
  this.tabIndex = 0;
28
33
  this.addEventListener('click', this.#onButtonClick, {
29
34
  signal
@@ -71,6 +76,7 @@ defineCustomElement('sinch-button', class extends NectaryElement {
71
76
  updateBooleanAttribute(this, 'disabled', isAttrTrue(newVal));
72
77
  }
73
78
  this.ariaDisabled = isAttrTrue(newVal).toString();
79
+ this.#internals.ariaDisabled = isAttrTrue(newVal).toString();
74
80
  break;
75
81
  }
76
82
  case 'toggled':
@@ -79,6 +85,7 @@ defineCustomElement('sinch-button', class extends NectaryElement {
79
85
  updateBooleanAttribute(this, 'toggled', isAttrTrue(newVal));
80
86
  }
81
87
  this.ariaPressed = isAttrTrue(newVal).toString();
88
+ this.#internals.ariaPressed = isAttrTrue(newVal).toString();
82
89
  break;
83
90
  }
84
91
  case 'size':
@@ -126,6 +133,12 @@ defineCustomElement('sinch-button', class extends NectaryElement {
126
133
  get focusable() {
127
134
  return true;
128
135
  }
136
+ set formType(value) {
137
+ updateLiteralAttribute(this, formTypeValues, 'form-type', value);
138
+ }
139
+ get formType() {
140
+ return getLiteralAttribute(this, formTypeValues, 'form-type', 'button');
141
+ }
129
142
  #onSizeUpdate() {
130
143
  if (!this.isDomConnected) {
131
144
  return;
@@ -164,6 +177,15 @@ defineCustomElement('sinch-button', class extends NectaryElement {
164
177
  e.stopPropagation();
165
178
  e.preventDefault();
166
179
  } else {
180
+ const form = this.#internals.form;
181
+ if (form !== null) {
182
+ if (this.formType === 'submit') {
183
+ requestSubmitForm(form, this);
184
+ }
185
+ if (this.formType === 'reset') {
186
+ form.reset();
187
+ }
188
+ }
167
189
  this.dispatchEvent(new CustomEvent('-click'));
168
190
  }
169
191
  };
package/button/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { NectaryComponentReactByType, NectaryComponentVanillaByType } from '../types';
2
2
  import type { TSinchSizeEx } from '../utils/size';
3
+ export type TSinchButtonFormType = 'submit' | 'reset' | 'button';
3
4
  export type TSinchButtonType = 'primary' | 'secondary'
4
5
  /** @deprecated */
5
6
  | 'tertiary' | 'subtle-primary' | 'subtle-secondary' | 'cta-primary' | 'cta-secondary' | 'destructive';
@@ -16,6 +17,8 @@ export type TSinchButtonProps = {
16
17
  disabled?: boolean;
17
18
  /** Toggled (pressed) */
18
19
  toggled?: boolean;
20
+ /** Makes button participate in forms, `button` by default */
21
+ 'form-type'?: TSinchButtonFormType;
19
22
  };
20
23
  export type TSinchButtonEvents = {
21
24
  /** Click event handler */
package/button/utils.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- import type { TSinchButtonType } from './types';
1
+ import type { TSinchButtonType, TSinchButtonFormType } from './types';
2
2
  export declare const typeValues: readonly TSinchButtonType[];
3
+ export declare const formTypeValues: readonly TSinchButtonFormType[];
package/button/utils.js CHANGED
@@ -1 +1,2 @@
1
- export const typeValues = ['primary', 'secondary', 'subtle-primary', 'subtle-secondary', 'cta-primary', 'cta-secondary', 'destructive'];
1
+ export const typeValues = ['primary', 'secondary', 'subtle-primary', 'subtle-secondary', 'cta-primary', 'cta-secondary', 'destructive'];
2
+ export const formTypeValues = ['submit', 'reset', 'button'];
package/card-v2/index.js CHANGED
@@ -112,7 +112,7 @@ defineCustomElement('sinch-card-v2', class extends NectaryElement {
112
112
  {
113
113
  const bool = isAttrTrue(newVal);
114
114
  const titleElement = this.querySelector('sinch-card-v2-title');
115
- if (titleElement != null) {
115
+ if (titleElement !== null) {
116
116
  updateBooleanAttribute(titleElement, name, bool);
117
117
  }
118
118
  break;
package/checkbox/index.js CHANGED
@@ -1,15 +1,19 @@
1
1
  import '../rich-text';
2
2
  import { defineCustomElement, getAttribute, getBooleanAttribute, getReactEventHandler, isAttrEqual, isAttrTrue, NectaryElement, updateAttribute, updateBooleanAttribute, updateExplicitBooleanAttribute } from '../utils';
3
+ import { setFormValue } from '../utils/form';
3
4
  const templateHTML = '<style>:host{display:inline-block;vertical-align:middle;outline:0}#wrapper{display:flex;flex-direction:row;box-sizing:border-box;width:100%;min-height:24px;--sinch-local-color-background:var(--sinch-comp-checkbox-color-default-background-initial);--sinch-local-color-background-hover:var(--sinch-comp-checkbox-color-default-background-hover);--sinch-local-color-background-active:var(--sinch-comp-checkbox-color-default-background-active);--sinch-local-color-border:var(--sinch-comp-checkbox-color-default-border-initial);--sinch-local-color-border-hover:var(--sinch-comp-checkbox-color-default-border-hover);--sinch-local-color-border-active:var(--sinch-comp-checkbox-color-default-border-active);--sinch-local-color-text:var(--sinch-comp-checkbox-color-default-text-initial)}:host([invalid])>#wrapper{--sinch-local-color-background:var(--sinch-comp-checkbox-color-invalid-background-initial);--sinch-local-color-background-hover:var(--sinch-comp-checkbox-color-invalid-background-hover);--sinch-local-color-background-active:var(--sinch-comp-checkbox-color-invalid-background-active);--sinch-local-color-border:var(--sinch-comp-checkbox-color-invalid-border-initial);--sinch-local-color-border-hover:var(--sinch-comp-checkbox-color-invalid-border-hover);--sinch-local-color-border-active:var(--sinch-comp-checkbox-color-invalid-border-active);--sinch-local-color-text:var(--sinch-comp-checkbox-color-invalid-text-initial)}:host([checked])>#wrapper{--sinch-local-color-background:var(--sinch-comp-checkbox-color-checked-background-initial);--sinch-local-color-background-hover:var(--sinch-comp-checkbox-color-checked-background-hover);--sinch-local-color-background-active:var(--sinch-comp-checkbox-color-checked-background-active);--sinch-local-color-border:var(--sinch-comp-checkbox-color-checked-border-initial);--sinch-local-color-border-hover:var(--sinch-comp-checkbox-color-checked-border-hover);--sinch-local-color-border-active:var(--sinch-comp-checkbox-color-checked-border-active)}:host([disabled])>#wrapper{--sinch-local-color-background:var(--sinch-comp-checkbox-color-disabled-background-initial);--sinch-local-color-border:var(--sinch-comp-checkbox-color-disabled-border-initial);--sinch-local-color-text:var(--sinch-comp-checkbox-color-disabled-text-initial)}:host([disabled][checked])>#wrapper{--sinch-local-color-background:var(--sinch-comp-checkbox-color-checked-disabled-background-initial);--sinch-local-color-border:var(--sinch-comp-checkbox-color-checked-disabled-border-initial)}#checkbox{width:18px;height:18px;cursor:pointer}:host([disabled]) #checkbox{cursor:initial}#icon-container{position:relative;width:18px;height:18px;align-self:flex-start;margin-top:3px}#checkbox::before{content:"";position:absolute;inset:-3px;border:2px solid var(--sinch-comp-checkbox-color-default-outline-focus);border-radius:calc(var(--sinch-comp-checkbox-shape-radius) + 3px);transition:opacity .1s linear;opacity:0;box-sizing:border-box;pointer-events:none}:host(:focus-visible) #checkbox::before{opacity:1}#checkbox::after{content:"";position:absolute;width:18px;height:18px;inset:0;margin:auto;background-color:var(--sinch-local-color-background);border:1px solid var(--sinch-local-color-border);border-radius:var(--sinch-comp-checkbox-shape-radius);transition:background-color .1s linear;box-sizing:border-box;pointer-events:none}:host(:hover:not([disabled])) #checkbox::after{background-color:var(--sinch-local-color-background-hover);border-color:var(--sinch-local-color-border-hover)}:host(:active:not([disabled])) #checkbox::after{background-color:var(--sinch-local-color-background-active);border-color:var(--sinch-local-color-border-active)}#icon-checkmark,#icon-indeterminate{position:absolute;left:1px;top:1px;width:16px;height:16px;transition:opacity .1s linear;opacity:0;pointer-events:none;fill:var(--sinch-sys-color-surface-primary-default)}:host(:not([indeterminate])[checked]) #icon-checkmark{opacity:1}:host([indeterminate][checked]) #icon-indeterminate{opacity:1}@media (prefers-reduced-motion){#checkbox::after,#checkbox::before,#icon-checkmark,#icon-indeterminate{transition:none}}#label{flex:1;align-self:center;padding-left:8px;font:var(--sinch-comp-checkbox-font-label);cursor:pointer;--sinch-global-color-text:var(--sinch-local-color-text)}:host(:not([text])) #label,:host([text=""]) #label{display:none}:host([disabled]) #label{cursor:initial}</style><div id="wrapper"><div id="icon-container"><div id="checkbox"></div><svg id="icon-checkmark" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 16.17 5.53 12.7a.996.996 0 1 0-1.41 1.41l4.18 4.18c.39.39 1.02.39 1.41 0L20.29 7.71a.996.996 0 1 0-1.41-1.41L9 16.17Z"/></svg><svg id="icon-indeterminate" viewBox="0 0 24 24" aria-hidden="true"><path d="M18 13H6c-.55 0-1-.45-1-1s.45-1 1-1h12c.55 0 1 .45 1 1s-.45 1-1 1Z"/></svg></div><sinch-rich-text id="label"></sinch-rich-text></div>';
4
5
  const template = document.createElement('template');
5
6
  template.innerHTML = templateHTML;
6
7
  defineCustomElement('sinch-checkbox', class extends NectaryElement {
7
8
  #$label;
8
9
  #controller = null;
10
+ #internals;
11
+ static formAssociated = true;
9
12
  constructor() {
10
13
  super();
11
14
  const shadowRoot = this.attachShadow();
12
15
  shadowRoot.appendChild(template.content.cloneNode(true));
16
+ this.#internals = this.attachInternals();
13
17
  this.#$label = shadowRoot.querySelector('#label');
14
18
  }
15
19
  connectedCallback() {
@@ -21,6 +25,7 @@ defineCustomElement('sinch-checkbox', class extends NectaryElement {
21
25
  signal
22
26
  };
23
27
  this.setAttribute('role', 'checkbox');
28
+ this.#internals.role = 'checkbox';
24
29
  this.tabIndex = 0;
25
30
  this.addEventListener('click', this.#onClick, options);
26
31
  this.addEventListener('focus', this.#onFocus, options);
@@ -33,6 +38,26 @@ defineCustomElement('sinch-checkbox', class extends NectaryElement {
33
38
  this.#controller.abort();
34
39
  this.#controller = null;
35
40
  }
41
+ formAssociatedCallback() {
42
+ setFormValue(this.#internals, this.#getFormValue());
43
+ }
44
+ formResetCallback() {
45
+ this.checked = false;
46
+ setFormValue(this.#internals, '');
47
+ }
48
+ formStateRestoreCallback(state) {
49
+ if (this.#internals.form === null || getBooleanAttribute(this.#internals.form, 'data-form-state-restore') === false) {
50
+ return;
51
+ }
52
+ if (state !== null) {
53
+ const value = typeof state === 'string' ? state : state.get(this.name);
54
+ this.checked = (value?.toString() ?? '').length > 0;
55
+ setFormValue(this.#internals, this.#getFormValue());
56
+ }
57
+ }
58
+ #getFormValue() {
59
+ return this.checked ? this.value.length > 0 ? this.value : 'on' : '';
60
+ }
36
61
  static get observedAttributes() {
37
62
  return ['checked', 'disabled', 'text', 'invalid', 'indeterminate'];
38
63
  }
@@ -51,12 +76,15 @@ defineCustomElement('sinch-checkbox', class extends NectaryElement {
51
76
  const isChecked = isAttrTrue(newVal);
52
77
  updateExplicitBooleanAttribute(this, 'aria-checked', isChecked);
53
78
  updateBooleanAttribute(this, 'checked', isChecked);
79
+ this.#internals.ariaChecked = isChecked.toString();
80
+ setFormValue(this.#internals, this.#getFormValue());
54
81
  break;
55
82
  }
56
83
  case 'disabled':
57
84
  {
58
85
  const isDisabled = isAttrTrue(newVal);
59
86
  updateExplicitBooleanAttribute(this, 'aria-disabled', isDisabled);
87
+ this.#internals.ariaDisabled = isDisabled.toString();
60
88
  updateBooleanAttribute(this, 'disabled', isDisabled);
61
89
  break;
62
90
  }
@@ -68,6 +96,18 @@ defineCustomElement('sinch-checkbox', class extends NectaryElement {
68
96
  }
69
97
  }
70
98
  }
99
+ set name(value) {
100
+ updateAttribute(this, 'name', value);
101
+ }
102
+ get name() {
103
+ return getAttribute(this, 'name', '');
104
+ }
105
+ set value(value) {
106
+ updateAttribute(this, 'value', value);
107
+ }
108
+ get value() {
109
+ return getAttribute(this, 'value', '');
110
+ }
71
111
  set checked(isChecked) {
72
112
  updateBooleanAttribute(this, 'checked', isChecked);
73
113
  }
@@ -1,5 +1,9 @@
1
1
  import type { NectaryComponentReactByType, NectaryComponentVanillaByType } from '../types';
2
2
  export type TSinchCheckboxProps = {
3
+ /** Identification for uncontrolled form submissions */
4
+ name?: string;
5
+ /** Value for uncontrolled form submissions, default: `on` if checked */
6
+ value?: string;
3
7
  /** Checked */
4
8
  checked?: boolean;
5
9
  /** Indeterminate */
@@ -1,6 +1,7 @@
1
1
  import '../icon';
2
2
  import '../text';
3
3
  import { defineCustomElement, getAttribute, getBooleanAttribute, getReactEventHandler, getRect, getTargetAttribute, isAttrTrue, NectaryElement, packCsv, setClass, unpackCsv, updateAttribute, updateBooleanAttribute } from '../utils';
4
+ import { setFormValue } from '../utils/form';
4
5
  const templateHTML = '<style>:host{display:block;outline:0}#content{width:fit-content;box-sizing:border-box;padding:16px;display:flex;flex-direction:column;gap:8px}#month{display:flex;flex-direction:column;row-gap:8px}.week{display:flex;flex-direction:row;column-gap:8px}.week.empty{display:none}.day{all:initial;font:var(--sinch-comp-date-picker-font-day);width:24px;height:24px;line-height:22px;color:var(--sinch-comp-date-picker-day-color-default-text-initial);background-color:var(--sinch-comp-date-picker-day-color-default-background-initial);border:1px solid var(--sinch-comp-date-picker-day-color-default-border-initial);border-radius:var(--sinch-comp-date-picker-day-shape-radius);text-align:center;box-sizing:border-box;user-select:none;cursor:pointer}.day:focus-visible{outline:1px solid var(--sinch-comp-date-picker-day-color-default-outline-focus);outline-offset:1px}.day:disabled{cursor:initial;color:var(--sinch-comp-date-picker-day-color-disabled-text-initial)}.day:enabled:hover{background-color:var(--sinch-comp-date-picker-day-color-default-background-hover)}.day:enabled.range{background-color:var(--sinch-comp-date-picker-day-color-default-range-background)}.day:enabled.selected{color:var(--sinch-comp-date-picker-day-color-checked-text-initial);background-color:var(--sinch-comp-date-picker-day-color-checked-background-initial);border-color:var(--sinch-comp-date-picker-day-color-checked-border-initial)}.day.today{font:var(--sinch-comp-date-picker-font-today);color:var(--sinch-comp-date-picker-today-color-default-text-initial);background-color:var(--sinch-comp-date-picker-today-color-default-background-initial);border-color:var(--sinch-comp-date-picker-today-color-default-border-initial)}.day.today:hover{background-color:var(--sinch-comp-date-picker-today-color-default-background-hover)}.day.today:disabled{color:var(--sinch-comp-date-picker-today-color-disabled-text-initial);border-color:var(--sinch-comp-date-picker-today-color-disabled-border-initial)}.day.today.selected{color:var(--sinch-comp-date-picker-today-color-checked-text-initial);background-color:var(--sinch-comp-date-picker-today-color-checked-background-initial);border-color:var(--sinch-comp-date-picker-today-color-checked-border-initial)}#week-day-names{display:flex;flex-direction:row;gap:8px;height:24px}.week-day-name{font:var(--sinch-comp-date-picker-font-weekday);color:var(--sinch-comp-date-picker-weekday-color-default-text-initial);text-align:center;width:24px;height:24px;line-height:24px;user-select:none;text-transform:uppercase}#content-header{display:flex;flex-direction:row;height:32px;align-items:center}#date{flex:1;text-align:center;text-transform:capitalize;--sinch-com-text-font:var(--sinch-comp-date-picker-font-header);--sinch-global-color-text:var(--sinch-comp-date-picker-header-color-default-text-initial)}#prev-year{margin-left:-4px}#next-year{margin-right:-4px}</style><div id="content"><div id="content-header"><sinch-button id="prev-year" size="s"><sinch-icon icons-version="2" name="fa-angles-left" id="icon-prev-year" slot="icon"></sinch-icon></sinch-button><sinch-button id="prev-month" size="s"><sinch-icon icons-version="2" name="fa-angle-left" id="icon-prev-month" slot="icon"></sinch-icon></sinch-button><sinch-text id="date" type="m" emphasized aria-live="polite"></sinch-text><sinch-button id="next-month" size="s"><sinch-icon icons-version="2" name="fa-angle-right" id="icon-next-month" slot="icon"></sinch-icon></sinch-button><sinch-button id="next-year" size="s"><sinch-icon icons-version="2" name="fa-angles-right" id="icon-next-year" slot="icon"></sinch-icon></sinch-button></div><div id="week-day-names"><div class="week-day-name"></div><div class="week-day-name"></div><div class="week-day-name"></div><div class="week-day-name"></div><div class="week-day-name"></div><div class="week-day-name"></div><div class="week-day-name"></div></div><div id="month"><div class="week"><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button></div><div class="week"><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button></div><div class="week"><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button></div><div class="week"><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button></div><div class="week"><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button></div><div class="week"><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button><button class="day"></button></div></div></div>';
5
6
  import { areDatesEqual, canGoNextMonth, canGoNextYear, canGoPrevMonth, canGoPrevYear, clampMaxDate, clampMinDate, cloneDate, dateToIso, decMonth, decYear, getCalendarMonth, getDayNames, getMonthNames, incMonth, incYear, isDateBetween, isDateOnScreen, isoToDate, isValidDate, sortDates, today } from './utils';
6
7
  const template = document.createElement('template');
@@ -23,10 +24,15 @@ defineCustomElement('sinch-date-picker', class extends NectaryElement {
23
24
  #monthNames;
24
25
  #controller = null;
25
26
  #isHoverSubscribed = false;
27
+ #internals;
28
+ static formAssociated = true;
26
29
  constructor() {
27
30
  super();
28
- const shadowRoot = this.attachShadow();
31
+ const shadowRoot = this.attachShadow({
32
+ delegatesFocus: true
33
+ });
29
34
  shadowRoot.appendChild(template.content.cloneNode(true));
35
+ this.#internals = this.attachInternals();
30
36
  this.#$prevMonth = shadowRoot.querySelector('#prev-month');
31
37
  this.#$nextMonth = shadowRoot.querySelector('#next-month');
32
38
  this.#$prevYear = shadowRoot.querySelector('#prev-year');
@@ -62,6 +68,23 @@ defineCustomElement('sinch-date-picker', class extends NectaryElement {
62
68
  this.#controller.abort();
63
69
  this.#controller = null;
64
70
  }
71
+ formAssociatedCallback() {
72
+ setFormValue(this.#internals, this.value);
73
+ }
74
+ formResetCallback() {
75
+ this.value = '';
76
+ setFormValue(this.#internals, '');
77
+ }
78
+ formStateRestoreCallback(state) {
79
+ if (this.#internals.form === null || getBooleanAttribute(this.#internals.form, 'data-form-state-restore') === false) {
80
+ return;
81
+ }
82
+ if (state !== null) {
83
+ const value = typeof state === 'string' ? state : state.get(this.name);
84
+ this.value = value?.toString() ?? '';
85
+ setFormValue(this.#internals, value?.toString() ?? '');
86
+ }
87
+ }
65
88
  static get observedAttributes() {
66
89
  return ['value', 'min', 'max', 'locale', 'range', 'prev-year-aria-label', 'next-year-aria-label', 'prev-month-aria-label', 'next-month-aria-label'];
67
90
  }
@@ -132,6 +155,12 @@ defineCustomElement('sinch-date-picker', class extends NectaryElement {
132
155
  }
133
156
  }
134
157
  }
158
+ set name(value) {
159
+ updateAttribute(this, 'name', value);
160
+ }
161
+ get name() {
162
+ return getAttribute(this, 'name', '');
163
+ }
135
164
  set locale(value) {
136
165
  updateAttribute(this, 'locale', value);
137
166
  }
@@ -273,6 +302,7 @@ defineCustomElement('sinch-date-picker', class extends NectaryElement {
273
302
  };
274
303
  #onValueChange() {
275
304
  const value = this.value;
305
+ setFormValue(this.#internals, this.value);
276
306
  this.#date1 = null;
277
307
  this.#date2 = null;
278
308
  if (this.range) {
@@ -1,5 +1,7 @@
1
1
  import type { NectaryComponentReactByType, NectaryComponentVanillaByType, TRect } from '../types';
2
2
  export type TSinchDatePickerProps = {
3
+ /** Identification for uncontrolled form submissions */
4
+ name?: string;
3
5
  /** Date value in ISO 8601 format */
4
6
  value: string;
5
7
  /** Date min limit in ISO 8601 format */
package/input/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Context, defineCustomElement, getAttribute, getBooleanAttribute, getLiteralAttribute, getReactEventHandler, isAttrEqual, isAttrTrue, isElementFocused, NectaryElement, setClass, subscribeContext, updateAttribute, updateBooleanAttribute, updateLiteralAttribute } from '../utils';
2
+ import { requestSubmitForm, setFormValue } from '../utils/form';
2
3
  import { DEFAULT_SIZE, sizeValues } from '../utils/size';
3
4
  const templateHTML = '<style>:host{all:initial;display:inline-block;vertical-align:middle}#wrapper{position:relative;display:flex;flex-direction:row;align-items:center;box-sizing:border-box;border-radius:var(--sinch-local-shape-radius);width:100%;height:var(--sinch-local-size);background-color:var(--sinch-comp-input-color-default-background-initial);--sinch-local-size:var(--sinch-comp-input-size-container-m);--sinch-global-size-icon:var(--sinch-comp-input-size-icon-m);--sinch-local-shape-radius:var(--sinch-comp-input-shape-radius-size-m)}:host([data-size="l"])>#wrapper{--sinch-local-size:var(--sinch-comp-input-size-container-l);--sinch-global-size-icon:var(--sinch-comp-input-size-icon-l);--sinch-local-shape-radius:var(--sinch-comp-input-shape-radius-size-l)}:host([data-size="m"])>#wrapper{--sinch-local-size:var(--sinch-comp-input-size-container-m);--sinch-global-size-icon:var(--sinch-comp-input-size-icon-m);--sinch-local-shape-radius:var(--sinch-comp-input-shape-radius-size-m)}:host([data-size="s"])>#wrapper{--sinch-local-size:var(--sinch-comp-input-size-container-s);--sinch-global-size-icon:var(--sinch-comp-input-size-icon-s);--sinch-local-shape-radius:var(--sinch-comp-input-shape-radius-size-s)}#input-wrapper{position:relative;flex:1;flex-basis:0;min-width:0;align-self:stretch}#input{all:initial;width:100%;height:100%;padding:0 12px;box-sizing:border-box;font:var(--sinch-comp-input-font-input);color:var(--sinch-comp-input-color-default-text-initial)}#input::placeholder{font:var(--sinch-comp-input-font-placeholder)!important;color:var(--sinch-comp-input-color-default-text-placeholder);opacity:1}#input:disabled{color:var(--sinch-comp-input-color-disabled-text-initial);-webkit-text-fill-color:var(--sinch-comp-input-color-disabled-text-initial)}#input-mask{display:none;position:absolute;inset:0;padding:0 12px;pointer-events:none;color:var(--sinch-comp-input-color-default-text-placeholder);white-space:pre;height:fit-content;margin:auto 0;overflow:hidden}#border{position:absolute;border:1px solid var(--sinch-comp-input-color-default-border-initial);border-radius:var(--sinch-local-shape-radius);inset:0;pointer-events:none}:host([disabled]) #border{border-color:var(--sinch-comp-input-color-disabled-border-initial)}#input-wrapper:focus-within+#border{border-color:var(--sinch-comp-input-color-default-border-focus);border-width:2px}#input-mask,:host([mask]) #input{font:var(--sinch-sys-font-body-monospace-m)}:host([mask]) #input-mask{display:block}:host([invalid]:not([disabled])) #input-wrapper:not(:focus-within)+#border{border-color:var(--sinch-comp-input-color-invalid-border-initial)}#input[type=password]:not(:placeholder-shown){font-size:1.5em;letter-spacing:.1em}#icon-wrapper{position:relative;height:100%}#icon{position:absolute;display:flex;align-items:center;left:12px;top:0;bottom:0;pointer-events:none;--sinch-global-color-icon:var(--sinch-comp-input-color-default-icon-initial)}:host([disabled]) #icon{--sinch-global-color-icon:var(--sinch-comp-input-color-disabled-icon-initial)}#icon-wrapper.empty{display:none}#icon-wrapper.empty~#input-wrapper>#input,#icon-wrapper.empty~#input-wrapper>#input-mask{padding-left:12px}#icon-wrapper:not(.empty)~#input-wrapper>#input,#icon-wrapper:not(.empty)~#input-wrapper>#input-mask{padding-left:calc(var(--sinch-global-size-icon) + 20px)}#right{display:flex;flex-direction:row;align-self:stretch;align-items:center;gap:4px;padding-right:4px}#right.empty{display:none}#left{display:flex;flex-direction:row;align-self:stretch;align-items:center;gap:4px;padding-left:4px}#left.empty{display:none}</style><div id="wrapper"><div id="left"><slot name="left"></slot></div><div id="icon-wrapper"><div id="icon"><slot name="icon"></slot></div></div><div id="input-wrapper"><div id="input-mask"></div><input id="input" type="text"/></div><div id="border"></div><div id="right"><slot name="right"></slot></div></div>';
4
5
  import { deleteContentBackward, deleteContentForward, getMaskSymbols, inputTypes, insertText, beginMaskedComposition, endMaskedComposition, splitValueAndMask, getMergedValueSliced, insertFromPaste } from './utils';
@@ -22,12 +23,15 @@ defineCustomElement('sinch-input', class extends NectaryElement {
22
23
  #controller = null;
23
24
  #sizeContext;
24
25
  #maskSymbols = null;
26
+ #internals;
27
+ static formAssociated = true;
25
28
  constructor() {
26
29
  super();
27
30
  const shadowRoot = this.attachShadow({
28
31
  delegatesFocus: true
29
32
  });
30
33
  shadowRoot.appendChild(template.content.cloneNode(true));
34
+ this.#internals = this.attachInternals();
31
35
  this.#$input = shadowRoot.querySelector('#input');
32
36
  this.#$inputMask = shadowRoot.querySelector('#input-mask');
33
37
  this.#$iconSlot = shadowRoot.querySelector('slot[name="icon"]');
@@ -43,12 +47,14 @@ defineCustomElement('sinch-input', class extends NectaryElement {
43
47
  connectedCallback() {
44
48
  super.connectedCallback();
45
49
  this.setAttribute('role', 'textbox');
50
+ this.#internals.role = 'textbox';
46
51
  if (this.#controller === null) {
47
52
  this.#controller = new AbortController();
48
53
  }
49
54
  const options = {
50
55
  signal: this.#controller.signal
51
56
  };
57
+ this.#$input.addEventListener('keydown', this.#onKeyDown, options);
52
58
  this.#$input.addEventListener('input', this.#onInput, options);
53
59
  this.#$input.addEventListener('cut', this.#onCut, options);
54
60
  this.#$input.addEventListener('copy', this.#onCopy, options);
@@ -80,11 +86,53 @@ defineCustomElement('sinch-input', class extends NectaryElement {
80
86
  this.#controller.abort();
81
87
  this.#controller = null;
82
88
  }
89
+ formAssociatedCallback() {
90
+ setFormValue(this.#internals, this.#$input.value);
91
+ }
92
+ formResetCallback() {
93
+ this.#$input.value = '';
94
+ setFormValue(this.#internals, '');
95
+ }
96
+ formStateRestoreCallback(state) {
97
+ if (this.#internals.form === null || getBooleanAttribute(this.#internals.form, 'data-form-state-restore') === false) {
98
+ return;
99
+ }
100
+ if (state !== null) {
101
+ const value = typeof state === 'string' ? state : state.get(this.name);
102
+ this.#$input.value = value?.toString() ?? '';
103
+ setFormValue(this.#internals, value?.toString() ?? '');
104
+ }
105
+ }
106
+ #onKeyDown = e => {
107
+ const form = this.#internals.form;
108
+ if (form === null) {
109
+ return;
110
+ }
111
+ if (form.disabled === true) {
112
+ return;
113
+ }
114
+ if (e.key === 'Enter') {
115
+ const submitSelectors = ['sinch-button[form-type="submit"]'];
116
+ const formSubmitters = Array.from(form.querySelectorAll(submitSelectors.join(',')));
117
+ const formSubmitter = formSubmitters.find(submitter => !submitter.disabled) ?? null;
118
+ if (formSubmitter !== null) {
119
+ requestSubmitForm(form, formSubmitter);
120
+ }
121
+ }
122
+ };
83
123
  static get observedAttributes() {
84
- return ['type', 'value', 'placeholder', 'mask', 'invalid', 'disabled', 'size', 'autocomplete', 'autofocus', 'data-size', 'aria-label'];
124
+ return ['name', 'type', 'value', 'placeholder', 'mask', 'invalid', 'disabled', 'size', 'autocomplete', 'autofocus', 'data-size', 'aria-label'];
85
125
  }
86
126
  attributeChangedCallback(name, oldVal, newVal) {
87
127
  switch (name) {
128
+ case 'name':
129
+ {
130
+ if (isAttrEqual(oldVal, newVal)) {
131
+ return;
132
+ }
133
+ updateAttribute(this.#$input, 'name', newVal);
134
+ break;
135
+ }
88
136
  case 'type':
89
137
  {
90
138
  updateLiteralAttribute(this.#$input, inputTypes, 'type', newVal);
@@ -107,6 +155,7 @@ defineCustomElement('sinch-input', class extends NectaryElement {
107
155
  placeholder
108
156
  } = splitValueAndMask(nextVal, this.#maskSymbols);
109
157
  this.#$input.value = value;
158
+ setFormValue(this.#internals, value);
110
159
  this.#$inputMask.textContent = placeholder;
111
160
  if (isElementFocused(this.#$input)) {
112
161
  this.#setSelectionRange(this.#selectionEnd, this.#selectionEnd);
@@ -115,6 +164,7 @@ defineCustomElement('sinch-input', class extends NectaryElement {
115
164
  }
116
165
  if (nextVal !== prevVal) {
117
166
  this.#$input.value = nextVal;
167
+ setFormValue(this.#internals, nextVal);
118
168
  if (isElementFocused(this.#$input)) {
119
169
  this.#setSelectionRange(this.#selectionEnd, this.#selectionEnd);
120
170
  }
@@ -139,6 +189,7 @@ defineCustomElement('sinch-input', class extends NectaryElement {
139
189
  const isInvalid = isAttrTrue(newVal);
140
190
  this.ariaInvalid = isInvalid.toString();
141
191
  this.#$input.ariaInvalid = this.ariaInvalid;
192
+ this.#internals.ariaInvalid = this.ariaInvalid;
142
193
  updateBooleanAttribute(this, name, isInvalid);
143
194
  break;
144
195
  }
@@ -180,10 +231,17 @@ defineCustomElement('sinch-input', class extends NectaryElement {
180
231
  case 'aria-label':
181
232
  {
182
233
  this.#$input.ariaLabel = newVal;
234
+ this.#internals.ariaLabel = newVal;
183
235
  break;
184
236
  }
185
237
  }
186
238
  }
239
+ set name(value) {
240
+ updateAttribute(this, 'name', value);
241
+ }
242
+ get name() {
243
+ return getAttribute(this, 'name', '');
244
+ }
187
245
  set type(value) {
188
246
  updateAttribute(this, 'type', value);
189
247
  }
@@ -411,11 +469,14 @@ defineCustomElement('sinch-input', class extends NectaryElement {
411
469
  }
412
470
  const nextValue = this.#$input.value;
413
471
  const prevValue = this.value;
472
+ setFormValue(this.#internals, nextValue);
414
473
  if (prevValue !== nextValue) {
415
474
  const nextSelectionStart = this.#$input.selectionStart;
416
475
  const nextSelectionEnd = this.#$input.selectionEnd;
417
- this.#$input.value = prevValue;
418
- this.#setSelectionRange(this.#selectionStart, this.#selectionEnd);
476
+ if (this.hasAttribute('value')) {
477
+ this.#$input.value = prevValue;
478
+ this.#setSelectionRange(this.#selectionStart, this.#selectionEnd);
479
+ }
419
480
  this.#selectionStart = nextSelectionStart;
420
481
  this.#selectionEnd = nextSelectionEnd;
421
482
  this.#dispatchChangeEvent(nextValue);
@@ -595,10 +656,12 @@ defineCustomElement('sinch-input', class extends NectaryElement {
595
656
  if (this.#maskSymbols === null) {
596
657
  const value = this.placeholder;
597
658
  this.#$input.placeholder = value ?? '';
659
+ this.#internals.ariaPlaceholder = value ?? '';
598
660
  updateAttribute(this, 'aria-placeholder', value);
599
661
  } else {
600
662
  updateAttribute(this, 'aria-placeholder', null);
601
663
  this.#$input.placeholder = '';
664
+ this.#internals.ariaPlaceholder = '';
602
665
  }
603
666
  }
604
667
  #onIconSlotChange = () => {
package/input/types.d.ts CHANGED
@@ -6,8 +6,10 @@ export type TSinchInputClipboardEvent = CustomEvent<{
6
6
  replaceWith: (value: string) => void;
7
7
  }>;
8
8
  export type TSinchInputProps = {
9
+ /** Identification for uncontrolled form submissions */
10
+ name?: string;
9
11
  /** Controlled value, doesn't change on its own and requres an onChange-value state loop */
10
- value: string;
12
+ value?: string;
11
13
  /** Mask */
12
14
  mask?: string | null;
13
15
  /** Label that is used for a11y – might be different from `label` */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nectary/components",
3
- "version": "4.9.0",
3
+ "version": "4.10.0",
4
4
  "files": [
5
5
  "**/*/*.css",
6
6
  "**/*/*.json",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@babel/runtime": "^7.22.15",
23
- "@nectary/assets": "2.3.0"
23
+ "@nectary/assets": "2.3.1"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@babel/cli": "^7.22.15",
@@ -34,6 +34,6 @@
34
34
  "typescript": "^5.2.2"
35
35
  },
36
36
  "peerDependencies": {
37
- "@nectary/theme-base": "1.4.1"
37
+ "@nectary/theme-base": "1.4.3"
38
38
  }
39
39
  }
package/radio/index.js CHANGED
@@ -1,14 +1,20 @@
1
1
  import { defineCustomElement, getAttribute, getBooleanAttribute, getReactEventHandler, getTargetByAttribute, isAttrTrue, NectaryElement, updateAttribute, updateBooleanAttribute } from '../utils';
2
+ import { setFormValue } from '../utils/form';
2
3
  const templateHTML = '<style>:host{display:block}#wrapper{display:flex;flex-direction:var(--sinch-comp-radio-direction,column);gap:var(--sinch-comp-radio-gap,8px);box-sizing:border-box;width:100%}</style><div id="wrapper"><slot></slot></div>';
3
4
  const template = document.createElement('template');
4
5
  template.innerHTML = templateHTML;
5
6
  defineCustomElement('sinch-radio', class extends NectaryElement {
6
7
  #$slot;
7
8
  #controller = null;
9
+ #internals;
10
+ static formAssociated = true;
8
11
  constructor() {
9
12
  super();
10
- const shadowRoot = this.attachShadow();
13
+ const shadowRoot = this.attachShadow({
14
+ delegatesFocus: true
15
+ });
11
16
  shadowRoot.appendChild(template.content.cloneNode(true));
17
+ this.#internals = this.attachInternals();
12
18
  this.#$slot = shadowRoot.querySelector('slot');
13
19
  }
14
20
  connectedCallback() {
@@ -20,6 +26,7 @@ defineCustomElement('sinch-radio', class extends NectaryElement {
20
26
  signal
21
27
  };
22
28
  this.setAttribute('role', 'radiogroup');
29
+ this.#internals.role = 'radiogroup';
23
30
  this.#$slot.addEventListener('slotchange', this.#onSlotChange, options);
24
31
  this.#$slot.addEventListener('keydown', this.#onOptionKeyDown, options);
25
32
  this.#$slot.addEventListener('click', this.#onOptionClick, options);
@@ -29,9 +36,32 @@ defineCustomElement('sinch-radio', class extends NectaryElement {
29
36
  this.#controller.abort();
30
37
  this.#controller = null;
31
38
  }
39
+ formAssociatedCallback() {
40
+ setFormValue(this.#internals, this.value);
41
+ }
42
+ formResetCallback() {
43
+ this.value = '';
44
+ setFormValue(this.#internals, '');
45
+ }
46
+ formStateRestoreCallback(state) {
47
+ if (this.#internals.form === null || getBooleanAttribute(this.#internals.form, 'data-form-state-restore') === false) {
48
+ return;
49
+ }
50
+ if (state !== null) {
51
+ const value = typeof state === 'string' ? state : state.get(this.name);
52
+ this.value = value?.toString() ?? '';
53
+ setFormValue(this.#internals, value?.toString() ?? '');
54
+ }
55
+ }
32
56
  static get observedAttributes() {
33
57
  return ['value', 'invalid'];
34
58
  }
59
+ set name(value) {
60
+ updateAttribute(this, 'name', value);
61
+ }
62
+ get name() {
63
+ return getAttribute(this, 'name', '');
64
+ }
35
65
  set value(value) {
36
66
  updateAttribute(this, 'value', value);
37
67
  }
@@ -49,6 +79,7 @@ defineCustomElement('sinch-radio', class extends NectaryElement {
49
79
  case 'value':
50
80
  {
51
81
  this.#onValueChange(newVal ?? '');
82
+ setFormValue(this.#internals, newVal ?? '');
52
83
  break;
53
84
  }
54
85
  case 'invalid':
package/radio/types.d.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  import type { NectaryComponentReactByType, NectaryComponentVanillaByType } from '../types';
2
2
  export type TSinchRadioProps = {
3
- value: string;
3
+ /** Identification for uncontrolled form submissions */
4
+ name?: string;
5
+ /** Value */
6
+ value?: string;
7
+ /** Invalid state */
4
8
  invalid?: boolean;
9
+ /** Label that is used for a11y – might be different from `label` */
5
10
  'aria-label': string;
6
11
  };
7
12
  export type TSinchRadioEvents = {
@@ -3,7 +3,8 @@ import '../icon';
3
3
  import '../text';
4
4
  import { isSelectMenuOption } from '../select-menu-option/utils';
5
5
  import { attrValueToPixels, defineCustomElement, getAttribute, getBooleanAttribute, unpackCsv, getFirstCsvValue, getIntegerAttribute, getReactEventHandler, isAttrTrue, NectaryElement, updateAttribute, updateBooleanAttribute, updateCsv, updateExplicitBooleanAttribute, updateIntegerAttribute, debounceTimeout, setClass, subscribeContext, hasClass, isTargetEqual } from '../utils';
6
- const templateHTML = '<style>:host{display:block;outline:0}#listbox{overflow-y:auto}#search{display:none;margin:10px}#search.active{display:block}#search-clear:not(.active){display:none}#not-found{display:flex;align-items:center;justify-content:center;width:100%;height:30px;margin-bottom:10px;pointer-events:none;user-select:none;--sinch-comp-text-font:var(--sinch-comp-select-menu-font-not-found-text);--sinch-global-color-text:var(--sinch-comp-select-menu-color-default-not-found-text-initial)}#not-found:not(.active){display:none}::slotted(.hidden){display:none}::slotted(sinch-title){padding:8px 16px;--sinch-global-color-text:var(--sinch-comp-select-menu-color-default-title-initial)}</style><sinch-input id="search" size="s" placeholder="Search"><sinch-icon icons-version="2" name="magnifying-glass" id="icon-search" slot="icon"></sinch-icon><sinch-button id="search-clear" slot="right"><sinch-icon icons-version="2" name="fa-xmark" slot="icon"></sinch-icon></sinch-button></sinch-input><div id="not-found"><sinch-text type="m">No results</sinch-text></div><div id="listbox" role="presentation"><slot></slot></div>';
6
+ import { CSVToFormData, setFormValue } from '../utils/form';
7
+ const templateHTML = '<style>:host{display:block;outline:0}#listbox{overflow-y:auto;max-height:var(--sinch-comp-select-menu-font-max-height)}#search{display:none;margin:10px}#search.active{display:block}#search-clear:not(.active){display:none}#not-found{display:flex;align-items:center;justify-content:center;width:100%;height:30px;margin-bottom:10px;pointer-events:none;user-select:none;--sinch-comp-text-font:var(--sinch-comp-select-menu-font-not-found-text);--sinch-global-color-text:var(--sinch-comp-select-menu-color-default-not-found-text-initial)}#not-found:not(.active){display:none}::slotted(.hidden){display:none}::slotted(sinch-title){padding:8px 16px;--sinch-global-color-text:var(--sinch-comp-select-menu-color-default-title-initial)}</style><sinch-input id="search" size="s" placeholder="Search"><sinch-icon icons-version="2" name="magnifying-glass" id="icon-search" slot="icon"></sinch-icon><sinch-button id="search-clear" slot="right"><sinch-icon icons-version="2" name="fa-xmark" slot="icon"></sinch-icon></sinch-button></sinch-input><div id="not-found"><sinch-text type="m">No results</sinch-text></div><div id="listbox" role="presentation"><slot></slot></div>';
7
8
  const ITEM_HEIGHT = 40;
8
9
  const NUM_ITEMS_SEARCH = 7;
9
10
  const template = document.createElement('template');
@@ -16,11 +17,14 @@ defineCustomElement('sinch-select-menu', class extends NectaryElement {
16
17
  #$notFound;
17
18
  #controller = null;
18
19
  #searchDebounce;
20
+ #internals;
19
21
  #userManagedSearch = false;
22
+ static formAssociated = true;
20
23
  constructor() {
21
24
  super();
22
25
  const shadowRoot = this.attachShadow();
23
26
  shadowRoot.appendChild(template.content.cloneNode(true));
27
+ this.#internals = this.attachInternals();
24
28
  this.#$optionSlot = shadowRoot.querySelector('slot');
25
29
  this.#$listbox = shadowRoot.querySelector('#listbox');
26
30
  this.#$search = shadowRoot.querySelector('#search');
@@ -34,6 +38,7 @@ defineCustomElement('sinch-select-menu', class extends NectaryElement {
34
38
  signal: this.#controller.signal
35
39
  };
36
40
  this.setAttribute('role', 'listbox');
41
+ this.#internals.role = 'listbox';
37
42
  this.tabIndex = 0;
38
43
  this.addEventListener('keydown', this.#onListboxKeyDown, options);
39
44
  this.addEventListener('focus', this.#onFocus, options);
@@ -53,6 +58,23 @@ defineCustomElement('sinch-select-menu', class extends NectaryElement {
53
58
  this.#controller.abort();
54
59
  this.#controller = null;
55
60
  }
61
+ formAssociatedCallback() {
62
+ setFormValue(this.#internals, CSVToFormData(this.name, this.value));
63
+ }
64
+ formResetCallback() {
65
+ this.value = '';
66
+ setFormValue(this.#internals, '');
67
+ }
68
+ formStateRestoreCallback(state) {
69
+ if (this.#internals.form === null || getBooleanAttribute(this.#internals.form, 'data-form-state-restore') === false) {
70
+ return;
71
+ }
72
+ if (state !== null) {
73
+ const value = typeof state === 'string' ? state : state.get(this.name);
74
+ this.value = value?.toString() ?? '';
75
+ setFormValue(this.#internals, CSVToFormData(this.name, value?.toString() ?? ''));
76
+ }
77
+ }
56
78
  static get observedAttributes() {
57
79
  return ['value', 'rows', 'multiple', 'search-value', 'search-placeholder', 'search-autocomplete'];
58
80
  }
@@ -62,6 +84,7 @@ defineCustomElement('sinch-select-menu', class extends NectaryElement {
62
84
  {
63
85
  this.#onValueChange(this.value);
64
86
  updateExplicitBooleanAttribute(this, 'aria-multiselectable', isAttrTrue(newVal));
87
+ this.#internals.ariaMultiSelectable = isAttrTrue(newVal).toString();
65
88
  break;
66
89
  }
67
90
  case 'search-autocomplete':
@@ -99,6 +122,12 @@ defineCustomElement('sinch-select-menu', class extends NectaryElement {
99
122
  }
100
123
  }
101
124
  }
125
+ set name(value) {
126
+ updateAttribute(this, 'name', value);
127
+ }
128
+ get name() {
129
+ return getAttribute(this, 'name', '');
130
+ }
102
131
  set value(value) {
103
132
  updateAttribute(this, 'value', value);
104
133
  }
@@ -265,12 +294,18 @@ defineCustomElement('sinch-select-menu', class extends NectaryElement {
265
294
  const isChecked = !getBooleanAttribute($option, 'disabled') && values.includes(getAttribute($option, 'value', ''));
266
295
  updateBooleanAttribute($option, 'data-checked', isChecked);
267
296
  }
297
+ const formData = new FormData();
298
+ values.forEach(value => {
299
+ formData.append(this.name, value);
300
+ });
301
+ setFormValue(this.#internals, formData);
268
302
  } else {
269
303
  const value = getFirstCsvValue(csv);
270
304
  for (const $option of this.#getOptionElements()) {
271
305
  const isChecked = !getBooleanAttribute($option, 'disabled') && value === getAttribute($option, 'value', '');
272
306
  updateBooleanAttribute($option, 'data-checked', isChecked);
273
307
  }
308
+ setFormValue(this.#internals, value ?? '');
274
309
  }
275
310
  }
276
311
  #getFirstOption() {
@@ -1,5 +1,7 @@
1
1
  import type { NectaryComponentReactByType, NectaryComponentVanillaByType } from '../types';
2
2
  export type TSinchSelectMenuProps = {
3
+ /** Identification for uncontrolled form submissions */
4
+ name?: string;
3
5
  /** Selected value, CSV when multiple */
4
6
  value: string;
5
7
  /** How many rows to show and scroll the rest */
@@ -25,6 +27,7 @@ export type TSinchSelectMenuStyle = {
25
27
  '--sinch-comp-select-menu-color-default-title-initial'?: string;
26
28
  '--sinch-comp-select-menu-color-default-not-found-text-initial'?: string;
27
29
  '--sinch-comp-select-menu-font-not-found-text'?: string;
30
+ '--sinch-comp-select-menu-font-max-height'?: string;
28
31
  };
29
32
  export type TSinchSelectMenu = {
30
33
  props: TSinchSelectMenuProps;
package/textarea/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Context, defineCustomElement, getAttribute, getBooleanAttribute, getIntegerAttribute, getReactEventHandler, getRect, hasClass, isAttrEqual, isAttrTrue, NectaryElement, setClass, updateAttribute, updateBooleanAttribute } from '../utils';
2
+ import { setFormValue } from '../utils/form';
2
3
  import { DEFAULT_SIZE } from '../utils/size';
3
4
  const templateHTML = '<style>:host{display:block}#wrapper{display:flex;flex-direction:column;position:relative;width:100%;box-sizing:border-box;background-color:var(--sinch-comp-textarea-color-default-background-initial);border-radius:var(--sinch-local-shape-radius);padding-right:2px;overflow:hidden;--sinch-local-shape-radius:var(--sinch-comp-textarea-shape-radius)}#input{all:initial;display:block;font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-initial);resize:none;white-space:pre-wrap;overflow-wrap:break-word;padding:8px 10px 8px 12px;border:none;box-sizing:border-box}#input::placeholder{color:var(--sinch-comp-textarea-color-default-text-placeholder);opacity:1}#input:disabled{color:var(--sinch-comp-textarea-color-disabled-text-initial);-webkit-text-fill-color:var(--sinch-comp-textarea-color-disabled-text-initial)}#border{position:absolute;border:1px solid var(--sinch-comp-textarea-color-default-border-initial);border-radius:var(--sinch-local-shape-radius);inset:0;pointer-events:none}:host([invalid]) #border{border-color:var(--sinch-comp-textarea-color-invalid-border-initial)}#input:focus+#border{border-color:var(--sinch-comp-textarea-color-default-border-focus);border-width:2px}#input:disabled+#border{border-color:var(--sinch-comp-textarea-color-disabled-border-initial)}#bottom{display:flex;flex-direction:row;align-items:center;gap:8px;padding:12px 4px 4px}#bottom.empty{display:none}:host([resizable]) #bottom{padding-right:calc(var(--sinch-comp-textarea-size-resize-handle) + 4px)}#resize-handle{display:none;position:absolute;width:var(--sinch-comp-textarea-size-resize-handle);height:var(--sinch-comp-textarea-size-resize-handle);bottom:0;right:0;cursor:ns-resize}:host([resizable]) #resize-handle{display:block}#resize-icon{display:block;pointer-events:none;fill:var(--sinch-comp-textarea-color-default-border-initial)}</style><div id="wrapper"><textarea id="input"></textarea><div id="border"></div><div id="bottom"><slot name="bottom"></slot><div id="resize-handle"><svg id="resize-icon" width="16" height="16"><path d="m14.833 4.724-9.61 9.61-.942-.944 9.61-9.609.942.943ZM15.443 10 10.5 14.943 9.557 14 14.5 9.057l.943.943Z"/></svg></div></div></div>';
4
5
  const template = document.createElement('template');
@@ -15,12 +16,15 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
15
16
  #prevContentHeight = 0;
16
17
  #dragStartY = 0;
17
18
  #intersectionObserver = null;
19
+ #internals;
20
+ static formAssociated = true;
18
21
  constructor() {
19
22
  super();
20
23
  const shadowRoot = this.attachShadow({
21
24
  delegatesFocus: true
22
25
  });
23
26
  shadowRoot.appendChild(template.content.cloneNode(true));
27
+ this.#internals = this.attachInternals();
24
28
  this.#$input = shadowRoot.querySelector('#input');
25
29
  this.#$bottomSlot = shadowRoot.querySelector('slot[name="bottom"]');
26
30
  this.#$bottomWrapper = shadowRoot.querySelector('#bottom');
@@ -34,7 +38,9 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
34
38
  signal: this.#controller.signal
35
39
  };
36
40
  this.setAttribute('role', 'textbox');
41
+ this.#internals.role = 'textbox';
37
42
  this.ariaMultiLine = 'true';
43
+ this.#internals.ariaMultiLine = 'true';
38
44
  this.#$input.addEventListener('input', this.#onInput, options);
39
45
  this.#$input.addEventListener('compositionstart', this.#onCompositionStart, options);
40
46
  this.#$input.addEventListener('mousedown', this.#onSelectionChange, options);
@@ -60,6 +66,23 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
60
66
  this.#intersectionObserver = null;
61
67
  }
62
68
  }
69
+ formAssociatedCallback() {
70
+ setFormValue(this.#internals, this.#$input.value);
71
+ }
72
+ formResetCallback() {
73
+ this.#$input.value = '';
74
+ setFormValue(this.#internals, '');
75
+ }
76
+ formStateRestoreCallback(state) {
77
+ if (this.#internals.form === null || getBooleanAttribute(this.#internals.form, 'data-form-state-restore') === false) {
78
+ return;
79
+ }
80
+ if (state !== null) {
81
+ const value = typeof state === 'string' ? state : state.get(this.name);
82
+ this.#$input.value = value?.toString() ?? '';
83
+ setFormValue(this.#internals, value?.toString() ?? '');
84
+ }
85
+ }
63
86
  static get observedAttributes() {
64
87
  return ['value', 'placeholder', 'invalid', 'disabled', 'rows', 'minrows', 'resizable'];
65
88
  }
@@ -74,6 +97,7 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
74
97
  const isPrevCursorEnd = prevCursorPos === prevVal.length;
75
98
  const isShrinkingContent = nextVal.length < prevVal.length;
76
99
  this.#$input.value = nextVal;
100
+ setFormValue(this.#internals, nextVal);
77
101
  if (!this.resizable) {
78
102
  if (isShrinkingContent) {
79
103
  this.#$input.style.removeProperty('height');
@@ -94,6 +118,7 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
94
118
  {
95
119
  this.#$input.placeholder = newVal ?? '';
96
120
  updateAttribute(this, 'aria-placeholder', newVal);
121
+ this.#internals.ariaPlaceholder = newVal ?? '';
97
122
  break;
98
123
  }
99
124
  case 'invalid':
@@ -103,6 +128,7 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
103
128
  }
104
129
  const isInvalid = isAttrTrue(newVal);
105
130
  this.ariaInvalid = isInvalid.toString();
131
+ this.#internals.ariaInvalid = isInvalid.toString();
106
132
  updateBooleanAttribute(this, 'invalid', isInvalid);
107
133
  break;
108
134
  }
@@ -140,6 +166,12 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
140
166
  }
141
167
  }
142
168
  }
169
+ set name(value) {
170
+ updateAttribute(this, 'name', value);
171
+ }
172
+ get name() {
173
+ return getAttribute(this, 'name', '');
174
+ }
143
175
  set value(value) {
144
176
  updateAttribute(this, 'value', value);
145
177
  }
@@ -250,6 +282,7 @@ defineCustomElement('sinch-textarea', class extends NectaryElement {
250
282
  e.stopPropagation();
251
283
  const nextValue = this.#$input.value;
252
284
  const prevValue = this.value;
285
+ setFormValue(this.#internals, nextValue);
253
286
  if (prevValue !== nextValue) {
254
287
  const nextCursorPos = this.#$input.selectionEnd;
255
288
  if (!this.#isPendingDk) {
@@ -1,5 +1,7 @@
1
1
  import type { NectaryComponentReactByType, NectaryComponentVanillaByType } from '../types';
2
2
  export type TSinchTextareaProps = {
3
+ /** Identification for uncontrolled form submissions */
4
+ name?: string;
3
5
  /** Value */
4
6
  value: string;
5
7
  /** Text that appears in the text field when it has no value set */
@@ -0,0 +1,8 @@
1
+ import type { NectaryComponentVanilla } from '../types';
2
+ export declare const setFormValue: (internals: ElementInternals, value: File | string | FormData | null) => void;
3
+ /**
4
+ * The ElementInternals API currently does not support web components as form submitters,
5
+ * so we need to create a native button and copy form-related options to it.
6
+ */
7
+ export declare const requestSubmitForm: (form: HTMLFormElement, submitter: NectaryComponentVanilla<"sinch-button">) => void;
8
+ export declare const CSVToFormData: (name: string, csv: string) => "" | FormData;
package/utils/form.js ADDED
@@ -0,0 +1,36 @@
1
+ export const setFormValue = (internals, value) => {
2
+ let formValue = value ?? '';
3
+ if (formValue instanceof FormData && [...formValue.keys()].length === 0) {
4
+ formValue = '';
5
+ }
6
+ if (formValue instanceof File && formValue.size === 0) {
7
+ formValue = '';
8
+ }
9
+ if (internals.form !== null) {
10
+ internals.setFormValue(formValue);
11
+ }
12
+ };
13
+ export const requestSubmitForm = (form, submitter) => {
14
+ const submitterProxy = document.createElement('button');
15
+ submitterProxy.style.display = 'none';
16
+ submitterProxy.type = submitter.formType;
17
+ Array.from(submitter.attributes).filter(attr => attr.name.startsWith('aria-')).forEach(attr => {
18
+ submitterProxy.setAttribute(attr.name, attr.value);
19
+ });
20
+ form.appendChild(submitterProxy);
21
+ try {
22
+ form.requestSubmit(submitterProxy);
23
+ } finally {
24
+ form.removeChild(submitterProxy);
25
+ }
26
+ };
27
+ export const CSVToFormData = (name, csv) => {
28
+ if (csv.length === 0) {
29
+ return '';
30
+ }
31
+ const formData = new FormData();
32
+ csv.split(',').forEach(value => {
33
+ formData.append(name, value);
34
+ });
35
+ return formData;
36
+ };