Skip to content

Commit 87addbb

Browse files
feat (UI): DOMA-11759 add textarea (#6275)
* fix(ui): DOMA-11759 add textarea * fix(ui): DOMA-11759 fix styles * fix(ui): DOMA-11759 fix styles * fix(ui): DOMA-11759 fix styles * fix(ui): DOMA-11759 fix styles and stories * fix(ui): DOMA-11759 update textarea * fix(ui): DOMA-11759 fix size * feat(ui): DOMA-11759 add disable for submit and utils * feat(ui): DOMA-11759 fix styles * feat(ui): DOMA-11759 fix stories * feat(ui): DOMA-11759 refactor * feat(ui): DOMA-11759 fix types * fix(ui): DOMA-111759 fix focus and submit icon * fix(ui): DOMA-11759 review fixes * fix(ui): DOMA-11759 fix disable * fix(ui): DOMA-11759 fix story * fix(ui): DOMA-11759 fix types * fix(ui): DOMA-11759 fix build * fix(ui): DOMA-11759 add empty icon combination
1 parent 3ba754f commit 87addbb

File tree

4 files changed

+295
-18
lines changed

4 files changed

+295
-18
lines changed

packages/ui/src/components/Input/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Input as InputComponent } from './input'
22
import { Password } from './password'
33
import { Phone } from './phone'
4+
import { TextArea } from './textArea'
45
import './style.less'
56

67
export type { BaseInputProps, InputProps } from './input'
@@ -10,11 +11,13 @@ export type { PhoneInputProps } from './phone'
1011
export type InputType = typeof InputComponent & {
1112
Password: typeof Password
1213
Phone: typeof Phone
14+
TextArea: typeof TextArea
1315
}
1416

1517
const Input = InputComponent as InputType
1618
Input.Password = Password
1719
Input.Phone = Phone
20+
Input.TextArea = TextArea
1821

1922
export {
2023
Input,

packages/ui/src/components/Input/style.less

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,36 @@
3838
}
3939
}
4040

41+
.condo-input-affix-wrapper {
42+
padding: @condo-global-spacing-12 - @condo-global-border-width-default;
43+
}
44+
4145
.condo-input:not(.condo-input-affix-wrapper .condo-input),
4246
.condo-input-affix-wrapper {
4347
box-sizing: border-box;
4448
width: 100%;
49+
border-radius: @condo-global-border-radius-medium;
50+
}
51+
52+
.condo-input:not(.condo-input-affix-wrapper .condo-input, textarea, .condo-input-textarea-wrapper) {
4553
height: @condo-global-typography-text-l-line-height + 2 * @condo-global-spacing-12;
4654
border: @condo-global-border-width-default solid @condo-global-color-gray-5;
47-
border-radius: @condo-global-border-radius-medium;
4855
}
4956

50-
.condo-input-affix-wrapper {
51-
padding: @condo-global-spacing-12 - @condo-global-border-width-default;
57+
.condo-input:not(.condo-input-affix-wrapper .condo-input,
58+
.condo-input-disabled,
59+
.condo-input-phone-disabled .condo-input),
60+
.condo-input-affix-wrapper:not(.condo-input-affix-wrapper-disabled) {
61+
&:hover,
62+
&:focus,
63+
&:active {
64+
border-color: @condo-global-color-gray-7;
65+
box-shadow: none;
66+
}
5267
}
5368

5469
.condo-input-focused,
55-
.condo-input-affix-wrapper-focused {
70+
.condo-input-wrapper-focused:not(:disabled) {
5671
border-color: @condo-global-color-gray-7;
5772
box-shadow: none;
5873
}
@@ -91,26 +106,15 @@
91106
color: @condo-global-color-gray-7;
92107
}
93108

94-
.condo-input:not(.condo-input-affix-wrapper .condo-input,
95-
.condo-input-disabled,
96-
.condo-input-phone-disabled .condo-input),
97-
.condo-input-affix-wrapper:not(.condo-input-affix-wrapper-disabled) {
98-
&:hover,
99-
&:focus,
100-
&:active {
101-
border-color: @condo-global-color-gray-7;
102-
box-shadow: none;
103-
}
104-
}
105-
106109
.condo-input::placeholder {
107110
color: @condo-global-color-gray-7;
108111
.condo-typography-text-large();
109112
}
110113

111114
.condo-input-affix-wrapper-disabled,
112115
.condo-input-disabled[disabled],
113-
.condo-input[disabled] {
116+
.condo-input[disabled],
117+
.condo-input-disabled {
114118
color: @condo-global-color-black;
115119
background-color: @condo-global-color-white;
116120
opacity: @condo-global-opacity-disabled;
@@ -139,4 +143,48 @@
139143
&:hover {
140144
color: @condo-global-color-black;
141145
}
142-
}
146+
}
147+
148+
.condo-input.condo-input-textarea-wrapper > .condo-input-textarea {
149+
height: auto;
150+
padding: @condo-global-spacing-12;
151+
border: none;
152+
}
153+
154+
.condo-input.condo-input-textarea-wrapper {
155+
position: relative;
156+
height: fit-content;
157+
padding: 0;
158+
}
159+
160+
.condo-input-utils {
161+
display: flex;
162+
gap: @condo-global-spacing-12;
163+
align-items: center;
164+
max-height: @condo-global-spacing-20;
165+
}
166+
167+
.condo-input-count {
168+
color: @condo-global-color-gray-7;
169+
font-size: @condo-global-font-size-text-s;
170+
font-family: "Noto Sans Mono", monospace;
171+
}
172+
173+
.condo-input-bottom-panel {
174+
position: relative;
175+
left: @condo-global-spacing-12;
176+
display: flex;
177+
flex-direction: row;
178+
align-items: center;
179+
width: calc(100% - @condo-global-spacing-12 * 2);
180+
max-height: @condo-global-spacing-12 + @condo-global-spacing-32;
181+
padding: @condo-global-spacing-8 0 @condo-global-spacing-12;
182+
background-color: @condo-global-color-white;
183+
184+
&-right {
185+
display: flex;
186+
gap: @condo-global-spacing-16;
187+
align-items: center;
188+
margin-left: auto;
189+
}
190+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { Input as DefaultInput } from 'antd'
2+
import { TextAreaProps as AntdTextAreaProps } from 'antd/es/input'
3+
import classNames from 'classnames'
4+
import React, { forwardRef, useState, useEffect, TextareaHTMLAttributes } from 'react'
5+
6+
import { ArrowUp } from '@open-condo/icons'
7+
8+
import { Button } from '../Button'
9+
10+
import type { InputRef } from 'antd'
11+
12+
const { TextArea: DefaultTextArea } = DefaultInput
13+
14+
export const TEXTAREA_CLASS_PREFIX = 'condo-input'
15+
16+
export type TextAreaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'style' | 'size' | 'onResize'> &
17+
Pick<AntdTextAreaProps, 'autoSize'> & {
18+
value?: string
19+
isSubmitDisabled?: boolean
20+
showCount?: boolean
21+
onSubmit?: (value: string) => void
22+
bottomPanelUtils?: React.ReactElement[]
23+
}
24+
25+
const TextArea = forwardRef<InputRef, TextAreaProps>((props, ref) => {
26+
const {
27+
className,
28+
disabled,
29+
onSubmit,
30+
autoFocus,
31+
maxLength = 1000,
32+
showCount = true,
33+
isSubmitDisabled,
34+
value: propsValue,
35+
bottomPanelUtils = [],
36+
onChange: propsOnChange,
37+
autoSize = { minRows: 1 },
38+
...restProps
39+
} = props
40+
41+
const [internalValue, setInternalValue] = useState('')
42+
const [isFocused, setIsFocused] = useState(false)
43+
44+
useEffect(() => {
45+
if (propsValue !== undefined) {
46+
setInternalValue(propsValue)
47+
}
48+
}, [propsValue])
49+
50+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
51+
const newValue = e.target.value
52+
53+
if (propsValue === undefined) {
54+
setInternalValue(newValue)
55+
}
56+
57+
if (propsOnChange) {
58+
propsOnChange(e)
59+
}
60+
}
61+
62+
const handleFocus = (e: React.FocusEvent<HTMLTextAreaElement>) => {
63+
setIsFocused(true)
64+
if (restProps.onFocus) restProps.onFocus(e)
65+
}
66+
67+
const handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
68+
setIsFocused(false)
69+
if (restProps.onBlur) restProps.onBlur(e)
70+
}
71+
72+
const currentValue = propsValue !== undefined ? propsValue : internalValue
73+
const characterCount = `${currentValue.length}/${maxLength}`
74+
75+
const hasBottomPanelUtils = bottomPanelUtils.length > 0
76+
const shouldShowRightPanel = showCount || onSubmit
77+
const showBottomPanel = hasBottomPanelUtils || shouldShowRightPanel
78+
79+
const textareaClassName = classNames(
80+
`${TEXTAREA_CLASS_PREFIX}-textarea`,
81+
{
82+
[`${TEXTAREA_CLASS_PREFIX}-disabled`]: disabled,
83+
[`${TEXTAREA_CLASS_PREFIX}-show-bottom-panel`]: showBottomPanel,
84+
[`${TEXTAREA_CLASS_PREFIX}-focused`]: autoFocus,
85+
},
86+
className,
87+
)
88+
89+
const textAreaWrapperClassName = classNames(
90+
`${TEXTAREA_CLASS_PREFIX} ${TEXTAREA_CLASS_PREFIX}-textarea-wrapper`,
91+
{
92+
[`${TEXTAREA_CLASS_PREFIX}-wrapper-focused`]: isFocused,
93+
[`${TEXTAREA_CLASS_PREFIX}-disabled`]: disabled,
94+
},
95+
)
96+
97+
98+
return (
99+
<div className={textAreaWrapperClassName}>
100+
<DefaultTextArea
101+
{...restProps}
102+
ref={ref}
103+
prefixCls={TEXTAREA_CLASS_PREFIX}
104+
className={textareaClassName}
105+
disabled={disabled}
106+
onFocus={handleFocus}
107+
onBlur={handleBlur}
108+
autoSize={autoSize}
109+
maxLength={maxLength}
110+
showCount={false}
111+
value={currentValue}
112+
onChange={handleChange}
113+
autoFocus={autoFocus}
114+
/>
115+
116+
{showBottomPanel && (
117+
<span className={`${TEXTAREA_CLASS_PREFIX}-bottom-panel`}>
118+
{hasBottomPanelUtils && (
119+
<span className={`${TEXTAREA_CLASS_PREFIX}-utils`}>
120+
{bottomPanelUtils.map((util, index) => (
121+
<React.Fragment key={index}>
122+
{React.cloneElement(util, { disabled: util.props.disabled || disabled })}
123+
</React.Fragment>
124+
))}
125+
</span>
126+
)}
127+
128+
{shouldShowRightPanel && (
129+
<span className={`${TEXTAREA_CLASS_PREFIX}-bottom-panel-right`}>
130+
{showCount && (
131+
<span className={`${TEXTAREA_CLASS_PREFIX}-count`}>
132+
{characterCount}
133+
</span>
134+
)}
135+
136+
{
137+
onSubmit &&
138+
<Button
139+
disabled={disabled || isSubmitDisabled}
140+
type='accent'
141+
size='medium'
142+
onClick={() => onSubmit(currentValue)}
143+
icon={<ArrowUp size='small' />}
144+
/>
145+
}
146+
</span>
147+
)}
148+
</span>
149+
)}
150+
</div>
151+
)
152+
})
153+
154+
TextArea.displayName = 'TextArea'
155+
156+
export { TextArea }
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React from 'react'
2+
3+
import { Copy, Search, Edit, Eye, FileUp, Trash, Lock } from '@open-condo/icons'
4+
import { Button, Input as Component } from '@open-condo/ui/src'
5+
6+
import type { Meta, StoryObj } from '@storybook/react'
7+
8+
const DemoButton = ({ icon, text, disabled }: { text?: string, icon: React.ReactNode, disabled?: boolean }) => (
9+
<Button
10+
type='secondary'
11+
minimal
12+
compact
13+
size='medium'
14+
disabled={disabled}
15+
icon={icon}
16+
>{text}</Button>
17+
)
18+
19+
const iconCombinations = {
20+
'empty': [],
21+
'copy-search': [
22+
<DemoButton icon={<Copy size='small'/>} key='copy' />,
23+
<DemoButton icon={<Search size='small'/>} key='search' />,
24+
],
25+
'edit-copy': [
26+
<DemoButton icon={<Edit size='small'/>} key='edit' />,
27+
<DemoButton icon={<Copy size='small'/>} key='copy' />,
28+
],
29+
'lock-eye': [
30+
<DemoButton icon={<Lock size='small'/>} key='lock' />,
31+
<DemoButton icon={<Eye size='small'/>} key='eye' />,
32+
<DemoButton icon={<FileUp size='small'/>} key='eye-invisible' />,
33+
],
34+
'full-set': [
35+
<DemoButton icon={<Copy size='small'/>} key='copy' />,
36+
<DemoButton icon={<Search size='small'/>} key='search' />,
37+
<DemoButton icon={<Edit size='small'/>} key='edit' />,
38+
<DemoButton icon={<Trash size='small'/>} key='trash' text='Delete'/>,
39+
],
40+
}
41+
42+
export default {
43+
title: 'Components/Input',
44+
component: Component.TextArea,
45+
args: {
46+
placeholder: 'Placeholder',
47+
bottomPanelUtils: iconCombinations['copy-search'],
48+
autoSize: { minRows: 1, maxRows: 4 },
49+
disabled: false,
50+
showCount: true,
51+
},
52+
argTypes: {
53+
onSubmit: {
54+
options: [false, () => alert('Submit')],
55+
mapping: iconCombinations,
56+
control: {
57+
type: 'select',
58+
},
59+
},
60+
bottomPanelUtils: {
61+
options: Object.keys(iconCombinations),
62+
mapping: iconCombinations,
63+
control: {
64+
type: 'select',
65+
},
66+
},
67+
},
68+
} as Meta<typeof Component.TextArea>
69+
70+
export const TextArea: StoryObj<typeof Component.TextArea> = {}

0 commit comments

Comments
 (0)