-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement
OnyxMoreList
component (#2022)
Relates to #986 Implement a generic `OnyxMoreList` component that can be used to render any list of components with a "+ more" indicator. I will integrate it in following PRs into the nav bar as well as implementing the positioning of the more indicator which is currently not visible at all. ## Checklist - [x] The added / edited code has been documented with [JSDoc](https://jsdoc.app/about-getting-started) - [ ] If a new component is added, at least one [Playwright screenshot test](https://github.com/SchwarzIT/onyx/actions/workflows/playwright-screenshots.yml) is added - [ ] A changeset is added with `npx changeset add` if your changes should be released as npm package (because they affect the library usage)
- Loading branch information
1 parent
1a17bb0
commit 4db5e6d
Showing
13 changed files
with
484 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
58 changes: 58 additions & 0 deletions
58
packages/sit-onyx/src/components/OnyxMoreList/OnyxMoreList.ct.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { expect, test } from "../../playwright/a11y"; | ||
import TestWrapperCt from "./TestWrapper.ct.vue"; | ||
import type { MoreListSlotBindings } from "./types"; | ||
|
||
test("should render", async ({ mount, makeAxeBuilder }) => { | ||
const events: MoreListSlotBindings[] = []; | ||
|
||
const eventHandlers = { | ||
onVisibilityChange: (data: MoreListSlotBindings) => events.push(data), | ||
}; | ||
|
||
// ARRANGE | ||
const component = await mount(TestWrapperCt, { | ||
props: { | ||
count: 2.5, | ||
}, | ||
on: eventHandlers, | ||
}); | ||
|
||
const expectVisible = (label: string) => { | ||
return expect(component.getByLabel(label, { exact: true })).toBeInViewport({ | ||
ratio: 1, | ||
}); | ||
}; | ||
|
||
const expectHidden = (label: string) => { | ||
return expect(component.getByLabel(label, { exact: true })).toBeHidden(); | ||
}; | ||
|
||
// ACT | ||
const accessibilityScanResults = await makeAxeBuilder().analyze(); | ||
|
||
// ASSERT | ||
expect(accessibilityScanResults.violations).toEqual([]); | ||
await expectVisible("Element 1"); | ||
await expectVisible("Element 2"); | ||
await expectHidden("Element 3"); | ||
expect(events.at(-1)).toStrictEqual({ visibleElements: 2, hiddenElements: 22 }); | ||
|
||
// ACT | ||
await component.update({ props: { count: 1 }, on: eventHandlers }); | ||
|
||
// ASSERT | ||
await expectVisible("Element 1"); | ||
await expectHidden("Element 2"); | ||
expect(events.at(-1)).toStrictEqual({ visibleElements: 1, hiddenElements: 23 }); | ||
|
||
// ACT | ||
await component.update({ props: { count: 4.25 }, on: eventHandlers }); | ||
|
||
// ASSERT | ||
await expectVisible("Element 1"); | ||
await expectVisible("Element 2"); | ||
await expectVisible("Element 3"); | ||
await expectVisible("Element 4"); | ||
await expectHidden("Element 5"); | ||
expect(events.at(-1)).toStrictEqual({ visibleElements: 4, hiddenElements: 20 }); | ||
}); |
31 changes: 31 additions & 0 deletions
31
packages/sit-onyx/src/components/OnyxMoreList/OnyxMoreList.stories.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import type { Meta, StoryObj } from "@storybook/vue3"; | ||
import { h } from "vue"; | ||
import OnyxNavButton from "../OnyxNavBar/modules/OnyxNavButton/OnyxNavButton.vue"; | ||
import { NAV_BAR_MORE_LIST_INJECTION_KEY } from "../OnyxNavBar/types"; | ||
import OnyxMoreList from "./OnyxMoreList.vue"; | ||
|
||
/** | ||
* Support component for rendering a horizontal list of components with a "+ more" indicator. | ||
* If using custom or not natively supported components, make sure to implement the `useMoreChild()` composable in all child components. | ||
*/ | ||
const meta: Meta<typeof OnyxMoreList> = { | ||
title: "Support/MoreList", | ||
component: OnyxMoreList, | ||
argTypes: { | ||
default: { control: { disable: true } }, | ||
more: { control: { disable: true } }, | ||
}, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof OnyxMoreList>; | ||
|
||
export const Default = { | ||
args: { | ||
is: "div", | ||
injectionKey: NAV_BAR_MORE_LIST_INJECTION_KEY, | ||
default: () => | ||
Array.from({ length: 24 }, (_, index) => h(OnyxNavButton, { label: `Element ${index + 1}` })), | ||
more: ({ hiddenElements }) => h("div", `+${hiddenElements} more`), | ||
}, | ||
} satisfies Story; |
70 changes: 70 additions & 0 deletions
70
packages/sit-onyx/src/components/OnyxMoreList/OnyxMoreList.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
<script lang="ts" setup> | ||
import { provide, reactive, ref, toRef, watch, type Ref } from "vue"; | ||
import { useMoreList, type HTMLOrInstanceRef } from "../../composables/useMoreList"; | ||
import type { MoreListSlotBindings, OnyxMoreListProps } from "./types"; | ||
const props = defineProps<OnyxMoreListProps>(); | ||
const emit = defineEmits<{ | ||
/** | ||
* Emitted when the number of visible elements changes. | ||
*/ | ||
visibilityChange: [MoreListSlotBindings]; | ||
}>(); | ||
defineSlots<{ | ||
/** | ||
* List of components to render. Each child must implement the `useMoreChild()` composable. | ||
*/ | ||
default(): unknown; | ||
/** | ||
* Slot to display at the end if not all default slot elements fit in the available width. | ||
*/ | ||
more(props: MoreListSlotBindings): unknown; | ||
}>(); | ||
const parentRef = ref<HTMLOrInstanceRef>(); | ||
const componentRefs = reactive(new Map<string, Ref<HTMLOrInstanceRef>>()); | ||
const disabled = toRef(props, "disabled"); | ||
const more = useMoreList({ parentRef, componentRefs, disabled }); | ||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss -- provide does not support reactive symbols, this reactivity loss is mentioned in the property docs | ||
provide(props.injectionKey, { | ||
components: componentRefs, | ||
visibleElements: more.visibleElements, | ||
disabled, | ||
}); | ||
watch([more.visibleElements, more.hiddenElements], ([visibleElements, hiddenElements]) => { | ||
emit("visibilityChange", { | ||
visibleElements: visibleElements.length, | ||
hiddenElements: hiddenElements.length, | ||
}); | ||
}); | ||
</script> | ||
|
||
<template> | ||
<component :is="props.is" ref="parentRef" class="onyx-more"> | ||
<slot></slot> | ||
<slot | ||
v-if="more.hiddenElements.value.length > 0" | ||
name="more" | ||
:hidden-elements="more.hiddenElements.value.length" | ||
:visible-elements="more.visibleElements.value.length" | ||
></slot> | ||
</component> | ||
</template> | ||
|
||
<style lang="scss"> | ||
@use "../../styles/mixins/layers.scss"; | ||
.onyx-more { | ||
@include layers.component() { | ||
display: flex; | ||
align-items: center; | ||
gap: var(--onyx-spacing-4xs); | ||
overflow-x: clip; | ||
} | ||
} | ||
</style> |
47 changes: 47 additions & 0 deletions
47
packages/sit-onyx/src/components/OnyxMoreList/TestWrapper.ct.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
<script lang="ts" setup> | ||
import OnyxNavButton from "../OnyxNavBar/modules/OnyxNavButton/OnyxNavButton.vue"; | ||
import { NAV_BAR_MORE_LIST_INJECTION_KEY } from "../OnyxNavBar/types"; | ||
import OnyxMoreList from "./OnyxMoreList.vue"; | ||
import type { MoreListSlotBindings } from "./types"; | ||
const props = defineProps<{ | ||
/** | ||
* Number of components to show. Can also be decimal. | ||
*/ | ||
count?: number; | ||
}>(); | ||
const emit = defineEmits<{ | ||
visibilityChange: [value: MoreListSlotBindings]; | ||
}>(); | ||
const COMPONENT_WIDTH = "8rem"; | ||
</script> | ||
|
||
<template> | ||
<OnyxMoreList | ||
is="ul" | ||
class="list" | ||
role="menu" | ||
:injection-key="NAV_BAR_MORE_LIST_INJECTION_KEY" | ||
:style="{ | ||
width: props.count ? `calc(${props.count} * ${COMPONENT_WIDTH})` : undefined, | ||
}" | ||
@visibility-change="emit('visibilityChange', $event)" | ||
> | ||
<OnyxNavButton | ||
v-for="i in 24" | ||
:key="i" | ||
:label="`Element ${i}`" | ||
:style="{ minWidth: COMPONENT_WIDTH }" | ||
/> | ||
</OnyxMoreList> | ||
</template> | ||
|
||
<!-- eslint-disable-next-line vue-scoped-css/enforce-style-type --> | ||
<style scoped> | ||
.list { | ||
padding: 0; | ||
gap: 0; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import type { MoreListInjectionKey } from "../../composables/useMoreList"; | ||
|
||
export type OnyxMoreListProps = { | ||
/** | ||
* Component to render (e.g. `<ul>` or `<div>`). | ||
*/ | ||
is: string; | ||
/** | ||
* Injection key to use. Must match the one used in the child components. | ||
* Will not be reactive so it must not be changed. | ||
*/ | ||
injectionKey: MoreListInjectionKey; | ||
/** | ||
* Whether the intersection observer should be disabled (e.g. when more feature is currently not needed due to mobile layout). | ||
* Can increase performance. | ||
*/ | ||
disabled?: boolean; | ||
}; | ||
|
||
export type MoreListSlotBindings = { | ||
/** | ||
* Number of currently fully visible elements. | ||
*/ | ||
visibleElements: number; | ||
/** | ||
* Number of currently completely or partially hidden elements. | ||
*/ | ||
hiddenElements: number; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.