@txstate-mws/svelte-components 1.0.4 → 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.
Files changed (123) hide show
  1. package/.eslintrc.cjs +37 -0
  2. package/docs/CollapsingTable.md +79 -0
  3. package/docs/ConditionalWrapper.md +35 -0
  4. package/docs/Modal.md +63 -0
  5. package/docs/PopupMenu.md +31 -0
  6. package/docs/ScreenReaderOnly.md +10 -0
  7. package/docs/actions.md +84 -0
  8. package/docs/stores.md +203 -0
  9. package/package.json +15 -37
  10. package/playwright.config.js +112 -0
  11. package/src/app.html +14 -0
  12. package/src/global.d.ts +1 -0
  13. package/src/lib/actions/buttonify.ts +27 -0
  14. package/src/lib/actions/glue.ts +96 -0
  15. package/src/lib/actions/index.ts +7 -0
  16. package/src/lib/actions/offset.ts +43 -0
  17. package/src/lib/actions/portal.ts +13 -0
  18. package/src/lib/actions/resize.ts +72 -0
  19. package/src/lib/actions/sticky.ts +105 -0
  20. package/src/lib/actions/stickyfixed.ts +160 -0
  21. package/src/lib/components/Card.svelte +25 -0
  22. package/src/lib/components/CardLayout.svelte +188 -0
  23. package/{components → src/lib/components}/CollapsingTable.svelte +65 -63
  24. package/{components → src/lib/components}/ConditionalWrapper.svelte +7 -6
  25. package/src/lib/components/FocusLock.svelte +102 -0
  26. package/{components → src/lib/components}/Modal.svelte +24 -22
  27. package/src/lib/components/MultiSelect.svelte +174 -0
  28. package/src/lib/components/PopupMenu.svelte +285 -0
  29. package/{components → src/lib/components}/ScreenReaderOnly.svelte +4 -3
  30. package/src/lib/components/index.ts +9 -0
  31. package/src/lib/index.ts +4 -0
  32. package/src/lib/types/CardLayout.ts +14 -0
  33. package/src/lib/types/CollapsingTable.ts +8 -0
  34. package/src/lib/types/PopupMenu.ts +5 -0
  35. package/src/lib/types/index.ts +3 -0
  36. package/src/lib/util/bodyoffset.ts +77 -0
  37. package/src/lib/util/index.ts +3 -0
  38. package/src/lib/util/repositioned.ts +91 -0
  39. package/src/lib/util/util.ts +36 -0
  40. package/src/routes/__layout.svelte +125 -0
  41. package/src/routes/actions/index.svelte +1 -0
  42. package/src/routes/actions/offset.svelte +4 -0
  43. package/src/routes/actions/offsetdebounce.svelte +4 -0
  44. package/src/routes/actions/resize.svelte +4 -0
  45. package/src/routes/actions/resizedebounce.svelte +4 -0
  46. package/src/routes/actions/sticky.svelte +4 -0
  47. package/src/routes/cards/index.svelte +4 -0
  48. package/src/routes/cards/preserve.svelte +4 -0
  49. package/src/routes/collapsingtable/index.svelte +4 -0
  50. package/src/routes/collapsingtable/stickyheader.svelte +4 -0
  51. package/src/routes/index.svelte +2 -0
  52. package/src/routes/modal.svelte +4 -0
  53. package/src/routes/multiselect.svelte +4 -0
  54. package/src/routes/popupmenu/body.svelte +4 -0
  55. package/src/routes/popupmenu/complex.svelte +4 -0
  56. package/src/routes/popupmenu/empty.svelte +5 -0
  57. package/src/routes/popupmenu/index.svelte +4 -0
  58. package/src/routes/popupmenu/parent.svelte +4 -0
  59. package/src/routes/popupmenu/scrollable.svelte +27 -0
  60. package/src/routes/popupmenu/showselected.svelte +4 -0
  61. package/src/test/Cards.svelte +64 -0
  62. package/src/test/CollapsingTableTest.svelte +36 -0
  63. package/src/test/ModalTest.svelte +58 -0
  64. package/src/test/MultiSelectTest.svelte +26 -0
  65. package/src/test/Offset.svelte +32 -0
  66. package/src/test/PopupMenuBody.svelte +34 -0
  67. package/src/test/PopupMenuButtonIsParent.svelte +36 -0
  68. package/src/test/PopupMenuComplex.svelte +34 -0
  69. package/src/test/PopupMenuSimple.svelte +36 -0
  70. package/src/test/Resize.svelte +29 -0
  71. package/src/test/Sticky.svelte +65 -0
  72. package/static/layout.css +263 -0
  73. package/svelte.config.js +15 -0
  74. package/tests/.eslintrc.cjs +19 -0
  75. package/tests/CardLayout.test.js +41 -0
  76. package/tests/CollapsingTable.test.js +37 -0
  77. package/tests/Modal.test.js +18 -0
  78. package/tests/MultiSelect.test.js +58 -0
  79. package/tests/PopupMenu.test.js +55 -0
  80. package/tests/common.js +4 -0
  81. package/tsconfig.json +31 -0
  82. package/actions/buttonify.d.ts +0 -8
  83. package/actions/buttonify.js +0 -26
  84. package/actions/glue.d.ts +0 -16
  85. package/actions/glue.js +0 -82
  86. package/actions/index.d.ts +0 -7
  87. package/actions/index.js +0 -7
  88. package/actions/offset.d.ts +0 -15
  89. package/actions/offset.js +0 -35
  90. package/actions/portal.d.ts +0 -4
  91. package/actions/portal.js +0 -13
  92. package/actions/resize.d.ts +0 -20
  93. package/actions/resize.js +0 -45
  94. package/actions/sticky.d.ts +0 -33
  95. package/actions/sticky.js +0 -85
  96. package/actions/stickyfixed.d.ts +0 -25
  97. package/actions/stickyfixed.js +0 -157
  98. package/components/CollapsingTable.svelte.d.ts +0 -59
  99. package/components/ConditionalWrapper.svelte.d.ts +0 -23
  100. package/components/FocusLock.svelte +0 -107
  101. package/components/FocusLock.svelte.d.ts +0 -26
  102. package/components/Modal.svelte.d.ts +0 -27
  103. package/components/PopupMenu.svelte +0 -279
  104. package/components/PopupMenu.svelte.d.ts +0 -44
  105. package/components/ScreenReaderOnly.svelte.d.ts +0 -20
  106. package/components/index.d.ts +0 -6
  107. package/components/index.js +0 -6
  108. package/index.d.ts +0 -4
  109. package/index.js +0 -4
  110. package/types/CollapsingTable.d.ts +0 -8
  111. package/types/CollapsingTable.js +0 -1
  112. package/types/PopupMenu.d.ts +0 -5
  113. package/types/PopupMenu.js +0 -1
  114. package/types/index.d.ts +0 -2
  115. package/types/index.js +0 -2
  116. package/util/bodyoffset.d.ts +0 -14
  117. package/util/bodyoffset.js +0 -72
  118. package/util/index.d.ts +0 -3
  119. package/util/index.js +0 -3
  120. package/util/repositioned.d.ts +0 -12
  121. package/util/repositioned.js +0 -81
  122. package/util/util.d.ts +0 -2
  123. package/util/util.js +0 -10
@@ -0,0 +1,188 @@
1
+ <script lang="ts">
2
+ import { onDestroy, tick, setContext, onMount } from 'svelte'
3
+ import { writable } from 'svelte/store'
4
+ import { browser } from '$app/env'
5
+ import { ElementSize, resize } from '$lib/actions'
6
+ import { CARDLAYOUT } from '$lib/types'
7
+ import type { Block } from '$lib/types'
8
+ export let maxwidth = 500
9
+ export let preserveorder = false
10
+ export let gutter = 10
11
+ export let className = ''
12
+ const blocks = []
13
+ const gutterstore = writable(gutter)
14
+ let defaultOrder = 0
15
+ setContext(CARDLAYOUT, {
16
+ registerBlock: (block: Block) => {
17
+ blocks.push(block)
18
+ hardrecalc()
19
+ onDestroy(() => {
20
+ blocks.splice(blocks.indexOf(block), 1)
21
+ hardrecalc()
22
+ })
23
+ block.order = writable(defaultOrder++)
24
+ block.linebreak = writable(false)
25
+ const cols = savecolumns || Math.ceil(1024 / maxwidth)
26
+ block.width = writable(`calc(${100.0 / cols}% - ${gutter * (cols - 1) / cols}px)`)
27
+ return block
28
+ },
29
+ recalculate: () => {
30
+ hardrecalc()
31
+ },
32
+ gutter: gutterstore
33
+ })
34
+ $: gutterstore.set(gutter)
35
+ let layoutelement: HTMLElement
36
+ let cycle1 = 0
37
+ let cycle2 = 0
38
+ function detectcycle (w: number) {
39
+ if (cycle1 === 0 && Math.abs(w - cycle2) > 5) {
40
+ cycle1 = w
41
+ } else if (cycle2 === 0 && Math.abs(w - cycle1) > 5) {
42
+ cycle2 = w
43
+ } else if (w === cycle1 || w === cycle2) {
44
+ return true
45
+ } else {
46
+ cycle1 = 0
47
+ cycle2 = 0
48
+ }
49
+ return false
50
+ }
51
+ let savecolumns = 0
52
+ let optimal: Block[][]
53
+ let fullheight = 0
54
+ let hardrequired = false
55
+ async function recalculate (realw: number) {
56
+ const columns = Math.ceil(realw / maxwidth)
57
+ const guttereach = gutter * (columns - 1) / columns
58
+ const cycling = detectcycle(realw)
59
+ if (columns !== savecolumns) {
60
+ for (const block of blocks) block.width.set(`calc(${100.0 / columns}% - ${guttereach}px`)
61
+ await tick()
62
+ }
63
+ // collect all the card heights at this new column width
64
+ for (const block of blocks) block.height = block.element?.offsetHeight ?? 200
65
+ if (columns !== savecolumns || hardrequired) { // only do real work if number of columns has changed, recalculate triggers on resize
66
+ if (preserveorder) {
67
+ optimal = [blocks]
68
+ if (columns > 1) {
69
+ const totalheight = blocks.reduce((totalheight, block) => totalheight + block.height + gutter, 0)
70
+ const tallestblock = Math.max(...blocks.map(b => b.height))
71
+ const minheight = Math.max(totalheight / columns, tallestblock)
72
+ let shortestoverall = totalheight
73
+ // 2d packing problem is NP-hard so this is an O(n) heuristic
74
+ // optimal configuration would be if each column is `minheight` so start there
75
+ // then relax it in increments and see if the overall height shrinks
76
+ const increment = Math.max(1, Math.floor(Math.min(minheight, tallestblock) * 0.05))
77
+ for (let colmaxheight = minheight; colmaxheight < minheight + tallestblock + gutter; colmaxheight += increment) {
78
+ let colheight = 0
79
+ let colidx = 0
80
+ let tallestcol = 0
81
+ let tallestcolidx = 0
82
+ const arrangement = []
83
+ for (const block of blocks) {
84
+ if (colheight + block.height > colmaxheight && colidx < columns - 1) {
85
+ if (colheight > tallestcol) {
86
+ tallestcol = colheight
87
+ tallestcolidx = colidx
88
+ }
89
+ colidx++
90
+ colheight = 0
91
+ }
92
+ if (!arrangement[colidx]) arrangement[colidx] = []
93
+ arrangement[colidx].push(block)
94
+ colheight += block.height + gutter
95
+ }
96
+ if (colheight > tallestcol) {
97
+ tallestcol = colheight
98
+ tallestcolidx = colidx
99
+ }
100
+ if (tallestcol < shortestoverall) {
101
+ shortestoverall = tallestcol
102
+ optimal = arrangement
103
+ }
104
+ // if leftmost column is tallest, increasing the allowable height can never make the overall height shorter
105
+ if (tallestcolidx === 0) break
106
+ }
107
+ }
108
+ } else {
109
+ optimal = []
110
+ const heights: number[] = Array.apply(null, Array(columns)).map((_: any) => 0) // initializes heights to an array of zeroes
111
+ // begin sorting blocks into columns
112
+ for (const block of blocks) {
113
+ // find the column with the smallest current height
114
+ const colidx = heights.reduce((acc, curr, curridx) => curr < heights[acc] ? curridx : acc, 0)
115
+ // record the height we are adding to the chosen column
116
+ heights[colidx] += block.height + gutter
117
+ // move the current card to the chosen column
118
+ if (!optimal[colidx]) optimal[colidx] = []
119
+ optimal[colidx].push(block)
120
+ }
121
+ }
122
+ // we created the optimal arrangement, now move the block elements to
123
+ // reflect it
124
+ let order = 0
125
+ for (let i = 0; i < optimal.length; i++) {
126
+ for (let j = 0; j < optimal[i].length; j++) {
127
+ const block = optimal[i][j]
128
+ block.order.set(order++)
129
+ block.linebreak.set(j === optimal[i].length - 1 && i < optimal.length - 1)
130
+ }
131
+ }
132
+ savecolumns = columns
133
+ hardrequired = false
134
+ }
135
+ // re-adjust container height
136
+ const saveheight = fullheight
137
+ fullheight = 0
138
+ for (let i = 0; i < optimal.length; i++) {
139
+ let top = 0
140
+ for (const block of optimal[i]) {
141
+ top += block.height + gutter
142
+ }
143
+ fullheight = Math.max(fullheight, top)
144
+ }
145
+ if (cycling && saveheight < fullheight) fullheight = saveheight
146
+ }
147
+
148
+ let savewidth = 0
149
+ let timer = 0
150
+ function triggerrecalc (width: number) {
151
+ if (width === savewidth && !hardrequired) return
152
+ cancelAnimationFrame(timer)
153
+ timer = requestAnimationFrame(() => {
154
+ recalculate(width)
155
+ savewidth = width
156
+ })
157
+ }
158
+ function hardrecalc () {
159
+ if (!layoutelement) return
160
+ hardrequired = true
161
+ cycle1 = 0
162
+ cycle2 = 0
163
+ triggerrecalc(layoutelement.clientWidth)
164
+ }
165
+ function onResize (e: UIEvent & { currentTarget: any, detail: ElementSize }) {
166
+ triggerrecalc(e.detail.clientWidth)
167
+ }
168
+ onMount(() => triggerrecalc(layoutelement.clientWidth))
169
+ </script>
170
+
171
+ <ul class="cardlayout {className}" class:ssr={!browser} style:height={browser ? `${fullheight + gutter}px` : undefined} bind:this={layoutelement} use:resize on:resize={onResize}>
172
+ <slot></slot>
173
+ </ul>
174
+
175
+ <style>
176
+ .cardlayout {
177
+ list-style: none;
178
+ padding: 0;
179
+ margin: 0;
180
+ display: flex;
181
+ flex-wrap: wrap;
182
+ flex-direction: column;
183
+ align-content: space-between;
184
+ }
185
+ .cardlayout.ssr {
186
+ flex-direction: row;
187
+ }
188
+ </style>
@@ -1,75 +1,77 @@
1
- <script >import { Store } from '@txstate-mws/svelte-store';
2
- import { derived } from 'svelte/store';
3
- import { classes } from '../util';
4
- import DefaultPopupMenu from './PopupMenu.svelte';
5
- import ScreenReaderOnly from './ScreenReaderOnly.svelte';
6
- export let tableClass = '';
7
- export let bodyRowClass = '';
8
- export let bodyCellClass = '';
9
- export let headerRowClass = '';
10
- export let headerCellClass = '';
11
- export let menuContainerClass = '';
12
- export let menuClass = '';
13
- export let menuItemClass = '';
14
- export let menuItemHilitedClass = '';
15
- export let items;
16
- export let config = undefined;
17
- export let defaultCellWidth = 150;
18
- export let PopupMenu = DefaultPopupMenu;
19
- export let slots = $$slots;
20
- export let stickyheader = false;
21
- let columns;
22
- let width = 320;
23
- let selected;
24
- let identifyingkeys = [];
25
- $: firstrow = items?.[0] ?? {};
26
- $: columns = config ?? Object.keys(firstrow).map(key => ({ key }));
27
- function reactToColumns(columns) {
1
+ <script lang="ts">
2
+ import { Store } from '@txstate-mws/svelte-store'
3
+ import { derived } from 'svelte/store'
4
+ import type { CollapsingTableColumn, PopupMenuItem } from '$lib/types'
5
+ import { classes } from '$lib/util'
6
+ import DefaultPopupMenu from './PopupMenu.svelte'
7
+ import ScreenReaderOnly from './ScreenReaderOnly.svelte'
8
+
9
+ export let tableClass = ''
10
+ export let bodyRowClass = ''
11
+ export let bodyCellClass = ''
12
+ export let headerRowClass = ''
13
+ export let headerCellClass = ''
14
+ export let menuContainerClass = ''
15
+ export let menuClass = ''
16
+ export let menuItemClass = ''
17
+ export let menuItemHilitedClass = ''
18
+ export let items: any[]
19
+ export let config: CollapsingTableColumn[]|undefined = undefined
20
+ export let defaultCellWidth = 150
21
+ export let PopupMenu = DefaultPopupMenu
22
+ export let slots: Record<string, boolean> = $$slots
23
+ export let stickyheader: boolean = false
24
+ let columns: CollapsingTableColumn[]
25
+ let width: number = 320
26
+ let selected: PopupMenuItem | undefined
27
+ let identifyingkeys: string[] = []
28
+ $: firstrow = items?.[0] ?? {}
29
+ $: columns = config ?? Object.keys(firstrow).map(key => ({ key }))
30
+ function reactToColumns (columns: CollapsingTableColumn[]) {
28
31
  if (!columns.some(c => c.neverhide)) {
29
- columns[0].neverhide = true;
32
+ columns[0].neverhide = true
30
33
  }
31
34
  const sortedcolumns = columns // stable sort
32
- .map((c, i) => ({ col: c, idx: i }))
33
- .sort((a, b) => ((a.col.neverhide ? 0 : 1) - (b.col.neverhide ? 0 : 1)) || a.idx - b.idx)
34
- .map(o => o.col);
35
+ .map((c, i) => ({ col: c, idx: i }))
36
+ .sort((a, b) => ((a.col.neverhide ? 0 : 1) - (b.col.neverhide ? 0 : 1)) || a.idx - b.idx)
37
+ .map(o => o.col)
35
38
  if (!selected) {
36
- const firstcol = sortedcolumns.find(c => !c.neverhide);
37
- selected = firstcol ? { value: firstcol.key } : undefined;
39
+ const firstcol = sortedcolumns.find(c => !c.neverhide)
40
+ selected = firstcol ? { value: firstcol.key } : undefined
38
41
  }
39
- identifyingkeys = sortedcolumns.filter(c => c.neverhide).map(c => c.key);
40
- return sortedcolumns;
41
- }
42
- $: sortedcolumns = reactToColumns(columns);
43
- $: itemkeys = items.map(item => item.id ?? item._id ?? identifyingkeys.map(k => item[k]).join('.'));
44
- const state = new Store({
45
- keepcolumns: [],
46
- hiddencolumns: [],
47
- dropdowncolumn: undefined
48
- });
49
- const menuitems = derived(state, obj => obj.hiddencolumns.map(c => ({ value: c.key, label: c.title })));
50
- function react(..._) {
51
- const selectedcol = sortedcolumns.find(c => c.key === selected?.value) ?? columns[1];
52
- const keepcolumns = [];
53
- const hiddencolumns = [];
54
- let used = sortedcolumns.filter(c => c.neverhide || c === selectedcol).reduce((sum, col) => sum + (col.width ?? defaultCellWidth), 0);
42
+ identifyingkeys = sortedcolumns.filter(c => c.neverhide).map(c => c.key)
43
+ return sortedcolumns
44
+ }
45
+ $: sortedcolumns = reactToColumns(columns)
46
+
47
+ $: itemkeys = items.map(item => item.id ?? item._id ?? identifyingkeys.map(k => item[k]).join('.'))
48
+
49
+ const state = new Store({
50
+ keepcolumns: [] as (CollapsingTableColumn & { widthPercent?: number })[],
51
+ hiddencolumns: [] as CollapsingTableColumn[],
52
+ dropdowncolumn: undefined as (CollapsingTableColumn & { widthPercent?: number }) | undefined
53
+ })
54
+ const menuitems = derived(state, obj => obj.hiddencolumns.map(c => ({ value: c.key, label: c.title })))
55
+ function react (..._: any[]) {
56
+ const selectedcol = sortedcolumns.find(c => c.key === selected?.value) ?? columns[1]
57
+ const keepcolumns: CollapsingTableColumn[] = []
58
+ const hiddencolumns: CollapsingTableColumn[] = []
59
+ let used = sortedcolumns.filter(c => c.neverhide || c === selectedcol).reduce((sum, col) => sum + (col.width ?? defaultCellWidth), 0)
55
60
  for (const col of sortedcolumns) {
56
- if (col.neverhide || col === selectedcol || used + (col.width ?? defaultCellWidth) < width) {
57
- keepcolumns.push(col);
58
- if (col !== selectedcol && !col.neverhide)
59
- used += col.width ?? defaultCellWidth;
60
- }
61
- else
62
- hiddencolumns.push(col);
61
+ if (col.neverhide || col === selectedcol || used + (col.width ?? defaultCellWidth) < width) {
62
+ keepcolumns.push(col)
63
+ if (col !== selectedcol && !col.neverhide) used += col.width ?? defaultCellWidth
64
+ } else hiddencolumns.push(col)
63
65
  }
64
66
  for (const col of keepcolumns) {
65
- col.widthPercent = 100 * (col.width ?? defaultCellWidth) / used;
67
+ (col as any).widthPercent = 100 * (col.width ?? defaultCellWidth) / used
66
68
  }
67
- const dropdowncolumn = hiddencolumns.length ? keepcolumns[keepcolumns.length - 1] : undefined;
68
- state.set({ keepcolumns, hiddencolumns, dropdowncolumn });
69
- }
70
- $: react(width, selected);
71
- const headers = [];
72
- $: menubuttonelement = headers[$state.keepcolumns.length - 1];
69
+ const dropdowncolumn = hiddencolumns.length ? keepcolumns[keepcolumns.length - 1] : undefined
70
+ state.set({ keepcolumns, hiddencolumns, dropdowncolumn })
71
+ }
72
+ $: react(width, selected)
73
+ const headers: HTMLElement[] = []
74
+ $: menubuttonelement = headers[$state.keepcolumns.length - 1]
73
75
  </script>
74
76
 
75
77
  <div bind:clientWidth={width}>
@@ -1,9 +1,10 @@
1
- <script >export let condition;
2
- export let component = undefined;
3
- export let a = false;
4
- export let span = false;
5
- export let element = undefined;
6
- $: passthrough = (({ condition, component, a, span, element, ...passthrough }) => passthrough)($$props);
1
+ <script lang="ts">
2
+ export let condition: boolean|undefined
3
+ export let component: Function|undefined = undefined
4
+ export let a = false
5
+ export let span = false
6
+ export let element: HTMLElement|undefined = undefined
7
+ $: passthrough = (({ condition, component, a, span, element, ...passthrough }) => passthrough)($$props)
7
8
  </script>
8
9
 
9
10
  {#if condition || (typeof condition === 'undefined' && component)}
@@ -0,0 +1,102 @@
1
+ <script context="module" lang="ts">
2
+ const FocusLockStack: { pause: () => void, unpause: () => void }[] = []
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ export let returnfocusto: HTMLElement|null|undefined = undefined
7
+ export let initialfocus: string|undefined = undefined
8
+ export let hidefocus = true
9
+ export let hidefocuslabel = 'focus is above modal dialog, start tabbing'
10
+ export let escapable = true
11
+ let className = ''
12
+ export { className as class }
13
+
14
+ import { onMount, onDestroy, createEventDispatcher, tick } from 'svelte'
15
+ import { tabbable } from 'tabbable'
16
+ import { sleep } from 'txstate-utils'
17
+ import { buttonify } from '../actions'
18
+ import ScreenReaderOnly from './ScreenReaderOnly.svelte'
19
+ const dispatch = createEventDispatcher()
20
+ let lockelement: HTMLElement
21
+ let abovelockelement: HTMLElement
22
+ let active = true
23
+ let listenforescape = false
24
+ if (initialfocus) hidefocus = false
25
+ onMount(async () => {
26
+ const prevFocusLock = FocusLockStack.slice(-1)[0]
27
+ if (prevFocusLock) prevFocusLock.pause()
28
+ FocusLockStack.push({
29
+ pause: () => { active = false },
30
+ unpause: () => { active = true }
31
+ })
32
+ if (typeof returnfocusto === 'undefined') {
33
+ returnfocusto = document.querySelector(':focus') as HTMLElement
34
+ }
35
+ await sleep(1) // sapper resets focus on page load, so we'll wait a millisecond in case our modal is on screen at load
36
+ if (!lockelement) return // in case we already unmounted
37
+ if (initialfocus) {
38
+ const focusEl = lockelement.querySelector(initialfocus)
39
+ if (focusEl instanceof HTMLElement) focusEl.focus()
40
+ } else {
41
+ setInitialFocus()
42
+ }
43
+ setTimeout(() => { listenforescape = true }, 0)
44
+ })
45
+ onDestroy(async () => {
46
+ const wasactive = active
47
+ active = false
48
+ await tick()
49
+ if (returnfocusto && wasactive) {
50
+ returnfocusto.focus()
51
+ }
52
+ FocusLockStack.pop()
53
+ const prevFocusLock = FocusLockStack.slice(-1)[0]
54
+ if (prevFocusLock) prevFocusLock.unpause()
55
+ })
56
+ const setInitialFocus = () => {
57
+ const firstfocus = lockelement ? tabbable(lockelement)[0] : undefined
58
+ if (firstfocus && firstfocus.focus) {
59
+ firstfocus.focus()
60
+ }
61
+ }
62
+ const setLastFocus = () => {
63
+ const lastfocus = lockelement ? tabbable(lockelement).slice(-1)[0] : undefined
64
+ if (lastfocus && lastfocus.focus) {
65
+ lastfocus.focus()
66
+ }
67
+ }
68
+ const keydown = (e: KeyboardEvent) => {
69
+ if (active && e.key === 'Escape' && escapable) {
70
+ e.preventDefault()
71
+ dispatch('escape')
72
+ }
73
+ }
74
+ const focusin = (e: FocusEvent) => {
75
+ if (active && e.target instanceof HTMLElement && !lockelement?.contains(e.target)) {
76
+ if (e.target === abovelockelement) setLastFocus()
77
+ else setInitialFocus()
78
+ }
79
+ }
80
+ const windowclick = (_: MouseEvent) => {
81
+ if (listenforescape && active) {
82
+ returnfocusto = undefined
83
+ dispatch('escape')
84
+ }
85
+ }
86
+ </script>
87
+ <svelte:window on:click={windowclick} />
88
+ <div class={className} role="alertdialog" aria-modal="true" on:click|stopPropagation on:keydown|stopPropagation={keydown} on:focusin={focusin}>
89
+ <div bind:this={abovelockelement} tabindex="0"></div>
90
+ <div bind:this={lockelement} on:keydown={keydown}>
91
+ {#if hidefocus}<div class="hiddenfocus" use:buttonify on:blur={() => { hidefocus = false }} on:click={() => escapable && dispatch('escape')}><ScreenReaderOnly>{hidefocuslabel}{#if escapable}, click to escape or use escape key at any time{/if}</ScreenReaderOnly></div>{/if}
92
+ <slot></slot>
93
+ </div>
94
+ <div tabindex="0"></div>
95
+ </div>
96
+
97
+ <style>
98
+ .hiddenfocus {
99
+ opacity: 0;
100
+ outline: 0;
101
+ }
102
+ </style>
@@ -1,26 +1,28 @@
1
- <script >export let escapable = true;
2
- export let hidefocus = true;
3
- export let hidefocuslabel = undefined;
4
- export let returnfocusto = undefined;
5
- export let initialfocus = undefined;
6
- export let containerClass = '';
7
- export let opaque = false;
8
- export let usePortal = undefined;
9
- import { createEventDispatcher, onMount } from 'svelte';
10
- import FocusLock from './FocusLock.svelte';
11
- import { portal } from '../actions';
12
- const dispatch = createEventDispatcher();
13
- const endmodal = () => {
14
- dispatch('escape');
15
- };
16
- onMount(() => {
17
- document.body.style.marginRight = (window.innerWidth - document.body.clientWidth) + 'px';
18
- document.body.style.overflow = 'hidden';
1
+ <script lang="ts">
2
+ export let escapable = true
3
+ export let hidefocus = true
4
+ export let hidefocuslabel: string|undefined = undefined
5
+ export let returnfocusto: HTMLElement|undefined = undefined
6
+ export let initialfocus: string|undefined = undefined
7
+ export let containerClass = ''
8
+ export let opaque = false
9
+ export let usePortal: HTMLElement|undefined = undefined
10
+ import { createEventDispatcher, onMount } from 'svelte'
11
+ import FocusLock from './FocusLock.svelte'
12
+ import { portal } from '$lib/actions'
13
+
14
+ const dispatch = createEventDispatcher()
15
+ const endmodal = () => {
16
+ dispatch('escape')
17
+ }
18
+ onMount(() => {
19
+ document.body.style.marginRight = (window.innerWidth - document.body.clientWidth) + 'px'
20
+ document.body.style.overflow = 'hidden'
19
21
  return () => {
20
- document.body.style.overflow = '';
21
- document.body.style.marginRight = '';
22
- };
23
- });
22
+ document.body.style.overflow = ''
23
+ document.body.style.marginRight = ''
24
+ }
25
+ })
24
26
  </script>
25
27
 
26
28
  <div use:portal={usePortal} class="modal-backdrop" class:opaque on:click|stopPropagation|preventDefault={() => escapable && endmodal()}>