Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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
7 changes: 7 additions & 0 deletions .changeset/lucky-moons-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@kaizen/components': minor
---

feat(RichTextEditor): allow schema controls without toolbar buttons via `showInToolbar`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Maybe elaborate here a little bit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by 8949205

Comment and fix generated by Claude Code


Also even out the field border when the toolbar wrapper is empty.
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@

.toolbarWrapper {
border-bottom: inherit;

&:empty {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convention: no new additions to .module.scss files

AGENTS.md §3 and REVIEW.md both state: "Existing SCSS (.module.scss) is legacy and being migrated out — do not add to it."

The fix itself is correct, but per project convention it should live in a .module.css file rather than being appended to the legacy SCSS file. Since no RichTextEditor.module.css exists yet, one option is to create a new .module.css file with just the override:

.toolbarWrapper:empty {
  border-bottom: none;
}

and compose/merge the class names in the component. Alternatively, if the team considers this too heavyweight for a 3-line patch fix, an explicit exception is worth noting in the PR discussion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by 7ea1ddc

Removed the :empty addition from the legacy SCSS and apply toolbarWrapper only when controls are toolbar-visible.

Comment and fix generated by Claude Code

border-bottom: none;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convention violation: AGENTS.md and REVIEW.md prohibit adding new rules to existing .module.scss files — they are legacy and being migrated to .module.css.

The :empty border fix is a valid bug fix, but per repo conventions it should live in a .module.css file. If the RichTextEditor hasn't been migrated yet, consider either:

  1. Creating a RichTextEditor.module.css for the new rule, or
  2. Noting this as tech debt to be addressed in the SCSS-to-CSS migration

Ref: REVIEW.md — "No new .scss — CSS Modules only (.module.css). Existing .module.scss is legacy; do not add to it."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by 7ea1ddc

Same fix as the earlier SCSS convention thread — no new SCSS was kept.

Comment and fix generated by Claude Code

}

/* stylelint-disable no-descending-specificity */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,19 @@ describe('RTE receives list controls', () => {
})
})
})

describe('showInToolbar', () => {
it('omits toolbar buttons for controls with showInToolbar false', () => {
render(
<TestRTE
controls={[
{ name: 'bold', group: 'inline', showInToolbar: false },
{ name: 'italic', group: 'inline' },
]}
/>,
)

expect(screen.queryByRole('button', { name: 'Bold' })).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Italic' })).toBeInTheDocument()
})
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing posture: This test follows the "does the component render X when prop Y is set" pattern, which AGENTS.md §8 and REVIEW.md direct to stickersheet or *.spec.stories.tsx play-function stories instead of unit tests.

Consider moving this assertion to a RichTextEditor.spec.stories.tsx with a play() function, or covering it via the existing SchemaOnlyBold story with Chromatic visual regression.

Ref: REVIEW.md — "Reject 'does the component render X when prop Y is set' as a unit test — that is a stickersheet or spec story job."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by 0ada889

Comment and fix generated by Claude Code

Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ export const RichTextEditor = ({
styles.editor,
styles[`rows${rows}`],
classNameOverride,
controls != null && controls.length > 0 && styles.hasToolbar,
controls != null &&
controls.some((control) => control.showInToolbar !== false) &&

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Make this a variable with a clear name

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by 27831f1

Comment and fix generated by Claude Code

styles.hasToolbar,
)}
{...restProps}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ As the schema is defined by the `controls`, removing an item will remove its fun

<Canvas of={RichTextEditorStories.ControlsWithoutBold} />

To keep a control in the schema (for paste, keyboard shortcuts, or `defaultValue`) without showing its toolbar button, set `showInToolbar: false`.

<Canvas of={RichTextEditorStories.SchemaOnlyBold} />

With all controls, the Kaizen `RichTextEditor` can create and render formatted text, lists, and links.

<Canvas of={RichTextEditorStories.AllAvailableContent} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,38 @@ export const ControlsWithoutBold: Story = {
},
}

export const SchemaOnlyBold: Story = {
args: {
controls: [
{ name: 'bold', group: 'inline', showInToolbar: false },
{ name: 'italic', group: 'inline' },
{ name: 'underline', group: 'inline' },
],
defaultValue: [
{
type: 'paragraph',
content: [
{
type: 'text',
marks: [{ type: 'strong' }],
text: 'Bold text can be pasted or loaded',
},
{
type: 'text',
text: ' without a Bold toolbar button',
},
],
},
],
},
}

export const NoControls: Story = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convention: visual variants belong in a stickersheet

REVIEW.md states: "New or changed visual components must have or extend a *.stickersheet.stories.tsx covering the variants touched by the PR." The NoControls variant is a visual state that should be captured in a RichTextEditor.stickersheet.stories.tsx file (which doesn't yet exist for this component).

The doc story here is fine to keep for documentation, but adding a stickersheet would align with the project's Priority 1 testing convention and provide a dedicated visual-regression anchor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by 5e79797

Comment and fix generated by Claude Code

args: {
controls: undefined,
},
}

export const DefaultValue: Story = {
args: {
defaultValue: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ const getGroupIndex = (
const filterToolbarControls = (groupedControls: GroupedToolbarControls): ToolbarControl[][] =>
Object.values(groupedControls).filter((controls) => controls.length > 0)

const getToolbarVisibleControlNames = (controls: ToolbarItems[]): Set<ToolbarControlTypes> =>
new Set(
controls.filter((control) => control.showInToolbar !== false).map((control) => control.name),
)

/** Builds an array of object used to map control configuration to rte toolbar buttons */
export const useControlMap = (
schema: ProseMirrorModel.Schema,
Expand All @@ -186,8 +191,9 @@ export const useControlMap = (
const controlGroupIndex: ControlGroupTypes = createControlGroupIndex(controls)
const toolbarControls: GroupedToolbarControls = createInitialControls(controlGroupIndex)
const listNodes = [schema.nodes.bulletList, schema.nodes.orderedList]
const toolbarVisibleControls = getToolbarVisibleControlNames(controls)

if (schema.marks.strong) {
if (schema.marks.strong && toolbarVisibleControls.has('bold')) {
const type = schema.marks.strong
const groupIndex = getGroupIndex(controlGroupIndex, 'bold')
toolbarControls[groupIndex].push({
Expand All @@ -202,7 +208,7 @@ export const useControlMap = (
})
}

if (schema.marks.em) {
if (schema.marks.em && toolbarVisibleControls.has('italic')) {
const type = schema.marks.em
const groupIndex = getGroupIndex(controlGroupIndex, 'italic')
toolbarControls[groupIndex].push({
Expand All @@ -217,7 +223,7 @@ export const useControlMap = (
})
}

if (schema.marks.underline) {
if (schema.marks.underline && toolbarVisibleControls.has('underline')) {
const type = schema.marks.underline
const groupIndex = getGroupIndex(controlGroupIndex, 'underline')
toolbarControls[groupIndex].push({
Expand All @@ -232,7 +238,7 @@ export const useControlMap = (
})
}

if (schema.nodes.bulletList) {
if (schema.nodes.bulletList && toolbarVisibleControls.has('bulletList')) {
const type = schema.nodes.bulletList
const groupIndex = getGroupIndex(controlGroupIndex, 'bulletList')
toolbarControls[groupIndex].push({
Expand All @@ -247,7 +253,7 @@ export const useControlMap = (
})
}

if (schema.nodes.orderedList) {
if (schema.nodes.orderedList && toolbarVisibleControls.has('orderedList')) {
const type = schema.nodes.orderedList
const groupIndex = getGroupIndex(controlGroupIndex, 'orderedList')
toolbarControls[groupIndex].push({
Expand All @@ -262,7 +268,10 @@ export const useControlMap = (
})
}

if (schema.nodes.orderedList || schema.nodes.bulletList) {
const hasVisibleListControl =
toolbarVisibleControls.has('orderedList') || toolbarVisibleControls.has('bulletList')

if (hasVisibleListControl && (schema.nodes.orderedList || schema.nodes.bulletList)) {
const groupIndex = controlGroupIndex.orderedList ?? controlGroupIndex.bulletList ?? 'ungrouped'

toolbarControls[groupIndex].push(
Expand Down Expand Up @@ -291,7 +300,7 @@ export const useControlMap = (
)
}

if (schema.marks.link) {
if (schema.marks.link && toolbarVisibleControls.has('link')) {
const type = schema.marks.link
const groupIndex = getGroupIndex(controlGroupIndex, 'link')
toolbarControls[groupIndex].push({
Expand Down
6 changes: 6 additions & 0 deletions packages/components/src/RichTextEditor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export type ToolbarItems = {
* A group is used to wrap items in the same section wrapper
* ungrouped items will be appended to the end of the toolbar */
group?: string
/**
* When false, the control is included in the editor schema
* (paste, keyboard shortcuts, defaultValue) but omitted from the toolbar.
* @default true
*/
showInToolbar?: boolean
}

export type EditorContentArray = Record<string, any>[]
Expand Down
Loading