@nexdom/uimed-vue 2.0.0-beta.25 → 2.0.0-beta.26

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.
@@ -433,8 +433,8 @@ declare const __VLS_export$14: import("vue", { with: { "resolution-mode": "impor
433
433
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
434
434
  }>, {
435
435
  variant: TextFieldVariant;
436
- clearable: boolean;
437
436
  type: TextFieldType;
437
+ clearable: boolean;
438
438
  }, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>;
439
439
  //#endregion
440
440
  //#region src/components/inputs/checkbox/types.d.ts
@@ -1,5 +1,5 @@
1
- import { a as messages, n as owners, r as requests, s as useVuetifyColor } from "./use-dialog-Ck2_ZW7T.js";
2
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, guardReactiveProps, inject, isRef, mergeModels, mergeProps, normalizeClass, normalizeProps, onMounted, onScopeDispose, onUnmounted, openBlock, provide, ref, renderList, renderSlot, resolveDynamicComponent, shallowRef, toDisplayString, toValue, unref, useId, useModel, useSlots, useTemplateRef, watch, withCtx, withModifiers } from "vue";
1
+ import { a as messages, n as owners$1, r as requests, s as useVuetifyColor } from "./use-dialog-Ck2_ZW7T.js";
2
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, guardReactiveProps, inject, isRef, mergeModels, mergeProps, nextTick, normalizeClass, normalizeProps, onMounted, onScopeDispose, onUnmounted, openBlock, provide, ref, renderList, renderSlot, resolveDynamicComponent, shallowRef, toDisplayString, toValue, unref, useId, useModel, useSlots, useTemplateRef, watch, withCtx, withKeys, withModifiers } from "vue";
3
3
  import { pt } from "vuetify/locale";
4
4
  import { VApp, VAppBar, VAppBarNavIcon, VAppBarTitle, VAutocomplete, VAvatar, VBadge, VBtn, VCard, VCardActions, VCardText, VCheckboxBtn, VCol, VCombobox, VContainer, VDataIterator, VDatePicker, VDialog, VDivider, VEmptyState, VExpansionPanel, VExpansionPanelText, VExpansionPanelTitle, VExpansionPanels, VForm, VIcon, VIconBtn, VImg, VList, VListGroup, VListItem, VListItemAction, VListSubheader, VLocaleProvider, VMain, VMenu, VNavigationDrawer, VPagination, VProgressLinear, VRow, VSkeletonLoader, VSnackbarQueue, VTable, VTextField, VTimePicker } from "vuetify/components";
5
5
  import { formatDateTime, isEmail, isEmpty, isPhone, isUrl } from "@nexdom/shared/utils";
@@ -15,6 +15,101 @@ function isExternalRoute(route) {
15
15
  return typeof route === "string" && route.startsWith("http");
16
16
  }
17
17
  //#endregion
18
+ //#region src/composables/navigation/use-navigation-search-shortcut.ts
19
+ /**
20
+ * Mounted navigation menus, in mounting order. Only the first one answers the shortcut, so pages
21
+ * with more than one `UMain` (like the docs) don't open every menu at once.
22
+ */
23
+ const owners = ref([]);
24
+ /**
25
+ * Focus targets where the shortcut is left to the page: overlays (dialogs, confirmations and
26
+ * menus) and other modals, the activators of open menus (which keep the focus when opened by a
27
+ * click, and would pull it back from the navigation menu) and rich text editors, which usually
28
+ * take Ctrl+K to insert links.
29
+ */
30
+ const IGNORED_TARGETS = [
31
+ ".v-overlay",
32
+ "dialog",
33
+ "[aria-modal='true']",
34
+ "[aria-haspopup][aria-expanded='true']",
35
+ "[contenteditable]:not([contenteditable='false'])"
36
+ ].join(", ");
37
+ /** Accessible name of the search field, whose placeholder also shows the shortcut. */
38
+ const SEARCH_NAME = "Buscar";
39
+ /**
40
+ * Shortcut keys for the current platform. Starts with the non-macOS keys and switches on mount,
41
+ * so server-rendered pages (the docs) hydrate without mismatches.
42
+ */
43
+ function useNavigationShortcutKeys() {
44
+ const isMac = ref(false);
45
+ onMounted(() => isMac.value = isMacOS());
46
+ return {
47
+ label: computed(() => isMac.value ? "⌘K" : "Ctrl+K"),
48
+ ariaKeyshortcuts: computed(() => isMac.value ? "Meta+K" : "Control+K")
49
+ };
50
+ }
51
+ /**
52
+ * Listens to Ctrl+K (⌘K on macOS) on `window` while mounted: opens the menu, waits for it to
53
+ * render (the drawer is `inert` while closed) and focuses the search field (a disabled field, as
54
+ * while loading, can't take the focus). When the menu opened by the shortcut closes with the focus
55
+ * inside it, the focus goes back to where it was.
56
+ */
57
+ function useNavigationSearchShortcut(options) {
58
+ const { open, field } = options;
59
+ const owner = Symbol("navigation-search-shortcut");
60
+ /** Element focused before the shortcut opened the menu, which gets the focus back on close. */
61
+ let origin = null;
62
+ onMounted(() => {
63
+ owners.value.push(owner);
64
+ globalThis.addEventListener("keydown", onKeydown, { capture: true });
65
+ findInput().setAttribute("aria-label", SEARCH_NAME);
66
+ });
67
+ onUnmounted(() => {
68
+ owners.value = owners.value.filter((item) => item !== owner);
69
+ globalThis.removeEventListener("keydown", onKeydown, { capture: true });
70
+ });
71
+ watch(open, (isOpen) => {
72
+ if (isOpen) return;
73
+ const target = origin;
74
+ origin = null;
75
+ if (!findInput().closest(".v-navigation-drawer").contains(document.activeElement)) return;
76
+ if (target instanceof HTMLElement || target instanceof SVGElement) target.focus();
77
+ });
78
+ function onKeydown(event) {
79
+ if (owners.value[0] !== owner || isIgnoredTarget(event)) return;
80
+ if (isShortcut(event)) openAndFocus(event);
81
+ else if (event.key === "Escape" && origin) close();
82
+ }
83
+ async function openAndFocus(event) {
84
+ event.preventDefault();
85
+ if (!open.value) origin = document.activeElement;
86
+ open.value = true;
87
+ await nextTick();
88
+ const input = findInput();
89
+ input.focus({ preventScroll: true });
90
+ input.select();
91
+ }
92
+ function findInput() {
93
+ return field.value.$el.querySelector("input");
94
+ }
95
+ function close() {
96
+ open.value = false;
97
+ }
98
+ return {
99
+ ...useNavigationShortcutKeys(),
100
+ close
101
+ };
102
+ }
103
+ function isMacOS() {
104
+ return navigator.userAgent.includes("Macintosh");
105
+ }
106
+ function isShortcut(event) {
107
+ return (isMacOS() ? event.metaKey : event.ctrlKey) && !event.shiftKey && !event.altKey && !event.isComposing && event.key.toLowerCase() === "k";
108
+ }
109
+ function isIgnoredTarget(event) {
110
+ return event.target instanceof Element && event.target.closest(IGNORED_TARGETS) !== null;
111
+ }
112
+ //#endregion
18
113
  //#region src/components/app-bar/app-bar.vue?vue&type=script&setup=true&lang.ts
19
114
  const _hoisted_1$3 = { class: "opacity-60" };
20
115
  //#endregion
@@ -69,6 +164,7 @@ var app_bar_default = /* @__PURE__ */ defineComponent({
69
164
  const props = __props;
70
165
  const notificationsOpen = useModel(__props, "notificationsOpen");
71
166
  const navigationOpen = useModel(__props, "navigationOpen");
167
+ const { ariaKeyshortcuts: navigationKeyshortcuts } = useNavigationShortcutKeys();
72
168
  const helpBtnDataTestid = computed(() => `${props.dataTestid}-help`);
73
169
  const notificationsBtnDataTestid = computed(() => `${props.dataTestid}-notifications`);
74
170
  const navigationBtnDataTestid = computed(() => `${props.dataTestid}-navigation`);
@@ -221,8 +317,13 @@ var app_bar_default = /* @__PURE__ */ defineComponent({
221
317
  "data-testid": navigationBtnDataTestid.value,
222
318
  "aria-expanded": navigationOpen.value,
223
319
  "aria-label": "Menu de navegação",
320
+ "aria-keyshortcuts": unref(navigationKeyshortcuts),
224
321
  onClick: _cache[0] || (_cache[0] = ($event) => navigationOpen.value = !navigationOpen.value)
225
- }, null, 8, ["data-testid", "aria-expanded"])) : createCommentVNode("v-if", true),
322
+ }, null, 8, [
323
+ "data-testid",
324
+ "aria-expanded",
325
+ "aria-keyshortcuts"
326
+ ])) : createCommentVNode("v-if", true),
226
327
  __props.logo ? (openBlock(), createBlock(unref(VImg), {
227
328
  key: 1,
228
329
  src: props.logo,
@@ -488,6 +589,10 @@ var navigation_menu_default = /* @__PURE__ */ defineComponent({
488
589
  const props = __props;
489
590
  const open = useModel(__props, "modelValue");
490
591
  const search = ref("");
592
+ const { label: shortcutLabel, close } = useNavigationSearchShortcut({
593
+ open,
594
+ field: useTemplateRef("searchField")
595
+ });
491
596
  const filteredItems = computed(() => filterItems());
492
597
  const searchDataTestid = computed(() => `${props.dataTestid}-search`);
493
598
  const query = computed(() => search.value.toLowerCase());
@@ -518,17 +623,20 @@ var navigation_menu_default = /* @__PURE__ */ defineComponent({
518
623
  "data-testid": props.dataTestid,
519
624
  "aria-busy": props.loading || void 0,
520
625
  absolute: "",
521
- temporary: ""
626
+ temporary: "",
627
+ onKeydown: withKeys(unref(close), ["esc"])
522
628
  }, {
523
629
  prepend: withCtx(() => [createElementVNode("div", _hoisted_1$2, [createVNode(text_field_default, {
630
+ ref: "searchField",
524
631
  modelValue: search.value,
525
632
  "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => search.value = $event),
526
633
  type: "search",
527
- label: "Buscar",
634
+ placeholder: `Buscar (${unref(shortcutLabel)})`,
528
635
  "data-testid": searchDataTestid.value,
529
636
  disabled: props.loading
530
637
  }, null, 8, [
531
638
  "modelValue",
639
+ "placeholder",
532
640
  "data-testid",
533
641
  "disabled"
534
642
  ])])]),
@@ -561,7 +669,8 @@ var navigation_menu_default = /* @__PURE__ */ defineComponent({
561
669
  }, 8, [
562
670
  "modelValue",
563
671
  "data-testid",
564
- "aria-busy"
672
+ "aria-busy",
673
+ "onKeydown"
565
674
  ]);
566
675
  };
567
676
  }
@@ -892,7 +1001,7 @@ var dialog_host_default = /* @__PURE__ */ defineComponent({
892
1001
  __name: "dialog-host",
893
1002
  setup(__props) {
894
1003
  const owner = Symbol("dialog");
895
- const isOwner = computed(() => owners.value[0] === owner);
1004
+ const isOwner = computed(() => owners$1.value[0] === owner);
896
1005
  const next = computed(() => isOwner.value ? requests.value[0] : void 0);
897
1006
  /** Dialog on display, kept until its leave transition ends. */
898
1007
  const request = shallowRef();
@@ -921,8 +1030,8 @@ var dialog_host_default = /* @__PURE__ */ defineComponent({
921
1030
  persistent: () => isRunning.value,
922
1031
  afterLeave: showNext
923
1032
  });
924
- onMounted(() => owners.value.push(owner));
925
- onUnmounted(() => owners.value = owners.value.filter((item) => item !== owner));
1033
+ onMounted(() => owners$1.value.push(owner));
1034
+ onUnmounted(() => owners$1.value = owners$1.value.filter((item) => item !== owner));
926
1035
  watch(next, show);
927
1036
  function show() {
928
1037
  if (request.value) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexdom/uimed-vue",
3
- "version": "2.0.0-beta.25",
3
+ "version": "2.0.0-beta.26",
4
4
  "description": "A ui framework for NEXDOM applications.",
5
5
  "keywords": [
6
6
  "lib",