@sproutsocial/seeds-react-textarea 1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/Textarea.tsx","../src/styles.ts","../src/TextareaTypes.ts"],"sourcesContent":["import Textarea from \"./Textarea\";\n\nexport default Textarea;\nexport { Textarea };\nexport * from \"./TextareaTypes\";\n","import * as React from \"react\";\nimport Container, { Accessory } from \"./styles\";\nimport type { TypeTextareaProps } from \"./TextareaTypes\";\n\n/**\n * @deprecated Use TypeTextareaProps from root instead\n */\nexport type TypeProps = TypeTextareaProps;\n\nexport default class Textarea extends React.Component<TypeTextareaProps> {\n static defaultProps = {\n rows: 4,\n };\n\n handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {\n if (this.props.onBlur) {\n this.props.onBlur(e);\n }\n };\n\n handleChange = (e: React.SyntheticEvent<HTMLTextAreaElement>) => {\n if (this.props.onChange) {\n this.props.onChange(e);\n }\n };\n\n handleFocus = (e: React.FocusEvent<HTMLTextAreaElement>) => {\n if (this.props.onFocus) {\n this.props.onFocus(e);\n }\n };\n\n handleKeyDown = (e: React.SyntheticEvent<HTMLTextAreaElement>) => {\n if (this.props.onKeyDown) {\n this.props.onKeyDown(e);\n }\n };\n\n handleKeyUp = (e: React.SyntheticEvent<HTMLTextAreaElement>) => {\n if (this.props.onKeyUp) {\n this.props.onKeyUp(e);\n }\n };\n\n override render() {\n const {\n autoFocus,\n disabled,\n readOnly,\n isInvalid,\n id,\n name,\n placeholder,\n value,\n enableSpellcheck,\n enableResize,\n required,\n rows,\n elemBefore,\n elemAfter,\n maxLength,\n ariaLabel,\n ariaDescribedby,\n innerRef,\n onBlur,\n onChange,\n onFocus,\n onKeyDown,\n onKeyUp,\n color,\n qa = {},\n inputProps = {},\n ...rest\n } = this.props;\n\n return (\n <Container\n hasBeforeElement={!!elemBefore}\n hasAfterElement={!!elemAfter}\n disabled={disabled}\n invalid={isInvalid}\n resizable={enableResize}\n // TODO: fix this type since `color` should be valid here. TS can't resolve the correct type.\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n color={color}\n data-qa-textarea={id}\n data-qa-textarea-isdisabled={disabled === true}\n data-qa-textarea-isrequired={required === true}\n data-qa-textarea-isinvalid={isInvalid === true}\n {...qa}\n {...rest}\n >\n {elemBefore && <Accessory before>{elemBefore}</Accessory>}\n\n <textarea\n id={id}\n aria-label={ariaLabel}\n aria-describedby={ariaDescribedby}\n aria-invalid={isInvalid}\n value={value}\n name={name}\n placeholder={placeholder}\n rows={rows}\n disabled={disabled}\n readOnly={Boolean(readOnly)}\n autoFocus={autoFocus}\n spellCheck={enableSpellcheck}\n required={required}\n maxLength={maxLength}\n onChange={this.handleChange}\n onKeyUp={this.handleKeyUp}\n onKeyDown={this.handleKeyDown}\n onBlur={this.handleBlur}\n onFocus={this.handleFocus}\n ref={innerRef}\n data-qa-textarea-input=\"\"\n data-qa-input={name}\n {...inputProps}\n />\n\n {elemAfter && <Accessory after>{elemAfter}</Accessory>}\n </Container>\n );\n }\n}\n","import styled, { css } from \"styled-components\";\nimport { COMMON } from \"@sproutsocial/seeds-react-system-props\";\nimport { focusRing } from \"@sproutsocial/seeds-react-mixins\";\nimport type {\n TypeTextareaAccessoryProps,\n TypeTextareaContainerProps,\n} from \"./TextareaTypes\";\n\nconst Container = styled.div<TypeTextareaContainerProps>`\n box-sizing: border-box;\n position: relative;\n\n textarea {\n box-sizing: border-box;\n display: block;\n width: 100%;\n padding: ${(props) => props.theme.space[300]};\n border: 1px solid ${(props) => props.theme.colors.form.border.base};\n border-radius: ${(props) => props.theme.radii[500]};\n background-color: ${(props) => props.theme.colors.form.background.base};\n color: ${(props) => props.theme.colors.text.body};\n outline: none;\n resize: none;\n transition: border-color ${(props) => props.theme.duration.fast}\n ${(props) => props.theme.easing.ease_in},\n box-shadow ${(props) => props.theme.duration.fast}\n ${(props) => props.theme.easing.ease_in};\n font-family: ${(props) => props.theme.fontFamily};\n ${(props) => props.theme.typography[200]}\n font-weight: ${(props) => props.theme.fontWeights.normal};\n appearance: none;\n\n &:focus {\n ${focusRing}\n }\n\n &::placeholder {\n color: ${(props) => props.theme.colors.form.placeholder.base};\n font-style: italic;\n }\n\n ${(props) =>\n props.resizable &&\n css`\n resize: vertical;\n `}\n\n ${(props) =>\n props.hasBeforeElement &&\n css`\n padding-left: 40px;\n `}\n\n ${(props) =>\n props.hasAfterElement &&\n css`\n padding-right: 40px;\n `}\n }\n\n ${(props) =>\n props.disabled &&\n css`\n opacity: 0.4;\n\n textarea {\n cursor: not-allowed;\n }\n `}\n\n ${(props) =>\n props.invalid &&\n css`\n textarea {\n border-color: ${(props) => props.theme.colors.form.border.error};\n }\n `}\n\n ${COMMON}\n`;\n\nexport const Accessory = styled.div<TypeTextareaAccessoryProps>`\n position: absolute;\n color: ${(props) => props.theme.colors.icon.base};\n\n ${(props) =>\n props.before &&\n css`\n top: ${props.theme.space[300]};\n left: ${props.theme.space[350]};\n `};\n\n ${(props) =>\n props.after &&\n css`\n right: ${props.theme.space[350]};\n bottom: ${props.theme.space[300]};\n `};\n`;\n\nContainer.displayName = \"TextareaContainer\";\nAccessory.displayName = \"TextareaAccessory\";\n\nexport default Container;\n","import * as React from \"react\";\nimport type {\n TypeSystemCommonProps,\n TypeStyledComponentsCommonProps,\n} from \"@sproutsocial/seeds-react-system-props\";\n\ntype CollisionTypes =\n | \"color\"\n | \"onChange\"\n | \"onFocus\"\n | \"onBlur\"\n | \"onKeyDown\"\n | \"onKeyUp\";\n\nexport interface TypeTextareaProps\n extends TypeStyledComponentsCommonProps,\n TypeSystemCommonProps,\n Omit<React.ComponentPropsWithoutRef<\"textarea\">, CollisionTypes> {\n /** ID of the form element, should match the \"for\" value of the associated label */\n id: string;\n name: string;\n\n /** Label used to describe the input if not used with an accompanying visual label */\n ariaLabel?: string;\n\n /** Attribute used to associate other elements that describe the Textarea, like an error */\n ariaDescribedby?: string;\n\n /** Current value of the textarea */\n value?: string;\n\n /** Will autofocus the element when mounted to the DOM */\n autoFocus?: boolean;\n\n /** HTML disabled attribute */\n disabled?: boolean;\n\n /** HTML readonly attribute */\n readOnly?: boolean;\n\n /** Whether or not the current contents of the input are invalid */\n isInvalid?: boolean;\n\n /** Placeholder text for when value is undefined or empty */\n placeholder?: string;\n\n /** HTML required attribute */\n required?: boolean;\n\n /** 16x16 element, such as an icon */\n elemBefore?: React.ReactNode;\n\n /** 16x16 element, such as an icon */\n elemAfter?: React.ReactNode;\n\n /** Max length of the input */\n maxLength?: number;\n\n /** HTML spellcheck attribute */\n enableSpellcheck?: boolean;\n\n /** Makes the text area vertically resizable */\n enableResize?: boolean;\n\n /** The number of visible lines of text without scrolling */\n rows?: number;\n qa?: object;\n\n /** Props to spread onto the underlying textarea element */\n inputProps?: React.ComponentPropsWithoutRef<\"textarea\">;\n\n /** Used to get a reference to the underlying element */\n innerRef?:\n | {\n current: null | HTMLTextAreaElement;\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n | ((arg0: React.ElementRef<any> | HTMLElement) => void);\n onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;\n onChange?: (e: React.SyntheticEvent<HTMLTextAreaElement>) => void;\n onFocus?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;\n onKeyDown?: (e: React.SyntheticEvent<HTMLTextAreaElement>) => void;\n onKeyUp?: (e: React.SyntheticEvent<HTMLTextAreaElement>) => void;\n}\n\nexport interface TypeTextareaContainerProps {\n resizable?: TypeTextareaProps[\"enableResize\"];\n hasBeforeElement?: boolean;\n hasAfterElement?: boolean;\n disabled?: TypeTextareaProps[\"disabled\"];\n invalid?: TypeTextareaProps[\"isInvalid\"];\n}\n\nexport interface TypeTextareaAccessoryProps {\n before?: boolean;\n after?: boolean;\n}\n"],"mappings":"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","names":["styled","props","React"]}
package/jest.config.js ADDED
@@ -0,0 +1,9 @@
1
+ const baseConfig = require("@sproutsocial/seeds-testing");
2
+
3
+ /** * @type {import('jest').Config} */
4
+ const config = {
5
+ ...baseConfig,
6
+ displayName: "seeds-react-textarea",
7
+ };
8
+
9
+ module.exports = config;
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@sproutsocial/seeds-react-textarea",
3
+ "version": "1.0.0",
4
+ "description": "Seeds React Textarea",
5
+ "author": "Sprout Social, Inc.",
6
+ "license": "MIT",
7
+ "main": "dist/index.js",
8
+ "module": "dist/esm/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "scripts": {
11
+ "build": "tsup --dts",
12
+ "build:debug": "tsup --dts --metafile",
13
+ "dev": "tsup --watch --dts",
14
+ "clean": "rm -rf .turbo dist",
15
+ "clean:modules": "rm -rf node_modules",
16
+ "typecheck": "tsc --noEmit",
17
+ "test": "jest",
18
+ "test:watch": "jest --watch --coverage=false"
19
+ },
20
+ "dependencies": {
21
+ "@sproutsocial/seeds-react-theme": "*",
22
+ "@sproutsocial/seeds-react-system-props": "*",
23
+ "@sproutsocial/seeds-react-mixins": "*"
24
+ },
25
+ "devDependencies": {
26
+ "@types/react": "^18.0.0",
27
+ "@types/styled-components": "^5.1.26",
28
+ "@sproutsocial/eslint-config-seeds": "*",
29
+ "react": "^18.0.0",
30
+ "styled-components": "^5.2.3",
31
+ "tsup": "^8.0.2",
32
+ "typescript": "^5.6.2",
33
+ "@sproutsocial/seeds-tsconfig": "*",
34
+ "@sproutsocial/seeds-testing": "*",
35
+ "@sproutsocial/seeds-react-testing-library": "*",
36
+ "@sproutsocial/seeds-react-text": "*"
37
+ },
38
+ "peerDependencies": {
39
+ "styled-components": "^5.2.3"
40
+ },
41
+ "engines": {
42
+ "node": ">=18"
43
+ }
44
+ }
@@ -0,0 +1,97 @@
1
+ import React from "react";
2
+ import { Icon } from "@sproutsocial/seeds-react-icon";
3
+ import { Textarea } from "./";
4
+ import type { Meta, StoryObj } from "@storybook/react";
5
+
6
+ const meta: Meta<typeof Textarea> = {
7
+ title: "Components/Form Elements/Textarea",
8
+ component: Textarea,
9
+ argTypes: {
10
+ isInvalid: {
11
+ control: "boolean",
12
+ },
13
+ disabled: {
14
+ control: "boolean",
15
+ },
16
+ readOnly: {
17
+ control: "boolean",
18
+ },
19
+ },
20
+ args: {
21
+ id: "textarea",
22
+ value: "",
23
+ name: "basic-textarea",
24
+ ariaLabel: "Descriptive label",
25
+ isInvalid: undefined,
26
+ disabled: undefined,
27
+ readOnly: undefined,
28
+ },
29
+ };
30
+
31
+ export default meta;
32
+
33
+ type Story = StoryObj<typeof Textarea>;
34
+
35
+ export const Default: Story = {
36
+ render: (args) => <Textarea {...args} />,
37
+ name: "Default",
38
+ };
39
+
40
+ export const ReadOnly: Story = {
41
+ args: {
42
+ value: "This is a read only textarea",
43
+ readOnly: true,
44
+ },
45
+ render: (args) => <Textarea {...args} />,
46
+ name: "Read Only",
47
+ };
48
+
49
+ export const LeftIcon: Story = {
50
+ render: (args) => (
51
+ <Textarea
52
+ aria-label="Search"
53
+ elemBefore={<Icon name="magnifying-glass-outline" aria-hidden />}
54
+ {...args}
55
+ />
56
+ ),
57
+ name: "Left icon",
58
+ };
59
+
60
+ export const RightIcon: Story = {
61
+ render: (args) => (
62
+ <Textarea
63
+ aria-label="Search"
64
+ elemAfter={<Icon name="magnifying-glass-outline" aria-hidden />}
65
+ {...args}
66
+ />
67
+ ),
68
+ name: "Right icon",
69
+ };
70
+
71
+ export const Autofocus: Story = {
72
+ render: (args) => <Textarea autoFocus {...args} />,
73
+ name: "Autofocus",
74
+ };
75
+
76
+ export const MaxLength: Story = {
77
+ args: {
78
+ maxLength: 10,
79
+ },
80
+ render: (args) => (
81
+ <Textarea
82
+ id={args.id}
83
+ name={args.name}
84
+ maxLength={args.maxLength}
85
+ ariaLabel={args.ariaLabel}
86
+ />
87
+ ),
88
+ name: "Max Length",
89
+ };
90
+
91
+ export const WithPlaceholder: Story = {
92
+ args: {
93
+ placeholder: "Enter message here...",
94
+ },
95
+ render: (args) => <Textarea {...args} />,
96
+ name: "With Placeholder",
97
+ };
@@ -0,0 +1,126 @@
1
+ import * as React from "react";
2
+ import Container, { Accessory } from "./styles";
3
+ import type { TypeTextareaProps } from "./TextareaTypes";
4
+
5
+ /**
6
+ * @deprecated Use TypeTextareaProps from root instead
7
+ */
8
+ export type TypeProps = TypeTextareaProps;
9
+
10
+ export default class Textarea extends React.Component<TypeTextareaProps> {
11
+ static defaultProps = {
12
+ rows: 4,
13
+ };
14
+
15
+ handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
16
+ if (this.props.onBlur) {
17
+ this.props.onBlur(e);
18
+ }
19
+ };
20
+
21
+ handleChange = (e: React.SyntheticEvent<HTMLTextAreaElement>) => {
22
+ if (this.props.onChange) {
23
+ this.props.onChange(e);
24
+ }
25
+ };
26
+
27
+ handleFocus = (e: React.FocusEvent<HTMLTextAreaElement>) => {
28
+ if (this.props.onFocus) {
29
+ this.props.onFocus(e);
30
+ }
31
+ };
32
+
33
+ handleKeyDown = (e: React.SyntheticEvent<HTMLTextAreaElement>) => {
34
+ if (this.props.onKeyDown) {
35
+ this.props.onKeyDown(e);
36
+ }
37
+ };
38
+
39
+ handleKeyUp = (e: React.SyntheticEvent<HTMLTextAreaElement>) => {
40
+ if (this.props.onKeyUp) {
41
+ this.props.onKeyUp(e);
42
+ }
43
+ };
44
+
45
+ override render() {
46
+ const {
47
+ autoFocus,
48
+ disabled,
49
+ readOnly,
50
+ isInvalid,
51
+ id,
52
+ name,
53
+ placeholder,
54
+ value,
55
+ enableSpellcheck,
56
+ enableResize,
57
+ required,
58
+ rows,
59
+ elemBefore,
60
+ elemAfter,
61
+ maxLength,
62
+ ariaLabel,
63
+ ariaDescribedby,
64
+ innerRef,
65
+ onBlur,
66
+ onChange,
67
+ onFocus,
68
+ onKeyDown,
69
+ onKeyUp,
70
+ color,
71
+ qa = {},
72
+ inputProps = {},
73
+ ...rest
74
+ } = this.props;
75
+
76
+ return (
77
+ <Container
78
+ hasBeforeElement={!!elemBefore}
79
+ hasAfterElement={!!elemAfter}
80
+ disabled={disabled}
81
+ invalid={isInvalid}
82
+ resizable={enableResize}
83
+ // TODO: fix this type since `color` should be valid here. TS can't resolve the correct type.
84
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
85
+ // @ts-ignore
86
+ color={color}
87
+ data-qa-textarea={id}
88
+ data-qa-textarea-isdisabled={disabled === true}
89
+ data-qa-textarea-isrequired={required === true}
90
+ data-qa-textarea-isinvalid={isInvalid === true}
91
+ {...qa}
92
+ {...rest}
93
+ >
94
+ {elemBefore && <Accessory before>{elemBefore}</Accessory>}
95
+
96
+ <textarea
97
+ id={id}
98
+ aria-label={ariaLabel}
99
+ aria-describedby={ariaDescribedby}
100
+ aria-invalid={isInvalid}
101
+ value={value}
102
+ name={name}
103
+ placeholder={placeholder}
104
+ rows={rows}
105
+ disabled={disabled}
106
+ readOnly={Boolean(readOnly)}
107
+ autoFocus={autoFocus}
108
+ spellCheck={enableSpellcheck}
109
+ required={required}
110
+ maxLength={maxLength}
111
+ onChange={this.handleChange}
112
+ onKeyUp={this.handleKeyUp}
113
+ onKeyDown={this.handleKeyDown}
114
+ onBlur={this.handleBlur}
115
+ onFocus={this.handleFocus}
116
+ ref={innerRef}
117
+ data-qa-textarea-input=""
118
+ data-qa-input={name}
119
+ {...inputProps}
120
+ />
121
+
122
+ {elemAfter && <Accessory after>{elemAfter}</Accessory>}
123
+ </Container>
124
+ );
125
+ }
126
+ }
@@ -0,0 +1,97 @@
1
+ import * as React from "react";
2
+ import type {
3
+ TypeSystemCommonProps,
4
+ TypeStyledComponentsCommonProps,
5
+ } from "@sproutsocial/seeds-react-system-props";
6
+
7
+ type CollisionTypes =
8
+ | "color"
9
+ | "onChange"
10
+ | "onFocus"
11
+ | "onBlur"
12
+ | "onKeyDown"
13
+ | "onKeyUp";
14
+
15
+ export interface TypeTextareaProps
16
+ extends TypeStyledComponentsCommonProps,
17
+ TypeSystemCommonProps,
18
+ Omit<React.ComponentPropsWithoutRef<"textarea">, CollisionTypes> {
19
+ /** ID of the form element, should match the "for" value of the associated label */
20
+ id: string;
21
+ name: string;
22
+
23
+ /** Label used to describe the input if not used with an accompanying visual label */
24
+ ariaLabel?: string;
25
+
26
+ /** Attribute used to associate other elements that describe the Textarea, like an error */
27
+ ariaDescribedby?: string;
28
+
29
+ /** Current value of the textarea */
30
+ value?: string;
31
+
32
+ /** Will autofocus the element when mounted to the DOM */
33
+ autoFocus?: boolean;
34
+
35
+ /** HTML disabled attribute */
36
+ disabled?: boolean;
37
+
38
+ /** HTML readonly attribute */
39
+ readOnly?: boolean;
40
+
41
+ /** Whether or not the current contents of the input are invalid */
42
+ isInvalid?: boolean;
43
+
44
+ /** Placeholder text for when value is undefined or empty */
45
+ placeholder?: string;
46
+
47
+ /** HTML required attribute */
48
+ required?: boolean;
49
+
50
+ /** 16x16 element, such as an icon */
51
+ elemBefore?: React.ReactNode;
52
+
53
+ /** 16x16 element, such as an icon */
54
+ elemAfter?: React.ReactNode;
55
+
56
+ /** Max length of the input */
57
+ maxLength?: number;
58
+
59
+ /** HTML spellcheck attribute */
60
+ enableSpellcheck?: boolean;
61
+
62
+ /** Makes the text area vertically resizable */
63
+ enableResize?: boolean;
64
+
65
+ /** The number of visible lines of text without scrolling */
66
+ rows?: number;
67
+ qa?: object;
68
+
69
+ /** Props to spread onto the underlying textarea element */
70
+ inputProps?: React.ComponentPropsWithoutRef<"textarea">;
71
+
72
+ /** Used to get a reference to the underlying element */
73
+ innerRef?:
74
+ | {
75
+ current: null | HTMLTextAreaElement;
76
+ }
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
+ | ((arg0: React.ElementRef<any> | HTMLElement) => void);
79
+ onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
80
+ onChange?: (e: React.SyntheticEvent<HTMLTextAreaElement>) => void;
81
+ onFocus?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
82
+ onKeyDown?: (e: React.SyntheticEvent<HTMLTextAreaElement>) => void;
83
+ onKeyUp?: (e: React.SyntheticEvent<HTMLTextAreaElement>) => void;
84
+ }
85
+
86
+ export interface TypeTextareaContainerProps {
87
+ resizable?: TypeTextareaProps["enableResize"];
88
+ hasBeforeElement?: boolean;
89
+ hasAfterElement?: boolean;
90
+ disabled?: TypeTextareaProps["disabled"];
91
+ invalid?: TypeTextareaProps["isInvalid"];
92
+ }
93
+
94
+ export interface TypeTextareaAccessoryProps {
95
+ before?: boolean;
96
+ after?: boolean;
97
+ }
@@ -0,0 +1,36 @@
1
+ import * as React from "react";
2
+ import Textarea from "../";
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5
+ function TextareaTypes() {
6
+ const [value, setValue] = React.useState("");
7
+ return (
8
+ <>
9
+ <Textarea id="test" name="test" />
10
+ <Textarea id="disabled" name="disabled" disabled={true} />
11
+ <Textarea id="readOnly" name="readOnly" readOnly={true} />
12
+ <Textarea id="required" name="required" required />
13
+ <Textarea id="error" name="error" isInvalid />
14
+ <Textarea
15
+ id="search"
16
+ name="search"
17
+ value={value}
18
+ onChange={(e) => setValue(e.currentTarget.value)}
19
+ />
20
+ <Textarea id="autofocus" name="autofocus" autoFocus />
21
+ <Textarea id="maxlength" name="maxlength" autoFocus maxLength={10} />
22
+ <Textarea
23
+ id="test"
24
+ name="test"
25
+ inputProps={{
26
+ defaultValue: value,
27
+ style: {
28
+ borderRadius: "999999px",
29
+ },
30
+ }}
31
+ />
32
+ {/* @ts-expect-error - test missing required props is rejected */}
33
+ <Textarea />
34
+ </>
35
+ );
36
+ }
@@ -0,0 +1,112 @@
1
+ import React from "react";
2
+ import {
3
+ render,
4
+ fireEvent,
5
+ screen,
6
+ } from "@sproutsocial/seeds-react-testing-library";
7
+ import Textarea from "../";
8
+ import { Text } from "@sproutsocial/seeds-react-text";
9
+
10
+ describe("Textarea", () => {
11
+ it("should render statuses properly", () => {
12
+ render(<Textarea id="text" name="text" />);
13
+ expect(
14
+ screen.getByDataQaLabel({
15
+ textarea: "text",
16
+ })
17
+ ).toBeTruthy();
18
+ });
19
+
20
+ it("should render before and after element", () => {
21
+ render(
22
+ <Textarea
23
+ elemAfter={<Text>After</Text>}
24
+ elemBefore={<Text>Before</Text>}
25
+ id="text"
26
+ name="text"
27
+ />
28
+ );
29
+
30
+ expect(screen.getByText("Before")).toBeInTheDocument();
31
+ expect(screen.getByText("After")).toBeInTheDocument();
32
+ });
33
+
34
+ it("should notify on changes", () => {
35
+ const mockOnChange = jest.fn();
36
+ const mockEventHandler = jest.fn();
37
+
38
+ render(
39
+ <Textarea
40
+ id="name"
41
+ name="name"
42
+ onChange={mockOnChange}
43
+ onBlur={mockEventHandler}
44
+ onFocus={mockEventHandler}
45
+ onKeyDown={mockEventHandler}
46
+ onKeyUp={mockEventHandler}
47
+ value="User"
48
+ />
49
+ );
50
+
51
+ const textarea = screen.getByDataQaLabel({
52
+ input: "name",
53
+ });
54
+
55
+ expect(textarea).toBeTruthy();
56
+ fireEvent.change(textarea, {
57
+ target: {
58
+ value: "updated",
59
+ },
60
+ });
61
+
62
+ expect(mockOnChange).toHaveBeenCalled();
63
+ // Ensure that the event handlers aren't getting attached to the root level
64
+ // element (only the actual textarea itself)
65
+ const textareaContainer = screen.getByDataQaLabel({
66
+ textarea: "name",
67
+ });
68
+
69
+ expect(textareaContainer).toBeTruthy();
70
+
71
+ fireEvent.change(textareaContainer);
72
+
73
+ expect(mockOnChange.mock.calls.length).toEqual(1);
74
+
75
+ fireEvent.blur(textareaContainer);
76
+ fireEvent.focus(textareaContainer);
77
+ fireEvent.keyUp(textareaContainer);
78
+ fireEvent.keyDown(textareaContainer);
79
+
80
+ expect(mockEventHandler.mock.calls.length).toEqual(0);
81
+ });
82
+
83
+ describe("readOnly prop", () => {
84
+ it.each([true, "foobar", 1])(
85
+ "should correctly add the readonly prop for truthy values: %p",
86
+ (truthyValue) => {
87
+ //eslint-disable-next-line @typescript-eslint/ban-ts-comment
88
+ // @ts-ignore
89
+ render(<Textarea id="name" name="name" readOnly={truthyValue} />);
90
+ expect(
91
+ screen.getByDataQaLabel({
92
+ input: "name",
93
+ })
94
+ ).toHaveAttribute("readonly", "");
95
+ }
96
+ );
97
+
98
+ it.each([false, null, undefined, 0, ""])(
99
+ "should correctly leave off readonly prop for falsey values: %p",
100
+ (truthyValue) => {
101
+ //eslint-disable-next-line @typescript-eslint/ban-ts-comment
102
+ // @ts-ignore
103
+ render(<Textarea id="name" name="name" readOnly={truthyValue} />);
104
+ expect(
105
+ screen.getByDataQaLabel({
106
+ input: "name",
107
+ })
108
+ ).not.toHaveAttribute("readonly");
109
+ }
110
+ );
111
+ });
112
+ });
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import Textarea from "./Textarea";
2
+
3
+ export default Textarea;
4
+ export { Textarea };
5
+ export * from "./TextareaTypes";
@@ -0,0 +1,7 @@
1
+ import "styled-components";
2
+ import { TypeTheme } from "@sproutsocial/seeds-react-theme";
3
+
4
+ declare module "styled-components" {
5
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
6
+ export interface DefaultTheme extends TypeTheme {}
7
+ }
package/src/styles.ts ADDED
@@ -0,0 +1,104 @@
1
+ import styled, { css } from "styled-components";
2
+ import { COMMON } from "@sproutsocial/seeds-react-system-props";
3
+ import { focusRing } from "@sproutsocial/seeds-react-mixins";
4
+ import type {
5
+ TypeTextareaAccessoryProps,
6
+ TypeTextareaContainerProps,
7
+ } from "./TextareaTypes";
8
+
9
+ const Container = styled.div<TypeTextareaContainerProps>`
10
+ box-sizing: border-box;
11
+ position: relative;
12
+
13
+ textarea {
14
+ box-sizing: border-box;
15
+ display: block;
16
+ width: 100%;
17
+ padding: ${(props) => props.theme.space[300]};
18
+ border: 1px solid ${(props) => props.theme.colors.form.border.base};
19
+ border-radius: ${(props) => props.theme.radii[500]};
20
+ background-color: ${(props) => props.theme.colors.form.background.base};
21
+ color: ${(props) => props.theme.colors.text.body};
22
+ outline: none;
23
+ resize: none;
24
+ transition: border-color ${(props) => props.theme.duration.fast}
25
+ ${(props) => props.theme.easing.ease_in},
26
+ box-shadow ${(props) => props.theme.duration.fast}
27
+ ${(props) => props.theme.easing.ease_in};
28
+ font-family: ${(props) => props.theme.fontFamily};
29
+ ${(props) => props.theme.typography[200]}
30
+ font-weight: ${(props) => props.theme.fontWeights.normal};
31
+ appearance: none;
32
+
33
+ &:focus {
34
+ ${focusRing}
35
+ }
36
+
37
+ &::placeholder {
38
+ color: ${(props) => props.theme.colors.form.placeholder.base};
39
+ font-style: italic;
40
+ }
41
+
42
+ ${(props) =>
43
+ props.resizable &&
44
+ css`
45
+ resize: vertical;
46
+ `}
47
+
48
+ ${(props) =>
49
+ props.hasBeforeElement &&
50
+ css`
51
+ padding-left: 40px;
52
+ `}
53
+
54
+ ${(props) =>
55
+ props.hasAfterElement &&
56
+ css`
57
+ padding-right: 40px;
58
+ `}
59
+ }
60
+
61
+ ${(props) =>
62
+ props.disabled &&
63
+ css`
64
+ opacity: 0.4;
65
+
66
+ textarea {
67
+ cursor: not-allowed;
68
+ }
69
+ `}
70
+
71
+ ${(props) =>
72
+ props.invalid &&
73
+ css`
74
+ textarea {
75
+ border-color: ${(props) => props.theme.colors.form.border.error};
76
+ }
77
+ `}
78
+
79
+ ${COMMON}
80
+ `;
81
+
82
+ export const Accessory = styled.div<TypeTextareaAccessoryProps>`
83
+ position: absolute;
84
+ color: ${(props) => props.theme.colors.icon.base};
85
+
86
+ ${(props) =>
87
+ props.before &&
88
+ css`
89
+ top: ${props.theme.space[300]};
90
+ left: ${props.theme.space[350]};
91
+ `};
92
+
93
+ ${(props) =>
94
+ props.after &&
95
+ css`
96
+ right: ${props.theme.space[350]};
97
+ bottom: ${props.theme.space[300]};
98
+ `};
99
+ `;
100
+
101
+ Container.displayName = "TextareaContainer";
102
+ Accessory.displayName = "TextareaAccessory";
103
+
104
+ export default Container;
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@sproutsocial/seeds-tsconfig/bundler/dom/library-monorepo",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "module": "esnext"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist", "coverage", "src/Textarea.stories.tsx"]
9
+ }