carbon-components-svelte 0.106.2 → 0.107.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carbon-components-svelte",
3
- "version": "0.106.2",
3
+ "version": "0.107.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Svelte implementation of the Carbon Design System",
6
6
  "type": "module",
@@ -55,6 +55,7 @@
55
55
  createEventDispatcher,
56
56
  onMount,
57
57
  setContext,
58
+ tick,
58
59
  } from "svelte";
59
60
  import { readonly, writable } from "svelte/store";
60
61
 
@@ -67,6 +68,14 @@
67
68
  const groupName = writable(name);
68
69
  const groupRequired = writable(required);
69
70
  const groupDisabled = writable(disabled);
71
+ let isInitialRender = true;
72
+ let isSyncingSelected = false;
73
+
74
+ function syncSelectedValues() {
75
+ isSyncingSelected = true;
76
+ $selectedValues = selected;
77
+ isSyncingSelected = false;
78
+ }
70
79
 
71
80
  /**
72
81
  * @type {(value: string | number, checked: boolean) => void}
@@ -89,16 +98,21 @@
89
98
  });
90
99
 
91
100
  onMount(() => {
92
- $selectedValues = selected;
101
+ syncSelectedValues();
102
+ tick().then(() => {
103
+ isInitialRender = false;
104
+ });
93
105
  });
94
106
 
95
107
  beforeUpdate(() => {
96
- $selectedValues = selected;
108
+ syncSelectedValues();
97
109
  });
98
110
 
99
111
  selectedValues.subscribe((value) => {
100
112
  selected = value;
101
- dispatch("change", value);
113
+ if (!isInitialRender && !isSyncingSelected) {
114
+ dispatch("change", value);
115
+ }
102
116
  });
103
117
 
104
118
  $: $groupName = name;
@@ -31,11 +31,16 @@
31
31
  /** @type {HTMLDivElement | null} */
32
32
  let refContainer = null;
33
33
 
34
+ let prevIndex = -1;
35
+
34
36
  $: currentIndex = -1;
35
37
  $: focusedIndex = -1;
36
38
  $: switches = [];
37
39
  $: if (switches[currentIndex]) {
38
- dispatch("change", currentIndex);
40
+ if (prevIndex > -1 && prevIndex !== currentIndex) {
41
+ dispatch("change", currentIndex);
42
+ }
43
+ prevIndex = currentIndex;
39
44
  currentId.set(switches[currentIndex].id);
40
45
  }
41
46
 
@@ -90,6 +90,15 @@
90
90
  */
91
91
  export let ref = null;
92
92
 
93
+ /**
94
+ * Specify the DOM element to mount the portal into.
95
+ * When not set, mounts into the anchor's nearest `<dialog>` ancestor if one
96
+ * exists (so the portal participates in the dialog's top layer), otherwise
97
+ * falls back to `document.body`.
98
+ * @type {HTMLElement | null}
99
+ */
100
+ export let target = null;
101
+
93
102
  import { onMount, tick } from "svelte";
94
103
  import Portal from "./Portal.svelte";
95
104
 
@@ -181,11 +190,29 @@
181
190
  }
182
191
  }
183
192
 
193
+ // Auto-detect the nearest <dialog> ancestor of the anchor so that portalled
194
+ // content participates in the dialog's top layer by default. An explicit
195
+ // `target` prop overrides this.
196
+ $: effectiveTarget = target ?? anchor?.closest("dialog") ?? null;
197
+
198
+ // When the portal is mounted into a custom target (e.g. a native <dialog>
199
+ // opened with showModal()), `position: absolute` resolves against the target's
200
+ // containing block rather than the viewport. Use `position: fixed` in that
201
+ // case — fixed stays viewport-relative even inside a top-layer dialog — and
202
+ // skip the document scroll offsets, which only apply to absolute positioning
203
+ // relative to `document.body`.
204
+ $: useFixedPosition =
205
+ effectiveTarget != null &&
206
+ typeof document !== "undefined" &&
207
+ effectiveTarget !== document.body;
208
+
184
209
  function updatePosition() {
185
210
  if (!mounted || !anchor || !ref) return;
186
211
 
187
212
  const rect = anchor.getBoundingClientRect();
188
213
  const floatingRect = ref.getBoundingClientRect();
214
+ const scrollYOffset = useFixedPosition ? 0 : window.scrollY;
215
+ const scrollXOffset = useFixedPosition ? 0 : window.scrollX;
189
216
 
190
217
  const isVertical = direction === "top" || direction === "bottom";
191
218
  let actualDirection = direction;
@@ -234,27 +261,27 @@
234
261
  let width;
235
262
 
236
263
  if (actualDirection === "bottom") {
237
- top = rect.bottom + window.scrollY + gapBottom;
238
- left = rect.left + window.scrollX;
264
+ top = rect.bottom + scrollYOffset + gapBottom;
265
+ left = rect.left + scrollXOffset;
239
266
  width = rect.width;
240
267
  } else if (actualDirection === "top") {
241
- top = rect.top + window.scrollY - floatingRect.height - gapTop;
242
- left = rect.left + window.scrollX;
268
+ top = rect.top + scrollYOffset - floatingRect.height - gapTop;
269
+ left = rect.left + scrollXOffset;
243
270
  width = rect.width;
244
271
  } else if (actualDirection === "right") {
245
272
  if (intrinsicWidth) {
246
273
  if (intrinsicAlign === "start") {
247
- top = rect.top + window.scrollY + verticalAlignOffsetRight;
274
+ top = rect.top + scrollYOffset + verticalAlignOffsetRight;
248
275
  } else if (intrinsicAlign === "end") {
249
276
  top =
250
277
  rect.bottom +
251
- window.scrollY -
278
+ scrollYOffset -
252
279
  floatingRect.height +
253
280
  verticalAlignOffsetRight;
254
281
  } else {
255
282
  top =
256
283
  rect.top +
257
- window.scrollY +
284
+ scrollYOffset +
258
285
  rect.height / 2 -
259
286
  floatingRect.height / 2 +
260
287
  verticalAlignOffsetRight;
@@ -262,27 +289,27 @@
262
289
  } else {
263
290
  top =
264
291
  rect.top +
265
- window.scrollY +
292
+ scrollYOffset +
266
293
  rect.height / 2 -
267
294
  floatingRect.height / 2 +
268
295
  verticalAlignOffsetRight;
269
296
  }
270
- left = rect.right + window.scrollX + horizontalGapRight;
297
+ left = rect.right + scrollXOffset + horizontalGapRight;
271
298
  } else {
272
299
  // left
273
300
  if (intrinsicWidth) {
274
301
  if (intrinsicAlign === "start") {
275
- top = rect.top + window.scrollY + verticalAlignOffsetLeft;
302
+ top = rect.top + scrollYOffset + verticalAlignOffsetLeft;
276
303
  } else if (intrinsicAlign === "end") {
277
304
  top =
278
305
  rect.bottom +
279
- window.scrollY -
306
+ scrollYOffset -
280
307
  floatingRect.height +
281
308
  verticalAlignOffsetLeft;
282
309
  } else {
283
310
  top =
284
311
  rect.top +
285
- window.scrollY +
312
+ scrollYOffset +
286
313
  rect.height / 2 -
287
314
  floatingRect.height / 2 +
288
315
  verticalAlignOffsetLeft;
@@ -290,13 +317,12 @@
290
317
  } else {
291
318
  top =
292
319
  rect.top +
293
- window.scrollY +
320
+ scrollYOffset +
294
321
  rect.height / 2 -
295
322
  floatingRect.height / 2 +
296
323
  verticalAlignOffsetLeft;
297
324
  }
298
- left =
299
- rect.left + window.scrollX - floatingRect.width - horizontalGapLeft;
325
+ left = rect.left + scrollXOffset - floatingRect.width - horizontalGapLeft;
300
326
  }
301
327
 
302
328
  let posLeft = left;
@@ -308,11 +334,11 @@
308
334
  (actualDirection === "top" || actualDirection === "bottom")
309
335
  ) {
310
336
  if (intrinsicAlign === "center") {
311
- posLeft = rect.left + window.scrollX + rect.width / 2;
337
+ posLeft = rect.left + scrollXOffset + rect.width / 2;
312
338
  } else if (intrinsicAlign === "start") {
313
- posLeft = rect.left + window.scrollX;
339
+ posLeft = rect.left + scrollXOffset;
314
340
  } else {
315
- posLeft = rect.right + window.scrollX;
341
+ posLeft = rect.right + scrollXOffset;
316
342
  }
317
343
  posWidth = undefined;
318
344
  }
@@ -370,7 +396,7 @@
370
396
  : null;
371
397
 
372
398
  $: portalStyle = [
373
- "position: absolute",
399
+ useFixedPosition ? "position: fixed" : "position: absolute",
374
400
  `top: ${pos.top}px`,
375
401
  `left: ${pos.left}px`,
376
402
  intrinsicTranslateX,
@@ -423,6 +449,7 @@
423
449
  {#if open}
424
450
  <Portal
425
451
  bind:ref
452
+ target={effectiveTarget}
426
453
  {...$$restProps}
427
454
  data-floating-portal
428
455
  data-floating-direction={pos.actualDirection}
@@ -89,6 +89,15 @@ export type FloatingPortalProps = {
89
89
  */
90
90
  ref?: null | HTMLElement;
91
91
 
92
+ /**
93
+ * Specify the DOM element to mount the portal into.
94
+ * When not set, mounts into the anchor's nearest `<dialog>` ancestor if one
95
+ * exists (so the portal participates in the dialog's top layer), otherwise
96
+ * falls back to `document.body`.
97
+ * @default null
98
+ */
99
+ target?: HTMLElement | null;
100
+
92
101
  children?: (
93
102
  this: void,
94
103
  ...args: [{ direction: "bottom" | "top" | "left" | "right" }]
@@ -5,6 +5,13 @@
5
5
  */
6
6
  export let tag = "div";
7
7
 
8
+ /**
9
+ * Specify the DOM element to mount the portal into.
10
+ * Defaults to `document.body`.
11
+ * @type {HTMLElement | null}
12
+ */
13
+ export let target = null;
14
+
8
15
  /**
9
16
  * Obtain a reference to the portal element.
10
17
  * @type {null | HTMLElement}
@@ -27,11 +34,14 @@
27
34
  };
28
35
  });
29
36
 
30
- $: if (mounted && ref) {
31
- if (typeof document !== "undefined" && ref.parentNode !== document.body) {
37
+ $: effectiveTarget =
38
+ target ?? (typeof document === "undefined" ? null : document.body);
39
+
40
+ $: if (mounted && ref && effectiveTarget) {
41
+ if (ref.parentNode !== effectiveTarget) {
32
42
  const activeEl = document.activeElement;
33
43
  const hadFocus = ref.contains(activeEl);
34
- document.body.appendChild(ref);
44
+ effectiveTarget.appendChild(ref);
35
45
  if (hadFocus && activeEl instanceof HTMLElement) {
36
46
  activeEl.focus();
37
47
  }
@@ -10,6 +10,13 @@ type $Props = {
10
10
  */
11
11
  tag?: keyof HTMLElementTagNameMap;
12
12
 
13
+ /**
14
+ * Specify the DOM element to mount the portal into.
15
+ * Defaults to `document.body`.
16
+ * @default null
17
+ */
18
+ target?: HTMLElement | null;
19
+
13
20
  /**
14
21
  * Obtain a reference to the portal element.
15
22
  * @default null
@@ -65,6 +65,7 @@
65
65
  export let id = undefined;
66
66
 
67
67
  import {
68
+ afterUpdate,
68
69
  beforeUpdate,
69
70
  createEventDispatcher,
70
71
  onMount,
@@ -79,6 +80,7 @@
79
80
  const selectedValue = writable(selected);
80
81
  const groupName = writable(name);
81
82
  const groupRequired = writable(required);
83
+ let isInitialRender = true;
82
84
 
83
85
  /**
84
86
  * @type {(data: { checked: boolean; value: Value }) => void}
@@ -114,7 +116,13 @@
114
116
 
115
117
  selectedValue.subscribe((value) => {
116
118
  selected = value;
117
- dispatch("change", value);
119
+ if (!isInitialRender) {
120
+ dispatch("change", value);
121
+ }
122
+ });
123
+
124
+ afterUpdate(() => {
125
+ isInitialRender = false;
118
126
  });
119
127
 
120
128
  $: $groupName = name;