Compare commits
1 Commits
tach-typog
...
content-su
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bb8f9c697 |
@@ -29,7 +29,7 @@ export class ContentTextWithSuggestionsComponent {
|
|||||||
[renderLink]="renderLink"
|
[renderLink]="renderLink"
|
||||||
(viewed)="viewed.emit()"
|
(viewed)="viewed.emit()"
|
||||||
(expandedChange)="expandedChange.emit($event)"
|
(expandedChange)="expandedChange.emit($event)"
|
||||||
/>
|
></content-text>
|
||||||
`, isInline: true, dependencies: [{ kind: "component", type: ContentTextComponent, selector: "content-text", inputs: ["className", "weight", "text", "ellipsis", "blur", "style", "onView", "renderMention", "renderTag", "renderLink"], outputs: ["viewed", "expandedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
`, isInline: true, dependencies: [{ kind: "component", type: ContentTextComponent, selector: "content-text", inputs: ["className", "weight", "text", "ellipsis", "blur", "style", "onView", "renderMention", "renderTag", "renderLink"], outputs: ["viewed", "expandedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
||||||
}
|
}
|
||||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentTextWithSuggestionsComponent, decorators: [{
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentTextWithSuggestionsComponent, decorators: [{
|
||||||
@@ -53,7 +53,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
|
|||||||
[renderLink]="renderLink"
|
[renderLink]="renderLink"
|
||||||
(viewed)="viewed.emit()"
|
(viewed)="viewed.emit()"
|
||||||
(expandedChange)="expandedChange.emit($event)"
|
(expandedChange)="expandedChange.emit($event)"
|
||||||
/>
|
></content-text>
|
||||||
`,
|
`,
|
||||||
}]
|
}]
|
||||||
}], propDecorators: { className: [{
|
}], propDecorators: { className: [{
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export class ContentTitleWithSuggestionsComponent {
|
|||||||
[renderLink]="renderLink"
|
[renderLink]="renderLink"
|
||||||
(viewed)="viewed.emit()"
|
(viewed)="viewed.emit()"
|
||||||
(expandedChange)="expandedChange.emit($event)"
|
(expandedChange)="expandedChange.emit($event)"
|
||||||
/>
|
></content-text-with-suggestions>
|
||||||
`, isInline: true, dependencies: [{ kind: "component", type: ContentTextWithSuggestionsComponent, selector: "content-text-with-suggestions", inputs: ["className", "weight", "text", "ellipsis", "blur", "style", "onView", "renderMention", "renderTag", "renderLink"], outputs: ["viewed", "expandedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
`, isInline: true, dependencies: [{ kind: "component", type: ContentTextWithSuggestionsComponent, selector: "content-text-with-suggestions", inputs: ["className", "weight", "text", "ellipsis", "blur", "style", "onView", "renderMention", "renderTag", "renderLink"], outputs: ["viewed", "expandedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
||||||
}
|
}
|
||||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentTitleWithSuggestionsComponent, decorators: [{
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentTitleWithSuggestionsComponent, decorators: [{
|
||||||
@@ -58,7 +58,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
|
|||||||
[renderLink]="renderLink"
|
[renderLink]="renderLink"
|
||||||
(viewed)="viewed.emit()"
|
(viewed)="viewed.emit()"
|
||||||
(expandedChange)="expandedChange.emit($event)"
|
(expandedChange)="expandedChange.emit($event)"
|
||||||
/>
|
></content-text-with-suggestions>
|
||||||
`,
|
`,
|
||||||
}]
|
}]
|
||||||
}], propDecorators: { className: [{
|
}], propDecorators: { className: [{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hublib-web/content-suggestions",
|
"name": "@hublib-web/content-suggestions",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"description": "Content text/title with mentions, tags and links for React and Angular",
|
"description": "Content text/title with mentions, tags and links for React and Angular",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"build:angular": "ngc -p tsconfig.angular.json && node ./scripts/fix-angular-entry.mjs",
|
"build:angular": "ngc -p tsconfig.angular.json && node ./scripts/fix-angular-entry.mjs",
|
||||||
"clean": "rm -rf dist storybook-static",
|
"clean": "rm -rf dist storybook-static",
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||||
"test": "vitest run --passWithNoTests",
|
"test": "yarn run -T vitest run --passWithNoTests",
|
||||||
"lint": "eslint src --ext .ts,.tsx",
|
"lint": "eslint src --ext .ts,.tsx",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"storybook:build": "storybook build"
|
"storybook:build": "storybook build"
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/common": ">=17.0.0",
|
"@angular/common": ">=17.0.0",
|
||||||
"@angular/core": ">=17.0.0",
|
"@angular/core": ">=17.0.0",
|
||||||
"@hublib-web/tach-typography": "0.3.0",
|
"@hublib-web/tach-typography": "0.3.1",
|
||||||
"antd": ">=5.0.0",
|
"antd": ">=5.0.0",
|
||||||
"react": ">=18.0.0",
|
"react": ">=18.0.0",
|
||||||
"react-dom": ">=18.0.0",
|
"react-dom": ">=18.0.0",
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
"@angular/compiler": "^20.3.17",
|
"@angular/compiler": "^20.3.17",
|
||||||
"@angular/compiler-cli": "^20.3.17",
|
"@angular/compiler-cli": "^20.3.17",
|
||||||
"@angular/core": "^20.3.17",
|
"@angular/core": "^20.3.17",
|
||||||
"@hublib-web/tach-typography": "workspace:0.3.0",
|
"@hublib-web/tach-typography": "workspace:0.3.1",
|
||||||
"@storybook/addon-essentials": "8.6.14",
|
"@storybook/addon-essentials": "8.6.14",
|
||||||
"@storybook/react": "8.6.14",
|
"@storybook/react": "8.6.14",
|
||||||
"@storybook/react-vite": "8.6.14",
|
"@storybook/react-vite": "8.6.14",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { ContentTextComponent } from "./content-text.component";
|
|||||||
[renderLink]="renderLink"
|
[renderLink]="renderLink"
|
||||||
(viewed)="viewed.emit()"
|
(viewed)="viewed.emit()"
|
||||||
(expandedChange)="expandedChange.emit($event)"
|
(expandedChange)="expandedChange.emit($event)"
|
||||||
/>
|
></content-text>
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
export class ContentTextWithSuggestionsComponent {
|
export class ContentTextWithSuggestionsComponent {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { ContentTextWithSuggestionsComponent } from "./content-text-with-suggest
|
|||||||
[renderLink]="renderLink"
|
[renderLink]="renderLink"
|
||||||
(viewed)="viewed.emit()"
|
(viewed)="viewed.emit()"
|
||||||
(expandedChange)="expandedChange.emit($event)"
|
(expandedChange)="expandedChange.emit($event)"
|
||||||
/>
|
></content-text-with-suggestions>
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
export class ContentTitleWithSuggestionsComponent {
|
export class ContentTitleWithSuggestionsComponent {
|
||||||
|
|||||||
97
packages/content-suggestions/src/angular/index.test.ts
Normal file
97
packages/content-suggestions/src/angular/index.test.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
|
import "@angular/compiler";
|
||||||
|
|
||||||
|
import { Component, provideZonelessChangeDetection } from "@angular/core";
|
||||||
|
import { TestBed, getTestBed } from "@angular/core/testing";
|
||||||
|
import { BrowserTestingModule, platformBrowserTesting } from "@angular/platform-browser/testing";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ContentTextWithSuggestionsComponent,
|
||||||
|
ContentTitleWithSuggestionsComponent,
|
||||||
|
} from "../../dist/angular/index.js";
|
||||||
|
|
||||||
|
const ensureAngularTestEnvironment = (): void => {
|
||||||
|
try {
|
||||||
|
getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
if (!message.includes("Cannot set base providers")) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ensureAngularTestEnvironment();
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
imports: [ContentTextWithSuggestionsComponent, ContentTitleWithSuggestionsComponent],
|
||||||
|
template: `
|
||||||
|
<content-text-with-suggestions [text]="text"></content-text-with-suggestions>
|
||||||
|
<content-title-with-suggestions [text]="title"></content-title-with-suggestions>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
class ContentSuggestionsHostComponent {
|
||||||
|
text =
|
||||||
|
"Привет @[John Doe](d290f1ee-6c54-4b01-90e6-d701748f0851) #frontend смотри example.com";
|
||||||
|
title = "Заголовок @[Jane Roe](123e4567-e89b-12d3-a456-426614174000) #news docs.example.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("content-suggestions (angular)", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders text with mention, tag and link tokens", async () => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [provideZonelessChangeDetection()],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(ContentSuggestionsHostComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
|
||||||
|
const host: HTMLElement = fixture.nativeElement;
|
||||||
|
const content = host.querySelector("content-text-with-suggestions");
|
||||||
|
expect(content).not.toBeNull();
|
||||||
|
|
||||||
|
const text = content?.textContent ?? "";
|
||||||
|
expect(text).toContain("Привет");
|
||||||
|
expect(text).toContain("@John Doe");
|
||||||
|
expect(text).toContain("#frontend");
|
||||||
|
expect(text).toContain("example.com");
|
||||||
|
|
||||||
|
const link = content?.querySelector('a.tach-typography[href="https://example.com"]');
|
||||||
|
expect(link).not.toBeNull();
|
||||||
|
expect(link?.textContent).toContain("example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders title variant and keeps entities visible", async () => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [provideZonelessChangeDetection()],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(ContentSuggestionsHostComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
|
||||||
|
const host: HTMLElement = fixture.nativeElement;
|
||||||
|
const title = host.querySelector("content-title-with-suggestions");
|
||||||
|
expect(title).not.toBeNull();
|
||||||
|
|
||||||
|
const text = title?.textContent ?? "";
|
||||||
|
expect(text).toContain("Заголовок");
|
||||||
|
expect(text).toContain("@Jane Roe");
|
||||||
|
expect(text).toContain("#news");
|
||||||
|
expect(text).toContain("docs.example.com");
|
||||||
|
|
||||||
|
const boldNode = title?.querySelector(".tach-typography--bold");
|
||||||
|
expect(boldNode).not.toBeNull();
|
||||||
|
|
||||||
|
const link = title?.querySelector('a.tach-typography[href="https://docs.example.com"]');
|
||||||
|
expect(link).not.toBeNull();
|
||||||
|
expect(link?.textContent).toContain("docs.example.com");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import "@angular/compiler";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { createComponent, provideZonelessChangeDetection, type ApplicationRef } from "@angular/core";
|
||||||
|
import { createApplication } from "@angular/platform-browser";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import { expect, waitFor, within } from "@storybook/test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ContentTextWithSuggestionsComponent,
|
||||||
|
ContentTitleWithSuggestionsComponent,
|
||||||
|
} from "../../dist/angular/index.js";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Angular/ContentSuggestions DOM",
|
||||||
|
tags: ["autodocs"],
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
"Angular runtime verification in Storybook: text and title components with mention/tag/link rendered into real DOM.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
type MountedTextComponent = ReturnType<typeof createComponent<ContentTextWithSuggestionsComponent>>;
|
||||||
|
type MountedTitleComponent = ReturnType<typeof createComponent<ContentTitleWithSuggestionsComponent>>;
|
||||||
|
|
||||||
|
const TEXT_SAMPLE =
|
||||||
|
"Angular text: @[Иван](123e4567-e89b-12d3-a456-426614174000) #frontend docs.example.com";
|
||||||
|
const TITLE_SAMPLE =
|
||||||
|
"Angular title: @[Мария](123e4567-e89b-12d3-a456-426614174001) #release github.com";
|
||||||
|
|
||||||
|
const createSection = (title: string): HTMLDivElement => {
|
||||||
|
const section = document.createElement("div");
|
||||||
|
section.style.display = "grid";
|
||||||
|
section.style.gap = "8px";
|
||||||
|
section.style.padding = "10px 12px";
|
||||||
|
section.style.border = "1px solid #e2e8f0";
|
||||||
|
section.style.borderRadius = "10px";
|
||||||
|
|
||||||
|
const heading = document.createElement("div");
|
||||||
|
heading.style.fontFamily =
|
||||||
|
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Courier New", monospace';
|
||||||
|
heading.style.fontSize = "12px";
|
||||||
|
heading.style.color = "#475569";
|
||||||
|
heading.textContent = title;
|
||||||
|
|
||||||
|
const body = document.createElement("div");
|
||||||
|
section.append(heading, body);
|
||||||
|
body.dataset.slot = "body";
|
||||||
|
|
||||||
|
return section;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AngularContentDomHarness: React.FC = () => {
|
||||||
|
const hostRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [status, setStatus] = useState("mounting");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const host = hostRef.current;
|
||||||
|
if (!host) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
let appRef: ApplicationRef | null = null;
|
||||||
|
let textRef: MountedTextComponent | null = null;
|
||||||
|
let titleRef: MountedTitleComponent | null = null;
|
||||||
|
|
||||||
|
host.innerHTML = "";
|
||||||
|
setStatus("mounting");
|
||||||
|
|
||||||
|
void createApplication({
|
||||||
|
providers: [provideZonelessChangeDetection()],
|
||||||
|
})
|
||||||
|
.then(app => {
|
||||||
|
if (cancelled) {
|
||||||
|
app.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
appRef = app;
|
||||||
|
|
||||||
|
const textSection = createSection("ContentTextWithSuggestionsComponent");
|
||||||
|
const titleSection = createSection("ContentTitleWithSuggestionsComponent");
|
||||||
|
textSection.dataset.testid = "angular-content-text";
|
||||||
|
titleSection.dataset.testid = "angular-content-title";
|
||||||
|
host.append(textSection, titleSection);
|
||||||
|
|
||||||
|
const textHost = textSection.querySelector('[data-slot="body"]') as HTMLElement;
|
||||||
|
const titleHost = titleSection.querySelector('[data-slot="body"]') as HTMLElement;
|
||||||
|
|
||||||
|
textRef = createComponent(ContentTextWithSuggestionsComponent, {
|
||||||
|
environmentInjector: app.injector,
|
||||||
|
hostElement: textHost,
|
||||||
|
});
|
||||||
|
app.attachView(textRef.hostView);
|
||||||
|
textRef.setInput("text", TEXT_SAMPLE);
|
||||||
|
textRef.setInput("ellipsis", false);
|
||||||
|
textRef.changeDetectorRef.detectChanges();
|
||||||
|
|
||||||
|
titleRef = createComponent(ContentTitleWithSuggestionsComponent, {
|
||||||
|
environmentInjector: app.injector,
|
||||||
|
hostElement: titleHost,
|
||||||
|
});
|
||||||
|
app.attachView(titleRef.hostView);
|
||||||
|
titleRef.setInput("text", TITLE_SAMPLE);
|
||||||
|
titleRef.setInput("ellipsis", false);
|
||||||
|
titleRef.changeDetectorRef.detectChanges();
|
||||||
|
|
||||||
|
setStatus("ready");
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
setStatus(`error: ${String(error)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
textRef?.destroy();
|
||||||
|
titleRef?.destroy();
|
||||||
|
appRef?.destroy();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ maxWidth: 960, width: "100%", display: "grid", gap: 12 }}
|
||||||
|
data-testid="angular-content-dom-story"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#475569",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
status={status}
|
||||||
|
</div>
|
||||||
|
<div ref={hostRef} style={{ display: "grid", gap: 12 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TextAndTitleRenderCheck: Story = {
|
||||||
|
render: () => <AngularContentDomHarness />,
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(canvas.getByText("status=ready")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(canvas.getByText("Angular text:")).toBeTruthy();
|
||||||
|
expect(canvas.getByText("@Иван")).toBeTruthy();
|
||||||
|
expect(canvas.getByText("#frontend")).toBeTruthy();
|
||||||
|
expect(canvas.getByText("docs.example.com")).toBeTruthy();
|
||||||
|
expect(canvas.getByText("Angular title:")).toBeTruthy();
|
||||||
|
expect(canvas.getByText("@Мария")).toBeTruthy();
|
||||||
|
expect(canvas.getByText("#release")).toBeTruthy();
|
||||||
|
expect(canvas.getByText("github.com")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const docsLink = canvas.getByRole("link", { name: "docs.example.com" });
|
||||||
|
expect(docsLink.getAttribute("href")).toBe("https://docs.example.com");
|
||||||
|
|
||||||
|
const githubLink = canvas.getByRole("link", { name: "github.com" });
|
||||||
|
expect(githubLink.getAttribute("href")).toBe("https://github.com");
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user