Skip to content
Open
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
10 changes: 10 additions & 0 deletions .changeset/shy-olives-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@sit-onyx/tiptap": minor
---

feat(OnyxTextEditor): use `OnyxFormElementV2` internally

- refactor!: change type for `message` and `success` property
- refactor!: remove `hideLabel` and `labelTooltip` property in favor of `label.hidden` and `label.tooltipText`
- feat: support left aligned label using `label.position`
- feat: support new properties: `loading`, `error` `showError`, `required` and `requiredMarker`
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,13 @@ const popoverLayoutProps = useForwardProps(props, MaybePopoverLayout);
&:not(.onyx-form-element--suppress-invalid) {
&.onyx-form-element--touched-invalid:has(
.onyx-form-element-v2__input:user-invalid,
.onyx-form-element-v2__input--touched:invalid
.onyx-form-element-v2__input--touched:invalid,
.onyx-form-element-v2__input--touched[aria-invalid="true"]
),
&.onyx-form-element--immediate-invalid:has(.onyx-form-element-v2__input:invalid) {
&.onyx-form-element--immediate-invalid:has(
.onyx-form-element-v2__input:invalid,
.onyx-form-element-v2__input[aria-invalid="true"]
) {
--onyx-form-element-v2-border-color: var(--onyx-color-component-border-danger);
--onyx-form-element-v2-border-color-hover: var(--onyx-color-component-border-danger-hover);
--onyx-form-element-v2-border-color-focus: var(--onyx-color-component-border-danger);
Expand Down
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.
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.
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.
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.
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.
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.
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.
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
210 changes: 210 additions & 0 deletions packages/tiptap/src/components/OnyxTextEditor/EditorToolbar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<script lang="ts" setup>
import {
iconAlignmentBlock,
iconAlignmentCenter,
iconAlignmentLeft,
iconAlignmentRight,
iconQuote,
iconRedo,
iconToolBold,
iconToolItalic,
iconToolStrike,
iconToolUnderlined,
iconUndo,
} from "@sit-onyx/icons";
import type { Editor } from "@tiptap/vue-3";
import { injectI18n } from "sit-onyx";
import { computed } from "vue";
import { useEditorUtils } from "../../composables/useEditorUtils.js";
import OnyxEditorToolbarAction from "../OnyxEditorToolbarAction/OnyxEditorToolbarAction.vue";
import OnyxEditorToolbarGroup from "../OnyxEditorToolbarGroup/OnyxEditorToolbarGroup.vue";
import HeadingToolbarAction from "./actions/HeadingToolbarAction.vue";
import LinkToolbarAction from "./actions/LinkToolbarAction.vue";
import ListToolbarAction from "./actions/ListToolbarAction.vue";

const props = defineProps<{
editor?: Editor;
}>();

const slots = defineSlots<{
/**
* Optional slot to add custom actions to the toolbar.
*/
default?(): unknown;
}>();

const { t } = injectI18n();
const { hasExtension, hasTextExtension } = useEditorUtils(computed(() => props.editor));
</script>

<template>
<div class="onyx-text-editor__toolbar">
<div class="onyx-text-editor__actions">
<OnyxEditorToolbarGroup>
<HeadingToolbarAction v-if="hasExtension('heading')" :editor />

<ListToolbarAction
v-if="hasExtension('bulletList') || hasExtension('orderedList')"
:editor
/>
</OnyxEditorToolbarGroup>

<OnyxEditorToolbarGroup>
<OnyxEditorToolbarAction
v-if="hasExtension('bold')"
:label="t('editor.bold')"
:icon="iconToolBold"
:active="editor?.isActive('bold')"
:disabled="!editor?.can().chain().toggleBold().run()"
@click="editor?.chain().focus().toggleBold().run()"
/>
<OnyxEditorToolbarAction
v-if="hasExtension('italic')"
:label="t('editor.italic')"
:icon="iconToolItalic"
:active="editor?.isActive('italic')"
:disabled="!editor?.can().chain().toggleItalic().run()"
@click="editor?.chain().focus().toggleItalic().run()"
/>
<OnyxEditorToolbarAction
v-if="hasExtension('underline')"
:label="t('editor.underline')"
:icon="iconToolUnderlined"
:active="editor?.isActive('underline')"
:disabled="!editor?.can().chain().toggleUnderline().run()"
@click="editor?.chain().focus().toggleUnderline().run()"
/>
<OnyxEditorToolbarAction
v-if="hasExtension('strike')"
:label="t('editor.strike')"
:icon="iconToolStrike"
:active="editor?.isActive('strike')"
:disabled="!editor?.can().chain().toggleStrike().run()"
@click="editor?.chain().focus().toggleStrike().run()"
/>
</OnyxEditorToolbarGroup>

<OnyxEditorToolbarGroup>
<OnyxEditorToolbarAction
v-if="hasTextExtension('left')"
:label="t('editor.alignments.left')"
:icon="iconAlignmentLeft"
:active="editor?.isActive({ textAlign: 'left' })"
:disabled="!editor?.can().chain().toggleTextAlign('left').run()"
@click="editor?.chain().focus().toggleTextAlign('left').run()"
/>
<OnyxEditorToolbarAction
v-if="hasTextExtension('center')"
:label="t('editor.alignments.center')"
:icon="iconAlignmentCenter"
:active="editor?.isActive({ textAlign: 'center' })"
:disabled="!editor?.can().chain().toggleTextAlign('center').run()"
@click="editor?.chain().focus().toggleTextAlign('center').run()"
/>
<OnyxEditorToolbarAction
v-if="hasTextExtension('right')"
:label="t('editor.alignments.right')"
:icon="iconAlignmentRight"
:active="editor?.isActive({ textAlign: 'right' })"
:disabled="!editor?.can().chain().toggleTextAlign('right').run()"
@click="editor?.chain().focus().toggleTextAlign('right').run()"
/>
<OnyxEditorToolbarAction
v-if="hasTextExtension('justify')"
:label="t('editor.alignments.block')"
:icon="iconAlignmentBlock"
:active="editor?.isActive({ textAlign: 'justify' })"
:disabled="!editor?.can().chain().toggleTextAlign('justify').run()"
@click="editor?.chain().focus().toggleTextAlign('justify').run()"
/>
</OnyxEditorToolbarGroup>

<OnyxEditorToolbarGroup>
<LinkToolbarAction v-if="hasExtension('link')" :editor />

<OnyxEditorToolbarAction
v-if="hasExtension('blockquote')"
:label="t('editor.blockquote')"
:icon="iconQuote"
:active="editor?.isActive('blockquote')"
:disabled="!editor?.can().chain().toggleBlockquote().run()"
@click="editor?.chain().focus().toggleBlockquote().run()"
/>
</OnyxEditorToolbarGroup>

<OnyxEditorToolbarGroup v-if="slots.default">
<slot></slot>
</OnyxEditorToolbarGroup>
</div>

<div
v-if="hasExtension('undoRedo')"
class="onyx-text-editor__actions onyx-text-editor__actions--fixed"
>
<OnyxEditorToolbarAction
:label="t('editor.undo')"
:icon="iconUndo"
:disabled="!editor?.can().chain().undo().run()"
@click="editor?.chain().focus().undo().run()"
/>
<OnyxEditorToolbarAction
:label="t('editor.redo')"
:icon="iconRedo"
:disabled="!editor?.can().chain().redo().run()"
@click="editor?.chain().focus().redo().run()"
/>
</div>
</div>
</template>

<style lang="scss">
@use "sit-onyx/src/styles/mixins/layers.scss";
@use "sit-onyx/src/styles/mixins/input.scss";

.onyx-text-editor {
@include layers.component() {
&__toolbar {
border: var(--onyx-form-element-v2-border-size) solid var(--onyx-form-element-v2-border-color);
border-radius: inherit;
color: var(--onyx-color-text-icons-neutral-medium);
background-color: var(--onyx-color-base-background-tinted); // TODO: adjust this in Figma
display: flex;
align-items: center;
justify-content: space-between;
max-width: 100%;
width: 100%;

border-top-left-radius: var(--onyx-text-editor-input-border-radius-bottom);
border-top-right-radius: var(--onyx-text-editor-input-border-radius-bottom);
border-bottom-left-radius: var(--onyx-text-editor-input-border-radius-top);
border-bottom-right-radius: var(--onyx-text-editor-input-border-radius-top);
}

// styles for top toolbar
&:not(&--toolbar-bottom) {
.onyx-text-editor__toolbar {
border-bottom: none;
}
}

// styles for bottom toolbar
&--toolbar-bottom {
.onyx-text-editor__toolbar {
border-top: none;
}
}

&__actions {
display: flex;
align-items: center;
gap: var(--onyx-density-xs);
overflow: auto;
padding: var(--onyx-form-element-v2-padding-block) var(--onyx-form-element-v2-padding-inline);

&--fixed {
overflow: visible;
}
}
}
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,8 @@ test.describe("Screenshot tests (truncation)", () => {
return (
<OnyxTextEditor
style={{ maxWidth: "12.5rem" }}
label={label}
labelTooltip="Label tooltip"
hideLabel={row === "hideLabel"}
message={{ shortMessage: message, longMessage: "Message tooltip" }}
label={{ label, tooltipText: "Label tooltip", hidden: row === "hideLabel" }}
message={{ label: message, tooltipText: "Message tooltip" }}
/>
);
},
Expand All @@ -78,13 +76,14 @@ test.describe("Screenshot tests (truncation)", () => {
test.describe("Screenshot tests (disabled)", () => {
executeMatrixScreenshotTest({
name: "Text editor (disabled)",
columns: ["disabled"],
columns: ["disabled", "loading"],
rows: ["default", "hover", "focus"],
component: (column) => (
<OnyxTextEditor
label="Test label"
disabled={column === "disabled"}
modelValue="Filled value"
loading={column === "loading"}
/>
),
hooks: {
Expand Down Expand Up @@ -241,6 +240,7 @@ test.describe("extensions", () => {
}

await editor.pressSequentially("Plain text");
await page.getByRole("document").click({ position: { x: 0, y: 0 } }); // reset focus

// ASSERT
await expect(component).toHaveScreenshot("headlines.png");
Expand Down Expand Up @@ -310,6 +310,7 @@ test.describe("extensions", () => {
await editor.pressSequentially("Option 2");

// ASSERT
await page.getByRole("document").click({ position: { x: 0, y: 0 } }); // reset focus
await expect(component).toHaveScreenshot("lists.png");

// ACT
Expand Down Expand Up @@ -490,7 +491,7 @@ test.describe("extensions", () => {
});

test.describe("textAlign", () => {
test("should support textAlign", async ({ mount }) => {
test("should support textAlign", async ({ mount, page }) => {
// ARRANGE
const component = await mount(<TestCase label="Test label" />);
const editor = component.getByLabel("Test label");
Expand All @@ -514,6 +515,8 @@ test.describe("extensions", () => {
}
}

await hoverAction(page, "Block aligned");

// ASSERT
await expect(component).toHaveScreenshot("textAlign.png");
});
Expand Down Expand Up @@ -572,7 +575,7 @@ test.describe("extensions", () => {
});

test.describe("link", () => {
test("should support link", async ({ mount }) => {
test("should support link", async ({ mount, page }) => {
// ARRANGE
const component = await mount(<TestCase label="Test label" />);
const editor = component.getByLabel("Test label");
Expand Down Expand Up @@ -628,6 +631,7 @@ test.describe("extensions", () => {
await editor.pressSequentially(" followed by regular text");

// ASSERT
await page.getByRole("document").click({ position: { x: 0, y: 0 } }); // reset focus
await expect(component).toHaveScreenshot("link.png");
});

Expand Down Expand Up @@ -728,6 +732,55 @@ test("should disable all actions if editor is disabled", async ({ mount }) => {
await expect(headingTooltip).toBeHidden();
});

test("should show error", async ({ mount }) => {
// ARRANGE
const component = await mount(OnyxTextEditor, {
props: {
label: "Test label",
required: true,
},
});

const input = component.getByLabel("Test label");
const error = component.locator(".onyx-form-element-v2__message--danger");

// ASSERT
await expect(error).toBeHidden();

// ACT
await component.update({ props: { showError: true } });

// ASSERT
await expect(error, "should show immediately when 'showError' is true").toBeVisible();
await expect(error).toContainText("Required");

// ACT
await component.update({ props: { showError: "touched" } });

// ASSERT
await expect(error).toBeHidden();

// ACT
await input.fill("Filled value");

// ASSERT
await expect(error).toBeHidden();

// ACT
await input.clear();

// ASSERT
await expect(error, "should show error when touched").toBeVisible();
await expect(error).toContainText("Required");

// ACT
await component.update({ props: { error: "Custom error" } });

// ASSERT
await expect(error, "should show custom error").toBeVisible();
await expect(error).toContainText("Custom error");
});

/**
* Expects that the given editor toolbar flyout option is selected.
*/
Expand Down Expand Up @@ -756,8 +809,8 @@ async function expectFlyoutOptionSelected(page: Page, label: string, optionName:
* Useful when capturing screenshots.
*/
async function hoverAction(page: Page, label: string) {
// reset hover
await page.getByRole("document").hover({ position: { x: 0, y: 0 } });
// reset focus
await page.getByRole("document").click({ position: { x: 0, y: 0 } });
await page.getByRole("button", { name: label }).hover();
await expect(page.getByRole("tooltip", { name: label })).toBeVisible();
}
Loading
Loading