@pienter/ui 0.3.0 → 0.7.1
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/CHANGELOG.md +258 -0
- package/CONVENTIONS.md +342 -385
- package/README.md +33 -18
- package/components/display/record-details/RecordDetails.vue +61 -0
- package/components/display/record-details/record-details.css +37 -0
- package/components/display/record-details/types.ts +8 -0
- package/components/feedback/toast/toast.css +1 -1
- package/components/form/block-editor/BlockEditor.vue +454 -0
- package/components/form/block-editor/block-editor.css +149 -0
- package/components/form/block-editor/types.ts +15 -0
- package/components/form/combobox/Combobox.vue +42 -72
- package/components/form/combobox/combobox.css +1 -1
- package/components/form/form/Form.vue +1 -2
- package/components/form/label/label.css +1 -1
- package/components/form/number-field/NumberField.vue +1 -2
- package/components/form/number-field/number-field.css +1 -1
- package/components/form/radio-group/RadioGroup.vue +2 -5
- package/components/form/record-form/RecordFields.vue +128 -0
- package/components/form/record-form/RecordForm.vue +116 -0
- package/components/form/record-form/fields.ts +20 -0
- package/components/form/record-form/record-form.css +15 -0
- package/components/form/record-form/types.ts +28 -0
- package/components/form/slider/slider.css +2 -3
- package/components/form/tags-input/tags-input.css +1 -2
- package/components/form/text-input/text-input.css +2 -0
- package/components/form/textarea/textarea.css +1 -1
- package/components/layout/accordion/Accordion.vue +6 -27
- package/components/layout/collapsible/Collapsible.vue +9 -19
- package/components/layout/index/Index.vue +353 -0
- package/components/layout/index/index.css +114 -0
- package/components/layout/index/useIndex.ts +390 -0
- package/components/layout/table/table.css +5 -4
- package/components/navigation/breadcrumb/Breadcrumb.vue +24 -5
- package/components/navigation/breadcrumb/breadcrumb.css +15 -0
- package/components/navigation/sidebar/Sidebar.vue +329 -84
- package/components/navigation/sidebar/SidebarMenuItem.vue +199 -0
- package/components/navigation/sidebar/context.ts +21 -0
- package/components/navigation/sidebar/sidebar.css +346 -78
- package/components/navigation/sidebar/types.ts +13 -1
- package/components/navigation/tabs/Tabs.vue +6 -0
- package/components/overlay/alert-dialog/AlertDialog.vue +10 -31
- package/components/overlay/command/Command.vue +10 -38
- package/components/overlay/command/command.css +1 -1
- package/components/overlay/dropdown-menu/DropdownMenu.vue +37 -62
- package/components/overlay/modal/Modal.vue +7 -28
- package/components/overlay/popover/Popover.vue +9 -31
- package/components/overlay/sheet/Sheet.vue +7 -28
- package/components/overlay/tooltip/Tooltip.vue +14 -19
- package/{utils/controllers/dialog.ts → composables/useDialog.ts} +90 -78
- package/composables/useDisclosure.ts +113 -0
- package/composables/useEventListener.ts +16 -0
- package/composables/useMenu.ts +212 -0
- package/{utils/controllers/popover.ts → composables/usePopover.ts} +107 -120
- package/package.json +16 -40
- package/styles/0-settings/colors.css +10 -0
- package/styles/0-settings/layout.css +18 -0
- package/styles/0-settings/motion.css +2 -2
- package/styles/0-settings/spacing.css +3 -1
- package/utils/a11y/focus.ts +9 -3
- package/utils/cms/index.ts +283 -0
- package/utils/cms/schema.json +126 -0
- package/utils/navigation/sidebar.ts +97 -0
- package/utils/validation/form.ts +6 -9
- package/components/action/button/Button.astro +0 -95
- package/components/action/button/IconButton.astro +0 -86
- package/components/display/avatar/Avatar.astro +0 -17
- package/components/display/avatar/AvatarStack.astro +0 -9
- package/components/display/badge/Badge.astro +0 -15
- package/components/display/empty/Empty.astro +0 -9
- package/components/display/icon/Icon.astro +0 -52
- package/components/feedback/alert/Alert.astro +0 -52
- package/components/feedback/progress/Progress.astro +0 -68
- package/components/feedback/skeleton/Skeleton.astro +0 -32
- package/components/feedback/spinner/Spinner.astro +0 -25
- package/components/feedback/toast/Toast.astro +0 -50
- package/components/form/checkbox/Checkbox.astro +0 -79
- package/components/form/date-input/DateInput.astro +0 -105
- package/components/form/form/Form.astro +0 -106
- package/components/form/input-otp/InputOTP.astro +0 -147
- package/components/form/label/Label.astro +0 -13
- package/components/form/number-field/NumberField.astro +0 -142
- package/components/form/radio-group/RadioGroup.astro +0 -105
- package/components/form/select/Select.astro +0 -105
- package/components/form/slider/Slider.astro +0 -205
- package/components/form/switch/Switch.astro +0 -75
- package/components/form/tags-input/TagsInput.astro +0 -153
- package/components/form/text-input/TextInput.astro +0 -84
- package/components/form/textarea/Textarea.astro +0 -86
- package/components/layout/card/Card.astro +0 -13
- package/components/layout/separator/Separator.astro +0 -31
- package/components/layout/table/Table.astro +0 -116
- package/components/navigation/breadcrumb/Breadcrumb.astro +0 -36
- package/components/navigation/navbar/Navbar.astro +0 -62
- package/components/navigation/pagination/PaginationFooter.astro +0 -24
- package/components/navigation/sidebar/Sidebar.astro +0 -132
- package/components/navigation/sidebar/SidebarItemRender.astro +0 -83
- package/components/navigation/sidebar/SidebarItemRender.vue +0 -98
- package/components/overlay/alert-dialog/AlertDialog.astro +0 -112
- package/components/overlay/modal/Modal.astro +0 -66
- package/utils/controllers/disclosure.ts +0 -117
- package/utils/controllers/form.ts +0 -524
- package/utils/controllers/index.ts +0 -39
- package/utils/controllers/menu.ts +0 -255
- package/utils/controllers/number-field.ts +0 -103
- package/utils/controllers/otp.ts +0 -252
- package/utils/controllers/sidebar.ts +0 -610
- package/utils/controllers/slider.ts +0 -336
- package/utils/controllers/tags-input.ts +0 -255
- package/utils/controllers/toast.ts +0 -426
|
@@ -38,12 +38,9 @@
|
|
|
38
38
|
</template>
|
|
39
39
|
|
|
40
40
|
<script setup lang="ts">
|
|
41
|
-
import { ref
|
|
41
|
+
import { ref } from 'vue';
|
|
42
42
|
import { generateId } from '../../../utils/a11y/id.js';
|
|
43
|
-
import {
|
|
44
|
-
mountDialog,
|
|
45
|
-
type DialogControl,
|
|
46
|
-
} from '../../../utils/controllers/dialog.js';
|
|
43
|
+
import { useDialog } from '../../../composables/useDialog.js';
|
|
47
44
|
|
|
48
45
|
defineOptions({ inheritAttrs: false });
|
|
49
46
|
|
|
@@ -72,7 +69,6 @@ const emit = defineEmits<{
|
|
|
72
69
|
const rootRef = ref<HTMLDialogElement | null>(null);
|
|
73
70
|
const titleId = generateId('alertdialog-title');
|
|
74
71
|
const descriptionId = generateId('alertdialog-desc');
|
|
75
|
-
let control: DialogControl | null = null;
|
|
76
72
|
|
|
77
73
|
function onConfirm(): void {
|
|
78
74
|
emit('confirm');
|
|
@@ -84,31 +80,14 @@ function onCancel(): void {
|
|
|
84
80
|
emit('update:open', false);
|
|
85
81
|
}
|
|
86
82
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// explicit confirm/cancel buttons wired in the template.
|
|
96
|
-
});
|
|
97
|
-
if (props.open) control.open();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
watch(
|
|
101
|
-
() => props.open,
|
|
102
|
-
(isOpen) => {
|
|
103
|
-
if (!control) return;
|
|
104
|
-
if (isOpen) control.open();
|
|
105
|
-
else control.close();
|
|
106
|
-
},
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
onBeforeUnmount(() => {
|
|
110
|
-
control?.teardown();
|
|
111
|
-
control = null;
|
|
83
|
+
useDialog(rootRef, {
|
|
84
|
+
role: 'alertdialog',
|
|
85
|
+
persistent: true,
|
|
86
|
+
initialFocus: 'cancel',
|
|
87
|
+
open: () => props.open,
|
|
88
|
+
// No onClose — persistent dialogs suppress Escape and backdrop-click
|
|
89
|
+
// dismissal at the composable layer. Close happens only through the
|
|
90
|
+
// explicit confirm/cancel buttons wired in the template.
|
|
112
91
|
});
|
|
113
92
|
</script>
|
|
114
93
|
|
|
@@ -82,11 +82,8 @@
|
|
|
82
82
|
</template>
|
|
83
83
|
|
|
84
84
|
<script setup lang="ts">
|
|
85
|
-
import { computed, ref
|
|
86
|
-
import {
|
|
87
|
-
mountDialog,
|
|
88
|
-
type DialogControl,
|
|
89
|
-
} from '../../../utils/controllers/dialog.js';
|
|
85
|
+
import { computed, ref } from 'vue';
|
|
86
|
+
import { useDialog } from '../../../composables/useDialog.js';
|
|
90
87
|
import { generateId } from '../../../utils/a11y/id.js';
|
|
91
88
|
import Icon from '../../display/icon/Icon.vue';
|
|
92
89
|
import type { IconName } from '../../../icons/index.js';
|
|
@@ -164,8 +161,6 @@ const listboxRef = ref<HTMLUListElement | null>(null);
|
|
|
164
161
|
const query = ref('');
|
|
165
162
|
const activeIndex = ref<number | null>(null);
|
|
166
163
|
|
|
167
|
-
let control: DialogControl | null = null;
|
|
168
|
-
|
|
169
164
|
// `commandsOpen` mirrors the parent's `open` for the input's
|
|
170
165
|
// `aria-expanded` state — when the dialog is open, the listbox
|
|
171
166
|
// inside is also "open" (visible), so the combobox is expanded.
|
|
@@ -317,37 +312,14 @@ function resetState(): void {
|
|
|
317
312
|
activeIndex.value = firstEnabled >= 0 ? firstEnabled : null;
|
|
318
313
|
}
|
|
319
314
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
control.open();
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
watch(
|
|
333
|
-
() => props.open,
|
|
334
|
-
(isOpen) => {
|
|
335
|
-
if (!control) return;
|
|
336
|
-
if (isOpen) {
|
|
337
|
-
// Reset state on every open so the palette always starts
|
|
338
|
-
// fresh — the canonical Cmd+K UX. Prior query / cursor
|
|
339
|
-
// position are not preserved between sessions.
|
|
340
|
-
resetState();
|
|
341
|
-
control.open();
|
|
342
|
-
} else {
|
|
343
|
-
control.close();
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
onBeforeUnmount(() => {
|
|
349
|
-
control?.teardown();
|
|
350
|
-
control = null;
|
|
315
|
+
useDialog(dialogRef, {
|
|
316
|
+
role: 'dialog',
|
|
317
|
+
open: () => props.open,
|
|
318
|
+
// Reset state on every open so the palette always starts fresh —
|
|
319
|
+
// the canonical Cmd+K UX. Prior query / cursor position are not
|
|
320
|
+
// preserved between sessions.
|
|
321
|
+
onBeforeOpen: resetState,
|
|
322
|
+
onClose: requestClose,
|
|
351
323
|
});
|
|
352
324
|
</script>
|
|
353
325
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* ===== pui-command =====
|
|
3
3
|
*
|
|
4
4
|
* Search palette built on the native `<dialog>` element via
|
|
5
|
-
* `
|
|
5
|
+
* `useDialog`. Reuses the combobox-pattern input + listbox
|
|
6
6
|
* inside the dialog chrome (search field at the top, scrollable
|
|
7
7
|
* commands below). The dialog provides top-layer rendering, focus
|
|
8
8
|
* trapping, and Escape dismissal; the controller layers on
|
|
@@ -13,13 +13,9 @@
|
|
|
13
13
|
</template>
|
|
14
14
|
|
|
15
15
|
<script setup lang="ts">
|
|
16
|
-
import { ref
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
type PopoverControl,
|
|
20
|
-
type Placement,
|
|
21
|
-
} from '../../../utils/controllers/popover.js';
|
|
22
|
-
import { mountMenu } from '../../../utils/controllers/menu.js';
|
|
16
|
+
import { ref } from 'vue';
|
|
17
|
+
import { usePopover, type Placement } from '../../../composables/usePopover.js';
|
|
18
|
+
import { useMenu } from '../../../composables/useMenu.js';
|
|
23
19
|
|
|
24
20
|
defineOptions({ inheritAttrs: false });
|
|
25
21
|
|
|
@@ -28,7 +24,7 @@ const props = withDefaults(
|
|
|
28
24
|
/**
|
|
29
25
|
* Required `id` for the dropdown menu element. The consumer
|
|
30
26
|
* wires their trigger button via the native HTML
|
|
31
|
-
* `popovertarget="<id>"` attribute, which `
|
|
27
|
+
* `popovertarget="<id>"` attribute, which `usePopover`
|
|
32
28
|
* auto-detects on mount as the positioning anchor and as the
|
|
33
29
|
* focus-restore target on close.
|
|
34
30
|
*/
|
|
@@ -78,63 +74,42 @@ const emit = defineEmits<{
|
|
|
78
74
|
}>();
|
|
79
75
|
|
|
80
76
|
const menuRef = ref<HTMLElement | null>(null);
|
|
81
|
-
let popoverControl: PopoverControl | null = null;
|
|
82
|
-
let menuTeardown: (() => void) | null = null;
|
|
83
77
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
'[role="
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
firstItem?.focus();
|
|
105
|
-
},
|
|
106
|
-
onClose: () => emit('update:open', false),
|
|
107
|
-
});
|
|
108
|
-
menuTeardown = mountMenu({
|
|
109
|
-
menuEl: menuRef.value,
|
|
110
|
-
// Escape closes the menu and lets `popover="auto"`'s
|
|
111
|
-
// light-dismiss + the controller's `toggle` event handle
|
|
112
|
-
// focus restoration to the trigger.
|
|
113
|
-
onEscape: () => popoverControl?.hide(),
|
|
114
|
-
// Auto-close on item click is the standard menu UX. The
|
|
115
|
-
// consumer's own `@click` on each `<button role="menuitem">`
|
|
116
|
-
// runs first (DOM bubble); this callback fires from
|
|
117
|
-
// `mountMenu`'s container-level click listener and closes
|
|
118
|
-
// the menu after the action.
|
|
119
|
-
onItemActivate: () => popoverControl?.hide(),
|
|
120
|
-
});
|
|
121
|
-
if (props.open) popoverControl.show();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
watch(
|
|
125
|
-
() => props.open,
|
|
126
|
-
(isOpen) => {
|
|
127
|
-
if (!popoverControl || isOpen === undefined) return;
|
|
128
|
-
if (isOpen) popoverControl.show();
|
|
129
|
-
else popoverControl.hide();
|
|
78
|
+
const popover = usePopover(menuRef, {
|
|
79
|
+
placement: () => props.placement,
|
|
80
|
+
offset: () => props.offset,
|
|
81
|
+
// `modal: false` (default) → `popover="auto"` so the browser
|
|
82
|
+
// handles outside-click / focus-loss / Escape light-dismiss.
|
|
83
|
+
// The menu is non-modal — page content remains visible and
|
|
84
|
+
// interactive once the menu closes.
|
|
85
|
+
open: () => props.open,
|
|
86
|
+
onOpen: () => {
|
|
87
|
+
emit('update:open', true);
|
|
88
|
+
// Move focus to the first menu item on open per the
|
|
89
|
+
// WAI-ARIA Menu pattern. The browser does NOT auto-focus
|
|
90
|
+
// popover content; the composable leaves focus management
|
|
91
|
+
// to consumers, so we explicitly walk into the menu here.
|
|
92
|
+
const firstItem = menuRef.value?.querySelector<HTMLElement>(
|
|
93
|
+
'[role="menuitem"]:not([disabled]):not([aria-disabled="true"]),' +
|
|
94
|
+
' [role="menuitemcheckbox"]:not([disabled]):not([aria-disabled="true"]),' +
|
|
95
|
+
' [role="menuitemradio"]:not([disabled]):not([aria-disabled="true"])',
|
|
96
|
+
);
|
|
97
|
+
firstItem?.focus();
|
|
130
98
|
},
|
|
131
|
-
)
|
|
99
|
+
onClose: () => emit('update:open', false),
|
|
100
|
+
});
|
|
132
101
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
102
|
+
useMenu(menuRef, {
|
|
103
|
+
// Escape closes the menu and lets `popover="auto"`'s
|
|
104
|
+
// light-dismiss + the composable's `toggle` event handle
|
|
105
|
+
// focus restoration to the trigger.
|
|
106
|
+
onEscape: () => popover.hide(),
|
|
107
|
+
// Auto-close on item click is the standard menu UX. The
|
|
108
|
+
// consumer's own `@click` on each `<button role="menuitem">`
|
|
109
|
+
// runs first (DOM bubble); this callback fires from
|
|
110
|
+
// `useMenu`'s container-level click listener and closes
|
|
111
|
+
// the menu after the action.
|
|
112
|
+
onItemActivate: () => popover.hide(),
|
|
138
113
|
});
|
|
139
114
|
</script>
|
|
140
115
|
|
|
@@ -19,12 +19,9 @@
|
|
|
19
19
|
</template>
|
|
20
20
|
|
|
21
21
|
<script setup lang="ts">
|
|
22
|
-
import { ref
|
|
22
|
+
import { ref } from 'vue';
|
|
23
23
|
import { generateId } from '../../../utils/a11y/id.js';
|
|
24
|
-
import {
|
|
25
|
-
mountDialog,
|
|
26
|
-
type DialogControl,
|
|
27
|
-
} from '../../../utils/controllers/dialog.js';
|
|
24
|
+
import { useDialog } from '../../../composables/useDialog.js';
|
|
28
25
|
|
|
29
26
|
defineOptions({ inheritAttrs: false });
|
|
30
27
|
|
|
@@ -48,35 +45,17 @@ const emit = defineEmits<{
|
|
|
48
45
|
|
|
49
46
|
const rootRef = ref<HTMLDialogElement | null>(null);
|
|
50
47
|
const titleId = generateId('modal-title');
|
|
51
|
-
let control: DialogControl | null = null;
|
|
52
48
|
|
|
53
49
|
function requestClose() {
|
|
54
50
|
emit('update:open', false);
|
|
55
51
|
emit('close');
|
|
56
52
|
}
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
onClose: requestClose,
|
|
64
|
-
});
|
|
65
|
-
if (props.open) control.open();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
watch(
|
|
69
|
-
() => props.open,
|
|
70
|
-
(isOpen) => {
|
|
71
|
-
if (!control) return;
|
|
72
|
-
if (isOpen) control.open();
|
|
73
|
-
else control.close();
|
|
74
|
-
},
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
onBeforeUnmount(() => {
|
|
78
|
-
control?.teardown();
|
|
79
|
-
control = null;
|
|
54
|
+
useDialog(rootRef, {
|
|
55
|
+
role: 'dialog',
|
|
56
|
+
persistent: () => props.persistent,
|
|
57
|
+
open: () => props.open,
|
|
58
|
+
onClose: requestClose,
|
|
80
59
|
});
|
|
81
60
|
</script>
|
|
82
61
|
|
|
@@ -14,12 +14,8 @@
|
|
|
14
14
|
</template>
|
|
15
15
|
|
|
16
16
|
<script setup lang="ts">
|
|
17
|
-
import { ref
|
|
18
|
-
import {
|
|
19
|
-
mountPopover,
|
|
20
|
-
type PopoverControl,
|
|
21
|
-
type Placement,
|
|
22
|
-
} from '../../../utils/controllers/popover.js';
|
|
17
|
+
import { ref } from 'vue';
|
|
18
|
+
import { usePopover, type Placement } from '../../../composables/usePopover.js';
|
|
23
19
|
|
|
24
20
|
defineOptions({ inheritAttrs: false });
|
|
25
21
|
|
|
@@ -79,32 +75,14 @@ const emit = defineEmits<{
|
|
|
79
75
|
}>();
|
|
80
76
|
|
|
81
77
|
const rootRef = ref<HTMLElement | null>(null);
|
|
82
|
-
let control: PopoverControl | null = null;
|
|
83
78
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
onClose: () => emit('update:open', false),
|
|
92
|
-
});
|
|
93
|
-
if (props.open) control.show();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
watch(
|
|
97
|
-
() => props.open,
|
|
98
|
-
(isOpen) => {
|
|
99
|
-
if (!control || isOpen === undefined) return;
|
|
100
|
-
if (isOpen) control.show();
|
|
101
|
-
else control.hide();
|
|
102
|
-
},
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
onBeforeUnmount(() => {
|
|
106
|
-
control?.teardown();
|
|
107
|
-
control = null;
|
|
79
|
+
usePopover(rootRef, {
|
|
80
|
+
placement: () => props.placement,
|
|
81
|
+
offset: () => props.offset,
|
|
82
|
+
modal: props.modal,
|
|
83
|
+
open: () => props.open,
|
|
84
|
+
onOpen: () => emit('update:open', true),
|
|
85
|
+
onClose: () => emit('update:open', false),
|
|
108
86
|
});
|
|
109
87
|
</script>
|
|
110
88
|
|
|
@@ -20,12 +20,9 @@
|
|
|
20
20
|
</template>
|
|
21
21
|
|
|
22
22
|
<script setup lang="ts">
|
|
23
|
-
import { ref
|
|
23
|
+
import { ref } from 'vue';
|
|
24
24
|
import { generateId } from '../../../utils/a11y/id.js';
|
|
25
|
-
import {
|
|
26
|
-
mountDialog,
|
|
27
|
-
type DialogControl,
|
|
28
|
-
} from '../../../utils/controllers/dialog.js';
|
|
25
|
+
import { useDialog } from '../../../composables/useDialog.js';
|
|
29
26
|
|
|
30
27
|
defineOptions({ inheritAttrs: false });
|
|
31
28
|
|
|
@@ -51,35 +48,17 @@ const emit = defineEmits<{
|
|
|
51
48
|
|
|
52
49
|
const rootRef = ref<HTMLDialogElement | null>(null);
|
|
53
50
|
const titleId = generateId('sheet-title');
|
|
54
|
-
let control: DialogControl | null = null;
|
|
55
51
|
|
|
56
52
|
function requestClose(): void {
|
|
57
53
|
emit('update:open', false);
|
|
58
54
|
emit('close');
|
|
59
55
|
}
|
|
60
56
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
onClose: requestClose,
|
|
67
|
-
});
|
|
68
|
-
if (props.open) control.open();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
watch(
|
|
72
|
-
() => props.open,
|
|
73
|
-
(isOpen) => {
|
|
74
|
-
if (!control) return;
|
|
75
|
-
if (isOpen) control.open();
|
|
76
|
-
else control.close();
|
|
77
|
-
},
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
onBeforeUnmount(() => {
|
|
81
|
-
control?.teardown();
|
|
82
|
-
control = null;
|
|
57
|
+
useDialog(rootRef, {
|
|
58
|
+
role: 'dialog',
|
|
59
|
+
persistent: () => props.persistent,
|
|
60
|
+
open: () => props.open,
|
|
61
|
+
onClose: requestClose,
|
|
83
62
|
});
|
|
84
63
|
</script>
|
|
85
64
|
|
|
@@ -13,11 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
<script setup lang="ts">
|
|
15
15
|
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
|
16
|
-
import {
|
|
17
|
-
mountPopover,
|
|
18
|
-
type PopoverControl,
|
|
19
|
-
type Placement,
|
|
20
|
-
} from '../../../utils/controllers/popover.js';
|
|
16
|
+
import { usePopover, type Placement } from '../../../composables/usePopover.js';
|
|
21
17
|
import { generateId } from '../../../utils/a11y/id.js';
|
|
22
18
|
|
|
23
19
|
const props = withDefaults(
|
|
@@ -64,11 +60,19 @@ const tooltipRef = ref<HTMLElement | null>(null);
|
|
|
64
60
|
// `generateId` runs at script-setup execution time — safe in SSR/hydration
|
|
65
61
|
// because the same id is produced in the same order on each render path.
|
|
66
62
|
const tooltipId = generateId('tooltip');
|
|
67
|
-
let control: PopoverControl | null = null;
|
|
68
63
|
let openTimer: number | null = null;
|
|
69
64
|
let closeTimer: number | null = null;
|
|
70
65
|
let trigger: HTMLElement | null = null;
|
|
71
66
|
|
|
67
|
+
// The trigger is resolved from the slot in `onMounted`, after the
|
|
68
|
+
// composable wires itself; the lazy getter hands it over on first open.
|
|
69
|
+
const control = usePopover(tooltipRef, {
|
|
70
|
+
anchor: () => trigger,
|
|
71
|
+
placement: () => props.placement,
|
|
72
|
+
offset: 6,
|
|
73
|
+
modal: false,
|
|
74
|
+
});
|
|
75
|
+
|
|
72
76
|
function clearTimers(): void {
|
|
73
77
|
if (openTimer != null) {
|
|
74
78
|
clearTimeout(openTimer);
|
|
@@ -83,7 +87,7 @@ function clearTimers(): void {
|
|
|
83
87
|
function scheduleOpen(): void {
|
|
84
88
|
clearTimers();
|
|
85
89
|
openTimer = window.setTimeout(() => {
|
|
86
|
-
control
|
|
90
|
+
control.show();
|
|
87
91
|
openTimer = null;
|
|
88
92
|
}, props.openDelay);
|
|
89
93
|
}
|
|
@@ -91,7 +95,7 @@ function scheduleOpen(): void {
|
|
|
91
95
|
function scheduleClose(): void {
|
|
92
96
|
clearTimers();
|
|
93
97
|
closeTimer = window.setTimeout(() => {
|
|
94
|
-
control
|
|
98
|
+
control.hide();
|
|
95
99
|
closeTimer = null;
|
|
96
100
|
}, props.closeDelay);
|
|
97
101
|
}
|
|
@@ -110,12 +114,12 @@ function onPointerLeave(): void {
|
|
|
110
114
|
// are a mouse-only affordance.
|
|
111
115
|
function onFocus(): void {
|
|
112
116
|
clearTimers();
|
|
113
|
-
control
|
|
117
|
+
control.show();
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
function onBlur(): void {
|
|
117
121
|
clearTimers();
|
|
118
|
-
control
|
|
122
|
+
control.hide();
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
function onTooltipPointerEnter(): void {
|
|
@@ -154,13 +158,6 @@ onMounted(() => {
|
|
|
154
158
|
existing ? `${existing} ${tooltipId}` : tooltipId,
|
|
155
159
|
);
|
|
156
160
|
|
|
157
|
-
control = mountPopover(tooltipEl, {
|
|
158
|
-
anchor: trigger,
|
|
159
|
-
placement: props.placement,
|
|
160
|
-
offset: 6,
|
|
161
|
-
modal: false,
|
|
162
|
-
});
|
|
163
|
-
|
|
164
161
|
trigger.addEventListener('pointerenter', onPointerEnter);
|
|
165
162
|
trigger.addEventListener('pointerleave', onPointerLeave);
|
|
166
163
|
trigger.addEventListener('focus', onFocus);
|
|
@@ -199,8 +196,6 @@ onBeforeUnmount(() => {
|
|
|
199
196
|
onTooltipPointerLeave,
|
|
200
197
|
);
|
|
201
198
|
}
|
|
202
|
-
control?.teardown();
|
|
203
|
-
control = null;
|
|
204
199
|
trigger = null;
|
|
205
200
|
});
|
|
206
201
|
</script>
|