1 Commits

Author SHA1 Message Date
2bb8f9c697 release(content-suggestions): v0.2.1 2026-04-10 18:01:53 +03:00
7 changed files with 284 additions and 10 deletions

View File

@@ -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: [{

View File

@@ -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: [{

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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 {

View 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");
});
});

View File

@@ -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");
},
};