From 3e690ada80d5a59ba2192b194409c77c8022d490 Mon Sep 17 00:00:00 2001 From: edleeks87 Date: Thu, 24 Aug 2023 14:36:20 +0100 Subject: [PATCH 1/2] feat(character-count): increase font size to 14px and update styles to use relevant tokens --- .../character-count/character-count.spec.tsx | 8 ++++---- .../character-count/character-count.style.ts | 12 ++++-------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/__internal__/character-count/character-count.spec.tsx b/src/__internal__/character-count/character-count.spec.tsx index c1a7f01738..9519e3eb1c 100644 --- a/src/__internal__/character-count/character-count.spec.tsx +++ b/src/__internal__/character-count/character-count.spec.tsx @@ -22,9 +22,9 @@ describe("CharacterCount", () => { assertStyleMatch( { textAlign: "left", - fontSize: "12px", - marginTop: "4px", - marginBottom: "4px", + fontSize: "var(--fontSizes100)", + marginTop: "var(--spacing050)", + marginBottom: "var(--spacing050)", color: "var(--colorsUtilityYin055)", }, wrapper.find(StyledCharacterCount) @@ -99,7 +99,7 @@ describe("CharacterCount", () => { wrapper.setProps({ isOverLimit: true }); assertStyleMatch( { - fontWeight: "700", + fontWeight: "var(--fontWeights700)", color: "var(--colorsSemanticNegative500)", }, wrapper.find(StyledCharacterCount) diff --git a/src/__internal__/character-count/character-count.style.ts b/src/__internal__/character-count/character-count.style.ts index 0a7a29ca7c..5789bde57d 100644 --- a/src/__internal__/character-count/character-count.style.ts +++ b/src/__internal__/character-count/character-count.style.ts @@ -6,14 +6,10 @@ import visuallyHidden from "../../style/utils/visually-hidden"; const StyledCharacterCountWrapper = styled.div``; const StyledCharacterCount = styled.div<{ isOverLimit: boolean }>` - ::after { - content: " "; - } - text-align: left; - font-size: 12px; - margin-top: 4px; - margin-bottom: 4px; + font-size: var(--fontSizes100); + margin-top: var(--spacing050); + margin-bottom: var(--spacing050); color: ${({ isOverLimit }) => isOverLimit ? "var(--colorsSemanticNegative500)" @@ -22,7 +18,7 @@ const StyledCharacterCount = styled.div<{ isOverLimit: boolean }>` ${({ isOverLimit }) => isOverLimit && css` - font-weight: 700; + font-weight: var(--fontWeights700); `} `; From 91bb986f0714a6e1267ab558469578ee2b210554 Mon Sep 17 00:00:00 2001 From: edleeks87 Date: Thu, 24 Aug 2023 17:01:05 +0100 Subject: [PATCH 2/2] feat(text-editor): add support for new validation designs --- .../components/text-editor/text-editor.cy.tsx | 12 +- cypress/locators/text-editor/index.js | 5 +- cypress/locators/text-editor/locators.js | 1 - .../editor-counter.component.tsx | 40 ---- .../editor-counter/editor-counter.spec.tsx | 76 ------- .../editor-counter/editor-counter.style.ts | 22 -- .../__internal__/editor-counter/index.ts | 2 - .../editor-validation-wrapper.component.tsx | 31 +++ .../editor-validation-wrapper.spec.tsx | 42 ++++ .../editor-validation-wrapper.style.ts | 13 ++ .../editor-validation-wrapper/index.ts | 2 + .../text-editor/text-editor.component.tsx | 195 ++++++++++++------ .../text-editor/text-editor.spec.tsx | 164 ++++++++++----- .../text-editor/text-editor.stories.mdx | 4 + .../text-editor/text-editor.stories.tsx | 76 ++++--- 15 files changed, 387 insertions(+), 298 deletions(-) delete mode 100644 src/components/text-editor/__internal__/editor-counter/editor-counter.component.tsx delete mode 100644 src/components/text-editor/__internal__/editor-counter/editor-counter.spec.tsx delete mode 100644 src/components/text-editor/__internal__/editor-counter/editor-counter.style.ts delete mode 100644 src/components/text-editor/__internal__/editor-counter/index.ts create mode 100644 src/components/text-editor/__internal__/editor-validation-wrapper/editor-validation-wrapper.component.tsx create mode 100644 src/components/text-editor/__internal__/editor-validation-wrapper/editor-validation-wrapper.spec.tsx create mode 100644 src/components/text-editor/__internal__/editor-validation-wrapper/editor-validation-wrapper.style.ts create mode 100644 src/components/text-editor/__internal__/editor-validation-wrapper/index.ts diff --git a/cypress/components/text-editor/text-editor.cy.tsx b/cypress/components/text-editor/text-editor.cy.tsx index 9b64c1c861..48b9215e13 100644 --- a/cypress/components/text-editor/text-editor.cy.tsx +++ b/cypress/components/text-editor/text-editor.cy.tsx @@ -6,6 +6,8 @@ import { TextEditorCustomValidation, } from "../../../src/components/text-editor/text-editor-test.stories"; +import { WithNewValidation as TextEditorNewValidation } from "../../../src/components/text-editor/text-editor.stories"; + import { textEditorInput, textEditorCounter, @@ -45,7 +47,7 @@ context("Test for TextEditor component", () => { CypressMountWithProviders(); textEditorInput().clear().type(textForInput); - textEditorCounter().should("have.text", 2982); + textEditorCounter().should("have.text", "2,982 characters left"); }); it.each(["bold", "italic"])( @@ -200,7 +202,7 @@ context("Test for TextEditor component", () => { textEditorInput().clear().type(longText); - textEditorCounter().should("have.text", 0); + textEditorCounter().should("have.text", "0 characters left"); innerText().should("have.text", longTextAssert); }); @@ -443,5 +445,11 @@ context("Test for TextEditor component", () => { cy.checkAccessibility(); } ); + + it("should pass accessibility tests for TextEditor validation when opt in flag is set", () => { + CypressMountWithProviders(); + + cy.checkAccessibility(); + }); }); }); diff --git a/cypress/locators/text-editor/index.js b/cypress/locators/text-editor/index.js index 350d7c76f3..00e3c1ec08 100644 --- a/cypress/locators/text-editor/index.js +++ b/cypress/locators/text-editor/index.js @@ -1,13 +1,12 @@ -import { LINK } from "../locators"; +import { CHARACTER_COUNT, LINK } from "../locators"; import { TEXT_EDITOR_CONTAINER, - TEXT_EDITOR_COUNTER, TEXT_EDITOR_INPUT, TEXT_EDITOR_TOOLBAR, } from "./locators"; // component preview locators -export const textEditorCounter = () => cy.get(TEXT_EDITOR_COUNTER); +export const textEditorCounter = () => cy.get(CHARACTER_COUNT); export const textEditorInput = () => cy.get(TEXT_EDITOR_INPUT); export const textEditorToolbar = (buttonType) => cy diff --git a/cypress/locators/text-editor/locators.js b/cypress/locators/text-editor/locators.js index 492cc32e91..0a29b18006 100644 --- a/cypress/locators/text-editor/locators.js +++ b/cypress/locators/text-editor/locators.js @@ -1,5 +1,4 @@ // component preview locators -export const TEXT_EDITOR_COUNTER = '[data-component="text-editor-counter"]'; export const TEXT_EDITOR_INPUT = '[role="textbox"]'; export const TEXT_EDITOR_TOOLBAR = '[data-component="text-editor-toolbar"]'; export const TEXT_EDITOR_CONTAINER = '[data-component="text-editor-container"]'; diff --git a/src/components/text-editor/__internal__/editor-counter/editor-counter.component.tsx b/src/components/text-editor/__internal__/editor-counter/editor-counter.component.tsx deleted file mode 100644 index a66f5004ce..0000000000 --- a/src/components/text-editor/__internal__/editor-counter/editor-counter.component.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from "react"; - -import { StyledCounterWrapper, StyledCounter } from "./editor-counter.style"; -import ValidationIcon from "../../../../__internal__/validations"; - -export interface EditorCounterProps { - /** Sets the current count value */ - count?: number; - /** Sets the current limit value */ - limit?: number; - /** Message to be displayed when there is an error */ - error?: string; - /** Message to be displayed when there is a warning */ - warning?: string; - /** Message to be displayed when there is an info */ - info?: string; -} - -const Counter = ({ - count = 0, - limit = 3000, - error, - warning, - info, -}: EditorCounterProps) => ( - - {!!(error || warning || info) && ( - - )} - {`${limit - count}`} - -); - -export default Counter; diff --git a/src/components/text-editor/__internal__/editor-counter/editor-counter.spec.tsx b/src/components/text-editor/__internal__/editor-counter/editor-counter.spec.tsx deleted file mode 100644 index 23aaa9a56d..0000000000 --- a/src/components/text-editor/__internal__/editor-counter/editor-counter.spec.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from "react"; -import { mount } from "enzyme"; -import { assertStyleMatch } from "../../../../__spec_helper__/test-utils"; -import Counter, { EditorCounterProps } from "./editor-counter.component"; -import { StyledCounter } from "./editor-counter.style"; -import ValidationIcon from "../../../../__internal__/validations"; - -const render = (props: EditorCounterProps = {}, renderer = mount) => { - return renderer(); -}; - -describe("EditorCounter", () => { - it("has the expected styles", () => { - const wrapper = render(); - assertStyleMatch( - { - margin: "16px 16px 0px 4px", - minWidth: "40px", - height: "21px", - float: "right", - textAlign: "right", - alignItems: "center", - }, - wrapper - ); - - assertStyleMatch( - { - color: "var(--colorsUtilityYin055)", - width: "100%", - }, - wrapper.find(StyledCounter) - ); - }); - - it("displays the correct value for permitted characters when the default `limit` is used", () => { - const wrapper = render({ count: 10 }); - - expect(wrapper.find("div").text()).toEqual("2990"); - }); - - it("displays the correct value for permitted characters when the `limit` prop is passed a value of `10`", () => { - const wrapper = render({ count: 10, limit: 10 }); - - expect(wrapper.find("div").text()).toEqual("0"); - }); - - describe("with validation", () => { - it("does not render the icon by default", () => { - expect(render().find(ValidationIcon).exists()).toEqual(false); - }); - - it.each([ - { error: "error" }, - { warning: "warning" }, - { info: "info" }, - ] as const)("renders the icon when a message is provided", (msg) => { - expect( - render({ ...msg }) - .find(ValidationIcon) - .exists() - ).toEqual(true); - }); - - it("has expected styling overrides applied when there is an error", () => { - const wrapper = render({ error: "error" }); - - assertStyleMatch( - { - color: "var(--colorsSemanticNegative500)", - }, - wrapper.find(StyledCounter) - ); - }); - }); -}); diff --git a/src/components/text-editor/__internal__/editor-counter/editor-counter.style.ts b/src/components/text-editor/__internal__/editor-counter/editor-counter.style.ts deleted file mode 100644 index c50891b3f7..0000000000 --- a/src/components/text-editor/__internal__/editor-counter/editor-counter.style.ts +++ /dev/null @@ -1,22 +0,0 @@ -import styled from "styled-components"; - -const StyledCounter = styled.span<{ hasError: boolean }>` - color: ${({ hasError }) => - hasError - ? "var(--colorsSemanticNegative500)" - : "var(--colorsUtilityYin055)"}; - width: 100%; -`; - -const StyledCounterWrapper = styled.div` - margin: 16px 16px 0px 4px; - min-width: 40px; - height: 21px; - font-size: 14px; - display: flex; - float: right; - text-align: right; - align-items: center; -`; - -export { StyledCounter, StyledCounterWrapper }; diff --git a/src/components/text-editor/__internal__/editor-counter/index.ts b/src/components/text-editor/__internal__/editor-counter/index.ts deleted file mode 100644 index df1797a266..0000000000 --- a/src/components/text-editor/__internal__/editor-counter/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./editor-counter.component"; -export type { EditorCounterProps } from "./editor-counter.component"; diff --git a/src/components/text-editor/__internal__/editor-validation-wrapper/editor-validation-wrapper.component.tsx b/src/components/text-editor/__internal__/editor-validation-wrapper/editor-validation-wrapper.component.tsx new file mode 100644 index 0000000000..d771b38c08 --- /dev/null +++ b/src/components/text-editor/__internal__/editor-validation-wrapper/editor-validation-wrapper.component.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +import StyledValidationWrapper from "./editor-validation-wrapper.style"; +import ValidationIcon from "../../../../__internal__/validations"; + +export interface EditorValidationWrapperProps { + /** Message to be displayed when there is an error */ + error?: string; + /** Message to be displayed when there is a warning */ + warning?: string; + /** Message to be displayed when there is an info */ + info?: string; +} + +const ValidationWrapper = ({ + error, + warning, + info, +}: EditorValidationWrapperProps) => ( + + + +); + +export default ValidationWrapper; diff --git a/src/components/text-editor/__internal__/editor-validation-wrapper/editor-validation-wrapper.spec.tsx b/src/components/text-editor/__internal__/editor-validation-wrapper/editor-validation-wrapper.spec.tsx new file mode 100644 index 0000000000..405322f6b6 --- /dev/null +++ b/src/components/text-editor/__internal__/editor-validation-wrapper/editor-validation-wrapper.spec.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { mount } from "enzyme"; +import { assertStyleMatch } from "../../../../__spec_helper__/test-utils"; +import ValidationWrapper, { + EditorValidationWrapperProps, +} from "./editor-validation-wrapper.component"; +import ValidationIcon from "../../../../__internal__/validations"; + +const render = (props: EditorValidationWrapperProps = {}, renderer = mount) => { + return renderer(); +}; + +describe("EditorValidationWrapper", () => { + it("has the expected styles", () => { + const wrapper = render(); + assertStyleMatch( + { + margin: + "var(--spacing200) var(--spacing200) var(--spacing000) var(--spacing050)", + minWidth: "var(--sizing500)", + height: "var(--sizing275)", + float: "right", + alignItems: "center", + }, + wrapper + ); + }); + + describe("with validation", () => { + it.each([ + { error: "error" }, + { warning: "warning" }, + { info: "info" }, + ] as const)("renders the icon when a message is provided", (msg) => { + expect( + render({ ...msg }) + .find(ValidationIcon) + .exists() + ).toEqual(true); + }); + }); +}); diff --git a/src/components/text-editor/__internal__/editor-validation-wrapper/editor-validation-wrapper.style.ts b/src/components/text-editor/__internal__/editor-validation-wrapper/editor-validation-wrapper.style.ts new file mode 100644 index 0000000000..043876c5d4 --- /dev/null +++ b/src/components/text-editor/__internal__/editor-validation-wrapper/editor-validation-wrapper.style.ts @@ -0,0 +1,13 @@ +import styled from "styled-components"; + +const StyledValidationWrapper = styled.div` + margin: var(--spacing200) var(--spacing200) var(--spacing000) + var(--spacing050); + min-width: var(--sizing500); + height: var(--sizing275); + display: flex; + float: right; + align-items: center; +`; + +export default StyledValidationWrapper; diff --git a/src/components/text-editor/__internal__/editor-validation-wrapper/index.ts b/src/components/text-editor/__internal__/editor-validation-wrapper/index.ts new file mode 100644 index 0000000000..c700373c38 --- /dev/null +++ b/src/components/text-editor/__internal__/editor-validation-wrapper/index.ts @@ -0,0 +1,2 @@ +export { default } from "./editor-validation-wrapper.component"; +export type { EditorValidationWrapperProps } from "./editor-validation-wrapper.component"; diff --git a/src/components/text-editor/text-editor.component.tsx b/src/components/text-editor/text-editor.component.tsx index c1e92da1ed..7b6c68f7b0 100644 --- a/src/components/text-editor/text-editor.component.tsx +++ b/src/components/text-editor/text-editor.component.tsx @@ -1,4 +1,10 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { + useCallback, + useEffect, + useRef, + useState, + useContext, +} from "react"; import { MarginProps } from "styled-system"; import { ContentState, @@ -29,11 +35,11 @@ import { StyledEditorOutline, StyledEditorContainer, } from "./text-editor.style"; -import Counter from "./__internal__/editor-counter"; +import ValidationWrapper from "./__internal__/editor-validation-wrapper"; import Toolbar from "./__internal__/toolbar"; import Label from "../../__internal__/label"; import Events from "../../__internal__/utils/helpers/events"; -import createGuid from "../../__internal__/utils/helpers/guid"; +import guid from "../../__internal__/utils/helpers/guid"; import LabelWrapper from "./__internal__/label-wrapper"; import { BOLD, @@ -44,6 +50,12 @@ import { BlockType, } from "./types"; import { LinkPreviewProps } from "../link-preview"; +import { NewValidationContext } from "../carbon-provider/carbon-provider.component"; +import { ErrorBorder, StyledHintText } from "../textbox/textbox.style"; +import ValidationMessage from "../../__internal__/validation-message"; +import useInputAccessibility from "../../hooks/__internal__/useInputAccessibility"; +import Box from "../box"; +import useCharacterCount from "../../hooks/__internal__/useCharacterCount"; const NUMBERS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; const INLINE_STYLES = [BOLD, ITALIC] as const; @@ -78,6 +90,8 @@ export interface TextEditorProps extends MarginProps { previews?: React.ReactNode; /** Callback to report a url when a link is added */ onLinkAdded?: (url: string) => void; + /** Hint text to be rendered when validationRedesignOptIn flag is set */ + inputHint?: string; } export const TextEditor = React.forwardRef( @@ -95,10 +109,12 @@ export const TextEditor = React.forwardRef( rows, previews, onLinkAdded, + inputHint, ...rest }: TextEditorProps, ref ) => { + const { validationRedesignOptIn } = useContext(NewValidationContext); const [isFocused, setIsFocused] = useState(false); const [inlines, setInlines] = useState([]); const [activeInlines, setActiveInlines] = useState< @@ -111,7 +127,30 @@ export const TextEditor = React.forwardRef( const contentLength = getContent(value).getPlainText("").length; const moveCursor = useRef(contentLength > 0); const lastKeyPressed = useRef(); - const labelId = useRef(`text-editor-label-${createGuid()}`); + const inputHintId = useRef(`${guid()}-hint`); + const { current: id } = useRef(guid()); + + const { labelId, validationId, ariaDescribedBy } = useInputAccessibility({ + id, + validationRedesignOptIn, + error, + warning, + info, + label: labelText, + }); + + const [characterCount, visuallyHiddenHintId] = useCharacterCount( + getContent(value).getPlainText(""), + characterLimit + ); + + const combinedAriaDescribedBy = [ + ariaDescribedBy, + inputHint ? inputHintId.current : undefined, + visuallyHiddenHintId, + ] + .filter(Boolean) + .join(" "); if (rows && (typeof rows !== "number" || rows < 2)) { // eslint-disable-next-line no-console @@ -377,73 +416,93 @@ export const TextEditor = React.forwardRef( handleEditorFocus(true)}> - - - - - handleEditorFocus(true)} - onBlur={() => handleEditorFocus(false)} - editorState={editorState} - onChange={onChange} - handleBeforeInput={ - handleBeforeInput as ( - chars: string, - state: EditorState - ) => DraftHandleValue - } - handlePastedText={handlePastedText} - handleKeyCommand={ - handleKeyCommand as ( - command: EditorCommand - ) => DraftHandleValue - } - ariaLabelledBy={labelId.current} - ariaDescribedBy={labelId.current} - blockStyleFn={blockStyleFn} - keyBindingFn={keyBindingFn} - tabIndex={0} - /> - {React.Children.map(previews, (preview) => { - if (React.isValidElement(preview)) { - const { onClose } = preview?.props; - return React.cloneElement(preview, { - as: "div", - onClose: onClose - ? (url?: string) => handlePreviewClose(onClose, url) - : undefined, - }); - } - return null; - })} - - handleBlockStyleChange(ev, newBlockType) - } - setInlineStyle={(ev, inlineStyle) => - handleInlineStyleChange(ev, inlineStyle) - } - activeControls={activeControls} - canFocus={focusToolbar} - toolbarElements={toolbarElements} - /> - - + {inputHint && ( + + {inputHint} + + )} + + {validationRedesignOptIn && ( + <> + + {(error || warning) && ( + + )} + + )} + + + {!validationRedesignOptIn && (error || warning || info) && ( + + )} + handleEditorFocus(true)} + onBlur={() => handleEditorFocus(false)} + editorState={editorState} + onChange={onChange} + handleBeforeInput={ + handleBeforeInput as ( + chars: string, + state: EditorState + ) => DraftHandleValue + } + handlePastedText={handlePastedText} + handleKeyCommand={ + handleKeyCommand as ( + command: EditorCommand + ) => DraftHandleValue + } + ariaLabelledBy={labelId} + ariaDescribedBy={combinedAriaDescribedBy} + blockStyleFn={blockStyleFn} + keyBindingFn={keyBindingFn} + tabIndex={0} + /> + {React.Children.map(previews, (preview) => { + if (React.isValidElement(preview)) { + const { onClose } = preview?.props; + return React.cloneElement(preview, { + as: "div", + onClose: onClose + ? (url?: string) => handlePreviewClose(onClose, url) + : undefined, + }); + } + return null; + })} + + handleBlockStyleChange(ev, newBlockType) + } + setInlineStyle={(ev, inlineStyle) => + handleInlineStyleChange(ev, inlineStyle) + } + activeControls={activeControls} + canFocus={focusToolbar} + toolbarElements={toolbarElements} + /> + + + {characterCount} + ); diff --git a/src/components/text-editor/text-editor.spec.tsx b/src/components/text-editor/text-editor.spec.tsx index 31c3e3332f..e5f7bc09e1 100644 --- a/src/components/text-editor/text-editor.spec.tsx +++ b/src/components/text-editor/text-editor.spec.tsx @@ -28,7 +28,6 @@ import { import ToolbarButton, { ToolbarButtonProps, } from "./__internal__/toolbar/toolbar-button/toolbar-button.component"; -import Counter from "./__internal__/editor-counter"; import Toolbar from "./__internal__/toolbar"; import guid from "../../__internal__/utils/helpers/guid"; import Label from "../../__internal__/label"; @@ -38,6 +37,9 @@ import ValidationIcon from "../../__internal__/validations"; import { isSafari } from "../../__internal__/utils/helpers/browser-type-check"; import IconButton from "../icon-button"; import { BlockType, InlineStyleType } from "./types"; +import CarbonProvider from "../carbon-provider"; +import { ErrorBorder, StyledHintText } from "../textbox/textbox.style"; +import StyledValidationMessage from "../../__internal__/validation-message/validation-message.style"; jest.mock("../../__internal__/utils/helpers/browser-type-check"); (isSafari as jest.MockedFunction).mockImplementation( @@ -113,11 +115,20 @@ const MockComponent = (props: Partial) => { ); }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const render = (props: Partial = {}, renderer: any = mount) => - renderer(, { - attachTo: document.getElementById("enzymeContainer"), - }); +const render = ( + props: Partial = {}, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + renderer: any = mount, + validationRedesignOptIn = false +) => + renderer( + + + , + { + attachTo: document.getElementById("enzymeContainer"), + } + ); describe("TextEditor", () => { beforeAll(() => { @@ -872,30 +883,6 @@ describe("TextEditor", () => { }); describe("Character Limit", () => { - describe("Counter", () => { - it("displays the default text when no prop value passed", () => { - wrapper = render(); - expect(wrapper.find(Counter).text()).toEqual("3000"); - }); - - it("overrides the text when `characterLimit` prop passed", () => { - wrapper = render({ characterLimit: 200 }); - expect(wrapper.find(Counter).text()).toEqual("200"); - }); - - it("the text updates as content is added to the editor", () => { - wrapper = render({ characterLimit: 10 }); - const textEditor = wrapper.find(TextEditor); - const editor = wrapper.find(Editor); - act(() => { - textEditor - .props() - .onChange(addToEditorState("foo foo", editor.props())); - }); - expect(wrapper.find(Counter).text()).toEqual("3"); - }); - }); - it("prevents enter key press when the limit has been reached", () => { wrapper = render({ characterLimit: 0 }); const editor = wrapper.find(Editor); @@ -962,44 +949,107 @@ describe("TextEditor", () => { }); describe("validation", () => { - it.each([{ error: "error" }, { warning: "warning" }, { info: "info" }])( - "passes the validation props to the counter and renders the icon", + it.each(["error", "warning", "info"])( + "renders the %s icon when the validationRedesignOptIn flag is not set", (msg) => { expect( - render({ ...msg }) + render({ [msg]: msg }) .find(ValidationIcon) .exists() - ).toEqual(true); + ).toBe(true); } ); + }); - it("applies the expected outline when an error message is passed", () => { - assertStyleMatch( - { - outline: "2px solid var(--colorsSemanticNegative500)", - }, - render({ error: "error" }).find(StyledEditorContainer) + it("applies the expected outline when an error message is passed", () => { + assertStyleMatch( + { + outline: "2px solid var(--colorsSemanticNegative500)", + }, + render({ error: "error" }).find(StyledEditorContainer) + ); + }); + + it("applies the correct outline-offset when there is an error and the editor is focused", () => { + wrapper = render({ error: "error" }); + act(() => { + wrapper + .find(Editor) + .props() + .onFocus?.({} as SyntheticEvent); + }); + act(() => { + wrapper.update(); + }); + + assertStyleMatch( + { + boxShadow: + "0px 0px 0px var(--borderWidth300) var(--colorsSemanticFocus500),0px 0px 0px var(--borderWidth600) var(--colorsUtilityYin090)", + }, + wrapper.find(StyledEditorOutline) + ); + }); + + describe("new validations", () => { + it.each(["error", "warning"])( + "renders the new message and layout when the validationRedesignOptIn flag is set", + (msg) => { + wrapper = render({ [msg]: msg }, mount, true); + expect(wrapper.find(ValidationIcon).exists()).toBe(false); + expect(wrapper.find(ErrorBorder).exists()).toBe(true); + expect(wrapper.find(StyledValidationMessage).text()).toBe(msg); + } + ); + + it('passes id to the "aria-describedby" of the editor element when there is an error', () => { + wrapper = render({ error: "bar" }, mount, true); + + expect(wrapper.find(Editor).prop("ariaDescribedBy")).toBe( + "guid-12345-validation guid-12345" + ); + expect(wrapper.find(Editor).prop("ariaLabelledBy")).toBe( + "guid-12345-label" ); }); - it("applies the correct outline-offset when there is an error and the editor is focused", () => { - wrapper = render({ error: "error" }); - act(() => { - wrapper - .find(Editor) - .props() - .onFocus?.({} as SyntheticEvent); - }); - act(() => { - wrapper.update(); - }); + it('passes id to the "aria-describedby" of the editor element when there is a warning', () => { + wrapper = render({ warning: "bar" }, mount, true); - assertStyleMatch( - { - boxShadow: - "0px 0px 0px var(--borderWidth300) var(--colorsSemanticFocus500),0px 0px 0px var(--borderWidth600) var(--colorsUtilityYin090)", - }, - wrapper.find(StyledEditorOutline) + expect(wrapper.find(Editor).prop("ariaDescribedBy")).toBe( + "guid-12345-validation guid-12345" + ); + expect(wrapper.find(Editor).prop("ariaLabelledBy")).toBe( + "guid-12345-label" + ); + }); + }); + + describe("with inputHint", () => { + it("renders the element as expected", () => { + wrapper = render({ inputHint: "foo" }); + expect(wrapper.find(StyledHintText).text()).toEqual("foo"); + }); + + it('passes id to the "aria-describedby" of the editor element when there is an error', () => { + wrapper = render({ error: "bar", inputHint: "foo" }, mount, true); + + expect(wrapper.find(Editor).prop("ariaDescribedBy")).toBe( + "guid-12345-validation guid-12345-hint guid-12345" + ); + expect(wrapper.find(Editor).prop("ariaLabelledBy")).toBe( + "guid-12345-label" + ); + }); + + it('passes id to the "aria-describedby" of the editor element when there is a warning', () => { + wrapper = render({ warning: "bar", inputHint: "foo" }, mount, true); + + expect(wrapper.find(Editor).prop("ariaDescribedBy")).toBe( + "guid-12345-validation guid-12345-hint guid-12345" + ); + expect(wrapper.find(Editor).prop("ariaLabelledBy")).toBe( + "guid-12345-label" ); }); }); diff --git a/src/components/text-editor/text-editor.stories.mdx b/src/components/text-editor/text-editor.stories.mdx index c42a7aa633..1aabb032e4 100644 --- a/src/components/text-editor/text-editor.stories.mdx +++ b/src/components/text-editor/text-editor.stories.mdx @@ -106,6 +106,10 @@ With use of `template strings` it is possible to pass multiline validation messa /> + + + + ### With custom row height The `rows` prop allows for overriding the default min-height of the `TextEditor`. It accepts any number greater than 2 diff --git a/src/components/text-editor/text-editor.stories.tsx b/src/components/text-editor/text-editor.stories.tsx index 2b821e3f2c..9cbfd6e9dc 100644 --- a/src/components/text-editor/text-editor.stories.tsx +++ b/src/components/text-editor/text-editor.stories.tsx @@ -6,11 +6,13 @@ import TextEditor, { } from "./text-editor.component"; import Button from "../button"; import EditorLinkPreview from "../link-preview"; +import Box from "../box"; +import CarbonProvider from "../carbon-provider"; export const Default = () => { const [value, setValue] = useState(EditorState.createEmpty()); return ( -
+ { setValue(newValue); @@ -18,7 +20,7 @@ export const Default = () => { value={value} labelText="Text Editor Label" /> -
+ ); }; @@ -31,7 +33,7 @@ export const WithContent = () => { ) ); return ( -
+ { setValue(newValue); @@ -40,7 +42,7 @@ export const WithContent = () => { labelText="Text Editor Label" required /> -
+ ); }; @@ -48,15 +50,13 @@ WithContent.storyName = "with content"; export const WithOptionalButtons = () => { const [value, setValue] = useState(EditorState.createEmpty()); - const ref = useRef(null); return ( -
+ { setValue(newValue); }} value={value} - ref={ref} toolbarElements={[
+ ); }; @@ -82,19 +82,17 @@ WithOptionalButtons.storyName = "with optional buttons"; export const WithOptionalCharacterLimit = () => { const [value, setValue] = useState(EditorState.createEmpty()); const limit = 100; - const ref = useRef(null); return ( -
+ { setValue(newValue); }} value={value} - ref={ref} labelText="Text Editor Label" characterLimit={limit} /> -
+ ); }; @@ -106,24 +104,21 @@ export const WithValidation = () => { ); const limit = 16; const contentLength = value.getCurrentContent().getPlainText().length; - const ref = useRef(null); return ( -
+ { setValue(newValue); }} value={value} - ref={ref} labelText="Text Editor Label" characterLimit={limit} error={limit - contentLength <= 5 ? "There is an error" : undefined} - warning={ - limit - contentLength <= 10 ? "There is an warning" : undefined - } + warning={limit - contentLength <= 10 ? "There is a warning" : undefined} info={limit - contentLength <= 15 ? "There is an info" : undefined} + inputHint="Some additional hint text" /> -
+ ); }; @@ -135,7 +130,6 @@ export const WithMultilineValidation = () => { ); const limit = 16; const contentLength = value.getCurrentContent().getPlainText().length; - const ref = useRef(null); const error = limit - contentLength <= 5 ? `There is an error. @@ -143,28 +137,56 @@ The content is too long. Maybe try writing a little bit less?` : undefined; return ( -
+ { setValue(newValue); }} value={value} - ref={ref} labelText="Text Editor Label" characterLimit={limit} error={error} /> -
+ ); }; WithMultilineValidation.storyName = "with multiline validation"; +export const WithNewValidation = () => { + const [value, setValue] = useState( + EditorState.createWithContent(ContentState.createFromText("Add content")) + ); + const limit = 16; + const contentLength = value.getCurrentContent().getPlainText().length; + return ( + + + { + setValue(newValue); + }} + value={value} + labelText="Text Editor Label" + characterLimit={limit} + error={limit - contentLength <= 5 ? "There is an error" : undefined} + warning={ + limit - contentLength <= 10 ? "There is a warning" : undefined + } + inputHint="Some additional hint text" + /> + + + ); +}; + +WithNewValidation.storyName = "with new validation"; + export const WithCustomRowHeight = () => { const [value, setValue] = useState(EditorState.createEmpty()); return ( -
+ { setValue(newValue); @@ -173,7 +195,7 @@ export const WithCustomRowHeight = () => { labelText="Text Editor Label" rows={2} /> -
+ ); }; @@ -228,7 +250,7 @@ export const WithLinkPreviews = () => { } }; return ( -
+ { setValue(newValue); @@ -239,6 +261,6 @@ export const WithLinkPreviews = () => { previews={previews.current} onLinkAdded={addUrl} /> -
+ ); };