Repository: Sage/carbon (ts)

Issue:
Ensure character count message is not read out after form submission
### Current behaviour

Screenreader reads out character count 'You have n characters remaining' message after form submission

### Expected behaviour

Screenreader should not read out character count 'You have n characters remaining' message after form submission

### CodeSandbox or Storybook URL

https://codesandbox.io/s/with-characterlimit-reset-v3jks3

### JIRA Ticket (Sage Only)

SBS-64744

### Suggested Solution

_No response_

### Carbon Version

1119.6.0

### Design Tokens Version

4.19.0

### What browsers are you seeing the problem on?

Chrome

### What Operating System are you seeing the problem on?

MacOS

### Anything else we should know?

_No response_

### Confidentiality

- [X] I confirm there is no confidential or commercially sensitive information included.

Pull request:
fix(character-count): prevent screen reader announcing value when submitting a form
This work fixes a bug whereby a screen reader announces the character count 'You have `n` characters remaining' message after form submission. To address this, we have defaulted aria-live to off until the input is focused where it will then have the value of polite.

fixes #6181

### Proposed behaviour

Default aria-live value is `off` until the input is focused where it will then have the value of `polite`. Once the input is blurred, the aria-live value will be reset to `off`.

### Current behaviour

By default, the aria-live value is always `polite`.

### Checklist

<!-- Each PR should include the following -->

- [x] Commits follow our style guide
- [x] Related issues linked in commit messages if required
- [ ] Screenshots are included in the PR if useful
- [x] All themes are supported if required
- [x] Unit tests added or updated if required
- [x] Playwright automation tests added or updated if required
- [x] Storybook added or updated if required
- [ ] Translations added or updated (including creating or amending translation keys table in storybook) if required
- [ ] Typescript `d.ts` file added or updated if required
- [x] Related docs have been updated if required

#### QA

- [ ] Tested in provided StackBlitz sandbox/Storybook
- [ ] Add new Playwright test coverage if required
- [ ] Carbon implementation matches Design System/designs
- [ ] UI Tests GitHub check reviewed if required

### Additional context

N/A

### Testing instructions

I have created a test story in Form called `Mock form for aria live demo`. In this demo you should check:
- Each component has an aria-live value of `off` by default (you can find this value by looking at the `div` element with `data-element="visually-hidden-character-count"`). When the input is focused, the aria-live value should change to `polite`. 
- We will need to ensure that the inputs are still announced correctly by the screen readers when typed in. 
- When the form is submitted using the `Save` button, there should be no update provided regarding the character counts in any of the inputs. 


Patch:
diff --git a/playwright/components/index.ts b/playwright/components/index.ts
index aacb6a284..d201afccf 100644
--- a/playwright/components/index.ts
+++ b/playwright/components/index.ts
@@ -16,6 +16,10 @@ import {
   HELP_ICON_PREVIEW,
   BACKGROUND_UI_LOCATOR,
   COMMON_INPUT_PREFIX,
+  VISUALLY_HIDDEN_CHARACTER_COUNT,
+  VISUALLY_HIDDEN_HINT,
+  CHARACTER_COUNT,
+  CHARACTER_LIMIT,
 } from "./locators";
 
 export const icon = (page: Page) => {
@@ -86,6 +90,17 @@ export const characterLimit = (page: Page) => {
   return page.locator(COMMON_INPUT_CHARACTER_LIMIT);
 };
 
+export const visuallyHiddenCharacterCount = (page: Page) =>
+  page.locator(VISUALLY_HIDDEN_CHARACTER_COUNT);
+
+export const visuallyHiddenHint = (page: Page) =>
+  page.locator(VISUALLY_HIDDEN_HINT);
+
+export const characterCount = (page: Page) => page.locator(CHARACTER_COUNT);
+
+export const characterLimitDefaultTextarea = (page: Page) =>
+  page.locator(CHARACTER_LIMIT);
+
 export const legendSpan = (page: Page) => {
   return page.locator("legend > span");
 };
diff --git a/playwright/components/locators.ts b/playwright/components/locators.ts
index 6b55c9d35..dfe7edfbc 100644
--- a/playwright/components/locators.ts
+++ b/playwright/components/locators.ts
@@ -19,8 +19,12 @@ export const BUTTON = 'button[type="button"]';
 export const COMMON_INPUT_PREFIX = '[data-element="textbox-prefix"]';
 export const COMMON_INPUT_CHARACTER_LIMIT =
   'div[data-element="character-count"]';
+export const VISUALLY_HIDDEN_CHARACTER_COUNT =
+  'div[data-element="visually-hidden-character-count"]';
+export const VISUALLY_HIDDEN_HINT = 'div[data-element="visually-hidden-hint"]';
+export const CHARACTER_COUNT = 'div[data-element="character-count"]';
+export const CHARACTER_LIMIT = 'div[data-element="character-limit"]';
 export const PORTAL = ".carbon-portal";
 export const LEGEND = "legend";
 export const STICKY_FOOTER = '[data-component="sticky-footer"]';
 export const PILL_CLOSE_ICON = '[data-element="close"]';
-export const CHARACTER_COUNT = 'div[data-element="character-count"]';
diff --git a/playwright/components/textarea/index.ts b/playwright/components/textarea/index.ts
index 841065164..3f514425a 100644
--- a/playwright/components/textarea/index.ts
+++ b/playwright/components/textarea/index.ts
@@ -1,25 +1,8 @@
 import { Page } from "@playwright/test";
-import {
-  TEXTAREA,
-  CHARACTER_LIMIT,
-  VISUALLY_HIDDEN_CHARACTER_COUNT,
-  VISUALLY_HIDDEN_HINT,
-  CHARACTER_COUNT,
-} from "./locators";
+import { TEXTAREA } from "./locators";
 
 // component preview locators
 export const textarea = (page: Page) => page.locator(TEXTAREA);
 
 export const textareaChildren = (page: Page) =>
   page.locator(TEXTAREA).locator("textarea");
-
-export const characterLimitDefaultTextarea = (page: Page) =>
-  page.locator(CHARACTER_LIMIT);
-
-export const visuallyHiddenCharacterCount = (page: Page) =>
-  page.locator(VISUALLY_HIDDEN_CHARACTER_COUNT);
-
-export const visuallyHiddenHint = (page: Page) =>
-  page.locator(VISUALLY_HIDDEN_HINT);
-
-export const characterCount = (page: Page) => page.locator(CHARACTER_COUNT);
diff --git a/playwright/components/textarea/locators.ts b/playwright/components/textarea/locators.ts
index eefcd1b3e..477b78d62 100644
--- a/playwright/components/textarea/locators.ts
+++ b/playwright/components/textarea/locators.ts
@@ -1,13 +1,4 @@
 // component preview locators
 export const TEXTAREA = 'div[role="presentation"]';
 
-export const CHARACTER_LIMIT = 'div[data-element="character-limit"]';
-
-export const VISUALLY_HIDDEN_CHARACTER_COUNT =
-  'div[data-element="visually-hidden-character-count"]';
-
-export const VISUALLY_HIDDEN_HINT = 'div[data-element="visually-hidden-hint"]';
-
-export const CHARACTER_COUNT = 'div[data-element="character-count"]';
-
 export const FORM_CONTENT = 'div[data-element="form-content"]';
diff --git a/src/__internal__/character-count/character-count.component.tsx b/src/__internal__/character-count/character-count.component.tsx
index 0a4e18313..5fc32ff1b 100644
--- a/src/__internal__/character-count/character-count.component.tsx
+++ b/src/__internal__/character-count/character-count.component.tsx
@@ -9,6 +9,7 @@ import {
 import useLocale from "../../hooks/__internal__/useLocale";
 
 interface CharacterCountProps {
+  ariaLive?: "off" | "polite";
   value: number;
   debouncedValue?: number;
   limit: number;
@@ -18,6 +19,7 @@ interface CharacterCountProps {
 }
 
 const CharacterCount = ({
+  ariaLive = "off",
   value,
   debouncedValue = value,
   limit,
@@ -61,7 +63,8 @@ const CharacterCount = ({
       </StyledCharacterCount>
       <VisuallyHiddenCharacterCount
         data-element="visually-hidden-character-count"
-        aria-live="polite"
+        data-role="visually-hidden-character-count"
+        aria-live={ariaLive}
       >
         {!isDebouncedOverLimit
           ? l.characterCount.charactersLeft(
diff --git a/src/__internal__/character-count/character-count.spec.tsx b/src/__internal__/character-count/character-count.spec.tsx
index 0dc4c6961..d208e1c43 100644
--- a/src/__internal__/character-count/character-count.spec.tsx
+++ b/src/__internal__/character-count/character-count.spec.tsx
@@ -43,9 +43,9 @@ describe("CharacterCount", () => {
       );
     });
 
-    it("visually hidden character counter has aria-live='polite'", () => {
+    it("visually hidden character counter has aria-live='off'", () => {
       expect(wrapper.find(VisuallyHiddenCharacterCount).prop("aria-live")).toBe(
-        "polite"
+        "off"
       );
     });
 
diff --git a/src/components/text-editor/text-editor.component.tsx b/src/components/text-editor/text-editor.component.tsx
index 7994dde36..f7ad32642 100644
--- a/src/components/text-editor/text-editor.component.tsx
+++ b/src/components/text-editor/text-editor.component.tsx
@@ -141,7 +141,8 @@ export const TextEditor = React.forwardRef<Editor, TextEditorProps>(
 
     const [characterCount, visuallyHiddenHintId] = useCharacterCount(
       getContent(value).getPlainText(""),
-      characterLimit
+      characterLimit,
+      isFocused ? "polite" : "off"
     );
 
     const combinedAriaDescribedBy = [
diff --git a/src/components/text-editor/text-editor.pw.tsx b/src/components/text-editor/text-editor.pw.tsx
index 72380e5a1..8909e96b8 100644
--- a/src/components/text-editor/text-editor.pw.tsx
+++ b/src/components/text-editor/text-editor.pw.tsx
@@ -2,6 +2,7 @@
 import React from "react";
 import { test, expect } from "@playwright/experimental-ct-react17";
 import {
+  TextEditorCharacterCount,
   TextEditorCustom,
   TextEditorCustomValidation,
   TextEditorNewValidation,
@@ -17,6 +18,7 @@ import {
 import {
   getComponent,
   getDataElementByValue,
+  visuallyHiddenCharacterCount,
 } from "../../../playwright/components";
 import {
   checkGoldenOutline,
@@ -765,3 +767,25 @@ test.describe("Accessibility tests", () => {
     await checkAccessibility(page);
   });
 });
+
+test("should set aria-live attribute on Character Count to `polite` when component is focused and then change back to `off` when component is blurred", async ({
+  mount,
+  page,
+}) => {
+  await mount(<TextEditorCharacterCount />);
+
+  const CharacterCountElement = visuallyHiddenCharacterCount(page);
+  const textEditorInputElement = textEditorInput(page);
+  const buttonElement = page.getByRole("button", { name: "Click Me" });
+
+  await expect(CharacterCountElement).toHaveAttribute("aria-live", "off");
+
+  await textEditorInputElement.focus();
+  await textEditorInputElement.fill("Foo");
+
+  await expect(CharacterCountElement).toHaveAttribute("aria-live", "polite");
+
+  await buttonElement.click();
+
+  await expect(CharacterCountElement).toHaveAttribute("aria-live", "off");
+});
diff --git a/src/components/textarea/textarea.component.tsx b/src/components/textarea/textarea.component.tsx
index 2d249de2f..0c36a95d6 100644
--- a/src/components/textarea/textarea.component.tsx
+++ b/src/components/textarea/textarea.component.tsx
@@ -1,4 +1,10 @@
-import React, { useRef, useEffect, useContext, useCallback } from "react";
+import React, {
+  useRef,
+  useEffect,
+  useContext,
+  useCallback,
+  useState,
+} from "react";
 import { MarginProps } from "styled-system";
 
 import { IconType } from "../icon";
@@ -139,6 +145,8 @@ export const Textarea = React.forwardRef(
       children,
       characterLimit,
       onChange,
+      onFocus,
+      onBlur,
       disabled = false,
       labelInline,
       labelAlign,
@@ -198,6 +206,24 @@ export const Textarea = React.forwardRef(
       [ref]
     );
 
+    const [characterCountAriaLive, setCharacterCountAriaLive] = useState<
+      "off" | "polite"
+    >("off");
+
+    // This block of code has been covered in a Playwright test.
+    // istanbul ignore next
+    const handleFocus = (ev: React.FocusEvent<HTMLInputElement>) => {
+      if (characterLimit) setCharacterCountAriaLive("polite");
+      onFocus?.(ev);
+    };
+
+    // This block of code has been covered in a Playwright test.
+    // istanbul ignore next
+    const handleBlur = (ev: React.FocusEvent<HTMLInputElement>) => {
+      if (characterLimit) setCharacterCountAriaLive("off");
+      onBlur?.(ev);
+    };
+
     if (!deprecateUncontrolledWarnTriggered && !onChange) {
       deprecateUncontrolledWarnTriggered = true;
       Logger.deprecate(
@@ -264,7 +290,8 @@ export const Textarea = React.forwardRef(
 
     const [characterCount, visuallyHiddenHintId] = useCharacterCount(
       value,
-      characterLimit
+      characterLimit,
+      characterCountAriaLive
     );
 
     useEffect(() => {
@@ -332,6 +359,8 @@ export const Textarea = React.forwardRef(
           value={value}
           ref={callbackRef}
           onChange={onChange}
+          onFocus={handleFocus}
+          onBlur={handleBlur}
           disabled={disabled}
           readOnly={readOnly}
           placeholder={disabled ? "" : placeholder}
diff --git a/src/components/textarea/textarea.pw.tsx b/src/components/textarea/textarea.pw.tsx
index 51b8cdea6..4362a4bce 100644
--- a/src/components/textarea/textarea.pw.tsx
+++ b/src/components/textarea/textarea.pw.tsx
@@ -4,20 +4,20 @@ import React from "react";
 
 import { HooksConfig } from "../../../playwright";
 import {
+  characterCount,
   fieldHelpPreview,
   getComponent,
   getDataElementByValue,
   getDataRoleByValue,
   getElement,
   tooltipPreview,
+  visuallyHiddenCharacterCount,
+  visuallyHiddenHint,
 } from "../../../playwright/components";
 import { ICON } from "../../../playwright/components/locators";
 import {
-  characterCount,
   textarea,
   textareaChildren,
-  visuallyHiddenCharacterCount,
-  visuallyHiddenHint,
 } from "../../../playwright/components/textarea";
 import { CHARACTERS, VALIDATION } from "../../../playwright/support/constants";
 import {
@@ -31,6 +31,7 @@ import Box from "../box";
 import {
   AutoFocusExample,
   CharacterLimitExample,
+  CharacterLimitExampleWithButton,
   CustomWidthExample,
   Default,
   DisabledExample,
@@ -1047,3 +1048,25 @@ test("should not change the scroll position of a scrollable container when typin
 
   expect(scrollTop).toBeGreaterThan(1000);
 });
+
+test("should set aria-live attribute on Character Count to `polite` when component is focused and then change back to `off` when component is blurred", async ({
+  mount,
+  page,
+}) => {
+  await mount(<CharacterLimitExampleWithButton />);
+
+  const CharacterCountElement = visuallyHiddenCharacterCount(page);
+  const textareaElement = textareaChildren(page);
+  const buttonElement = page.getByRole("button");
+
+  await expect(CharacterCountElement).toHaveAttribute("aria-live", "off");
+
+  await textareaElement.focus();
+  await textareaElement.fill("Foo");
+
+  await expect(CharacterCountElement).toHaveAttribute("aria-live", "polite");
+
+  await buttonElement.click();
+
+  await expect(CharacterCountElement).toHaveAttribute("aria-live", "off");
+});
diff --git a/src/components/textbox/textbox.component.tsx b/src/components/textbox/textbox.component.tsx
index ea8edea80..3eadd8aed 100644
--- a/src/components/textbox/textbox.component.tsx
+++ b/src/components/textbox/textbox.component.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useRef } from "react";
+import React, { useContext, useRef, useState } from "react";
 import { MarginProps } from "styled-system";
 
 import { filterStyledSystemMarginProps } from "../../style/utils";
@@ -198,9 +198,28 @@ export const Textbox = React.forwardRef(
 
     const [uniqueId, uniqueName] = useUniqueId(id, name);
 
+    const [characterCountAriaLive, setCharacterCountAriaLive] = useState<
+      "off" | "polite"
+    >("off");
+
+    // This block of code has been covered in a Playwright test.
+    // istanbul ignore next
+    const handleFocus = (ev: React.FocusEvent<HTMLInputElement>) => {
+      if (characterLimit) setCharacterCountAriaLive("polite");
+      onFocus?.(ev);
+    };
+
+    // This block of code has been covered in a Playwright test.
+    // istanbul ignore next
+    const handleBlur = (ev: React.FocusEvent<HTMLInputElement>) => {
+      if (characterLimit) setCharacterCountAriaLive("off");
+      onBlur?.(ev);
+    };
+
     const [characterCount, visuallyHiddenHintId] = useCharacterCount(
       characterCountValue,
-      characterLimit
+      characterLimit,
+      characterCountAriaLive
     );
     const { validationRedesignOptIn } = useContext(NewValidationContext);
     const { disableErrorBorder } = useContext(NumeralDateContext);
@@ -275,11 +294,11 @@ export const Textbox = React.forwardRef(
           id={uniqueId}
           ref={ref}
           name={uniqueName}
-          onBlur={onBlur}
+          onBlur={handleBlur}
           onChange={onChange}
           onChangeDeferred={onChangeDeferred}
           onClick={disabled || readOnly ? undefined : onClick}
-          onFocus={onFocus}
+          onFocus={handleFocus}
           onMouseDown={disabled || readOnly ? undefined : onMouseDown}
           placeholder={disabled || readOnly ? "" : placeholder}
           readOnly={readOnly}
diff --git a/src/components/textbox/textbox.pw.tsx b/src/components/textbox/textbox.pw.tsx
index 3a3485edf..fbf79afbb 100644
--- a/src/components/textbox/textbox.pw.tsx
+++ b/src/components/textbox/textbox.pw.tsx
@@ -17,6 +17,7 @@ import {
   getDataRoleByValue,
   tooltipPreview,
   characterLimit,
+  visuallyHiddenCharacterCount,
 } from "../../../playwright/components";
 import {
   TextboxComponent,
@@ -29,6 +30,7 @@ import {
   TextboxValidationsAsAStringWithTooltipCustom,
   TextboxValidationsAsAStringDisplayedOnLabel,
   TextboxNewDesignsValidation,
+  TextboxComponentWithCharacterLimit,
 } from "./components.test-pw";
 import {
   textbox,
@@ -284,15 +286,10 @@ test.describe("Prop checks for Textbox component", () => {
       }) => {
         await mount(<TextboxComponent characterLimit={characterLimitVal} />);
 
-        const visuallyHiddenCharacterCount = getDataElementByValue(
-          page,
-          "visually-hidden-character-count"
-        );
-
         if (renderStatus === "with") {
-          await expect(visuallyHiddenCharacterCount).toHaveCount(1);
+          await expect(visuallyHiddenCharacterCount(page)).toHaveCount(1);
         } else {
-          await expect(visuallyHiddenCharacterCount).toHaveCount(0);
+          await expect(visuallyHiddenCharacterCount(page)).toHaveCount(0);
         }
       });
     }
@@ -1192,3 +1189,25 @@ test.describe("Accessibility tests for Textbox component", () => {
     await checkAccessibility(page);
   });
 });
+
+test("should set aria-live attribute on Character Count to `polite` when component is focused and then change back to `off` when component is blurred", async ({
+  mount,
+  page,
+}) => {
+  await mount(<TextboxComponentWithCharacterLimit />);
+
+  const CharacterCountElement = visuallyHiddenCharacterCount(page);
+  const textboxElement = textboxInput(page);
+  const buttonElement = page.getByRole("button");
+
+  await expect(CharacterCountElement).toHaveAttribute("aria-live", "off");
+
+  await textboxElement.focus();
+  await textboxElement.fill("Foo");
+
+  await expect(CharacterCountElement).toHaveAttribute("aria-live", "polite");
+
+  await buttonElement.click();
+
+  await expect(CharacterCountElement).toHaveAttribute("aria-live", "off");
+});
diff --git a/src/hooks/__internal__/useCharacterCount/useCharacterCount.tsx b/src/hooks/__internal__/useCharacterCount/useCharacterCount.tsx
index 29831be53..126154212 100644
--- a/src/hooks/__internal__/useCharacterCount/useCharacterCount.tsx
+++ b/src/hooks/__internal__/useCharacterCount/useCharacterCount.tsx
@@ -5,7 +5,8 @@ import useDebounce from "../useDebounce";
 
 const useCharacterCount = (
   value = "",
-  characterLimit?: number
+  characterLimit?: number,
+  characterCountAriaLive?: "off" | "polite"
 ): [JSX.Element | null, string | undefined] => {
   const isCharacterLimitValid =
     typeof characterLimit === "number" && !Number.isNaN(characterLimit);
@@ -41,6 +42,7 @@ const useCharacterCount = (
   return [
     isCharacterLimitValid ? (
       <CharacterCount
+        ariaLive={characterCountAriaLive}
         isOverLimit={isOverLimit}
         isDebouncedOverLimit={isDebouncedOverLimit}
         value={value.length}


Test patch:
diff --git a/src/components/form/form-test.stories.tsx b/src/components/form/form-test.stories.tsx
index 248b7c419..975ca1f1a 100644
--- a/src/components/form/form-test.stories.tsx
+++ b/src/components/form/form-test.stories.tsx
@@ -1,5 +1,7 @@
 /* eslint-disable no-console */
-import React from "react";
+import React, { useState } from "react";
+
+import TextEditor, { EditorState } from "../text-editor";
 import Form from ".";
 import Button from "../button";
 import { Tab, Tabs } from "../tabs";
@@ -21,6 +23,7 @@ import NumeralDate from "../numeral-date";
 import Hr from "../hr";
 import InlineInputs from "../inline-inputs";
 import Pager from "../pager";
+import Password from "../password";
 
 export default {
   title: "Form/Test",
@@ -367,3 +370,87 @@ export const DefaultWithPager = () => (
 );
 
 DefaultWithPager.storyName = "default with pager";
+
+export const MockFormForAriaLiveDemo = () => {
+  const [textareaValue, setTextareaValue] = useState("");
+  const [textboxValue, setTextboxValue] = useState("");
+  const [textEditorValue, setTextEditorValue] = useState(
+    EditorState.createEmpty()
+  );
+  const [passwordValue, setPasswordValue] = useState("");
+
+  const resetValues = () => {
+    setTextareaValue("");
+    setTextboxValue("");
+    setTextEditorValue(EditorState.createEmpty());
+    setPasswordValue("");
+  };
+
+  const handleSubmit = (event: { preventDefault: () => void }) => {
+    event?.preventDefault(); // prevents the page from changing
+    console.log("Form has been submitted");
+    resetValues();
+  };
+
+  return (
+    <Form
+      fieldSpacing={0}
+      onSubmit={handleSubmit}
+      leftSideButtons={
+        <Button onClick={() => console.log("cancel")}>Cancel</Button>
+      }
+      saveButton={
+        <Button buttonType="primary" type="submit">
+          Save
+        </Button>
+      }
+      stickyFooter
+    >
+      <Textarea
+        mt={2}
+        label="Textarea"
+        value={textareaValue}
+        characterLimit={100}
+        onChange={(newValue: {
+          target: { value: React.SetStateAction<string> };
+        }) => {
+          setTextareaValue(newValue.target.value);
+        }}
+      />
+      <Textbox
+        mt={5}
+        label="Textbox"
+        value={textboxValue}
+        characterLimit={69}
+        onChange={(newValue: {
+          target: { value: React.SetStateAction<string> };
+        }) => {
+          setTextboxValue(newValue.target.value);
+        }}
+      />
+      <TextEditor
+        mt={5}
+        onChange={(newValue) => {
+          setTextEditorValue(newValue);
+        }}
+        value={textEditorValue}
+        labelText="Text Editor Label"
+        characterLimit={100}
+      />
+      <Password
+        mt={5}
+        mb={2}
+        label="Password"
+        characterLimit={20}
+        value={passwordValue}
+        onChange={(newValue: {
+          target: { value: React.SetStateAction<string> };
+        }) => {
+          setPasswordValue(newValue.target.value);
+        }}
+      />
+    </Form>
+  );
+};
+
+MockFormForAriaLiveDemo.storyName = "Mock form for aria live demo";
diff --git a/src/components/password/components.test-pw.tsx b/src/components/password/components.test-pw.tsx
index 1e4ab2e8c..da929eabf 100644
--- a/src/components/password/components.test-pw.tsx
+++ b/src/components/password/components.test-pw.tsx
@@ -161,3 +161,28 @@ export const PasswordNewDesignsValidation = () => {
     </Box>
   );
 };
+
+export const PasswordComponentWithCharacterCount = ({
+  onChange,
+}: PasswordProps) => {
+  const [state, setState] = useState("test");
+  const setValue = (ev: React.ChangeEvent<HTMLInputElement>) => {
+    setState(ev.target.value);
+    if (onChange) {
+      onChange(ev);
+    }
+  };
+  return (
+    <>
+      <Password
+        label="Password"
+        value={state}
+        onChange={setValue}
+        characterLimit={69}
+      />
+      <button type="button" onClick={() => {}}>
+        Click Me
+      </button>
+    </>
+  );
+};
diff --git a/src/components/text-editor/components.test-pw.tsx b/src/components/text-editor/components.test-pw.tsx
index d1f45bb7a..75713e671 100644
--- a/src/components/text-editor/components.test-pw.tsx
+++ b/src/components/text-editor/components.test-pw.tsx
@@ -116,3 +116,49 @@ export const TextEditorNewValidation = () => {
     </CarbonProvider>
   );
 };
+
+export const TextEditorCharacterCount = ({
+  onChange,
+  onLinkAdded,
+}: Partial<TextEditorProps>) => {
+  const [value, setValue] = React.useState(createContent());
+  const ref = React.useRef(null);
+
+  return (
+    <div
+      style={{
+        padding: "4px",
+      }}
+    >
+      <TextEditor
+        onChange={(newValue) => {
+          if (onChange) {
+            onChange(newValue);
+          }
+          setValue(newValue);
+        }}
+        value={value}
+        ref={ref}
+        labelText="Text Editor Label"
+        onLinkAdded={onLinkAdded}
+        toolbarElements={[
+          <Button key="cancel button" buttonType="tertiary" onClick={() => {}}>
+            Cancel
+          </Button>,
+          <Button
+            key="save button"
+            buttonType="primary"
+            type="button"
+            onClick={() => {}}
+          >
+            Save
+          </Button>,
+        ]}
+        characterLimit={69}
+      />
+      <button type="button" onClick={() => {}}>
+        Click Me
+      </button>
+    </div>
+  );
+};
diff --git a/src/components/textarea/components.test-pw.tsx b/src/components/textarea/components.test-pw.tsx
index 142320e64..08df245d3 100644
--- a/src/components/textarea/components.test-pw.tsx
+++ b/src/components/textarea/components.test-pw.tsx
@@ -140,6 +140,25 @@ export const CharacterLimitExample = () => {
   );
 };
 
+export const CharacterLimitExampleWithButton = () => {
+  const [value, setValue] = useState("");
+  return (
+    <>
+      <Textarea
+        label="Textarea"
+        inputHint="Hint text (optional)."
+        expandable
+        value={value}
+        onChange={({ target }) => setValue(target.value)}
+        characterLimit={50}
+      />
+      <button type="button" onClick={() => {}}>
+        Click Me
+      </button>
+    </>
+  );
+};
+
 export const LabelInlineExample = () => {
   return <Textarea label="Textarea" labelInline />;
 };
diff --git a/src/components/textbox/components.test-pw.tsx b/src/components/textbox/components.test-pw.tsx
index 3f6276bfa..04b12c17b 100644
--- a/src/components/textbox/components.test-pw.tsx
+++ b/src/components/textbox/components.test-pw.tsx
@@ -206,3 +206,23 @@ export const TextboxNewDesignsValidation = () => {
     </Box>
   );
 };
+
+export const TextboxComponentWithCharacterLimit = () => {
+  const [state, setState] = useState("Textbox");
+  const setValue = ({ target }: React.ChangeEvent<HTMLInputElement>) => {
+    setState(target.value);
+  };
+  return (
+    <>
+      <Textbox
+        label="Textbox"
+        value={state}
+        onChange={setValue}
+        characterLimit={69}
+      />
+      <button type="button" onClick={() => {}}>
+        Click Me
+      </button>
+    </>
+  );
+};