Skip to content

Commit c216be5

Browse files
committed
chore: Adds internal style api v2 to textarea
1 parent b3106e4 commit c216be5

6 files changed

Lines changed: 309 additions & 9 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React, { useState } from 'react';
4+
import clsx from 'clsx';
5+
6+
import { KeyValuePairs, Textarea, TextareaProps } from '~components';
7+
8+
import { SimplePage } from '../app/templates';
9+
10+
import styles from './style-api-v2.scss';
11+
12+
// Owned states (invalid, warning, disabled, read-only) are passed as a different class; the interaction states
13+
// (:hover, :focus, :placeholder-shown) are handled by selectors in the stylesheet.
14+
function StyledTextarea({ value: initialValue, invalid, warning, disabled, readOnly, ...rest }: TextareaProps) {
15+
const [value, setValue] = useState(initialValue);
16+
const className = clsx({
17+
[styles['textarea-invalid']]: invalid,
18+
[styles['textarea-warning']]: warning && !invalid,
19+
[styles['textarea-disabled']]: disabled && !invalid && !warning,
20+
[styles['textarea-readonly']]: readOnly && !disabled && !invalid && !warning,
21+
[styles.textarea]: !invalid && !warning && !disabled && !readOnly,
22+
});
23+
return (
24+
<Textarea
25+
{...rest}
26+
ariaLabel="Textarea field"
27+
value={value}
28+
onChange={({ detail }) => setValue(detail.value)}
29+
invalid={invalid}
30+
warning={warning}
31+
disabled={disabled}
32+
readOnly={readOnly}
33+
{...{ styleClassNames: { textarea: className } }}
34+
/>
35+
);
36+
}
37+
38+
export default function () {
39+
return (
40+
<SimplePage title="Textarea - Style API v2" screenshotArea={{}} i18n={{}}>
41+
<KeyValuePairs
42+
columns={2}
43+
items={[
44+
{
45+
type: 'pair',
46+
label: 'Default',
47+
value: <StyledTextarea value={'First line\nSecond line'} />,
48+
},
49+
{
50+
type: 'pair',
51+
label: 'Placeholder',
52+
value: <StyledTextarea value="" placeholder="Enter a description" />,
53+
},
54+
{
55+
type: 'pair',
56+
label: 'Invalid',
57+
value: <StyledTextarea value="Too short" invalid={true} />,
58+
},
59+
{
60+
type: 'pair',
61+
label: 'Warning',
62+
value: <StyledTextarea value="Almost at the limit" warning={true} />,
63+
},
64+
{
65+
type: 'pair',
66+
label: 'Disabled',
67+
value: <StyledTextarea value="Cannot be edited" disabled={true} />,
68+
},
69+
{
70+
type: 'pair',
71+
label: 'Disabled with placeholder',
72+
value: <StyledTextarea value="" placeholder="Enter a description" disabled={true} />,
73+
},
74+
{
75+
type: 'pair',
76+
label: 'Read-only',
77+
value: <StyledTextarea value="Read-only text" readOnly={true} />,
78+
},
79+
{
80+
type: 'pair',
81+
label: 'Resize disabled',
82+
value: <StyledTextarea value="No resize handle" resize="none" rows={2} />,
83+
},
84+
]}
85+
/>
86+
</SimplePage>
87+
);
88+
}

‎pages/textarea/style-api-v2.scss‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
@use '~components/styling/textarea';
7+
@use '../style-api/palette' as palette;
8+
9+
// The native textarea is also the field box. One class per owned state (invalid, warning, disabled, read-only);
10+
// `:hover`, `:focus` and `:placeholder-shown` for the interaction states.
11+
@mixin field {
12+
@include textarea.textarea(
13+
$color: palette.$text,
14+
$background: palette.$surface,
15+
$border-width: 2px,
16+
$border-radius: 12px,
17+
$box-shadow: 0 2px 4px palette.$subtle,
18+
$padding-block: 10px,
19+
$padding-inline: 16px,
20+
$font-family: monospace,
21+
$font-size: 15px,
22+
$font-weight: 600,
23+
$placeholder-color: palette.$accent,
24+
$placeholder-font-size: 13px,
25+
$placeholder-font-style: normal,
26+
$placeholder-font-weight: 400
27+
);
28+
}
29+
30+
.textarea {
31+
@include field;
32+
@include textarea.textarea($border-color: palette.$border);
33+
34+
&:hover {
35+
@include textarea.textarea($background: palette.$surface-alt, $border-color: palette.$border-accent);
36+
}
37+
38+
&:focus {
39+
@include textarea.textarea(
40+
$color: palette.$accent,
41+
$border-color: palette.$accent,
42+
$box-shadow: 0 0 0 3px palette.$accent-active
43+
);
44+
}
45+
46+
&:placeholder-shown {
47+
@include textarea.textarea($padding-inline: 24px);
48+
}
49+
}
50+
51+
.textarea-invalid {
52+
@include field;
53+
@include textarea.textarea($background: palette.$error-surface, $border-color: palette.$error);
54+
55+
&:focus {
56+
@include textarea.textarea($box-shadow: 0 0 0 3px palette.$error-hover);
57+
}
58+
}
59+
60+
.textarea-warning {
61+
@include field;
62+
@include textarea.textarea($background: palette.$warning-surface, $border-color: palette.$warning);
63+
64+
&:focus {
65+
@include textarea.textarea($box-shadow: 0 0 0 3px palette.$warning);
66+
}
67+
}
68+
69+
.textarea-disabled {
70+
@include field;
71+
@include textarea.textarea(
72+
$color: palette.$text-disabled,
73+
$background: palette.$control-bg-disabled,
74+
$border-color: palette.$border-disabled,
75+
$box-shadow: none,
76+
$placeholder-color: palette.$text-disabled
77+
);
78+
}
79+
80+
.textarea-readonly {
81+
@include field;
82+
@include textarea.textarea($background: palette.$surface-alt, $border-color: palette.$border);
83+
84+
&:focus {
85+
@include textarea.textarea($border-color: palette.$accent, $box-shadow: 0 0 0 3px palette.$accent-active);
86+
}
87+
}

‎src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,37 @@ exports[`Style API docs definition for radio-button matches the snapshot: radio-
248248
}
249249
`;
250250

251+
exports[`Style API docs definition for textarea matches the snapshot: textarea 1`] = `
252+
{
253+
"slots": [
254+
{
255+
"name": "textarea",
256+
"properties": [
257+
"color",
258+
"background",
259+
"border",
260+
"border-radius",
261+
"box-shadow",
262+
"padding",
263+
"font-family",
264+
"font-size",
265+
"font-weight",
266+
],
267+
"propertyDescriptions": {
268+
"box-shadow": "Also draws the focus ring under \`:focus\`.",
269+
},
270+
"tokenDescriptions": {},
271+
"tokens": [
272+
"placeholder-color",
273+
"placeholder-font-size",
274+
"placeholder-font-style",
275+
"placeholder-font-weight",
276+
],
277+
},
278+
],
279+
}
280+
`;
281+
251282
exports[`Style API docs definition for toggle matches the snapshot: toggle 1`] = `
252283
{
253284
"slots": [
@@ -278,6 +309,7 @@ exports[`Style API docs list of components with a Style API doc matches the snap
278309
"input",
279310
"link",
280311
"radio-button",
312+
"textarea",
281313
"toggle",
282314
]
283315
`;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React from 'react';
4+
import { render } from '@testing-library/react';
5+
6+
import customCssProps from '../../../lib/components/internal/generated/custom-css-properties';
7+
import createWrapper from '../../../lib/components/test-utils/dom';
8+
import Textarea, { TextareaProps } from '../../../lib/components/textarea';
9+
10+
function renderTextarea(
11+
props: Omit<TextareaProps, 'value'> & { value?: string; styleClassNames?: Record<string, string> } = {}
12+
) {
13+
const { container } = render(<Textarea value="" onChange={() => {}} ariaLabel="Textarea" {...props} />);
14+
return { container, wrapper: createWrapper(container).findTextarea()! };
15+
}
16+
17+
describe('Textarea Style API v2', () => {
18+
test('applies the textarea class to the native textarea and ignores unknown keys', () => {
19+
const { container, wrapper } = renderTextarea({ styleClassNames: { textarea: 'a', unknown: 'b' } });
20+
const nativeTextarea = wrapper.findNativeTextarea().getElement();
21+
expect(nativeTextarea).toHaveClass('a');
22+
expect(wrapper.getElement()).not.toHaveClass('a');
23+
expect(container.querySelector('.b')).toBeNull();
24+
});
25+
26+
test('applies the textarea class in every state', () => {
27+
for (const state of [{}, { invalid: true }, { warning: true }, { disabled: true }, { readOnly: true }]) {
28+
const { wrapper } = renderTextarea({ ...state, styleClassNames: { textarea: 'a' } });
29+
expect(wrapper.findNativeTextarea().getElement()).toHaveClass('a');
30+
}
31+
});
32+
33+
test('does not leak the styleClassNames prop to the DOM', () => {
34+
const { container } = renderTextarea({ styleClassNames: { textarea: 'a' } });
35+
expect(container.querySelector('[styleClassNames]')).toBeNull();
36+
});
37+
38+
test('keeps the v1 inline styles on the native textarea alongside the class', () => {
39+
const { wrapper } = renderTextarea({
40+
styleClassNames: { textarea: 'a' },
41+
style: {
42+
root: { borderRadius: '6px', color: { default: '#222222', hover: '#333333' } },
43+
placeholder: { color: '#999999' },
44+
},
45+
});
46+
const nativeTextarea = wrapper.findNativeTextarea().getElement();
47+
expect(nativeTextarea).toHaveClass('a');
48+
expect(nativeTextarea.style.borderRadius).toBe('6px');
49+
expect(nativeTextarea.style.getPropertyValue(customCssProps.styleColorDefault)).toBe('#222222');
50+
expect(nativeTextarea.style.getPropertyValue(customCssProps.styleColorHover)).toBe('#333333');
51+
expect(nativeTextarea.style.getPropertyValue(customCssProps.stylePlaceholderColor)).toBe('#999999');
52+
});
53+
});

‎src/textarea/index.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ import styles from './styles.css.js';
2222

2323
export { TextareaProps };
2424

25+
// Style API v2
26+
interface StyleClassNames {
27+
textarea?: string;
28+
}
29+
2530
const Textarea = React.forwardRef(
2631
(
2732
{
@@ -64,6 +69,7 @@ const Textarea = React.forwardRef(
6469
const { ariaLabelledby, ariaDescribedby, controlId, invalid, warning } = useFormFieldContext(rest);
6570

6671
const baseProps = getBaseProps(rest);
72+
const { styleClassNames } = rest as { styleClassNames?: StyleClassNames };
6773

6874
const textareaRef = useRef<HTMLTextAreaElement>(null);
6975
useForwardFocus(ref, textareaRef);
@@ -77,7 +83,7 @@ const Textarea = React.forwardRef(
7783
name,
7884
placeholder,
7985
autoFocus,
80-
className: clsx(styles.textarea, styles[`textarea-resize-${resize}`], {
86+
className: clsx(styleClassNames?.textarea, styles.textarea, styles[`textarea-resize-${resize}`], {
8187
[styles['textarea-readonly']]: readOnly,
8288
[styles['textarea-invalid']]: invalid,
8389
[styles['textarea-warning']]: warning && !invalid,

0 commit comments

Comments
 (0)