Skip to content

Commit 17b150f

Browse files
committed
feat(CFormInput, CFormSelect, CFormTextarea): automatically add label, helper text, and feedback
1 parent 0982164 commit 17b150f

File tree

4 files changed

+286
-67
lines changed

4 files changed

+286
-67
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import React, { FC, ReactNode } from 'react'
2+
3+
import PropTypes from 'prop-types'
4+
5+
import { CFormFeedback, CFormFloating, CFormLabel, CFormText } from './'
6+
7+
export interface CFormControlWrapperProps {
8+
/**
9+
* @ignore
10+
*/
11+
children?: ReactNode
12+
/**
13+
* @ignore
14+
*/
15+
describedby?: string
16+
/**
17+
* Provide valuable, actionable feedback.
18+
*
19+
* @since 4.2.0
20+
*/
21+
feedback?: ReactNode | string
22+
/**
23+
* Provide valuable, actionable feedback.
24+
*
25+
* @since 4.2.0
26+
*/
27+
feedbackInvalid?: ReactNode | string
28+
/**
29+
* Provide valuable, actionable invalid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`.
30+
*
31+
* @since 4.2.0
32+
*/
33+
feedbackValid?: ReactNode | string
34+
/**
35+
* Provide valuable, actionable valid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`.
36+
*
37+
* @since 4.2.0
38+
*/
39+
floatingLabel?: ReactNode | string
40+
/**
41+
* @ignore
42+
*/
43+
id?: string
44+
/**
45+
* Set component validation state to invalid.
46+
*/
47+
invalid?: boolean
48+
/**
49+
* Add a caption for a component.
50+
*
51+
* @since 4.2.0
52+
*/
53+
label?: ReactNode | string
54+
/**
55+
* Add helper text to the component.
56+
*
57+
* @since 4.2.0
58+
*/
59+
text?: ReactNode | string
60+
/**
61+
* Display validation feedback in a styled tooltip.
62+
*
63+
* @since 4.2.0
64+
*/
65+
tooltipFeedback?: boolean
66+
/**
67+
* Set component validation state to valid.
68+
*/
69+
valid?: boolean
70+
}
71+
72+
export const CFormControlWrapper: FC<CFormControlWrapperProps> = ({
73+
children,
74+
describedby,
75+
feedback,
76+
feedbackInvalid,
77+
feedbackValid,
78+
floatingLabel,
79+
id,
80+
invalid,
81+
label,
82+
text,
83+
tooltipFeedback,
84+
valid,
85+
}) => {
86+
return floatingLabel ? (
87+
<CFormFloating>
88+
{children}
89+
<CFormLabel htmlFor={id}>{label || floatingLabel}</CFormLabel>
90+
</CFormFloating>
91+
) : (
92+
<>
93+
{label && <CFormLabel htmlFor={id}>{label}</CFormLabel>}
94+
{children}
95+
{text && <CFormText id={describedby}>{text}</CFormText>}
96+
{feedback && (valid || invalid) && (
97+
<CFormFeedback
98+
{...(invalid && { id: describedby })}
99+
invalid={invalid}
100+
tooltip={tooltipFeedback}
101+
valid={valid}
102+
>
103+
{feedback}
104+
</CFormFeedback>
105+
)}
106+
{feedbackInvalid && (
107+
<CFormFeedback id={describedby} invalid tooltip={tooltipFeedback}>
108+
{feedbackInvalid}
109+
</CFormFeedback>
110+
)}
111+
{feedbackValid && (
112+
<CFormFeedback valid tooltip={tooltipFeedback}>
113+
{feedbackValid}
114+
</CFormFeedback>
115+
)}
116+
</>
117+
)
118+
}
119+
120+
CFormControlWrapper.propTypes = {
121+
children: PropTypes.node,
122+
describedby: PropTypes.string,
123+
feedback: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
124+
feedbackValid: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
125+
feedbackInvalid: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
126+
floatingLabel: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
127+
invalid: PropTypes.bool,
128+
label: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
129+
text: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
130+
tooltipFeedback: PropTypes.bool,
131+
valid: PropTypes.bool,
132+
}
133+
134+
CFormControlWrapper.displayName = 'CFormControlWrapper'

packages/coreui-react/src/components/form/CFormInput.tsx

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import React, { ChangeEventHandler, forwardRef, InputHTMLAttributes } from 'react'
2-
import PropTypes from 'prop-types'
2+
33
import classNames from 'classnames'
4-
// import { CFormLabel } from './CFormLabel'
4+
import PropTypes from 'prop-types'
55

6-
export interface CFormInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
6+
import { CFormControlWrapper, CFormControlWrapperProps } from './CFormControlWrapper'
7+
8+
export interface CFormInputProps
9+
extends CFormControlWrapperProps,
10+
Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
711
/**
812
* A string of all className you want applied to the component.
913
*/
@@ -12,10 +16,6 @@ export interface CFormInputProps extends Omit<InputHTMLAttributes<HTMLInputEleme
1216
* Toggle the disabled state for the component.
1317
*/
1418
disabled?: boolean
15-
/**
16-
* Set component validation state to invalid.
17-
*/
18-
invalid?: boolean
1919
/**
2020
* Method called immediately after the `value` prop changes.
2121
*/
@@ -36,10 +36,6 @@ export interface CFormInputProps extends Omit<InputHTMLAttributes<HTMLInputEleme
3636
* Specifies the type of component.
3737
*/
3838
type?: 'color' | 'file' | 'text' | string
39-
/**
40-
* Set component validation state to valid.
41-
*/
42-
valid?: boolean
4339
/**
4440
* The `value` attribute of component.
4541
*
@@ -49,7 +45,27 @@ export interface CFormInputProps extends Omit<InputHTMLAttributes<HTMLInputEleme
4945
}
5046

5147
export const CFormInput = forwardRef<HTMLInputElement, CFormInputProps>(
52-
({ children, className, invalid, plainText, size, type = 'text', valid, ...rest }, ref) => {
48+
(
49+
{
50+
children,
51+
className,
52+
feedback,
53+
feedbackInvalid,
54+
feedbackValid,
55+
floatingLabel,
56+
id,
57+
invalid,
58+
label,
59+
plainText,
60+
size,
61+
text,
62+
tooltipFeedback,
63+
type = 'text',
64+
valid,
65+
...rest
66+
},
67+
ref,
68+
) => {
5369
const _className = classNames(
5470
plainText ? 'form-control-plaintext' : 'form-control',
5571
{
@@ -61,21 +77,34 @@ export const CFormInput = forwardRef<HTMLInputElement, CFormInputProps>(
6177
className,
6278
)
6379
return (
64-
<input type={type} className={_className} {...rest} ref={ref}>
65-
{children}
66-
</input>
80+
<CFormControlWrapper
81+
describedby={rest['aria-describedby']}
82+
feedback={feedback}
83+
feedbackInvalid={feedbackInvalid}
84+
feedbackValid={feedbackValid}
85+
floatingLabel={floatingLabel}
86+
id={id}
87+
invalid={invalid}
88+
label={label}
89+
text={text}
90+
tooltipFeedback={tooltipFeedback}
91+
valid={valid}
92+
>
93+
<input className={_className} id={id} type={type} {...rest} ref={ref}>
94+
{children}
95+
</input>
96+
</CFormControlWrapper>
6797
)
6898
},
6999
)
70100

71101
CFormInput.propTypes = {
72-
children: PropTypes.node,
73102
className: PropTypes.string,
74-
invalid: PropTypes.bool,
103+
id: PropTypes.string,
75104
plainText: PropTypes.bool,
76105
size: PropTypes.oneOf(['sm', 'lg']),
77106
type: PropTypes.oneOfType([PropTypes.oneOf(['color', 'file', 'text']), PropTypes.string]),
78-
valid: PropTypes.bool,
107+
...CFormControlWrapper.propTypes,
79108
}
80109

81110
CFormInput.displayName = 'CFormInput'
Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import React, { ChangeEventHandler, forwardRef, InputHTMLAttributes } from 'react'
2-
import PropTypes from 'prop-types'
2+
33
import classNames from 'classnames'
4+
import PropTypes from 'prop-types'
5+
6+
import { CFormControlWrapper, CFormControlWrapperProps } from './CFormControlWrapper'
47

58
type Option = {
69
disabled?: boolean
710
label?: string
811
value?: string
912
}
10-
export interface CFormSelectProps extends Omit<InputHTMLAttributes<HTMLSelectElement>, 'size'> {
13+
export interface CFormSelectProps
14+
extends CFormControlWrapperProps,
15+
Omit<InputHTMLAttributes<HTMLSelectElement>, 'size'> {
1116
/**
1217
* A string of all className you want applied to the component.
1318
*/
@@ -16,10 +21,6 @@ export interface CFormSelectProps extends Omit<InputHTMLAttributes<HTMLSelectEle
1621
* Specifies the number of visible options in a drop-down list.
1722
*/
1823
htmlSize?: number
19-
/**
20-
* Set component validation state to invalid.
21-
*/
22-
invalid?: boolean
2324
/**
2425
* Method called immediately after the `value` prop changes.
2526
*/
@@ -35,20 +36,36 @@ export interface CFormSelectProps extends Omit<InputHTMLAttributes<HTMLSelectEle
3536
* Size the component small or large.
3637
*/
3738
size?: 'sm' | 'lg'
38-
/**
39-
* Set component validation state to valid.
40-
*/
41-
valid?: boolean
4239
/**
4340
* The `value` attribute of component.
4441
*
4542
* @controllable onChange
46-
* */
43+
*/
4744
value?: string | string[] | number
4845
}
4946

5047
export const CFormSelect = forwardRef<HTMLSelectElement, CFormSelectProps>(
51-
({ children, className, htmlSize, invalid, options, size, valid, ...rest }, ref) => {
48+
(
49+
{
50+
children,
51+
className,
52+
feedback,
53+
feedbackInvalid,
54+
feedbackValid,
55+
floatingLabel,
56+
htmlSize,
57+
id,
58+
invalid,
59+
label,
60+
options,
61+
size,
62+
text,
63+
tooltipFeedback,
64+
valid,
65+
...rest
66+
},
67+
ref,
68+
) => {
5269
const _className = classNames(
5370
'form-select',
5471
{
@@ -59,34 +76,45 @@ export const CFormSelect = forwardRef<HTMLSelectElement, CFormSelectProps>(
5976
className,
6077
)
6178
return (
62-
<select className={_className} size={htmlSize} {...rest} ref={ref}>
63-
{options
64-
? options.map((option, index) => {
65-
return (
66-
<option
67-
{...(typeof option === 'object' &&
68-
option.disabled && { disabled: option.disabled })}
69-
{...(typeof option === 'object' && option.value && { value: option.value })}
70-
key={index}
71-
>
72-
{typeof option === 'string' ? option : option.label}
73-
</option>
74-
)
75-
})
76-
: children}
77-
</select>
79+
<CFormControlWrapper
80+
describedby={rest['aria-describedby']}
81+
feedback={feedback}
82+
feedbackInvalid={feedbackInvalid}
83+
feedbackValid={feedbackValid}
84+
floatingLabel={floatingLabel}
85+
id={id}
86+
invalid={invalid}
87+
label={label}
88+
text={text}
89+
tooltipFeedback={tooltipFeedback}
90+
valid={valid}
91+
>
92+
<select id={id} className={_className} size={htmlSize} {...rest} ref={ref}>
93+
{options
94+
? options.map((option, index) => {
95+
return (
96+
<option
97+
{...(typeof option === 'object' &&
98+
option.disabled && { disabled: option.disabled })}
99+
{...(typeof option === 'object' && option.value && { value: option.value })}
100+
key={index}
101+
>
102+
{typeof option === 'string' ? option : option.label}
103+
</option>
104+
)
105+
})
106+
: children}
107+
</select>
108+
</CFormControlWrapper>
78109
)
79110
},
80111
)
81112

82113
CFormSelect.propTypes = {
83-
children: PropTypes.node,
84114
className: PropTypes.string,
85115
htmlSize: PropTypes.number,
86-
invalid: PropTypes.bool,
87116
options: PropTypes.array,
88-
size: PropTypes.oneOf(['sm', 'lg']),
89-
valid: PropTypes.bool,
117+
...CFormControlWrapper.propTypes,
90118
}
91119

92120
CFormSelect.displayName = 'CFormSelect'

0 commit comments

Comments
 (0)