Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement OnyxColorSchemeMenuItem #1465

Merged
merged 16 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
larsrickert marked this conversation as resolved.
Show resolved Hide resolved
</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",
larsrickert marked this conversation as resolved.
Show resolved Hide resolved
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>`,
}),
],
}),
};

larsrickert marked this conversation as resolved.
Show resolved Hide resolved
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";

larsrickert marked this conversation as resolved.
Show resolved Hide resolved
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
Loading