@txstate-mws/svelte-components 1.6.12 → 1.6.13
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/dist/actions/elementqueries.d.ts +12 -9
- package/dist/components/ButtonMenu.svelte +31 -25
- package/dist/components/Card.svelte +12 -8
- package/dist/components/CardLayout.svelte +151 -158
- package/dist/components/CollapsingTable.svelte +65 -63
- package/dist/components/ConditionalWrapper.svelte +7 -6
- package/dist/components/FocusLock.svelte +79 -85
- package/dist/components/Loading.svelte +16 -15
- package/dist/components/Lottie.svelte +37 -31
- package/dist/components/Modal.svelte +36 -33
- package/dist/components/MultiSelect.svelte +158 -165
- package/dist/components/PopupMenu.svelte +270 -271
- package/dist/components/ScreenReaderOnly.svelte +5 -4
- package/dist/stores/toasts.d.ts +2 -2
- package/dist/util/util.d.ts +2 -2
- package/package.json +6 -7
|
@@ -12,115 +12,109 @@ When they do, the escape event will be fired, allowing you to remove the dialog.
|
|
|
12
12
|
`FocusLock`s can be nested. The user will be trapped inside the deepest `FocusLock` present in the DOM.
|
|
13
13
|
When it goes away, they'll be trapped inside the previous `FocusLock`, and so on.
|
|
14
14
|
-->
|
|
15
|
-
<script context="module" lang="ts">
|
|
16
|
-
const
|
|
15
|
+
<script context="module" lang="ts">
|
|
16
|
+
export const FocusLockStack: { focusId: string, pause: () => void, unpause: () => void, update: () => void }[] = []
|
|
17
|
+
const waitAtick = typeof requestAnimationFrame !== 'undefined' ? (resolve: (value: unknown) => void) => requestAnimationFrame(resolve) : (resolve: (value: unknown) => void) => resolve(0)
|
|
17
18
|
</script>
|
|
18
19
|
|
|
19
|
-
<script lang="ts">
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export let
|
|
27
|
-
export let
|
|
28
|
-
export let
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
let
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
20
|
+
<script lang="ts">
|
|
21
|
+
import { onMount, onDestroy, createEventDispatcher } from 'svelte'
|
|
22
|
+
import { tabbable } from 'tabbable'
|
|
23
|
+
import { randomid, sleep } from 'txstate-utils'
|
|
24
|
+
import { buttonify } from '../actions'
|
|
25
|
+
import ScreenReaderOnly from './ScreenReaderOnly.svelte'
|
|
26
|
+
|
|
27
|
+
export let escapable = true
|
|
28
|
+
export let hidefocus = true
|
|
29
|
+
export let hidefocuslabel = 'focus is above modal dialog, start tabbing'
|
|
30
|
+
export let initialfocus: string | undefined = undefined
|
|
31
|
+
export let returnfocusto: HTMLElement | undefined = undefined
|
|
32
|
+
/** If you expect any popup menus to be added to the body, we need to know that they
|
|
33
|
+
are considered to be part of the focus lock, or else the modal will be dismissed
|
|
34
|
+
when the user clicks inside. Use commas to include multiple selectors. */
|
|
35
|
+
export let includeselector: string | undefined = undefined
|
|
36
|
+
let className = ''
|
|
37
|
+
export { className as class }
|
|
38
|
+
export let focusId = randomid()
|
|
39
|
+
|
|
40
|
+
const dispatch = createEventDispatcher()
|
|
41
|
+
let lockelement: HTMLElement
|
|
42
|
+
let abovelockelement: HTMLElement
|
|
43
|
+
let state: 'init' | 'active' | 'paused' | 'destroyed' = 'init'
|
|
44
|
+
if (initialfocus) hidefocus = false
|
|
45
|
+
onMount(async () => {
|
|
46
|
+
const prevFocusLock = FocusLockStack.slice(-1)[0]
|
|
47
|
+
if (prevFocusLock) prevFocusLock.pause()
|
|
46
48
|
FocusLockStack.push({
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
for (const entry of FocusLockStack)
|
|
54
|
-
entry.update();
|
|
49
|
+
focusId,
|
|
50
|
+
pause: () => { state = 'paused' },
|
|
51
|
+
unpause: () => { if (state === 'paused') state = 'active' },
|
|
52
|
+
update: () => { dispatch('focuslockupdate') }
|
|
53
|
+
})
|
|
54
|
+
for (const entry of FocusLockStack) entry.update()
|
|
55
55
|
if (typeof returnfocusto === 'undefined') {
|
|
56
|
-
|
|
56
|
+
returnfocusto = document.activeElement as HTMLElement | undefined
|
|
57
57
|
}
|
|
58
|
-
await sleep(1)
|
|
59
|
-
if (!lockelement)
|
|
60
|
-
return; // in case we already unmounted
|
|
58
|
+
await sleep(1) // sapper resets focus on page load, so we'll wait a millisecond in case our modal is on screen at load
|
|
59
|
+
if (!lockelement) return // in case we already unmounted
|
|
61
60
|
if (initialfocus) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
else {
|
|
67
|
-
setInitialFocus();
|
|
61
|
+
const focusEl = lockelement.querySelector(initialfocus)
|
|
62
|
+
if (focusEl instanceof HTMLElement) focusEl.focus()
|
|
63
|
+
} else {
|
|
64
|
+
setInitialFocus()
|
|
68
65
|
}
|
|
69
|
-
setTimeout(() => { state = 'active'
|
|
70
|
-
})
|
|
71
|
-
onDestroy(async () => {
|
|
72
|
-
const wasactive = state !== 'paused'
|
|
73
|
-
state = 'destroyed'
|
|
66
|
+
setTimeout(() => { state = 'active' }, 0)
|
|
67
|
+
})
|
|
68
|
+
onDestroy(async () => {
|
|
69
|
+
const wasactive = state !== 'paused'
|
|
70
|
+
state = 'destroyed'
|
|
71
|
+
|
|
74
72
|
// we need to allow the screen to redraw so that we can
|
|
75
73
|
// change focus back to where it was, and we need
|
|
76
74
|
// to allow the click event that destroyed us to finish bubbling
|
|
77
75
|
// before we unpause the next focuslock in the stack or else it
|
|
78
76
|
// will think it was an "outside" click and kill itself
|
|
79
|
-
await new Promise(waitAtick)
|
|
77
|
+
await new Promise(waitAtick)
|
|
78
|
+
|
|
80
79
|
if (returnfocusto && wasactive) {
|
|
81
|
-
|
|
80
|
+
returnfocusto.focus()
|
|
82
81
|
}
|
|
83
|
-
const idx = FocusLockStack.findIndex(f => f.focusId === focusId)
|
|
84
|
-
if (idx > -1)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
});
|
|
92
|
-
const setInitialFocus = () => {
|
|
93
|
-
const firstfocus = lockelement ? tabbable(lockelement)[0] : undefined;
|
|
82
|
+
const idx = FocusLockStack.findIndex(f => f.focusId === focusId)
|
|
83
|
+
if (idx > -1) FocusLockStack.splice(idx, 1)
|
|
84
|
+
const prevFocusLock = FocusLockStack.slice(-1)[0]
|
|
85
|
+
if (prevFocusLock) prevFocusLock.unpause()
|
|
86
|
+
for (const entry of FocusLockStack) entry.update()
|
|
87
|
+
})
|
|
88
|
+
const setInitialFocus = () => {
|
|
89
|
+
const firstfocus = lockelement ? tabbable(lockelement)[0] : undefined
|
|
94
90
|
if (firstfocus?.focus) {
|
|
95
|
-
|
|
91
|
+
firstfocus.focus()
|
|
96
92
|
}
|
|
97
|
-
}
|
|
98
|
-
const setLastFocus = () => {
|
|
99
|
-
const lastfocus = lockelement ? tabbable(lockelement).slice(-1)[0] : undefined
|
|
93
|
+
}
|
|
94
|
+
const setLastFocus = () => {
|
|
95
|
+
const lastfocus = lockelement ? tabbable(lockelement).slice(-1)[0] : undefined
|
|
100
96
|
if (lastfocus?.focus) {
|
|
101
|
-
|
|
97
|
+
lastfocus.focus()
|
|
102
98
|
}
|
|
103
|
-
}
|
|
104
|
-
const keydown = (e) => {
|
|
99
|
+
}
|
|
100
|
+
const keydown = (e: KeyboardEvent) => {
|
|
105
101
|
if (state === 'active' && e.key === 'Escape' && escapable) {
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
e.preventDefault()
|
|
103
|
+
dispatch('escape')
|
|
108
104
|
}
|
|
109
|
-
}
|
|
110
|
-
const focusin = (e) => {
|
|
105
|
+
}
|
|
106
|
+
const focusin = (e: FocusEvent) => {
|
|
111
107
|
if (state === 'active' && e.target instanceof HTMLElement && !lockelement?.contains(e.target)) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
else
|
|
115
|
-
setInitialFocus();
|
|
108
|
+
if (e.target === abovelockelement) setLastFocus()
|
|
109
|
+
else setInitialFocus()
|
|
116
110
|
}
|
|
117
|
-
}
|
|
118
|
-
const windowclick = (e) => {
|
|
111
|
+
}
|
|
112
|
+
const windowclick = (e: MouseEvent) => {
|
|
119
113
|
if (state === 'active' && e.target instanceof HTMLElement && e.target.isConnected && (!includeselector || !e.target.closest(includeselector))) {
|
|
120
|
-
|
|
121
|
-
|
|
114
|
+
returnfocusto = undefined
|
|
115
|
+
dispatch('escape')
|
|
122
116
|
}
|
|
123
|
-
}
|
|
117
|
+
}
|
|
124
118
|
</script>
|
|
125
119
|
<svelte:window on:mousedown={windowclick} />
|
|
126
120
|
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
@@ -3,21 +3,22 @@
|
|
|
3
3
|
until the slotted content is signals it is done loading via the `loading` boolean bind at which point the slotted content
|
|
4
4
|
will be rendered.
|
|
5
5
|
-->
|
|
6
|
-
<script lang="ts">
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
export let
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import { afterUpdate } from 'svelte'
|
|
8
|
+
import ScreenReaderOnly from './ScreenReaderOnly.svelte'
|
|
9
|
+
import { resize, ResizeStore } from '../actions'
|
|
10
|
+
export let loading: boolean | undefined
|
|
11
|
+
export let height: string | undefined = undefined
|
|
12
|
+
const store = new ResizeStore()
|
|
13
|
+
|
|
14
|
+
let containerelement: HTMLElement
|
|
15
|
+
let parentScrolls: boolean
|
|
16
|
+
afterUpdate(() => {
|
|
17
|
+
if (height || !containerelement) return
|
|
18
|
+
const parent = containerelement.offsetParent!
|
|
19
|
+
const overflowY = window.getComputedStyle(parent).overflowY
|
|
20
|
+
parentScrolls = overflowY === 'scroll' || overflowY === 'auto'
|
|
21
|
+
})
|
|
21
22
|
</script>
|
|
22
23
|
|
|
23
24
|
{#if loading}
|
|
@@ -2,39 +2,45 @@
|
|
|
2
2
|
The purpose of `Lottie` is to provide a [Lottie](https://www.npmjs.com/package/lottie-web) player element
|
|
3
3
|
with Screen Reader compatible attributes and lables readily available through the `alt` prop.
|
|
4
4
|
-->
|
|
5
|
-
<script lang="ts">
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export let
|
|
12
|
-
export let
|
|
13
|
-
export let
|
|
14
|
-
export let
|
|
15
|
-
export let
|
|
16
|
-
|
|
17
|
-
export let
|
|
18
|
-
let
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import type { AnimationItem } from 'lottie-web/build/player/lottie_light'
|
|
7
|
+
import { onMount } from 'svelte'
|
|
8
|
+
import { isBlank } from 'txstate-utils'
|
|
9
|
+
import ScreenReaderOnly from './ScreenReaderOnly.svelte'
|
|
10
|
+
|
|
11
|
+
export let animationData: any
|
|
12
|
+
export let loop = true
|
|
13
|
+
export let name: string | undefined = undefined
|
|
14
|
+
export let speed = 1
|
|
15
|
+
export let direction: 1 | -1 = 1
|
|
16
|
+
export let paused = false
|
|
17
|
+
export let width: string | undefined = undefined
|
|
18
|
+
export let height: string | undefined = undefined
|
|
19
|
+
/** `<ScreenReaderOnly>` lable to be applied. */
|
|
20
|
+
export let alt = ''
|
|
21
|
+
|
|
22
|
+
let container: HTMLElement
|
|
23
|
+
let animation: AnimationItem | undefined
|
|
24
|
+
|
|
25
|
+
$: animation?.setSpeed(speed)
|
|
26
|
+
$: animation?.setDirection(direction)
|
|
27
|
+
$: paused ? animation?.pause() : animation?.play()
|
|
28
|
+
|
|
29
|
+
onMount(async () => {
|
|
30
|
+
const lottie = (await import('lottie-web/build/player/lottie_light')).default
|
|
25
31
|
animation = lottie.loadAnimation({
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
})
|
|
32
|
+
container,
|
|
33
|
+
animationData,
|
|
34
|
+
renderer: 'svg',
|
|
35
|
+
loop,
|
|
36
|
+
autoplay: !paused,
|
|
37
|
+
name
|
|
38
|
+
})
|
|
33
39
|
animation.addEventListener('DOMLoaded', () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
})
|
|
37
|
-
})
|
|
40
|
+
animation!.setSpeed(speed)
|
|
41
|
+
animation!.setDirection(direction)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
38
44
|
</script>
|
|
39
45
|
|
|
40
46
|
<div bind:this={container} style:width style:height aria-hidden={isBlank(alt)}><ScreenReaderOnly>{alt}</ScreenReaderOnly></div>
|
|
@@ -5,40 +5,43 @@ It provides only the backdrop and a scrollable container for your content. If yo
|
|
|
5
5
|
|
|
6
6
|
Any time the Modal is in the DOM, it will take over the screen. You make it go away by removing it from the DOM.
|
|
7
7
|
-->
|
|
8
|
-
<script lang="ts">
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export let
|
|
15
|
-
export let
|
|
16
|
-
export let
|
|
17
|
-
export let
|
|
18
|
-
export let
|
|
19
|
-
export let
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { createEventDispatcher, onMount } from 'svelte'
|
|
10
|
+
import { randomid } from 'txstate-utils'
|
|
11
|
+
import FocusLock, { FocusLockStack } from './FocusLock.svelte'
|
|
12
|
+
import { portal } from '../actions'
|
|
13
|
+
|
|
14
|
+
export let opaque = false
|
|
15
|
+
export let containerClass = ''
|
|
16
|
+
export let escapable = true
|
|
17
|
+
export let hidefocus = true
|
|
18
|
+
export let hidefocuslabel: string | undefined = undefined
|
|
19
|
+
export let initialfocus: string | undefined = undefined
|
|
20
|
+
export let returnfocusto: HTMLElement | undefined = undefined
|
|
21
|
+
export let usePortal: HTMLElement | undefined = undefined
|
|
22
|
+
/** If you expect any popup menus to be added to the body, we need to know that they
|
|
23
|
+
are considered to be part of the focus lock, or else the modal will be dismissed
|
|
24
|
+
when the user clicks inside. Use commas to include multiple selectors. */
|
|
25
|
+
export let includeselector: string | undefined = undefined
|
|
26
|
+
export let focusId = randomid()
|
|
27
|
+
|
|
28
|
+
const dispatch = createEventDispatcher()
|
|
29
|
+
const endmodal = () => {
|
|
30
|
+
dispatch('escape')
|
|
31
|
+
}
|
|
32
|
+
let stackPosition: number
|
|
33
|
+
function onFocusLockUpdate () {
|
|
34
|
+
stackPosition = FocusLockStack.findIndex(f => f.focusId === focusId)
|
|
35
|
+
}
|
|
36
|
+
onMount(() => {
|
|
37
|
+
onFocusLockUpdate()
|
|
38
|
+
document.body.style.marginRight = (window.innerWidth - document.body.clientWidth) + 'px'
|
|
39
|
+
document.body.style.overflow = 'hidden'
|
|
37
40
|
return () => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
})
|
|
41
|
+
document.body.style.overflow = ''
|
|
42
|
+
document.body.style.marginRight = ''
|
|
43
|
+
}
|
|
44
|
+
})
|
|
42
45
|
</script>
|
|
43
46
|
|
|
44
47
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|