Repository: Sage/carbon (ts)

Issue:
Textarea characters remaining (CRLF option)
### Description

On Sage 200 UKI we are a product which has always run on Windows and are porting all of our forms to the web.

When users enter text in a text area, we convert line endings to CRLF in our back end just before saving to the database. This is required so that our Windows Forms based desktop client is able to display new lines correctly.

In our web front end we have yup validations which convert \n to \r\n before doing max length checks to ensure that user input will fit in our database without errors.

As web browsers work with just `\n`, the characters remaining displayed on the Carbon textarea ends up telling our users that they have characters remaining, while our yup validations which convert to CRLF tell the user they have exceeded the limit.


To make these consistent, we'd like to be able to either be able to optionally tell the Textarea component to do a conversion to CRLF when calculating characters remaining, or to be able to pass a custom function to calculate characters remaining if you think that would be better.

### Suggested solution

Either;

- A prop to optionally tell the Carbon Textarea to convert to CRLF before calculating remaining characters
- A prop that allows us to pass a custom function to calculate characters remaining

### Demo URL

_No response_

### Alternatives

Our alternative would be not showing characters remaining but this would be poor from a UX perspective.

### Additional context

_No response_

### Confidentiality

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

Pull request description:
feat(use-character-count): make hook public
fix #7242

### Proposed behaviour

<!--
A clear and concise description of what changes this PR makes. If applicable, include any UI screenshots to help explain your request. If you are a Sage contributor, please DO NOT share any commercially sensitive information, such as UI screenshots or code from Sage products.
-->

- `useCharacterCount` is a public hook. 
-  Add `aria-describedby` prop to text based input props, which gets added to the hint and error message description. 
- Deprecate `ariaDescribedBy` prop in text based components. 

### Current behaviour

<!--
A clear and concise description of the behaviour before this change. If applicable, include any UI screenshots to help explain your request. If you are a Sage contributor, please DO NOT share any commercially sensitive information, such as UI screenshots or code from Sage products.
-->

- `useCharacterCount` is an internal hook. 
- `ariaDescribedBy` prop is available in text based components. 

### 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
- [ ] All themes are supported if required
- [ ] Unit tests added or updated if required
- [ ] 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

<!-- Add any other context or links about the pull request here. -->

### Testing instructions

<!--
How can a reviewer test this PR?

If this PR addresses a pre-existing bug, please include a link to a sandbox that reproduces the original bug. A starter template has been provided to help you do this:
<https://stackblitz.com/fork/github/Parsium/carbon-starter>
-->


Patch:
diff --git a/src/__internal__/input/input.component.tsx b/src/__internal__/input/input.component.tsx
index ebe5a63c8..2c335f8e8 100644
--- a/src/__internal__/input/input.component.tsx
+++ b/src/__internal__/input/input.component.tsx
@@ -17,8 +17,13 @@ export interface CommonInputProps
   extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
   /* The default value alignment on the input */
   align?: "right" | "left";
-  /** The id of the element that describe the input. */
+  /**
+   * Set the ID of the input's description.
+   * @deprecated Please use `aria-describedby` instead.
+   */
   ariaDescribedBy?: string;
+  /** The ID of the input's description, is set along with hint text and error message. */
+  "aria-describedby"?: string;
   /** Override the variant component */
   as?: React.ElementType;
   /** If true the Component will be focused when rendered */
@@ -93,7 +98,8 @@ const Input = React.forwardRef<
     {
       align,
       "aria-labelledby": ariaLabelledBy,
-      ariaDescribedBy,
+      "aria-describedby": ariaDescribedBy,
+      ariaDescribedBy: ariaDescribedByDeprecated,
       placeholder,
       disabled,
       readOnly,
@@ -214,7 +220,10 @@ const Input = React.forwardRef<
         groupContext.hasMouseOver) &&
       validationIconId;
 
-    const descriptionList = ariaDescribedBy ? [ariaDescribedBy] : [];
+    const descriptionList =
+      ariaDescribedBy || ariaDescribedByDeprecated
+        ? [ariaDescribedBy || ariaDescribedByDeprecated]
+        : [];
 
     if (hasValidationPart) {
       descriptionList.push(validationIconId);
diff --git a/src/components/select/__internal__/select-textbox/select-textbox.component.tsx b/src/components/select/__internal__/select-textbox/select-textbox.component.tsx
index 8eccbdaf1..cba121263 100644
--- a/src/components/select/__internal__/select-textbox/select-textbox.component.tsx
+++ b/src/components/select/__internal__/select-textbox/select-textbox.component.tsx
@@ -172,7 +172,7 @@ const SelectTextbox = React.forwardRef(
     return (
       <SelectTextboxContext.Provider value={{ isInputInSelect: true }}>
         <Textbox
-          ariaDescribedBy={ariaDescribedBy}
+          aria-describedby={ariaDescribedBy}
           aria-label={ariaLabel}
           data-element="select-input"
           data-role="select-textbox"
diff --git a/src/components/select/simple-select/simple-select.component.tsx b/src/components/select/simple-select/simple-select.component.tsx
index 0d4484ce9..78bcc3fc2 100644
--- a/src/components/select/simple-select/simple-select.component.tsx
+++ b/src/components/select/simple-select/simple-select.component.tsx
@@ -554,7 +554,7 @@ export const SimpleSelect = React.forwardRef<
             aria-controls={selectListId.current}
             activeDescendantId={activeDescendantId}
             ariaLabelledby={ariaLabelledby}
-            ariaDescribedBy={ariaDescribedBy}
+            aria-describedby={ariaDescribedBy}
             isOpen={isOpen}
             {...getTextboxProps()}
           />
diff --git a/src/components/textarea/textarea.component.tsx b/src/components/textarea/textarea.component.tsx
index ddc620cea..38ffd441a 100644
--- a/src/components/textarea/textarea.component.tsx
+++ b/src/components/textarea/textarea.component.tsx
@@ -12,7 +12,7 @@ import { ValidationProps } from "../../__internal__/validations";
 import tagComponent, { TagProps } from "../../__internal__/utils/helpers/tags";
 import { CommonInputProps, InputPresentation } from "../../__internal__/input";
 import FormField from "../../__internal__/form-field";
-import useCharacterCount from "../../hooks/__internal__/useCharacterCount";
+import useCharacterCount from "../../hooks/useCharacterCount";
 
 import Input from "../../__internal__/input/input.component";
 import { InputBehaviour } from "../../__internal__/input-behaviour";
@@ -131,12 +131,15 @@ export interface TextareaProps
 }
 
 let deprecateUncontrolledWarnTriggered = false;
+let deprecatedAriaDescribedByWarnTriggered = false;
 let warnBorderRadiusArrayTooLarge = false;
 
 export const Textarea = React.forwardRef(
   (
     {
       "aria-labelledby": ariaLabelledBy,
+      "aria-describedby": ariaDescribedByProp,
+      ariaDescribedBy: ariaDescribedByPropDeprecated,
       autoFocus,
       inputHint,
       fieldHelp,
@@ -237,6 +240,16 @@ export const Textarea = React.forwardRef(
       );
     }
 
+    if (
+      !deprecatedAriaDescribedByWarnTriggered &&
+      ariaDescribedByPropDeprecated
+    ) {
+      deprecatedAriaDescribedByWarnTriggered = true;
+      Logger.deprecate(
+        "The `ariaDescribedBy` prop in `Textarea` is deprecated and will soon be removed, please use `aria-describedby` instead.",
+      );
+    }
+
     if (
       Array.isArray(borderRadius) &&
       borderRadius.length > 4 &&
@@ -337,6 +350,7 @@ export const Textarea = React.forwardRef(
       ariaDescribedBy,
       inputHintId,
       visuallyHiddenHintId,
+      ariaDescribedByProp || ariaDescribedByPropDeprecated,
     ]
       .filter(Boolean)
       .join(" ");
@@ -358,7 +372,7 @@ export const Textarea = React.forwardRef(
         <Input
           aria-invalid={!!error}
           aria-labelledby={ariaLabelledBy}
-          ariaDescribedBy={combinedAriaDescribedBy}
+          aria-describedby={combinedAriaDescribedBy}
           autoFocus={autoFocus}
           name={name}
           value={value}
diff --git a/src/components/textbox/textbox.component.tsx b/src/components/textbox/textbox.component.tsx
index e0b5fe383..1726ba721 100644
--- a/src/components/textbox/textbox.component.tsx
+++ b/src/components/textbox/textbox.component.tsx
@@ -20,7 +20,7 @@ import { filterStyledSystemMarginProps } from "../../style/utils";
 import NewValidationContext from "../carbon-provider/__internal__/new-validation.context";
 import NumeralDateContext from "../numeral-date/__internal__/numeral-date.context";
 import Logger from "../../__internal__/utils/logger";
-import useCharacterCount from "../../hooks/__internal__/useCharacterCount";
+import useCharacterCount from "../../hooks/useCharacterCount";
 import useUniqueId from "../../hooks/__internal__/useUniqueId";
 import guid from "../../__internal__/utils/helpers/guid";
 import useInputAccessibility from "../../hooks/__internal__/useInputAccessibility/useInputAccessibility";
@@ -138,11 +138,14 @@ export interface TextboxProps extends CommonTextboxProps {
 }
 
 let deprecateUncontrolledWarnTriggered = false;
+let deprecatedAriaDescribedByWarnTriggered = false;
 
 export const Textbox = React.forwardRef(
   (
     {
       "aria-labelledby": ariaLabelledBy,
+      "aria-describedby": ariaDescribedByProp,
+      ariaDescribedBy: ariaDescribedByDeprecated,
       align = ALIGN_DEFAULT,
       autoFocus,
       children,
@@ -236,6 +239,13 @@ export const Textbox = React.forwardRef(
       );
     }
 
+    if (!deprecatedAriaDescribedByWarnTriggered && ariaDescribedByDeprecated) {
+      deprecatedAriaDescribedByWarnTriggered = true;
+      Logger.deprecate(
+        "The `ariaDescribedBy` prop in `Textbox` is deprecated and will soon be removed, please use `aria-describedby` instead.",
+      );
+    }
+
     const { labelId, validationId, fieldHelpId, ariaDescribedBy } =
       useInputAccessibility({
         id: uniqueId,
@@ -254,6 +264,7 @@ export const Textbox = React.forwardRef(
       ariaDescribedBy,
       inputHintId,
       visuallyHiddenHintId,
+      ariaDescribedByProp || ariaDescribedByDeprecated,
     ]
       .filter(Boolean)
       .join(" ");
@@ -286,7 +297,7 @@ export const Textbox = React.forwardRef(
           align={align}
           aria-invalid={!!error}
           aria-labelledby={ariaLabelledBy}
-          ariaDescribedBy={combinedAriaDescribedBy}
+          aria-describedby={combinedAriaDescribedBy}
           autoFocus={autoFocus}
           deferTimeout={deferTimeout}
           disabled={disabled}
diff --git a/src/hooks/__internal__/useCharacterCount/index.ts b/src/hooks/useCharacterCount/index.ts
similarity index 100%
rename from src/hooks/__internal__/useCharacterCount/index.ts
rename to src/hooks/useCharacterCount/index.ts
diff --git a/src/hooks/useCharacterCount/use-character-count.mdx b/src/hooks/useCharacterCount/use-character-count.mdx
new file mode 100644
index 000000000..21445ee14
--- /dev/null
+++ b/src/hooks/useCharacterCount/use-character-count.mdx
@@ -0,0 +1,41 @@
+import { ArgTypes, Meta, Canvas } from "@storybook/addon-docs";
+
+import * as useCharacterCountStories from "./use-character-count.stories";
+
+<Meta title="Documentation/Hooks/useCharacterCount"/>
+
+# useCharacterCount
+
+`useCharacterCount` is a custom React hook that provides a character counter for text based input fields, and can be used in place of the `characterLimit` prop.
+
+The hook returns an array containing a JSX element and an ID as a string. The JSX element displays a visual character counter alongside a visually hidden hint that
+provides additional context to screen reader users. 
+
+To associate the character counter with the corresponding input field please ensure you pass the returned ID to the input's `aria-describedby` attribute. 
+This hook also implements a debounce function to ensure screen reader announcements occur after a brief delay after the user stops typing, rather than on every keypress.
+
+**Note:** This hook is currently not supported on the `TextEditor` component as this component uses its own custom character counter plugin.
+
+## Contents
+
+- [Import path](#import-path)
+- [Examples](#examples)
+- [Parameters](#parameters)
+
+## Import path
+
+```javascript
+import useCharacterCount from "carbon-react/lib/hooks/useCharacterCount";
+```
+
+## Examples
+
+Here are some examples of how to use the `useCharacterCount` hook:
+
+<Canvas of={useCharacterCountStories.WithTextbox} />
+
+<Canvas of={useCharacterCountStories.WithTextarea} />
+
+## Parameters
+
+<ArgTypes of={useCharacterCountStories} />
\ No newline at end of file
diff --git a/src/hooks/useCharacterCount/use-character-count.stories.tsx b/src/hooks/useCharacterCount/use-character-count.stories.tsx
new file mode 100644
index 000000000..0ed70ec5d
--- /dev/null
+++ b/src/hooks/useCharacterCount/use-character-count.stories.tsx
@@ -0,0 +1,85 @@
+import React, { useState } from "react";
+import { Meta, StoryObj } from "@storybook/react";
+
+import useCharacterCount from "./useCharacterCount";
+import Textbox from "../../components/textbox";
+import Textarea from "../../components/textarea";
+
+/**
+ * This file is used primarily as a means to generate the props table and examples.
+ * It contains the tag: ["hideInSidebar"] so that it is not included in the sidebar.
+ */
+
+const meta: Meta = {
+  tags: ["hideInSidebar"],
+  parameters: {
+    chromatic: {
+      disableSnapshot: true,
+    },
+  },
+  argTypes: {
+    value: {
+      type: { name: "string", required: true },
+      description: "The value of the input the character count is applied to.",
+    },
+    characterLimit: {
+      type: "number",
+      description: "The character limit of the input.",
+    },
+    characterCountAriaLive: {
+      table: {
+        type: { summary: '"off" | "polite"' },
+      },
+      description:
+        "Set the value of the aria-live attribute on the character count element.",
+    },
+  },
+};
+
+export default meta;
+
+export const WithTextbox: StoryObj = () => {
+  const [value, setValue] = useState("");
+  const [characterCount, characterCountId] = useCharacterCount(
+    value,
+    50,
+    "polite",
+  );
+
+  return (
+    <>
+      <Textbox
+        label="Textbox with character count"
+        inputHint="Input hint"
+        aria-describedby={characterCountId}
+        value={value}
+        onChange={(e) => setValue(e.target.value)}
+      />
+      {characterCount}
+    </>
+  );
+};
+WithTextbox.storyName = "With Textbox";
+
+export const WithTextarea: StoryObj = () => {
+  const [value, setValue] = useState("");
+  const [characterCount, characterCountId] = useCharacterCount(
+    value,
+    50,
+    "polite",
+  );
+
+  return (
+    <>
+      <Textarea
+        label="Textarea with character count"
+        inputHint="Input hint"
+        aria-describedby={characterCountId}
+        value={value}
+        onChange={(e) => setValue(e.target.value)}
+      />
+      {characterCount}
+    </>
+  );
+};
+WithTextarea.storyName = "With Textarea";
diff --git a/src/hooks/__internal__/useCharacterCount/useCharacterCount.test.tsx b/src/hooks/useCharacterCount/useCharacterCount.test.tsx
similarity index 100%
rename from src/hooks/__internal__/useCharacterCount/useCharacterCount.test.tsx
rename to src/hooks/useCharacterCount/useCharacterCount.test.tsx
diff --git a/src/hooks/__internal__/useCharacterCount/useCharacterCount.tsx b/src/hooks/useCharacterCount/useCharacterCount.tsx
similarity index 90%
rename from src/hooks/__internal__/useCharacterCount/useCharacterCount.tsx
rename to src/hooks/useCharacterCount/useCharacterCount.tsx
index 67b5d86d7..6d88eead0 100644
--- a/src/hooks/__internal__/useCharacterCount/useCharacterCount.tsx
+++ b/src/hooks/useCharacterCount/useCharacterCount.tsx
@@ -1,7 +1,7 @@
 import React, { useMemo, useRef, useEffect, useState } from "react";
-import CharacterCount from "../../../__internal__/character-count";
-import guid from "../../../__internal__/utils/helpers/guid";
-import useDebounce from "../useDebounce";
+import CharacterCount from "../../__internal__/character-count";
+import guid from "../../__internal__/utils/helpers/guid";
+import useDebounce from "../__internal__/useDebounce";
 
 const useCharacterCount = (
   value = "",
diff --git a/src/hooks/useMediaQuery/use-media-query.stories.tsx b/src/hooks/useMediaQuery/use-media-query.stories.tsx
index 5f1d4c340..725089893 100644
--- a/src/hooks/useMediaQuery/use-media-query.stories.tsx
+++ b/src/hooks/useMediaQuery/use-media-query.stories.tsx
@@ -1,18 +1,16 @@
 import { Meta, StoryObj } from "@storybook/react";
-import useMediaQuery from "./useMediaQuery";
 
 /**
  * This file is used primarily as a means to generate the props table.
  * It contains the tag: ["hideInSidebar"] so that it is not included in the sidebar.
  */
 
-const meta: Meta<typeof useMediaQuery> = {
+const meta: Meta = {
   tags: ["hideInSidebar"],
   argTypes: {
     query: {
-      type: { summary: "string" },
+      type: { name: "string", required: true },
       description: "Any valid CSS media query.",
-      required: true,
     },
   },
   parameters: {
@@ -21,9 +19,8 @@ const meta: Meta<typeof useMediaQuery> = {
 };
 
 export default meta;
-type Story = StoryObj<typeof useMediaQuery>;
 
-export const Default: Story = {
+export const Default: StoryObj = {
   args: {
     children: [],
   },


Test patch:
diff --git a/src/__internal__/input/input.test.tsx b/src/__internal__/input/input.test.tsx
index 0d9b8611f..a497fc45e 100644
--- a/src/__internal__/input/input.test.tsx
+++ b/src/__internal__/input/input.test.tsx
@@ -227,3 +227,29 @@ test("when the input is rendered with a type other than 'text', setSelectionRang
   expect(setSelectionRangeSpy).not.toHaveBeenCalled();
   jest.useRealTimers();
 });
+
+test("sets the provided `aria-describedby` to the accessible description of the input", () => {
+  render(<Input aria-describedby="description" />);
+
+  const input = screen.getByRole("textbox");
+  expect(input).toHaveAttribute("aria-describedby", "description");
+});
+
+test("sets the provided `ariaDescribedBy` to the accessible description of the input", () => {
+  render(<Input ariaDescribedBy="description" />);
+
+  const input = screen.getByRole("textbox");
+  expect(input).toHaveAttribute("aria-describedby", "description");
+});
+
+test("when both `aria-describedby` and `ariaDescribedBy` are provided, `aria-describedby` takes precedence", () => {
+  render(
+    <Input
+      aria-describedby="description"
+      ariaDescribedBy="I shouldn't be here"
+    />,
+  );
+
+  const input = screen.getByRole("textbox");
+  expect(input).toHaveAttribute("aria-describedby", "description");
+});
diff --git a/src/components/textarea/textarea.test.tsx b/src/components/textarea/textarea.test.tsx
index ee8b5ceca..2d3b715c6 100644
--- a/src/components/textarea/textarea.test.tsx
+++ b/src/components/textarea/textarea.test.tsx
@@ -31,6 +31,20 @@ test("should display deprecation warning once when rendered as uncontrolled", ()
   loggerSpy.mockRestore();
 });
 
+test("should display deprecation warning once for `ariaDescribedBy`", () => {
+  const loggerSpy = jest.spyOn(Logger, "deprecate");
+
+  render(<Textarea onChange={() => {}} ariaDescribedBy="test" />);
+
+  expect(loggerSpy).toHaveBeenCalledWith(
+    "The `ariaDescribedBy` prop in `Textarea` is deprecated and will soon be removed, please use `aria-describedby` instead.",
+  );
+
+  expect(loggerSpy).toHaveBeenCalledTimes(1);
+
+  loggerSpy.mockRestore();
+});
+
 test("should render a textarea element", () => {
   render(<Textarea />);
   const textarea = screen.getByRole("textbox");
@@ -323,6 +337,46 @@ test.each(["info", "warning", "error"])(
   },
 );
 
+test("sets the accessible label to the provided aria-labelledby", () => {
+  const Component = () => (
+    <>
+      <p id="test">label</p>
+      <Textarea aria-labelledby="test" />
+    </>
+  );
+  render(<Component />);
+
+  expect(screen.getByRole("textbox")).toHaveAccessibleName("label");
+});
+
+test("appends the provided `aria-describedby` to the accessible description", () => {
+  const Component = () => (
+    <>
+      <p id="test">description</p>
+      <Textarea inputHint="hint text" aria-describedby="test" />
+    </>
+  );
+  render(<Component />);
+
+  expect(screen.getByRole("textbox")).toHaveAccessibleDescription(
+    "hint text description",
+  );
+});
+
+test("appends the provided `ariaDescribedBy` to the accessible description", () => {
+  const Component = () => (
+    <>
+      <p id="test">description</p>
+      <Textarea inputHint="hint text" ariaDescribedBy="test" />
+    </>
+  );
+  render(<Component />);
+
+  expect(screen.getByRole("textbox")).toHaveAccessibleDescription(
+    "hint text description",
+  );
+});
+
 describe(`when the characterLimit prop is passed`, () => {
   it("renders a character counter with the appropriate message when the value is too long by 1 character", () => {
     const valueString = "foo";
diff --git a/src/components/textbox/textbox.test.tsx b/src/components/textbox/textbox.test.tsx
index ec776d50d..35f3ff7aa 100644
--- a/src/components/textbox/textbox.test.tsx
+++ b/src/components/textbox/textbox.test.tsx
@@ -45,6 +45,16 @@ test("should display deprecation warning once when rendered as uncontrolled", ()
   expect(loggerSpy).toHaveBeenCalledTimes(1);
 });
 
+test("should display deprecation warning once for `ariaDescribedby`", () => {
+  render(<Textbox onChange={() => {}} ariaDescribedBy="test" />);
+
+  expect(loggerSpy).toHaveBeenCalledWith(
+    "The `ariaDescribedBy` prop in `Textbox` is deprecated and will soon be removed, please use `aria-describedby` instead.",
+  );
+
+  expect(loggerSpy).toHaveBeenCalledTimes(1);
+});
+
 testStyledSystemMargin(
   (props) => <Textbox data-role="textbox-wrapper" {...props} />,
   () => screen.getByTestId("textbox-wrapper"),
@@ -85,7 +95,7 @@ describe(`when the characterLimit prop is passed`, () => {
     ).toHaveAttribute("id", mockedGuid);
   });
 
-  it("should reference the visually hidden hint id in the input's aria-described by", () => {
+  it("should reference the visually hidden hint id in the input's aria-describedby", () => {
     render(<Textbox value="foo" characterLimit={73} />);
     expect(screen.getByRole("textbox")).toHaveAttribute(
       "aria-describedby",
@@ -391,13 +401,43 @@ test("passes the helpAriaLabel prop down to the help component", () => {
   expect(screen.getByRole("button")).toHaveAttribute("aria-label", text);
 });
 
-test("passes the aria-labelledby prop down to the input", () => {
-  const labelId = "foo";
-  render(<Textbox aria-labelledby={labelId} />);
+test("sets the accessible label to the provided aria-labelledby", () => {
+  const Component = () => (
+    <>
+      <p id="test">label</p>
+      <Textbox aria-labelledby="test" />
+    </>
+  );
+  render(<Component />);
 
-  expect(screen.getByRole("textbox")).toHaveAttribute(
-    "aria-labelledby",
-    labelId,
+  expect(screen.getByRole("textbox")).toHaveAccessibleName("label");
+});
+
+test("appends the provided `aria-describedby` to the accessible description", () => {
+  const Component = () => (
+    <>
+      <p id="test">description</p>
+      <Textbox inputHint="hint text" aria-describedby="test" />
+    </>
+  );
+  render(<Component />);
+
+  expect(screen.getByRole("textbox")).toHaveAccessibleDescription(
+    "hint text description",
+  );
+});
+
+test("appends the provided `ariaDescribedBy` to the accessible description", () => {
+  const Component = () => (
+    <>
+      <p id="test">description</p>
+      <Textbox inputHint="hint text" ariaDescribedBy="test" />
+    </>
+  );
+  render(<Component />);
+
+  expect(screen.getByRole("textbox")).toHaveAccessibleDescription(
+    "hint text description",
   );
 });