diff --git a/packages/lexical-playground/src/context/ToolbarContext.tsx b/packages/lexical-playground/src/context/ToolbarContext.tsx
index 921de34eb1b..f8b1c1f082b 100644
--- a/packages/lexical-playground/src/context/ToolbarContext.tsx
+++ b/packages/lexical-playground/src/context/ToolbarContext.tsx
@@ -67,6 +67,7 @@ const INITIAL_TOOLBAR_STATE = {
isUnderline: false,
isLowercase: false,
isUppercase: false,
+ isCapitalize: false,
rootType: 'root' as keyof typeof rootTypeToRootName,
};
diff --git a/packages/lexical-playground/src/images/icons/type-capitalize.svg b/packages/lexical-playground/src/images/icons/type-capitalize.svg
new file mode 100644
index 00000000000..359fcd0707c
--- /dev/null
+++ b/packages/lexical-playground/src/images/icons/type-capitalize.svg
@@ -0,0 +1 @@
+
diff --git a/packages/lexical-playground/src/index.css b/packages/lexical-playground/src/index.css
index 8d7b2c3e353..b57f34b85d5 100644
--- a/packages/lexical-playground/src/index.css
+++ b/packages/lexical-playground/src/index.css
@@ -403,6 +403,10 @@ i.lowercase {
background-image: url(images/icons/type-lowercase.svg);
}
+i.capitalize {
+ background-image: url(images/icons/type-capitalize.svg);
+}
+
i.strikethrough {
background-image: url(images/icons/type-strikethrough.svg);
}
diff --git a/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts
index 10131c8875f..5ea8514e98a 100644
--- a/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts
+++ b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts
@@ -29,6 +29,7 @@ export const SHORTCUTS = Object.freeze({
STRIKETHROUGH: IS_APPLE ? '⌘+Shift+S' : 'Ctrl+Shift+S',
LOWERCASE: IS_APPLE ? '⌘+Shift+1' : 'Ctrl+Shift+1',
UPPERCASE: IS_APPLE ? '⌘+Shift+2' : 'Ctrl+Shift+2',
+ CAPITALIZE: IS_APPLE ? '⌘+Shift+3' : 'Ctrl+Shift+3',
CENTER_ALIGN: IS_APPLE ? '⌘+Shift+E' : 'Ctrl+Shift+E',
JUSTIFY_ALIGN: IS_APPLE ? '⌘+Shift+J' : 'Ctrl+Shift+J',
LEFT_ALIGN: IS_APPLE ? '⌘+Shift+L' : 'Ctrl+Shift+L',
@@ -139,6 +140,16 @@ export function isUppercase(event: KeyboardEvent): boolean {
);
}
+export function isCapitalize(event: KeyboardEvent): boolean {
+ const {code, shiftKey, altKey, metaKey, ctrlKey} = event;
+ return (
+ (code === 'Numpad3' || code === 'Digit3') &&
+ shiftKey &&
+ !altKey &&
+ controlOrMeta(metaKey, ctrlKey)
+ );
+}
+
export function isStrikeThrough(event: KeyboardEvent): boolean {
const {code, shiftKey, altKey, metaKey, ctrlKey} = event;
return (
diff --git a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx
index eeed31b493b..1dd6dc066d4 100644
--- a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx
+++ b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx
@@ -617,6 +617,7 @@ export default function ToolbarPlugin({
);
updateToolbarState('isLowercase', selection.hasFormat('lowercase'));
updateToolbarState('isUppercase', selection.hasFormat('uppercase'));
+ updateToolbarState('isCapitalize', selection.hasFormat('capitalize'));
}
}, [activeEditor, editor, updateToolbarState]);
@@ -920,6 +921,21 @@ export default function ToolbarPlugin({
{SHORTCUTS.UPPERCASE}
+ {
+ activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'capitalize');
+ }}
+ className={
+ 'item wide ' + dropDownActiveClass(toolbarState.isCapitalize)
+ }
+ title="Capitalize"
+ aria-label="Format text to capitalize">
+
+
+ Capitalize
+
+ {SHORTCUTS.CAPITALIZE}
+
{
activeEditor.dispatchCommand(
diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css
index 1f1b9028c11..60fc2a96675 100644
--- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css
+++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css
@@ -83,6 +83,9 @@
.PlaygroundEditorTheme__textUppercase {
text-transform: uppercase;
}
+.PlaygroundEditorTheme__textCapitalize {
+ text-transform: capitalize;
+}
.PlaygroundEditorTheme__hashtag {
background-color: rgba(88, 144, 255, 0.15);
border-bottom: 1px solid rgba(88, 144, 255, 0.3);
diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts
index a766fbcca29..9dfd9e95c29 100644
--- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts
+++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts
@@ -105,6 +105,7 @@ const theme: EditorThemeClasses = {
tableSelection: 'PlaygroundEditorTheme__tableSelection',
text: {
bold: 'PlaygroundEditorTheme__textBold',
+ capitalize: 'PlaygroundEditorTheme__textCapitalize',
code: 'PlaygroundEditorTheme__textCode',
italic: 'PlaygroundEditorTheme__textItalic',
lowercase: 'PlaygroundEditorTheme__textLowercase',
diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts
index 3e18bcef723..7102bd6eeeb 100644
--- a/packages/lexical-rich-text/src/index.ts
+++ b/packages/lexical-rich-text/src/index.ts
@@ -552,7 +552,7 @@ function $isSelectionAtEndOfRoot(selection: RangeSelection) {
}
function $resetCapitalization(selection: RangeSelection): void {
- for (const format of ['lowercase', 'uppercase'] as const) {
+ for (const format of ['lowercase', 'uppercase', 'capitalize'] as const) {
if (selection.hasFormat(format)) {
selection.toggleFormat(format);
}
diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow
index 06069be16fb..cb5d33ef8b7 100644
--- a/packages/lexical/flow/Lexical.js.flow
+++ b/packages/lexical/flow/Lexical.js.flow
@@ -83,6 +83,7 @@ declare export var IS_SUPERSCRIPT: number;
declare export var IS_UNDERLINE: number;
declare export var IS_UPPERCASE: number;
declare export var IS_LOWERCASE: number;
+declare export var IS_CAPITALIZE: number;
declare export var TEXT_TYPE_TO_FORMAT: Record;
/**
@@ -245,8 +246,9 @@ type TextNodeThemeClasses = {
code?: EditorThemeClassName,
subscript?: EditorThemeClassName,
superscript?: EditorThemeClassName,
- uppercase?: EditorThemeClassName,
lowercase?: EditorThemeClassName,
+ uppercase?: EditorThemeClassName,
+ capitalize?: EditorThemeClassName,
};
export type EditorThemeClasses = {
characterLimit?: EditorThemeClassName,
diff --git a/packages/lexical/src/LexicalConstants.ts b/packages/lexical/src/LexicalConstants.ts
index 570d26e5cf4..aead3dbddff 100644
--- a/packages/lexical/src/LexicalConstants.ts
+++ b/packages/lexical/src/LexicalConstants.ts
@@ -46,6 +46,7 @@ export const IS_SUPERSCRIPT = 1 << 6;
export const IS_HIGHLIGHT = 1 << 7;
export const IS_LOWERCASE = 1 << 8;
export const IS_UPPERCASE = 1 << 9;
+export const IS_CAPITALIZE = 1 << 10;
export const IS_ALL_FORMATTING =
IS_BOLD |
@@ -57,7 +58,8 @@ export const IS_ALL_FORMATTING =
IS_SUPERSCRIPT |
IS_HIGHLIGHT |
IS_LOWERCASE |
- IS_UPPERCASE;
+ IS_UPPERCASE |
+ IS_CAPITALIZE;
// Text node details
export const IS_DIRECTIONLESS = 1;
@@ -101,6 +103,7 @@ export const LTR_REGEX = new RegExp('^[^' + RTL + ']*[' + LTR + ']');
export const TEXT_TYPE_TO_FORMAT: Record = {
bold: IS_BOLD,
+ capitalize: IS_CAPITALIZE,
code: IS_CODE,
highlight: IS_HIGHLIGHT,
italic: IS_ITALIC,
diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts
index ee4172f6cfe..4798f4d8118 100644
--- a/packages/lexical/src/LexicalEditor.ts
+++ b/packages/lexical/src/LexicalEditor.ts
@@ -71,6 +71,7 @@ export type TextNodeThemeClasses = {
italic?: EditorThemeClassName;
lowercase?: EditorThemeClassName;
uppercase?: EditorThemeClassName;
+ capitalize: EditorThemeClassName;
strikethrough?: EditorThemeClassName;
subscript?: EditorThemeClassName;
superscript?: EditorThemeClassName;
diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts
index 7d859a1ae2a..d6af132f690 100644
--- a/packages/lexical/src/LexicalUtils.ts
+++ b/packages/lexical/src/LexicalUtils.ts
@@ -228,8 +228,13 @@ export function toggleTextFormatType(
newFormat &= ~TEXT_TYPE_TO_FORMAT.subscript;
} else if (type === 'lowercase') {
newFormat &= ~TEXT_TYPE_TO_FORMAT.uppercase;
+ newFormat &= ~TEXT_TYPE_TO_FORMAT.capitalize;
} else if (type === 'uppercase') {
newFormat &= ~TEXT_TYPE_TO_FORMAT.lowercase;
+ newFormat &= ~TEXT_TYPE_TO_FORMAT.capitalize;
+ } else if (type === 'capitalize') {
+ newFormat &= ~TEXT_TYPE_TO_FORMAT.lowercase;
+ newFormat &= ~TEXT_TYPE_TO_FORMAT.uppercase;
}
return newFormat;
}
diff --git a/packages/lexical/src/nodes/LexicalTextNode.ts b/packages/lexical/src/nodes/LexicalTextNode.ts
index 344117500e0..e31b845c738 100644
--- a/packages/lexical/src/nodes/LexicalTextNode.ts
+++ b/packages/lexical/src/nodes/LexicalTextNode.ts
@@ -92,7 +92,8 @@ export type TextFormatType =
| 'subscript'
| 'superscript'
| 'lowercase'
- | 'uppercase';
+ | 'uppercase'
+ | 'capitalize';
export type TextModeType = 'normal' | 'token' | 'segmented';
diff --git a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx
index 44c8e4729b6..bdf90b63972 100644
--- a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx
+++ b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx
@@ -32,6 +32,7 @@ import {
} from '../../../__tests__/utils';
import {
IS_BOLD,
+ IS_CAPITALIZE,
IS_CODE,
IS_HIGHLIGHT,
IS_ITALIC,
@@ -53,6 +54,7 @@ const editorConfig = Object.freeze({
theme: {
text: {
bold: 'my-bold-class',
+ capitalize: 'my-capitalize-class',
code: 'my-code-class',
highlight: 'my-highlight-class',
italic: 'my-italic-class',
@@ -216,6 +218,7 @@ describe('LexicalTextNode tests', () => {
['highlight', IS_HIGHLIGHT],
['lowercase', IS_LOWERCASE],
['uppercase', IS_UPPERCASE],
+ ['capitalize', IS_CAPITALIZE],
] as const)('%s flag', (formatFlag: TextFormatType, stateFormat: number) => {
const flagPredicate = (node: TextNode) => node.hasFormat(formatFlag);
const flagToggle = (node: TextNode) => node.toggleFormat(formatFlag);
@@ -272,43 +275,54 @@ describe('LexicalTextNode tests', () => {
});
});
- test.each([
- ['subscript', 'superscript'],
- ['superscript', 'subscript'],
- ['lowercase', 'uppercase'],
- ['uppercase', 'lowercase'],
- ])('setting %s clears %s', async (newFormat, otherFormat) => {
+ test('setting subscript clears superscript', async () => {
await update(() => {
const paragraphNode = $createParagraphNode();
const textNode = $createTextNode('Hello World');
paragraphNode.append(textNode);
$getRoot().append(paragraphNode);
-
- textNode.toggleFormat(otherFormat as TextFormatType);
- textNode.toggleFormat(newFormat as TextFormatType);
-
- expect(textNode.hasFormat(newFormat as TextFormatType)).toBe(true);
- expect(textNode.hasFormat(otherFormat as TextFormatType)).toBe(false);
+ textNode.toggleFormat('superscript');
+ textNode.toggleFormat('subscript');
+ expect(textNode.hasFormat('subscript')).toBe(true);
+ expect(textNode.hasFormat('superscript')).toBe(false);
});
});
- test.each([
- ['subscript', 'superscript'],
- ['superscript', 'subscript'],
- ['lowercase', 'uppercase'],
- ['uppercase', 'lowercase'],
- ])('clearing %s does not set %s', async (formatToClear, otherFormat) => {
+ test('setting superscript clears subscript', async () => {
await update(() => {
const paragraphNode = $createParagraphNode();
const textNode = $createTextNode('Hello World');
paragraphNode.append(textNode);
$getRoot().append(paragraphNode);
+ textNode.toggleFormat('subscript');
+ textNode.toggleFormat('superscript');
+ expect(textNode.hasFormat('superscript')).toBe(true);
+ expect(textNode.hasFormat('subscript')).toBe(false);
+ });
+ });
- textNode.toggleFormat(formatToClear as TextFormatType);
- textNode.toggleFormat(formatToClear as TextFormatType);
+ test('capitalization formats are mutually exclusive', async () => {
+ const capitalizationFormats: TextFormatType[] = [
+ 'lowercase',
+ 'uppercase',
+ 'capitalize',
+ ];
- expect(textNode.hasFormat(formatToClear as TextFormatType)).toBe(false);
- expect(textNode.hasFormat(otherFormat as TextFormatType)).toBe(false);
+ await update(() => {
+ const paragraphNode = $createParagraphNode();
+ const textNode = $createTextNode('Hello World');
+ paragraphNode.append(textNode);
+ $getRoot().append(paragraphNode);
+
+ capitalizationFormats.forEach((formatToSet) => {
+ textNode.toggleFormat(formatToSet as TextFormatType);
+ capitalizationFormats
+ .filter((format) => format !== formatToSet)
+ .forEach((format) =>
+ expect(textNode.hasFormat(format as TextFormatType)).toBe(false),
+ );
+ expect(textNode.hasFormat(formatToSet as TextFormatType)).toBe(true);
+ });
});
});
@@ -642,6 +656,12 @@ describe('LexicalTextNode tests', () => {
'My text node',
'My text node',
],
+ [
+ 'capitalize',
+ IS_CAPITALIZE,
+ 'My text node',
+ 'My text node',
+ ],
[
'underline + strikethrough',
IS_UNDERLINE | IS_STRIKETHROUGH,