@nectary/components 5.41.1 → 5.42.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/bundle.js CHANGED
@@ -291,40 +291,19 @@ class Icon extends NectaryElement {
291
291
  static get observedAttributes() {
292
292
  return ["name", "icons-version"];
293
293
  }
294
- /* Font class is now set before the text content. Covered by new regression tests.
295
-
296
- Before:
297
- 1. textContent = "fa-clone" Safari renders the text using the default font
298
- (Material Icons)
299
- 2. _matchNameToFont() → switches font-family to Sinch Icons Zero To D via
300
- class
301
-
302
- After:
303
- 1. _matchNameToFont() → sets the correct font-family class first
304
- 2. textContent = "fa-clone" → Safari renders the text using the correct font
305
- from the start
306
-
307
- Reasoning:
308
- * Safari caches the ligature layout from the first paint. When
309
- the text was set in Material Icons (which doesn't have these ligatures),
310
- Safari calculated glyph positions for individual characters. When the font
311
- then switched to Sinch Icons, Safari reused some of that cached layout
312
- instead of fully recalculating — shifting certain glyphs to the right.
313
- * _matchNameToFont() reads this.name via getAttribute(this,
314
- 'name'), which reads the HTML attribute — not textContent. The attribute is
315
- already set when the callback fires (that's what triggers the callback), so
316
- the method gets the correct name regardless of when textContent is set.
317
- */
294
+ // Font class is set before textContent to avoid a WebKit ligature caching
295
+ // bug: Safari caches glyph layout from the first paint, so if textContent
296
+ // lands while the default font (Material Icons) is active, the cached
297
+ // positions stick even after the font switches. Setting the class first
298
+ // ensures the correct font-family is active before any text renders.
318
299
  attributeChangedCallback(name, _, newVal) {
319
300
  switch (name) {
320
301
  case "name": {
321
- {
322
- this.#$icon.textContent = newVal;
323
- updateAttribute(this.#$icon, "aria-label", newVal);
324
- if (getAttribute(this, "icons-version", "1") !== "1") {
325
- this._matchNameToFont();
326
- }
302
+ if (getAttribute(this, "icons-version", "1") !== "1") {
303
+ this._matchNameToFont();
327
304
  }
305
+ this.#$icon.textContent = newVal;
306
+ updateAttribute(this.#$icon, "aria-label", newVal);
328
307
  break;
329
308
  }
330
309
  case "icons-version": {
@@ -9032,6 +9011,7 @@ const applyCountPlaceholder = (template2, count) => {
9032
9011
  return template2.replaceAll("{count}", count);
9033
9012
  };
9034
9013
  const sanitizeAttr = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;");
9014
+ const toReactCamelCaseHandlerName = (eventSuffix) => `on${eventSuffix.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}`;
9035
9015
  const isValidAction = (value) => {
9036
9016
  if (typeof value !== "object" || value === null) {
9037
9017
  return false;
@@ -9102,9 +9082,9 @@ class FloatingPanel extends NectaryElement {
9102
9082
  this.#resizeThrottle?.fn();
9103
9083
  });
9104
9084
  this.#resizeObserver.observe(this.#$dialog);
9105
- this.addEventListener("-close", this.#onCloseReact, { signal });
9106
- this.addEventListener("-select-all", this.#onSelectAllReact, { signal });
9107
- this.addEventListener("-action", this.#onActionReact, { signal });
9085
+ this.addEventListener("-close", this.#onCloseReactHandler, { signal });
9086
+ this.addEventListener("-select-all", this.#onSelectAllReactHandler, { signal });
9087
+ this.addEventListener("-action", this.#onActionReactHandler, { signal });
9108
9088
  if (!this.hasAttribute("actions")) {
9109
9089
  this.#renderActions(null);
9110
9090
  }
@@ -9305,7 +9285,7 @@ class FloatingPanel extends NectaryElement {
9305
9285
  * `-${action}` convenience event. The per-action event lets consumers bind
9306
9286
  * directly to a specific action (e.g. `on-archive`) without switching on
9307
9287
  * `e.detail` — and it works for any user-defined action, not just the
9308
- * built-in defaults. React handlers are forwarded by `#onActionReact`.
9288
+ * built-in defaults. React handlers are forwarded by `#onActionReactHandler`.
9309
9289
  */
9310
9290
  #dispatchAction(action) {
9311
9291
  this.dispatchEvent(new CustomEvent("-action", { detail: action }));
@@ -9411,16 +9391,20 @@ class FloatingPanel extends NectaryElement {
9411
9391
  #onActionsPopoverClose = () => {
9412
9392
  this.#closeOverflowMenu();
9413
9393
  };
9414
- #onCloseReact = (e) => {
9394
+ #onCloseReactHandler = (e) => {
9415
9395
  getReactEventHandler(this, "on-close")?.(e);
9396
+ getReactEventHandler(this, "onClose")?.(e);
9416
9397
  };
9417
- #onSelectAllReact = (e) => {
9398
+ #onSelectAllReactHandler = (e) => {
9418
9399
  getReactEventHandler(this, "on-select-all")?.(e);
9400
+ getReactEventHandler(this, "onSelectAll")?.(e);
9419
9401
  };
9420
- #onActionReact = (e) => {
9402
+ #onActionReactHandler = (e) => {
9421
9403
  getReactEventHandler(this, "on-action")?.(e);
9404
+ getReactEventHandler(this, "onAction")?.(e);
9422
9405
  if (e instanceof CustomEvent && typeof e.detail === "string") {
9423
9406
  getReactEventHandler(this, `on-${e.detail}`)?.(e);
9407
+ getReactEventHandler(this, toReactCamelCaseHandlerName(e.detail))?.(e);
9424
9408
  }
9425
9409
  };
9426
9410
  get panelRect() {
@@ -12232,11 +12216,18 @@ const assertListItem = ($n) => {
12232
12216
  throw new Error(`Node is not a ListItem: ${$n?.nodeName}`);
12233
12217
  }
12234
12218
  };
12219
+ const BLOCK_CLASSNAME = "block";
12235
12220
  const markListItemAsBlock = ($li) => {
12236
- $li.classList.add("block");
12221
+ $li.classList.add(BLOCK_CLASSNAME);
12237
12222
  };
12238
12223
  const isListItemMarkedArBlock = ($li) => {
12239
- return $li.classList.contains("block");
12224
+ return $li.classList.contains(BLOCK_CLASSNAME);
12225
+ };
12226
+ const markParagraphAsBlock = ($p) => {
12227
+ $p.classList.add(BLOCK_CLASSNAME);
12228
+ };
12229
+ const isParagraphMarkedAsBlock = ($p) => {
12230
+ return $p.classList.contains(BLOCK_CLASSNAME);
12240
12231
  };
12241
12232
  const MAX_LISTITEM_LEVEL = 4;
12242
12233
  const removeListItemLevel = ($li) => {
@@ -13753,6 +13744,7 @@ const MD_ULISTITEM_TOKEN = "*";
13753
13744
  const MD_OLISTITEM_TOKEN = "1.";
13754
13745
  const MD_LISTITEM_JOIN = "\n";
13755
13746
  const MD_PARAGRAPH_JOIN = "\n\n";
13747
+ const MD_SOFT_LINEBREAK = "\n";
13756
13748
  const serializeTextReducer = (state, desc, i, descArray) => {
13757
13749
  const { chunks } = state;
13758
13750
  if (desc.isLink === true) {
@@ -13874,7 +13866,7 @@ const serializeRoot = ($root, range) => {
13874
13866
  };
13875
13867
  const flushParagraphChunks = () => {
13876
13868
  if (paragraphChunks.length > 0) {
13877
- chunks.push(...paragraphChunks);
13869
+ chunks.push(paragraphChunks.join(MD_SOFT_LINEBREAK));
13878
13870
  paragraphChunks.length = 0;
13879
13871
  }
13880
13872
  };
@@ -13892,6 +13884,9 @@ const serializeRoot = ($root, range) => {
13892
13884
  } else {
13893
13885
  assertTextBlock($child);
13894
13886
  flushListChunks();
13887
+ if (isParagraph($child) && isParagraphMarkedAsBlock($child) && paragraphChunks.length > 0) {
13888
+ flushParagraphChunks();
13889
+ }
13895
13890
  paragraphChunks.push(
13896
13891
  serializeTextBlock($child, range)
13897
13892
  );
@@ -14008,8 +14003,10 @@ const createParseVisitor = (doc) => {
14008
14003
  },
14009
14004
  paragraph() {
14010
14005
  if (listsStack.length === 0) {
14011
- $currentBlock = createActuallyEmptyParagraph(doc);
14012
- $root.appendChild($currentBlock);
14006
+ const $p = createActuallyEmptyParagraph(doc);
14007
+ markParagraphAsBlock($p);
14008
+ $currentBlock = $p;
14009
+ $root.appendChild($p);
14013
14010
  }
14014
14011
  },
14015
14012
  end() {
@@ -14036,7 +14033,7 @@ const setBrowserCaret = ({ startContainer, startOffset, endContainer, endOffset
14036
14033
  range.setEnd(endContainer, endOffset);
14037
14034
  selection.addRange(range);
14038
14035
  };
14039
- const templateHTML$p = '<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);overflow:hidden;--sinch-local-shape-radius:var(--sinch-comp-textarea-shape-radius)}#input-wrapper{position:relative;padding:8px 10px 8px 12px;box-sizing:border-box}#input{font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-initial);white-space:pre-wrap;overflow-wrap:break-word;border:none;outline:0}#placeholder{display:none;position:absolute;left:0;top:0;font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-placeholder);padding:8px 10px 8px 12px;pointer-events:none;user-select:none}#input.empty+#placeholder{display:block}#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)}:host([disabled]){color:var(--sinch-comp-textarea-color-disabled-text-initial);-webkit-text-fill-color:var(--sinch-comp-textarea-color-disabled-text-initial)}:host([disabled]) #border{border-color:var(--sinch-comp-textarea-color-disabled-border-initial)}:host(:not([disabled])) #input-wrapper:focus-within~#border{border-color:var(--sinch-comp-textarea-color-default-border-focus);border-width:2px}.oli,.p,.uli{margin:0}.oli.l0,.uli.l0{margin-left:6px}.oli.l1,.uli.l1{margin-left:36px}.oli.l2,.uli.l2{margin-left:64px}.oli.l3,.uli.l3{margin-left:92px}.oli.l4,.uli.l4{margin-left:120px}.uli.l0{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli.l1{counter-reset:list-1 list-2 list-3 list-4}.uli.l2{counter-reset:list-2 list-3 list-4}.uli.l3{counter-reset:list-3 list-4}.uli.l4{counter-reset:list-4}.oli.l0{counter-reset:list-1 list-2 list-3 list-4}.oli.l1{counter-reset:list-2 list-3 list-4}.oli.l2{counter-reset:list-3 list-4}.oli.l3{counter-reset:list-4}.oli.l0::before{counter-increment:list-0;content:counter(list-0,decimal) ". "}.oli.l1::before{counter-increment:list-1;content:counter(list-1,lower-alpha) ". "}.oli.l2::before{counter-increment:list-2;content:counter(list-2,lower-roman) ". "}.oli.l3::before{counter-increment:list-3;content:counter(list-3,decimal) ". "}.oli.l4::before{counter-increment:list-4;content:counter(list-4,lower-alpha) ". "}.oli.block,.oli:first-of-type,.p+.oli{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli::before{content:"\\25CF";display:inline-block;width:16px}.oli+.p,.oli.block,.p+.oli,.p+.uli,.uli+.p,.uli.block{margin-top:.5em}.c{font:var(--sinch-comp-code-tag-font-text);font-size:inherit;line-height:inherit;color:var(--sinch-comp-code-tag-color-default-text-initial);border:1px solid var(--sinch-comp-code-tag-color-default-border-initial);background-color:var(--sinch-comp-code-tag-color-default-background-initial);padding:0 .25em;border-radius:var(--sinch-comp-code-tag-shape-radius)}.l{font:var(--sinch-comp-link-default-font-initial);color:var(--sinch-comp-link-color-default-text-initial);text-decoration:underline}sinch-rich-textarea-chip{display:inline-flex;vertical-align:middle;user-select:none}.i{font-style:italic}.b{font-weight:700}.s{text-decoration:line-through}.e{background-repeat:no-repeat;background-position:50% 50%;background-size:contain;width:1em;height:1em;vertical-align:-.2em}#top-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:4px 4px 0}#top-wrapper.empty{display:none}#bottom-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:0 4px 4px}#bottom-wrapper.empty{display:none}</style><div id="wrapper"><div id="top-wrapper"><slot id="top" name="top"></slot></div><div id="input-wrapper"><div id="input" contenteditable="true" role="textbox" aria-multiline="true" suppresscontenteditablewarning autocapitalize="false" autocorrect="false" autosave="false" spellcheck="false"></div><div id="placeholder"></div></div><div id="border"></div><div id="bottom-wrapper"><slot id="bottom" name="bottom"></slot></div></div>';
14036
+ const templateHTML$p = '<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);overflow:hidden;--sinch-local-shape-radius:var(--sinch-comp-textarea-shape-radius)}#input-wrapper{position:relative;padding:8px 10px 8px 12px;box-sizing:border-box}#input{font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-initial);caret-color:var(--sinch-comp-textarea-color-default-text-initial);white-space:pre-wrap;overflow-wrap:break-word;border:none;outline:0}#placeholder{display:none;position:absolute;left:0;top:0;font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-placeholder);padding:8px 10px 8px 12px;pointer-events:none;user-select:none}#input.empty+#placeholder{display:block}#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)}:host([disabled]){color:var(--sinch-comp-textarea-color-disabled-text-initial);-webkit-text-fill-color:var(--sinch-comp-textarea-color-disabled-text-initial)}:host([disabled]) #border{border-color:var(--sinch-comp-textarea-color-disabled-border-initial)}:host(:not([disabled])) #input-wrapper:focus-within~#border{border-color:var(--sinch-comp-textarea-color-default-border-focus);border-width:2px}.oli,.p,.uli{margin:0}.oli.l0,.uli.l0{margin-left:6px}.oli.l1,.uli.l1{margin-left:36px}.oli.l2,.uli.l2{margin-left:64px}.oli.l3,.uli.l3{margin-left:92px}.oli.l4,.uli.l4{margin-left:120px}.uli.l0{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli.l1{counter-reset:list-1 list-2 list-3 list-4}.uli.l2{counter-reset:list-2 list-3 list-4}.uli.l3{counter-reset:list-3 list-4}.uli.l4{counter-reset:list-4}.oli.l0{counter-reset:list-1 list-2 list-3 list-4}.oli.l1{counter-reset:list-2 list-3 list-4}.oli.l2{counter-reset:list-3 list-4}.oli.l3{counter-reset:list-4}.oli.l0::before{counter-increment:list-0;content:counter(list-0,decimal) ". "}.oli.l1::before{counter-increment:list-1;content:counter(list-1,lower-alpha) ". "}.oli.l2::before{counter-increment:list-2;content:counter(list-2,lower-roman) ". "}.oli.l3::before{counter-increment:list-3;content:counter(list-3,decimal) ". "}.oli.l4::before{counter-increment:list-4;content:counter(list-4,lower-alpha) ". "}.oli.block,.oli:first-of-type,.p+.oli{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli::before{content:"\\25CF";display:inline-block;width:16px}.oli+.p,.oli.block,.p+.oli,.p+.uli,.uli+.p,.uli.block{margin-top:.5em}.c{font:var(--sinch-comp-code-tag-font-text);font-size:inherit;line-height:inherit;color:var(--sinch-comp-code-tag-color-default-text-initial);border:1px solid var(--sinch-comp-code-tag-color-default-border-initial);background-color:var(--sinch-comp-code-tag-color-default-background-initial);padding:0 .25em;border-radius:var(--sinch-comp-code-tag-shape-radius)}.l{font:var(--sinch-comp-link-default-font-initial);color:var(--sinch-comp-link-color-default-text-initial);text-decoration:underline}sinch-rich-textarea-chip{display:inline-flex;vertical-align:middle;user-select:none}.i{font-style:italic}.b{font-weight:700}.s{text-decoration:line-through}.e{background-repeat:no-repeat;background-position:50% 50%;background-size:contain;width:1em;height:1em;vertical-align:-.2em}#top-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:4px 4px 0}#top-wrapper.empty{display:none}#bottom-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:0 4px 4px}#bottom-wrapper.empty{display:none}</style><div id="wrapper"><div id="top-wrapper"><slot id="top" name="top"></slot></div><div id="input-wrapper"><div id="input" contenteditable="true" role="textbox" aria-multiline="true" suppresscontenteditablewarning autocapitalize="false" autocorrect="false" autosave="false" spellcheck="false"></div><div id="placeholder"></div></div><div id="border"></div><div id="bottom-wrapper"><slot id="bottom" name="bottom"></slot></div></div>';
14040
14037
  const template$p = document.createElement("template");
14041
14038
  template$p.innerHTML = templateHTML$p;
14042
14039
  const SUPPORTS_SHADOW_SELECTION = typeof window.ShadowRoot.prototype.getSelection === "function";
@@ -14054,7 +14051,13 @@ class RichTextarea extends NectaryElement {
14054
14051
  #cachedRange = null;
14055
14052
  #lastSelectionInfo = null;
14056
14053
  #prevDispatchedValue = null;
14054
+ #changeDebounce = null;
14057
14055
  #parseVisitor;
14056
+ /* @internal
14057
+ * The chip resolver is used to resolve the color and icon of a chip.
14058
+ * It is used to resolve the color and icon of a chip.
14059
+ * It is used to resolve the color and icon of a chip.
14060
+ */
14058
14061
  #chipResolver = null;
14059
14062
  constructor() {
14060
14063
  super();
@@ -14118,6 +14121,7 @@ class RichTextarea extends NectaryElement {
14118
14121
  }
14119
14122
  disconnectedCallback() {
14120
14123
  super.disconnectedCallback();
14124
+ this.#clearChangeDebounce();
14121
14125
  this.#controller.abort();
14122
14126
  this.#controller = null;
14123
14127
  }
@@ -14443,9 +14447,23 @@ class RichTextarea extends NectaryElement {
14443
14447
  if (result.range !== null) {
14444
14448
  setBrowserCaret(result.range);
14445
14449
  }
14450
+ this.#scheduleChangeDispatch();
14446
14451
  }
14447
14452
  this.#updateEditorEmptyClass();
14448
14453
  }
14454
+ #clearChangeDebounce() {
14455
+ if (this.#changeDebounce !== null) {
14456
+ clearTimeout(this.#changeDebounce);
14457
+ this.#changeDebounce = null;
14458
+ }
14459
+ }
14460
+ #scheduleChangeDispatch() {
14461
+ this.#clearChangeDebounce();
14462
+ this.#changeDebounce = setTimeout(() => {
14463
+ this.#changeDebounce = null;
14464
+ this.#dispatchChangeEvent();
14465
+ }, 100);
14466
+ }
14449
14467
  #onInputFocus = () => {
14450
14468
  if (this.#cachedRange !== null) {
14451
14469
  setBrowserCaret(this.#cachedRange);
@@ -14454,6 +14472,7 @@ class RichTextarea extends NectaryElement {
14454
14472
  };
14455
14473
  #onInputBlur = () => {
14456
14474
  this.dispatchEvent(new CustomEvent("-blur"));
14475
+ this.#clearChangeDebounce();
14457
14476
  this.#dispatchChangeEvent();
14458
14477
  };
14459
14478
  #dispatchChangeEvent() {
@@ -29,6 +29,7 @@ const applyCountPlaceholder = (template2, count) => {
29
29
  return template2.replaceAll("{count}", count);
30
30
  };
31
31
  const sanitizeAttr = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;");
32
+ const toReactCamelCaseHandlerName = (eventSuffix) => `on${eventSuffix.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}`;
32
33
  const isValidAction = (value) => {
33
34
  if (typeof value !== "object" || value === null) {
34
35
  return false;
@@ -99,9 +100,9 @@ class FloatingPanel extends NectaryElement {
99
100
  this.#resizeThrottle?.fn();
100
101
  });
101
102
  this.#resizeObserver.observe(this.#$dialog);
102
- this.addEventListener("-close", this.#onCloseReact, { signal });
103
- this.addEventListener("-select-all", this.#onSelectAllReact, { signal });
104
- this.addEventListener("-action", this.#onActionReact, { signal });
103
+ this.addEventListener("-close", this.#onCloseReactHandler, { signal });
104
+ this.addEventListener("-select-all", this.#onSelectAllReactHandler, { signal });
105
+ this.addEventListener("-action", this.#onActionReactHandler, { signal });
105
106
  if (!this.hasAttribute("actions")) {
106
107
  this.#renderActions(null);
107
108
  }
@@ -302,7 +303,7 @@ class FloatingPanel extends NectaryElement {
302
303
  * `-${action}` convenience event. The per-action event lets consumers bind
303
304
  * directly to a specific action (e.g. `on-archive`) without switching on
304
305
  * `e.detail` — and it works for any user-defined action, not just the
305
- * built-in defaults. React handlers are forwarded by `#onActionReact`.
306
+ * built-in defaults. React handlers are forwarded by `#onActionReactHandler`.
306
307
  */
307
308
  #dispatchAction(action) {
308
309
  this.dispatchEvent(new CustomEvent("-action", { detail: action }));
@@ -408,16 +409,20 @@ class FloatingPanel extends NectaryElement {
408
409
  #onActionsPopoverClose = () => {
409
410
  this.#closeOverflowMenu();
410
411
  };
411
- #onCloseReact = (e) => {
412
+ #onCloseReactHandler = (e) => {
412
413
  getReactEventHandler(this, "on-close")?.(e);
414
+ getReactEventHandler(this, "onClose")?.(e);
413
415
  };
414
- #onSelectAllReact = (e) => {
416
+ #onSelectAllReactHandler = (e) => {
415
417
  getReactEventHandler(this, "on-select-all")?.(e);
418
+ getReactEventHandler(this, "onSelectAll")?.(e);
416
419
  };
417
- #onActionReact = (e) => {
420
+ #onActionReactHandler = (e) => {
418
421
  getReactEventHandler(this, "on-action")?.(e);
422
+ getReactEventHandler(this, "onAction")?.(e);
419
423
  if (e instanceof CustomEvent && typeof e.detail === "string") {
420
424
  getReactEventHandler(this, `on-${e.detail}`)?.(e);
425
+ getReactEventHandler(this, toReactCamelCaseHandlerName(e.detail))?.(e);
421
426
  }
422
427
  };
423
428
  get panelRect() {
package/icon/index.js CHANGED
@@ -14,40 +14,19 @@ class Icon extends NectaryElement {
14
14
  static get observedAttributes() {
15
15
  return ["name", "icons-version"];
16
16
  }
17
- /* Font class is now set before the text content. Covered by new regression tests.
18
-
19
- Before:
20
- 1. textContent = "fa-clone" Safari renders the text using the default font
21
- (Material Icons)
22
- 2. _matchNameToFont() → switches font-family to Sinch Icons Zero To D via
23
- class
24
-
25
- After:
26
- 1. _matchNameToFont() → sets the correct font-family class first
27
- 2. textContent = "fa-clone" → Safari renders the text using the correct font
28
- from the start
29
-
30
- Reasoning:
31
- * Safari caches the ligature layout from the first paint. When
32
- the text was set in Material Icons (which doesn't have these ligatures),
33
- Safari calculated glyph positions for individual characters. When the font
34
- then switched to Sinch Icons, Safari reused some of that cached layout
35
- instead of fully recalculating — shifting certain glyphs to the right.
36
- * _matchNameToFont() reads this.name via getAttribute(this,
37
- 'name'), which reads the HTML attribute — not textContent. The attribute is
38
- already set when the callback fires (that's what triggers the callback), so
39
- the method gets the correct name regardless of when textContent is set.
40
- */
17
+ // Font class is set before textContent to avoid a WebKit ligature caching
18
+ // bug: Safari caches glyph layout from the first paint, so if textContent
19
+ // lands while the default font (Material Icons) is active, the cached
20
+ // positions stick even after the font switches. Setting the class first
21
+ // ensures the correct font-family is active before any text renders.
41
22
  attributeChangedCallback(name, _, newVal) {
42
23
  switch (name) {
43
24
  case "name": {
44
- {
45
- this.#$icon.textContent = newVal;
46
- updateAttribute(this.#$icon, "aria-label", newVal);
47
- if (getAttribute(this, "icons-version", "1") !== "1") {
48
- this._matchNameToFont();
49
- }
25
+ if (getAttribute(this, "icons-version", "1") !== "1") {
26
+ this._matchNameToFont();
50
27
  }
28
+ this.#$icon.textContent = newVal;
29
+ updateAttribute(this.#$icon, "aria-label", newVal);
51
30
  break;
52
31
  }
53
32
  case "icons-version": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nectary/components",
3
- "version": "5.41.1",
3
+ "version": "5.42.0",
4
4
  "files": [
5
5
  "**/*/*.css",
6
6
  "**/*/*.json",
@@ -6,7 +6,7 @@ import { isElementFocused } from "../utils/slot.js";
6
6
  import { getReactEventHandler } from "../utils/get-react-event-handler.js";
7
7
  import { parseMarkdown } from "../utils/markdown.js";
8
8
  import { createParseVisitor, getEndRange, formatList, handleEmojiMousedown, removeChip, setBrowserCaret, formatOutdent, formatIndent, formatInline, insertChip, insertLink, insertText, insertLineBreak, deleteContentBackward, serializeMarkdown, getSelectionInfo, isSelectionEqual, insertFromPaste, isEditorEmpty } from "./utils.js";
9
- 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);overflow:hidden;--sinch-local-shape-radius:var(--sinch-comp-textarea-shape-radius)}#input-wrapper{position:relative;padding:8px 10px 8px 12px;box-sizing:border-box}#input{font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-initial);white-space:pre-wrap;overflow-wrap:break-word;border:none;outline:0}#placeholder{display:none;position:absolute;left:0;top:0;font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-placeholder);padding:8px 10px 8px 12px;pointer-events:none;user-select:none}#input.empty+#placeholder{display:block}#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)}:host([disabled]){color:var(--sinch-comp-textarea-color-disabled-text-initial);-webkit-text-fill-color:var(--sinch-comp-textarea-color-disabled-text-initial)}:host([disabled]) #border{border-color:var(--sinch-comp-textarea-color-disabled-border-initial)}:host(:not([disabled])) #input-wrapper:focus-within~#border{border-color:var(--sinch-comp-textarea-color-default-border-focus);border-width:2px}.oli,.p,.uli{margin:0}.oli.l0,.uli.l0{margin-left:6px}.oli.l1,.uli.l1{margin-left:36px}.oli.l2,.uli.l2{margin-left:64px}.oli.l3,.uli.l3{margin-left:92px}.oli.l4,.uli.l4{margin-left:120px}.uli.l0{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli.l1{counter-reset:list-1 list-2 list-3 list-4}.uli.l2{counter-reset:list-2 list-3 list-4}.uli.l3{counter-reset:list-3 list-4}.uli.l4{counter-reset:list-4}.oli.l0{counter-reset:list-1 list-2 list-3 list-4}.oli.l1{counter-reset:list-2 list-3 list-4}.oli.l2{counter-reset:list-3 list-4}.oli.l3{counter-reset:list-4}.oli.l0::before{counter-increment:list-0;content:counter(list-0,decimal) ". "}.oli.l1::before{counter-increment:list-1;content:counter(list-1,lower-alpha) ". "}.oli.l2::before{counter-increment:list-2;content:counter(list-2,lower-roman) ". "}.oli.l3::before{counter-increment:list-3;content:counter(list-3,decimal) ". "}.oli.l4::before{counter-increment:list-4;content:counter(list-4,lower-alpha) ". "}.oli.block,.oli:first-of-type,.p+.oli{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli::before{content:"\\25CF";display:inline-block;width:16px}.oli+.p,.oli.block,.p+.oli,.p+.uli,.uli+.p,.uli.block{margin-top:.5em}.c{font:var(--sinch-comp-code-tag-font-text);font-size:inherit;line-height:inherit;color:var(--sinch-comp-code-tag-color-default-text-initial);border:1px solid var(--sinch-comp-code-tag-color-default-border-initial);background-color:var(--sinch-comp-code-tag-color-default-background-initial);padding:0 .25em;border-radius:var(--sinch-comp-code-tag-shape-radius)}.l{font:var(--sinch-comp-link-default-font-initial);color:var(--sinch-comp-link-color-default-text-initial);text-decoration:underline}sinch-rich-textarea-chip{display:inline-flex;vertical-align:middle;user-select:none}.i{font-style:italic}.b{font-weight:700}.s{text-decoration:line-through}.e{background-repeat:no-repeat;background-position:50% 50%;background-size:contain;width:1em;height:1em;vertical-align:-.2em}#top-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:4px 4px 0}#top-wrapper.empty{display:none}#bottom-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:0 4px 4px}#bottom-wrapper.empty{display:none}</style><div id="wrapper"><div id="top-wrapper"><slot id="top" name="top"></slot></div><div id="input-wrapper"><div id="input" contenteditable="true" role="textbox" aria-multiline="true" suppresscontenteditablewarning autocapitalize="false" autocorrect="false" autosave="false" spellcheck="false"></div><div id="placeholder"></div></div><div id="border"></div><div id="bottom-wrapper"><slot id="bottom" name="bottom"></slot></div></div>';
9
+ 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);overflow:hidden;--sinch-local-shape-radius:var(--sinch-comp-textarea-shape-radius)}#input-wrapper{position:relative;padding:8px 10px 8px 12px;box-sizing:border-box}#input{font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-initial);caret-color:var(--sinch-comp-textarea-color-default-text-initial);white-space:pre-wrap;overflow-wrap:break-word;border:none;outline:0}#placeholder{display:none;position:absolute;left:0;top:0;font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-placeholder);padding:8px 10px 8px 12px;pointer-events:none;user-select:none}#input.empty+#placeholder{display:block}#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)}:host([disabled]){color:var(--sinch-comp-textarea-color-disabled-text-initial);-webkit-text-fill-color:var(--sinch-comp-textarea-color-disabled-text-initial)}:host([disabled]) #border{border-color:var(--sinch-comp-textarea-color-disabled-border-initial)}:host(:not([disabled])) #input-wrapper:focus-within~#border{border-color:var(--sinch-comp-textarea-color-default-border-focus);border-width:2px}.oli,.p,.uli{margin:0}.oli.l0,.uli.l0{margin-left:6px}.oli.l1,.uli.l1{margin-left:36px}.oli.l2,.uli.l2{margin-left:64px}.oli.l3,.uli.l3{margin-left:92px}.oli.l4,.uli.l4{margin-left:120px}.uli.l0{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli.l1{counter-reset:list-1 list-2 list-3 list-4}.uli.l2{counter-reset:list-2 list-3 list-4}.uli.l3{counter-reset:list-3 list-4}.uli.l4{counter-reset:list-4}.oli.l0{counter-reset:list-1 list-2 list-3 list-4}.oli.l1{counter-reset:list-2 list-3 list-4}.oli.l2{counter-reset:list-3 list-4}.oli.l3{counter-reset:list-4}.oli.l0::before{counter-increment:list-0;content:counter(list-0,decimal) ". "}.oli.l1::before{counter-increment:list-1;content:counter(list-1,lower-alpha) ". "}.oli.l2::before{counter-increment:list-2;content:counter(list-2,lower-roman) ". "}.oli.l3::before{counter-increment:list-3;content:counter(list-3,decimal) ". "}.oli.l4::before{counter-increment:list-4;content:counter(list-4,lower-alpha) ". "}.oli.block,.oli:first-of-type,.p+.oli{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli::before{content:"\\25CF";display:inline-block;width:16px}.oli+.p,.oli.block,.p+.oli,.p+.uli,.uli+.p,.uli.block{margin-top:.5em}.c{font:var(--sinch-comp-code-tag-font-text);font-size:inherit;line-height:inherit;color:var(--sinch-comp-code-tag-color-default-text-initial);border:1px solid var(--sinch-comp-code-tag-color-default-border-initial);background-color:var(--sinch-comp-code-tag-color-default-background-initial);padding:0 .25em;border-radius:var(--sinch-comp-code-tag-shape-radius)}.l{font:var(--sinch-comp-link-default-font-initial);color:var(--sinch-comp-link-color-default-text-initial);text-decoration:underline}sinch-rich-textarea-chip{display:inline-flex;vertical-align:middle;user-select:none}.i{font-style:italic}.b{font-weight:700}.s{text-decoration:line-through}.e{background-repeat:no-repeat;background-position:50% 50%;background-size:contain;width:1em;height:1em;vertical-align:-.2em}#top-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:4px 4px 0}#top-wrapper.empty{display:none}#bottom-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:0 4px 4px}#bottom-wrapper.empty{display:none}</style><div id="wrapper"><div id="top-wrapper"><slot id="top" name="top"></slot></div><div id="input-wrapper"><div id="input" contenteditable="true" role="textbox" aria-multiline="true" suppresscontenteditablewarning autocapitalize="false" autocorrect="false" autosave="false" spellcheck="false"></div><div id="placeholder"></div></div><div id="border"></div><div id="bottom-wrapper"><slot id="bottom" name="bottom"></slot></div></div>';
10
10
  const template = document.createElement("template");
11
11
  template.innerHTML = templateHTML;
12
12
  const SUPPORTS_SHADOW_SELECTION = typeof window.ShadowRoot.prototype.getSelection === "function";
@@ -24,7 +24,13 @@ class RichTextarea extends NectaryElement {
24
24
  #cachedRange = null;
25
25
  #lastSelectionInfo = null;
26
26
  #prevDispatchedValue = null;
27
+ #changeDebounce = null;
27
28
  #parseVisitor;
29
+ /* @internal
30
+ * The chip resolver is used to resolve the color and icon of a chip.
31
+ * It is used to resolve the color and icon of a chip.
32
+ * It is used to resolve the color and icon of a chip.
33
+ */
28
34
  #chipResolver = null;
29
35
  constructor() {
30
36
  super();
@@ -88,6 +94,7 @@ class RichTextarea extends NectaryElement {
88
94
  }
89
95
  disconnectedCallback() {
90
96
  super.disconnectedCallback();
97
+ this.#clearChangeDebounce();
91
98
  this.#controller.abort();
92
99
  this.#controller = null;
93
100
  }
@@ -413,9 +420,23 @@ class RichTextarea extends NectaryElement {
413
420
  if (result.range !== null) {
414
421
  setBrowserCaret(result.range);
415
422
  }
423
+ this.#scheduleChangeDispatch();
416
424
  }
417
425
  this.#updateEditorEmptyClass();
418
426
  }
427
+ #clearChangeDebounce() {
428
+ if (this.#changeDebounce !== null) {
429
+ clearTimeout(this.#changeDebounce);
430
+ this.#changeDebounce = null;
431
+ }
432
+ }
433
+ #scheduleChangeDispatch() {
434
+ this.#clearChangeDebounce();
435
+ this.#changeDebounce = setTimeout(() => {
436
+ this.#changeDebounce = null;
437
+ this.#dispatchChangeEvent();
438
+ }, 100);
439
+ }
419
440
  #onInputFocus = () => {
420
441
  if (this.#cachedRange !== null) {
421
442
  setBrowserCaret(this.#cachedRange);
@@ -424,6 +445,7 @@ class RichTextarea extends NectaryElement {
424
445
  };
425
446
  #onInputBlur = () => {
426
447
  this.dispatchEvent(new CustomEvent("-blur"));
448
+ this.#clearChangeDebounce();
427
449
  this.#dispatchChangeEvent();
428
450
  };
429
451
  #dispatchChangeEvent() {
@@ -24,6 +24,7 @@ export type TSinchRichTextareaProps = {
24
24
  'chip-color'?: TSinchTagColor;
25
25
  /** Default icon for chips/tags */
26
26
  'chip-icon'?: string;
27
+ /** Accessible label for the rich textarea */
27
28
  'aria-label': string;
28
29
  };
29
30
  export type TSinchRichTextareaMethods = {
@@ -44,7 +45,7 @@ export type TSinchRichTextareaMethods = {
44
45
  chipResolver: TChipResolver | null;
45
46
  };
46
47
  export type TSinchRichTextareaEvents = {
47
- /** Change value handler */
48
+ /** Change value handler — fires while editing (debounced) and on blur */
48
49
  '-change'?: (e: CustomEvent<string>) => void;
49
50
  /** Focus handler */
50
51
  '-focus'?: (e: CustomEvent<void>) => void;
@@ -55,11 +55,18 @@ const assertListItem = ($n) => {
55
55
  throw new Error(`Node is not a ListItem: ${$n?.nodeName}`);
56
56
  }
57
57
  };
58
+ const BLOCK_CLASSNAME = "block";
58
59
  const markListItemAsBlock = ($li) => {
59
- $li.classList.add("block");
60
+ $li.classList.add(BLOCK_CLASSNAME);
60
61
  };
61
62
  const isListItemMarkedArBlock = ($li) => {
62
- return $li.classList.contains("block");
63
+ return $li.classList.contains(BLOCK_CLASSNAME);
64
+ };
65
+ const markParagraphAsBlock = ($p) => {
66
+ $p.classList.add(BLOCK_CLASSNAME);
67
+ };
68
+ const isParagraphMarkedAsBlock = ($p) => {
69
+ return $p.classList.contains(BLOCK_CLASSNAME);
63
70
  };
64
71
  const MAX_LISTITEM_LEVEL = 4;
65
72
  const removeListItemLevel = ($li) => {
@@ -1576,6 +1583,7 @@ const MD_ULISTITEM_TOKEN = "*";
1576
1583
  const MD_OLISTITEM_TOKEN = "1.";
1577
1584
  const MD_LISTITEM_JOIN = "\n";
1578
1585
  const MD_PARAGRAPH_JOIN = "\n\n";
1586
+ const MD_SOFT_LINEBREAK = "\n";
1579
1587
  const serializeTextReducer = (state, desc, i, descArray) => {
1580
1588
  const { chunks } = state;
1581
1589
  if (desc.isLink === true) {
@@ -1697,7 +1705,7 @@ const serializeRoot = ($root, range) => {
1697
1705
  };
1698
1706
  const flushParagraphChunks = () => {
1699
1707
  if (paragraphChunks.length > 0) {
1700
- chunks.push(...paragraphChunks);
1708
+ chunks.push(paragraphChunks.join(MD_SOFT_LINEBREAK));
1701
1709
  paragraphChunks.length = 0;
1702
1710
  }
1703
1711
  };
@@ -1715,6 +1723,9 @@ const serializeRoot = ($root, range) => {
1715
1723
  } else {
1716
1724
  assertTextBlock($child);
1717
1725
  flushListChunks();
1726
+ if (isParagraph($child) && isParagraphMarkedAsBlock($child) && paragraphChunks.length > 0) {
1727
+ flushParagraphChunks();
1728
+ }
1718
1729
  paragraphChunks.push(
1719
1730
  serializeTextBlock($child, range)
1720
1731
  );
@@ -1831,8 +1842,10 @@ const createParseVisitor = (doc) => {
1831
1842
  },
1832
1843
  paragraph() {
1833
1844
  if (listsStack.length === 0) {
1834
- $currentBlock = createActuallyEmptyParagraph(doc);
1835
- $root.appendChild($currentBlock);
1845
+ const $p = createActuallyEmptyParagraph(doc);
1846
+ markParagraphAsBlock($p);
1847
+ $currentBlock = $p;
1848
+ $root.appendChild($p);
1836
1849
  }
1837
1850
  },
1838
1851
  end() {