Files
leadchat/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue
Shivam Mishra 6e6c5a2f02 refactor: use css only last item detection (#10363)
The last item in the sidebar top level group has an indicator specified,
the problem in our case is that the structure can be nested and have sub
groups. So selecting the last item correctly can be tricky.

Previous implementation relied on the using DOM queries to find the last
item from a flat list of children, it would trigger on a `watch`. This
was error-prone as well as non idiomatic. The new approach is CSS-only
and reduces the unnecessary compute required.

Codepen for reference: https://codepen.io/scmmishra/pen/yLmKNLW

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-10-31 09:39:18 +05:30

198 lines
5.1 KiB
Vue

<script setup>
import { computed } from 'vue';
import { useSidebarContext } from './provider';
import { useRoute, useRouter } from 'vue-router';
import Policy from 'dashboard/components/policy.vue';
import SidebarGroupHeader from './SidebarGroupHeader.vue';
import SidebarGroupLeaf from './SidebarGroupLeaf.vue';
import SidebarSubGroup from './SidebarSubGroup.vue';
import SidebarGroupEmptyLeaf from './SidebarGroupEmptyLeaf.vue';
const props = defineProps({
name: { type: String, required: true },
label: { type: String, required: true },
icon: { type: [String, Object, Function], default: null },
to: { type: Object, default: null },
activeOn: { type: Array, default: () => [] },
children: { type: Array, default: undefined },
});
const {
expandedItem,
setExpandedItem,
resolvePath,
resolvePermissions,
resolveFeatureFlag,
isAllowed,
} = useSidebarContext();
const navigableChildren = computed(() => {
return props.children?.flatMap(child => child.children || child) || [];
});
const route = useRoute();
const router = useRouter();
const isExpanded = computed(() => expandedItem.value === props.name);
const isExpandable = computed(() => props.children);
const hasChildren = computed(
() => Array.isArray(props.children) && props.children.length > 0
);
const accessibleItems = computed(() => {
if (!hasChildren.value) return [];
return props.children.filter(child => isAllowed(child.to));
});
const hasAccessibleChildren = computed(() => {
return accessibleItems.value.length > 0;
});
const isActive = computed(() => {
if (props.to) {
if (route.path === resolvePath(props.to)) return true;
return props.activeOn.includes(route.name);
}
return false;
});
// We could use the RouterLink isActive too, but our routes are not always
// nested correctly, so we need to check the active state ourselves
// TODO: Audit the routes and fix the nesting and remove this
const activeChild = computed(() => {
const pathSame = navigableChildren.value.find(
child => child.to && route.path === resolvePath(child.to)
);
if (pathSame) return pathSame;
const pathSatrtsWith = navigableChildren.value.find(
child => child.to && route.path.startsWith(resolvePath(child.to))
);
if (pathSatrtsWith) return pathSatrtsWith;
return navigableChildren.value.find(child =>
child.activeOn?.includes(route.name)
);
});
const hasActiveChild = computed(() => {
return activeChild.value !== undefined;
});
const toggleTrigger = () => {
if (
hasAccessibleChildren.value &&
!isExpanded.value &&
!hasActiveChild.value
) {
// if not already expanded, navigate to the first child
const firstItem = accessibleItems.value[0];
router.push(firstItem.to);
}
setExpandedItem(props.name);
};
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<Policy
v-if="!hasChildren || hasAccessibleChildren"
:permissions="resolvePermissions(to)"
:feature-flag="resolveFeatureFlag(to)"
as="li"
class="text-sm cursor-pointer select-none gap-1 grid"
>
<SidebarGroupHeader
:icon
:name
:label
:to
:is-active="isActive"
:has-active-child="hasActiveChild"
:expandable="hasChildren"
:is-expanded="isExpanded"
@toggle="toggleTrigger"
/>
<ul
v-if="hasChildren"
v-show="isExpanded || hasActiveChild"
class="list-none m-0 grid sidebar-group-children"
>
<template v-for="child in children" :key="child.name">
<SidebarSubGroup
v-if="child.children"
v-bind="child"
:is-expanded="isExpanded"
:active-child="activeChild"
/>
<SidebarGroupLeaf
v-else
v-show="isExpanded || activeChild?.name === child.name"
v-bind="child"
:active="activeChild?.name === child.name"
/>
</template>
</ul>
<ul v-else-if="isExpandable && isExpanded">
<SidebarGroupEmptyLeaf />
</ul>
</Policy>
</template>
<style>
.sidebar-group-children .child-item::before {
content: '';
position: absolute;
width: 0.125rem;
/* 0.5px */
height: 100%;
}
.sidebar-group-children .child-item:first-child::before {
border-radius: 4px 4px 0 0;
}
/* This selects the last child in a group */
/* https://codepen.io/scmmishra/pen/yLmKNLW */
.sidebar-group-children > .child-item:last-child::before,
.sidebar-group-children
> *:last-child
> *:last-child
> .child-item:last-child::before {
height: 20%;
}
.sidebar-group-children > .child-item:last-child::after,
.sidebar-group-children
> *:last-child
> *:last-child
> .child-item:last-child::after {
content: '';
position: absolute;
width: 10px;
height: 12px;
bottom: calc(50% - 2px);
border-bottom-width: 0.125rem;
border-left-width: 0.125rem;
border-right-width: 0px;
border-top-width: 0px;
border-radius: 0 0 0 4px;
left: 0;
}
.app-rtl--wrapper .sidebar-group-children > .child-item:last-child::after,
.app-rtl--wrapper
.sidebar-group-children
> *:last-child
> *:last-child
> .child-item:last-child::after {
right: 0;
border-bottom-width: 0.125rem;
border-right-width: 0.125rem;
border-left-width: 0px;
border-top-width: 0px;
border-radius: 0 0 4px 0px;
}
</style>