@txstate-mws/svelte-components 1.5.1 → 1.5.3

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.
@@ -4,6 +4,7 @@ export interface GlueArgs<T extends GlueAlignStore = GlueAlignStore> {
4
4
  target: HTMLElement;
5
5
  align?: GlueAlignOpts;
6
6
  cover?: boolean;
7
+ gap?: number;
7
8
  adjustparentheight?: boolean;
8
9
  store?: SettableSubject<T>;
9
10
  }
@@ -11,7 +12,7 @@ export interface GlueAlignStore {
11
12
  valign: 'bottom' | 'top';
12
13
  halign: 'left' | 'right';
13
14
  }
14
- export declare function glue(el: HTMLElement, { target, align, cover, adjustparentheight, store }: GlueArgs): {
15
- update({ target: utarget, align: ualign, cover: ucover, store: ustore }: GlueArgs): void;
15
+ export declare function glue(el: HTMLElement, { target, align, cover, gap, adjustparentheight, store }: GlueArgs): {
16
+ update({ target: utarget, align: ualign, cover: ucover, gap: ugap, store: ustore }: GlueArgs): void;
16
17
  destroy(): void;
17
18
  };
@@ -1,6 +1,6 @@
1
1
  import { watchForMutations } from '../util/index.js';
2
2
  import { equal } from 'txstate-utils';
3
- export function glue(el, { target, align = 'auto', cover = false, adjustparentheight = false, store }) {
3
+ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjustparentheight = false, store }) {
4
4
  let halign;
5
5
  let valign;
6
6
  const parent = el.offsetParent;
@@ -34,7 +34,7 @@ export function glue(el, { target, align = 'auto', cover = false, adjustparenthe
34
34
  const targetHeight = rect.height;
35
35
  if (autoalign === 'bottomleft') {
36
36
  requestAnimationFrame(() => {
37
- el.style.top = `${rect.top + (cover ? 0 : targetHeight)}px`;
37
+ el.style.top = `${rect.top + (cover ? 0 : targetHeight) + gap}px`;
38
38
  el.style.left = `${rect.left}px`;
39
39
  el.style.bottom = '';
40
40
  el.style.right = '';
@@ -45,7 +45,7 @@ export function glue(el, { target, align = 'auto', cover = false, adjustparenthe
45
45
  }
46
46
  else if (autoalign === 'bottomright') {
47
47
  requestAnimationFrame(() => {
48
- el.style.top = `${rect.top + (cover ? 0 : targetHeight)}px`;
48
+ el.style.top = `${rect.top + (cover ? 0 : targetHeight) + gap}px`;
49
49
  el.style.left = '';
50
50
  el.style.bottom = '';
51
51
  el.style.right = `${rect.right}px`;
@@ -58,7 +58,7 @@ export function glue(el, { target, align = 'auto', cover = false, adjustparenthe
58
58
  requestAnimationFrame(() => {
59
59
  el.style.top = '';
60
60
  el.style.left = `${rect.left}px`;
61
- el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight)}px`;
61
+ el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight) + gap}px`;
62
62
  el.style.right = '';
63
63
  adjustParentHeight();
64
64
  });
@@ -69,7 +69,7 @@ export function glue(el, { target, align = 'auto', cover = false, adjustparenthe
69
69
  requestAnimationFrame(() => {
70
70
  el.style.top = '';
71
71
  el.style.left = '';
72
- el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight)}px`;
72
+ el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight) + gap}px`;
73
73
  el.style.right = `${rect.right}px`;
74
74
  adjustParentHeight();
75
75
  });
@@ -81,9 +81,10 @@ export function glue(el, { target, align = 'auto', cover = false, adjustparenthe
81
81
  const { destroy: watchDestroy } = watchForMutations(reposition);
82
82
  document.addEventListener('scroll', reposition, { capture: true });
83
83
  return {
84
- update({ target: utarget, align: ualign = 'auto', cover: ucover = false, store: ustore }) {
84
+ update({ target: utarget, align: ualign = 'auto', cover: ucover = false, gap: ugap = 0, store: ustore }) {
85
85
  align = ualign;
86
86
  cover = ucover;
87
+ gap = ugap;
87
88
  if (ustore && ustore !== store) {
88
89
  ustore.update(v => ({ ...v, valign, halign }));
89
90
  store = ustore;
@@ -6,11 +6,12 @@
6
6
  controlled by the parent component via the `getOptions` function that will be used as a
7
7
  debounced callback on the contents of the text input.
8
8
  -->
9
- <script>import { createEventDispatcher } from 'svelte';
9
+ <script>import { createEventDispatcher, onMount } from 'svelte';
10
10
  import { randomid, Cache } from 'txstate-utils';
11
11
  import ScreenReaderOnly from './ScreenReaderOnly.svelte';
12
12
  import DefaultPopupMenu from './PopupMenu.svelte';
13
13
  import { modifierKey, selectionIsLeft } from '../util';
14
+ import { SafeStore } from '@txstate-mws/svelte-store';
14
15
  export let name;
15
16
  /**
16
17
  * Function to pass to the component that tells it how to use the text in the text input to determine what
@@ -51,7 +52,9 @@ const dispatch = createEventDispatcher();
51
52
  const optionsCache = new Cache(async (ipt) => {
52
53
  return await getOptions(ipt);
53
54
  }, { freshseconds: 5 });
54
- $: selectedSet = new Set(selected.map(s => s.value));
55
+ const selectedStore = new SafeStore(selected);
56
+ $: $selectedStore = selected;
57
+ $: selectedSet = new Set($selectedStore.map(s => s.value));
55
58
  // Stop showing our menu if the maxSelections limit has been reached.
56
59
  $: if (maxSelections > 1 && selected.length >= maxSelections && menushown)
57
60
  menushown = false;
@@ -137,6 +140,12 @@ function reactToHilite(..._) {
137
140
  inputelement.removeAttribute('aria-activedescendant');
138
141
  }
139
142
  $: reactToHilite(hilitedpill, id);
143
+ let gap = 0;
144
+ onMount(() => {
145
+ const ul = inputelement.closest('.multiselect-selected');
146
+ if (ul != null)
147
+ gap = ((ul.offsetHeight - ul.clientHeight) / 2) + parseInt(window.getComputedStyle(ul).paddingBottom);
148
+ });
140
149
  </script>
141
150
 
142
151
  <fieldset>
@@ -166,7 +175,7 @@ $: reactToHilite(hilitedpill, id);
166
175
  <slot></slot>
167
176
  </fieldset>
168
177
  <svelte:component this={PopupMenu} bind:menushown bind:hilited={popuphilited} bind:value={popupvalue} align='bottomleft'
169
- {usePortal} {loading} {emptyText}
178
+ {usePortal} {loading} {emptyText} {gap}
170
179
  {menuContainerClass} {menuClass} {menuItemClass} {menuItemHilitedClass} {menuDividerClass}
171
180
  items={options} buttonelement={inputelement}
172
181
  on:change={addSelection}/>
@@ -7,7 +7,7 @@ import { isNotBlank, randomid } from 'txstate-utils';
7
7
  import { Store } from '@txstate-mws/svelte-store';
8
8
  import { glue, portal } from '../actions';
9
9
  import ScreenReaderOnly from './ScreenReaderOnly.svelte';
10
- import { modifierKey } from '../util';
10
+ import { getScrollParents, modifierKey } from '../util';
11
11
  const dispatch = createEventDispatcher();
12
12
  /** The DOM element that will act as the "button" for this menu. The menu will be placed next to the
13
13
  button and be controlled by the button. Keyboard access will be enabled when the button has focus.
@@ -34,6 +34,8 @@ of when this is useful is when you are trying to round corners of your button an
34
34
  menu is above or below your button so you know which corners to round and which to leave square. Note that
35
35
  the values do not update when the menu is hidden, so you may need to rely on `menushown` as well. */
36
36
  export let computedalign = new Store({ valign: 'bottom', halign: 'left' });
37
+ /** Create some spacing between the specified button element and the menu, in pixels */
38
+ export let gap = 0;
37
39
  /** Set to `true` if you want the parent element's styling to increase its minHeight when necessary. Useful
38
40
  for parent elements not styled for variable size children and which you want override the the size of. */
39
41
  export let adjustparentheight = false;
@@ -201,6 +203,9 @@ async function onblur(e) {
201
203
  }
202
204
  }
203
205
  function cleanup(element) {
206
+ for (const observer of observers)
207
+ observer.disconnect();
208
+ observers = [];
204
209
  if (element) {
205
210
  element.removeEventListener('click', onbuttonclick);
206
211
  element.removeEventListener('keydown', onkeydown);
@@ -215,6 +220,7 @@ function cleanup(element) {
215
220
  onDestroy(() => cleanup(buttonelement));
216
221
  // if buttonelement changes we need to handle listeners and aria
217
222
  let lastbuttonelement;
223
+ let observers = [];
218
224
  function reactToButtonElement(buttonelement) {
219
225
  cleanup(lastbuttonelement);
220
226
  lastbuttonelement = buttonelement;
@@ -223,6 +229,17 @@ function reactToButtonElement(buttonelement) {
223
229
  buttonelement.addEventListener('click', onbuttonclick);
224
230
  buttonelement.addEventListener('keydown', onkeydown);
225
231
  buttonelement.addEventListener('blur', onblur);
232
+ const parents = getScrollParents(buttonelement);
233
+ for (const parent of [...parents, undefined]) {
234
+ const observer = new IntersectionObserver((entries) => {
235
+ for (const entry of entries) {
236
+ if (entry.intersectionRatio < 1)
237
+ menushown = false;
238
+ }
239
+ }, { root: parent, threshold: 1 });
240
+ observer.observe(buttonelement);
241
+ observers.push(observer);
242
+ }
226
243
  }
227
244
  }
228
245
  $: reactToButtonElement(buttonelement);
@@ -240,7 +257,7 @@ $: hasSelected = showSelected && items.some(itm => 'value' in itm && itm.value =
240
257
 
241
258
  {#if menushown}
242
259
  <div use:portal={usePortal === true ? undefined : (usePortal || null)}
243
- use:glue={{ target: buttonelement, align, cover, adjustparentheight, store: computedalign }}
260
+ use:glue={{ target: buttonelement, align, cover, gap, adjustparentheight, store: computedalign }}
244
261
  class={menuContainerClass}>
245
262
  <ul bind:this={menuelement} id={menuid} role='listbox' style={width ? `width: ${width}` : ''}
246
263
  class={menuClass} class:hasSelected class:defaultmenu={!menuClass && !menuContainerClass}
@@ -22,6 +22,7 @@ declare const __propDef: {
22
22
  of when this is useful is when you are trying to round corners of your button and need to know whether the
23
23
  menu is above or below your button so you know which corners to round and which to leave square. Note that
24
24
  the values do not update when the menu is hidden, so you may need to rely on `menushown` as well. */ computedalign?: Store<GlueAlignStore> | undefined;
25
+ /** Create some spacing between the specified button element and the menu, in pixels */ gap?: number | undefined;
25
26
  /** Set to `true` if you want the parent element's styling to increase its minHeight when necessary. Useful
26
27
  for parent elements not styled for variable size children and which you want override the the size of. */ adjustparentheight?: boolean | undefined;
27
28
  /** If the menu would be clipped by an `overflow: hidden`, you can set this prop and it will be placed in
@@ -2,3 +2,4 @@ export declare function classes(...classNames: (string | undefined)[]): string;
2
2
  export declare function debounced(fn: (..._: any[]) => any, timeout: number): (...args: any[]) => void;
3
3
  export declare function modifierKey(e: any): any;
4
4
  export declare function selectionIsLeft(ele: HTMLInputElement | HTMLTextAreaElement): boolean;
5
+ export declare function getScrollParents(element: HTMLElement): any;
package/dist/util/util.js CHANGED
@@ -14,3 +14,18 @@ export function modifierKey(e) {
14
14
  export function selectionIsLeft(ele) {
15
15
  return ele.selectionStart === 0 && ele.selectionEnd === 0;
16
16
  }
17
+ export function getScrollParents(element) {
18
+ let style = getComputedStyle(element);
19
+ const excludeStaticParent = style.position === 'absolute';
20
+ if (style.position === 'fixed')
21
+ return [document.body];
22
+ for (let parent = element; (parent = parent.parentElement);) {
23
+ style = getComputedStyle(parent);
24
+ if (excludeStaticParent && style.position === 'static') {
25
+ continue;
26
+ }
27
+ if (/(auto|scroll|hidden)/.test(style.overflow + style.overflowY + style.overflowX))
28
+ return [parent, ...getScrollParents(parent)];
29
+ }
30
+ return [document.body];
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@txstate-mws/svelte-components",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "Svelte components that are generically useful.",
5
5
  "scripts": {
6
6
  "prepublishOnly": "svelte-package",