@purpurds/text-area 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LICENSE.txt +52 -0
- package/dist/styles.css +1 -0
- package/dist/text-area.cjs.js +22 -0
- package/dist/text-area.cjs.js.map +1 -0
- package/dist/text-area.d.ts +52 -0
- package/dist/text-area.d.ts.map +1 -0
- package/dist/text-area.es.js +490 -0
- package/dist/text-area.es.js.map +1 -0
- package/package.json +67 -0
- package/readme.mdx +60 -0
- package/scripts/generate-assets.mts +22 -0
- package/src/assets/resize-handle-default-icon.svg +4 -0
- package/src/assets/resize-handle-disabled-icon.svg +4 -0
- package/src/global.d.ts +4 -0
- package/src/text-area.module.scss +118 -0
- package/src/text-area.stories.tsx +64 -0
- package/src/text-area.test.tsx +113 -0
- package/src/text-area.tsx +107 -0
package/readme.mdx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Meta, Stories, ArgTypes, Primary, Subtitle } from "@storybook/blocks";
|
|
2
|
+
|
|
3
|
+
import * as TextAreaStories from "./src/text-area.stories";
|
|
4
|
+
import packageInfo from "./package.json";
|
|
5
|
+
|
|
6
|
+
<Meta name="Docs" title="Components/TextArea" of={TextAreaStories} />
|
|
7
|
+
|
|
8
|
+
# TextArea
|
|
9
|
+
|
|
10
|
+
<Subtitle>Version {packageInfo.version}</Subtitle>
|
|
11
|
+
|
|
12
|
+
### Showcase
|
|
13
|
+
|
|
14
|
+
<Primary />
|
|
15
|
+
|
|
16
|
+
### Properties
|
|
17
|
+
|
|
18
|
+
Except for the props below, [all "native" textarea attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) are also valid props. The only exception is:
|
|
19
|
+
|
|
20
|
+
- `id` - Required
|
|
21
|
+
|
|
22
|
+
<ArgTypes />
|
|
23
|
+
|
|
24
|
+
### Installation
|
|
25
|
+
|
|
26
|
+
#### Via NPM
|
|
27
|
+
|
|
28
|
+
Add the dependency to your consumer app like `"@purpurds/text-area": "x.y.z"`
|
|
29
|
+
|
|
30
|
+
#### From outside the monorepo (build-time)
|
|
31
|
+
|
|
32
|
+
To install this package, you need to setup access to the artifactory. [Click here to go to the guide on how to do that](https://github.com/telia-company/jfrog-documentation/blob/main/doc/JFrog/JFrog_Onboarding.md#getting-access-to-artifactory-and-other-jfrog-applications).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
In MyApp.tsx
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import "@purpurds/tokens/index.css";
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
and
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import "@purpurds/text-area/styles";
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
In MyComponent.tsx
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { TextArea } from "@purpurds/text-area";
|
|
52
|
+
|
|
53
|
+
export const MyComponent = () => {
|
|
54
|
+
return (
|
|
55
|
+
<div>
|
|
56
|
+
<TextArea {...someProps}>Some content</TextArea>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { purpurColorTextMedium, purpurColorTextWeak } from "@purpurds/tokens";
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import dedent from "dedent";
|
|
6
|
+
|
|
7
|
+
const srcDir = path.join(process.cwd(), "src");
|
|
8
|
+
const assetsDir = path.join(srcDir, "assets");
|
|
9
|
+
|
|
10
|
+
[
|
|
11
|
+
{ name: "default", color: purpurColorTextMedium },
|
|
12
|
+
{ name: "disabled", color: purpurColorTextWeak },
|
|
13
|
+
].forEach(({ name, color }) => {
|
|
14
|
+
const svgFileContent = dedent`
|
|
15
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
|
16
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.29298 12.9028C1.90234 12.5121 1.90234 11.8788 2.29298 11.4881L11.4881 2.29298C11.8788 1.90234 12.5121 1.90234 12.9028 2.29298C13.2934 2.68362 13.2934 3.31698 12.9028 3.70762L3.70762 12.9028C3.31698 13.2934 2.68362 13.2934 2.29298 12.9028Z" fill="${color}" />
|
|
17
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.2948 14.307C7.90415 13.9164 7.90415 13.283 8.2948 12.8924L12.8924 8.2948C13.283 7.90415 13.9164 7.90415 14.307 8.2948C14.6977 8.68544 14.6977 9.3188 14.307 9.70944L9.70944 14.307C9.31879 14.6977 8.68544 14.6977 8.2948 14.307Z" fill="${color}" />
|
|
18
|
+
</svg>
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
fs.writeFileSync(path.join(assetsDir, `resize-handle-${name}-icon.svg`), svgFileContent);
|
|
22
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.29298 12.9028C1.90234 12.5121 1.90234 11.8788 2.29298 11.4881L11.4881 2.29298C11.8788 1.90234 12.5121 1.90234 12.9028 2.29298C13.2934 2.68362 13.2934 3.31698 12.9028 3.70762L3.70762 12.9028C3.31698 13.2934 2.68362 13.2934 2.29298 12.9028Z" fill="rgba(0, 0, 0, 0.62)" />
|
|
3
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.2948 14.307C7.90415 13.9164 7.90415 13.283 8.2948 12.8924L12.8924 8.2948C13.283 7.90415 13.9164 7.90415 14.307 8.2948C14.6977 8.68544 14.6977 9.3188 14.307 9.70944L9.70944 14.307C9.31879 14.6977 8.68544 14.6977 8.2948 14.307Z" fill="rgba(0, 0, 0, 0.62)" />
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.29298 12.9028C1.90234 12.5121 1.90234 11.8788 2.29298 11.4881L11.4881 2.29298C11.8788 1.90234 12.5121 1.90234 12.9028 2.29298C13.2934 2.68362 13.2934 3.31698 12.9028 3.70762L3.70762 12.9028C3.31698 13.2934 2.68362 13.2934 2.29298 12.9028Z" fill="rgba(0, 0, 0, 0.44)" />
|
|
3
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.2948 14.307C7.90415 13.9164 7.90415 13.283 8.2948 12.8924L12.8924 8.2948C13.283 7.90415 13.9164 7.90415 14.307 8.2948C14.6977 8.68544 14.6977 9.3188 14.307 9.70944L9.70944 14.307C9.31879 14.6977 8.68544 14.6977 8.2948 14.307Z" fill="rgba(0, 0, 0, 0.44)" />
|
|
4
|
+
</svg>
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
.purpur-text-area {
|
|
2
|
+
$root: &;
|
|
3
|
+
|
|
4
|
+
position: relative;
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
gap: var(--purpur-spacing-50);
|
|
8
|
+
max-width: 100%;
|
|
9
|
+
|
|
10
|
+
&__label {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
gap: var(--purpur-spacing-50);
|
|
14
|
+
width: fit-content;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
&__label-text {
|
|
18
|
+
width: fit-content;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
&__frame {
|
|
22
|
+
position: absolute;
|
|
23
|
+
inset: 0;
|
|
24
|
+
border: var(--purpur-border-width-xs) solid var(--purpur-color-border-interactive-subtle);
|
|
25
|
+
border-radius: var(--purpur-border-radius-sm);
|
|
26
|
+
pointer-events: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&__textarea-container {
|
|
30
|
+
position: relative;
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
width: 100%;
|
|
34
|
+
padding: 0 var(--purpur-spacing-100) var(--purpur-spacing-100) 0;
|
|
35
|
+
border-radius: var(--purpur-border-radius-sm);
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
background: var(--purpur-color-background-primary);
|
|
38
|
+
|
|
39
|
+
&--disabled {
|
|
40
|
+
background: var(--purpur-color-background-interactive-disabled);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&--readonly {
|
|
44
|
+
background: var(--purpur-color-background-interactive-read-only);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&__textarea {
|
|
49
|
+
$textAreaRoot: &;
|
|
50
|
+
|
|
51
|
+
width: 100%;
|
|
52
|
+
max-width: 100%; /* Needed to keep the textarea inside its container when resizing in Firefox */
|
|
53
|
+
padding: var(--purpur-spacing-150) 0 var(--purpur-spacing-200) var(--purpur-spacing-150);
|
|
54
|
+
outline: none;
|
|
55
|
+
border: none;
|
|
56
|
+
border-radius: var(--purpur-border-radius-sm);
|
|
57
|
+
font-family: var(--purpur-typography-family-default), Helvetica, Arial, "Lucida Grande",
|
|
58
|
+
sans-serif;
|
|
59
|
+
font-size: var(--purpur-typography-scale-100);
|
|
60
|
+
line-height: 150%;
|
|
61
|
+
background: transparent;
|
|
62
|
+
|
|
63
|
+
&::-webkit-resizer {
|
|
64
|
+
background: url("./assets/resize-handle-default-icon.svg");
|
|
65
|
+
background-size: cover;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
&:hover {
|
|
69
|
+
~ #{$root}__frame {
|
|
70
|
+
border-width: var(--purpur-border-width-sm);
|
|
71
|
+
border-color: var(--purpur-color-border-interactive-subtle-hover);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
&:active:not(:disabled):not(:read-only),
|
|
76
|
+
&:focus:not(:disabled):not(:read-only) {
|
|
77
|
+
~ #{$root}__frame {
|
|
78
|
+
outline: var(--purpur-border-width-sm) solid var(--purpur-color-border-interactive-focus);
|
|
79
|
+
outline-offset: calc(var(--purpur-spacing-10) * 2);
|
|
80
|
+
border-width: var(--purpur-border-width-xs);
|
|
81
|
+
border-color: var(--purpur-color-border-interactive-subtle-hover);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
&:disabled {
|
|
86
|
+
color: var(--purpur-color-text-weak);
|
|
87
|
+
|
|
88
|
+
~ #{$root}__frame {
|
|
89
|
+
border-width: var(--purpur-border-width-xs);
|
|
90
|
+
border-color: var(--purpur-color-border-medium);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
&::-webkit-resizer {
|
|
94
|
+
background: url("./assets/resize-handle-disabled-icon.svg");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
&:read-only:not(:disabled) {
|
|
99
|
+
color: var(--purpur-color-text-default);
|
|
100
|
+
|
|
101
|
+
~ #{$root}__frame {
|
|
102
|
+
border-width: var(--purpur-border-width-xs);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&:not(#{$textAreaRoot}--valid):not(#{$textAreaRoot}--error) {
|
|
106
|
+
~ #{$root}__frame {
|
|
107
|
+
border-color: var(--purpur-color-border-medium);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
&#{$textAreaRoot}--error {
|
|
113
|
+
~ #{$root}__frame {
|
|
114
|
+
border-color: var(--purpur-color-border-status-error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useArgs } from "@storybook/client-api";
|
|
3
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
4
|
+
|
|
5
|
+
import "@purpurds/label/styles";
|
|
6
|
+
import "@purpurds/field-helper-text/styles";
|
|
7
|
+
import "@purpurds/field-error-text/styles";
|
|
8
|
+
import { TextArea } from "./text-area";
|
|
9
|
+
|
|
10
|
+
const meta: Meta<typeof TextArea> = {
|
|
11
|
+
title: "Inputs/TextArea",
|
|
12
|
+
component: TextArea,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof TextArea>;
|
|
17
|
+
|
|
18
|
+
export const Showcase: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
value: "Text area",
|
|
21
|
+
label: "Text area label",
|
|
22
|
+
helperText: "Helper text",
|
|
23
|
+
placeholder: "Enter text",
|
|
24
|
+
errorText: undefined,
|
|
25
|
+
disabled: false,
|
|
26
|
+
readOnly: false,
|
|
27
|
+
required: false,
|
|
28
|
+
rows: 3,
|
|
29
|
+
cols: 90,
|
|
30
|
+
},
|
|
31
|
+
argTypes: {
|
|
32
|
+
onChange: { action: "inputChange", table: { disable: true } },
|
|
33
|
+
},
|
|
34
|
+
parameters: {
|
|
35
|
+
design: [
|
|
36
|
+
{
|
|
37
|
+
name: "TextArea",
|
|
38
|
+
type: "figma",
|
|
39
|
+
url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?type=design&node-id=1565-1896&mode=design&t=diHAdohOrRRVp09w-0",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
decorators: [
|
|
44
|
+
(Story) => (
|
|
45
|
+
<div style={{ maxWidth: "18.5rem" }}>
|
|
46
|
+
<Story />
|
|
47
|
+
</div>
|
|
48
|
+
),
|
|
49
|
+
],
|
|
50
|
+
render: ({ onChange, ...args }) => {
|
|
51
|
+
const [{ value }, updateArgs] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
|
|
52
|
+
return (
|
|
53
|
+
<TextArea
|
|
54
|
+
{...args}
|
|
55
|
+
id="playground-textarea"
|
|
56
|
+
value={value}
|
|
57
|
+
onChange={(e) => {
|
|
58
|
+
onChange?.(e);
|
|
59
|
+
updateArgs({ value: e.target.value });
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import * as matchers from "@testing-library/jest-dom/matchers";
|
|
3
|
+
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { TextArea } from "./text-area";
|
|
7
|
+
const rootClassName = "purpur-text-area";
|
|
8
|
+
|
|
9
|
+
expect.extend(matchers);
|
|
10
|
+
|
|
11
|
+
describe("TextArea", () => {
|
|
12
|
+
afterEach(cleanup);
|
|
13
|
+
|
|
14
|
+
it("should render plain", () => {
|
|
15
|
+
render(<TextArea id="test" data-testid="test" />);
|
|
16
|
+
|
|
17
|
+
const textarea = screen.getByTestId("test-textarea");
|
|
18
|
+
expect(textarea).toBeInTheDocument();
|
|
19
|
+
expect(textarea.className).toBe(`${rootClassName}__textarea`);
|
|
20
|
+
expect(screen.queryByTestId("test-label")).not.toBeInTheDocument();
|
|
21
|
+
expect(screen.queryByTestId("test-helper-text")).not.toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should render with label", () => {
|
|
25
|
+
render(<TextArea id="test" data-testid="test" label="Test label" />);
|
|
26
|
+
|
|
27
|
+
const textarea = screen.getByTestId("test-textarea");
|
|
28
|
+
expect(textarea).toBeInTheDocument();
|
|
29
|
+
expect(textarea.className).toBe(`${rootClassName}__textarea`);
|
|
30
|
+
expect(screen.getByTestId("test-label")).toHaveTextContent("Test label");
|
|
31
|
+
expect(screen.queryByTestId("test-helper-text")).not.toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should render with helper-text", () => {
|
|
35
|
+
render(<TextArea id="test" data-testid="test" label="Test label" helperText="Helper text" />);
|
|
36
|
+
|
|
37
|
+
const textarea = screen.getByTestId("test-textarea");
|
|
38
|
+
expect(textarea).toBeInTheDocument();
|
|
39
|
+
expect(textarea.className).toBe(`${rootClassName}__textarea`);
|
|
40
|
+
expect(screen.getByTestId("test-label")).toHaveTextContent("Test label");
|
|
41
|
+
expect(screen.getByTestId("test-helper-text")).toHaveTextContent("Helper text");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should render with helper-text and error-text", () => {
|
|
45
|
+
render(
|
|
46
|
+
<TextArea
|
|
47
|
+
errorText="Error text"
|
|
48
|
+
id="test"
|
|
49
|
+
data-testid="test"
|
|
50
|
+
label="Test label"
|
|
51
|
+
helperText="Helper text"
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const textarea = screen.getByTestId("test-textarea");
|
|
56
|
+
expect(textarea).toBeInTheDocument();
|
|
57
|
+
expect(textarea).toHaveClass(`${rootClassName}__textarea--error`);
|
|
58
|
+
expect(screen.getByTestId("test-label")).toHaveTextContent("Test label");
|
|
59
|
+
expect(screen.getByTestId("test-helper-text")).toHaveTextContent("Helper text");
|
|
60
|
+
expect(screen.getByTestId("test-error-text")).toHaveTextContent("Error text");
|
|
61
|
+
expect(screen.getByTestId("test-error-text").querySelector("svg")).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should render required", () => {
|
|
65
|
+
render(
|
|
66
|
+
<TextArea required id="test" data-testid="test" label="Test label" helperText="Helper text" />
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const textarea = screen.getByTestId("test-textarea");
|
|
70
|
+
expect(textarea).toBeRequired();
|
|
71
|
+
expect(textarea).not.toHaveClass(`${rootClassName}__textarea--error`);
|
|
72
|
+
expect(screen.getByTestId("test-label")).toHaveTextContent("* Test label");
|
|
73
|
+
expect(screen.getByTestId("test-helper-text")).toHaveTextContent("Helper text");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should render disabled", () => {
|
|
77
|
+
render(
|
|
78
|
+
<TextArea disabled id="test" data-testid="test" label="Test label" helperText="Helper text" />
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const textarea = screen.getByTestId("test-textarea");
|
|
82
|
+
expect(textarea).toBeDisabled();
|
|
83
|
+
expect(textarea).not.toHaveClass(`${rootClassName}__textarea--error`);
|
|
84
|
+
expect(screen.getByTestId("test-label")).toHaveTextContent("Test label");
|
|
85
|
+
expect(screen.getByTestId("test-helper-text")).toHaveTextContent("Helper text");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should render readOnly", () => {
|
|
89
|
+
render(
|
|
90
|
+
<TextArea readOnly id="test" data-testid="test" label="Test label" helperText="Helper text" />
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const textarea = screen.getByTestId("test-textarea");
|
|
94
|
+
expect(textarea).toHaveAttribute("readOnly");
|
|
95
|
+
expect(textarea).not.toHaveClass(`${rootClassName}__textarea--error`);
|
|
96
|
+
expect(screen.getByTestId("test-label")).toHaveTextContent("Test label");
|
|
97
|
+
expect(screen.getByTestId("test-helper-text")).toHaveTextContent("Helper text");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should render with default value and onChange", () => {
|
|
101
|
+
const onChangeMock = vi.fn();
|
|
102
|
+
render(
|
|
103
|
+
<TextArea id="test" data-testid="test" onChange={onChangeMock} defaultValue="Default value" />
|
|
104
|
+
);
|
|
105
|
+
const textarea = screen.getByTestId("test-textarea");
|
|
106
|
+
expect(textarea).toHaveValue("Default value");
|
|
107
|
+
|
|
108
|
+
fireEvent.change(textarea, { target: { value: "Changed" } });
|
|
109
|
+
|
|
110
|
+
expect(textarea).toHaveValue("Changed");
|
|
111
|
+
expect(onChangeMock).toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React, { ComponentPropsWithoutRef, ForwardedRef, forwardRef } from "react";
|
|
2
|
+
import { FieldErrorText } from "@purpurds/field-error-text";
|
|
3
|
+
import { FieldHelperText } from "@purpurds/field-helper-text";
|
|
4
|
+
import { Label } from "@purpurds/label";
|
|
5
|
+
import c from "classnames";
|
|
6
|
+
|
|
7
|
+
import styles from "./text-area.module.scss";
|
|
8
|
+
|
|
9
|
+
export type TextAreaProps = ComponentPropsWithoutRef<"textarea"> & {
|
|
10
|
+
id: string;
|
|
11
|
+
["data-testid"]?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Use to render error message below the text area. The text area renders with error appearance.
|
|
15
|
+
* */
|
|
16
|
+
errorText?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Use to give context about the text area's input. Renders below the text area.
|
|
19
|
+
* */
|
|
20
|
+
helperText?: string;
|
|
21
|
+
/**
|
|
22
|
+
* The label of the text area.
|
|
23
|
+
* */
|
|
24
|
+
label?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The height of the text area, measured in number of rows.
|
|
27
|
+
* */
|
|
28
|
+
rows?: number;
|
|
29
|
+
/**
|
|
30
|
+
* The width of the text area, measured in number of cols.
|
|
31
|
+
* */
|
|
32
|
+
cols?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const rootClassName = "purpur-text-area";
|
|
36
|
+
|
|
37
|
+
const TextAreaComponent = (
|
|
38
|
+
{
|
|
39
|
+
id,
|
|
40
|
+
["data-testid"]: dataTestId,
|
|
41
|
+
className,
|
|
42
|
+
errorText,
|
|
43
|
+
helperText,
|
|
44
|
+
label,
|
|
45
|
+
rows = 3,
|
|
46
|
+
cols = 90,
|
|
47
|
+
...props
|
|
48
|
+
}: TextAreaProps,
|
|
49
|
+
ref: ForwardedRef<HTMLTextAreaElement>
|
|
50
|
+
) => {
|
|
51
|
+
const getTestId = (name: string) => (dataTestId ? `${dataTestId}-${name}` : undefined);
|
|
52
|
+
|
|
53
|
+
const helperTextId = helperText ? `${id}-helper-text` : undefined;
|
|
54
|
+
|
|
55
|
+
const textAreaContainerClassnames = c([
|
|
56
|
+
styles[`${rootClassName}__textarea-container`],
|
|
57
|
+
{
|
|
58
|
+
[styles[`${rootClassName}__textarea-container--disabled`]]: props.disabled,
|
|
59
|
+
[styles[`${rootClassName}__textarea-container--readonly`]]: props.readOnly && !props.disabled,
|
|
60
|
+
},
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className={c(className, styles[rootClassName])} data-testid={dataTestId}>
|
|
65
|
+
{label && (
|
|
66
|
+
<Label
|
|
67
|
+
htmlFor={id}
|
|
68
|
+
disabled={props.disabled}
|
|
69
|
+
className={styles[`${rootClassName}__label`]}
|
|
70
|
+
data-testid={getTestId("label")}
|
|
71
|
+
>
|
|
72
|
+
{`${props.required ? "* " : ""}${label}`}
|
|
73
|
+
</Label>
|
|
74
|
+
)}
|
|
75
|
+
<div className={textAreaContainerClassnames}>
|
|
76
|
+
<textarea
|
|
77
|
+
{...props}
|
|
78
|
+
ref={ref}
|
|
79
|
+
id={id}
|
|
80
|
+
data-testid={getTestId("textarea")}
|
|
81
|
+
aria-describedby={props["aria-describedby"] || helperTextId}
|
|
82
|
+
aria-invalid={props["aria-invalid"] || !!errorText}
|
|
83
|
+
className={c([
|
|
84
|
+
styles[`${rootClassName}__textarea`],
|
|
85
|
+
{
|
|
86
|
+
[styles[`${rootClassName}__textarea--error`]]: !!errorText,
|
|
87
|
+
},
|
|
88
|
+
])}
|
|
89
|
+
rows={rows}
|
|
90
|
+
cols={cols}
|
|
91
|
+
/>
|
|
92
|
+
<div className={styles[`${rootClassName}__frame`]} />
|
|
93
|
+
</div>
|
|
94
|
+
{helperTextId && (
|
|
95
|
+
<FieldHelperText data-testid={getTestId("helper-text")} id={helperTextId}>
|
|
96
|
+
{helperText}
|
|
97
|
+
</FieldHelperText>
|
|
98
|
+
)}
|
|
99
|
+
{errorText && (
|
|
100
|
+
<FieldErrorText data-testid={getTestId("error-text")}>{errorText}</FieldErrorText>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const TextArea = forwardRef(TextAreaComponent);
|
|
107
|
+
TextArea.displayName = "TextArea";
|