Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
larsrickert committed Oct 30, 2024
1 parent 1dd27d7 commit 7b9cba0
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ export const WithCustomAppArea = {
export const WithOverflowingMobileContent = {
args: {
...WithContextArea.args,
mobileBreakpoint: "xl",
default: () => [
h(OnyxNavButton, { label: "Item 1", href: "/" }),
h(
Expand Down
29 changes: 24 additions & 5 deletions packages/sit-onyx/src/components/OnyxNavBar/OnyxNavBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ import { createNavigationMenu } from "@sit-onyx/headless";
import chevronLeftSmall from "@sit-onyx/icons/chevron-left-small.svg?raw";
import menu from "@sit-onyx/icons/menu.svg?raw";
import moreVertical from "@sit-onyx/icons/more-vertical.svg?raw";
import { computed, provide, ref, toRef } from "vue";
import { computed, provide, reactive, ref, toRef, unref, type Ref } from "vue";
import { useMore, type HTMLOrInstanceRef } from "../../composables/useMore";
import { useResizeObserver } from "../../composables/useResizeObserver";
import { injectI18n } from "../../i18n";
import { ONYX_BREAKPOINTS } from "../../types";
import OnyxIconButton from "../OnyxIconButton/OnyxIconButton.vue";
import OnyxMobileNavButton from "../OnyxMobileNavButton/OnyxMobileNavButton.vue";
import OnyxNavAppArea from "../OnyxNavAppArea/OnyxNavAppArea.vue";
import { MOBILE_NAV_BAR_INJECTION_KEY, type OnyxNavBarProps } from "./types";
import {
MOBILE_NAV_BAR_INJECTION_KEY,
NAV_BAR_BUTTONS_INJECTION_KEY,
type OnyxNavBarProps,
} from "./types";
const props = withDefaults(defineProps<OnyxNavBarProps>(), {
mobileBreakpoint: "sm",
Expand Down Expand Up @@ -76,6 +81,16 @@ const isMobile = computed(() => {
provide(MOBILE_NAV_BAR_INJECTION_KEY, isMobile);
const menuBarRef = ref<HTMLElement>();
const navButtonRefs = reactive(new Map<string, Ref<HTMLOrInstanceRef>>());
provide(NAV_BAR_BUTTONS_INJECTION_KEY, navButtonRefs);
const { visibleElements, hiddenElements, totalElements } = useMore({
parentRef: menuBarRef,
componentRefs: computed(() => Array.from(navButtonRefs.values()).map((ref) => unref(ref))),
disabled: isMobile,
});
defineExpose({
/**
* Closes the mobile burger and context menu.
Expand All @@ -98,6 +113,9 @@ defineExpose({
</script>

<template>
<div>{{ visibleElements }} visible</div>
<div>{{ hiddenElements }} hidden</div>
<div>{{ totalElements }} total</div>
<header ref="navBarRef" class="onyx-nav-bar" :class="{ 'onyx-nav-bar--mobile': isMobile }">
<div class="onyx-nav-bar__content">
<span
Expand Down Expand Up @@ -148,7 +166,7 @@ defineExpose({
</OnyxMobileNavButton>

<nav v-else class="onyx-nav-bar__nav" v-bind="nav">
<ul role="menubar">
<ul ref="menuBarRef" role="menubar">
<slot></slot>
</ul>
</nav>
Expand Down Expand Up @@ -211,7 +229,7 @@ $gap: var(--onyx-spacing-md);
&__content {
display: grid;
grid-template-columns: max-content 1fr auto;
grid-template-columns: max-content minmax(0, 1fr) auto;
grid-template-areas: "app nav context";
align-items: center;
gap: $gap;
Expand All @@ -223,7 +241,7 @@ $gap: var(--onyx-spacing-md);
margin-inline: var(--onyx-grid-margin-inline);
&:has(.onyx-nav-bar__back) {
grid-template-columns: max-content max-content 1fr auto;
grid-template-columns: max-content max-content minmax(0, 1fr) auto;
grid-template-areas: "app back nav context";
}
}
Expand All @@ -234,6 +252,7 @@ $gap: var(--onyx-spacing-md);
&__nav {
grid-area: nav;
overflow-x: clip;
> ul {
display: flex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MANAGED_SYMBOL, useManagedState } from "../../../../composables/useMana
import OnyxExternalLinkIcon from "../../../OnyxExternalLinkIcon/OnyxExternalLinkIcon.vue";
import OnyxIcon from "../../../OnyxIcon/OnyxIcon.vue";
import { MOBILE_NAV_BAR_INJECTION_KEY } from "../../types";
import { useNavBarButtons } from "../../useNavBarButtons";
import NavButtonLayout from "./NavButtonLayout.vue";
import type { OnyxNavButtonProps } from "./types";
Expand Down Expand Up @@ -38,6 +39,7 @@ const slots = defineSlots<{
const isMobile = inject(MOBILE_NAV_BAR_INJECTION_KEY);
const hasChildren = computed(() => !!slots.children);
const { navButtonRef } = useNavBarButtons();
const { state: mobileChildrenOpen } = useManagedState(
toRef(() => props.mobileChildrenOpen),
Expand All @@ -56,6 +58,7 @@ const handleParentClick = (event: MouseEvent) => {

<template>
<NavButtonLayout
ref="navButtonRef"
v-bind="props"
v-model:mobile-children-open="mobileChildrenOpen"
class="onyx-nav-button"
Expand Down
7 changes: 6 additions & 1 deletion packages/sit-onyx/src/components/OnyxNavBar/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ComputedRef, InjectionKey } from "vue";
import type { ComputedRef, InjectionKey, Ref } from "vue";
import type { HTMLOrInstanceRef } from "../../composables/useMore";
import type { OnyxBreakpoint } from "../../types";
import type { OnyxNavAppAreaProps } from "../OnyxNavAppArea/types";

Expand Down Expand Up @@ -29,3 +30,7 @@ export type OnyxNavBarProps = Omit<OnyxNavAppAreaProps, "label"> & {
* @returns `true` if mobile, `false` otherwise
*/
export const MOBILE_NAV_BAR_INJECTION_KEY = Symbol() as InjectionKey<ComputedRef<boolean>>;

export const NAV_BAR_BUTTONS_INJECTION_KEY = Symbol() as InjectionKey<
Map<string, Ref<HTMLOrInstanceRef>>
>;
32 changes: 32 additions & 0 deletions packages/sit-onyx/src/components/OnyxNavBar/useNavBarButtons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { inject, onBeforeUnmount, ref, useId } from "vue";
import type { HTMLOrInstanceRef } from "../../composables/useMore";
import { NAV_BAR_BUTTONS_INJECTION_KEY } from "./types";

export const useNavBarButtons = () => {
const id = useId();
const navButtonRef = ref<HTMLOrInstanceRef>();

const map = inject(NAV_BAR_BUTTONS_INJECTION_KEY);

map?.set(id, navButtonRef);
onBeforeUnmount(() => map?.delete(id));

return {
/**
* Nav button template ref.
*
* @example
*
* ```vue
* <script lang="ts" setup
* const { navButtonRef } = useNavBarButtons();
* </script>
*
* <template
* <OnyxNavButton ref="navButtonRef" />
* </template>
* ```
*/
navButtonRef,
};
};
75 changes: 42 additions & 33 deletions packages/sit-onyx/src/composables/useMore.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import {
computed,
onBeforeUnmount,
ref,
watchEffect,
type ComponentPublicInstance,
type Ref,
} from "vue";
import { computed, onBeforeUnmount, ref, watch, type Ref } from "vue";

export type HTMLOrInstanceRef = Element | { $el: Element } | null | undefined;

export type UseMoreOptions = {
/**
* Vue template ref for the parent element containing the list of components.
*/
parentRef: Ref<HTMLElement | null | undefined>;
parentRef: Ref<HTMLOrInstanceRef>;
/**
* Refs for the individual components in the list.
*/
componentRefs: Ref<(HTMLElement | Pick<ComponentPublicInstance, "$el">)[]>;
componentRefs: Ref<HTMLOrInstanceRef[]>;
/**
* Whether the intersection observer should be disabled (e.g. when more feature is currently not needed due to mobile layout).
*/
Expand Down Expand Up @@ -69,32 +64,42 @@ export const useMore = (options: UseMoreOptions) => {
const observer = ref<IntersectionObserver>();
onBeforeUnmount(() => observer.value?.disconnect());

watchEffect(() => {
observer.value?.disconnect(); // reset observer before all changes
if (!options.parentRef.value || options.disabled) return;
watch(
[options.parentRef, options.componentRefs, options.disabled],
() => {
observer.value?.disconnect(); // reset observer before all changes

const root = refToHTMLElement(options.parentRef.value);
if (!root || options.disabled?.value) {
visibleElements.value = 0;
return;
}

observer.value = new IntersectionObserver(
(res) => {
// res contains all changed components (not all available components)
// if component is shown, intersectionRatio is 1 so remainingItems should be decremented
// otherwise remainingItems should be increment because component is no longer shown
const shownElements = res.reduce(
(prev, curr) => (curr.intersectionRatio === 1 ? prev + 1 : prev),
0,
);
const hiddenChips = res.length - shownElements;
observer.value = new IntersectionObserver(
(res) => {
// res contains all changed components (not all available components)
// if component is shown, intersectionRatio is 1 so remainingItems should be decremented
// otherwise remainingItems should be increment because component is no longer shown
const shownElements = res.reduce(
(prev, curr) => (curr.intersectionRatio === 1 ? prev + 1 : prev),
0,
);
const hiddenElements = res.length - shownElements;

if (visibleElements.value <= 0) visibleElements.value = shownElements;
else visibleElements.value += shownElements - hiddenChips;
},
{ root: options.parentRef.value, threshold: 1 },
);
if (visibleElements.value <= 0) visibleElements.value = shownElements;
else visibleElements.value += shownElements - hiddenElements;
},
{ root, threshold: 1 },
);

options.componentRefs.value.forEach((element) => {
const htmlElement = element instanceof HTMLElement ? element : element.$el;
observer.value?.observe(htmlElement);
});
});
options.componentRefs.value.forEach((ref) => {
const element = refToHTMLElement(ref);
if (!element) return;
observer.value?.observe(element);
});
},
{ immediate: true, deep: true },
);

return {
/**
Expand All @@ -111,3 +116,7 @@ export const useMore = (options: UseMoreOptions) => {
totalElements,
};
};

const refToHTMLElement = (ref: HTMLOrInstanceRef) => {
return ref instanceof Element ? ref : ref?.$el;
};

0 comments on commit 7b9cba0

Please sign in to comment.