Skip to content
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
12 changes: 12 additions & 0 deletions .changeset/change-history-label-and-scrollbar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@doc-kit/generator-react': patch
---

Fix `[object Object]` in ChangeHistory aria-label and dropdown horizontal scrollbar

- Change history labels were passing a JSX AST object instead of a plain text
string to the `ChangeHistory` component, causing `aria-label` to render as
`[object Object]`. Labels are now extracted as plain text via `remark-parse`.
- The ChangeHistory dropdown could show a horizontal scrollbar when label text
overflowed the fixed-width container. Added `overflow-wrap` and `word-break`
rules to prevent this.
6 changes: 6 additions & 0 deletions packages/react/src/html/ui/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ main {

div[role='menu'] {
left: 0;

/* Prevent long labels from overflowing dropdown width */
a[role='menuitem'] div {
overflow-wrap: anywhere;
word-break: break-word;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { describe, it } from 'node:test';

import { setConfig } from '@doc-kit/core/utils/configuration/index.mjs';

import { transformHeadingNode } from '../buildContent.mjs';
import { transformHeadingNode, gatherChangeEntries } from '../buildContent.mjs';

const heading = {
type: 'heading',
Expand Down Expand Up @@ -67,3 +67,78 @@ describe('transformHeadingNode (deprecation Type -> AlertBox level)', () => {
assert.equal(levelAttr.value, 'danger');
});
});

describe('gatherChangeEntries', () => {
it('returns empty array when entry has no changes', () => {
assert.deepEqual(gatherChangeEntries({}), []);
});

it('collects lifecycle changes with formatted labels', () => {
const result = gatherChangeEntries({
added: ['v20.0.0', 'v18.0.0'],
deprecated: 'v22.0.0',
});

assert.equal(result.length, 2);
assert.deepEqual(result[0], {
versions: ['v20.0.0', 'v18.0.0'],
label: 'Added in: v20.0.0, v18.0.0',
});
assert.deepEqual(result[1], {
versions: ['v22.0.0'],
label: 'Deprecated in: v22.0.0',
});
});

it('extracts plain text labels from markdown descriptions', () => {
const result = gatherChangeEntries({
changes: [
{
version: 'v25.0.0',
description:
'Add `modifyPrototype` option to conditionally modify the prototype.',
'pr-url': 'https://github.com/nodejs/node/pull/123',
},
],
});

assert.equal(result.length, 1);
assert.equal(
result[0].label,
'Add `modifyPrototype` option to conditionally modify the prototype.'
);
assert.equal(result[0].url, 'https://github.com/nodejs/node/pull/123');
assert.deepEqual(result[0].versions, ['v25.0.0']);
});

it('produces a string label, not an object (regression for [object Object])', () => {
const result = gatherChangeEntries({
changes: [
{
version: 'v1.0.0',
description: 'Some **bold** and _italic_ text.',
},
],
});

assert.equal(typeof result[0].label, 'string');
assert.equal(result[0].label, 'Some **bold** and _italic_ text.');
});

it('combines lifecycle changes and explicit changes', () => {
const result = gatherChangeEntries({
added: 'v20.0.0',
changes: [
{
version: 'v21.0.0',
description: 'Added new feature.',
'pr-url': 'https://example.com/pr/1',
},
],
});

assert.equal(result.length, 2);
assert.equal(result[0].label, 'Added in: v20.0.0');
assert.equal(result[1].label, 'Added new feature.');
});
});
20 changes: 17 additions & 3 deletions packages/react/src/jsx-ast/utils/buildContent.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import {
} from '@doc-kit/core/utils/configuration/templates.mjs';
import { omitKeys } from '@doc-kit/core/utils/misc.mjs';
import { UNIST } from '@doc-kit/core/utils/queries/index.mjs';
import { transformNodesToString } from '@doc-kit/core/utils/unist.mjs';
import { h as createElement } from 'hastscript';
import { slice } from 'mdast-util-slice-markdown';
import readingTime from 'reading-time';
import remarkParse from 'remark-parse';
import { unified } from 'unified';
import { u as createTree } from 'unist-builder';
import { SKIP, visit } from 'unist-util-visit';

Expand All @@ -35,6 +38,18 @@ import {
getFullName,
} from './signature.mjs';

/**
* Converts a markdown string to plain text by parsing it and extracting
* text and inline code values.
*
* @param {string} markdown - The markdown string to convert.
* @returns {string} The plain text representation.
*/
const toPlainText = markdown =>
transformNodesToString(
unified().use(remarkParse).parse(markdown).children
).trim();

/**
* Processes lifecycle and change history data into a sorted array of change entries.
* @param {import('@doc-kit/core/generators/metadata/types').MetadataEntry} entry - The metadata entry
Expand All @@ -48,11 +63,10 @@ export const gatherChangeEntries = entry => {
label: `${label}: ${enforceArray(entry[field]).join(', ')}`,
}));

// Explicit changes with parsed JSX labels
// Explicit changes with plain-text labels extracted from markdown
const explicitChanges = (entry.changes || []).map(change => ({
versions: enforceArray(change.version),
label: remark().runSync(remark().parse(change.description)).body[0]
.expression,
label: toPlainText(change.description),
url: change['pr-url'],
}));

Expand Down
Loading