@winkintel/bootstrap-svelte 1.0.4 → 1.0.6

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.
Files changed (81) hide show
  1. package/README.md +1 -1
  2. package/dist/Alert/alert.svelte +4 -2
  3. package/dist/Badge/badge.svelte +6 -4
  4. package/dist/Badge/badge.svelte.d.ts +2 -2
  5. package/dist/Badge/types.d.ts +2 -1
  6. package/dist/Badge/types.js +3 -11
  7. package/dist/Button/button.svelte +29 -9
  8. package/dist/ButtonGroup/button-group.svelte +3 -2
  9. package/dist/ButtonGroup/button-toolbar.svelte +3 -2
  10. package/dist/ButtonGroup/types.contract.d.ts +1 -0
  11. package/dist/ButtonGroup/types.contract.js +15 -0
  12. package/dist/ButtonGroup/types.d.ts +2 -2
  13. package/dist/Carousel/carousel-indicator-button.svelte +8 -1
  14. package/dist/Carousel/carousel-item.svelte +29 -9
  15. package/dist/Carousel/carousel.svelte +12 -5
  16. package/dist/Carousel/carousel.svelte.js +200 -42
  17. package/dist/Col/col.svelte +30 -22
  18. package/dist/Collapse/collapse.svelte +3 -3
  19. package/dist/Collapse/transition.js +78 -15
  20. package/dist/Collapse/types.d.ts +1 -0
  21. package/dist/Dropdown/dropdown-item.svelte +7 -0
  22. package/dist/Dropdown/dropdown.svelte.js +66 -19
  23. package/dist/Form/form-check-input.svelte +6 -6
  24. package/dist/Form/form-color-input.svelte +8 -3
  25. package/dist/Form/form-date-input.svelte +4 -4
  26. package/dist/Form/form-datetime-local-input.svelte +4 -4
  27. package/dist/Form/form-email-input.svelte +4 -4
  28. package/dist/Form/form-file-input.svelte +46 -6
  29. package/dist/Form/form-file-input.svelte.d.ts +3 -2
  30. package/dist/Form/form-hidden-input.svelte +1 -1
  31. package/dist/Form/form-hidden-input.svelte.d.ts +1 -1
  32. package/dist/Form/form-image-input.svelte +1 -1
  33. package/dist/Form/form-image-input.svelte.d.ts +2 -1
  34. package/dist/Form/form-month-input.svelte +4 -4
  35. package/dist/Form/form-number-input.svelte +4 -4
  36. package/dist/Form/form-password-input.svelte +4 -4
  37. package/dist/Form/form-radio-input.svelte +53 -3
  38. package/dist/Form/form-radio-input.svelte.d.ts +3 -2
  39. package/dist/Form/form-range-input.svelte +1 -1
  40. package/dist/Form/form-range-input.svelte.d.ts +2 -1
  41. package/dist/Form/form-search-input.svelte +4 -4
  42. package/dist/Form/form-select.svelte +7 -7
  43. package/dist/Form/form-switch-input.svelte +4 -4
  44. package/dist/Form/form-telephone-input.svelte +4 -4
  45. package/dist/Form/form-text-area.svelte +4 -4
  46. package/dist/Form/form-text-input.svelte +4 -4
  47. package/dist/Form/form-time-input.svelte +4 -4
  48. package/dist/Form/form-url-input.svelte +4 -4
  49. package/dist/Form/form-week-Input.svelte +4 -4
  50. package/dist/Form/types.d.ts +26 -20
  51. package/dist/ListGroup/list-group-item-action.svelte +14 -7
  52. package/dist/Modal/modal-title.svelte +7 -7
  53. package/dist/Modal/modal.svelte +86 -26
  54. package/dist/Modal/modal.svelte.js +15 -0
  55. package/dist/Nav/nav-link.svelte +12 -8
  56. package/dist/Navbar/navbar-collapse.svelte +18 -7
  57. package/dist/Navbar/navbar-collapse.svelte.d.ts +1 -1
  58. package/dist/Navbar/navbar-nav.svelte +1 -1
  59. package/dist/Navbar/navbar-toggler.svelte +9 -7
  60. package/dist/Navbar/navbar-toggler.svelte.d.ts +1 -1
  61. package/dist/Navbar/types.d.ts +3 -2
  62. package/dist/Offcanvas/offcanvas.svelte +38 -7
  63. package/dist/Pagination/pagination-item.svelte +27 -20
  64. package/dist/Pagination/pagination-link.svelte +20 -5
  65. package/dist/Pagination/pagination.svelte.js +28 -8
  66. package/dist/Placeholder/placeholder-item.svelte +7 -2
  67. package/dist/Placeholder/placeholder-item.svelte.d.ts +1 -1
  68. package/dist/Placeholder/types.d.ts +1 -0
  69. package/dist/Portal/portal.svelte +6 -1
  70. package/dist/Progress/progress-bar.svelte +22 -9
  71. package/dist/Row/row.svelte +9 -10
  72. package/dist/Row/types.d.ts +8 -7
  73. package/dist/Scrollspy/scrollspy-attachment.svelte.js +47 -28
  74. package/dist/Tooltip/tooltip.svelte +57 -10
  75. package/dist/common/css.js +96 -24
  76. package/dist/common/navbar-offcanvas.svelte.d.ts +2 -0
  77. package/dist/common/navbar-offcanvas.svelte.js +5 -4
  78. package/dist/common/overlay-stack.d.ts +13 -0
  79. package/dist/common/overlay-stack.js +49 -0
  80. package/dist/common/scrollbar.js +24 -8
  81. package/package.json +3 -1
@@ -1,4 +1,6 @@
1
1
  import { Context } from '../common/index.js';
2
+ import { untrack } from 'svelte';
3
+ import { SvelteSet } from 'svelte/reactivity';
2
4
  import { Pagination } from './index.js';
3
5
  /**
4
6
  * Manages the state for the Pagination root component.
@@ -8,13 +10,15 @@ import { Pagination } from './index.js';
8
10
  export class PaginationRootState {
9
11
  props;
10
12
  #activePaginationItemId = $state('');
11
- #disabledPaginationItemId = $state('');
13
+ #disabledPaginationItemIds = new SvelteSet();
12
14
  constructor(props) {
13
15
  this.props = props;
14
16
  this.isPaginationItemActive = this.isPaginationItemActive.bind(this);
15
17
  this.setPaginationItemActive = this.setPaginationItemActive.bind(this);
18
+ this.removePaginationItemActive = this.removePaginationItemActive.bind(this);
16
19
  this.isPaginationItemDisabled = this.isPaginationItemDisabled.bind(this);
17
20
  this.setPaginationItemDisabled = this.setPaginationItemDisabled.bind(this);
21
+ this.removePaginationItemDisabled = this.removePaginationItemDisabled.bind(this);
18
22
  }
19
23
  isPaginationItemActive(itemId) {
20
24
  return this.#activePaginationItemId === itemId;
@@ -22,11 +26,29 @@ export class PaginationRootState {
22
26
  setPaginationItemActive(itemId) {
23
27
  this.#activePaginationItemId = itemId;
24
28
  }
29
+ removePaginationItemActive(itemId) {
30
+ untrack(() => {
31
+ if (this.#activePaginationItemId === itemId) {
32
+ this.#activePaginationItemId = '';
33
+ }
34
+ });
35
+ }
25
36
  isPaginationItemDisabled(itemId) {
26
- return this.#disabledPaginationItemId === itemId;
37
+ return this.#disabledPaginationItemIds.has(itemId);
27
38
  }
28
39
  setPaginationItemDisabled(itemId) {
29
- this.#disabledPaginationItemId = itemId;
40
+ untrack(() => {
41
+ if (itemId && !this.#disabledPaginationItemIds.has(itemId)) {
42
+ this.#disabledPaginationItemIds.add(itemId);
43
+ }
44
+ });
45
+ }
46
+ removePaginationItemDisabled(itemId) {
47
+ untrack(() => {
48
+ if (itemId && this.#disabledPaginationItemIds.has(itemId)) {
49
+ this.#disabledPaginationItemIds.delete(itemId);
50
+ }
51
+ });
30
52
  }
31
53
  }
32
54
  /**
@@ -37,17 +59,14 @@ export class PaginationRootState {
37
59
  export class PaginationItemState {
38
60
  props;
39
61
  root;
40
- isActive = $derived.by(() => this.root.isPaginationItemActive(this.props.id || ''));
41
- isDisabled = $derived.by(() => this.root.isPaginationItemDisabled(this.props.id || ''));
62
+ isActive = $derived.by(() => this.props.isActive || this.root.isPaginationItemActive(this.props.id || ''));
63
+ isDisabled = $derived.by(() => this.props.isDisabled || this.root.isPaginationItemDisabled(this.props.id || ''));
42
64
  constructor(props, root) {
43
65
  this.props = props;
44
66
  this.root = root;
45
67
  if (this.props.isActive) {
46
68
  this.root.setPaginationItemActive(this.props.id || '');
47
69
  }
48
- if (this.props.isDisabled) {
49
- this.root.setPaginationItemDisabled(this.props.id || '');
50
- }
51
70
  }
52
71
  }
53
72
  /**
@@ -59,6 +78,7 @@ export class PaginationLinkState {
59
78
  props;
60
79
  root;
61
80
  item;
81
+ isDisabled = $derived.by(() => this.item.isDisabled || this.props.isDisabled);
62
82
  constructor(props, root, item) {
63
83
  this.props = props;
64
84
  this.root = root;
@@ -40,14 +40,19 @@ A component that represents an individual placeholder element.
40
40
  - `colorVariant` (string): Optional. Color variant for the placeholder ('primary', 'success', etc.).
41
41
  - `elementRef` (HTMLSpanElement): Optional. Reference to the placeholder item DOM element.
42
42
  - `id` (string): Optional. ID for the placeholder item element.
43
+ - `isAnimated` (boolean): Deprecated compatibility no-op. Set `type` on `Placeholder.Root` to animate a placeholder group.
43
44
  - `size` ('xs' | 'sm' | 'lg'): Optional. Size of the placeholder.
44
- - `isAnimated` (boolean): Optional. Whether the placeholder should animate, defaults to true.
45
45
  -->
46
46
  <script lang="ts">import { uniqueClsx } from '../common/css.js';
47
47
  let { children, class: classValues, colorVariant, elementRef = $bindable(null), size, ...restOfProps } = $props();
48
+ let sanitizedRestOfProps = $derived.by(() => {
49
+ // Keep accepting the legacy no-op without forwarding it as an invalid DOM attribute.
50
+ const { isAnimated: _isAnimated, ...sanitizedProps } = restOfProps;
51
+ return sanitizedProps;
52
+ });
48
53
  let classes = $derived(uniqueClsx('placeholder', size && `placeholder-${size}`, colorVariant && `bg-${colorVariant}`, classValues));
49
54
  </script>
50
55
 
51
- <span bind:this={elementRef} class={classes} {...restOfProps}>
56
+ <span bind:this={elementRef} class={classes} {...sanitizedRestOfProps}>
52
57
  {@render children?.()}
53
58
  </span>
@@ -40,8 +40,8 @@ import type { Placeholder } from './index.js';
40
40
  * - `colorVariant` (string): Optional. Color variant for the placeholder ('primary', 'success', etc.).
41
41
  * - `elementRef` (HTMLSpanElement): Optional. Reference to the placeholder item DOM element.
42
42
  * - `id` (string): Optional. ID for the placeholder item element.
43
+ * - `isAnimated` (boolean): Deprecated compatibility no-op. Set `type` on `Placeholder.Root` to animate a placeholder group.
43
44
  * - `size` ('xs' | 'sm' | 'lg'): Optional. Size of the placeholder.
44
- * - `isAnimated` (boolean): Optional. Whether the placeholder should animate, defaults to true.
45
45
  */
46
46
  declare const PlaceholderItem: import("svelte").Component<Placeholder.ItemProps, {}, "elementRef">;
47
47
  type PlaceholderItem = ReturnType<typeof PlaceholderItem>;
@@ -7,6 +7,7 @@ export type PlaceholderRootProps = DivElement & {
7
7
  };
8
8
  export type PlaceholderItemProps = SpanElement & {
9
9
  colorVariant?: PlaceholderItemColorVariant;
10
+ /** @deprecated Placeholder animation belongs on Placeholder.Root. Retained as a no-op for compatibility. */
10
11
  isAnimated?: boolean;
11
12
  size?: PlaceholderItemSize;
12
13
  };
@@ -42,7 +42,12 @@ $effect(() => {
42
42
  return;
43
43
  }
44
44
  if (typeof target === 'string') {
45
- element = document.querySelector(target);
45
+ try {
46
+ element = document.querySelector(target);
47
+ }
48
+ catch {
49
+ element = null;
50
+ }
46
51
  }
47
52
  else {
48
53
  element = target;
@@ -36,7 +36,7 @@ Supports percentage-based progress visualization with customizable styling, them
36
36
  -->
37
37
  <script lang="ts">import { fromStyle, toStyle, uniqueClsx } from '../common/css.js';
38
38
  import { initProgressState, ProgressState } from './progress.svelte.js';
39
- let { ariaLabel = 'Progress Bar', backgroundColorVariant, children, class: classValues, barProps = {}, elementRef = $bindable(null), style, valueMax = 100, valueMin = 0, valueNow = 0, ...restOfProps } = $props();
39
+ let { 'aria-label': nativeAriaLabel, ariaLabel, backgroundColorVariant, children, class: classValues, barProps = {}, elementRef = $bindable(null), style, valueMax = 100, valueMin = 0, valueNow = 0, ...restOfProps } = $props();
40
40
  let { class: barClassValues, isAnimated = false, isStriped = false, style: barStyle, ...restOfBarProps } = $derived(barProps);
41
41
  // Initialize state and detect if this progress bar is part of a stacked container
42
42
  const barState = initProgressState({
@@ -47,6 +47,19 @@ const barState = initProgressState({
47
47
  // Determine where to apply width - when in a stacked layout, width is on the root element,
48
48
  // otherwise it's applied to the inner bar element
49
49
  let widthPlacement = $derived(barState.isStacked ? 'root' : 'bar');
50
+ let resolvedAriaLabel = $derived(ariaLabel ?? nativeAriaLabel ?? 'Progress Bar');
51
+ let normalizedProgress = $derived.by(() => {
52
+ if (!Number.isFinite(valueNow) || !Number.isFinite(valueMin) || !Number.isFinite(valueMax) || valueMax <= valueMin) {
53
+ return { max: 100, min: 0, now: 0, width: 0 };
54
+ }
55
+ const now = Math.min(Math.max(valueNow, valueMin), valueMax);
56
+ return {
57
+ max: valueMax,
58
+ min: valueMin,
59
+ now,
60
+ width: ((now - valueMin) / (valueMax - valueMin)) * 100
61
+ };
62
+ });
50
63
  // Root element...
51
64
  let classes = $derived(uniqueClsx('progress', classValues));
52
65
  // Conditionally apply width to the root element's style if this bar is part of a stacked layout
@@ -55,7 +68,7 @@ let styles = $derived.by(() => {
55
68
  let cssProps = fromStyle(style);
56
69
  return toStyle({
57
70
  ...cssProps,
58
- width: `${valueNow}%`
71
+ width: `${normalizedProgress.width}%`
59
72
  });
60
73
  }
61
74
  return style;
@@ -72,7 +85,7 @@ let barStyles = $derived.by(() => {
72
85
  let cssProps = fromStyle(barStyle);
73
86
  return toStyle({
74
87
  ...cssProps,
75
- width: `${valueNow}%`
88
+ width: `${normalizedProgress.width}%`
76
89
  });
77
90
  }
78
91
  return barStyle;
@@ -86,15 +99,15 @@ let theme = $derived.by(() => {
86
99
  </script>
87
100
 
88
101
  <div
89
- aria-label={ariaLabel}
90
- aria-valuemax={valueMax}
91
- aria-valuemin={valueMin}
92
- aria-valuenow={valueNow}
102
+ {...restOfProps}
103
+ aria-label={resolvedAriaLabel}
104
+ aria-valuemax={normalizedProgress.max}
105
+ aria-valuemin={normalizedProgress.min}
106
+ aria-valuenow={normalizedProgress.now}
93
107
  bind:this={elementRef}
94
108
  class={classes}
95
109
  role="progressbar"
96
- style={styles}
97
- {...restOfProps}>
110
+ style={styles}>
98
111
  <div class={barClasses} data-bs-theme={theme} style={barStyles} {...restOfBarProps}>
99
112
  {@render children?.()}
100
113
  </div>
@@ -43,25 +43,24 @@ A component that implements Bootstrap's grid row functionality.
43
43
  -->
44
44
  <script lang="ts">import { uniqueClsx } from '../common/css.js';
45
45
  let { children, class: classValues, cols, elementRef = $bindable(null), ...restOfProps } = $props();
46
+ const breakpoints = new Set(['xs', 'sm', 'md', 'lg', 'xl', 'xxl']);
47
+ const isRowColsValue = (value) => value === 'auto' || (typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 6);
46
48
  // Generate cols classes based on the cols prop
47
49
  let colsClasses = $derived.by(() => {
48
50
  // If cols is a string (e.g., 'auto'), return a single row-cols-auto class
49
- if (typeof cols === 'string' && cols === 'auto') {
50
- return [`row-cols-${cols}`];
51
- }
52
- // If cols is a number (OneToSix type), generate a simple row-cols-n class
53
- if (typeof cols === 'number') {
51
+ if (isRowColsValue(cols)) {
54
52
  return [`row-cols-${cols}`];
55
53
  }
56
54
  // If cols is an object (RowColsMap type), generate breakpoint-specific classes
57
55
  else if (cols && typeof cols === 'object') {
58
- return Object.entries(cols).map(([breakpoint, value]) => {
59
- // If the breakpoint is 'xs', return 'row-col-{value}'
56
+ return Object.entries(cols).flatMap(([breakpoint, value]) => {
57
+ if (!breakpoints.has(breakpoint) || !isRowColsValue(value)) {
58
+ return [];
59
+ }
60
60
  if (breakpoint === 'xs') {
61
- return `row-col-${value}`;
61
+ return [`row-cols-${value}`];
62
62
  }
63
- // Otherwise, return 'row-cols-{breakpoint}-{value}'
64
- return `row-cols-${breakpoint}-${value}`;
63
+ return [`row-cols-${breakpoint}-${value}`];
65
64
  });
66
65
  }
67
66
  // Default: no row-cols classes
@@ -1,12 +1,13 @@
1
1
  import type { DivElement, OneToSix } from '../common/types.js';
2
+ export type RowColsValue = OneToSix | 'auto';
2
3
  export type RowColsMap = {
3
- xs?: OneToSix;
4
- sm?: OneToSix;
5
- md?: OneToSix;
6
- lg?: OneToSix;
7
- xl?: OneToSix;
8
- xxl?: OneToSix;
4
+ xs?: RowColsValue;
5
+ sm?: RowColsValue;
6
+ md?: RowColsValue;
7
+ lg?: RowColsValue;
8
+ xl?: RowColsValue;
9
+ xxl?: RowColsValue;
9
10
  };
10
11
  export type RowRootProps = DivElement & {
11
- cols?: RowColsMap | OneToSix | 'auto';
12
+ cols?: RowColsMap | RowColsValue;
12
13
  };
@@ -1,5 +1,20 @@
1
1
  import { isDisabled } from '../common/dom.js';
2
- import { SvelteMap } from 'svelte/reactivity';
2
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
3
+ function resolveHashTarget(hash) {
4
+ const rawId = hash.startsWith('#') ? hash.slice(1) : hash;
5
+ if (!rawId) {
6
+ return null;
7
+ }
8
+ let decodedId = rawId;
9
+ try {
10
+ decodedId = decodeURIComponent(rawId);
11
+ }
12
+ catch {
13
+ // A literal percent sequence can be a valid element ID even when it is
14
+ // not a valid URI escape. Fall back to the exact fragment text.
15
+ }
16
+ return document.getElementById(decodedId) ?? (decodedId === rawId ? null : document.getElementById(rawId));
17
+ }
3
18
  /**
4
19
  * Creates a scrollspy attachment that monitors elements matching a target selector
5
20
  * and triggers a callback when they intersect with the scrolling container.
@@ -20,14 +35,26 @@ export function scrollspy(options) {
20
35
  throw new Error('Scrollspy: callback must be a function');
21
36
  }
22
37
  // Get the target element from options targetSelector...
23
- const targetElement = document.querySelector(options.targetSelector);
38
+ let targetElement;
39
+ try {
40
+ targetElement = document.querySelector(options.targetSelector);
41
+ }
42
+ catch {
43
+ throw new Error(`Scrollspy: invalid target selector "${options.targetSelector}"`);
44
+ }
24
45
  if (!targetElement) {
25
46
  throw new Error(`Scrollspy: target element not found for selector "${options.targetSelector}"`);
26
47
  }
48
+ const observerOptions = {
49
+ root: element,
50
+ rootMargin: '0px 0px -25%',
51
+ threshold: [0.1, 0.5, 1],
52
+ ...options.observerOptions
53
+ };
27
54
  // Smooth scroll functionality
28
55
  const handleClick = (event) => {
29
56
  const target = event.target;
30
- if (!(target instanceof HTMLElement)) {
57
+ if (!(target instanceof Element)) {
31
58
  return;
32
59
  }
33
60
  // Only handle clicks on anchor links with hash
@@ -36,32 +63,32 @@ export function scrollspy(options) {
36
63
  return;
37
64
  }
38
65
  // Find the target element that corresponds to the clicked link's hash
39
- const targetElement = element.querySelector(anchor.hash);
40
- if (targetElement) {
66
+ const targetElement = resolveHashTarget(anchor.hash);
67
+ if (targetElement && element.contains(targetElement)) {
41
68
  event.preventDefault();
42
- // Get the scroll container (root element or window)
43
- const root = options.observerOptions?.root || element;
69
+ // A null observer root represents the viewport/window.
70
+ const root = observerOptions.root;
71
+ const scrollContainer = root instanceof HTMLElement ? root : null;
44
72
  // Calculate the scroll position
45
73
  const elementRect = targetElement.getBoundingClientRect();
46
- const containerRect = root.getBoundingClientRect();
47
74
  let scrollTop;
48
- if (root === element || root instanceof HTMLElement) {
75
+ if (scrollContainer) {
49
76
  // Scrolling within a container element
50
77
  // Cast targetElement to HTMLElement to use offsetTop
51
78
  scrollTop = targetElement.offsetTop - element.offsetTop;
52
79
  }
53
80
  else {
54
81
  // Scrolling the window
55
- scrollTop = elementRect.top + window.pageYOffset - containerRect.top;
82
+ scrollTop = elementRect.top + window.pageYOffset;
56
83
  }
57
84
  // Perform smooth scroll
58
- if (root === element || root instanceof HTMLElement) {
59
- if (root.scrollTo) {
60
- root.scrollTo({ top: scrollTop, behavior: 'smooth' });
85
+ if (scrollContainer) {
86
+ if (scrollContainer.scrollTo) {
87
+ scrollContainer.scrollTo({ top: scrollTop, behavior: 'smooth' });
61
88
  }
62
89
  else {
63
90
  // Fallback for older browsers
64
- root.scrollTop = scrollTop;
91
+ scrollContainer.scrollTop = scrollTop;
65
92
  }
66
93
  }
67
94
  else {
@@ -78,7 +105,7 @@ export function scrollspy(options) {
78
105
  }
79
106
  };
80
107
  // Maps to store links and their target sections
81
- const targetLinks = new SvelteMap();
108
+ const targetLinks = new SvelteSet();
82
109
  const observableSections = new SvelteMap();
83
110
  // Find all the anchor links within the target element...
84
111
  const anchors = targetElement.querySelectorAll('[href]');
@@ -87,24 +114,16 @@ export function scrollspy(options) {
87
114
  if (!anchor.hash || ['#', '#!'].includes(anchor.hash) || isDisabled(anchor)) {
88
115
  continue;
89
116
  }
90
- const observableSection = document.querySelector(decodeURI(anchor.hash));
91
- if (observableSection !== null) {
92
- targetLinks.set(decodeURI(anchor.hash), anchor);
117
+ const observableSection = resolveHashTarget(anchor.hash);
118
+ if (observableSection !== null && element.contains(observableSection)) {
119
+ targetLinks.add(anchor);
93
120
  observableSections.set(anchor.hash, observableSection);
94
121
  // attach the click handler to the anchor
95
122
  anchor.addEventListener('click', handleClick);
96
123
  }
97
124
  }
98
- // Set default options for the IntersectionObserver...
99
- const defaultBootstrapObserverOptions = {
100
- root: element,
101
- rootMargin: '0px 0px -25%',
102
- threshold: [0.1, 0.5, 1]
103
- };
104
- // Ensure observerOptions exists, create it if it doesn't
105
- options.observerOptions = { ...defaultBootstrapObserverOptions, ...options.observerOptions };
106
125
  // Create an IntersectionObserver that will invoke the callback when targets intersect
107
- const observer = new IntersectionObserver(options.callback, options.observerOptions);
126
+ const observer = new IntersectionObserver(options.callback, observerOptions);
108
127
  // Start observing each target element for intersection events
109
128
  for (const section of observableSections.values()) {
110
129
  observer.observe(section);
@@ -112,7 +131,7 @@ export function scrollspy(options) {
112
131
  // Return cleanup function that will be called when the attachment is removed
113
132
  return () => {
114
133
  // Remove click event listener
115
- for (const anchor of targetLinks.values()) {
134
+ for (const anchor of targetLinks) {
116
135
  anchor.removeEventListener('click', handleClick);
117
136
  }
118
137
  // Disconnect the observer to prevent memory leaks
@@ -66,6 +66,9 @@ let previousTrigger = unset;
66
66
  let popperInstance = null;
67
67
  let popperPlacement = $state('');
68
68
  let referenceElement = $state(null);
69
+ let isClickActive = $state(false);
70
+ let isHovering = $state(false);
71
+ let isFocusing = $state(false);
69
72
  let currentPopperPlacement = $derived(popperPlacement || effectivePlacement || 'top');
70
73
  let triggers = $derived.by(() => effectiveTrigger.split(' ')); // support space-delimited list of triggers
71
74
  let classes = $derived(uniqueClsx(type, {
@@ -78,14 +81,41 @@ let classes = $derived(uniqueClsx(type, {
78
81
  show: isShown
79
82
  }, classValues));
80
83
  // Operations...
81
- const hide = () => (isShown = false);
82
84
  const show = () => (isShown = true);
85
+ const hide = () => (isShown = false);
86
+ const hideWhenInactive = () => {
87
+ if (!isClickActive && !isHovering && !isFocusing) {
88
+ hide();
89
+ }
90
+ };
83
91
  const toggle = () => {
84
92
  if (triggers.includes('focus') && hasFocus(referenceElement)) {
85
93
  // If the reference element is focused, we don't want to toggle the tooltip
86
94
  return;
87
95
  }
88
- isShown = !isShown;
96
+ isClickActive = !isClickActive;
97
+ if (isClickActive) {
98
+ show();
99
+ }
100
+ else {
101
+ hideWhenInactive();
102
+ }
103
+ };
104
+ const handleMouseEnter = () => {
105
+ isHovering = true;
106
+ show();
107
+ };
108
+ const handleMouseLeave = () => {
109
+ isHovering = false;
110
+ hideWhenInactive();
111
+ };
112
+ const handleFocusIn = () => {
113
+ isFocusing = true;
114
+ show();
115
+ };
116
+ const handleFocusOut = () => {
117
+ isFocusing = false;
118
+ hideWhenInactive();
89
119
  };
90
120
  const addEventListeners = () => {
91
121
  if (!referenceElement) {
@@ -98,12 +128,12 @@ const addEventListeners = () => {
98
128
  referenceElement.addEventListener('click', toggle);
99
129
  }
100
130
  if (triggers.includes('hover')) {
101
- referenceElement.addEventListener('mouseenter', show);
102
- referenceElement.addEventListener('mouseleave', hide);
131
+ referenceElement.addEventListener('mouseenter', handleMouseEnter);
132
+ referenceElement.addEventListener('mouseleave', handleMouseLeave);
103
133
  }
104
134
  if (triggers.includes('focus')) {
105
- referenceElement.addEventListener('focusin', show);
106
- referenceElement.addEventListener('focusout', hide);
135
+ referenceElement.addEventListener('focusin', handleFocusIn);
136
+ referenceElement.addEventListener('focusout', handleFocusOut);
107
137
  }
108
138
  };
109
139
  const destroyPopper = () => {
@@ -118,10 +148,26 @@ const removeEventListeners = () => {
118
148
  return;
119
149
  }
120
150
  referenceElement.removeEventListener('click', toggle);
121
- referenceElement.removeEventListener('mouseenter', show);
122
- referenceElement.removeEventListener('mouseleave', hide);
123
- referenceElement.removeEventListener('focusin', show);
124
- referenceElement.removeEventListener('focusout', hide);
151
+ referenceElement.removeEventListener('mouseenter', handleMouseEnter);
152
+ referenceElement.removeEventListener('mouseleave', handleMouseLeave);
153
+ referenceElement.removeEventListener('focusin', handleFocusIn);
154
+ referenceElement.removeEventListener('focusout', handleFocusOut);
155
+ };
156
+ const reconcileTriggerActivity = () => {
157
+ isClickActive = triggers.includes('click') && isClickActive;
158
+ isHovering = triggers.includes('hover') && !!referenceElement?.matches(':hover');
159
+ if (triggers.includes('focus')) {
160
+ isFocusing = hasFocus(referenceElement);
161
+ }
162
+ else {
163
+ isFocusing = false;
164
+ }
165
+ if (isClickActive || isHovering || isFocusing) {
166
+ show();
167
+ }
168
+ else {
169
+ hide();
170
+ }
125
171
  };
126
172
  // Event listeners...
127
173
  const handleOnHidden = (event) => {
@@ -149,6 +195,7 @@ onMount(() => {
149
195
  $effect(() => {
150
196
  if (previousTrigger !== unset && previousTrigger !== effectiveTrigger) {
151
197
  removeEventListeners();
198
+ reconcileTriggerActivity();
152
199
  addEventListeners();
153
200
  }
154
201
  previousTrigger = effectiveTrigger;