🐛 Fix text style change not being applied (#7036)

* 🐛 Fix text styles not being applied to current cursor

* 🔧 Add text file for bug 11552

* 📚 Update changelog
This commit is contained in:
Belén Albeza
2025-08-05 13:35:54 +02:00
committed by GitHub
parent 7efc297cd9
commit d5abf34538
9 changed files with 431 additions and 22 deletions

View File

@@ -14,13 +14,14 @@
- Continuous display of distances between elements when moving a layer with the keyboard [Taiga #1780](https://tree.taiga.io/project/penpot/issue/1780) - Continuous display of distances between elements when moving a layer with the keyboard [Taiga #1780](https://tree.taiga.io/project/penpot/issue/1780)
### :bug: Bugs fixed ### :bug: Bugs fixed
- Display strokes information in inspect tab [Taiga #11154](https://tree.taiga.io/project/penpot/issue/11154)
- Display strokes information in inspect tab [Taiga #11154](https://tree.taiga.io/project/penpot/issue/11154)
- Fix problem with booleans selection [Taiga #11627](https://tree.taiga.io/project/penpot/issue/11627) - Fix problem with booleans selection [Taiga #11627](https://tree.taiga.io/project/penpot/issue/11627)
- Fix missing font when copy&paste a chunk of text [Taiga #11522](https://tree.taiga.io/project/penpot/issue/11522) - Fix missing font when copy&paste a chunk of text [Taiga #11522](https://tree.taiga.io/project/penpot/issue/11522)
- Fix bad swap slot after two swaps [Taiga #11659](https://tree.taiga.io/project/penpot/issue/11659) - Fix bad swap slot after two swaps [Taiga #11659](https://tree.taiga.io/project/penpot/issue/11659)
- Fix missing package for the penport_exporter Docker image [GitHub #7205](https://github.com/penpot/penpot/issues/7025) - Fix missing package for the `penpot_exporter` Docker image [GitHub #7205](https://github.com/penpot/penpot/issues/7025)
- Fix issue where multiple dropdown menus could be opened simultaneously on the dashboard page [Taiga #11500](https://tree.taiga.io/project/penpot/issue/11500) - Fix issue where multiple dropdown menus could be opened simultaneously on the dashboard page [Taiga #11500](https://tree.taiga.io/project/penpot/issue/11500)
- Fix font size/variant not updated when editing a text [Taiga #11552](https://tree.taiga.io/project/penpot/issue/11552)
## 2.9.0 (Unreleased) ## 2.9.0 (Unreleased)

View File

@@ -0,0 +1,349 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type",
"text-editor/v2"
]
},
"~:team-id": "~u9e6e22b2-db76-81d6-8006-75d7cdbb8bad",
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "Bug 11552",
"~:revn": 3,
"~:modified-at": "~m1753957736516",
"~:vern": 0,
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2230",
"~:is-shared": false,
"~:migrations": {
"~#ordered-set": [
"legacy-2",
"legacy-3",
"legacy-5",
"legacy-6",
"legacy-7",
"legacy-8",
"legacy-9",
"legacy-10",
"legacy-11",
"legacy-12",
"legacy-13",
"legacy-14",
"legacy-16",
"legacy-17",
"legacy-18",
"legacy-19",
"legacy-25",
"legacy-26",
"legacy-27",
"legacy-28",
"legacy-29",
"legacy-31",
"legacy-32",
"legacy-33",
"legacy-34",
"legacy-36",
"legacy-37",
"legacy-38",
"legacy-39",
"legacy-40",
"legacy-41",
"legacy-42",
"legacy-43",
"legacy-44",
"legacy-45",
"legacy-46",
"legacy-47",
"legacy-48",
"legacy-49",
"legacy-50",
"legacy-51",
"legacy-52",
"legacy-53",
"legacy-54",
"legacy-55",
"legacy-56",
"legacy-57",
"legacy-59",
"legacy-62",
"legacy-65",
"legacy-66",
"legacy-67",
"0001-remove-tokens-from-groups",
"0002-normalize-bool-content-v2",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content-v2",
"0004-clean-shadow-color",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0007-clear-invalid-strokes-and-fills-v2",
"0008-fix-library-colors-v4",
"0009-clean-library-colors",
"0009-add-partial-text-touched-flags"
]
},
"~:version": 67,
"~:project-id": "~u9e6e22b2-db76-81d6-8006-75d7cdc30669",
"~:created-at": "~m1753957644225",
"~:data": {
"~:pages": [
"~u238a17e0-75ff-8075-8006-934586ea2231"
],
"~:pages-index": {
"~u238a17e0-75ff-8075-8006-934586ea2231": {
"~:objects": {
"~u00000000-0000-0000-0000-000000000000": {
"~#shape": {
"~:y": 0,
"~:hide-fill-on-export": false,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:name": "Root Frame",
"~:width": 0.01,
"~:type": "~:frame",
"~:points": [
{
"~#point": {
"~:x": 0.0,
"~:y": 0.0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0.0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0.01
}
},
{
"~#point": {
"~:x": 0.0,
"~:y": 0.01
}
}
],
"~:r2": 0,
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:r3": 0,
"~:r1": 0,
"~:id": "~u00000000-0000-0000-0000-000000000000",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 0,
"~:proportion": 1.0,
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": 0,
"~:y": 0,
"~:width": 0.01,
"~:height": 0.01,
"~:x1": 0,
"~:y1": 0,
"~:x2": 0.01,
"~:y2": 0.01
}
},
"~:fills": [
{
"~:fill-color": "#FFFFFF",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 0.01,
"~:flip-y": null,
"~:shapes": [
"~ucc6f0580-449c-8019-8006-9345db077fa0"
]
}
},
"~ucc6f0580-449c-8019-8006-9345db077fa0": {
"~#shape": {
"~:y": 438,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:grow-type": "~:auto-width",
"~:content": {
"~:type": "root",
"~:key": "1s4am1jl24s",
"~:children": [
{
"~:type": "paragraph-set",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:font-id": "sourcesanspro",
"~:key": "13p0zwl2yhc",
"~:font-size": "14",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro",
"~:text": "Lorem ipsum"
}
],
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:key": "20hf3kmyoub",
"~:font-size": "14",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:text-direction": "ltr",
"~:type": "paragraph",
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro"
}
]
}
],
"~:vertical-align": "top"
},
"~:hide-in-viewer": false,
"~:name": "Lorem ipsum",
"~:width": 77,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": 404,
"~:y": 438
}
},
{
"~#point": {
"~:x": 481,
"~:y": 438
}
},
{
"~#point": {
"~:x": 481,
"~:y": 455
}
},
{
"~#point": {
"~:x": 404,
"~:y": 455
}
}
],
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:id": "~ucc6f0580-449c-8019-8006-9345db077fa0",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:x": 404,
"~:selrect": {
"~#rect": {
"~:x": 404,
"~:y": 438,
"~:width": 77,
"~:height": 17,
"~:x1": 404,
"~:y1": 438,
"~:x2": 481,
"~:y2": 455
}
},
"~:flip-x": null,
"~:height": 17,
"~:flip-y": null
}
}
},
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2231",
"~:name": "Page 1"
}
},
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2230",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -0,0 +1,5 @@
{
"~:revn": 2,
"~:lagged": []
}

View File

@@ -318,4 +318,4 @@ export class WorkspacePage extends BaseWebSocketPage {
} }
} }
export default WorkspacePage; export default WorkspacePage;

View File

@@ -0,0 +1,37 @@
import { test, expect } from "@playwright/test";
import { WorkspacePage } from "../pages/WorkspacePage";
test.beforeEach(async ({ page }) => {
await WorkspacePage.init(page);
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
});
test("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
const workspace = new WorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-11552.json");
await workspace.mockRPC(
"update-file?id=*",
"text-editor/update-file-11552.json",
);
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.clickLeafLayer("Lorem ipsum");
const fontSizeInput = workspace.rightSidebar.getByRole("textbox", {
name: "Font Size",
});
await expect(fontSizeInput).toBeVisible();
await workspace.page.keyboard.press("Enter");
await workspace.page.keyboard.press("ArrowRight");
await fontSizeInput.fill("36");
await workspace.clickLeafLayer("Lorem ipsum");
// display Mixed placeholder
await expect(fontSizeInput).toHaveValue("");
await expect(fontSizeInput).toHaveAttribute("placeholder", "Mixed");
});

View File

@@ -148,7 +148,7 @@
(when (some? instance) (when (some? instance)
(st/emit! (dwt/focus-editor))) (st/emit! (dwt/focus-editor)))
;; This function is called when the component is unmount ;; This function is called when the component is unmounted
(fn [] (fn []
(.removeEventListener ^js global/document "keyup" on-key-up) (.removeEventListener ^js global/document "keyup" on-key-up)
(.removeEventListener ^js instance "blur" on-blur) (.removeEventListener ^js instance "blur" on-blur)

View File

@@ -111,9 +111,11 @@ export function createInline(textOrLineBreak, styles, attrs) {
) { ) {
throw new TypeError("Invalid inline child"); throw new TypeError("Invalid inline child");
} }
if (textOrLineBreak instanceof Text if (
&& textOrLineBreak.nodeValue.length === 0) { textOrLineBreak instanceof Text &&
console.trace("nodeValue", textOrLineBreak.nodeValue) textOrLineBreak.nodeValue.length === 0
) {
console.trace("nodeValue", textOrLineBreak.nodeValue);
throw new TypeError("Invalid inline child, cannot be an empty text"); throw new TypeError("Invalid inline child, cannot be an empty text");
} }
return createElement(TAG, { return createElement(TAG, {
@@ -139,7 +141,7 @@ export function createInlineFrom(inline, textOrLineBreak, styles, attrs) {
return createInline( return createInline(
textOrLineBreak, textOrLineBreak,
mergeStyles(STYLES, inline.style, styles), mergeStyles(STYLES, inline.style, styles),
attrs attrs,
); );
} }
@@ -153,6 +155,16 @@ export function createEmptyInline(styles) {
return createInline(createLineBreak(), styles); return createInline(createLineBreak(), styles);
} }
export function createVoidInline(styles) {
return createElement(TAG, {
attributes: { id: createRandomId() },
data: { itype: TYPE },
styles: styles,
allowedStyles: STYLES,
children: new Text(""),
});
}
/** /**
* Sets the inline styles. * Sets the inline styles.
* *

View File

@@ -45,7 +45,7 @@ export const STYLES = [
["text-decoration"], ["text-decoration"],
["text-transform"], ["text-transform"],
["text-align"], ["text-align"],
["direction"] ["direction"],
]; ];
/** /**
@@ -56,13 +56,11 @@ export const STYLES = [
* @param {*} node * @param {*} node
*/ */
export function fixParagraph(node) { export function fixParagraph(node) {
if (!isParagraph(node) || !isLineBreak(node.firstChild)) { if (!isParagraph(node) || !isLineBreak(node.firstChild)) {
return; return;
} }
const br = createLineBreak(); const br = createLineBreak();
node.replaceChildren( node.replaceChildren(createInline(br));
createInline(br)
);
return br; return br;
} }
@@ -132,9 +130,7 @@ export function createParagraph(inlines, styles, attrs) {
* @returns {HTMLDivElement} * @returns {HTMLDivElement}
*/ */
export function createEmptyParagraph(styles) { export function createEmptyParagraph(styles) {
return createParagraph([ return createParagraph([createEmptyInline(styles)], styles);
createEmptyInline(styles)
], styles);
} }
/** /**
@@ -157,8 +153,7 @@ export function setParagraphStyles(element, styles) {
export function getParagraph(node) { export function getParagraph(node) {
if (!node) return null; if (!node) return null;
if (isParagraph(node)) return node; if (isParagraph(node)) return node;
if (node.nodeType === Node.TEXT_NODE if (node.nodeType === Node.TEXT_NODE || isLineBreak(node)) {
|| isLineBreak(node)) {
const paragraph = node?.parentElement?.parentElement; const paragraph = node?.parentElement?.parentElement;
if (!paragraph) { if (!paragraph) {
return null; return null;

View File

@@ -18,6 +18,7 @@ import {
setInlineStyles, setInlineStyles,
splitInline, splitInline,
createEmptyInline, createEmptyInline,
createVoidInline,
} from "../content/dom/Inline.js"; } from "../content/dom/Inline.js";
import { import {
createEmptyParagraph, createEmptyParagraph,
@@ -1697,7 +1698,7 @@ export class SelectionController extends EventTarget {
// node, then we can apply styles directly to that // node, then we can apply styles directly to that
// node. // node.
if (startNode === endNode && startNode.nodeType === Node.TEXT_NODE) { if (startNode === endNode && startNode.nodeType === Node.TEXT_NODE) {
// The styles are applied to the node completelly. // The styles are applied to the node completely.
if (startOffset === 0 && endOffset === endNode.nodeValue.length) { if (startOffset === 0 && endOffset === endNode.nodeValue.length) {
const paragraph = this.startParagraph; const paragraph = this.startParagraph;
const inline = this.startInline; const inline = this.startInline;
@@ -1728,9 +1729,18 @@ export class SelectionController extends EventTarget {
// FIXME: This can change focus <-> anchor order. // FIXME: This can change focus <-> anchor order.
this.setSelection(midText, 0, midText, midText.nodeValue.length); this.setSelection(midText, 0, midText, midText.nodeValue.length);
}
// The styles are applied to the paragraph. // the styles are applied to the current caret
} else { else if (
this.startOffset === this.endOffset &&
this.endOffset === endNode.nodeValue.length
) {
const newInline = createVoidInline(newStyles);
this.endInline.after(newInline);
this.setSelection(newInline.firstChild, 0, newInline.firstChild, 0);
}
// The styles are applied to the paragraph
else {
const paragraph = this.startParagraph; const paragraph = this.startParagraph;
setParagraphStyles(paragraph, newStyles); setParagraphStyles(paragraph, newStyles);
} }