@txstate-mws/svelte-components 1.4.8 → 1.5.1

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.
@@ -65,7 +65,7 @@ class ElementQueries {
65
65
  const m = document.location.hash.match(/^#([a-z][\w:.-]*)$/i);
66
66
  if (m) {
67
67
  const id = m[1];
68
- requestAnimationFrame(() => { document.getElementById(id) || document.querySelector(`[name="${id}"]`).scrollIntoView(true); });
68
+ requestAnimationFrame(() => { (document.getElementById(id) || document.querySelector(`[name="${id}"]`))?.scrollIntoView(true); });
69
69
  }
70
70
  }
71
71
  fullresync() {
@@ -1,43 +1,41 @@
1
- import { sharedOffsetParent, targetOffset, watchForPositionChangeInContainer } from '../util/index.js';
2
- let setBodyRelative = false;
1
+ import { watchForMutations } from '../util/index.js';
2
+ import { equal } from 'txstate-utils';
3
3
  export function glue(el, { target, align = 'auto', cover = false, adjustparentheight = false, store }) {
4
- if (!setBodyRelative) {
5
- setBodyRelative = true;
6
- document.body.style.position = 'relative';
7
- }
8
4
  let halign;
9
5
  let valign;
10
6
  const parent = el.offsetParent;
11
7
  const formerMinHeight = parent.style.minHeight;
12
- let sharedOffset = sharedOffsetParent(el, target);
13
- function reposition(offset) {
14
- if (!target || !sharedOffset || !(el.offsetParent instanceof HTMLElement))
8
+ if (target)
9
+ el.style.position = 'fixed';
10
+ function adjustParentHeight() {
11
+ if (!adjustparentheight)
12
+ return;
13
+ if (parent.style.overflowY === 'auto')
14
+ return;
15
+ const minHeight = el.offsetTop + el.offsetHeight;
16
+ if (minHeight > parent.clientHeight) {
17
+ requestAnimationFrame(() => {
18
+ parent.style.minHeight = String(minHeight) + 'px';
19
+ });
20
+ }
21
+ }
22
+ let lastrect;
23
+ function reposition() {
24
+ const tmpRect = target.getBoundingClientRect();
25
+ const rect = { left: tmpRect.left, right: window.innerWidth - tmpRect.right, top: tmpRect.top, bottom: window.innerHeight - tmpRect.bottom, width: tmpRect.width, height: tmpRect.height };
26
+ if (equal(rect, lastrect))
15
27
  return;
16
28
  let autoalign = align;
17
- const parentoffset = el.offsetParent === sharedOffset && target !== sharedOffset ? { top: 0, left: 0, bottom: 0, right: 0 } : targetOffset(el.offsetParent, sharedOffset);
18
29
  if (align === 'auto') {
19
- const rect = target.getBoundingClientRect();
20
- const leftright = window.innerWidth - rect.right > rect.left ? 'left' : 'right';
21
- const topbottom = window.innerHeight - rect.bottom > rect.top ? 'bottom' : 'top';
30
+ const leftright = rect.right > rect.left ? 'left' : 'right';
31
+ const topbottom = rect.bottom > rect.top ? 'bottom' : 'top';
22
32
  autoalign = topbottom + leftright;
23
33
  }
24
- function adjustParentHeight() {
25
- if (!adjustparentheight)
26
- return;
27
- if (parent.style.overflowY === 'auto')
28
- return;
29
- const minHeight = el.offsetTop + el.offsetHeight;
30
- if (minHeight > parent.clientHeight) {
31
- requestAnimationFrame(() => {
32
- parent.style.minHeight = String(minHeight) + 'px';
33
- });
34
- }
35
- }
36
- const targetHeight = target.offsetHeight;
34
+ const targetHeight = rect.height;
37
35
  if (autoalign === 'bottomleft') {
38
36
  requestAnimationFrame(() => {
39
- el.style.top = `${offset.top + (cover ? 0 : targetHeight) - parentoffset.top}px`;
40
- el.style.left = `${offset.left - parentoffset.left}px`;
37
+ el.style.top = `${rect.top + (cover ? 0 : targetHeight)}px`;
38
+ el.style.left = `${rect.left}px`;
41
39
  el.style.bottom = '';
42
40
  el.style.right = '';
43
41
  adjustParentHeight();
@@ -47,10 +45,10 @@ export function glue(el, { target, align = 'auto', cover = false, adjustparenthe
47
45
  }
48
46
  else if (autoalign === 'bottomright') {
49
47
  requestAnimationFrame(() => {
50
- el.style.top = `${offset.top + (cover ? 0 : targetHeight) - parentoffset.top}px`;
48
+ el.style.top = `${rect.top + (cover ? 0 : targetHeight)}px`;
51
49
  el.style.left = '';
52
50
  el.style.bottom = '';
53
- el.style.right = `${offset.right - parentoffset.right}px`;
51
+ el.style.right = `${rect.right}px`;
54
52
  adjustParentHeight();
55
53
  });
56
54
  valign = 'bottom';
@@ -59,8 +57,8 @@ export function glue(el, { target, align = 'auto', cover = false, adjustparenthe
59
57
  else if (autoalign === 'topleft') {
60
58
  requestAnimationFrame(() => {
61
59
  el.style.top = '';
62
- el.style.left = `${offset.left - parentoffset.left}px`;
63
- el.style.bottom = `${offset.bottom + (cover ? 0 : targetHeight) - parentoffset.bottom}px`;
60
+ el.style.left = `${rect.left}px`;
61
+ el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight)}px`;
64
62
  el.style.right = '';
65
63
  adjustParentHeight();
66
64
  });
@@ -71,8 +69,8 @@ export function glue(el, { target, align = 'auto', cover = false, adjustparenthe
71
69
  requestAnimationFrame(() => {
72
70
  el.style.top = '';
73
71
  el.style.left = '';
74
- el.style.bottom = `${offset.bottom + (cover ? 0 : targetHeight) - parentoffset.bottom}px`;
75
- el.style.right = `${offset.right - parentoffset.right}px`;
72
+ el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight)}px`;
73
+ el.style.right = `${rect.right}px`;
76
74
  adjustParentHeight();
77
75
  });
78
76
  valign = 'top';
@@ -80,7 +78,8 @@ export function glue(el, { target, align = 'auto', cover = false, adjustparenthe
80
78
  }
81
79
  store?.update(v => ({ ...v, valign, halign }));
82
80
  }
83
- const { destroy: watchDestroy, update } = watchForPositionChangeInContainer(target, sharedOffset, reposition);
81
+ const { destroy: watchDestroy } = watchForMutations(reposition);
82
+ document.addEventListener('scroll', reposition, { capture: true });
84
83
  return {
85
84
  update({ target: utarget, align: ualign = 'auto', cover: ucover = false, store: ustore }) {
86
85
  align = ualign;
@@ -90,13 +89,16 @@ export function glue(el, { target, align = 'auto', cover = false, adjustparenthe
90
89
  store = ustore;
91
90
  }
92
91
  if (target !== utarget) {
93
- sharedOffset = sharedOffsetParent(el, utarget);
94
92
  target = utarget;
95
- update(target, sharedOffset, reposition);
93
+ if (!target)
94
+ el.style.removeProperty('position');
95
+ else
96
+ el.style.position = 'fixed';
96
97
  }
97
98
  },
98
99
  destroy() {
99
100
  watchDestroy();
101
+ document.removeEventListener('scroll', reposition);
100
102
  if (adjustparentheight) {
101
103
  if (formerMinHeight)
102
104
  parent.style.minHeight = formerMinHeight;
@@ -16,5 +16,5 @@ export declare class ResizeStore extends Store<ElementSize> {
16
16
  export declare function resize(el: HTMLElement, config?: ResizeConfig): {
17
17
  update(newConfig?: ResizeConfig): void;
18
18
  destroy(): void;
19
- };
19
+ } | undefined;
20
20
  export {};
@@ -2,8 +2,8 @@ import { SvelteComponentTyped } from "svelte";
2
2
  import { type HTMLActionEntry } from '../actions';
3
3
  declare const __propDef: {
4
4
  props: {
5
- className?: string;
6
- use?: HTMLActionEntry[];
5
+ className?: string | undefined;
6
+ use?: HTMLActionEntry[] | undefined;
7
7
  };
8
8
  events: {
9
9
  [evt: string]: CustomEvent<any>;
@@ -176,7 +176,7 @@ function hardrecalc() {
176
176
  triggerrecalc(layoutelement.clientWidth);
177
177
  }
178
178
  function onResize(e) {
179
- triggerrecalc(e.detail.clientWidth);
179
+ triggerrecalc(e.detail.clientWidth ?? 0);
180
180
  }
181
181
  onMount(() => triggerrecalc(layoutelement.clientWidth));
182
182
  </script>
@@ -2,11 +2,11 @@ import { SvelteComponentTyped } from "svelte";
2
2
  import { type HTMLActionEntry } from '../actions';
3
3
  declare const __propDef: {
4
4
  props: {
5
- maxwidth?: number;
6
- preserveorder?: boolean;
7
- gutter?: number;
8
- className?: string;
9
- use?: HTMLActionEntry[];
5
+ maxwidth?: number | undefined;
6
+ preserveorder?: boolean | undefined;
7
+ gutter?: number | undefined;
8
+ className?: string | undefined;
9
+ use?: HTMLActionEntry[] | undefined;
10
10
  };
11
11
  events: {
12
12
  [evt: string]: CustomEvent<any>;
@@ -3,21 +3,21 @@ import type { CollapsingTableColumn } from '../types';
3
3
  import DefaultPopupMenu from './PopupMenu.svelte';
4
4
  declare const __propDef: {
5
5
  props: {
6
- tableClass?: string;
7
- bodyRowClass?: string;
8
- bodyCellClass?: string;
9
- headerRowClass?: string;
10
- headerCellClass?: string;
11
- menuContainerClass?: string;
12
- menuClass?: string;
13
- menuItemClass?: string;
14
- menuItemHilitedClass?: string;
6
+ tableClass?: string | undefined;
7
+ bodyRowClass?: string | undefined;
8
+ bodyCellClass?: string | undefined;
9
+ headerRowClass?: string | undefined;
10
+ headerCellClass?: string | undefined;
11
+ menuContainerClass?: string | undefined;
12
+ menuClass?: string | undefined;
13
+ menuItemClass?: string | undefined;
14
+ menuItemHilitedClass?: string | undefined;
15
15
  items: any[];
16
16
  config?: CollapsingTableColumn[] | undefined;
17
- defaultCellWidth?: number;
18
- PopupMenu?: typeof DefaultPopupMenu;
19
- slots?: Record<string, boolean>;
20
- stickyheader?: boolean;
17
+ defaultCellWidth?: number | undefined;
18
+ PopupMenu?: typeof DefaultPopupMenu | undefined;
19
+ slots?: Record<string, boolean> | undefined;
20
+ stickyheader?: boolean | undefined;
21
21
  };
22
22
  events: {
23
23
  [evt: string]: CustomEvent<any>;
@@ -25,19 +25,19 @@ declare const __propDef: {
25
25
  slots: {
26
26
  headercell: {
27
27
  column: CollapsingTableColumn & {
28
- widthPercent?: number;
28
+ widthPercent?: number | undefined;
29
29
  };
30
30
  key: string;
31
31
  title: string;
32
- item: any;
33
- value: any;
32
+ item: undefined;
33
+ value: undefined;
34
34
  };
35
35
  dropicon: {
36
36
  column: CollapsingTableColumn & {
37
- widthPercent?: number;
37
+ widthPercent?: number | undefined;
38
38
  };
39
- item: any;
40
- value: any;
39
+ item: undefined;
40
+ value: undefined;
41
41
  };
42
42
  default: {
43
43
  key: string;
@@ -45,9 +45,9 @@ declare const __propDef: {
45
45
  item: any;
46
46
  };
47
47
  footer: {
48
- item: any;
49
- key: any;
50
- value: any;
48
+ item: undefined;
49
+ key: undefined;
50
+ value: undefined;
51
51
  };
52
52
  };
53
53
  };
@@ -4,8 +4,8 @@ declare const __propDef: {
4
4
  [x: string]: any;
5
5
  condition: boolean | undefined;
6
6
  component?: any | undefined;
7
- a?: boolean;
8
- span?: boolean;
7
+ a?: boolean | undefined;
8
+ span?: boolean | undefined;
9
9
  element?: HTMLElement | undefined;
10
10
  };
11
11
  events: {
@@ -7,16 +7,16 @@ export declare const FocusLockStack: {
7
7
  }[];
8
8
  declare const __propDef: {
9
9
  props: {
10
- escapable?: boolean;
11
- hidefocus?: boolean;
12
- hidefocuslabel?: string;
10
+ escapable?: boolean | undefined;
11
+ hidefocus?: boolean | undefined;
12
+ hidefocuslabel?: string | undefined;
13
13
  initialfocus?: string | undefined;
14
14
  returnfocusto?: HTMLElement | null | undefined;
15
15
  /** If you expect any popup menus to be added to the body, we need to know that they
16
16
  are considered to be part of the focus lock, or else the modal will be dismissed
17
17
  when the user clicks inside. Use commas to include multiple selectors. */ includeselector?: string | undefined;
18
- class?: string;
19
- focusId?: string;
18
+ class?: string | undefined;
19
+ focusId?: string | undefined;
20
20
  };
21
21
  events: {
22
22
  click: MouseEvent;
@@ -2,14 +2,14 @@ import { SvelteComponentTyped } from "svelte";
2
2
  declare const __propDef: {
3
3
  props: {
4
4
  animationData: any;
5
- loop?: boolean;
5
+ loop?: boolean | undefined;
6
6
  name?: string | undefined;
7
- speed?: number;
8
- direction?: 1 | -1;
9
- paused?: boolean;
7
+ speed?: number | undefined;
8
+ direction?: 1 | -1 | undefined;
9
+ paused?: boolean | undefined;
10
10
  width?: string | undefined;
11
11
  height?: string | undefined;
12
- /** `<ScreenReaderOnly>` lable to be applied. */ alt?: string;
12
+ /** `<ScreenReaderOnly>` lable to be applied. */ alt?: string | undefined;
13
13
  };
14
14
  events: {
15
15
  [evt: string]: CustomEvent<any>;
@@ -1,10 +1,10 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
2
  declare const __propDef: {
3
3
  props: {
4
- opaque?: boolean;
5
- containerClass?: string;
6
- escapable?: boolean;
7
- hidefocus?: boolean;
4
+ opaque?: boolean | undefined;
5
+ containerClass?: string | undefined;
6
+ escapable?: boolean | undefined;
7
+ hidefocus?: boolean | undefined;
8
8
  hidefocuslabel?: string | undefined;
9
9
  initialfocus?: string | undefined;
10
10
  returnfocusto?: HTMLElement | undefined;
@@ -12,7 +12,7 @@ declare const __propDef: {
12
12
  /** If you expect any popup menus to be added to the body, we need to know that they
13
13
  are considered to be part of the focus lock, or else the modal will be dismissed
14
14
  when the user clicks inside. Use commas to include multiple selectors. */ includeselector?: string | undefined;
15
- focusId?: string;
15
+ focusId?: string | undefined;
16
16
  };
17
17
  events: {
18
18
  escape: CustomEvent<any>;
@@ -13,23 +13,28 @@ import DefaultPopupMenu from './PopupMenu.svelte';
13
13
  import { modifierKey, selectionIsLeft } from '../util';
14
14
  export let name;
15
15
  /**
16
- * Function to pass to the component that tells it how to use the text
17
- * in the text input to determine what `PopupMenuItem[]` should be displayed
18
- * in the `PopupMenu`. Items already 'selected' from the popup menu will be
19
- * tracked and automatically filtered from the popup if returned as one of the
20
- * `PopupMenuItem[]` by `getOptions`. */
16
+ * Function to pass to the component that tells it how to use the text in the text input to determine what
17
+ * `PopupMenuItem[]` should be displayed in the `PopupMenu`. Items already 'selected' from the popup menu will
18
+ * be tracked and automatically filtered from the popup if returned as one of the `PopupMenuItem[]` by `getOptions`. */
21
19
  export let getOptions;
22
20
  export let id = randomid();
23
21
  export let disabled = false;
24
- export let menuContainerClass = undefined;
25
- export let menuClass = undefined;
26
- export let menuItemClass = undefined;
27
- export let menuItemHilitedClass = undefined;
28
- export let inputClass = undefined;
22
+ export let menuContainerClass = '';
23
+ export let menuClass = '';
24
+ export let menuItemClass = '';
25
+ export let menuItemHilitedClass = '';
26
+ export let inputClass = '';
27
+ export let menuDividerClass = '';
29
28
  /** The maximum number of selections allowed before making new selections is disabled. Default of 0 is unlimited. */
30
29
  export let maxSelections = 0;
30
+ /** Consuming components may need to make decisions about what to display or return as options in the popup based
31
+ * on what's already selected. This component property can be used to both pass an initial set of selections but
32
+ * it can also be bound to provide a means to inspect what's selected. */
31
33
  export let selected = [];
34
+ /** The text to be displayed in the input text box for contextual feedback on what the input text is expecting. */
32
35
  export let placeholder = '';
36
+ /** When there are no items (e.g. it's a filtered search and there were no results), we still display one
37
+ disabled item in the menu to let the user know what is going on. Use this prop to specify the message. */
33
38
  export let emptyText = undefined;
34
39
  export let usePortal = undefined;
35
40
  export let descid = undefined;
@@ -59,14 +64,14 @@ async function reactToInput(..._) {
59
64
  const rawOptions = await optionsCache.get(saveval);
60
65
  if (inputvalue !== saveval)
61
66
  return; // ignore any results that are out of date
62
- options = rawOptions.filter(o => !selectedSet.has(o.value));
67
+ options = rawOptions.filter(o => !('value' in o) || !selectedSet.has(o.value));
63
68
  if (typeof document !== 'undefined' && inputelement === document.activeElement && (options.length || inputvalue?.length))
64
69
  menushown = true;
65
70
  loading = false;
66
71
  }, 250);
67
72
  }
68
73
  $: reactToInput(inputvalue, getOptions, selectedSet);
69
- $: availablemessage = options.filter(o => o.value).length + ' autocomplete choices available';
74
+ $: availablemessage = options.filter(o => 'value' in o && o.value).length + ' autocomplete choices available';
70
75
  function addSelection(e) {
71
76
  inputvalue = '';
72
77
  const opt = { ...e.detail, label: e.detail.label || e.detail.value };
@@ -142,10 +147,11 @@ $: reactToHilite(hilitedpill, id);
142
147
  on:click|preventDefault|stopPropagation={() => !disabled && removeSelection(option, i, 1)} on:mousedown={e => disabled && e.preventDefault()}
143
148
  aria-selected="true">
144
149
  {option.label || option.value}
150
+ <i aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256"><path fill="currentColor" d="M205.66 194.34a8 8 0 0 1-11.32 11.32L128 139.31l-66.34 66.35a8 8 0 0 1-11.32-11.32L116.69 128L50.34 61.66a8 8 0 0 1 11.32-11.32L128 116.69l66.34-66.35a8 8 0 0 1 11.32 11.32L139.31 128Z"/></svg></i>
145
151
  <ScreenReaderOnly>, click to deselect</ScreenReaderOnly>
146
152
  </li>
147
153
  {/each}
148
- <li class={`input ${inputClass ?? ''}`}>
154
+ <li class={`input ${inputClass}`}>
149
155
  <input type="text" {id} {name} {disabled} {placeholder}
150
156
  bind:this={inputelement} bind:value={inputvalue} on:blur
151
157
  on:focus={inputfocus} on:keydown={inputkeydown}
@@ -160,7 +166,9 @@ $: reactToHilite(hilitedpill, id);
160
166
  <slot></slot>
161
167
  </fieldset>
162
168
  <svelte:component this={PopupMenu} bind:menushown bind:hilited={popuphilited} bind:value={popupvalue} align='bottomleft'
163
- {usePortal} {loading} {emptyText} {menuContainerClass} {menuClass} {menuItemClass} {menuItemHilitedClass} items={options} buttonelement={inputelement}
169
+ {usePortal} {loading} {emptyText}
170
+ {menuContainerClass} {menuClass} {menuItemClass} {menuItemHilitedClass} {menuDividerClass}
171
+ items={options} buttonelement={inputelement}
164
172
  on:change={addSelection}/>
165
173
 
166
174
  <style>
@@ -199,6 +207,7 @@ $: reactToHilite(hilitedpill, id);
199
207
  height: 100%;
200
208
  }
201
209
  .multiselect-pill {
210
+ position: relative;
202
211
  cursor: pointer;
203
212
  flex-grow: 0;
204
213
  margin-right: 0.3em;
@@ -207,7 +216,21 @@ $: reactToHilite(hilitedpill, id);
207
216
  border: var(--multiselect-pill-border, 1px solid gray);
208
217
  background-color: var(--multiselect-pill-bg, transparent);
209
218
  color: var(--multiselect-pill-text, black);
210
- padding: var(--multiselect-pill-padding, 0.3em 0.5em);
219
+ padding: var(--multiselect-pill-padding-top, 0.3em) var(--multiselect-pill-padding-left, 0.5em);
220
+ padding-right: calc(var(--multiselect-pill-padding-left, 0.5em) + 1.1em);
221
+ }
222
+ .multiselect-pill i {
223
+ display: block;
224
+ position: absolute;
225
+ top: 50%;
226
+ right: calc(var(--multiselect-pill-padding-left, 0.5em) - 0.1em);
227
+ transform: translateY(-50%);
228
+ width: 1em;
229
+ height: 1em;
230
+ }
231
+ .multiselect-pill svg {
232
+ width: 100%;
233
+ height: 100%;
211
234
  }
212
235
  .multiselect-selected.disabled {
213
236
  opacity: 0.5;
@@ -1,32 +1,31 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
2
  import DefaultPopupMenu from './PopupMenu.svelte';
3
- import type { PopupMenuItem } from '../types';
3
+ import type { PopupMenuItem, PopupMenuTypes } from '../types';
4
4
  declare const __propDef: {
5
5
  props: {
6
6
  name: string;
7
7
  /**
8
- * Function to pass to the component that tells it how to use the text
9
- * in the text input to determine what `PopupMenuItem[]` should be displayed
10
- * in the `PopupMenu`. Items already 'selected' from the popup menu will be
11
- * tracked and automatically filtered from the popup if returned as one of the
12
- * `PopupMenuItem[]` by `getOptions`. */ getOptions: (search: string) => Promise<PopupMenuItem[]> | PopupMenuItem[];
13
- id?: string;
14
- disabled?: boolean;
8
+ * Function to pass to the component that tells it how to use the text in the text input to determine what
9
+ * `PopupMenuItem[]` should be displayed in the `PopupMenu`. Items already 'selected' from the popup menu will
10
+ * be tracked and automatically filtered from the popup if returned as one of the `PopupMenuItem[]` by `getOptions`. */ getOptions: (search: string) => Promise<PopupMenuTypes[]> | PopupMenuTypes[];
11
+ id?: string | undefined;
12
+ disabled?: boolean | undefined;
15
13
  menuContainerClass?: string | undefined;
16
14
  menuClass?: string | undefined;
17
15
  menuItemClass?: string | undefined;
18
16
  menuItemHilitedClass?: string | undefined;
19
17
  inputClass?: string | undefined;
20
- /** The maximum number of selections allowed before making new selections is disabled. Default of 0 is unlimited. */ maxSelections?: number;
21
- selected?: {
22
- value: string;
23
- label: string;
24
- }[];
25
- placeholder?: string;
26
- emptyText?: string | undefined;
18
+ menuDividerClass?: string | undefined;
19
+ /** The maximum number of selections allowed before making new selections is disabled. Default of 0 is unlimited. */ maxSelections?: number | undefined;
20
+ /** Consuming components may need to make decisions about what to display or return as options in the popup based
21
+ * on what's already selected. This component property can be used to both pass an initial set of selections but
22
+ * it can also be bound to provide a means to inspect what's selected. */ selected?: PopupMenuItem[] | undefined;
23
+ /** The text to be displayed in the input text box for contextual feedback on what the input text is expecting. */ placeholder?: string | undefined;
24
+ /** When there are no items (e.g. it's a filtered search and there were no results), we still display one
25
+ disabled item in the menu to let the user know what is going on. Use this prop to specify the message. */ emptyText?: string | undefined;
27
26
  usePortal?: HTMLElement | true | undefined;
28
27
  descid?: string | undefined;
29
- /** You can define your own PopupMenu and pass for that to be used or accept DefaultPopupMenu. */ PopupMenu?: typeof DefaultPopupMenu;
28
+ /** You can define your own PopupMenu and pass for that to be used or accept DefaultPopupMenu. */ PopupMenu?: typeof DefaultPopupMenu | undefined;
30
29
  };
31
30
  events: {
32
31
  blur: FocusEvent;
@@ -3,7 +3,7 @@
3
3
  bound to `buttonelement`. It can also be controlled from the parent, by binding the `menushown` boolean.
4
4
  -->
5
5
  <script>import { createEventDispatcher, onDestroy, tick } from 'svelte';
6
- import { randomid } from 'txstate-utils';
6
+ 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';
@@ -39,7 +39,7 @@ for parent elements not styled for variable size children and which you want ove
39
39
  export let adjustparentheight = false;
40
40
  /** If the menu would be clipped by an `overflow: hidden`, you can set this prop and it will be placed in
41
41
  the specified container, or the `document.body` if you simply say `true`. Placement will still be calculated
42
- correctly, unless there are scrolling containers between the button and the menu. */
42
+ correctly since it uses positon fixed for placement. */
43
43
  export let usePortal = undefined;
44
44
  /** Useful for when your `items` need to be fetched but you want the associated element shown. Set to loading
45
45
  until they're ready to be displayed and the popup menu will not be shown until the `loading` bind is `true`. */
@@ -58,6 +58,7 @@ export let menuClass = '';
58
58
  export let menuItemClass = '';
59
59
  export let menuItemHilitedClass = '';
60
60
  export let menuItemSelectedClass = '';
61
+ export let menuDividerClass = '';
61
62
  let menuelement;
62
63
  const itemelements = [];
63
64
  let firstactive = 0;
@@ -66,8 +67,8 @@ function hiddenItem(item) {
66
67
  return !!item && !showSelected && item.value === value;
67
68
  }
68
69
  async function reactToItems(..._) {
69
- firstactive = items.findIndex(itm => !itm.disabled && !hiddenItem(itm));
70
- lastactive = items.length - [...items].reverse().findIndex(itm => !itm.disabled && !hiddenItem(itm)) - 1;
70
+ firstactive = items.findIndex(itm => 'value' in itm && !itm.disabled && !hiddenItem(itm));
71
+ lastactive = items.length - [...items].reverse().findIndex(itm => 'value' in itm && !itm.disabled && !hiddenItem(itm)) - 1;
71
72
  if (hilited && items[hilited]?.disabled)
72
73
  hilited = firstactive;
73
74
  }
@@ -100,12 +101,17 @@ $: reactToMenuShown(menushown);
100
101
  function move(idx) {
101
102
  if (!menushown)
102
103
  return;
104
+ while (idx <= lastactive && 'divider' in items[idx])
105
+ idx++;
103
106
  if (items[idx]?.disabled)
104
107
  return;
105
108
  hilited = Math.max(firstactive, Math.min(lastactive, idx));
106
109
  itemelements[hilited].scrollIntoView({ block: 'center' });
107
110
  buttonelement.setAttribute('aria-activedescendant', `${menuid}-${hilited}`);
108
111
  }
112
+ function isSelectable(itm) {
113
+ return 'value' in itm && !itm.disabled && !hiddenItem(itm);
114
+ }
109
115
  function onkeydown(e) {
110
116
  if (modifierKey(e))
111
117
  return;
@@ -113,7 +119,7 @@ function onkeydown(e) {
113
119
  e.preventDefault();
114
120
  if (menushown) {
115
121
  let i = (hilited ?? firstactive - 1) + 1;
116
- while (items[i]?.disabled || hiddenItem(items[i]))
122
+ while (!isSelectable(items[i]))
117
123
  i++;
118
124
  move(i);
119
125
  }
@@ -126,7 +132,7 @@ function onkeydown(e) {
126
132
  e.preventDefault();
127
133
  if (menushown) {
128
134
  let i = (hilited ?? lastactive + 1) - 1;
129
- while (items[i]?.disabled || hiddenItem(items[i]))
135
+ while (!isSelectable(items[i]))
130
136
  i--;
131
137
  move(i);
132
138
  }
@@ -229,7 +235,7 @@ const onclick = (item) => (e) => {
229
235
  value = item.value;
230
236
  dispatch('change', item);
231
237
  };
232
- $: hasSelected = showSelected && items.some(itm => itm.value === value);
238
+ $: hasSelected = showSelected && items.some(itm => 'value' in itm && itm.value === value);
233
239
  </script>
234
240
 
235
241
  {#if menushown}
@@ -239,22 +245,24 @@ $: hasSelected = showSelected && items.some(itm => itm.value === value);
239
245
  <ul bind:this={menuelement} id={menuid} role='listbox' style={width ? `width: ${width}` : ''}
240
246
  class={menuClass} class:hasSelected class:defaultmenu={!menuClass && !menuContainerClass}
241
247
  on:keydown={onkeydown}>
242
- {#each items as item, i (item.value)}
243
- {#if showSelected || item.value !== value}
248
+ {#each items as item, i ('value' in item ? item.value : `popupmenu_divider_${i}`)}
249
+ {#if 'value' in item && (showSelected || item.value !== value)}
244
250
  <!-- svelte-ignore a11y-click-events-have-key-events -->
245
251
  <li
246
252
  id={`${menuid}-${i}`}
247
253
  bind:this={itemelements[i]}
248
- class={`${menuItemClass} ${i === hilited ? menuItemHilitedClass || '' : ''} ${value === item.value ? menuItemSelectedClass || '' : ''}`}
254
+ class={`${menuItemClass} ${i === hilited ? menuItemHilitedClass : ''} ${value === item.value ? menuItemSelectedClass : ''}`}
249
255
  class:disabled={!!item.disabled}
250
256
  class:hilited={!menuItemHilitedClass && i === hilited}
251
257
  class:selected={showSelected && !menuItemSelectedClass && value === item.value}
252
258
  on:click={onclick(item)}
253
- role="option"
259
+ role='option'
254
260
  tabindex=-1
255
261
  aria-selected={value === item.value}
256
262
  aria-disabled={item.disabled}
257
263
  ><slot {item} label={item.label || item.value} hilited={i === hilited} selected={value === item.value}>{item.label || item.value}</slot></li>
264
+ {:else if 'divider' in item && item.divider}
265
+ <li class={`divider ${menuDividerClass}`} on:mousedown|stopPropagation|preventDefault class:group={isNotBlank(item.label)}>{item.label}</li>
258
266
  {/if}
259
267
  {/each}
260
268
  {#if items.length === 0}
@@ -274,7 +282,6 @@ $: hasSelected = showSelected && items.some(itm => itm.value === value);
274
282
 
275
283
  <style>
276
284
  div {
277
- position: absolute !important;
278
285
  z-index: var(--popup-z, calc(var(--modal-z, 3000) + 1));
279
286
  }
280
287
  ul {
@@ -317,6 +324,15 @@ $: hasSelected = showSelected && items.some(itm => itm.value === value);
317
324
  li.selected {
318
325
  position: relative;
319
326
  }
327
+ li.divider {
328
+ height: 0px;
329
+ border-top: 2px solid slategray;
330
+ cursor: default;
331
+ }
332
+ li.divider.group {
333
+ height: auto;
334
+ border-bottom: 2px solid slategray;
335
+ }
320
336
  ul.hasSelected li {
321
337
  padding-left: 1.4em;
322
338
  }
@@ -1,55 +1,57 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
2
  import { Store } from '@txstate-mws/svelte-store';
3
3
  import type { GlueAlignOpts, GlueAlignStore } from '../actions';
4
- import type { PopupMenuItem } from '../types';
4
+ import type { PopupMenuTypes } from '../types';
5
5
  declare const __propDef: {
6
6
  props: {
7
7
  /** The DOM element that will act as the "button" for this menu. The menu will be placed next to the
8
8
  button and be controlled by the button. Keyboard access will be enabled when the button has focus.
9
9
  This component adds all appropriate attributes (tabindex, roles, and aria) automatically. */ buttonelement: HTMLElement;
10
- /** The list of menu items to be shown. Parent may change this at any time based on user activity. */ items?: PopupMenuItem[];
11
- menushown?: boolean;
10
+ /** The list of menu items to be shown. Parent may change this at any time based on user activity. */ items?: PopupMenuTypes[] | undefined;
11
+ menushown?: boolean | undefined;
12
12
  value?: string | undefined;
13
13
  /** Control where the menu will appear. Default is to use the current viewport to make a decision to
14
- maximize potential for menu growth. */ align?: GlueAlignOpts;
15
- /** When the menu is active, should it cover the button or be rendered above/below it? */ cover?: boolean;
14
+ maximize potential for menu growth. */ align?: GlueAlignOpts | undefined;
15
+ /** When the menu is active, should it cover the button or be rendered above/below it? */ cover?: boolean | undefined;
16
16
  /** When an item is currently selected, should it appear in the menu (with a visual indicator), or
17
- simply be removed from the list (`showSelected: false`). */ showSelected?: boolean;
17
+ simply be removed from the list (`showSelected: false`). */ showSelected?: boolean | undefined;
18
18
  /** When defined, this width will be passed through to the menu's CSS width. Use a valid CSS dimension
19
19
  such as 33em or 159px. Generally this will be used when you need to match the menu width to something
20
20
  else, like the button. If the width is static like 100% a simple CSS rule is likely more efficient. */ width?: string | undefined;
21
21
  /** Bind this prop to receive a store that is updated each time a menu placement decision is made. Example
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
- the values do not update when the menu is hidden, so you may need to rely on `menushown` as well. */ computedalign?: Store<GlueAlignStore>;
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
25
  /** Set to `true` if you want the parent element's styling to increase its minHeight when necessary. Useful
26
- for parent elements not styled for variable size children and which you want override the the size of. */ adjustparentheight?: boolean;
26
+ for parent elements not styled for variable size children and which you want override the the size of. */ adjustparentheight?: boolean | undefined;
27
27
  /** If the menu would be clipped by an `overflow: hidden`, you can set this prop and it will be placed in
28
28
  the specified container, or the `document.body` if you simply say `true`. Placement will still be calculated
29
- correctly, unless there are scrolling containers between the button and the menu. */ usePortal?: HTMLElement | true | undefined;
29
+ correctly since it uses positon fixed for placement. */ usePortal?: HTMLElement | true | undefined;
30
30
  /** Useful for when your `items` need to be fetched but you want the associated element shown. Set to loading
31
- until they're ready to be displayed and the popup menu will not be shown until the `loading` bind is `true`. */ loading?: boolean;
31
+ until they're ready to be displayed and the popup menu will not be shown until the `loading` bind is `true`. */ loading?: boolean | undefined;
32
32
  /** A bindable value for inspecting which item in `items` is currently highlighted as the item currently
33
33
  active in the list. This is not the same as `value` and the highlighted item may or may not be selected as
34
34
  for the `value` of the field. */ hilited?: number | undefined;
35
- /** The id of the <ul> element that is the displayed list of items. */ menuid?: string;
35
+ /** The id of the <ul> element that is the displayed list of items. */ menuid?: string | undefined;
36
36
  /** When there are no items (e.g. it's a filtered search and there were no results), we still display one
37
37
  disabled item in the menu to let the user know what is going on. Use this prop to specify the message. */ emptyText?: string | undefined;
38
- menuContainerClass?: string;
39
- menuClass?: string;
40
- menuItemClass?: string;
41
- menuItemHilitedClass?: string;
42
- menuItemSelectedClass?: string;
38
+ menuContainerClass?: string | undefined;
39
+ menuClass?: string | undefined;
40
+ menuItemClass?: string | undefined;
41
+ menuItemHilitedClass?: string | undefined;
42
+ menuItemSelectedClass?: string | undefined;
43
+ menuDividerClass?: string | undefined;
43
44
  };
44
45
  events: {
46
+ mousedown: MouseEvent;
45
47
  change: CustomEvent<any>;
46
48
  } & {
47
49
  [evt: string]: CustomEvent<any>;
48
50
  };
49
51
  slots: {
50
52
  default: {
51
- item: PopupMenuItem;
52
- label: string;
53
+ item: PopupMenuTypes;
54
+ label: any;
53
55
  hilited: boolean;
54
56
  selected: boolean;
55
57
  };
@@ -10,7 +10,7 @@ export declare const CARDLAYOUT: {};
10
10
  }
11
11
  ``` */
12
12
  export interface Block {
13
- element: HTMLElement;
13
+ element: HTMLElement | undefined;
14
14
  order?: SettableSubject<number>;
15
15
  linebreak?: SettableSubject<boolean>;
16
16
  width?: SettableSubject<string>;
@@ -1,8 +1,30 @@
1
- /** ```ts
2
- { value: string, lable? string, disabled?: boolean }
3
- ``` */
1
+ /** The basic information assocaited with individual choices that are presented in a PopupMenu. */
4
2
  export interface PopupMenuItem {
3
+ /** The value returned by the PopupMenu when the associated item is selected. */
5
4
  value: string;
5
+ /** An optional display string to show in the menu for the associated `value`. */
6
6
  label?: string;
7
+ /** Toggles the choice as one that's visible but is disabled and unclickable until conditions
8
+ * are met that would cause it to become enabled. */
7
9
  disabled?: boolean;
8
10
  }
11
+ /** A PopupMenu list native type for displaying */
12
+ export interface PopupMenuDivider {
13
+ /** Toggle for whether this instance of this type is shown. */
14
+ divider: boolean;
15
+ /** Optional labeling to display. Useful for dividers used as group headers.
16
+ * We may want to update PopupMenu to use <optgroup lable=
17
+ */
18
+ label?: string;
19
+ }
20
+ /** To support ARIA best practices for groups within listboxes as popup menus
21
+ * we'll need to group options under sub-lists. This interface provides a means
22
+ * for passing such to PopupMenu.
23
+ * @note Not currently in use. */
24
+ export interface PopupMenuGroup {
25
+ /** The label for the group. */
26
+ lable: string;
27
+ /** The listing of options that would be listed under the parent `<ul>`. */
28
+ options: PopupMenuTypes[];
29
+ }
30
+ export type PopupMenuTypes = (PopupMenuDivider | PopupMenuItem);
@@ -11,4 +11,4 @@ export declare function targetOffset(ele: HTMLElement, target: HTMLElement): {
11
11
  top: number;
12
12
  left: number;
13
13
  };
14
- export declare function sharedOffsetParent(a: HTMLElement, b: HTMLElement): HTMLElement;
14
+ export declare function sharedOffsetParent(a: HTMLElement, b: HTMLElement): HTMLElement | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@txstate-mws/svelte-components",
3
- "version": "1.4.8",
3
+ "version": "1.5.1",
4
4
  "description": "Svelte components that are generically useful.",
5
5
  "scripts": {
6
6
  "prepublishOnly": "svelte-package",
@@ -31,7 +31,7 @@
31
31
  "svelte-preprocess": "^5.0.0",
32
32
  "svelte2tsx": "^0.6.0",
33
33
  "tslib": "^2.3.1",
34
- "typescript": "^4.4.3"
34
+ "typescript": "^5.0.4"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "svelte": "^3.44.0"