@txstate-mws/svelte-components 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +37 -0
- package/README.md +33 -14
- package/docs/stores.md +1 -1
- package/package.json +24 -74
- package/playwright.config.js +112 -0
- package/src/app.html +14 -0
- package/src/global.d.ts +1 -0
- package/src/lib/actions/buttonify.ts +27 -0
- package/src/lib/actions/glue.ts +96 -0
- package/src/lib/actions/index.ts +7 -0
- package/src/lib/actions/offset.ts +43 -0
- package/src/lib/actions/portal.ts +13 -0
- package/src/lib/actions/resize.ts +72 -0
- package/src/lib/actions/sticky.ts +105 -0
- package/src/lib/actions/stickyfixed.ts +160 -0
- package/src/lib/components/Card.svelte +25 -0
- package/src/lib/components/CardLayout.svelte +188 -0
- package/{dist/ts → src/lib}/components/CollapsingTable.svelte +6 -5
- package/{dist/ts → src/lib}/components/ConditionalWrapper.svelte +0 -0
- package/{dist/ts → src/lib}/components/FocusLock.svelte +7 -6
- package/{dist/ts → src/lib}/components/Modal.svelte +5 -4
- package/src/lib/components/MultiSelect.svelte +174 -0
- package/{dist/ts → src/lib}/components/PopupMenu.svelte +22 -16
- package/{dist/ts → src/lib}/components/ScreenReaderOnly.svelte +2 -2
- package/src/lib/components/index.ts +9 -0
- package/src/lib/index.ts +4 -0
- package/src/lib/types/CardLayout.ts +14 -0
- package/src/lib/types/CollapsingTable.ts +8 -0
- package/src/lib/types/PopupMenu.ts +5 -0
- package/src/lib/types/index.ts +3 -0
- package/src/lib/util/bodyoffset.ts +77 -0
- package/src/lib/util/index.ts +3 -0
- package/src/lib/util/repositioned.ts +91 -0
- package/src/lib/util/util.ts +36 -0
- package/src/routes/__layout.svelte +125 -0
- package/src/routes/actions/index.svelte +1 -0
- package/src/routes/actions/offset.svelte +4 -0
- package/src/routes/actions/offsetdebounce.svelte +4 -0
- package/src/routes/actions/resize.svelte +4 -0
- package/src/routes/actions/resizedebounce.svelte +4 -0
- package/src/routes/actions/sticky.svelte +4 -0
- package/src/routes/cards/index.svelte +4 -0
- package/src/routes/cards/preserve.svelte +4 -0
- package/src/routes/collapsingtable/index.svelte +4 -0
- package/src/routes/collapsingtable/stickyheader.svelte +4 -0
- package/src/routes/index.svelte +2 -0
- package/src/routes/modal.svelte +4 -0
- package/src/routes/multiselect.svelte +4 -0
- package/src/routes/popupmenu/body.svelte +4 -0
- package/src/routes/popupmenu/complex.svelte +4 -0
- package/src/routes/popupmenu/empty.svelte +5 -0
- package/src/routes/popupmenu/index.svelte +4 -0
- package/src/routes/popupmenu/parent.svelte +4 -0
- package/src/routes/popupmenu/scrollable.svelte +27 -0
- package/src/routes/popupmenu/showselected.svelte +4 -0
- package/src/test/Cards.svelte +64 -0
- package/{dist/ts/tests/CollapsingTable → src/test}/CollapsingTableTest.svelte +2 -2
- package/{dist/ts/tests/Modal → src/test}/ModalTest.svelte +3 -3
- package/src/test/MultiSelectTest.svelte +26 -0
- package/{dist/ts/tests/Actions → src/test}/Offset.svelte +2 -4
- package/{dist/ts/tests/PopupMenu → src/test}/PopupMenuBody.svelte +1 -1
- package/{dist/ts/tests/PopupMenu → src/test}/PopupMenuButtonIsParent.svelte +2 -2
- package/{dist/ts/tests/PopupMenu → src/test}/PopupMenuComplex.svelte +2 -2
- package/{dist/ts/tests/PopupMenu → src/test}/PopupMenuSimple.svelte +2 -2
- package/{dist/ts/tests/Actions → src/test}/Resize.svelte +2 -4
- package/{dist/ts/tests/Actions → src/test}/Sticky.svelte +1 -1
- package/static/layout.css +263 -0
- package/svelte.config.js +15 -0
- package/tests/.eslintrc.cjs +19 -0
- package/tests/CardLayout.test.js +41 -0
- package/tests/CollapsingTable.test.js +37 -0
- package/tests/Modal.test.js +18 -0
- package/tests/MultiSelect.test.js +58 -0
- package/tests/PopupMenu.test.js +55 -0
- package/tests/common.js +4 -0
- package/tsconfig.json +31 -0
- package/LICENSE +0 -21
- package/dist/actions/buttonify.js +0 -31
- package/dist/actions/buttonify.js.map +0 -1
- package/dist/actions/glue.js +0 -87
- package/dist/actions/glue.js.map +0 -1
- package/dist/actions/index.js +0 -20
- package/dist/actions/index.js.map +0 -1
- package/dist/actions/offset.js +0 -35
- package/dist/actions/offset.js.map +0 -1
- package/dist/actions/portal.js +0 -19
- package/dist/actions/portal.js.map +0 -1
- package/dist/actions/resize.js +0 -48
- package/dist/actions/resize.js.map +0 -1
- package/dist/actions/sticky.js +0 -93
- package/dist/actions/sticky.js.map +0 -1
- package/dist/actions/stickyfixed.js +0 -166
- package/dist/actions/stickyfixed.js.map +0 -1
- package/dist/components/CollapsingTable.svelte +0 -147
- package/dist/components/ConditionalWrapper.svelte +0 -38
- package/dist/components/FocusLock.svelte +0 -105
- package/dist/components/Modal.svelte +0 -52
- package/dist/components/PopupMenu.svelte +0 -282
- package/dist/components/ScreenReaderOnly.svelte +0 -25
- package/dist/components/index.js +0 -19
- package/dist/components/index.js.map +0 -1
- package/dist/index.js +0 -17
- package/dist/index.js.map +0 -1
- package/dist/lib/activestore.js +0 -123
- package/dist/lib/activestore.js.map +0 -1
- package/dist/lib/bodyoffset.js +0 -76
- package/dist/lib/bodyoffset.js.map +0 -1
- package/dist/lib/convertedstore.js +0 -41
- package/dist/lib/convertedstore.js.map +0 -1
- package/dist/lib/derivedstore.js +0 -65
- package/dist/lib/derivedstore.js.map +0 -1
- package/dist/lib/index.js +0 -22
- package/dist/lib/index.js.map +0 -1
- package/dist/lib/repositioned.js +0 -92
- package/dist/lib/repositioned.js.map +0 -1
- package/dist/lib/safestore.js +0 -16
- package/dist/lib/safestore.js.map +0 -1
- package/dist/lib/store.js +0 -27
- package/dist/lib/store.js.map +0 -1
- package/dist/lib/substore.js +0 -47
- package/dist/lib/substore.js.map +0 -1
- package/dist/lib/util.js +0 -16
- package/dist/lib/util.js.map +0 -1
- package/dist/ts/actions/buttonify.d.ts +0 -8
- package/dist/ts/actions/glue.d.ts +0 -16
- package/dist/ts/actions/index.d.ts +0 -7
- package/dist/ts/actions/offset.d.ts +0 -15
- package/dist/ts/actions/portal.d.ts +0 -4
- package/dist/ts/actions/resize.d.ts +0 -16
- package/dist/ts/actions/sticky.d.ts +0 -33
- package/dist/ts/actions/stickyfixed.d.ts +0 -25
- package/dist/ts/components/index.d.ts +0 -6
- package/dist/ts/index.d.ts +0 -4
- package/dist/ts/lib/activestore.d.ts +0 -60
- package/dist/ts/lib/bodyoffset.d.ts +0 -13
- package/dist/ts/lib/convertedstore.d.ts +0 -16
- package/dist/ts/lib/derivedstore.d.ts +0 -23
- package/dist/ts/lib/index.d.ts +0 -9
- package/dist/ts/lib/repositioned.d.ts +0 -13
- package/dist/ts/lib/safestore.d.ts +0 -4
- package/dist/ts/lib/store.d.ts +0 -16
- package/dist/ts/lib/substore.d.ts +0 -24
- package/dist/ts/lib/util.d.ts +0 -2
- package/dist/ts/tests/Actions/Actions.stories.d.ts +0 -1
- package/dist/ts/tests/CollapsingTable/CollapsingTable.stories.d.ts +0 -1
- package/dist/ts/tests/CollapsingTable/CollapsingTable.test.d.ts +0 -1
- package/dist/ts/tests/Modal/Modal.stories.d.ts +0 -1
- package/dist/ts/tests/Modal/Modal.test.d.ts +0 -1
- package/dist/ts/tests/PopupMenu/PopupMenu.stories.d.ts +0 -1
- package/dist/ts/tests/PopupMenu/PopupMenu.test.d.ts +0 -1
- package/dist/ts/tests/stores/deepstore.test.d.ts +0 -1
- package/dist/ts/types/CollapsingTable.d.ts +0 -8
- package/dist/ts/types/PopupMenu.d.ts +0 -5
- package/dist/ts/types/index.d.ts +0 -2
- package/dist/types/CollapsingTable.js +0 -3
- package/dist/types/CollapsingTable.js.map +0 -1
- package/dist/types/PopupMenu.js +0 -3
- package/dist/types/PopupMenu.js.map +0 -1
- package/dist/types/index.js +0 -15
- package/dist/types/index.js.map +0 -1
- package/esm/index.js +0 -1
- package/esm/package.json +0 -3
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createEventDispatcher } from 'svelte'
|
|
3
|
+
import { randomid } from 'txstate-utils'
|
|
4
|
+
import ScreenReaderOnly from './ScreenReaderOnly.svelte'
|
|
5
|
+
import DefaultPopupMenu from './PopupMenu.svelte'
|
|
6
|
+
import { debouncedPromise, modifierKey, selectionIsLeft } from '$lib/util'
|
|
7
|
+
import type { PopupMenuItem } from '$lib/types'
|
|
8
|
+
|
|
9
|
+
export let id = randomid()
|
|
10
|
+
export let name: string
|
|
11
|
+
export let getOptions: (search: string) => Promise<PopupMenuItem[]>|PopupMenuItem[]
|
|
12
|
+
export let menuContainerClass: string|undefined = undefined
|
|
13
|
+
export let menuClass: string|undefined = undefined
|
|
14
|
+
export let menuItemClass: string|undefined = undefined
|
|
15
|
+
export let menuItemHilitedClass: string|undefined = undefined
|
|
16
|
+
export let selected: { value: string, label: string }[] = []
|
|
17
|
+
export let placeholder = ''
|
|
18
|
+
export let emptyText: string|undefined = undefined
|
|
19
|
+
export let PopupMenu = DefaultPopupMenu
|
|
20
|
+
|
|
21
|
+
let menushown: boolean
|
|
22
|
+
let options = []
|
|
23
|
+
let hilitedpill: string|undefined
|
|
24
|
+
let inputvalue = ''
|
|
25
|
+
let popupvalue = undefined
|
|
26
|
+
let inputelement: HTMLInputElement
|
|
27
|
+
const descriptionid = randomid()
|
|
28
|
+
|
|
29
|
+
$: debouncedGetOptions = debouncedPromise(getOptions, 100, [])
|
|
30
|
+
$: selectedSet = new Set(selected.map(s => s.value))
|
|
31
|
+
async function reactToInput (..._: any) {
|
|
32
|
+
const saveval = inputvalue
|
|
33
|
+
const rawOptions = await debouncedGetOptions(saveval)
|
|
34
|
+
if (inputvalue !== saveval) return // ignore any results that are out of date
|
|
35
|
+
options = rawOptions.filter(o => !selectedSet.has(o.value))
|
|
36
|
+
if (typeof document !== 'undefined' && inputelement === document.activeElement) menushown = true
|
|
37
|
+
}
|
|
38
|
+
$: reactToInput(inputvalue, debouncedGetOptions, selectedSet)
|
|
39
|
+
$: availablemessage = options.filter(o => o.value).length + ' autocomplete choices available'
|
|
40
|
+
|
|
41
|
+
function addSelection (e: CustomEvent & { detail: PopupMenuItem }) {
|
|
42
|
+
inputvalue = ''
|
|
43
|
+
const opt = { ...e.detail, label: e.detail.label || e.detail.value }
|
|
44
|
+
selected = [...selected, opt]
|
|
45
|
+
popupvalue = undefined
|
|
46
|
+
}
|
|
47
|
+
function removeSelection (opt: typeof selected[number], idx: number, nextfocus = 1) {
|
|
48
|
+
if (hilitedpill === opt.value) {
|
|
49
|
+
hilitedpill = selected[idx + nextfocus]?.value
|
|
50
|
+
}
|
|
51
|
+
selected = selected.filter(s => s.value !== opt.value)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function inputkeydown (e) {
|
|
55
|
+
if (modifierKey(e)) return
|
|
56
|
+
if (e.key === 'ArrowLeft' && selectionIsLeft(inputelement)) {
|
|
57
|
+
const idx = hilitedpill ? selected.findIndex(s => s.value === hilitedpill) : selected.length
|
|
58
|
+
if (idx !== 0) hilitedpill = selected[idx - 1].value
|
|
59
|
+
} else if (e.key === 'ArrowRight' && selectionIsLeft(inputelement)) {
|
|
60
|
+
if (hilitedpill) e.preventDefault()
|
|
61
|
+
const idx = hilitedpill ? selected.findIndex(s => s.value === hilitedpill) : undefined
|
|
62
|
+
if (idx != null) {
|
|
63
|
+
if (idx === selected.length - 1) hilitedpill = undefined
|
|
64
|
+
else hilitedpill = selected[idx + 1].value
|
|
65
|
+
}
|
|
66
|
+
} else if (['Backspace', 'Delete', 'Clear', ' ', 'Enter'].includes(e.key)) {
|
|
67
|
+
if (hilitedpill) {
|
|
68
|
+
const idx = selected.findIndex(s => s.value === hilitedpill)
|
|
69
|
+
removeSelection(selected[idx], idx, e.key === 'Delete' ? 1 : -1)
|
|
70
|
+
e.preventDefault()
|
|
71
|
+
} else if (e.key === 'Backspace' && selectionIsLeft(inputelement)) {
|
|
72
|
+
removeSelection(selected[selected.length - 1], selected.length - 1, -1)
|
|
73
|
+
e.preventDefault()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function inputfocus () {
|
|
78
|
+
reactToInput()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let popuphilited
|
|
82
|
+
$: if (popuphilited != null) hilitedpill = undefined
|
|
83
|
+
function reactToHilite (..._: any) {
|
|
84
|
+
if (!inputelement) return
|
|
85
|
+
if (hilitedpill) {
|
|
86
|
+
popuphilited = undefined
|
|
87
|
+
inputelement.setAttribute('aria-activedescendant', id + hilitedpill)
|
|
88
|
+
} else inputelement.removeAttribute('aria-activedescendant')
|
|
89
|
+
}
|
|
90
|
+
$: reactToHilite(hilitedpill, id)
|
|
91
|
+
|
|
92
|
+
const dispatch = createEventDispatcher()
|
|
93
|
+
$: dispatch('change', selected)
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<fieldset>
|
|
97
|
+
<ul class="multiselect-selected" role="listbox" on:click={() => inputelement.focus()}>
|
|
98
|
+
{#each selected as option, i}
|
|
99
|
+
<li id={id + option.value} role="option" tabindex="-1" class="multiselect-pill" class:hilited={hilitedpill === option.value}
|
|
100
|
+
on:click|preventDefault|stopPropagation={() => removeSelection(option, i, 1)}
|
|
101
|
+
aria-selected="true">
|
|
102
|
+
{option.label || option.value}
|
|
103
|
+
<ScreenReaderOnly>, click to remove</ScreenReaderOnly>
|
|
104
|
+
</li>
|
|
105
|
+
{/each}
|
|
106
|
+
<li class="input">
|
|
107
|
+
<input type="text" id={id} name={name} placeholder={placeholder}
|
|
108
|
+
bind:this={inputelement} bind:value={inputvalue}
|
|
109
|
+
on:focus={inputfocus} on:keydown={inputkeydown}
|
|
110
|
+
autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list"
|
|
111
|
+
aria-describedby="{descriptionid}">
|
|
112
|
+
</li>
|
|
113
|
+
</ul>
|
|
114
|
+
<ScreenReaderOnly id={descriptionid} arialive="assertive">
|
|
115
|
+
<span>{selected.length ? selected.length + ' selected' : 'select multiple, none selected'}, up down to choose, left right to hilite existing choices</span>
|
|
116
|
+
{#if menushown}<span>{availablemessage}, touch users explore to find autocomplete menu</span>{/if}
|
|
117
|
+
</ScreenReaderOnly>
|
|
118
|
+
<slot></slot>
|
|
119
|
+
</fieldset>
|
|
120
|
+
<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>
|
|
121
|
+
|
|
122
|
+
<style>
|
|
123
|
+
fieldset {
|
|
124
|
+
border: 0;
|
|
125
|
+
padding: 0;
|
|
126
|
+
margin: 0;
|
|
127
|
+
}
|
|
128
|
+
fieldset * {
|
|
129
|
+
box-sizing: border-box;
|
|
130
|
+
}
|
|
131
|
+
.multiselect-selected {
|
|
132
|
+
margin: 0;
|
|
133
|
+
padding: 0;
|
|
134
|
+
list-style: none;
|
|
135
|
+
display: flex;
|
|
136
|
+
flex-wrap: wrap;
|
|
137
|
+
align-items: stretch;
|
|
138
|
+
padding: var(--multiselect-padding, 0.3em);
|
|
139
|
+
border: var(--multiselect-border, 1px solid #666666);
|
|
140
|
+
border-radius: var(--multiselect-radius, 0.3em);
|
|
141
|
+
}
|
|
142
|
+
.multiselect-selected:focus-within {
|
|
143
|
+
outline-width: var(--multiselect-focus-width, 2px);
|
|
144
|
+
outline-style: solid;
|
|
145
|
+
outline-color: var(--multiselect-focus-color, Highlight);
|
|
146
|
+
}
|
|
147
|
+
li.input {
|
|
148
|
+
flex-grow: 1;
|
|
149
|
+
}
|
|
150
|
+
input {
|
|
151
|
+
outline: 0;
|
|
152
|
+
border: 0;
|
|
153
|
+
min-width: 0;
|
|
154
|
+
width: 100%;
|
|
155
|
+
height: 100%;
|
|
156
|
+
}
|
|
157
|
+
.multiselect-pill {
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
flex-grow: 0;
|
|
160
|
+
margin-right: 0.3em;
|
|
161
|
+
line-height: 1;
|
|
162
|
+
border-radius: var(--multiselect-pill-radius, 0.8em);
|
|
163
|
+
border: var(--multiselect-pill-border, 1px solid gray);
|
|
164
|
+
background-color: var(--multiselect-pill-bg, transparent);
|
|
165
|
+
color: var(--multiselect-pill-text, black);
|
|
166
|
+
padding: var(--multiselect-pill-padding, 0.3em 0.5em);
|
|
167
|
+
}
|
|
168
|
+
.multiselect-pill.hilited {
|
|
169
|
+
outline: 0;
|
|
170
|
+
background-color: var(--multiselect-pill-selected, gray);
|
|
171
|
+
border: var(--multiselect-pill-selected-border, 1px solid transparent);
|
|
172
|
+
color: var(--multiselect-pill-selected-text, white);
|
|
173
|
+
}
|
|
174
|
+
</style>
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { GlueAlignOpts, GlueAlignStore } from '../actions'
|
|
3
|
-
import { glue, portal } from '../actions'
|
|
4
2
|
import { createEventDispatcher, onDestroy, tick } from 'svelte'
|
|
5
3
|
import { randomid } from 'txstate-utils'
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
4
|
+
import { Store } from '@txstate-mws/svelte-store'
|
|
5
|
+
import { glue, portal } from '$lib/actions'
|
|
6
|
+
import type { GlueAlignOpts, GlueAlignStore } from '$lib/actions'
|
|
7
|
+
import type { PopupMenuItem } from '$lib/types'
|
|
8
8
|
import ScreenReaderOnly from './ScreenReaderOnly.svelte'
|
|
9
|
+
import { modifierKey } from '$lib/util'
|
|
9
10
|
const dispatch = createEventDispatcher()
|
|
10
11
|
|
|
11
12
|
export let menushown = false
|
|
@@ -24,10 +25,9 @@
|
|
|
24
25
|
export let usePortal: HTMLElement|true|undefined = undefined
|
|
25
26
|
export let emptyText: string|undefined = undefined
|
|
26
27
|
export let value: string|undefined = undefined
|
|
27
|
-
|
|
28
|
-
let hilited: number|undefined = undefined
|
|
28
|
+
export let hilited: number|undefined = undefined
|
|
29
29
|
let menuelement: HTMLElement|undefined
|
|
30
|
-
|
|
30
|
+
const itemelements: HTMLElement[] = []
|
|
31
31
|
const menuid = randomid()
|
|
32
32
|
let firstactive = 0
|
|
33
33
|
let lastactive = items.length - 1
|
|
@@ -50,14 +50,15 @@
|
|
|
50
50
|
}
|
|
51
51
|
await tick()
|
|
52
52
|
if (!menushown) {
|
|
53
|
-
hilited = undefined
|
|
54
53
|
buttonelement.removeAttribute('aria-controls')
|
|
55
54
|
buttonelement.setAttribute('aria-expanded', 'false')
|
|
55
|
+
if (hilited) buttonelement.removeAttribute('aria-activedescendant')
|
|
56
|
+
hilited = undefined
|
|
56
57
|
} else {
|
|
57
58
|
buttonelement.setAttribute('aria-controls', menuid)
|
|
58
59
|
buttonelement.setAttribute('aria-expanded', 'true')
|
|
60
|
+
buttonelement.focus()
|
|
59
61
|
}
|
|
60
|
-
buttonelement.focus()
|
|
61
62
|
}
|
|
62
63
|
$: reactToMenuShown(menushown)
|
|
63
64
|
|
|
@@ -70,25 +71,26 @@
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
function onkeydown (e: KeyboardEvent) {
|
|
74
|
+
if (modifierKey(e)) return
|
|
73
75
|
if (e.key === 'ArrowDown') {
|
|
74
76
|
e.preventDefault()
|
|
75
77
|
if (menushown) {
|
|
76
78
|
let i = (hilited ?? firstactive - 1) + 1
|
|
77
|
-
while(items[i]?.disabled || hiddenItem(items[i])) i++
|
|
79
|
+
while (items[i]?.disabled || hiddenItem(items[i])) i++
|
|
78
80
|
move(i)
|
|
79
81
|
} else {
|
|
80
82
|
menushown = true
|
|
81
|
-
move(firstactive)
|
|
83
|
+
tick().then(() => move(firstactive))
|
|
82
84
|
}
|
|
83
85
|
} else if (e.key === 'ArrowUp') {
|
|
84
86
|
e.preventDefault()
|
|
85
87
|
if (menushown) {
|
|
86
88
|
let i = (hilited ?? lastactive + 1) - 1
|
|
87
|
-
while(items[i]?.disabled || hiddenItem(items[i])) i--
|
|
89
|
+
while (items[i]?.disabled || hiddenItem(items[i])) i--
|
|
88
90
|
move(i)
|
|
89
91
|
} else {
|
|
90
92
|
menushown = true
|
|
91
|
-
move(lastactive)
|
|
93
|
+
tick().then(() => move(lastactive))
|
|
92
94
|
}
|
|
93
95
|
} else if (e.key === 'Enter') {
|
|
94
96
|
e.preventDefault()
|
|
@@ -100,7 +102,7 @@
|
|
|
100
102
|
}
|
|
101
103
|
} else {
|
|
102
104
|
menushown = true
|
|
103
|
-
move(firstactive)
|
|
105
|
+
tick().then(() => move(firstactive))
|
|
104
106
|
}
|
|
105
107
|
} else if (e.key === ' ') {
|
|
106
108
|
// buttonelement might be a text input if this popup is searchable,
|
|
@@ -121,7 +123,7 @@
|
|
|
121
123
|
if (buttonelement.tagName !== 'INPUT') {
|
|
122
124
|
e.preventDefault()
|
|
123
125
|
menushown = true
|
|
124
|
-
move(firstactive)
|
|
126
|
+
tick().then(() => move(firstactive))
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
} else if (menushown && e.key === 'Escape') {
|
|
@@ -131,14 +133,18 @@
|
|
|
131
133
|
}
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
let blurTimer: any
|
|
134
137
|
function onbuttonclick (e: MouseEvent) {
|
|
135
138
|
e.preventDefault()
|
|
139
|
+
cancelAnimationFrame(blurTimer)
|
|
136
140
|
menushown = !menushown
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
async function onblur (e: FocusEvent) {
|
|
140
144
|
// tabindex=-1 on our menu elements means e.relatedTarget will be set
|
|
141
|
-
if (!(e.relatedTarget instanceof HTMLElement && menuelement?.contains(e.relatedTarget)))
|
|
145
|
+
if (!(e.relatedTarget instanceof HTMLElement && menuelement?.contains(e.relatedTarget))) {
|
|
146
|
+
blurTimer = requestAnimationFrame(() => { menushown = false })
|
|
147
|
+
}
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
function cleanup (element: HTMLElement) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
export let id: string|undefined = undefined
|
|
3
|
-
export let arialive:
|
|
4
|
-
export let ariaatomic:
|
|
3
|
+
export let arialive: 'off' | 'assertive' | 'polite' | undefined = undefined
|
|
4
|
+
export let ariaatomic: boolean | 'false' | 'true' | undefined = undefined
|
|
5
5
|
</script>
|
|
6
6
|
|
|
7
7
|
<style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { default as CardLayout } from './CardLayout.svelte'
|
|
2
|
+
export { default as Card } from './Card.svelte'
|
|
3
|
+
export { default as CollapsingTable } from './CollapsingTable.svelte'
|
|
4
|
+
export { default as FocusLock } from './FocusLock.svelte'
|
|
5
|
+
export { default as ConditionalWrapper } from './ConditionalWrapper.svelte'
|
|
6
|
+
export { default as Modal } from './Modal.svelte'
|
|
7
|
+
export { default as MultiSelect } from './MultiSelect.svelte'
|
|
8
|
+
export { default as PopupMenu } from './PopupMenu.svelte'
|
|
9
|
+
export { default as ScreenReaderOnly } from './ScreenReaderOnly.svelte'
|
package/src/lib/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SettableSubject } from '@txstate-mws/svelte-store'
|
|
2
|
+
export 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,77 @@
|
|
|
1
|
+
export type ElementOffsets = Partial<ReturnType<typeof bodyOffset>>
|
|
2
|
+
|
|
3
|
+
export function bodyOffset (ele: HTMLElement) {
|
|
4
|
+
return targetOffset(ele, document.body)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function targetOffset (ele: HTMLElement, target: HTMLElement) {
|
|
8
|
+
const offset = targetOffsetRecursive(ele, target)
|
|
9
|
+
return {
|
|
10
|
+
...offset,
|
|
11
|
+
right: target.offsetWidth - offset.left - ele.offsetWidth,
|
|
12
|
+
bottom: target.offsetHeight - offset.top - ele.offsetHeight
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function sharedOffsetParent (a: HTMLElement, b: HTMLElement) {
|
|
17
|
+
if (!a || !b) return undefined
|
|
18
|
+
if (a.contains(b) && (a.firstElementChild as HTMLElement).offsetParent === a) return a
|
|
19
|
+
if (b.contains(a) && (b.firstElementChild as HTMLElement).offsetParent === b) return b
|
|
20
|
+
let c = a.offsetParent as HTMLElement
|
|
21
|
+
while (c instanceof HTMLElement && !c.contains(b)) c = c.offsetParent as HTMLElement
|
|
22
|
+
return c
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function targetOffsetRecursive (ele: HTMLElement, target: HTMLElement, currentOffset?: { top: number, left: number }): { top: number, left: number } {
|
|
26
|
+
const { x, y } = getTranslate(ele)
|
|
27
|
+
const offset = {
|
|
28
|
+
top: ele.offsetTop + (currentOffset?.top ?? 0) + y,
|
|
29
|
+
left: ele.offsetLeft + (currentOffset?.left ?? 0) + x
|
|
30
|
+
}
|
|
31
|
+
if (ele.offsetParent instanceof HTMLElement && target !== ele.offsetParent && target.contains(ele.offsetParent)) return targetOffsetRecursive(ele.offsetParent, target, offset)
|
|
32
|
+
return offset
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getTranslate (element: HTMLElement) {
|
|
36
|
+
const style = window.getComputedStyle(element)
|
|
37
|
+
const matrix = style.transform
|
|
38
|
+
|
|
39
|
+
// No transform property. Simply return 0 values.
|
|
40
|
+
if (matrix === 'none' || !matrix) {
|
|
41
|
+
return {
|
|
42
|
+
x: 0,
|
|
43
|
+
y: 0,
|
|
44
|
+
z: 0
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Can either be 2d or 3d transform
|
|
49
|
+
const matrixType = matrix.includes('3d') ? '3d' : '2d'
|
|
50
|
+
const matrixValues = matrix.match(/matrix.*\((.+)\)/)![1].split(', ')
|
|
51
|
+
|
|
52
|
+
// 2d matrices have 6 values
|
|
53
|
+
// Last 2 values are X and Y.
|
|
54
|
+
// 2d matrices does not have Z value.
|
|
55
|
+
if (matrixType === '2d') {
|
|
56
|
+
return {
|
|
57
|
+
x: Number(matrixValues[4]),
|
|
58
|
+
y: Number(matrixValues[5]),
|
|
59
|
+
z: 0
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 3d matrices have 16 values
|
|
64
|
+
// The 13th, 14th, and 15th values are X, Y, and Z
|
|
65
|
+
if (matrixType === '3d') {
|
|
66
|
+
return {
|
|
67
|
+
x: Number(matrixValues[12]),
|
|
68
|
+
y: Number(matrixValues[13]),
|
|
69
|
+
z: Number(matrixValues[14])
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
x: 0,
|
|
74
|
+
y: 0,
|
|
75
|
+
z: 0
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { equal, isNotNull, randomid, toArray } from 'txstate-utils'
|
|
2
|
+
import { bodyOffset, targetOffset } from './bodyoffset.js'
|
|
3
|
+
import type { ElementOffsets } from './bodyoffset.js'
|
|
4
|
+
|
|
5
|
+
const observers = new Map<string, () => void>()
|
|
6
|
+
let mutationobserver: MutationObserver
|
|
7
|
+
export function watchForMutations (cb: () => void) {
|
|
8
|
+
function watch () {
|
|
9
|
+
for (const cb of observers.values()) cb()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (observers.size === 0) {
|
|
13
|
+
mutationobserver ??= new MutationObserver(watch)
|
|
14
|
+
window.addEventListener('resize', watch, { passive: true })
|
|
15
|
+
mutationobserver.observe(document.body, {
|
|
16
|
+
subtree: true,
|
|
17
|
+
childList: true,
|
|
18
|
+
attributes: true,
|
|
19
|
+
characterData: true
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const id = randomid()
|
|
24
|
+
observers.set(id, cb)
|
|
25
|
+
cb()
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
destroy () {
|
|
29
|
+
observers.delete(id)
|
|
30
|
+
if (observers.size === 0) {
|
|
31
|
+
window.removeEventListener('resize', watch)
|
|
32
|
+
mutationobserver.disconnect()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function watchForPositionChange (el: (HTMLElement|undefined)[]|HTMLElement|undefined, cb: ((offset: Required<ElementOffsets>) => void)|((offsets: Required<ElementOffsets>[]) => void)) {
|
|
39
|
+
let lastoffsets: ElementOffsets[] = []
|
|
40
|
+
let els = toArray(el).filter(isNotNull)
|
|
41
|
+
function watch () {
|
|
42
|
+
const offsets = els.map(bodyOffset)
|
|
43
|
+
if (offsets.some((offset, i) => !equal(offset, lastoffsets[i]))) {
|
|
44
|
+
lastoffsets = offsets
|
|
45
|
+
Array.isArray(el) ? (cb as any)(offsets) : (cb as any)(offsets[0])
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { destroy } = watchForMutations(watch)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
update (newEl: (HTMLElement|undefined)[]|HTMLElement|undefined, newCb: ((offset: Required<ElementOffsets>) => void)|((offsets: Required<ElementOffsets>[]) => void)) {
|
|
53
|
+
const changed = newEl !== el || newCb !== cb
|
|
54
|
+
if (changed) {
|
|
55
|
+
el = newEl
|
|
56
|
+
els = toArray(el).filter(isNotNull)
|
|
57
|
+
cb = newCb
|
|
58
|
+
watch()
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
destroy
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function watchForPositionChangeInContainer (el: HTMLElement|undefined, container: HTMLElement|undefined, cb: (offset: Required<ElementOffsets>) => void) {
|
|
66
|
+
let lastoffset: ElementOffsets
|
|
67
|
+
|
|
68
|
+
function watch () {
|
|
69
|
+
if (!el || !container) return
|
|
70
|
+
const offset = targetOffset(el, container)
|
|
71
|
+
if (!equal(offset, lastoffset)) {
|
|
72
|
+
lastoffset = offset
|
|
73
|
+
cb(offset)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { destroy } = watchForMutations(watch)
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
update (newEl: HTMLElement|undefined, newContainer: HTMLElement|undefined, newCb: (offset: Required<ElementOffsets>) => void) {
|
|
81
|
+
const changed = newEl !== el || newContainer !== container || newCb !== cb
|
|
82
|
+
if (changed) {
|
|
83
|
+
el = newEl
|
|
84
|
+
container = newContainer
|
|
85
|
+
cb = newCb
|
|
86
|
+
watch()
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
destroy
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function classes (...classNames: (string | undefined)[]) {
|
|
2
|
+
return classNames.filter(Boolean).join(' ')
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function debounced (fn: Function, timeout: number) {
|
|
6
|
+
let timer: NodeJS.Timeout
|
|
7
|
+
return (...args: any[]) => {
|
|
8
|
+
clearTimeout(timer)
|
|
9
|
+
timer = setTimeout(() => fn(...args), timeout)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function debouncedPromise <T> (fn: (..._: any) => Promise<T>|T, timeout: number, defaultValue: T): (..._: any) => Promise<T> {
|
|
14
|
+
let version = 0
|
|
15
|
+
return async (...args: any[]) => {
|
|
16
|
+
const myversion = ++version
|
|
17
|
+
return await new Promise(resolve => {
|
|
18
|
+
setTimeout(() => {
|
|
19
|
+
if (version === myversion) {
|
|
20
|
+
version = 0
|
|
21
|
+
resolve(fn(...args))
|
|
22
|
+
} else {
|
|
23
|
+
resolve(defaultValue)
|
|
24
|
+
}
|
|
25
|
+
}, timeout)
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function modifierKey (e) {
|
|
31
|
+
return e.ctrlKey || e.altKey || e.metaKey || e.key === 'Insert'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function selectionIsLeft (ele: HTMLInputElement|HTMLTextAreaElement) {
|
|
35
|
+
return ele.selectionStart === 0 && ele.selectionEnd === 0
|
|
36
|
+
}
|