@apptegy/nuxt-navigation 0.1.3 → 0.1.4

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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@apptegy/nuxt-navigation",
3
3
  "configKey": "@apptegy/nuxt-navigation",
4
- "version": "0.1.3",
4
+ "version": "0.1.4",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "0.8.1",
7
7
  "unbuild": "unknown"
@@ -3,117 +3,35 @@
3
3
  :class="{ sticky }"
4
4
  role="banner"
5
5
  >
6
- <button
7
- class="sidebar-toggle"
8
- @click="toggleSidebar">
9
- <a-icon
10
- class="icon"
11
- icon="16:apptegy"
12
- />
13
- <span>Menu</span>
14
- </button >
6
+ <NavSidebarToggle @toggle-sidebar="$emit('toggle-sidebar')"/>
15
7
  <div class="header__breadcrumbs">
16
8
  <slot name="breadcrumbs" />
17
9
  </div>
18
10
  <slot name="org-select" />
19
11
  <slot name="user-controls" />
20
- <div
21
- ref="settingsTriggerRef"
22
- aria-controls="settings-dropdown"
23
- :aria-expanded="active"
24
- aria-haspopup="listbox"
25
- role="combobox"
26
- tabindex="0"
27
- class="header__dropdown"
28
- @keydown="keydownActionsOnTrigger($event, userSettingsOptions, settingsOptionsRef, settingsTriggerRef, false, '')"
29
- @click.prevent="toggleDropdown($event, settingsOptionsRef)">
30
- <NavAvatar
31
- :name="user.name"
32
- :src="user.avatar"
33
- />
34
- <a-icon
35
- class="icon"
36
- icon="24:hamburger"
37
- />
38
- <span class="sr-only">Account Settings</span>
39
- </div>
40
- <div
41
- v-show="active"
42
- id="settings-dropdown"
43
- class="options-dropdown"
44
- role="listbox"
45
- >
46
- <div
47
- v-for ="(option, idx) in userSettingsOptions"
48
- v-on-click-outside="onClickOutsideHandler"
49
- ref="settingsOptionsRef"
50
- :key="option.label"
51
- role="option"
52
- class="option"
53
- tabindex="-1"
54
- @keydown="handleKeydownAction(userSettingsOptions, idx, $event, settingsOptionsRef, settingsTriggerRef)"
55
- @click="handleSelectedClick(idx)"
56
- >
57
- <NuxtLink :to="option.url">
58
- {{ option.label }}
59
- <a-icon
60
- :icon="option.icon"
61
- color="black"
62
- font-size="32px"
63
- />
64
- </NuxtLink>
65
- </div>
66
- </div>
12
+ <NavUserDropdown :user :user-dropdown-options :selected-option @select-option="$emit('selectedOption')"/>
67
13
  </header>
68
14
  </template>
69
15
 
70
16
  <script setup lang="ts">
71
- import { ref } from 'vue';
72
- import { useDropdown } from '../../composables/accessibleActions.js';
73
- import { vOnClickOutside } from '@vueuse/components';
74
-
75
- const {
76
- active,
77
- toggleDropdown,
78
- closeDropdown,
79
- keydownActionsOnTrigger,
80
- handleKeydown,
81
- onClickOutsideHandler,
82
- } = useDropdown();
83
17
 
84
18
  interface IHeaderProps {
19
+ sticky?: boolean;
20
+ }
21
+
22
+ interface IUserDropdown {
85
23
  user: {
86
24
  name: string;
87
25
  avatar?: string;
88
26
  };
89
- sticky?: boolean;
90
- userSettingsOptions: Array<any>;
91
- selectedSetting?: {}
27
+ userDropdownOptions: Array<unknown>;
28
+ selectedOption?: unknown
92
29
  }
93
30
 
94
- const props = defineProps<IHeaderProps>();
95
31
 
96
- const settingsTriggerRef = ref();
97
- const settingsOptionsRef = ref();
98
-
99
- const emit = defineEmits(['toggle-sidebar', 'select-setting']);
100
-
101
- function handleSelectedClick(index: number) {
102
- emit('select-setting', index);
103
- closeDropdown();
104
- }
105
-
106
- function handleKeydownAction(userSettingsOptions: any[], idx: number, $event: { key: string | boolean; }, optionRef: { focus: () => void; }[], dropdownTriggerRef: { focus: () => void; }) {
107
- if ($event.key === 'Enter' || $event.key === ' ') {
108
- emit('select-setting', idx);
109
- } else {
110
- handleKeydown(userSettingsOptions, idx, $event, optionRef, dropdownTriggerRef);
111
- }
112
- }
32
+ defineProps<IHeaderProps & IUserDropdown>();
33
+ defineEmits(['toggle-sidebar', 'selectedOption']);
113
34
 
114
- function toggleSidebar() {
115
- emit('toggle-sidebar');
116
- }
117
35
  </script>
118
36
 
119
37
  <style scoped>
@@ -138,66 +56,4 @@ header {
138
56
  header.sticky {
139
57
  position: sticky;
140
58
  }
141
- header .sidebar-toggle {
142
- display: flex;
143
- align-items: center;
144
- gap: 4px;
145
- background: var(--purple-20);
146
- border: none;
147
- border-radius: 4px;
148
- padding: 8px 16px;
149
- color: var(--purple);
150
- cursor: pointer;
151
- }
152
- header .sidebar-toggle:focus {
153
- outline: none;
154
- box-shadow: 0 0 0 2px var(--primary-white), 0 0 0 3px var(--purple);
155
- }
156
- header .header__dropdown {
157
- font-size: 24px;
158
- min-width: 64px;
159
- width: 64px;
160
- max-width: 64px;
161
- border: none;
162
- padding: 0;
163
- outline: none;
164
- color: var(--purple);
165
- background: var(--purple-20);
166
- border-radius: 16px;
167
- overflow: hidden;
168
- text-align: left;
169
- position: relative;
170
- display: flex;
171
- align-items: center;
172
- cursor: pointer;
173
- }
174
- header .header__dropdown:focus {
175
- box-shadow: 0 0 0 2px var(--primary-white), 0 0 0 3px var(--purple);
176
- }
177
- header .header__dropdown .avatar-container:focus {
178
- outline: none;
179
- }
180
- header .options-dropdown {
181
- min-height: 100px;
182
- position: absolute;
183
- width: 198px;
184
- right: 65px;
185
- top: 80px;
186
- background-color: #FFFFFF;
187
- border-radius: 4px;
188
- box-shadow: 0px 6px 20px 0px rgba(0, 0, 0, 0.06);
189
- padding: 12px 0;
190
- z-index: 10;
191
- }
192
- header .options-dropdown .option {
193
- padding: 12px 16px;
194
- cursor: pointer;
195
- }
196
- header .options-dropdown .option a {
197
- color: #333;
198
- text-decoration: none;
199
- }
200
- header .options-dropdown .option:focus-visible, header .options-dropdown .option:hover {
201
- background-color: #F8F6FC;
202
- }
203
59
  </style>
@@ -1,15 +1,15 @@
1
1
  <template>
2
2
  <div class="help-btn-wrapper">
3
3
  <div
4
- class="help-btn-actions"
5
4
  ref="helpBtnActions"
6
- :class="{ expanded: expanded && navbarExpanded }"
7
- :style="{ 'max-height': `${expanded && navbarExpanded ? dropdownScrollHeight : 0}px` }"
5
+ class="help-btn-actions"
6
+ :class="{ expanded: expanded }"
7
+ :style="{ 'max-height': `${expanded ? dropdownScrollHeight : 0}px` }"
8
8
  >
9
9
  <button
10
10
  v-for="(option, idx) in helpArticleOptions"
11
- :key="idx"
12
11
  :id="`help-btn-${option.id}`"
12
+ :key="idx"
13
13
  class="help-btn-action"
14
14
  >
15
15
  {{ option.text }}
@@ -20,18 +20,18 @@
20
20
  :class="['help-articles-btn', !isDropdown && 'help-btn']"
21
21
  @click="expandDropdown"
22
22
  >
23
- <img v-if="isDropdown" src="../../assets/img/help-toggle.svg" alt="" class="help-center-btn-icon" />
24
- <img v-else src="../../assets/img/help.svg" alt="" class="help-center-btn-icon" />
25
- <span v-if="navbarExpanded">
23
+ <img v-if="isDropdown" src="../../assets/img/help-toggle.svg" alt="" class="help-center-btn-icon">
24
+ <img v-else src="../../assets/img/help.svg" alt="" class="help-center-btn-icon">
25
+ <span>
26
26
  {{ isDropdown ? $t('navigation.helpCenter') : $t('navigation.helpArticles') }}
27
27
  </span>
28
28
  <img
29
- v-if="isDropdown && navbarExpanded && !hasArticles"
29
+ v-if="isDropdown && !hasArticles"
30
30
  class="caret-icon"
31
31
  :class="{ collapse: !expanded }"
32
32
  src="../../assets/img/navbar-up.svg"
33
33
  alt=""
34
- />
34
+ >
35
35
  </button>
36
36
  </div>
37
37
  </template>
@@ -40,8 +40,7 @@
40
40
  import { ref, computed, onMounted } from 'vue'
41
41
 
42
42
  interface IHelpArticlesProps {
43
- navbarExpanded: boolean;
44
- helpArticleOptions: Array<any>;
43
+ helpArticleOptions: Array<unknown>;
45
44
  };
46
45
 
47
46
  const props = withDefaults(defineProps<IHelpArticlesProps>(), {
@@ -53,9 +52,7 @@ const helpBtnActions = ref();
53
52
  const dropdownScrollHeight = ref(0);
54
53
  const isDropdown = ref(false);
55
54
 
56
- const hasArticles = computed(() => {
57
- return !!props.helpArticleOptions?.values?.length
58
- })
55
+ const hasArticles = computed(() => !!props.helpArticleOptions?.values?.length)
59
56
 
60
57
  onMounted(() => {
61
58
  isDropdown.value = !!props.helpArticleOptions?.values?.length;
@@ -63,9 +60,7 @@ onMounted(() => {
63
60
  });
64
61
 
65
62
  function expandDropdown() {
66
- if (!isDropdown) {
67
- return;
68
- }
63
+ if (!isDropdown.value) return;
69
64
  expanded.value = !expanded.value;
70
65
  };
71
66
 
@@ -78,6 +73,7 @@ function expandDropdown() {
78
73
  flex-direction: column;
79
74
  align-items: center;
80
75
  width: 100%;
76
+ text-transform: uppercase;
81
77
  }
82
78
  .help-btn-wrapper .help-articles-btn {
83
79
  cursor: pointer;
@@ -87,6 +83,7 @@ function expandDropdown() {
87
83
  background: none;
88
84
  padding: 8px 10px 8px 16px;
89
85
  border: none;
86
+ text-transform: inherit;
90
87
  }
91
88
  .help-btn-wrapper .help-articles-btn:focus-visible {
92
89
  outline: var(--orange) solid 2px;
@@ -1,21 +1,21 @@
1
1
  <template>
2
- <div class="a-nav-section animated" id="impersonate-info">
3
- <button v-if="navbarExpanded" class="a-nav-section--title" @click="expanded = !expanded">
2
+ <div id="impersonate-info" class="a-nav-section animated">
3
+ <button class="a-nav-section--title" @click="expanded = !expanded">
4
4
  <div class="a-nav-section-title-wrapper">
5
5
  <div class="user-grid">
6
- <img src="../../assets/img/impersonate-user.svg" alt="" class="user-icon" />
6
+ <img src="../../assets/img/impersonate-user.svg" alt="" class="user-icon" >
7
7
  <p class="info">{{ impersonatedUser.client }}</p>
8
8
  <p class="info email">{{ impersonatedUser.email }}</p>
9
- <img v-if="!expanded " class="panel-icon" src="../../assets/icons/24/Plus.svg" alt="" />
10
- <img v-else class="panel-icon" src="../../assets/icons/24/Minus.svg" alt="" />
9
+ <img v-if="!expanded " class="panel-icon" src="../../assets/icons/24/Plus.svg" alt="" >
10
+ <img v-else class="panel-icon" src="../../assets/icons/24/Minus.svg" alt="" >
11
11
  </div>
12
12
  <div
13
13
  ref="dropdownWrapper"
14
- :style="{ 'max-height': `${expanded && navbarExpanded ? dropdownScrollHeight : 0}px` }"
14
+ :style="{ 'max-height': `${expanded ? dropdownScrollHeight : 0}px` }"
15
15
  class="a-nav-section-title-dropdown-wrapper"
16
16
  >
17
17
  <div class="user-grid">
18
- <img src="../../assets/img/user.svg" alt="" class="user-icon" />
18
+ <img src="../../assets/img/user.svg" alt="" class="user-icon" >
19
19
  <p class="info">{{ impersonatedUser.client }}</p>
20
20
  <p class="info email">{{ impersonatedUser.admin_email }}</p>
21
21
  </div>
@@ -30,14 +30,15 @@ import { ref, onMounted } from '#imports';
30
30
 
31
31
  interface IPanelProps {
32
32
  navbarExpanded: boolean;
33
- impersonatedUser: () => {
34
- show: false,
35
- name: '',
36
- email: '',
37
- client: ''
33
+ impersonatedUser: {
34
+ name: string,
35
+ email: string,
36
+ admin_email: string,
37
+ client: string,
38
38
  }
39
39
  }
40
- const props = defineProps<IPanelProps>();
40
+
41
+ defineProps<IPanelProps>();
41
42
 
42
43
  const expanded = ref(false);
43
44
  const dropdownScrollHeight = ref(0);
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="org-selection">
3
3
  <label id="org-dropdown-label" class="sr-only">
4
- {{ orgDropdownLabel }}
4
+ {{ $t("navigation.orgDropdown") }}
5
5
  </label>
6
6
  <div
7
7
  ref="dropdownTriggerRef"
@@ -14,48 +14,43 @@
14
14
  >
15
15
  <input
16
16
  ref="inputRef"
17
- is="input"
18
17
  class="org-title"
19
18
  type="text"
20
- placeholder="Start typing to search"
21
- :value="active? newQuery : selectedOrg.name"
22
- @keydown="keydownActionsOnTrigger($event, orgs, optionRef, dropdownTriggerRef, true, inputRef)"
23
- @click.prevent="toggleDropdown($event, optionRef)"
24
- @input="handleDropdownSearch($event, sortedOrgs, optionRef)"
25
- />
26
- <span v-if="selectedOrg.intranet" class="intranet selected">
27
- <a-icon icon="16:lock-locked" ></a-icon>
28
- {{ $t('navigation.intranet') }}
19
+ :placeholder="$t('navigation.orgPlaceholder')"
20
+ :value="active ? searchQuery : selectedOrg.name"
21
+ @keydown="keydownActionsOnTrigger"
22
+ @click.prevent="toggleDropdown"
23
+ @input="handleDropdownSearch"
24
+ >
25
+ <span v-if="selectedOrg.intranet" class="badge selected">
26
+ <a-icon icon="16:lock-locked" />
27
+ {{ $t("navigation.orgBadge") }}
29
28
  </span>
30
- <!-- <Icon
31
- style="margin-bottom: 2px"
32
- :name-or-path="`dropdown${active ? '-up' : ''}`"
33
- size="tiny"
34
- reduced
35
- variant="info"
36
- /> -->
37
29
  </div>
38
- <div v-show="active" id="organizations-options-list" class="dropdown" role="listbox">
30
+ <div
31
+ v-show="active"
32
+ id="organizations-options-list"
33
+ class="dropdown"
34
+ role="listbox"
35
+ >
39
36
  <div
40
37
  v-for="(option, idx) in sortedOrgs"
41
- v-on-click-outside="onClickOutsideHandler"
38
+ :key="idx"
42
39
  ref="optionRef"
43
- :key="option.name"
40
+ v-on-click-outside="onClickOutsideHandler"
44
41
  role="option"
45
42
  class="org-option"
46
43
  :class="{ selected: option === selectedOrg }"
47
44
  :aria-selected="!!String(option === selectedOrg)"
48
45
  tabindex="-1"
49
- @keydown="handleKeydownAction(orgs, idx, $event, optionRef, dropdownTriggerRef)"
46
+ @keydown="handleKeydownAction($event, idx)"
50
47
  @click="handleSelectedClick(idx)"
51
48
  >
52
- <div class="selected-space">
53
- <!-- <Icon v-if="option === selectedOrg" name-or-path="success" size="tiny" /> -->
54
- </div>
55
- <span :class="{ pinned: option.pinned, 'item-flex': true }">{{ optionLabel(option) }}
56
- <span v-if="option.intranet" class="intranet selected">
57
- <a-icon icon="16:lock-locked"></a-icon>
58
- {{ $t('navigation.intranet') }}
49
+ <span :class="{ pinned: option.pinned, 'item-flex': true }"
50
+ >{{ option.name }}
51
+ <span v-if="option.intranet" class="badge selected">
52
+ <a-icon icon="16:lock-locked" />
53
+ {{ $t("navigation.orgBadge") }}
59
54
  </span>
60
55
  </span>
61
56
  </div>
@@ -64,39 +59,49 @@
64
59
  </template>
65
60
 
66
61
  <script setup lang="ts">
67
- import { useDropdown } from '../../composables/accessibleActions.js';
68
- import { vOnClickOutside } from '@vueuse/components';
69
- import { ref, computed, useNuxtApp } from '#imports';
62
+ import { useDropdown, useNuxtApp } from "#imports";
63
+ import { vOnClickOutside } from "@vueuse/components";
64
+ import { ref, computed } from "vue";
65
+
70
66
  const { $t } = useNuxtApp();
71
67
 
72
- const emit = defineEmits(['selected']);
68
+ interface IOrg {
69
+ name: string;
70
+ intranet: boolean;
71
+ pinned: boolean;
72
+ }
73
+
74
+ interface IOrgSelectProps {
75
+ orgs: Array<IOrg>;
76
+ selectedOrg?: IOrg;
77
+ }
78
+
79
+ const emit = defineEmits(["selected"]);
80
+
81
+ const dropdownTriggerRef = ref();
82
+ const optionRef = ref();
83
+ const props = withDefaults(defineProps<IOrgSelectProps>(), {
84
+ selectedOrg: () => ({
85
+ name: "",
86
+ intranet: false,
87
+ pinned: false,
88
+ }),
89
+ });
73
90
 
74
91
  const {
75
92
  active,
76
- newQuery,
93
+ searchQuery,
77
94
  toggleDropdown,
78
95
  closeDropdown,
79
96
  onClickOutsideHandler,
80
97
  keydownActionsOnTrigger,
81
98
  handleKeydown,
82
99
  handleDropdownSearch,
83
- } = useDropdown();
84
-
85
- interface IOrgSelectProps {
86
- orgs: Array<any>;
87
- selectedOrg: {};
88
- };
89
-
90
- const props = withDefaults(defineProps<IOrgSelectProps>(), {
91
- selectedOrg: {
92
- name: '',
93
- intranet: false
94
- }
100
+ } = useDropdown(props.orgs, optionRef, dropdownTriggerRef, {
101
+ searchable: true,
102
+ trackBy: "name",
95
103
  });
96
104
 
97
- const dropdownTriggerRef = ref();
98
- const optionRef = ref();
99
-
100
105
  const sortedOrgs = computed(() => {
101
106
  return [...props.orgs].sort((a, b) => {
102
107
  if (a.pinned && !b.pinned) {
@@ -108,22 +113,15 @@ const sortedOrgs = computed(() => {
108
113
  }
109
114
  });
110
115
  });
111
- const orgDropdownLabel = computed(() => {
112
- return $t('navigation.orgDropdown') || 'Select Organization'
113
- })
114
-
115
- function optionLabel(option) {
116
- return option?.name || option.label
117
- }
118
116
 
119
117
  function handleSelectedClick(index: number) {
120
- emit('selected', index);
118
+ emit("selected", index);
121
119
  closeDropdown();
122
120
  }
123
121
 
124
- function handleKeydownAction(orgs: string | any[], idx: number, $event: { key: string | boolean; }, optionRef: { focus: () => void; }[], dropdownTriggerRef: { focus: () => void; }) {
125
- emit('selected', idx);
126
- handleKeydown(orgs, idx, $event, optionRef, dropdownTriggerRef);
122
+ function handleKeydownAction($event: { key: string | boolean }, index: number) {
123
+ emit("selected", index);
124
+ handleKeydown($event, index);
127
125
  }
128
126
  </script>
129
127
 
@@ -142,7 +140,7 @@ function handleKeydownAction(orgs: string | any[], idx: number, $event: { key: s
142
140
  margin-right: 25px;
143
141
  }
144
142
  .org-selection .dropdown-trigger:focus-visible {
145
- outline: 2px #000FFF solid;
143
+ outline: 2px #000fff solid;
146
144
  }
147
145
  .org-selection .dropdown-trigger .org-title {
148
146
  line-height: 16px;
@@ -163,8 +161,9 @@ function handleKeydownAction(orgs: string | any[], idx: number, $event: { key: s
163
161
  white-space: nowrap;
164
162
  cursor: pointer;
165
163
  }
166
- .org-selection .intranet {
164
+ .org-selection .badge {
167
165
  background-color: var(--purple-50);
166
+ text-transform: uppercase;
168
167
  }
169
168
  .org-selection .dropdown .org-option {
170
169
  display: block;
@@ -177,7 +176,7 @@ function handleKeydownAction(orgs: string | any[], idx: number, $event: { key: s
177
176
  cursor: pointer;
178
177
  }
179
178
  .org-selection .dropdown .org-option:focus-visible, .org-selection .dropdown .org-option:hover {
180
- background-color: #F8F6FC;
179
+ background-color: #f8f6fc;
181
180
  }
182
181
  .org-selection .custom-search {
183
182
  margin: 8px 16px;
@@ -189,18 +188,19 @@ function handleKeydownAction(orgs: string | any[], idx: number, $event: { key: s
189
188
  width: 100%;
190
189
  justify-content: space-between;
191
190
  }
192
- .org-selection .intranet {
191
+ .org-selection .badge {
193
192
  display: inline-flex;
194
193
  align-items: center;
195
194
  background-color: var(--purple-50);
196
195
  padding: 2px 4px;
197
196
  color: white;
198
197
  border-radius: 4px;
198
+ text-transform: uppercase;
199
199
  }
200
- .org-selection .intranet svg {
200
+ .org-selection .badge svg {
201
201
  margin-right: 4px;
202
202
  }
203
- .org-selection .intranet.selected {
203
+ .org-selection .badge.selected {
204
204
  background-color: var(--grey-50);
205
205
  }
206
206
  </style>
@@ -38,7 +38,6 @@
38
38
  <NavImpersonateInfoPanel
39
39
  v-if="impersonatedUser.show"
40
40
  :impersonated-user="impersonatedUser"
41
- :navbar-expanded="expanded"
42
41
  />
43
42
  </div>
44
43
 
@@ -84,7 +83,7 @@ interface ISidebarProps {
84
83
  expanded?: boolean;
85
84
  homepageUrl?: string;
86
85
  showHelpArticles?: boolean;
87
- helpArticleOptions?: Array<any>;
86
+ helpArticleOptions?: Array<unknown>;
88
87
  impersonatedUser?: IUser;
89
88
  logo: ILogo;
90
89
  }
@@ -96,18 +95,18 @@ withDefaults(defineProps<ISidebarProps>(), {
96
95
  showHelpArticles: true,
97
96
  expanded: true,
98
97
  helpArticleOptions: () => [],
99
- impersonatedUser: {
98
+ impersonatedUser: () => ({
100
99
  show: false,
101
100
  email: "staff3@lms.com",
102
- phone_number :null,
101
+ phone_number : '',
103
102
  client: "Sales Demo",
104
- admin_email: null
105
- },
106
- logo: {
103
+ admin_email: '',
104
+ }),
105
+ logo: () => ({
107
106
  alt: 'Thrillshare',
108
107
  src: 'logo.svg',
109
108
  iconSrc: 'logo.svg',
110
- }
109
+ })
111
110
  })
112
111
  </script>
113
112
 
@@ -0,0 +1,32 @@
1
+ <template>
2
+ <button class="sidebar-toggle" @click="toggleSidebar">
3
+ <a-icon class="icon" icon="16:apptegy" />
4
+ <span>Menu</span>
5
+ </button>
6
+ </template>
7
+
8
+ <script setup>
9
+ const emit = defineEmits(["toggle-sidebar"]);
10
+
11
+ function toggleSidebar() {
12
+ emit("toggle-sidebar");
13
+ }
14
+ </script>
15
+
16
+ <style scoped>
17
+ .sidebar-toggle {
18
+ display: flex;
19
+ align-items: center;
20
+ gap: 4px;
21
+ background: var(--purple-20);
22
+ border: none;
23
+ border-radius: 4px;
24
+ padding: 8px 16px;
25
+ color: var(--purple);
26
+ cursor: pointer;
27
+ }
28
+ .sidebar-toggle:focus {
29
+ outline: none;
30
+ box-shadow: 0 0 0 2px var(--primary-white), 0 0 0 3px var(--purple);
31
+ }
32
+ </style>
@@ -0,0 +1,143 @@
1
+ <script setup lang="ts">
2
+ import { ref } from "vue";
3
+ import { useDropdown } from "#imports";
4
+ import { vOnClickOutside } from "@vueuse/components";
5
+
6
+ interface IUserDropdown {
7
+ user: {
8
+ name: string;
9
+ avatar: string;
10
+ };
11
+ userDropdownOptions: Array<{ label: string; url: string; icon: string }>;
12
+ selectedOption: number;
13
+ }
14
+
15
+ const props = defineProps<IUserDropdown>();
16
+ const dropdownTriggerRef = ref<HTMLDivElement>();
17
+ const dropdownOptionsRef = ref<Array<HTMLDivElement>>();
18
+
19
+ const {
20
+ active,
21
+ toggleDropdown,
22
+ closeDropdown,
23
+ keydownActionsOnTrigger,
24
+ handleKeydown,
25
+ onClickOutsideHandler,
26
+ } = useDropdown(
27
+ props.userDropdownOptions,
28
+ dropdownOptionsRef,
29
+ dropdownTriggerRef,
30
+ );
31
+
32
+ const emit = defineEmits<{
33
+ 'select-option': [id: number]
34
+ }>();
35
+
36
+ function handleSelectedClick(index: number) {
37
+ emit("select-option", index);
38
+ closeDropdown();
39
+ }
40
+
41
+ function handleKeydownAction($event: KeyboardEvent, index: number) {
42
+ if ($event.key === "Enter" || $event.key === " ") {
43
+ emit("select-option", index);
44
+ } else {
45
+ handleKeydown($event, index);
46
+ }
47
+ }
48
+ </script>
49
+
50
+ <template>
51
+ <div
52
+ ref="dropdownTriggerRef"
53
+ aria-controls="user-dropdown"
54
+ :aria-expanded="active"
55
+ aria-haspopup="listbox"
56
+ role="combobox"
57
+ tabindex="0"
58
+ class="header__dropdown"
59
+ @keydown="keydownActionsOnTrigger"
60
+ @click.prevent="toggleDropdown"
61
+ >
62
+ <NavUserDropdownAvatar :name="user.name" :src="user.avatar" />
63
+ <a-icon class="icon" icon="24:hamburger" />
64
+ <span class="sr-only">Account Settings</span>
65
+ </div>
66
+ <div
67
+ v-show="active"
68
+ id="user-dropdown"
69
+ class="options-dropdown"
70
+ role="listbox"
71
+ >
72
+ <div
73
+ v-for="(option, idx) in userDropdownOptions"
74
+ :key="option.label"
75
+ ref="dropdownOptionsRef"
76
+ v-on-click-outside="onClickOutsideHandler"
77
+ role="option"
78
+ class="option-wrapper"
79
+ tabindex="-1"
80
+ @keydown="handleKeydownAction($event, idx)"
81
+ @click="handleSelectedClick(idx)"
82
+ >
83
+ <NuxtLink class="option" :to="option.url">
84
+ {{ option.label }}
85
+ <a-icon :icon="option.icon" color="black" font-size="32px" />
86
+ </NuxtLink>
87
+ </div>
88
+ </div>
89
+ </template>
90
+
91
+ <style scoped>
92
+ .header__dropdown {
93
+ font-size: 24px;
94
+ min-width: 64px;
95
+ width: 64px;
96
+ max-width: 64px;
97
+ border: none;
98
+ padding: 0;
99
+ outline: none;
100
+ color: var(--purple);
101
+ background: var(--purple-20);
102
+ border-radius: 16px;
103
+ overflow: hidden;
104
+ text-align: left;
105
+ position: relative;
106
+ display: flex;
107
+ align-items: center;
108
+ cursor: pointer;
109
+ }
110
+ .header__dropdown:focus {
111
+ box-shadow: 0 0 0 2px var(--primary-white), 0 0 0 3px var(--purple);
112
+ }
113
+ .header__dropdown .avatar-container:focus {
114
+ outline: none;
115
+ }
116
+
117
+ .options-dropdown {
118
+ min-height: 100px;
119
+ position: absolute;
120
+ max-width: 512px;
121
+ right: 65px;
122
+ top: 80px;
123
+ background-color: #ffffff;
124
+ border-radius: 4px;
125
+ box-shadow: 0px 6px 20px 0px rgba(0, 0, 0, 0.06);
126
+ padding: 12px 0;
127
+ z-index: 10;
128
+ }
129
+ .options-dropdown .option-wrapper {
130
+ padding: 12px 16px;
131
+ cursor: pointer;
132
+ }
133
+ .options-dropdown .option-wrapper .option {
134
+ display: flex;
135
+ justify-content: space-between;
136
+ align-items: center;
137
+ color: #333;
138
+ text-decoration: none;
139
+ }
140
+ .options-dropdown .option-wrapper:focus-visible, .options-dropdown .option-wrapper:hover {
141
+ background-color: #f8f6fc;
142
+ }
143
+ </style>
@@ -0,0 +1,16 @@
1
+ import type { Ref } from "vue";
2
+ export declare function useDropdown(options: Array<unknown>, dropdownOptionsRef: Ref<Array<HTMLElement>>, dropdownTriggerRef: Ref<HTMLElement>, config?: {
3
+ searchable: boolean;
4
+ trackBy?: string;
5
+ }): {
6
+ active: Ref<boolean>;
7
+ searchQuery: Ref<string>;
8
+ toggleDropdown: ($event: KeyboardEvent & MouseEvent) => Promise<void>;
9
+ openDropdown: () => void;
10
+ closeDropdown: () => void;
11
+ onClickOutsideHandler: () => void;
12
+ keydownActionsOnTrigger: (event: KeyboardEvent) => void;
13
+ handleKeydown: (event: KeyboardEvent, index: number) => number | undefined;
14
+ handleDropdownSearch: (inputValue: KeyboardEvent & InputEvent) => void;
15
+ debouncedFocus: any;
16
+ };
@@ -0,0 +1,204 @@
1
+ import debounce from "lodash/debounce";
2
+ import { ref, nextTick } from "vue";
3
+ export function useDropdown(options, dropdownOptionsRef, dropdownTriggerRef, config = { searchable: false, trackBy: "name" }) {
4
+ const active = ref(false);
5
+ const searchQuery = ref("");
6
+ async function toggleDropdown($event) {
7
+ active.value = !active.value;
8
+ if (active.value && $event.key === "ArrowDown") {
9
+ await nextTick(() => {
10
+ dropdownOptionsRef.value[0].focus();
11
+ });
12
+ }
13
+ }
14
+ function openDropdown() {
15
+ active.value = true;
16
+ }
17
+ function closeDropdown() {
18
+ active.value = false;
19
+ }
20
+ function onClickOutsideHandler() {
21
+ if (!active.value)
22
+ return;
23
+ closeDropdown();
24
+ }
25
+ function keydownActionsOnTrigger(event) {
26
+ if (event.ctrlKey || event.metaKey || event.shiftKey)
27
+ return;
28
+ if (event.key !== "Tab") {
29
+ event.preventDefault();
30
+ }
31
+ switch (event.key) {
32
+ case "Tab":
33
+ if (!active.value)
34
+ return;
35
+ closeDropdown();
36
+ break;
37
+ case "ArrowDown":
38
+ if (active.value && event.key === "ArrowDown") {
39
+ nextTick(() => {
40
+ dropdownOptionsRef.value[0].focus();
41
+ openDropdown();
42
+ });
43
+ }
44
+ break;
45
+ case "ArrowUp":
46
+ openDropdown();
47
+ if (config.searchable) {
48
+ nextTick(() => {
49
+ dropdownOptionsRef.value[0].focus();
50
+ });
51
+ }
52
+ break;
53
+ case " ":
54
+ if (active.value) {
55
+ closeDropdown();
56
+ nextTick(() => {
57
+ dropdownTriggerRef.value.focus();
58
+ });
59
+ } else {
60
+ openDropdown();
61
+ }
62
+ break;
63
+ case "Enter":
64
+ if (active.value) {
65
+ closeDropdown();
66
+ nextTick(() => {
67
+ dropdownTriggerRef.value.focus();
68
+ });
69
+ } else {
70
+ searchQuery.value = "";
71
+ openDropdown();
72
+ }
73
+ break;
74
+ case "Escape":
75
+ closeDropdown();
76
+ dropdownTriggerRef.value.focus();
77
+ break;
78
+ case "Home":
79
+ openDropdown();
80
+ nextTick(() => {
81
+ dropdownOptionsRef.value[0].focus();
82
+ });
83
+ break;
84
+ case "End":
85
+ openDropdown();
86
+ nextTick(() => {
87
+ dropdownOptionsRef.value[options.length - 1].focus();
88
+ });
89
+ break;
90
+ default:
91
+ handleDropdownSearch(event);
92
+ break;
93
+ }
94
+ }
95
+ function handleKeydown(event, index) {
96
+ if (event.ctrlKey || event.metaKey || event.shiftKey)
97
+ return;
98
+ if (event.altKey && event.key === "ArrowUp") {
99
+ closeDropdown();
100
+ dropdownTriggerRef.value.focus();
101
+ return index;
102
+ }
103
+ switch (event.key) {
104
+ case "Enter":
105
+ nextTick(() => {
106
+ closeDropdown();
107
+ dropdownTriggerRef.value.focus();
108
+ return index;
109
+ });
110
+ break;
111
+ case " ":
112
+ nextTick(() => {
113
+ closeDropdown();
114
+ dropdownTriggerRef.value.focus();
115
+ return index;
116
+ });
117
+ break;
118
+ case "Tab":
119
+ nextTick(() => {
120
+ closeDropdown();
121
+ return index;
122
+ });
123
+ break;
124
+ case "Escape":
125
+ nextTick(() => {
126
+ closeDropdown();
127
+ dropdownTriggerRef.value.focus();
128
+ });
129
+ break;
130
+ case "ArrowDown":
131
+ if (index === options.length - 1)
132
+ break;
133
+ nextTick(() => {
134
+ dropdownOptionsRef.value[index + 1].focus();
135
+ });
136
+ return index;
137
+ case "ArrowUp":
138
+ if (index === 0)
139
+ break;
140
+ nextTick(() => {
141
+ dropdownOptionsRef.value[index - 1].focus();
142
+ });
143
+ return index;
144
+ case "Home":
145
+ nextTick(() => {
146
+ dropdownOptionsRef.value[0].focus();
147
+ });
148
+ return index;
149
+ case "End":
150
+ nextTick(() => {
151
+ dropdownOptionsRef.value[options.length - 1].focus();
152
+ });
153
+ return index;
154
+ case "PageDown": {
155
+ const pageDownIndexToJump = index + 10 > options.length ? index + 10 : options.length - 1;
156
+ dropdownOptionsRef.value[pageDownIndexToJump].focus();
157
+ return index;
158
+ }
159
+ case "PageUp": {
160
+ const pageUpIndexToJump = index - 10 < 0 ? index - 10 : 0;
161
+ dropdownOptionsRef.value[pageUpIndexToJump].focus();
162
+ return index;
163
+ }
164
+ default:
165
+ handleDropdownSearch(event);
166
+ break;
167
+ }
168
+ }
169
+ const debouncedFocus = debounce((idx) => {
170
+ dropdownOptionsRef.value[idx].focus();
171
+ }, 500);
172
+ function handleDropdownSearch(inputValue) {
173
+ if (!config.searchable)
174
+ return;
175
+ if (inputValue.key == "Backspace") {
176
+ searchQuery.value = searchQuery.value.slice(0, -1);
177
+ return;
178
+ }
179
+ const pressedKey = inputValue.key || inputValue.data;
180
+ searchQuery.value = `${searchQuery.value}${pressedKey}`;
181
+ const searchIndex = options.findIndex((option) => {
182
+ if (!config.trackBy) {
183
+ return option.includes(searchQuery.value);
184
+ } else {
185
+ return option[config.trackBy].toLowerCase().includes(searchQuery.value.toLowerCase());
186
+ }
187
+ });
188
+ if (searchIndex !== -1) {
189
+ debouncedFocus(searchIndex);
190
+ }
191
+ }
192
+ return {
193
+ active,
194
+ searchQuery,
195
+ toggleDropdown,
196
+ openDropdown,
197
+ closeDropdown,
198
+ onClickOutsideHandler,
199
+ keydownActionsOnTrigger,
200
+ handleKeydown,
201
+ handleDropdownSearch,
202
+ debouncedFocus
203
+ };
204
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apptegy/nuxt-navigation",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -1,33 +0,0 @@
1
- export declare function useDropdown(): {
2
- active: import("vue").Ref<boolean>;
3
- newQuery: import("vue").Ref<string>;
4
- toggleDropdown: ($event: {
5
- key: string;
6
- }, optionRef: {
7
- focus: () => void;
8
- }[]) => Promise<void>;
9
- openDropdown: () => void;
10
- closeDropdown: () => void;
11
- onClickOutsideHandler: () => void;
12
- keydownActionsOnTrigger: (event: {
13
- key: string;
14
- preventDefault: () => void;
15
- }, options: any[], optionRef: {
16
- focus: () => void;
17
- }[], dropdownTriggerRef: {
18
- focus: () => void;
19
- }, searchable: false, inputRef: {
20
- focus: () => void;
21
- }) => void;
22
- handleKeydown: (options: any[], optionIndex: number, event: {
23
- key: string;
24
- }, optionRef: {
25
- focus: () => void;
26
- }[], triggerRef: {
27
- focus: () => void;
28
- }) => number | undefined;
29
- handleDropdownSearch: (val: string, orgs: any[], optionRef: {
30
- focus: () => void;
31
- }[]) => void;
32
- debouncedFocus: any;
33
- };
@@ -1,195 +0,0 @@
1
- import debounce from "lodash/debounce";
2
- import { ref, nextTick } from "vue";
3
- export function useDropdown() {
4
- const active = ref(false);
5
- let newQuery = ref("");
6
- async function toggleDropdown($event, optionRef) {
7
- active.value = !active.value;
8
- if (active.value && $event.key === "ArrowDown") {
9
- await nextTick(() => {
10
- optionRef[0].focus();
11
- });
12
- }
13
- }
14
- function openDropdown() {
15
- active.value = true;
16
- }
17
- function closeDropdown() {
18
- active.value = false;
19
- }
20
- function onClickOutsideHandler() {
21
- if (!active.value)
22
- return;
23
- closeDropdown();
24
- }
25
- function keydownActionsOnTrigger(event, options, optionRef, dropdownTriggerRef, searchable, inputRef) {
26
- if (event.key !== "Tab") {
27
- event.preventDefault();
28
- }
29
- switch (event.key) {
30
- case "Tab":
31
- if (!active.value)
32
- return;
33
- closeDropdown();
34
- break;
35
- case "ArrowDown":
36
- nextTick(() => {
37
- if (active.value && event.key === "ArrowDown") {
38
- optionRef[0].focus();
39
- }
40
- openDropdown();
41
- });
42
- break;
43
- case "ArrowUp":
44
- openDropdown();
45
- nextTick(() => {
46
- if (searchable) {
47
- return;
48
- }
49
- optionRef[0].focus();
50
- });
51
- break;
52
- case " ":
53
- if (active.value) {
54
- closeDropdown();
55
- nextTick(() => {
56
- dropdownTriggerRef.focus();
57
- });
58
- } else {
59
- openDropdown();
60
- }
61
- break;
62
- case "Enter":
63
- if (active.value) {
64
- closeDropdown();
65
- nextTick(() => {
66
- dropdownTriggerRef.focus();
67
- });
68
- } else {
69
- nextTick(() => {
70
- openDropdown();
71
- });
72
- }
73
- break;
74
- case "Escape":
75
- closeDropdown();
76
- dropdownTriggerRef.focus();
77
- break;
78
- case "Home":
79
- openDropdown();
80
- nextTick(() => {
81
- optionRef[0].focus();
82
- });
83
- break;
84
- case "End":
85
- openDropdown();
86
- nextTick(() => {
87
- optionRef[options.length - 1].focus();
88
- });
89
- break;
90
- default:
91
- debounce(handleDropdownSearch(event.key, options, optionRef), 500);
92
- break;
93
- }
94
- }
95
- function handleKeydown(options, optionIndex, event, optionRef, triggerRef) {
96
- if (event.altKey && event.key === "ArrowUp") {
97
- closeDropdown();
98
- triggerRef.focus();
99
- return optionIndex;
100
- }
101
- switch (event.key) {
102
- case "Enter":
103
- nextTick(() => {
104
- closeDropdown();
105
- triggerRef.focus();
106
- return optionIndex;
107
- });
108
- break;
109
- case " ":
110
- nextTick(() => {
111
- closeDropdown();
112
- triggerRef.focus();
113
- return optionIndex;
114
- });
115
- break;
116
- case "Tab":
117
- nextTick(() => {
118
- closeDropdown();
119
- return optionIndex;
120
- });
121
- break;
122
- case "Escape":
123
- nextTick(() => {
124
- closeDropdown();
125
- triggerRef.focus();
126
- });
127
- break;
128
- case "ArrowDown":
129
- if (optionIndex === options.length - 1)
130
- break;
131
- nextTick(() => {
132
- optionRef[optionIndex + 1].focus();
133
- });
134
- return optionIndex;
135
- case "ArrowUp":
136
- if (optionIndex === 0)
137
- break;
138
- nextTick(() => {
139
- optionRef[optionIndex - 1].focus();
140
- });
141
- return optionIndex;
142
- case "Home":
143
- nextTick(() => {
144
- optionRef[0].focus();
145
- });
146
- return optionIndex;
147
- case "End":
148
- nextTick(() => {
149
- optionRef[options.length - 1].focus();
150
- });
151
- return optionIndex;
152
- case "PageDown": {
153
- const pageDownIndexToJump = optionIndex + 10 > options.length ? optionIndex + 10 : options.length - 1;
154
- optionRef[pageDownIndexToJump].focus();
155
- return optionIndex;
156
- }
157
- case "PageUp": {
158
- const pageUpIndexToJump = optionIndex - 10 < 0 ? optionIndex - 10 : 0;
159
- optionRef[pageUpIndexToJump].focus();
160
- return optionIndex;
161
- }
162
- default:
163
- handleDropdownSearch(event.key, options, optionRef);
164
- break;
165
- }
166
- }
167
- const debouncedFocus = debounce((idx, optionRef) => {
168
- optionRef[idx].focus();
169
- }, 500);
170
- function handleDropdownSearch(val, orgs, optionRef) {
171
- if (val == "Backspace") {
172
- newQuery.value = newQuery.value.slice(0, -1);
173
- return;
174
- }
175
- newQuery.value = `${newQuery.value}${val}`;
176
- const searchIndex = orgs.findIndex(
177
- (option) => option.name.toLowerCase().includes(newQuery.value.toLowerCase())
178
- );
179
- if (searchIndex !== -1) {
180
- debouncedFocus(searchIndex, optionRef);
181
- }
182
- }
183
- return {
184
- active,
185
- newQuery,
186
- toggleDropdown,
187
- openDropdown,
188
- closeDropdown,
189
- onClickOutsideHandler,
190
- keydownActionsOnTrigger,
191
- handleKeydown,
192
- handleDropdownSearch,
193
- debouncedFocus
194
- };
195
- }