@txstate-mws/svelte-components 1.0.3 → 1.1.2

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/README.md CHANGED
@@ -1,24 +1,24 @@
1
- # create-svelte
2
-
3
- Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
4
-
5
- ## Creating a project
6
-
7
- If you're seeing this, you've probably already done this step. Congrats!
8
-
9
- ```bash
10
- # create a new project in the current directory
11
- npm init svelte@next
12
-
13
- # create a new project in my-app
14
- npm init svelte@next my-app
15
- ```
16
-
17
- > Note: the `@next` is temporary
1
+ # svelte-components
2
+ A set of generically useful components for svelte.
3
+
4
+ ## Components
5
+ - [CollapsingTable](/docs/CollapsingTable.md)
6
+ - [ConditionalWrapper](/docs/ConditionalWrapper.md)
7
+ - [FocusLock](/docs/Modal.md#FocusLock)
8
+ - [Modal](/docs/Modal.md#Modal)
9
+ - [PopupMenu](/docs/PopupMenu.md)
10
+ - [ScreenReaderOnly](/docs/ScreenReaderOnly.md)
11
+
12
+ ## Actions
13
+ - [use:buttonify](/docs/actions.md#use:buttonify)
14
+ - [use:glue](/docs/actions.md#use:glue)
15
+ - [use:offset](/docs/actions.md#use:offset)
16
+ - [use:portal](/docs/actions.md#use:portal)
17
+ - [use:resize](/docs/actions.md#use:resize)
18
18
 
19
19
  ## Developing
20
20
 
21
- Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
21
+ Installed dependencies with `npm install`, then start the development server:
22
22
 
23
23
  ```bash
24
24
  npm run dev
@@ -27,14 +27,10 @@ npm run dev
27
27
  npm run dev -- --open
28
28
  ```
29
29
 
30
- ## Building
31
-
32
- To create a production version of your app:
30
+ ## Publishing to npm
33
31
 
34
32
  ```bash
35
- npm run build
33
+ npm run package
34
+ cd package
35
+ npm publish
36
36
  ```
37
-
38
- You can preview the production build with `npm run preview`.
39
-
40
- > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment.
package/actions/resize.js CHANGED
@@ -3,7 +3,19 @@ import { Store } from '@txstate-mws/svelte-store';
3
3
  import { debounced } from '../util/index.js';
4
4
  export class ResizeStore extends Store {
5
5
  constructor(initialState) {
6
- super(initialState ?? { clientWidth: 1024, clientHeight: 1024, offsetWidth: 1024, offsetHeight: 1024 });
6
+ super(initialState ?? typeof window === 'undefined'
7
+ ? {
8
+ clientWidth: 1024,
9
+ clientHeight: 1024,
10
+ offsetWidth: 1024,
11
+ offsetHeight: 1024
12
+ }
13
+ : {
14
+ clientWidth: window.innerWidth,
15
+ clientHeight: window.innerHeight,
16
+ offsetWidth: window.innerWidth,
17
+ offsetHeight: window.innerHeight
18
+ });
7
19
  }
8
20
  }
9
21
  export function resize(el, config) {
@@ -19,7 +31,7 @@ export function resize(el, config) {
19
31
  el.dispatchEvent(new CustomEvent('resize', { detail: current }));
20
32
  }
21
33
  }
22
- if (!ResizeObserver)
34
+ if (typeof ResizeObserver === 'undefined')
23
35
  return watch(); // support SSR
24
36
  let observer = new ResizeObserver(config?.debounce ? debounced(watch, config.debounce) : watch);
25
37
  observer.observe(el);
package/actions/sticky.js CHANGED
@@ -34,7 +34,7 @@ export function sticky(el, config) {
34
34
  config?.store?.update(v => ({ ...v, translateY, floating }));
35
35
  }
36
36
  }
37
- function onscroll(e) {
37
+ function onscroll(_) {
38
38
  recalc(window.scrollY);
39
39
  }
40
40
  function onwheel(e) {
@@ -112,7 +112,7 @@ export function stickyfixed(el, config) {
112
112
  el.style.top = `${top}px`;
113
113
  }
114
114
  }
115
- function onscroll(e) {
115
+ function onscroll(_) {
116
116
  recalc(window.scrollY);
117
117
  }
118
118
  let lastOffsetParent = el.offsetParent;
@@ -0,0 +1,23 @@
1
+ <script >export let className = '';
2
+ import { getContext } from 'svelte';
3
+ import { resize } from '../actions';
4
+ import { CARDLAYOUT } from '../types';
5
+ const block = { element: undefined };
6
+ const { registerBlock, gutter, recalculate } = getContext(CARDLAYOUT);
7
+ const { width, order, linebreak } = registerBlock(block);
8
+ </script>
9
+
10
+ <li class="cardlayout-card {className}" bind:this={block.element} use:resize on:resize={recalculate} style="margin-bottom: {$gutter}px; width: {$width}; order: {$order};">
11
+ <slot></slot>
12
+ </li>
13
+ {#if $linebreak}
14
+ <li aria-hidden="true" class="cardlayout-columnbreak" style:order={$order}></li>
15
+ {/if}
16
+
17
+ <style>
18
+ .cardlayout-columnbreak {
19
+ width: 0;
20
+ height: 100%;
21
+ visibility: hidden;
22
+ }
23
+ </style>
@@ -0,0 +1,18 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ className?: string;
5
+ };
6
+ events: {
7
+ [evt: string]: CustomEvent<any>;
8
+ };
9
+ slots: {
10
+ default: {};
11
+ };
12
+ };
13
+ export declare type CardProps = typeof __propDef.props;
14
+ export declare type CardEvents = typeof __propDef.events;
15
+ export declare type CardSlots = typeof __propDef.slots;
16
+ export default class Card extends SvelteComponentTyped<CardProps, CardEvents, CardSlots> {
17
+ }
18
+ export {};
@@ -0,0 +1,197 @@
1
+ <script >import { onDestroy, tick, setContext, onMount } from 'svelte';
2
+ import { writable } from 'svelte/store';
3
+ import { browser } from '$app/env';
4
+ import { resize } from '../actions';
5
+ import { CARDLAYOUT } from '../types';
6
+ export let maxwidth = 500;
7
+ export let preserveorder = false;
8
+ export let gutter = 10;
9
+ export let className = '';
10
+ const blocks = [];
11
+ const gutterstore = writable(gutter);
12
+ let defaultOrder = 0;
13
+ setContext(CARDLAYOUT, {
14
+ registerBlock: (block) => {
15
+ blocks.push(block);
16
+ hardrecalc();
17
+ onDestroy(() => {
18
+ blocks.splice(blocks.indexOf(block), 1);
19
+ hardrecalc();
20
+ });
21
+ block.order = writable(defaultOrder++);
22
+ block.linebreak = writable(false);
23
+ const cols = savecolumns || Math.ceil(1024 / maxwidth);
24
+ block.width = writable(`calc(${100.0 / cols}% - ${gutter * (cols - 1) / cols}px)`);
25
+ return block;
26
+ },
27
+ recalculate: () => {
28
+ hardrecalc();
29
+ },
30
+ gutter: gutterstore
31
+ });
32
+ $: gutterstore.set(gutter);
33
+ let layoutelement;
34
+ let cycle1 = 0;
35
+ let cycle2 = 0;
36
+ function detectcycle(w) {
37
+ if (cycle1 === 0 && Math.abs(w - cycle2) > 5) {
38
+ cycle1 = w;
39
+ }
40
+ else if (cycle2 === 0 && Math.abs(w - cycle1) > 5) {
41
+ cycle2 = w;
42
+ }
43
+ else if (w === cycle1 || w === cycle2) {
44
+ return true;
45
+ }
46
+ else {
47
+ cycle1 = 0;
48
+ cycle2 = 0;
49
+ }
50
+ return false;
51
+ }
52
+ let savecolumns = 0;
53
+ let optimal;
54
+ let fullheight = 0;
55
+ let hardrequired = false;
56
+ async function recalculate(realw) {
57
+ const columns = Math.ceil(realw / maxwidth);
58
+ const guttereach = gutter * (columns - 1) / columns;
59
+ const cycling = detectcycle(realw);
60
+ if (columns !== savecolumns) {
61
+ for (const block of blocks)
62
+ block.width.set(`calc(${100.0 / columns}% - ${guttereach}px`);
63
+ await tick();
64
+ }
65
+ // collect all the card heights at this new column width
66
+ for (const block of blocks)
67
+ block.height = block.element?.offsetHeight ?? 200;
68
+ if (columns !== savecolumns || hardrequired) { // only do real work if number of columns has changed, recalculate triggers on resize
69
+ if (preserveorder) {
70
+ optimal = [blocks];
71
+ if (columns > 1) {
72
+ const totalheight = blocks.reduce((totalheight, block) => totalheight + block.height + gutter, 0);
73
+ const tallestblock = Math.max(...blocks.map(b => b.height));
74
+ const minheight = Math.max(totalheight / columns, tallestblock);
75
+ let shortestoverall = totalheight;
76
+ // 2d packing problem is NP-hard so this is an O(n) heuristic
77
+ // optimal configuration would be if each column is `minheight` so start there
78
+ // then relax it in increments and see if the overall height shrinks
79
+ const increment = Math.max(1, Math.floor(Math.min(minheight, tallestblock) * 0.05));
80
+ for (let colmaxheight = minheight; colmaxheight < minheight + tallestblock + gutter; colmaxheight += increment) {
81
+ let colheight = 0;
82
+ let colidx = 0;
83
+ let tallestcol = 0;
84
+ let tallestcolidx = 0;
85
+ const arrangement = [];
86
+ for (const block of blocks) {
87
+ if (colheight + block.height > colmaxheight && colidx < columns - 1) {
88
+ if (colheight > tallestcol) {
89
+ tallestcol = colheight;
90
+ tallestcolidx = colidx;
91
+ }
92
+ colidx++;
93
+ colheight = 0;
94
+ }
95
+ if (!arrangement[colidx])
96
+ arrangement[colidx] = [];
97
+ arrangement[colidx].push(block);
98
+ colheight += block.height + gutter;
99
+ }
100
+ if (colheight > tallestcol) {
101
+ tallestcol = colheight;
102
+ tallestcolidx = colidx;
103
+ }
104
+ if (tallestcol < shortestoverall) {
105
+ shortestoverall = tallestcol;
106
+ optimal = arrangement;
107
+ }
108
+ // if leftmost column is tallest, increasing the allowable height can never make the overall height shorter
109
+ if (tallestcolidx === 0)
110
+ break;
111
+ }
112
+ }
113
+ }
114
+ else {
115
+ optimal = [];
116
+ const heights = Array.apply(null, Array(columns)).map((_) => 0); // initializes heights to an array of zeroes
117
+ // begin sorting blocks into columns
118
+ for (const block of blocks) {
119
+ // find the column with the smallest current height
120
+ const colidx = heights.reduce((acc, curr, curridx) => curr < heights[acc] ? curridx : acc, 0);
121
+ // record the height we are adding to the chosen column
122
+ heights[colidx] += block.height + gutter;
123
+ // move the current card to the chosen column
124
+ if (!optimal[colidx])
125
+ optimal[colidx] = [];
126
+ optimal[colidx].push(block);
127
+ }
128
+ }
129
+ // we created the optimal arrangement, now move the block elements to
130
+ // reflect it
131
+ let order = 0;
132
+ for (let i = 0; i < optimal.length; i++) {
133
+ for (let j = 0; j < optimal[i].length; j++) {
134
+ const block = optimal[i][j];
135
+ block.order.set(order++);
136
+ block.linebreak.set(j === optimal[i].length - 1 && i < optimal.length - 1);
137
+ }
138
+ }
139
+ savecolumns = columns;
140
+ hardrequired = false;
141
+ }
142
+ // re-adjust container height
143
+ const saveheight = fullheight;
144
+ fullheight = 0;
145
+ for (let i = 0; i < optimal.length; i++) {
146
+ let top = 0;
147
+ for (const block of optimal[i]) {
148
+ top += block.height + gutter;
149
+ }
150
+ fullheight = Math.max(fullheight, top);
151
+ }
152
+ if (cycling && saveheight < fullheight)
153
+ fullheight = saveheight;
154
+ }
155
+ let savewidth = 0;
156
+ let timer = 0;
157
+ function triggerrecalc(width) {
158
+ if (width === savewidth && !hardrequired)
159
+ return;
160
+ cancelAnimationFrame(timer);
161
+ timer = requestAnimationFrame(() => {
162
+ recalculate(width);
163
+ savewidth = width;
164
+ });
165
+ }
166
+ function hardrecalc() {
167
+ if (!layoutelement)
168
+ return;
169
+ hardrequired = true;
170
+ cycle1 = 0;
171
+ cycle2 = 0;
172
+ triggerrecalc(layoutelement.clientWidth);
173
+ }
174
+ function onResize(e) {
175
+ triggerrecalc(e.detail.clientWidth);
176
+ }
177
+ onMount(() => triggerrecalc(layoutelement.clientWidth));
178
+ </script>
179
+
180
+ <ul class="cardlayout {className}" class:ssr={!browser} style:height={browser ? `${fullheight + gutter}px` : undefined} bind:this={layoutelement} use:resize on:resize={onResize}>
181
+ <slot></slot>
182
+ </ul>
183
+
184
+ <style>
185
+ .cardlayout {
186
+ list-style: none;
187
+ padding: 0;
188
+ margin: 0;
189
+ display: flex;
190
+ flex-wrap: wrap;
191
+ flex-direction: column;
192
+ align-content: space-between;
193
+ }
194
+ .cardlayout.ssr {
195
+ flex-direction: row;
196
+ }
197
+ </style>
@@ -0,0 +1,21 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ maxwidth?: number;
5
+ preserveorder?: boolean;
6
+ gutter?: number;
7
+ className?: string;
8
+ };
9
+ events: {
10
+ [evt: string]: CustomEvent<any>;
11
+ };
12
+ slots: {
13
+ default: {};
14
+ };
15
+ };
16
+ export declare type CardLayoutProps = typeof __propDef.props;
17
+ export declare type CardLayoutEvents = typeof __propDef.events;
18
+ export declare type CardLayoutSlots = typeof __propDef.slots;
19
+ export default class CardLayout extends SvelteComponentTyped<CardLayoutProps, CardLayoutEvents, CardLayoutSlots> {
20
+ }
21
+ export {};
@@ -0,0 +1,179 @@
1
+ <script >import { createEventDispatcher } from 'svelte';
2
+ import { randomid } from 'txstate-utils';
3
+ import ScreenReaderOnly from './ScreenReaderOnly.svelte';
4
+ import DefaultPopupMenu from './PopupMenu.svelte';
5
+ import { debouncedPromise, modifierKey, selectionIsLeft } from '../util';
6
+ export let id = randomid();
7
+ export let name;
8
+ export let getOptions;
9
+ export let menuContainerClass = undefined;
10
+ export let menuClass = undefined;
11
+ export let menuItemClass = undefined;
12
+ export let menuItemHilitedClass = undefined;
13
+ export let selected = [];
14
+ export let placeholder = '';
15
+ export let emptyText = undefined;
16
+ export let PopupMenu = DefaultPopupMenu;
17
+ let menushown;
18
+ let options = [];
19
+ let hilitedpill;
20
+ let inputvalue = '';
21
+ let popupvalue = undefined;
22
+ let inputelement;
23
+ const descriptionid = randomid();
24
+ $: debouncedGetOptions = debouncedPromise(getOptions, 100, []);
25
+ $: selectedSet = new Set(selected.map(s => s.value));
26
+ async function reactToInput(..._) {
27
+ const saveval = inputvalue;
28
+ const rawOptions = await debouncedGetOptions(saveval);
29
+ if (inputvalue !== saveval)
30
+ return; // ignore any results that are out of date
31
+ options = rawOptions.filter(o => !selectedSet.has(o.value));
32
+ if (typeof document !== 'undefined' && inputelement === document.activeElement)
33
+ menushown = true;
34
+ }
35
+ $: reactToInput(inputvalue, debouncedGetOptions, selectedSet);
36
+ $: availablemessage = options.filter(o => o.value).length + ' autocomplete choices available';
37
+ function addSelection(e) {
38
+ inputvalue = '';
39
+ const opt = { ...e.detail, label: e.detail.label || e.detail.value };
40
+ selected = [...selected, opt];
41
+ popupvalue = undefined;
42
+ }
43
+ function removeSelection(opt, idx, nextfocus = 1) {
44
+ if (hilitedpill === opt.value) {
45
+ hilitedpill = selected[idx + nextfocus]?.value;
46
+ }
47
+ selected = selected.filter(s => s.value !== opt.value);
48
+ }
49
+ function inputkeydown(e) {
50
+ if (modifierKey(e))
51
+ return;
52
+ if (e.key === 'ArrowLeft' && selectionIsLeft(inputelement)) {
53
+ const idx = hilitedpill ? selected.findIndex(s => s.value === hilitedpill) : selected.length;
54
+ if (idx !== 0)
55
+ hilitedpill = selected[idx - 1].value;
56
+ }
57
+ else if (e.key === 'ArrowRight' && selectionIsLeft(inputelement)) {
58
+ if (hilitedpill)
59
+ e.preventDefault();
60
+ const idx = hilitedpill ? selected.findIndex(s => s.value === hilitedpill) : undefined;
61
+ if (idx != null) {
62
+ if (idx === selected.length - 1)
63
+ hilitedpill = undefined;
64
+ else
65
+ hilitedpill = selected[idx + 1].value;
66
+ }
67
+ }
68
+ else if (['Backspace', 'Delete', 'Clear', ' ', 'Enter'].includes(e.key)) {
69
+ if (hilitedpill) {
70
+ const idx = selected.findIndex(s => s.value === hilitedpill);
71
+ removeSelection(selected[idx], idx, e.key === 'Delete' ? 1 : -1);
72
+ e.preventDefault();
73
+ }
74
+ else if (e.key === 'Backspace' && selectionIsLeft(inputelement)) {
75
+ removeSelection(selected[selected.length - 1], selected.length - 1, -1);
76
+ e.preventDefault();
77
+ }
78
+ }
79
+ }
80
+ function inputfocus() {
81
+ reactToInput();
82
+ }
83
+ let popuphilited;
84
+ $: if (popuphilited != null)
85
+ hilitedpill = undefined;
86
+ function reactToHilite(..._) {
87
+ if (!inputelement)
88
+ return;
89
+ if (hilitedpill) {
90
+ popuphilited = undefined;
91
+ inputelement.setAttribute('aria-activedescendant', id + hilitedpill);
92
+ }
93
+ else
94
+ inputelement.removeAttribute('aria-activedescendant');
95
+ }
96
+ $: reactToHilite(hilitedpill, id);
97
+ const dispatch = createEventDispatcher();
98
+ $: dispatch('change', selected);
99
+ </script>
100
+
101
+ <fieldset>
102
+ <ul class="multiselect-selected" role="listbox" on:click={() => inputelement.focus()}>
103
+ {#each selected as option, i}
104
+ <li id={id + option.value} role="option" tabindex="-1" class="multiselect-pill" class:hilited={hilitedpill === option.value}
105
+ on:click|preventDefault|stopPropagation={() => removeSelection(option, i, 1)}
106
+ aria-selected="true">
107
+ {option.label || option.value}
108
+ <ScreenReaderOnly>, click to remove</ScreenReaderOnly>
109
+ </li>
110
+ {/each}
111
+ <li class="input">
112
+ <input type="text" id={id} name={name} placeholder={placeholder}
113
+ bind:this={inputelement} bind:value={inputvalue}
114
+ on:focus={inputfocus} on:keydown={inputkeydown}
115
+ autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list"
116
+ aria-describedby="{descriptionid}">
117
+ </li>
118
+ </ul>
119
+ <ScreenReaderOnly id={descriptionid} arialive="assertive">
120
+ <span>{selected.length ? selected.length + ' selected' : 'select multiple, none selected'}, up down to choose, left right to hilite existing choices</span>
121
+ {#if menushown}<span>{availablemessage}, touch users explore to find autocomplete menu</span>{/if}
122
+ </ScreenReaderOnly>
123
+ <slot></slot>
124
+ </fieldset>
125
+ <svelte:component this={PopupMenu} bind:menushown bind:hilited={popuphilited} bind:value={popupvalue} align='bottomleft' {emptyText} {menuContainerClass} {menuClass} {menuItemClass} {menuItemHilitedClass} items={options} buttonelement={inputelement} on:change={addSelection}></svelte:component>
126
+
127
+ <style>
128
+ fieldset {
129
+ border: 0;
130
+ padding: 0;
131
+ margin: 0;
132
+ }
133
+ fieldset * {
134
+ box-sizing: border-box;
135
+ }
136
+ .multiselect-selected {
137
+ margin: 0;
138
+ padding: 0;
139
+ list-style: none;
140
+ display: flex;
141
+ flex-wrap: wrap;
142
+ align-items: stretch;
143
+ padding: var(--multiselect-padding, 0.3em);
144
+ border: var(--multiselect-border, 1px solid #666666);
145
+ border-radius: var(--multiselect-radius, 0.3em);
146
+ }
147
+ .multiselect-selected:focus-within {
148
+ outline-width: var(--multiselect-focus-width, 2px);
149
+ outline-style: solid;
150
+ outline-color: var(--multiselect-focus-color, Highlight);
151
+ }
152
+ li.input {
153
+ flex-grow: 1;
154
+ }
155
+ input {
156
+ outline: 0;
157
+ border: 0;
158
+ min-width: 0;
159
+ width: 100%;
160
+ height: 100%;
161
+ }
162
+ .multiselect-pill {
163
+ cursor: pointer;
164
+ flex-grow: 0;
165
+ margin-right: 0.3em;
166
+ line-height: 1;
167
+ border-radius: var(--multiselect-pill-radius, 0.8em);
168
+ border: var(--multiselect-pill-border, 1px solid gray);
169
+ background-color: var(--multiselect-pill-bg, transparent);
170
+ color: var(--multiselect-pill-text, black);
171
+ padding: var(--multiselect-pill-padding, 0.3em 0.5em);
172
+ }
173
+ .multiselect-pill.hilited {
174
+ outline: 0;
175
+ background-color: var(--multiselect-pill-selected, gray);
176
+ border: var(--multiselect-pill-selected-border, 1px solid transparent);
177
+ color: var(--multiselect-pill-selected-text, white);
178
+ }
179
+ </style>
@@ -0,0 +1,35 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ import DefaultPopupMenu from './PopupMenu.svelte';
3
+ import type { PopupMenuItem } from '../types';
4
+ declare const __propDef: {
5
+ props: {
6
+ id?: string;
7
+ name: string;
8
+ getOptions: (search: string) => Promise<PopupMenuItem[]> | PopupMenuItem[];
9
+ menuContainerClass?: string | undefined;
10
+ menuClass?: string | undefined;
11
+ menuItemClass?: string | undefined;
12
+ menuItemHilitedClass?: string | undefined;
13
+ selected?: {
14
+ value: string;
15
+ label: string;
16
+ }[];
17
+ placeholder?: string;
18
+ emptyText?: string | undefined;
19
+ PopupMenu?: typeof DefaultPopupMenu;
20
+ };
21
+ events: {
22
+ change: CustomEvent<any>;
23
+ } & {
24
+ [evt: string]: CustomEvent<any>;
25
+ };
26
+ slots: {
27
+ default: {};
28
+ };
29
+ };
30
+ export declare type MultiSelectProps = typeof __propDef.props;
31
+ export declare type MultiSelectEvents = typeof __propDef.events;
32
+ export declare type MultiSelectSlots = typeof __propDef.slots;
33
+ export default class MultiSelect extends SvelteComponentTyped<MultiSelectProps, MultiSelectEvents, MultiSelectSlots> {
34
+ }
35
+ export {};
@@ -3,6 +3,7 @@ import { randomid } from 'txstate-utils';
3
3
  import { Store } from '@txstate-mws/svelte-store';
4
4
  import { glue, portal } from '../actions';
5
5
  import ScreenReaderOnly from './ScreenReaderOnly.svelte';
6
+ import { modifierKey } from '../util';
6
7
  const dispatch = createEventDispatcher();
7
8
  export let menushown = false;
8
9
  export let menuContainerClass = '';
@@ -20,7 +21,7 @@ export let computedalign = new Store({ valign: 'bottom', halign: 'left' });
20
21
  export let usePortal = undefined;
21
22
  export let emptyText = undefined;
22
23
  export let value = undefined;
23
- let hilited = undefined;
24
+ export let hilited = undefined;
24
25
  let menuelement;
25
26
  const itemelements = [];
26
27
  const menuid = randomid();
@@ -43,15 +44,17 @@ async function reactToMenuShown(_) {
43
44
  }
44
45
  await tick();
45
46
  if (!menushown) {
46
- hilited = undefined;
47
47
  buttonelement.removeAttribute('aria-controls');
48
48
  buttonelement.setAttribute('aria-expanded', 'false');
49
+ if (hilited)
50
+ buttonelement.removeAttribute('aria-activedescendant');
51
+ hilited = undefined;
49
52
  }
50
53
  else {
51
54
  buttonelement.setAttribute('aria-controls', menuid);
52
55
  buttonelement.setAttribute('aria-expanded', 'true');
56
+ buttonelement.focus();
53
57
  }
54
- buttonelement.focus();
55
58
  }
56
59
  $: reactToMenuShown(menushown);
57
60
  function move(idx) {
@@ -64,6 +67,8 @@ function move(idx) {
64
67
  buttonelement.setAttribute('aria-activedescendant', `${menuid}-${hilited}`);
65
68
  }
66
69
  function onkeydown(e) {
70
+ if (modifierKey(e))
71
+ return;
67
72
  if (e.key === 'ArrowDown') {
68
73
  e.preventDefault();
69
74
  if (menushown) {
@@ -74,7 +79,7 @@ function onkeydown(e) {
74
79
  }
75
80
  else {
76
81
  menushown = true;
77
- move(firstactive);
82
+ tick().then(() => move(firstactive));
78
83
  }
79
84
  }
80
85
  else if (e.key === 'ArrowUp') {
@@ -87,7 +92,7 @@ function onkeydown(e) {
87
92
  }
88
93
  else {
89
94
  menushown = true;
90
- move(lastactive);
95
+ tick().then(() => move(lastactive));
91
96
  }
92
97
  }
93
98
  else if (e.key === 'Enter') {
@@ -101,7 +106,7 @@ function onkeydown(e) {
101
106
  }
102
107
  else {
103
108
  menushown = true;
104
- move(firstactive);
109
+ tick().then(() => move(firstactive));
105
110
  }
106
111
  }
107
112
  else if (e.key === ' ') {
@@ -125,7 +130,7 @@ function onkeydown(e) {
125
130
  if (buttonelement.tagName !== 'INPUT') {
126
131
  e.preventDefault();
127
132
  menushown = true;
128
- move(firstactive);
133
+ tick().then(() => move(firstactive));
129
134
  }
130
135
  }
131
136
  }
@@ -135,14 +140,17 @@ function onkeydown(e) {
135
140
  menushown = false;
136
141
  }
137
142
  }
143
+ let blurTimer;
138
144
  function onbuttonclick(e) {
139
145
  e.preventDefault();
146
+ cancelAnimationFrame(blurTimer);
140
147
  menushown = !menushown;
141
148
  }
142
149
  async function onblur(e) {
143
150
  // tabindex=-1 on our menu elements means e.relatedTarget will be set
144
- if (!(e.relatedTarget instanceof HTMLElement && menuelement?.contains(e.relatedTarget)))
145
- menushown = false;
151
+ if (!(e.relatedTarget instanceof HTMLElement && menuelement?.contains(e.relatedTarget))) {
152
+ blurTimer = requestAnimationFrame(() => { menushown = false; });
153
+ }
146
154
  }
147
155
  function cleanup(element) {
148
156
  if (element) {
@@ -20,6 +20,7 @@ declare const __propDef: {
20
20
  usePortal?: HTMLElement | true | undefined;
21
21
  emptyText?: string | undefined;
22
22
  value?: string | undefined;
23
+ hilited?: number | undefined;
23
24
  };
24
25
  events: {
25
26
  change: CustomEvent<any>;
@@ -1,6 +1,9 @@
1
+ export { default as CardLayout } from './CardLayout.svelte';
2
+ export { default as Card } from './Card.svelte';
1
3
  export { default as CollapsingTable } from './CollapsingTable.svelte';
2
4
  export { default as FocusLock } from './FocusLock.svelte';
3
5
  export { default as ConditionalWrapper } from './ConditionalWrapper.svelte';
4
6
  export { default as Modal } from './Modal.svelte';
7
+ export { default as MultiSelect } from './MultiSelect.svelte';
5
8
  export { default as PopupMenu } from './PopupMenu.svelte';
6
9
  export { default as ScreenReaderOnly } from './ScreenReaderOnly.svelte';
@@ -1,6 +1,9 @@
1
+ export { default as CardLayout } from './CardLayout.svelte';
2
+ export { default as Card } from './Card.svelte';
1
3
  export { default as CollapsingTable } from './CollapsingTable.svelte';
2
4
  export { default as FocusLock } from './FocusLock.svelte';
3
5
  export { default as ConditionalWrapper } from './ConditionalWrapper.svelte';
4
6
  export { default as Modal } from './Modal.svelte';
7
+ export { default as MultiSelect } from './MultiSelect.svelte';
5
8
  export { default as PopupMenu } from './PopupMenu.svelte';
6
9
  export { default as ScreenReaderOnly } from './ScreenReaderOnly.svelte';
package/package.json CHANGED
@@ -1,32 +1,33 @@
1
1
  {
2
2
  "name": "@txstate-mws/svelte-components",
3
- "version": "1.0.3",
3
+ "version": "1.1.2",
4
4
  "description": "Svelte components that are generically useful.",
5
5
  "dependencies": {
6
- "@txstate-mws/svelte-store": "^1.0.2",
6
+ "@txstate-mws/svelte-store": "^1.0.3",
7
7
  "tabbable": "^5.2.1",
8
8
  "txstate-utils": "^1.6.0"
9
9
  },
10
10
  "devDependencies": {
11
- "@babel/preset-env": "^7.16.11",
11
+ "@playwright/test": "^1.18.1",
12
12
  "@sveltejs/adapter-auto": "next",
13
13
  "@sveltejs/kit": "next",
14
- "@testing-library/jest-dom": "^5.16.1",
15
- "@testing-library/svelte": "^3.0.3",
16
- "@testing-library/user-event": "^13.5.0",
17
14
  "@types/tabbable": "^3.1.2",
18
15
  "eslint-config-standard-with-typescript": "^21.0.1",
19
16
  "eslint-plugin-svelte3": "^3.2.1",
20
17
  "svelte": "^3.44.0",
21
- "svelte2tsx": "^0.5.1",
22
18
  "svelte-check": "^2.2.6",
23
- "svelte-jester": "^2.3.1",
24
19
  "svelte-preprocess": "^4.9.4",
25
- "ts-jest": "^27.1.3",
20
+ "svelte2tsx": "^0.5.2",
26
21
  "tslib": "^2.3.1",
27
22
  "typescript": "^4.4.3"
28
23
  },
29
24
  "type": "module",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git@github.com:txstate-etc/svelte-components.git"
28
+ },
29
+ "author": "Nick Wing",
30
+ "license": "MIT",
30
31
  "exports": {
31
32
  "./package.json": "./package.json",
32
33
  "./actions/buttonify": "./actions/buttonify.js",
@@ -37,14 +38,18 @@
37
38
  "./actions/resize": "./actions/resize.js",
38
39
  "./actions/sticky": "./actions/sticky.js",
39
40
  "./actions/stickyfixed": "./actions/stickyfixed.js",
41
+ "./components/Card.svelte": "./components/Card.svelte",
42
+ "./components/CardLayout.svelte": "./components/CardLayout.svelte",
40
43
  "./components/CollapsingTable.svelte": "./components/CollapsingTable.svelte",
41
44
  "./components/ConditionalWrapper.svelte": "./components/ConditionalWrapper.svelte",
42
45
  "./components/FocusLock.svelte": "./components/FocusLock.svelte",
43
46
  "./components/Modal.svelte": "./components/Modal.svelte",
47
+ "./components/MultiSelect.svelte": "./components/MultiSelect.svelte",
44
48
  "./components/PopupMenu.svelte": "./components/PopupMenu.svelte",
45
49
  "./components/ScreenReaderOnly.svelte": "./components/ScreenReaderOnly.svelte",
46
50
  "./components": "./components/index.js",
47
51
  ".": "./index.js",
52
+ "./types/CardLayout": "./types/CardLayout.js",
48
53
  "./types/CollapsingTable": "./types/CollapsingTable.js",
49
54
  "./types/PopupMenu": "./types/PopupMenu.js",
50
55
  "./types": "./types/index.js",
@@ -0,0 +1,14 @@
1
+ import type { SettableSubject } from '@txstate-mws/svelte-store';
2
+ export declare const CARDLAYOUT: {};
3
+ export interface Block {
4
+ element: HTMLElement;
5
+ order?: SettableSubject<number>;
6
+ linebreak?: SettableSubject<boolean>;
7
+ width?: SettableSubject<string>;
8
+ height?: number;
9
+ }
10
+ export interface CardLayoutContext {
11
+ registerBlock: (block: Block) => Block;
12
+ recalculate: () => void;
13
+ gutter: SettableSubject<number>;
14
+ }
@@ -0,0 +1 @@
1
+ export const CARDLAYOUT = {};
package/types/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
+ export * from './CardLayout.js';
1
2
  export * from './CollapsingTable.js';
2
3
  export * from './PopupMenu.js';
package/types/index.js CHANGED
@@ -1,2 +1,3 @@
1
+ export * from './CardLayout.js';
1
2
  export * from './CollapsingTable.js';
2
3
  export * from './PopupMenu.js';
@@ -1,5 +1,4 @@
1
- import equal from 'fast-deep-equal';
2
- import { isNotNull, randomid, toArray } from 'txstate-utils';
1
+ import { equal, isNotNull, randomid, toArray } from 'txstate-utils';
3
2
  import { bodyOffset, targetOffset } from './bodyoffset.js';
4
3
  const observers = new Map();
5
4
  let mutationobserver;
package/util/util.d.ts CHANGED
@@ -1,2 +1,5 @@
1
1
  export declare function classes(...classNames: (string | undefined)[]): string;
2
2
  export declare function debounced(fn: Function, timeout: number): (...args: any[]) => void;
3
+ export declare function debouncedPromise<T>(fn: (..._: any) => Promise<T> | T, timeout: number, defaultValue: T): (..._: any) => Promise<T>;
4
+ export declare function modifierKey(e: any): any;
5
+ export declare function selectionIsLeft(ele: HTMLInputElement | HTMLTextAreaElement): boolean;
package/util/util.js CHANGED
@@ -8,3 +8,26 @@ export function debounced(fn, timeout) {
8
8
  timer = setTimeout(() => fn(...args), timeout);
9
9
  };
10
10
  }
11
+ export function debouncedPromise(fn, timeout, defaultValue) {
12
+ let version = 0;
13
+ return async (...args) => {
14
+ const myversion = ++version;
15
+ return await new Promise(resolve => {
16
+ setTimeout(() => {
17
+ if (version === myversion) {
18
+ version = 0;
19
+ resolve(fn(...args));
20
+ }
21
+ else {
22
+ resolve(defaultValue);
23
+ }
24
+ }, timeout);
25
+ });
26
+ };
27
+ }
28
+ export function modifierKey(e) {
29
+ return e.ctrlKey || e.altKey || e.metaKey || e.key === 'Insert';
30
+ }
31
+ export function selectionIsLeft(ele) {
32
+ return ele.selectionStart === 0 && ele.selectionEnd === 0;
33
+ }