-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(OnyxNavButton): Implement onyx nav button (#1281)
<!-- Is your PR related to an issue? Then please link it via the "Relates to #" below. Else, remove it. --> Relates to #883 Implement onyx nav button ## Checklist - [x] If a new component is added, at least one [Playwright screenshot test](https://github.com/SchwarzIT/onyx/actions/workflows/playwright-screenshots.yml) is added - [x] A changeset is added with `npx changeset add` if your changes should be released as npm package (because they affect the library usage) --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
- Loading branch information
1 parent
7f41c22
commit d3e9321
Showing
28 changed files
with
946 additions
and
3 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"sit-onyx": minor | ||
--- | ||
|
||
Add new OnyxNavButton component |
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,5 @@ | ||
--- | ||
"@sit-onyx/headless": minor | ||
--- | ||
|
||
Add new createMenuButton composable |
14 changes: 14 additions & 0 deletions
14
packages/headless/src/composables/menuButton/TestMenuButton.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,14 @@ | ||
import { test } from "@playwright/experimental-ct-vue"; | ||
import { menuButtonTesting } from "./createMenuButton.ct"; | ||
import TestMenuButton from "./TestMenuButton.vue"; | ||
|
||
test("menuButton", async ({ mount, page }) => { | ||
await mount(<TestMenuButton />); | ||
|
||
await menuButtonTesting({ | ||
page, | ||
button: page.getByRole("button"), | ||
menu: page.locator("ul"), | ||
menuItems: await page.locator("li").all(), | ||
}); | ||
}); |
33 changes: 33 additions & 0 deletions
33
packages/headless/src/composables/menuButton/TestMenuButton.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,33 @@ | ||
<script lang="ts" setup> | ||
import { createMenuButton } from "./createMenuButton"; | ||
import { ref } from "vue"; | ||
const items = Array.from({ length: 10 }, (_, index) => { | ||
const id = index + 1; | ||
return { label: `Item ${id}`, value: `/href-${id}` }; | ||
}); | ||
const activeItem = ref<string>(); | ||
const { | ||
elements: { button, menu, menuItem, listItem, flyout }, | ||
state: { isExpanded }, | ||
} = createMenuButton({ | ||
onSelect: (value) => { | ||
activeItem.value = value; | ||
}, | ||
}); | ||
</script> | ||
|
||
<template> | ||
<button v-bind="button">Toggle nav menu</button> | ||
<div v-bind="flyout"> | ||
<ul v-show="isExpanded" v-bind="menu"> | ||
<li v-for="item in items" v-bind="listItem" :key="item.value" title="item"> | ||
<a v-bind="menuItem({ active: activeItem === item.value, value: item.value })">{{ | ||
item.label | ||
}}</a> | ||
</li> | ||
</ul> | ||
</div> | ||
</template> |
54 changes: 54 additions & 0 deletions
54
packages/headless/src/composables/menuButton/createMenuButton.ct.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,54 @@ | ||
import { expect } from "@playwright/experimental-ct-vue"; | ||
import type { Locator, Page } from "@playwright/test"; | ||
|
||
export type MenuButtonTestingOptions = { | ||
/** | ||
* Playwright page. | ||
*/ | ||
page: Page; | ||
/** | ||
* Locator for the button element. | ||
*/ | ||
button: Locator; | ||
/** | ||
* Menu, e.g. a `<ul>` element. | ||
*/ | ||
menu: Locator; | ||
/** | ||
* List items (at least 3). | ||
*/ | ||
menuItems: Locator[]; | ||
}; | ||
|
||
/** | ||
* Playwright utility for executing accessibility testing for a navigation menu. | ||
* Will check aria attributes and keyboard shortcuts as defined in https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-links. | ||
*/ | ||
export const menuButtonTesting = async ({ button, menu }: MenuButtonTestingOptions) => { | ||
const menuId = await menu.getAttribute("id"); | ||
expect(menuId).toBeDefined(); | ||
await expect( | ||
button, | ||
"navigation menu should have set the list ID to the aria-controls", | ||
).toHaveAttribute("aria-controls", menuId!); | ||
|
||
await expect( | ||
button, | ||
'navigation menu should have an "aria-haspopup" attribute set to true', | ||
).toHaveAttribute("aria-haspopup", "true"); | ||
|
||
await expect(button).toBeVisible(); | ||
|
||
// ensure correct navigation menu aria attributes | ||
await expect( | ||
button, | ||
'flyout menu must have an "aria-expanded" attribute set to false', | ||
).toHaveAttribute("aria-expanded", "false"); | ||
|
||
button.hover(); | ||
|
||
await expect( | ||
button, | ||
'flyout menu must have an "aria-expanded" attribute set to true', | ||
).toHaveAttribute("aria-expanded", "true"); | ||
}; |
69 changes: 69 additions & 0 deletions
69
packages/headless/src/composables/menuButton/createMenuButton.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,69 @@ | ||
import { computed, ref } from "vue"; | ||
import { createBuilder } from "../../utils/builder"; | ||
import { createId } from "../../utils/id"; | ||
import { debounce } from "../../utils/timer"; | ||
|
||
export type CreateMenuButtonOptions = { | ||
/** | ||
* Called when a menu item is selected (via mouse or keyboard). | ||
*/ | ||
onSelect: (value: string) => void; | ||
}; | ||
|
||
export const createMenuButton = createBuilder((options: CreateMenuButtonOptions) => { | ||
const menuId = createId("menu"); | ||
const buttonId = createId("menu-button"); | ||
const isExpanded = ref<boolean>(false); | ||
|
||
/** | ||
* Debounced expanded state that will only be toggled after a given timeout. | ||
*/ | ||
const updateDebouncedExpanded = debounce( | ||
(expanded: boolean) => (isExpanded.value = expanded), | ||
200, | ||
); | ||
|
||
const hoverEvents = computed(() => { | ||
return { | ||
onMouseover: () => updateDebouncedExpanded(true), | ||
onMouseout: () => updateDebouncedExpanded(false), | ||
onFocusin: () => (isExpanded.value = true), | ||
onFocusout: () => (isExpanded.value = false), | ||
}; | ||
}); | ||
|
||
return { | ||
state: { isExpanded }, | ||
elements: { | ||
button: computed( | ||
() => | ||
({ | ||
"aria-controls": menuId, | ||
"aria-expanded": isExpanded.value, | ||
"aria-haspopup": true, | ||
id: buttonId, | ||
...hoverEvents.value, | ||
}) as const, | ||
), | ||
listItem: { | ||
role: "none", | ||
}, | ||
flyout: { | ||
...hoverEvents.value, | ||
}, | ||
menu: { | ||
id: menuId, | ||
role: "menu", | ||
"aria-labelledby": buttonId, | ||
}, | ||
menuItem: (data: { active?: boolean; value: string }) => ({ | ||
"aria-current": data.active ? "page" : undefined, | ||
role: "menuitem", | ||
tabindex: -1, | ||
onClick: () => { | ||
options.onSelect(data.value); | ||
}, | ||
}), | ||
}, | ||
}; | ||
}); |
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 |
---|---|---|
@@ -1,5 +1,6 @@ | ||
export * from "./composables/comboBox/createComboBox"; | ||
export * from "./composables/listbox/createListbox"; | ||
export * from "./composables/tooltip/createTooltip"; | ||
export * from "./composables/menuButton/createMenuButton"; | ||
export { createId } from "./utils/id"; | ||
export { isPrintableCharacter, wasKeyPressed } from "./utils/keyboard"; |
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 |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from "./composables/comboBox/createComboBox.ct"; | ||
export * from "./composables/listbox/createListbox.ct"; | ||
export * from "./composables/menuButton/createMenuButton.ct"; |
Binary file added
BIN
+25.1 KB
...onyx/playwright/snapshots/components/OnyxNavButton/NavButton-chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+33.8 KB
...-onyx/playwright/snapshots/components/OnyxNavButton/NavButton-firefox-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+25.5 KB
...t-onyx/playwright/snapshots/components/OnyxNavButton/NavButton-webkit-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+26.8 KB
...hots/components/OnyxNavButton/NavButton-with-nested-children-chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+37.2 KB
...shots/components/OnyxNavButton/NavButton-with-nested-children-firefox-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+25 KB
...pshots/components/OnyxNavButton/NavButton-with-nested-children-webkit-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 55 additions & 0 deletions
55
packages/sit-onyx/src/components/OnyxFlyoutMenu/future/OnyxFlyoutMenu.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,55 @@ | ||
import { defineStorybookActionsAndVModels } from "@sit-onyx/storybook-utils"; | ||
import type { Meta, StoryObj } from "@storybook/vue3"; | ||
import { h } from "vue"; | ||
import OnyxListItem from "../../OnyxListItem/OnyxListItem.vue"; | ||
import OnyxButton from "../../OnyxButton/OnyxButton.vue"; | ||
import OnyxFlyoutMenu from "./OnyxFlyoutMenu.vue"; | ||
|
||
const meta: Meta<typeof OnyxFlyoutMenu> = { | ||
title: "future/FlyoutMenu", | ||
...defineStorybookActionsAndVModels({ | ||
component: OnyxFlyoutMenu, | ||
events: [], | ||
decorators: [ | ||
(story) => ({ | ||
components: { story }, | ||
template: ` | ||
<div style="height: 28rem;"> | ||
<story /> | ||
</div>`, | ||
}), | ||
], | ||
argTypes: { | ||
default: { control: { disable: true } }, | ||
options: { control: { disable: true } }, | ||
header: { control: { disable: true } }, | ||
footer: { control: { disable: true } }, | ||
}, | ||
}), | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof OnyxFlyoutMenu>; | ||
|
||
const listAnimals = [ | ||
{ label: "Cat" }, | ||
{ label: "Dog" }, | ||
{ label: "Tiger" }, | ||
{ label: "Reindeer" }, | ||
{ label: "Racoon" }, | ||
{ label: "Dolphin" }, | ||
{ label: "Flounder" }, | ||
{ label: "Eel" }, | ||
{ label: "Falcon" }, | ||
{ label: "Owl" }, | ||
]; | ||
|
||
/** | ||
* This example shows a basic OnyxFlyoutMenu | ||
*/ | ||
export const Default = { | ||
args: { | ||
default: () => h(OnyxButton, { label: "Hover me" }), | ||
options: () => listAnimals.map(({ label }) => h(OnyxListItem, label)), | ||
}, | ||
} satisfies Story; |
140 changes: 140 additions & 0 deletions
140
packages/sit-onyx/src/components/OnyxFlyoutMenu/future/OnyxFlyoutMenu.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,140 @@ | ||
<script setup lang="ts" generic="TValue extends SelectOptionValue = SelectOptionValue"> | ||
import { createMenuButton } from "@sit-onyx/headless"; | ||
import type { SelectOptionValue } from "../../../types"; | ||
import { computed, ref, type VNode } from "vue"; | ||
import { injectI18n } from "../../../i18n"; | ||
const slots = defineSlots<{ | ||
/** | ||
* The trigger for the flyout menu | ||
*/ | ||
default(): VNode[]; | ||
/** | ||
* OnyxListItems to show | ||
*/ | ||
options?(): VNode[]; | ||
/** | ||
* Optional header content to display above the options. | ||
*/ | ||
header?(): unknown; | ||
/** | ||
* Optional footer content to display below the options (will replace `message` property). | ||
*/ | ||
footer?(): unknown; | ||
}>(); | ||
const activeItem = ref<string>(); | ||
const { | ||
elements: { button, menu, menuItem, listItem, flyout }, | ||
state: { isExpanded }, | ||
} = createMenuButton({ | ||
onSelect: (value) => { | ||
activeItem.value = value; | ||
}, | ||
}); | ||
const getSlotComponents = (vnodes: VNode[]): VNode[] => { | ||
// if the slot only contains a v-for, we need to use the children here which are the "actual" slot content | ||
const isVFor = vnodes.length === 1 && vnodes[0].type.toString() === "Symbol(v-fgt)"; | ||
const allNodes = | ||
isVFor && Array.isArray(vnodes[0].children) | ||
? (vnodes[0].children as Extract<(typeof vnodes)[number]["children"], []>) | ||
: vnodes; | ||
return allNodes; | ||
}; | ||
const options = computed(() => { | ||
return getSlotComponents(slots.options?.() ?? []); | ||
}); | ||
const { t } = injectI18n(); | ||
</script> | ||
|
||
<template> | ||
<div class="onyx-future-flyout-menu"> | ||
<component :is="slots.default?.()?.[0]" v-bind="button" /> | ||
<div | ||
v-if="slots.options || slots.header || slots.footer" | ||
v-show="isExpanded" | ||
v-bind="flyout" | ||
:aria-label="t('navigation.navigationHeadline')" | ||
:class="{ | ||
'onyx-future-flyout-menu__list--with-header': !!slots.header, | ||
'onyx-future-flyout-menu__list--with-footer': !!slots.footer, | ||
'onyx-future-flyout-menu__list': true, | ||
}" | ||
> | ||
<slot name="header"></slot> | ||
<ul | ||
v-if="slots.options" | ||
v-bind="menu" | ||
class="onyx-future-flyout-menu__wrapper onyx-flyout-menu__group" | ||
> | ||
<li | ||
v-for="(item, index) in options" | ||
v-bind="listItem" | ||
:key="index" | ||
class="onyx-future-flyout-menu__option" | ||
> | ||
<component | ||
:is="item" | ||
v-bind=" | ||
item.props?.href | ||
? menuItem({ active: activeItem === item.props.href, value: item.props.href }) | ||
: undefined | ||
" | ||
/> | ||
</li> | ||
</ul> | ||
<slot name="footer"></slot> | ||
</div> | ||
</div> | ||
</template> | ||
|
||
<style lang="scss"> | ||
@use "../../../styles/mixins/layers"; | ||
// TODO: replace 'onyx-future-flyout-menu' class name with 'onyx-flyout-menu' after merging this component | ||
// with the official OnyxFlyoutMenu | ||
.onyx-future-flyout-menu { | ||
@include layers.component() { | ||
// TODO: remove comment after replacing this component with the official OnyxFlyoutMenu | ||
// display: inline-block; | ||
width: min-content; | ||
&__list { | ||
position: absolute; | ||
margin-top: var(--onyx-spacing-sm); | ||
border-radius: var(--onyx-radius-md); | ||
background-color: var(--onyx-color-base-background-blank); | ||
padding: var(--onyx-spacing-2xs) 0; | ||
box-shadow: var(--onyx-shadow-medium-bottom); | ||
box-sizing: border-box; | ||
width: max-content; | ||
min-width: var(--onyx-spacing-4xl); | ||
max-width: 20rem; | ||
font-family: var(--onyx-font-family); | ||
z-index: var(--onyx-z-index-flyout); | ||
&--with-header { | ||
padding-top: 0; | ||
} | ||
&--with-footer { | ||
padding-bottom: 0; | ||
} | ||
} | ||
&__wrapper { | ||
padding: 0; | ||
} | ||
&__option { | ||
list-style: none; | ||
} | ||
} | ||
} | ||
</style> |
3 changes: 3 additions & 0 deletions
3
packages/sit-onyx/src/components/OnyxFlyoutMenu/future/readme.md
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,3 @@ | ||
# OnyxFlyoutMenu | ||
|
||
This is the new (future) version of OnyxFlyoutMenu component. The strucuture of the component is optimized and it will replace the current OnyxFlyoutMenu in the future. |
Oops, something went wrong.