@apptegy/nuxt-navigation 0.1.76 → 0.1.78

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/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=3.0.0"
5
5
  },
6
6
  "configKey": "@apptegy/nuxt-navigation",
7
- "version": "0.1.76",
7
+ "version": "0.1.78",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.1",
10
10
  "unbuild": "unknown"
@@ -6,6 +6,7 @@ const appConfig = useAppConfig();
6
6
  const zIndex = computed(() => appConfig?.navigation?.zIndex || 100);
7
7
  const emit = defineEmits(["update:model-value"]);
8
8
  const dropdownTriggerRef = ref();
9
+ const inputRef = ref();
9
10
  const optionRef = ref();
10
11
  const selectedOrg = computed(() => {
11
12
  return props.orgs.find(({ id }) => id == props.modelValue);
@@ -17,13 +18,14 @@ const props = defineProps({
17
18
  const {
18
19
  active,
19
20
  searchQuery,
21
+ highlightedIndex,
20
22
  toggleDropdown,
21
23
  closeDropdown,
22
24
  onClickOutsideHandler,
23
25
  keydownActionsOnTrigger,
24
26
  handleKeydown,
25
27
  handleDropdownSearch
26
- } = useDropdown(props.orgs, optionRef, dropdownTriggerRef, {
28
+ } = useDropdown(props.orgs, optionRef, inputRef, {
27
29
  searchable: true,
28
30
  trackBy: "name"
29
31
  });
@@ -38,6 +40,12 @@ const sortedOrgs = computed(() => {
38
40
  }
39
41
  });
40
42
  });
43
+ const highlightedOrg = computed(() => {
44
+ if (highlightedIndex.value >= 0 && highlightedIndex.value < props.orgs.length) {
45
+ return props.orgs[highlightedIndex.value];
46
+ }
47
+ return null;
48
+ });
41
49
  function handleSelectedOrg($event, orgId) {
42
50
  if ($event.type == "click") {
43
51
  emit("update:model-value", orgId);
@@ -97,7 +105,10 @@ function handleSelectedOrg($event, orgId) {
97
105
  v-on-click-outside="onClickOutsideHandler"
98
106
  role="option"
99
107
  class="org-option"
100
- :class="{ selected: option.id === modelValue }"
108
+ :class="{
109
+ selected: option.id === modelValue,
110
+ highlighted: highlightedOrg && option.id === highlightedOrg.id
111
+ }"
101
112
  :aria-selected="option.id === modelValue"
102
113
  tabindex="-1"
103
114
  @keydown="handleSelectedOrg($event, option.id)"
@@ -179,6 +190,9 @@ function handleSelectedOrg($event, orgId) {
179
190
  background-position: center left 15px;
180
191
  color: var(--purple);
181
192
  }
193
+ .org-selection .dropdown .org-option.highlighted {
194
+ background-color: #f8f6fc;
195
+ }
182
196
  .org-selection .dropdown .org-option:focus-visible, .org-selection .dropdown .org-option:hover {
183
197
  background-color: #f8f6fc;
184
198
  }
@@ -20,21 +20,21 @@
20
20
  <div :class="['header-actions', { 'collapsed': !expanded && !isMobile }]">
21
21
  <NuxtLink v-if="expanded" class="no-link-style" :to="homepageUrl">
22
22
  <slot name="logo">
23
- <div class="navbar--logo">
24
- <img v-if="brandLogo" :src="brandLogo" :alt="$t('navigation.brandLogoAlt')">
25
- <component
26
- :is="ApptegyLogo"
27
- v-else
28
- :alt="$t('navigation.brandLogoAlt')"
29
- :use-light-logo="isAdmin || Object.values(getBrandingStyles).length === 0"
30
- />
31
- <div v-if="!brand?.hide_school_name">
32
- <div class="school-name"> {{ brand?.school_name }} </div>
33
- <div v-if="brand?.school_tag_line" class="school-tag">{{ brand.school_tag_line }}</div>
34
- </div>
23
+ <div class="navbar--logo" :class="{ 'big-logo': _brandLogoWidth > 80 }">
24
+ <img v-if="brandLogo" ref="brandLogoRef" :src="brandLogo" :alt="$t('navigation.brandLogoAlt')">
25
+ <component
26
+ :is="ApptegyLogo"
27
+ v-else
28
+ :alt="$t('navigation.brandLogoAlt')"
29
+ :use-light-logo="isAdmin || Object.values(getBrandingStyles).length === 0"
30
+ />
31
+ <div v-if="!brand?.hide_school_name">
32
+ <div class="school-name"> {{ brand?.school_name }} </div>
33
+ <div v-if="brand?.school_tag_line" class="school-tag">{{ brand.school_tag_line }}</div>
35
34
  </div>
36
- </slot>
37
- </NuxtLink>
35
+ </div>
36
+ </slot>
37
+ </NuxtLink>
38
38
  <div v-if="!isMobile && showCollapseButton">
39
39
  <button v-if="lockedExpanded && !allowCollapse && expanded" class="hamburger-button" @click="closeSidebar" v-tooltip.bottom="'Unpin menu'">
40
40
  <Pinned />
@@ -91,7 +91,7 @@
91
91
  </template>
92
92
 
93
93
  <script setup>
94
- import { useSidebar, useBranding, useNuxtApp, ref, watch } from "#imports";
94
+ import { useSidebar, useBranding, useNuxtApp, ref, watch, computed, onMounted, onBeforeUnmount } from "#imports";
95
95
  import ApptegyLogo from "./Icons/ApptegyLogo.vue";
96
96
  import { useBreakpoints, breakpointsTailwind } from "@vueuse/core";
97
97
  import Unpinned from "./Icons/Unpinned.vue";
@@ -128,10 +128,30 @@ const emit = defineEmits(["toggle-expand"]);
128
128
  const { state: expanded } = useSidebar();
129
129
  const { brand, getBrandingStyles } = await useBranding(props.clientId);
130
130
  const brandLogo = ref(brand.value?.logo_light);
131
+ const brandLogoRef = ref(null);
132
+ const brandLogoWidthPx = ref(0);
133
+ let brandLogoResizeObserver = null;
134
+ const _brandLogoWidth = computed(() => brandLogoWidthPx.value);
131
135
  const breakpoints = useBreakpoints(breakpointsTailwind);
132
136
  const isMobile = breakpoints.smaller("md");
133
137
  const lockedExpanded = ref(true);
134
138
  const allowCollapse = ref(false);
139
+ function updateBrandLogoWidth() {
140
+ brandLogoWidthPx.value = brandLogoRef.value?.clientWidth ?? 0;
141
+ }
142
+ onMounted(() => {
143
+ updateBrandLogoWidth();
144
+ if (brandLogoRef.value) {
145
+ brandLogoResizeObserver = new ResizeObserver(() => {
146
+ updateBrandLogoWidth();
147
+ });
148
+ brandLogoResizeObserver.observe(brandLogoRef.value);
149
+ }
150
+ });
151
+ onBeforeUnmount(() => {
152
+ brandLogoResizeObserver?.disconnect();
153
+ brandLogoResizeObserver = null;
154
+ });
135
155
  watch(expanded, (value) => {
136
156
  emit("toggle-expand", value ? 264 : 64);
137
157
  });
@@ -223,6 +243,10 @@ function lockSidebar() {
223
243
  align-items: center;
224
244
  gap: 8px;
225
245
  }
246
+ .navbar--logo.big-logo {
247
+ flex-direction: column;
248
+ align-items: flex-start;
249
+ }
226
250
  .navbar--logo .school-name {
227
251
  font-size: 16px;
228
252
  font-style: normal;
@@ -6,13 +6,14 @@ export declare function useDropdown(options: Array<OptionItem>, dropdownOptionsR
6
6
  }): {
7
7
  active: Ref<boolean, boolean>;
8
8
  searchQuery: Ref<string, string>;
9
+ highlightedIndex: Ref<number, number>;
9
10
  toggleDropdown: ($event: KeyboardEvent | MouseEvent) => Promise<void>;
10
11
  openDropdown: () => void;
11
12
  closeDropdown: () => void;
12
13
  onClickOutsideHandler: () => void;
13
14
  keydownActionsOnTrigger: (event: KeyboardEvent) => void;
14
15
  handleKeydown: (event: KeyboardEvent, index: number) => number | undefined;
15
- handleDropdownSearch: (inputValue: KeyboardEvent & InputEvent) => void;
16
+ handleDropdownSearch: (inputValue: Event) => void;
16
17
  debouncedFocus: import("@vueuse/core").PromisifyFn<(idx: number) => void>;
17
18
  };
18
19
  export {};
@@ -3,6 +3,7 @@ import { ref, nextTick } from "#imports";
3
3
  export function useDropdown(options, dropdownOptionsRef, dropdownTriggerRef, config = { searchable: false, trackBy: "name" }) {
4
4
  const active = ref(false);
5
5
  const searchQuery = ref("");
6
+ const highlightedIndex = ref(-1);
6
7
  async function toggleDropdown($event) {
7
8
  active.value = !active.value;
8
9
  if ($event instanceof KeyboardEvent && active.value && $event.key === "ArrowDown") {
@@ -16,6 +17,8 @@ export function useDropdown(options, dropdownOptionsRef, dropdownTriggerRef, con
16
17
  }
17
18
  function closeDropdown() {
18
19
  active.value = false;
20
+ searchQuery.value = "";
21
+ highlightedIndex.value = -1;
19
22
  }
20
23
  function onClickOutsideHandler() {
21
24
  if (!active.value) return;
@@ -23,7 +26,8 @@ export function useDropdown(options, dropdownOptionsRef, dropdownTriggerRef, con
23
26
  }
24
27
  function keydownActionsOnTrigger(event) {
25
28
  if (event.ctrlKey || event.metaKey || event.shiftKey) return;
26
- if (event.key !== "Tab") {
29
+ if (config.searchable && active.value && (event.key.length === 1 || event.key === "Backspace")) {
30
+ } else if (event.key !== "Tab") {
27
31
  event.preventDefault();
28
32
  }
29
33
  switch (event.key) {
@@ -33,17 +37,21 @@ export function useDropdown(options, dropdownOptionsRef, dropdownTriggerRef, con
33
37
  break;
34
38
  case "ArrowDown":
35
39
  if (active.value && event.key === "ArrowDown") {
40
+ event.preventDefault();
36
41
  nextTick(() => {
37
- dropdownOptionsRef.value[0].focus();
42
+ const targetIndex = highlightedIndex.value >= 0 ? highlightedIndex.value : 0;
43
+ dropdownOptionsRef.value[targetIndex]?.focus();
38
44
  openDropdown();
39
45
  });
40
46
  }
41
47
  break;
42
48
  case "ArrowUp":
49
+ event.preventDefault();
43
50
  openDropdown();
44
51
  if (config.searchable) {
45
52
  nextTick(() => {
46
- dropdownOptionsRef.value[0].focus();
53
+ const targetIndex = highlightedIndex.value >= 0 ? highlightedIndex.value : 0;
54
+ dropdownOptionsRef.value[targetIndex]?.focus();
47
55
  });
48
56
  }
49
57
  break;
@@ -51,6 +59,9 @@ export function useDropdown(options, dropdownOptionsRef, dropdownTriggerRef, con
51
59
  case "ArrowRight":
52
60
  break;
53
61
  case " ":
62
+ if (config.searchable && active.value) {
63
+ return;
64
+ }
54
65
  if (active.value) {
55
66
  closeDropdown();
56
67
  nextTick(() => {
@@ -62,12 +73,19 @@ export function useDropdown(options, dropdownOptionsRef, dropdownTriggerRef, con
62
73
  break;
63
74
  case "Enter":
64
75
  if (active.value) {
65
- closeDropdown();
66
- nextTick(() => {
67
- dropdownTriggerRef.value.focus();
68
- });
76
+ if (highlightedIndex.value >= 0 && highlightedIndex.value < options.length) {
77
+ nextTick(() => {
78
+ dropdownOptionsRef.value[highlightedIndex.value]?.focus();
79
+ });
80
+ } else {
81
+ closeDropdown();
82
+ nextTick(() => {
83
+ dropdownTriggerRef.value.focus();
84
+ });
85
+ }
69
86
  } else {
70
87
  searchQuery.value = "";
88
+ highlightedIndex.value = -1;
71
89
  openDropdown();
72
90
  }
73
91
  break;
@@ -76,9 +94,11 @@ export function useDropdown(options, dropdownOptionsRef, dropdownTriggerRef, con
76
94
  dropdownTriggerRef.value.focus();
77
95
  break;
78
96
  case "Home":
97
+ event.preventDefault();
79
98
  openDropdown();
80
99
  nextTick(() => {
81
- dropdownOptionsRef.value[0].focus();
100
+ const targetIndex = highlightedIndex.value >= 0 ? highlightedIndex.value : 0;
101
+ dropdownOptionsRef.value[targetIndex]?.focus();
82
102
  });
83
103
  break;
84
104
  case "End":
@@ -88,7 +108,6 @@ export function useDropdown(options, dropdownOptionsRef, dropdownTriggerRef, con
88
108
  });
89
109
  break;
90
110
  default:
91
- handleDropdownSearch(event);
92
111
  break;
93
112
  }
94
113
  }
@@ -171,32 +190,40 @@ export function useDropdown(options, dropdownOptionsRef, dropdownTriggerRef, con
171
190
  }, 500);
172
191
  function handleDropdownSearch(inputValue) {
173
192
  if (!config.searchable) return;
174
- if (inputValue.key == "Backspace") {
175
- searchQuery.value = searchQuery.value.slice(0, -1);
176
- return;
193
+ if (inputValue.target && "value" in inputValue.target) {
194
+ searchQuery.value = inputValue.target.value;
195
+ updateHighlightedIndex();
177
196
  }
178
- const pressedKey = inputValue.key || inputValue.data;
179
- searchQuery.value = `${searchQuery.value}${pressedKey}`;
197
+ }
198
+ function updateHighlightedIndex() {
180
199
  const searchIndex = options.findIndex((option) => {
200
+ if (!searchQuery.value) return false;
181
201
  if (!config.trackBy) {
182
- return typeof option === "string" && option.includes(searchQuery.value);
202
+ return typeof option === "string" && option.toLowerCase().includes(searchQuery.value.toLowerCase());
183
203
  } else if (typeof option === "object" && option !== null && config.trackBy in option) {
184
204
  const value = option[config.trackBy];
185
205
  if (typeof value === "string") {
186
206
  return value.toLowerCase().includes(searchQuery.value.toLowerCase());
187
207
  } else if (value !== null && value !== void 0) {
188
- return String(value).includes(searchQuery.value);
208
+ return String(value).toLowerCase().includes(searchQuery.value.toLowerCase());
189
209
  }
190
210
  }
191
211
  return false;
192
212
  });
193
- if (searchIndex !== -1) {
194
- debouncedFocus(searchIndex);
213
+ highlightedIndex.value = searchIndex;
214
+ if (searchIndex >= 0 && dropdownOptionsRef.value[searchIndex]) {
215
+ nextTick(() => {
216
+ dropdownOptionsRef.value[searchIndex]?.scrollIntoView({
217
+ block: "nearest",
218
+ behavior: "smooth"
219
+ });
220
+ });
195
221
  }
196
222
  }
197
223
  return {
198
224
  active,
199
225
  searchQuery,
226
+ highlightedIndex,
200
227
  toggleDropdown,
201
228
  openDropdown,
202
229
  closeDropdown,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apptegy/nuxt-navigation",
3
- "version": "0.1.76",
3
+ "version": "0.1.78",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -46,5 +46,5 @@
46
46
  "vue": "^3.5.13",
47
47
  "vue-tsc": "^3.0.3"
48
48
  },
49
- "gitHead": "b115e75078e5a86f7ce2d2b358f68dbcdd43617a"
49
+ "gitHead": "0270d8823bc029e30d7e32ca8b331bba224250fb"
50
50
  }