Skip to content

Commit

Permalink
implement basic useMore composable
Browse files Browse the repository at this point in the history
  • Loading branch information
larsrickert committed Oct 30, 2024
1 parent e5480e3 commit 1dd27d7
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 8 deletions.
4 changes: 2 additions & 2 deletions apps/demo-app/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
OnyxUserMenu,
type OnyxNavButtonProps,
} from "sit-onyx";
import { ref, watch } from "vue";
import { ref, watch, type ComponentInstance } from "vue";
import { RouterView, useRoute, useRouter } from "vue-router";
import onyxLogo from "./assets/onyx-logo.svg";
import { useGridStore } from "./stores/grid-store";
Expand All @@ -30,7 +30,7 @@ const navItems = [
const { store: colorScheme } = useColorMode();
const navBarRef = ref<InstanceType<typeof OnyxNavBar>>();
const navBarRef = ref<ComponentInstance<typeof OnyxNavBar>>();
watch(
() => route.path,
Expand Down
6 changes: 3 additions & 3 deletions apps/playground/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { Repl } from "@vue/repl";
import Monaco from "@vue/repl/monaco-editor";
import { useDark } from "@vueuse/core";
import { OnyxAppLayout } from "sit-onyx";
import { computed, ref } from "vue";
import { computed, ref, type ComponentInstance } from "vue";
import TheHeader from "./components/TheHeader.vue";
import { useStore } from "./composables/useStore";
const { store, onyxVersion, isLoadingOnyxVersions } = useStore();
const replRef = ref<InstanceType<typeof Repl>>();
const replRef = ref<ComponentInstance<typeof Repl>>();
const reloadPage = () => {
replRef.value?.reload();
store.reloadLanguageTools?.();
Expand All @@ -18,7 +18,7 @@ const reloadPage = () => {
const isDark = useDark();
const theme = computed(() => (isDark.value ? "dark" : "light"));
const previewOptions = computed<InstanceType<typeof Repl>["previewOptions"]>(() => {
const previewOptions = computed<ComponentInstance<typeof Repl>["previewOptions"]>(() => {
return {
headHTML: `<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/sit-onyx@${onyxVersion.value}/dist/style.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/sit-onyx@${onyxVersion.value}/src/styles/global.css' />
Expand Down
2 changes: 1 addition & 1 deletion packages/sit-onyx/src/components/OnyxNavBar/OnyxNavBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ defineExpose({
*
* ```ts
* const route = useRoute();
* const navBarRef = ref<InstanceType<typeof OnyxNavBar>>();
* const navBarRef = ref<ComponentInstance<typeof OnyxNavBar>>();
*
* watch(() => route.path, () => navBarRef.value?.closeMobileMenus());
* ```
Expand Down
14 changes: 12 additions & 2 deletions packages/sit-onyx/src/components/OnyxSelect/OnyxSelect.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
<script lang="ts" setup generic="TValue extends SelectOptionValue = SelectOptionValue">
import { createComboBox, type ComboboxAutoComplete } from "@sit-onyx/headless";
import { computed, nextTick, ref, toRef, toRefs, useId, watch, watchEffect } from "vue";
import {
computed,
nextTick,
ref,
toRef,
toRefs,
useId,
watch,
watchEffect,
type ComponentInstance,
} from "vue";
import type { ComponentExposed } from "vue-component-type-helpers";
import { useCheckAll } from "../../composables/checkAll";
import { useDensity } from "../../composables/density";
Expand Down Expand Up @@ -124,7 +134,7 @@ const selectionLabels = computed(() => {
}, []);
});
const miniSearch = ref<InstanceType<typeof OnyxMiniSearch>>();
const miniSearch = ref<ComponentInstance<typeof OnyxMiniSearch>>();
const selectInput = ref<ComponentExposed<typeof OnyxSelectInput>>();
const filteredOptions = computed(() => {
Expand Down
113 changes: 113 additions & 0 deletions packages/sit-onyx/src/composables/useMore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {
computed,
onBeforeUnmount,
ref,
watchEffect,
type ComponentPublicInstance,
type Ref,
} from "vue";

export type UseMoreOptions = {
/**
* Vue template ref for the parent element containing the list of components.
*/
parentRef: Ref<HTMLElement | null | undefined>;
/**
* Refs for the individual components in the list.
*/
componentRefs: Ref<(HTMLElement | Pick<ComponentPublicInstance, "$el">)[]>;
/**
* Whether the intersection observer should be disabled (e.g. when more feature is currently not needed due to mobile layout).
*/
disabled?: Ref<boolean>;
};

/**
* Composable for managing a list of components where e.g. a "+3" more indicator should be shown if not all components
* fit into the available width.
*
* @example
*
* ```vue
* <script lang="ts" setup>
* import { ref } from "vue";
* import { useMore } from "../../composables/useMore";
* import OnyxNavButton from "../OnyxNavBar/modules/OnyxNavButton/OnyxNavButton.vue";
*
* const parentRef = ref<HTMLElement>();
* const componentRefs = ref<InstanceType<typeof OnyxNavButton>[]>([]);
*
* const { visibleElements, hiddenElements } = useMore({ parentRef, componentRefs });
* </script>
*
* <template>
* <div ref="parentRef" class="onyx-more">
* <OnyxNavButton v-for="i in 16" ref="componentRefs" :key="i" :label="`Nav button ${i}`" />
* </div>
* </template>
*
* <style lang="scss">
* @use "../../styles/mixins/layers.scss";
*
* .onyx-more {
* @include layers.component() {
* display: flex;
* align-items: center;
* overflow: hidden;
* flex-wrap: nowrap;
* gap: var(--onyx-spacing-sm);
* }
* }
* </style>
* ```
*/
export const useMore = (options: UseMoreOptions) => {
const visibleElements = ref(0);
const totalElements = computed(() => options.componentRefs.value.length);
const hiddenElements = computed(() => totalElements.value - visibleElements.value);

const observer = ref<IntersectionObserver>();
onBeforeUnmount(() => observer.value?.disconnect());

watchEffect(() => {
observer.value?.disconnect(); // reset observer before all changes
if (!options.parentRef.value || options.disabled) 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;

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

options.componentRefs.value.forEach((element) => {
const htmlElement = element instanceof HTMLElement ? element : element.$el;
observer.value?.observe(htmlElement);
});
});

return {
/**
* Number of currently completely visible components in the list.
*/
visibleElements,
/**
* Number of currently not or not fully visible components in the list.
*/
hiddenElements,
/**
* Total number of elements in the list independent on their visibility.
*/
totalElements,
};
};
1 change: 1 addition & 0 deletions packages/sit-onyx/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export * from "./components/OnyxForm/types";

export * from "./composables/density";
export * from "./composables/scrollEnd";
export * from "./composables/useMore";

export { provideI18n, type TranslationFunction } from "./i18n";
export type { OnyxTranslations, ProvideI18nOptions } from "./i18n";
Expand Down

0 comments on commit 1dd27d7

Please sign in to comment.