@lavalogic/scoria 0.38.12 → 0.38.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/Components/AccordionGroup.svelte +46 -46
- package/dist/Components/AccordionGroupButton.svelte +76 -76
- package/dist/Components/Action.svelte +136 -136
- package/dist/Components/AppointmentCalendar/AppointmentCalendar.svelte +712 -702
- package/dist/Components/Base/BaseModal.svelte +125 -125
- package/dist/Components/Base/ContextWrapper.svelte +72 -72
- package/dist/Components/Base/Pagination.svelte +198 -198
- package/dist/Components/Base/Snippets.svelte +36 -36
- package/dist/Components/BigRadioSet.svelte +38 -38
- package/dist/Components/Bubble.svelte +101 -101
- package/dist/Components/Button.svelte +209 -209
- package/dist/Components/Checkbox.svelte +86 -86
- package/dist/Components/Contexts/ContextMenu.svelte +164 -164
- package/dist/Components/Contexts/ContextMenuDivider.svelte +15 -15
- package/dist/Components/Contexts/ContextMenuOption.svelte +60 -60
- package/dist/Components/Contexts/ToastContainer.svelte +65 -65
- package/dist/Components/Contexts/Tooltip.svelte +37 -37
- package/dist/Components/DataMatrixIcon.svelte +112 -112
- package/dist/Components/DateInput.svelte +148 -148
- package/dist/Components/DatePicker.svelte +300 -300
- package/dist/Components/DesktopModal.svelte +90 -90
- package/dist/Components/Dial.svelte +204 -204
- package/dist/Components/DimensionInput.svelte +170 -170
- package/dist/Components/DragMenu/DragMenuHolder.svelte +119 -119
- package/dist/Components/DragMenu/DraggableCard.svelte +77 -77
- package/dist/Components/FileCard.svelte +24 -24
- package/dist/Components/FileUpload.svelte +236 -236
- package/dist/Components/GridInputSet.svelte +29 -29
- package/dist/Components/HorizontalTabGroup.svelte +82 -82
- package/dist/Components/HorizontalTabGroupButton.svelte +44 -44
- package/dist/Components/Icon.svelte +128 -128
- package/dist/Components/Icons.js +357 -357
- package/dist/Components/LoadingAnimation.svelte +29 -29
- package/dist/Components/LoadingModal.svelte +45 -45
- package/dist/Components/LoadingOverlay.svelte +25 -25
- package/dist/Components/Modal.svelte +258 -258
- package/dist/Components/MultiSelect.svelte +220 -220
- package/dist/Components/Nav/NavServiceMenuList.svelte +43 -43
- package/dist/Components/Nav/NavTab.svelte +62 -62
- package/dist/Components/Nav/ServicesNav.svelte +80 -80
- package/dist/Components/NumberInput.svelte +157 -157
- package/dist/Components/PageHeading.svelte +25 -25
- package/dist/Components/PasswordInput.svelte +88 -88
- package/dist/Components/Portal.svelte +36 -36
- package/dist/Components/QuerySelect.svelte +366 -366
- package/dist/Components/SingleSelect.svelte +250 -250
- package/dist/Components/StepButton.svelte +70 -70
- package/dist/Components/StepForm.svelte +140 -140
- package/dist/Components/Switch.svelte +69 -69
- package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +51 -51
- package/dist/Components/Table/Body/Rows/ColumnList.svelte +59 -59
- package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +79 -79
- package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +120 -120
- package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +105 -105
- package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +87 -87
- package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +126 -126
- package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +159 -159
- package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +246 -246
- package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +216 -216
- package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +179 -179
- package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +197 -197
- package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +190 -190
- package/dist/Components/Table/Body/Rows/TableRow.svelte +290 -290
- package/dist/Components/Table/Headers/HighlightAllHeader.svelte +41 -41
- package/dist/Components/Table/Headers/SelectAllHeader.svelte +30 -30
- package/dist/Components/Table/Misc/SidePanel.svelte +35 -35
- package/dist/Components/Table/Misc/ToolboxPanel.svelte +100 -100
- package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +107 -107
- package/dist/Components/TextArea.svelte +130 -130
- package/dist/Components/TextInput.svelte +176 -176
- package/dist/Components/ThreeStateRadio.svelte +131 -131
- package/dist/Components/Touch/BoolCard.svelte +45 -45
- package/dist/Components/Touch/DateModal.svelte +209 -209
- package/dist/Components/Touch/EditCard.svelte +59 -59
- package/dist/Components/Touch/InfoCard.svelte +31 -31
- package/dist/Components/Touch/InfoTextCard.svelte +28 -28
- package/dist/Components/Touch/ModalExplanation.svelte +17 -17
- package/dist/Components/Touch/NumberKeyboard.svelte +225 -225
- package/dist/Components/Touch/NumberModal.svelte +79 -79
- package/dist/Components/Touch/PlusMinusCard.svelte +46 -46
- package/dist/Components/Touch/SummaryCard.svelte +93 -93
- package/dist/Components/Touch/TextAreaModal.svelte +95 -95
- package/dist/Components/Touch/TextModal.svelte +106 -106
- package/dist/Components/Touch/TouchCard.svelte +33 -33
- package/dist/Components/Touch/TouchModal.svelte +274 -274
- package/dist/Components/Touch/UtilityButton.svelte +109 -109
- package/dist/Components/VerticalTabGroup.svelte +79 -79
- package/dist/Components/VerticalTabGroupButton.svelte +58 -58
- package/dist/Helpers/Datamatrix.d.ts +19 -19
- package/dist/Helpers/Datamatrix.js +291 -291
- package/dist/Types/Examples/ExampleModal.svelte +35 -35
- package/dist/scss/_motion.scss +57 -57
- package/dist/scss/_sizing.scss +85 -85
- package/package.json +30 -30
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
<svelte:options runes />
|
|
2
|
-
|
|
3
|
-
<script
|
|
4
|
-
lang="ts"
|
|
5
|
-
module
|
|
6
|
-
>
|
|
7
|
-
import { generateWeakId } from '../Helpers/Helpers.svelte.js';
|
|
8
|
-
import { Size } from '../Types/Internal/Size.js';
|
|
9
|
-
import type { CheckboxProps } from './CheckboxProps.js';
|
|
10
|
-
import Icon from './Icon.svelte';
|
|
11
|
-
</script>
|
|
12
|
-
|
|
13
|
-
<script lang="ts">
|
|
14
|
-
let {
|
|
15
|
-
value = false,
|
|
16
|
-
id = generateWeakId(),
|
|
17
|
-
icon,
|
|
18
|
-
size,
|
|
19
|
-
label,
|
|
20
|
-
disabled,
|
|
21
|
-
onchange,
|
|
22
|
-
}: CheckboxProps = $props();
|
|
23
|
-
</script>
|
|
24
|
-
|
|
25
|
-
{#if icon}
|
|
26
|
-
<div
|
|
27
|
-
class="outer-checkbox-container"
|
|
28
|
-
class:fill-width={size === Size.FillWidth}
|
|
29
|
-
class:fill-height={size === Size.FillHeight}
|
|
30
|
-
>
|
|
31
|
-
<div
|
|
32
|
-
class="inner-checkbox-container with-icon"
|
|
33
|
-
class:disabled
|
|
34
|
-
>
|
|
35
|
-
<input
|
|
36
|
-
{id}
|
|
37
|
-
name={id}
|
|
38
|
-
class="with-icon"
|
|
39
|
-
{onchange}
|
|
40
|
-
type="checkbox"
|
|
41
|
-
{disabled}
|
|
42
|
-
checked={value}
|
|
43
|
-
/>
|
|
44
|
-
<label
|
|
45
|
-
class="icon-label"
|
|
46
|
-
for={id}
|
|
47
|
-
class:disabled
|
|
48
|
-
>
|
|
49
|
-
<Icon
|
|
50
|
-
{...icon}
|
|
51
|
-
size={icon.size ?? size ?? Size.DoubleXLarge}
|
|
52
|
-
/>
|
|
53
|
-
</label>
|
|
54
|
-
</div>
|
|
55
|
-
{#if label}
|
|
56
|
-
<label
|
|
57
|
-
class="text-label"
|
|
58
|
-
for={id}>{label}</label
|
|
59
|
-
>
|
|
60
|
-
{/if}
|
|
61
|
-
</div>
|
|
62
|
-
{:else}
|
|
63
|
-
<div
|
|
64
|
-
class="checkbox-container"
|
|
65
|
-
class:compact={size === Size.Tiny ||
|
|
66
|
-
size === Size.Small ||
|
|
67
|
-
size === Size.MediumSmall ||
|
|
68
|
-
size === Size.Medium}
|
|
69
|
-
>
|
|
70
|
-
<input
|
|
71
|
-
{id}
|
|
72
|
-
{onchange}
|
|
73
|
-
type="checkbox"
|
|
74
|
-
checked={value}
|
|
75
|
-
{disabled}
|
|
76
|
-
/>
|
|
77
|
-
{#if label}
|
|
78
|
-
<label
|
|
79
|
-
class="text-label"
|
|
80
|
-
for={id}>{label}</label
|
|
81
|
-
>
|
|
82
|
-
{/if}
|
|
83
|
-
</div>
|
|
84
|
-
{/if}
|
|
85
|
-
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
<script
|
|
4
|
+
lang="ts"
|
|
5
|
+
module
|
|
6
|
+
>
|
|
7
|
+
import { generateWeakId } from '../Helpers/Helpers.svelte.js';
|
|
8
|
+
import { Size } from '../Types/Internal/Size.js';
|
|
9
|
+
import type { CheckboxProps } from './CheckboxProps.js';
|
|
10
|
+
import Icon from './Icon.svelte';
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<script lang="ts">
|
|
14
|
+
let {
|
|
15
|
+
value = false,
|
|
16
|
+
id = generateWeakId(),
|
|
17
|
+
icon,
|
|
18
|
+
size,
|
|
19
|
+
label,
|
|
20
|
+
disabled,
|
|
21
|
+
onchange,
|
|
22
|
+
}: CheckboxProps = $props();
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
{#if icon}
|
|
26
|
+
<div
|
|
27
|
+
class="outer-checkbox-container"
|
|
28
|
+
class:fill-width={size === Size.FillWidth}
|
|
29
|
+
class:fill-height={size === Size.FillHeight}
|
|
30
|
+
>
|
|
31
|
+
<div
|
|
32
|
+
class="inner-checkbox-container with-icon"
|
|
33
|
+
class:disabled
|
|
34
|
+
>
|
|
35
|
+
<input
|
|
36
|
+
{id}
|
|
37
|
+
name={id}
|
|
38
|
+
class="with-icon"
|
|
39
|
+
{onchange}
|
|
40
|
+
type="checkbox"
|
|
41
|
+
{disabled}
|
|
42
|
+
checked={value}
|
|
43
|
+
/>
|
|
44
|
+
<label
|
|
45
|
+
class="icon-label"
|
|
46
|
+
for={id}
|
|
47
|
+
class:disabled
|
|
48
|
+
>
|
|
49
|
+
<Icon
|
|
50
|
+
{...icon}
|
|
51
|
+
size={icon.size ?? size ?? Size.DoubleXLarge}
|
|
52
|
+
/>
|
|
53
|
+
</label>
|
|
54
|
+
</div>
|
|
55
|
+
{#if label}
|
|
56
|
+
<label
|
|
57
|
+
class="text-label"
|
|
58
|
+
for={id}>{label}</label
|
|
59
|
+
>
|
|
60
|
+
{/if}
|
|
61
|
+
</div>
|
|
62
|
+
{:else}
|
|
63
|
+
<div
|
|
64
|
+
class="checkbox-container"
|
|
65
|
+
class:compact={size === Size.Tiny ||
|
|
66
|
+
size === Size.Small ||
|
|
67
|
+
size === Size.MediumSmall ||
|
|
68
|
+
size === Size.Medium}
|
|
69
|
+
>
|
|
70
|
+
<input
|
|
71
|
+
{id}
|
|
72
|
+
{onchange}
|
|
73
|
+
type="checkbox"
|
|
74
|
+
checked={value}
|
|
75
|
+
{disabled}
|
|
76
|
+
/>
|
|
77
|
+
{#if label}
|
|
78
|
+
<label
|
|
79
|
+
class="text-label"
|
|
80
|
+
for={id}>{label}</label
|
|
81
|
+
>
|
|
82
|
+
{/if}
|
|
83
|
+
</div>
|
|
84
|
+
{/if}
|
|
85
|
+
|
|
86
86
|
<style>.outer-checkbox-container {
|
|
87
87
|
box-sizing: border-box;
|
|
88
88
|
display: flex;
|
|
@@ -176,4 +176,4 @@ label {
|
|
|
176
176
|
display: block;
|
|
177
177
|
cursor: pointer;
|
|
178
178
|
position: relative;
|
|
179
|
-
}</style>
|
|
179
|
+
}</style>
|
|
@@ -1,166 +1,166 @@
|
|
|
1
|
-
<svelte:options runes />
|
|
2
|
-
|
|
3
|
-
<!--
|
|
4
|
-
@component
|
|
5
|
-
Visual host for `ContextMenuContext`. Renders the fixed-positioned
|
|
6
|
-
`<ul role="menu">` near the `(x, y)` coordinates the context exposes,
|
|
7
|
-
and clamps the position so the menu never spills off the viewport.
|
|
8
|
-
|
|
9
|
-
Mounted exactly once at the app root by `ContextWrapper`; consumers do
|
|
10
|
-
not render `ContextMenu` directly. Open a menu through the context:
|
|
11
|
-
|
|
12
|
-
```svelte
|
|
13
|
-
<script lang="ts">
|
|
14
|
-
import { ContextMenuContext } from '@lavalogic/scoria';
|
|
15
|
-
import { getContext } from 'svelte';
|
|
16
|
-
|
|
17
|
-
const ctx = getContext<ContextMenuContext>(ContextMenuContext.identifier);
|
|
18
|
-
</script>
|
|
19
|
-
|
|
20
|
-
<button oncontextmenu={(e) => ctx.openContextMenu(e, items)}>
|
|
21
|
-
Right-click me
|
|
22
|
-
</button>
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
### Keyboard
|
|
26
|
-
|
|
27
|
-
`Escape` closes the menu and returns focus to the previously focused
|
|
28
|
-
element; `ArrowUp` / `ArrowDown` cycle focus through the items.
|
|
29
|
-
-->
|
|
30
|
-
<script lang="ts">
|
|
31
|
-
import { ContextMenuContext } from '../../Contexts/ContextMenuContext.svelte.js';
|
|
32
|
-
import { devCatch } from '../../Helpers/Helpers.svelte.js';
|
|
33
|
-
import { getContext, tick, untrack } from 'svelte';
|
|
34
|
-
import { fade } from 'svelte/transition';
|
|
35
|
-
import { usePrefersReducedMotion } from '../../Helpers/prefersReducedMotion.svelte.js';
|
|
36
|
-
import ContextMenuOption from './ContextMenuOption.svelte';
|
|
37
|
-
import type { MaybeFocusable } from './MaybeFocusable.js';
|
|
38
|
-
|
|
39
|
-
const contextMenuContext = getContext<ContextMenuContext>(ContextMenuContext.identifier);
|
|
40
|
-
const reducedMotion = usePrefersReducedMotion();
|
|
41
|
-
const fadeDuration = $derived(reducedMotion.matches ? 0 : 50);
|
|
42
|
-
|
|
43
|
-
let _x: number = $state(0);
|
|
44
|
-
let _y: number = $state(0);
|
|
45
|
-
|
|
46
|
-
$effect(() => {
|
|
47
|
-
if (contextMenuContext.showMenu) {
|
|
48
|
-
recalculateCoordinates();
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
function recalculateCoordinates() {
|
|
53
|
-
_x = contextMenuContext.x;
|
|
54
|
-
_y = contextMenuContext.y;
|
|
55
|
-
|
|
56
|
-
tick()
|
|
57
|
-
.then(() => {
|
|
58
|
-
const rect = menuEl?.getBoundingClientRect();
|
|
59
|
-
if (!rect) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
_x = Math.min(window.innerWidth - rect.width, _x);
|
|
63
|
-
if (_y > window.innerHeight - rect.height) {
|
|
64
|
-
_y -= rect.height;
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
.catch(devCatch);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
let menuEl = $state<HTMLUListElement | undefined>();
|
|
71
|
-
function closeOnClickOutside(e: MouseEvent) {
|
|
72
|
-
if (!menuEl) {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (e.target === menuEl || menuEl.contains(e.target as Node)) {
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
contextMenuContext.closeMenu();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let previousElement = $state<MaybeFocusable | null | undefined>();
|
|
82
|
-
|
|
83
|
-
$effect.pre(() => {
|
|
84
|
-
if (contextMenuContext.showMenu) {
|
|
85
|
-
untrack(() => {
|
|
86
|
-
previousElement = document.activeElement as MaybeFocusable;
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
function refocusPrevious() {
|
|
92
|
-
previousElement?.focus?.();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function focusNext() {
|
|
96
|
-
if (!menuEl) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
const items = [...menuEl.querySelectorAll<HTMLElement>('[role="menuitem"]')];
|
|
100
|
-
if (items.length === 0) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const index = items.indexOf(document.activeElement as HTMLElement);
|
|
104
|
-
items.at((index + 1) % items.length)?.focus();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function focusPrev() {
|
|
108
|
-
if (!menuEl) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
const items = [...menuEl.querySelectorAll<HTMLElement>('[role="menuitem"]')];
|
|
112
|
-
if (items.length === 0) {
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const index = items.indexOf(document.activeElement as HTMLElement);
|
|
116
|
-
items.at((index - 1 + items.length) % items.length)?.focus();
|
|
117
|
-
}
|
|
118
|
-
</script>
|
|
119
|
-
|
|
120
|
-
<svelte:body onclick={closeOnClickOutside} />
|
|
121
|
-
|
|
122
|
-
{#if contextMenuContext.showMenu}
|
|
123
|
-
<ul
|
|
124
|
-
transition:fade={{ duration: fadeDuration }}
|
|
125
|
-
bind:this={menuEl}
|
|
126
|
-
style="top: {_y}px; left: {_x}px;"
|
|
127
|
-
role="menu"
|
|
128
|
-
aria-label="Context menu"
|
|
129
|
-
tabindex="-1"
|
|
130
|
-
onkeydown={(e) => {
|
|
131
|
-
if (e.key === 'Escape') {
|
|
132
|
-
e.stopImmediatePropagation();
|
|
133
|
-
e.stopPropagation();
|
|
134
|
-
e.preventDefault();
|
|
135
|
-
contextMenuContext.closeMenu();
|
|
136
|
-
refocusPrevious();
|
|
137
|
-
} else if (e.key == 'ArrowUp') {
|
|
138
|
-
e.stopImmediatePropagation();
|
|
139
|
-
e.stopPropagation();
|
|
140
|
-
e.preventDefault();
|
|
141
|
-
focusPrev();
|
|
142
|
-
} else if (e.key == 'ArrowDown') {
|
|
143
|
-
e.stopImmediatePropagation();
|
|
144
|
-
e.stopPropagation();
|
|
145
|
-
e.preventDefault();
|
|
146
|
-
focusNext();
|
|
147
|
-
}
|
|
148
|
-
}}
|
|
149
|
-
>
|
|
150
|
-
{#each contextMenuContext.items as item, index (item)}
|
|
151
|
-
<ContextMenuOption
|
|
152
|
-
{index}
|
|
153
|
-
disabled={item.disabled}
|
|
154
|
-
onclick={() => {
|
|
155
|
-
void item.callback();
|
|
156
|
-
contextMenuContext.closeMenu();
|
|
157
|
-
}}
|
|
158
|
-
text={item.text}
|
|
159
|
-
/>
|
|
160
|
-
{/each}
|
|
161
|
-
</ul>
|
|
162
|
-
{/if}
|
|
163
|
-
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
@component
|
|
5
|
+
Visual host for `ContextMenuContext`. Renders the fixed-positioned
|
|
6
|
+
`<ul role="menu">` near the `(x, y)` coordinates the context exposes,
|
|
7
|
+
and clamps the position so the menu never spills off the viewport.
|
|
8
|
+
|
|
9
|
+
Mounted exactly once at the app root by `ContextWrapper`; consumers do
|
|
10
|
+
not render `ContextMenu` directly. Open a menu through the context:
|
|
11
|
+
|
|
12
|
+
```svelte
|
|
13
|
+
<script lang="ts">
|
|
14
|
+
import { ContextMenuContext } from '@lavalogic/scoria';
|
|
15
|
+
import { getContext } from 'svelte';
|
|
16
|
+
|
|
17
|
+
const ctx = getContext<ContextMenuContext>(ContextMenuContext.identifier);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<button oncontextmenu={(e) => ctx.openContextMenu(e, items)}>
|
|
21
|
+
Right-click me
|
|
22
|
+
</button>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Keyboard
|
|
26
|
+
|
|
27
|
+
`Escape` closes the menu and returns focus to the previously focused
|
|
28
|
+
element; `ArrowUp` / `ArrowDown` cycle focus through the items.
|
|
29
|
+
-->
|
|
30
|
+
<script lang="ts">
|
|
31
|
+
import { ContextMenuContext } from '../../Contexts/ContextMenuContext.svelte.js';
|
|
32
|
+
import { devCatch } from '../../Helpers/Helpers.svelte.js';
|
|
33
|
+
import { getContext, tick, untrack } from 'svelte';
|
|
34
|
+
import { fade } from 'svelte/transition';
|
|
35
|
+
import { usePrefersReducedMotion } from '../../Helpers/prefersReducedMotion.svelte.js';
|
|
36
|
+
import ContextMenuOption from './ContextMenuOption.svelte';
|
|
37
|
+
import type { MaybeFocusable } from './MaybeFocusable.js';
|
|
38
|
+
|
|
39
|
+
const contextMenuContext = getContext<ContextMenuContext>(ContextMenuContext.identifier);
|
|
40
|
+
const reducedMotion = usePrefersReducedMotion();
|
|
41
|
+
const fadeDuration = $derived(reducedMotion.matches ? 0 : 50);
|
|
42
|
+
|
|
43
|
+
let _x: number = $state(0);
|
|
44
|
+
let _y: number = $state(0);
|
|
45
|
+
|
|
46
|
+
$effect(() => {
|
|
47
|
+
if (contextMenuContext.showMenu) {
|
|
48
|
+
recalculateCoordinates();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function recalculateCoordinates() {
|
|
53
|
+
_x = contextMenuContext.x;
|
|
54
|
+
_y = contextMenuContext.y;
|
|
55
|
+
|
|
56
|
+
tick()
|
|
57
|
+
.then(() => {
|
|
58
|
+
const rect = menuEl?.getBoundingClientRect();
|
|
59
|
+
if (!rect) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
_x = Math.min(window.innerWidth - rect.width, _x);
|
|
63
|
+
if (_y > window.innerHeight - rect.height) {
|
|
64
|
+
_y -= rect.height;
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.catch(devCatch);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let menuEl = $state<HTMLUListElement | undefined>();
|
|
71
|
+
function closeOnClickOutside(e: MouseEvent) {
|
|
72
|
+
if (!menuEl) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (e.target === menuEl || menuEl.contains(e.target as Node)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
contextMenuContext.closeMenu();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let previousElement = $state<MaybeFocusable | null | undefined>();
|
|
82
|
+
|
|
83
|
+
$effect.pre(() => {
|
|
84
|
+
if (contextMenuContext.showMenu) {
|
|
85
|
+
untrack(() => {
|
|
86
|
+
previousElement = document.activeElement as MaybeFocusable;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
function refocusPrevious() {
|
|
92
|
+
previousElement?.focus?.();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function focusNext() {
|
|
96
|
+
if (!menuEl) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const items = [...menuEl.querySelectorAll<HTMLElement>('[role="menuitem"]')];
|
|
100
|
+
if (items.length === 0) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const index = items.indexOf(document.activeElement as HTMLElement);
|
|
104
|
+
items.at((index + 1) % items.length)?.focus();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function focusPrev() {
|
|
108
|
+
if (!menuEl) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const items = [...menuEl.querySelectorAll<HTMLElement>('[role="menuitem"]')];
|
|
112
|
+
if (items.length === 0) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const index = items.indexOf(document.activeElement as HTMLElement);
|
|
116
|
+
items.at((index - 1 + items.length) % items.length)?.focus();
|
|
117
|
+
}
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<svelte:body onclick={closeOnClickOutside} />
|
|
121
|
+
|
|
122
|
+
{#if contextMenuContext.showMenu}
|
|
123
|
+
<ul
|
|
124
|
+
transition:fade={{ duration: fadeDuration }}
|
|
125
|
+
bind:this={menuEl}
|
|
126
|
+
style="top: {_y}px; left: {_x}px;"
|
|
127
|
+
role="menu"
|
|
128
|
+
aria-label="Context menu"
|
|
129
|
+
tabindex="-1"
|
|
130
|
+
onkeydown={(e) => {
|
|
131
|
+
if (e.key === 'Escape') {
|
|
132
|
+
e.stopImmediatePropagation();
|
|
133
|
+
e.stopPropagation();
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
contextMenuContext.closeMenu();
|
|
136
|
+
refocusPrevious();
|
|
137
|
+
} else if (e.key == 'ArrowUp') {
|
|
138
|
+
e.stopImmediatePropagation();
|
|
139
|
+
e.stopPropagation();
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
focusPrev();
|
|
142
|
+
} else if (e.key == 'ArrowDown') {
|
|
143
|
+
e.stopImmediatePropagation();
|
|
144
|
+
e.stopPropagation();
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
focusNext();
|
|
147
|
+
}
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
{#each contextMenuContext.items as item, index (item)}
|
|
151
|
+
<ContextMenuOption
|
|
152
|
+
{index}
|
|
153
|
+
disabled={item.disabled}
|
|
154
|
+
onclick={() => {
|
|
155
|
+
void item.callback();
|
|
156
|
+
contextMenuContext.closeMenu();
|
|
157
|
+
}}
|
|
158
|
+
text={item.text}
|
|
159
|
+
/>
|
|
160
|
+
{/each}
|
|
161
|
+
</ul>
|
|
162
|
+
{/if}
|
|
163
|
+
|
|
164
164
|
<style>ul {
|
|
165
165
|
padding: 0.375rem;
|
|
166
166
|
position: fixed;
|
|
@@ -175,4 +175,4 @@ element; `ArrowUp` / `ArrowDown` cycle focus through the items.
|
|
|
175
175
|
|
|
176
176
|
ul:focus {
|
|
177
177
|
outline: none;
|
|
178
|
-
}</style>
|
|
178
|
+
}</style>
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
<svelte:options runes />
|
|
2
|
-
|
|
3
|
-
<!--
|
|
4
|
-
@component
|
|
5
|
-
Visual separator for {@link ContextMenu}. Renders a thin horizontal
|
|
6
|
-
rule. The native `<hr>` ARIA mapping is `role="separator"`, which is
|
|
7
|
-
the correct semantic inside a `role="menu"` group, so no explicit
|
|
8
|
-
role is set.
|
|
9
|
-
|
|
10
|
-
Intended for use between groups of `ContextMenuOption` entries inside
|
|
11
|
-
a context menu rendered via the visual host.
|
|
12
|
-
-->
|
|
13
|
-
<hr />
|
|
14
|
-
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
@component
|
|
5
|
+
Visual separator for {@link ContextMenu}. Renders a thin horizontal
|
|
6
|
+
rule. The native `<hr>` ARIA mapping is `role="separator"`, which is
|
|
7
|
+
the correct semantic inside a `role="menu"` group, so no explicit
|
|
8
|
+
role is set.
|
|
9
|
+
|
|
10
|
+
Intended for use between groups of `ContextMenuOption` entries inside
|
|
11
|
+
a context menu rendered via the visual host.
|
|
12
|
+
-->
|
|
13
|
+
<hr />
|
|
14
|
+
|
|
15
15
|
<style>hr {
|
|
16
16
|
border-top: solid 1px #cbd4da;
|
|
17
17
|
width: 100%;
|
|
18
18
|
margin: 2px 0;
|
|
19
|
-
}</style>
|
|
19
|
+
}</style>
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
<svelte:options runes />
|
|
2
|
-
|
|
3
|
-
<!--
|
|
4
|
-
@component
|
|
5
|
-
A single row inside the {@link ContextMenu} host. Renders a
|
|
6
|
-
`<button role="menuitem">` and assigns a roving `tabindex`: the
|
|
7
|
-
first option (index `0`) receives focus on mount and is the only
|
|
8
|
-
tab stop, all subsequent options carry `tabindex="-1"` so arrow-key
|
|
9
|
-
navigation owns focus traversal within the menu.
|
|
10
|
-
|
|
11
|
-
Consumers do not usually mount `ContextMenuOption` directly; the
|
|
12
|
-
visual host iterates over `ContextMenuContext.items` and renders one
|
|
13
|
-
option per entry.
|
|
14
|
-
-->
|
|
15
|
-
<script lang="ts">
|
|
16
|
-
import { devCatch } from '../../Helpers/Helpers.svelte.js';
|
|
17
|
-
import { tick } from 'svelte';
|
|
18
|
-
import type { ContextMenuOptionProps } from './ContextMenuOptionProps.js';
|
|
19
|
-
|
|
20
|
-
let { text = '', index, disabled = false, onclick, children }: ContextMenuOptionProps = $props();
|
|
21
|
-
|
|
22
|
-
let element = $state<HTMLButtonElement | undefined>();
|
|
23
|
-
|
|
24
|
-
$effect(() => {
|
|
25
|
-
void element;
|
|
26
|
-
// Only the first option seeds focus. The check stays inside the
|
|
27
|
-
// effect (rather than gating registration) so Svelte's reactive
|
|
28
|
-
// dependency on `index` is recorded.
|
|
29
|
-
if (index === 0) {
|
|
30
|
-
tick()
|
|
31
|
-
.then(() => {
|
|
32
|
-
element?.focus();
|
|
33
|
-
})
|
|
34
|
-
.catch(devCatch);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
</script>
|
|
38
|
-
|
|
39
|
-
<button
|
|
40
|
-
class="context-menu-item"
|
|
41
|
-
role="menuitem"
|
|
42
|
-
class:disabled
|
|
43
|
-
tabindex={index === 0 ? 0 : -1}
|
|
44
|
-
aria-disabled={disabled ? 'true' : undefined}
|
|
45
|
-
bind:this={element}
|
|
46
|
-
onclick={(e) => {
|
|
47
|
-
e.stopImmediatePropagation();
|
|
48
|
-
e.stopPropagation();
|
|
49
|
-
e.preventDefault();
|
|
50
|
-
void onclick?.(e);
|
|
51
|
-
}}
|
|
52
|
-
>
|
|
53
|
-
{#if text}
|
|
54
|
-
{text}
|
|
55
|
-
{:else if children}
|
|
56
|
-
{@render children()}
|
|
57
|
-
{/if}
|
|
58
|
-
</button>
|
|
59
|
-
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
@component
|
|
5
|
+
A single row inside the {@link ContextMenu} host. Renders a
|
|
6
|
+
`<button role="menuitem">` and assigns a roving `tabindex`: the
|
|
7
|
+
first option (index `0`) receives focus on mount and is the only
|
|
8
|
+
tab stop, all subsequent options carry `tabindex="-1"` so arrow-key
|
|
9
|
+
navigation owns focus traversal within the menu.
|
|
10
|
+
|
|
11
|
+
Consumers do not usually mount `ContextMenuOption` directly; the
|
|
12
|
+
visual host iterates over `ContextMenuContext.items` and renders one
|
|
13
|
+
option per entry.
|
|
14
|
+
-->
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
import { devCatch } from '../../Helpers/Helpers.svelte.js';
|
|
17
|
+
import { tick } from 'svelte';
|
|
18
|
+
import type { ContextMenuOptionProps } from './ContextMenuOptionProps.js';
|
|
19
|
+
|
|
20
|
+
let { text = '', index, disabled = false, onclick, children }: ContextMenuOptionProps = $props();
|
|
21
|
+
|
|
22
|
+
let element = $state<HTMLButtonElement | undefined>();
|
|
23
|
+
|
|
24
|
+
$effect(() => {
|
|
25
|
+
void element;
|
|
26
|
+
// Only the first option seeds focus. The check stays inside the
|
|
27
|
+
// effect (rather than gating registration) so Svelte's reactive
|
|
28
|
+
// dependency on `index` is recorded.
|
|
29
|
+
if (index === 0) {
|
|
30
|
+
tick()
|
|
31
|
+
.then(() => {
|
|
32
|
+
element?.focus();
|
|
33
|
+
})
|
|
34
|
+
.catch(devCatch);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<button
|
|
40
|
+
class="context-menu-item"
|
|
41
|
+
role="menuitem"
|
|
42
|
+
class:disabled
|
|
43
|
+
tabindex={index === 0 ? 0 : -1}
|
|
44
|
+
aria-disabled={disabled ? 'true' : undefined}
|
|
45
|
+
bind:this={element}
|
|
46
|
+
onclick={(e) => {
|
|
47
|
+
e.stopImmediatePropagation();
|
|
48
|
+
e.stopPropagation();
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
void onclick?.(e);
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{#if text}
|
|
54
|
+
{text}
|
|
55
|
+
{:else if children}
|
|
56
|
+
{@render children()}
|
|
57
|
+
{/if}
|
|
58
|
+
</button>
|
|
59
|
+
|
|
60
60
|
<style>button {
|
|
61
61
|
background-color: #ffffff;
|
|
62
62
|
border: none;
|
|
@@ -85,4 +85,4 @@ button.disabled {
|
|
|
85
85
|
|
|
86
86
|
button.disabled:hover {
|
|
87
87
|
background: #ffffff;
|
|
88
|
-
}</style>
|
|
88
|
+
}</style>
|