Skip to content

Commit

Permalink
feat: implement OnyxColorSchemeMenuItem (#1465)
Browse files Browse the repository at this point in the history
Implement `OnyxColorSchemeMenuItem` for easily letting the user choose
light/dark/auto mode inside the user menu
  • Loading branch information
larsrickert authored Jul 2, 2024
1 parent 7f3332c commit 90f9f86
Show file tree
Hide file tree
Showing 20 changed files with 197 additions and 54 deletions.
5 changes: 5 additions & 0 deletions .changeset/spotty-worms-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sit-onyx": minor
---

feat: implement OnyxColorSchemeMenuItem
5 changes: 5 additions & 0 deletions .changeset/wicked-sloths-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sit-onyx": patch
---

fix(OnyxMenuItem): make whole button/anchor clickable
16 changes: 2 additions & 14 deletions apps/alpha-test-app/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<script setup lang="ts">
import circleContrast from "@sit-onyx/icons/circle-contrast.svg?raw";
import logout from "@sit-onyx/icons/logout.svg?raw";
import { useColorMode } from "@vueuse/core";
import {
OnyxAppLayout,
OnyxColorSchemeDialog,
OnyxColorSchemeMenuItem,
OnyxIcon,
OnyxListItem,
OnyxNavBar,
Expand All @@ -13,7 +12,6 @@ import {
OnyxUserMenu,
type OnyxNavItemProps,
} from "sit-onyx";
import { ref } 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 +28,6 @@ const navItems = [
] satisfies OnyxNavItemProps[];
const { store: colorScheme } = useColorMode();
const isColorSchemeDialogOpen = ref(false);
</script>

<template>
Expand Down Expand Up @@ -66,10 +63,7 @@ const isColorSchemeDialogOpen = ref(false);

<template #contextArea>
<OnyxUserMenu username="John Doe">
<OnyxListItem @click="isColorSchemeDialogOpen = true">
<OnyxIcon :icon="circleContrast" />
Appearance
</OnyxListItem>
<OnyxColorSchemeMenuItem v-model="colorScheme" />

<OnyxListItem color="danger">
<OnyxIcon :icon="logout" />
Expand All @@ -87,12 +81,6 @@ const isColorSchemeDialogOpen = ref(false);

<RouterView />

<OnyxColorSchemeDialog
v-model="colorScheme"
:open="isColorSchemeDialogOpen"
@close="isColorSchemeDialogOpen = false"
/>

<OnyxToastProvider />
</OnyxAppLayout>
</template>
33 changes: 7 additions & 26 deletions apps/docs/src/development/theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,26 @@ Per default, onyx will be displayed in light mode after the [initial setup](/dev

## Let the user decide

In order to let the user switch between light, dark and auto mode, we recommend to use the [OnyxColorSchemeDialog](https://storybook.onyx.schwarz/?path=/docs/navigation-modules-colorschemedialog--docs) component inside the [nav bar](https://storybook.onyx.schwarz/?path=/story/navigation-navbar--with-context-area) together with the [@vueuse/core](https://vueuse.org/core/useColorMode) library:
In order to let the user switch between light, dark and auto mode, we recommend to use the pre-built [OnyxColorSchemeMenuItem](https://storybook.onyx.schwarz/?path=/docs/navigation-modules-colorschememenuitem--docs) component inside the [nav bar](https://storybook.onyx.schwarz/?path=/story/navigation-navbar--with-context-area) together with the [@vueuse/core](https://vueuse.org/core/useColorMode) library as shown in the example below.

```vue
<script setup lang="ts">
import circleContrast from "@sit-onyx/icons/circle-contrast.svg?raw";
import { useColorMode } from "@vueuse/core";
import { OnyxColorSchemeDialog, OnyxNavBar, OnyxUserMenu, type SelectOption } from "sit-onyx";
import { OnyxNavBar, OnyxUserMenu, OnyxColorSchemeMenuItem } from "sit-onyx";
import { ref } from "vue";
const userMenuOptions = [
{ value: "color-scheme", label: "Appearance", icon: circleContrast },
// your other user menu options
] as const satisfies SelectOption[];
const { store: colorScheme } = useColorMode();
const isColorSchemeDialogOpen = ref(false);
const handleOptionClick = (value: (typeof userMenuOptions)[number]["value"]) => {
if (value === "color-scheme") {
isColorSchemeDialogOpen.value = true;
}
};
</script>
<template>
<OnyxNavBar app-name="Example app">
<template #contextArea>
<OnyxUserMenu
username="John Doe"
:options="userMenuOptions"
@option-click="handleOptionClick"
/>
<OnyxUserMenu username="John Doe">
<OnyxColorSchemeMenuItem v-model="colorScheme" />
</OnyxUserMenu>
</template>
</OnyxNavBar>
<OnyxColorSchemeDialog
v-model="colorScheme"
:open="isColorSchemeDialogOpen"
@close="isColorSchemeDialogOpen = false"
/>
</template>
```

Alternatively, you can use the [OnyxColorSchemeDialog](https://storybook.onyx.schwarz/?path=/docs/navigation-modules-colorschemedialog--docs) component to build your own custom component.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ test.describe("Screenshot tests", () => {
);

if (row === "hover") {
// eslint-disable-next-line playwright/no-force-option -- since the radio button is visually hidden, we need to use force here
await component.getByLabel("Auto").hover({ force: true });
await component.getByRole("heading", { name: "Auto" }).hover();
}
},
});
Expand All @@ -59,8 +58,7 @@ test("should behave correctly", async ({ mount, page }) => {
);

const clickOption = (label: string) => {
// eslint-disable-next-line playwright/no-force-option -- since the radio button is visually hidden, we need to use force here
return component.getByLabel(label).click({ force: true });
return component.getByRole("heading", { name: label }).click();
};

// ASSERT (should be focussed initially)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { expect, test } from "../../playwright/a11y";
import { executeMatrixScreenshotTest } from "../../playwright/screenshots";
import type { ColorSchemeValue } from "../OnyxColorSchemeDialog/types";
import OnyxColorSchemeMenuItem from "./OnyxColorSchemeMenuItem.vue";

test.describe("Screenshot tests", () => {
executeMatrixScreenshotTest({
name: "Color scheme menu item",
columns: ["default"],
rows: ["default", "hover"],
// TODO: remove when contrast issues are fixed in https://github.com/SchwarzIT/onyx/issues/410
disabledAccessibilityRules: ["color-contrast"],
component: () => (
<ul style={{ listStyle: "none", padding: 0 }}>
<OnyxColorSchemeMenuItem modelValue="auto" />
</ul>
),
beforeScreenshot: async (component, page, column, row) => {
if (row === "hover") await component.getByText("Appearance: Auto").hover();
},
});
});

test("should behave correctly", async ({ page, mount }) => {
const modelValueEvents: ColorSchemeValue[] = [];

// ARRANGE
const component = await mount(OnyxColorSchemeMenuItem, {
props: {
modelValue: "auto",
},
on: {
"update:modelValue": (value: ColorSchemeValue) => modelValueEvents.push(value),
},
});

// ASSERT
await expect(component).toContainText("Appearance: Auto");

// ACT
await component.click();

// ASSERT
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();

// ACT
await dialog.getByRole("heading", { name: "Light" }).click();
await dialog.getByRole("button", { name: "Apply" }).click();
await component.update({ props: { modelValue: "light" } });

// ASSERT
await expect(modelValueEvents).toStrictEqual(["light"]);
await expect(component).toContainText("Appearance: Light");
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineStorybookActionsAndVModels } from "@sit-onyx/storybook-utils";
import type { Meta, StoryObj } from "@storybook/vue3";
import OnyxColorSchemeMenuItem from "./OnyxColorSchemeMenuItem.vue";

/**
* Pre-built menu item for the `OnyxUserMenu` that can be used inside the nav bar to
* display the current color scheme to the user and allow changing it by displaying a `OnyxColorSchemeDialog`.
*/
const meta: Meta<typeof OnyxColorSchemeMenuItem> = {
title: "Navigation/modules/ColorSchemeMenuItem",
...defineStorybookActionsAndVModels({
component: OnyxColorSchemeMenuItem,
events: ["update:modelValue"],
decorators: [
(story) => ({
components: { story },
template: `<div style="max-width: 16rem;"> <story /> </div>`,
}),
],
}),
};

export default meta;
type Story = StoryObj<typeof OnyxColorSchemeMenuItem>;

export const Default = {
args: {
modelValue: "auto",
},
} satisfies Story;
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script lang="ts" setup>
import circleContrast from "@sit-onyx/icons/circle-contrast.svg?raw";
import { ref } from "vue";
import { injectI18n } from "../../i18n";
import OnyxColorSchemeDialog from "../OnyxColorSchemeDialog/OnyxColorSchemeDialog.vue";
import type { ColorSchemeValue } from "../OnyxColorSchemeDialog/types";
import OnyxIcon from "../OnyxIcon/OnyxIcon.vue";
import OnyxMenuItem from "../OnyxMenuItem/OnyxMenuItem.vue";
import type { OnyxColorSchemeMenuItemProps } from "./types";
const props = defineProps<OnyxColorSchemeMenuItemProps>();
const emit = defineEmits<{
"update:modelValue": [value: ColorSchemeValue];
}>();
const { t } = injectI18n();
const isOpen = ref(false);
</script>

<template>
<OnyxMenuItem class="onyx-color-scheme-menu-item" @click="isOpen = true">
<OnyxIcon :icon="circleContrast" />

<div>
{{ t("colorScheme.appearance") }}:
<span class="onyx-color-scheme-menu-item__value">
{{ t(`colorScheme.${props.modelValue}.label`) }}
</span>
</div>

<!-- the menu button renders a <li> and <button> so we need to teleport the dialog
to not nest it inside the button -->
<Teleport to="body">
<OnyxColorSchemeDialog
:model-value="props.modelValue"
:open="isOpen"
@close="isOpen = false"
@update:model-value="emit('update:modelValue', $event)"
/>
</Teleport>
</OnyxMenuItem>
</template>

<style lang="scss">
@use "../../styles/mixins/layers.scss";
.onyx-color-scheme-menu-item {
@include layers.component() {
&__value {
color: var(--onyx-color-text-icons-neutral-soft);
}
.onyx-color-scheme-dialog {
text-align: left;
}
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ColorSchemeValue } from "../OnyxColorSchemeDialog/types";

export type OnyxColorSchemeMenuItemProps = {
/**
* Currently active color scheme.
*/
modelValue: ColorSchemeValue;
};
15 changes: 11 additions & 4 deletions packages/sit-onyx/src/components/OnyxMenuItem/OnyxMenuItem.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<script setup lang="ts">
import { inject } from "vue";
import OnyxListItem from "../OnyxListItem/OnyxListItem.vue";
import { MENU_BUTTON_ITEM_INJECTION_KEY } from "./types";
import { type OnyxMenuItemProps } from "./types";
import { MENU_BUTTON_ITEM_INJECTION_KEY, type OnyxMenuItemProps } from "./types";
const props = defineProps<OnyxMenuItemProps>();
const emit = defineEmits<{
/**
* Emitted when the menu item is clicked (via click or keyboard).
Expand All @@ -14,6 +14,7 @@ const emit = defineEmits<{
const menuButton = inject(MENU_BUTTON_ITEM_INJECTION_KEY);
</script>

<template>
<OnyxListItem
:selected="props.active"
Expand All @@ -40,15 +41,20 @@ const menuButton = inject(MENU_BUTTON_ITEM_INJECTION_KEY);
</component>
</OnyxListItem>
</template>
<style lang="scss">
@use "../../styles/mixins/layers";
.onyx-menu-item {
@include layers.component() {
// in order for the full menu item to be clickable, we remove the padding here
// and set it on the anchor/button instead
padding: 0;
&__anchor {
color: inherit;
text-decoration: none;
padding: 0;
padding: var(--onyx-list-item-padding);
&:focus {
outline: none;
Expand All @@ -58,13 +64,14 @@ const menuButton = inject(MENU_BUTTON_ITEM_INJECTION_KEY);
&__button {
background-color: inherit;
color: inherit;
padding: 0;
padding: var(--onyx-list-item-padding);
cursor: pointer;
border: none;
outline: none;
display: flex;
align-items: center;
gap: var(--onyx-spacing-sm);
width: 100%;
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/sit-onyx/src/components/OnyxMenuItem/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { OnyxColor } from "../../types";
import type { createMenuButton } from "@sit-onyx/headless";
import type { InjectionKey } from "vue";
import type { OnyxColor } from "../../types";

export type OnyxMenuItemProps = {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type OnyxNavItemProps } from "./types";
const props = defineProps<OnyxNavItemProps>();
</script>

<template>
<OnyxMenuItem :active="props.active" :href="props.href ?? 'javascript:void(0)'">
<slot></slot>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import logout from "@sit-onyx/icons/logout.svg?raw";
import settings from "@sit-onyx/icons/settings.svg?raw";
import { defineStorybookActionsAndVModels } from "@sit-onyx/storybook-utils";
import type { Meta, StoryObj } from "@storybook/vue3";
import { h } from "vue";
import OnyxColorSchemeMenuItem from "../OnyxColorSchemeMenuItem/OnyxColorSchemeMenuItem.vue";
import OnyxIcon from "../OnyxIcon/OnyxIcon.vue";
import OnyxListItem from "../OnyxListItem/OnyxListItem.vue";
import OnyxUserMenu from "./OnyxUserMenu.vue";
Expand Down Expand Up @@ -40,7 +40,7 @@ export const Default = {
username: "Jane Doe",
description: "Company Name",
default: () => [
h(OnyxListItem, () => [h(OnyxIcon, { icon: settings }), "Settings"]),
h(OnyxColorSchemeMenuItem, { modelValue: "auto" }),
h(OnyxListItem, { color: "danger" }, () => [h(OnyxIcon, { icon: logout }), "Logout"]),
],
footer: () => ["App version", h("span", { class: "onyx-text--monospace" }, "1.0.0")],
Expand Down
Loading

0 comments on commit 90f9f86

Please sign in to comment.