@apptegy/nuxt-navigation 0.1.2 → 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.2",
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,41 +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
- 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 props = defineProps<IOrgSelectProps>();
98
-
99
- const dropdownTriggerRef = ref();
100
- const optionRef = ref();
101
-
102
105
  const sortedOrgs = computed(() => {
103
106
  return [...props.orgs].sort((a, b) => {
104
107
  if (a.pinned && !b.pinned) {
@@ -110,22 +113,15 @@ const sortedOrgs = computed(() => {
110
113
  }
111
114
  });
112
115
  });
113
- const orgDropdownLabel = computed(() => {
114
- return $t('navigation.orgDropdown') || 'Select Organization'
115
- })
116
-
117
- function optionLabel(option) {
118
- return option?.name || option.label
119
- }
120
116
 
121
117
  function handleSelectedClick(index: number) {
122
- emit('selected', index);
118
+ emit("selected", index);
123
119
  closeDropdown();
124
120
  }
125
121
 
126
- function handleKeydownAction(orgs: string | any[], idx: number, $event: { key: string | boolean; }, optionRef: { focus: () => void; }[], dropdownTriggerRef: { focus: () => void; }) {
127
- emit('selected', idx);
128
- handleKeydown(orgs, idx, $event, optionRef, dropdownTriggerRef);
122
+ function handleKeydownAction($event: { key: string | boolean }, index: number) {
123
+ emit("selected", index);
124
+ handleKeydown($event, index);
129
125
  }
130
126
  </script>
131
127
 
@@ -144,7 +140,7 @@ function handleKeydownAction(orgs: string | any[], idx: number, $event: { key: s
144
140
  margin-right: 25px;
145
141
  }
146
142
  .org-selection .dropdown-trigger:focus-visible {
147
- outline: 2px #000FFF solid;
143
+ outline: 2px #000fff solid;
148
144
  }
149
145
  .org-selection .dropdown-trigger .org-title {
150
146
  line-height: 16px;
@@ -165,8 +161,9 @@ function handleKeydownAction(orgs: string | any[], idx: number, $event: { key: s
165
161
  white-space: nowrap;
166
162
  cursor: pointer;
167
163
  }
168
- .org-selection .intranet {
164
+ .org-selection .badge {
169
165
  background-color: var(--purple-50);
166
+ text-transform: uppercase;
170
167
  }
171
168
  .org-selection .dropdown .org-option {
172
169
  display: block;
@@ -179,7 +176,7 @@ function handleKeydownAction(orgs: string | any[], idx: number, $event: { key: s
179
176
  cursor: pointer;
180
177
  }
181
178
  .org-selection .dropdown .org-option:focus-visible, .org-selection .dropdown .org-option:hover {
182
- background-color: #F8F6FC;
179
+ background-color: #f8f6fc;
183
180
  }
184
181
  .org-selection .custom-search {
185
182
  margin: 8px 16px;
@@ -191,18 +188,19 @@ function handleKeydownAction(orgs: string | any[], idx: number, $event: { key: s
191
188
  width: 100%;
192
189
  justify-content: space-between;
193
190
  }
194
- .org-selection .intranet {
191
+ .org-selection .badge {
195
192
  display: inline-flex;
196
193
  align-items: center;
197
194
  background-color: var(--purple-50);
198
195
  padding: 2px 4px;
199
196
  color: white;
200
197
  border-radius: 4px;
198
+ text-transform: uppercase;
201
199
  }
202
- .org-selection .intranet svg {
200
+ .org-selection .badge svg {
203
201
  margin-right: 4px;
204
202
  }
205
- .org-selection .intranet.selected {
203
+ .org-selection .badge.selected {
206
204
  background-color: var(--grey-50);
207
205
  }
208
206
  </style>
@@ -36,9 +36,8 @@
36
36
  </div>
37
37
  <div class="navbar--footer">
38
38
  <NavImpersonateInfoPanel
39
- v-if="true"
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.2",
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
- }