Skip to content

Commit 64a6acf

Browse files
committed
feat: groups menu in Reka UI
1 parent 3252e0f commit 64a6acf

6 files changed

Lines changed: 2542 additions & 2190 deletions

File tree

client/Embedded.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import NTheme from './components/n-theme.vue'
1616
const availableComponents = {
1717
ChatLog: defineAsyncComponent(() => import('./components/ChatLog.vue')),
1818
Polls: defineAsyncComponent(() => import('./components/Polls.vue')),
19-
Status: defineAsyncComponent(() => import('./components/Status.vue'))
19+
Status: defineAsyncComponent(() => import('./components/Status.vue')),
20+
GroupMenu: defineAsyncComponent(() => import('./components/GroupMenu.vue'))
2021
}
2122
2223
// PROPS

client/components/GroupMenu.vue

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
<template>
2+
<NavigationMenuRoot
3+
v-if="menuData"
4+
:disable-hover-trigger="true"
5+
class="NavigationMenuRoot"
6+
>
7+
<NavigationMenuList class="NavigationMenuList">
8+
<NavigationMenuItem value="Groups">
9+
<NavigationMenuTrigger class="dropdown-toggle NavigationMenuTrigger">
10+
Groups
11+
</NavigationMenuTrigger>
12+
<NavigationMenuContent class="NavigationMenuContent">
13+
<NavigationMenuSub default-value="0">
14+
<NavigationMenuList class="NavigationMenu-NoList">
15+
<NavigationMenuItem
16+
v-for="(menuItem, menuItemIndex) in menuData"
17+
:key="menuItemIndex"
18+
:val="menuItemIndex"
19+
>
20+
<NavigationMenuTrigger class="NavigationMenuLevel1">
21+
{{ menuItem.label }}
22+
</NavigationMenuTrigger>
23+
<NavigationMenuContent>
24+
<fieldset
25+
v-for="(childrenItems, groupType, childrenIndex) in menuItem.children"
26+
:key="childrenIndex"
27+
>
28+
<legend class="NavigationMenuLegend">
29+
{{ groupType }}
30+
</legend>
31+
<ul class="NavigationMenu-NoList">
32+
<li
33+
v-for="(childrenItem, childrenItemIndex) in childrenItems"
34+
:key="childrenItemIndex"
35+
>
36+
<NavigationMenuLink as-child>
37+
<a :href="childrenItem.url" class="NavigationMenuLevel2">
38+
{{ childrenItem.acronym }}
39+
&mdash;
40+
{{ childrenItem.name }}
41+
</a>
42+
</NavigationMenuLink>
43+
</li>
44+
</ul>
45+
</fieldset>
46+
</NavigationMenuContent>
47+
</NavigationMenuItem>
48+
</NavigationMenuList>
49+
</NavigationMenuSub>
50+
</NavigationMenuContent>
51+
</NavigationMenuItem>
52+
</NavigationMenuList>
53+
</NavigationMenuRoot>
54+
</template>
55+
56+
<script setup lang="js">
57+
import {
58+
NavigationMenuContent,
59+
NavigationMenuIndicator,
60+
NavigationMenuItem,
61+
NavigationMenuLink,
62+
NavigationMenuList,
63+
NavigationMenuRoot,
64+
NavigationMenuSub,
65+
NavigationMenuTrigger,
66+
NavigationMenuViewport,
67+
} from 'reka-ui'
68+
import { onMounted, ref } from 'vue'
69+
import { groupBy } from 'lodash-es'
70+
71+
72+
// type GroupMenuData = Record<string, {
73+
// "acronym": string
74+
// "name": string
75+
// "type": string
76+
// "url": string
77+
// }[]>
78+
79+
// type MenuItem = {
80+
// label: string
81+
// href: string
82+
// parentId: string
83+
// children: GroupMenuData[string]
84+
// }
85+
86+
const menuData = ref(null)
87+
88+
onMounted(async () => {
89+
// The previous Bootstrap menu would download menu JSON
90+
// and then attach to specific elements in the DOM that
91+
// had this prefix in the 'class' attribute.
92+
const GROUP_MENU_PREFIX = 'group-parent-'
93+
// so we'll scrape those in order to build data for a
94+
// Reka menu
95+
const groupsLink = document.querySelector('[data-groups]')
96+
const groupsList = groupsLink.nextElementSibling
97+
const groupParents = groupsList.querySelectorAll(`li[class*=${GROUP_MENU_PREFIX}]`)
98+
const menu = Array.from(groupParents).map(element => {
99+
const parentId = element.getAttribute('class')
100+
.split(' ')
101+
.filter(classItem => classItem.includes(GROUP_MENU_PREFIX))
102+
.reduce((_acc, classItem) => classItem.replace(GROUP_MENU_PREFIX, ''), null)
103+
104+
const href = element.querySelector('a[href]').getAttribute('href')
105+
106+
return {
107+
label: element.innerText,
108+
href,
109+
parentId,
110+
}
111+
})
112+
113+
const updateMenu = (groupMenuData) => {
114+
menuData.value = menu.map(menuItem => ({
115+
...menuItem,
116+
children: groupBy(groupMenuData[menuItem.parentId], 'type')
117+
}))
118+
console.log({ menuDataValue: menuData.value })
119+
120+
// remove original 'Groups' menu
121+
groupsLink.parentNode.removeChild(groupsLink)
122+
groupsList.parentNode.removeChild(groupsList)
123+
}
124+
125+
// Download JSON for menu
126+
const groupMenuDataUrl = document.body.dataset.groupMenuDataUrl
127+
const response = await fetch(groupMenuDataUrl)
128+
if (response.ok) {
129+
response.json().then((groupMenuData) => {
130+
updateMenu(groupMenuData)
131+
})
132+
} else {
133+
console.error(`Problem downloading ${groupMenuDataUrl} ${response.status} ${response.statusText}`, response)
134+
}
135+
})
136+
137+
</script>
138+
139+
<style>
140+
.NavigationMenuRoot {
141+
}
142+
143+
.NavigationMenuList {
144+
list-style: none;
145+
margin: 0;
146+
padding: 0;
147+
}
148+
149+
.NavigationMenuTrigger {
150+
outline: none;
151+
user-select: none;
152+
line-height: 1;
153+
border: 0;
154+
background-color: inherit;
155+
color: var(--bs-nav-link-color);
156+
padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);
157+
}
158+
159+
.NavigationMenuLink {
160+
padding: 8px 12px;
161+
outline: none;
162+
user-select: none;
163+
font-weight: 500;
164+
line-height: 1;
165+
border: 0;
166+
display: block;
167+
text-decoration: none;
168+
line-height: 1;
169+
}
170+
171+
.NavigationMenuContent {
172+
position: absolute;
173+
animation-duration: 250ms;
174+
animation-timing-function: ease;
175+
background-color: #212529;
176+
border: solid 1px #ffffff26;
177+
border-radius: .375rem;
178+
padding: 0.5rem 0;
179+
min-width: 300px;
180+
}
181+
182+
.NavigationMenuContent[data-motion='from-start'] {
183+
animation-name: enterFromLeft;
184+
}
185+
.NavigationMenuContent[data-motion='from-end'] {
186+
animation-name: enterFromRight;
187+
}
188+
.NavigationMenuContent[data-motion='to-start'] {
189+
animation-name: exitToLeft;
190+
}
191+
.NavigationMenuContent[data-motion='to-end'] {
192+
animation-name: exitToRight;
193+
}
194+
195+
.NavigationMenu-NoList {
196+
list-style: none;
197+
margin: 0;
198+
padding: 0;
199+
}
200+
201+
.NavigationMenuLevel1 {
202+
border: 0;
203+
width: 100%;
204+
padding: 0.25rem 1rem;
205+
clear: both;
206+
color: #dee2e6;
207+
text-align:left;
208+
background: transparent;
209+
}
210+
211+
.NavigationMenuLevel1:hover,
212+
.NavigationMenuLevel1:focus {
213+
background: #2b3035;
214+
}
215+
216+
.NavigationMenuLevel2 {
217+
border: 0;
218+
width: 100%;
219+
padding: 0.25rem 1rem;
220+
margin-left: 1rem;
221+
clear: both;
222+
color: #dee2e6;
223+
text-align:left;
224+
background: transparent;
225+
}
226+
227+
.NavigationMenuLevel2:hover,
228+
.NavigationMenuLevel2:focus {
229+
background: #2b3035;
230+
}
231+
232+
233+
.NavigationMenuLegend {
234+
margin-left: 1rem;
235+
opacity: 0.8;
236+
font-size: inherit;
237+
}
238+
239+
.NavigationMenuViewport {
240+
position: fixed;
241+
transform-origin: top center;
242+
margin-top: 10px;
243+
width: 100%;
244+
background-color: white;
245+
border-radius: 6px;
246+
overflow: hidden;
247+
box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
248+
height: var(--reka-navigation-menu-viewport-height);
249+
transition: width, height, 300ms ease;
250+
}
251+
.NavigationMenuViewport[data-state='open'] {
252+
animation: scaleIn 200ms ease;
253+
}
254+
.NavigationMenuViewport[data-state='closed'] {
255+
animation: scaleOut 200ms ease;
256+
}
257+
@media only screen and (min-width: 600px) {
258+
.NavigationMenuViewport {
259+
width: var(--reka-navigation-menu-viewport-width);
260+
}
261+
}
262+
263+
@keyframes enterFromRight {
264+
from {
265+
opacity: 0;
266+
transform: translateX(200px);
267+
}
268+
to {
269+
opacity: 1;
270+
transform: translateX(0);
271+
}
272+
}
273+
274+
@keyframes enterFromLeft {
275+
from {
276+
opacity: 0;
277+
transform: translateX(-200px);
278+
}
279+
to {
280+
opacity: 1;
281+
transform: translateX(0);
282+
}
283+
}
284+
285+
@keyframes exitToRight {
286+
from {
287+
opacity: 1;
288+
transform: translateX(0);
289+
}
290+
to {
291+
opacity: 0;
292+
transform: translateX(200px);
293+
}
294+
}
295+
296+
@keyframes exitToLeft {
297+
from {
298+
opacity: 1;
299+
transform: translateX(0);
300+
}
301+
to {
302+
opacity: 0;
303+
transform: translateX(-200px);
304+
}
305+
}
306+
307+
@keyframes scaleIn {
308+
from {
309+
opacity: 0;
310+
transform: rotateX(-30deg) scale(0.9);
311+
}
312+
to {
313+
opacity: 1;
314+
transform: rotateX(0deg) scale(1);
315+
}
316+
}
317+
318+
@keyframes scaleOut {
319+
from {
320+
opacity: 1;
321+
transform: rotateX(0deg) scale(1);
322+
}
323+
to {
324+
opacity: 0;
325+
transform: rotateX(-10deg) scale(0.95);
326+
}
327+
}
328+
329+
@keyframes fadeIn {
330+
from {
331+
opacity: 0;
332+
}
333+
to {
334+
opacity: 1;
335+
}
336+
}
337+
338+
@keyframes fadeOut {
339+
from {
340+
opacity: 1;
341+
}
342+
to {
343+
opacity: 0;
344+
}
345+
}
346+
</style>

0 commit comments

Comments
 (0)