playbook_ui 14.9.0.pre.alpha.PBNTR738collapsiblewithintablekit4855 → 14.9.0.pre.alpha.PLAY1731inputmasking4868
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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_collapsible/_collapsible.tsx +4 -9
- data/app/pb_kits/playbook/pb_collapsible/child_kits/CollapsibleContent.tsx +2 -2
- data/app/pb_kits/playbook/pb_collapsible/child_kits/CollapsibleMain.tsx +2 -2
- data/app/pb_kits/playbook/pb_table/docs/example.yml +0 -5
- data/app/pb_kits/playbook/pb_table/docs/index.js +0 -5
- data/app/pb_kits/playbook/pb_table/styles/_all.scss +1 -2
- data/app/pb_kits/playbook/pb_table/subcomponents/_table_row.tsx +1 -106
- data/app/pb_kits/playbook/pb_text_input/_text_input.tsx +35 -3
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_mask.jsx +88 -0
- data/app/pb_kits/playbook/pb_text_input/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_text_input/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_text_input/inputMask.ts +64 -0
- data/app/pb_kits/playbook/pb_text_input/text_input.test.js +139 -2
- data/dist/chunks/_typeahead-Cx7J1O_I.js +22 -0
- data/dist/chunks/_weekday_stacked-Z6CSak-K.js +45 -0
- data/dist/chunks/{lib-CtS0DLMo.js → lib-SyD3buPZ.js} +3 -3
- data/dist/chunks/{pb_form_validation-C3vQKj7-.js → pb_form_validation-Dt8UJgrJ.js} +1 -1
- data/dist/chunks/vendor.js +1 -1
- data/dist/playbook-doc.js +1 -1
- data/dist/playbook-rails-react-bindings.js +1 -1
- data/dist/playbook-rails.js +1 -1
- data/dist/playbook.css +1 -1
- data/lib/playbook/version.rb +1 -1
- metadata +7 -16
- data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible.jsx +0 -75
- data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible.md +0 -1
- data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_custom_click.jsx +0 -108
- data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_custom_click.md +0 -2
- data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_custom_content.jsx +0 -94
- data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_custom_content.md +0 -0
- data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_nested_rows.jsx +0 -83
- data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_nested_rows.md +0 -3
- data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_nested_table.jsx +0 -120
- data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_nested_table.md +0 -1
- data/app/pb_kits/playbook/pb_table/styles/_collapsible.scss +0 -35
- data/dist/chunks/_typeahead-ClJHKLj6.js +0 -22
- data/dist/chunks/_weekday_stacked-DaVTrOfA.js +0 -45
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b40a6d80e2e00552963c2986d6ae0e1bad26cd43320b0959b534e37f0e7d71fa
|
4
|
+
data.tar.gz: 52bb830750b5edc87fe3a4c670c944e1fcdf78764649ebcc708db45257694c0d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c4f3b88e16a1efe2875d297f45e030854943cc62a8a8ddaf8629c6e6f674e54583e72bfe24d1185ef67de57b3e497597f0c328c427f362f6868b5b136e720541
|
7
|
+
data.tar.gz: 8d309708dbc3912906a4f2b73829fcff692c195b641bd352dee6e313409f74c6ba2c8ea782bbb4c8784a7ca5e5ee3ed5084e4cc147130e37bc9f5203e4cbdee2
|
@@ -2,7 +2,7 @@ import React, { useEffect, ReactElement } from 'react'
|
|
2
2
|
import classnames from 'classnames'
|
3
3
|
import useCollapsible from './useCollapsible'
|
4
4
|
|
5
|
-
import { globalProps, globalInlineProps
|
5
|
+
import { globalProps, globalInlineProps } from '../utilities/globalProps'
|
6
6
|
import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from '../utilities/props'
|
7
7
|
|
8
8
|
import CollapsibleContent from './child_kits/CollapsibleContent'
|
@@ -32,7 +32,6 @@ type CollapsibleProps = {
|
|
32
32
|
onClick?: ()=> void,
|
33
33
|
htmlOptions?: {[key: string]: string | number | boolean | (() => void)},
|
34
34
|
id?: string,
|
35
|
-
tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span' | 'div' | 'tr' | 'th' | 'td' | 'thead' | 'col',
|
36
35
|
}
|
37
36
|
|
38
37
|
const Collapsible = ({
|
@@ -48,9 +47,8 @@ const Collapsible = ({
|
|
48
47
|
onIconClick,
|
49
48
|
onClick,
|
50
49
|
id,
|
51
|
-
tag = 'div',
|
52
50
|
...props
|
53
|
-
}: CollapsibleProps
|
51
|
+
}: CollapsibleProps): React.ReactElement => {
|
54
52
|
const [isCollapsed, toggle, setIsCollapsed] = useCollapsible(collapsed)
|
55
53
|
|
56
54
|
useEffect(()=> {
|
@@ -78,12 +76,9 @@ const Collapsible = ({
|
|
78
76
|
className
|
79
77
|
)
|
80
78
|
const dynamicInlineProps = globalInlineProps(props)
|
81
|
-
|
82
|
-
const Tag: React.ReactElement | any = `${tag}`;
|
83
|
-
|
84
79
|
return (
|
85
80
|
<CollapsibleContext.Provider value={{ collapsed: isCollapsed, toggle, icon, iconSize, iconColor, onIconClick, onClick }}>
|
86
|
-
<
|
81
|
+
<div
|
87
82
|
{...ariaProps}
|
88
83
|
{...dataProps}
|
89
84
|
{...htmlProps}
|
@@ -101,7 +96,7 @@ const Collapsible = ({
|
|
101
96
|
<CollapsibleContent {...contentProps}>
|
102
97
|
{contentChildren}
|
103
98
|
</CollapsibleContent>
|
104
|
-
</
|
99
|
+
</div>
|
105
100
|
</CollapsibleContext.Provider>
|
106
101
|
)
|
107
102
|
}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import classnames from 'classnames'
|
2
2
|
import React, { useContext, useRef, useEffect } from 'react'
|
3
3
|
import { buildCss } from '../../utilities/props'
|
4
|
-
import { globalProps
|
4
|
+
import { globalProps } from '../../utilities/globalProps'
|
5
5
|
import { hideElement, showElement } from '../_helper_functions'
|
6
6
|
|
7
7
|
import CollapsibleContext from '../context'
|
@@ -15,7 +15,7 @@ const CollapsibleContent = ({
|
|
15
15
|
children,
|
16
16
|
className,
|
17
17
|
...props
|
18
|
-
}: CollapsibleContentProps
|
18
|
+
}: CollapsibleContentProps): React.ReactElement => {
|
19
19
|
const context: {[key: string]: boolean | string} = useContext(CollapsibleContext)
|
20
20
|
const contentCSS = buildCss('pb_collapsible_content_kit')
|
21
21
|
const contentSpacing = globalProps(props)
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import classnames from 'classnames'
|
4
4
|
import React, { useContext } from 'react'
|
5
5
|
import { buildCss } from '../../utilities/props'
|
6
|
-
import { globalProps
|
6
|
+
import { globalProps } from '../../utilities/globalProps'
|
7
7
|
|
8
8
|
import Flex from '../../pb_flex/_flex'
|
9
9
|
import FlexItem from '../../pb_flex/_flex_item'
|
@@ -25,7 +25,7 @@ const CollapsibleMain = ({
|
|
25
25
|
className,
|
26
26
|
cursor = 'pointer',
|
27
27
|
...props
|
28
|
-
}: CollapsibleMainProps
|
28
|
+
}: CollapsibleMainProps): React.ReactElement=> {
|
29
29
|
const {collapsed, toggle, icon, iconSize, iconColor, onIconClick, onClick}: any = useContext(CollapsibleContext)
|
30
30
|
const mainCSS = buildCss('pb_collapsible_main_kit')
|
31
31
|
const mainSpacing = globalProps(props, { cursor })
|
@@ -55,8 +55,3 @@ examples:
|
|
55
55
|
- table_with_subcomponents: Table with Sub Components (Table Elements)
|
56
56
|
- table_with_subcomponents_as_divs: Table with Sub Components (Divs)
|
57
57
|
- table_outer_padding: Outer Padding
|
58
|
-
- table_with_collapsible: Table with Collapsible
|
59
|
-
- table_with_collapsible_with_custom_click: Table with Collapsible with Custom Click
|
60
|
-
- table_with_collapsible_with_custom_content: Table with Collapsible with Custom Content
|
61
|
-
- table_with_collapsible_with_nested_rows: Table with Collapsible with Nested Rows
|
62
|
-
- table_with_collapsible_with_nested_table: Table with Collapsible with Nested Table
|
@@ -26,8 +26,3 @@ export { default as TableWithSubcomponents } from './_table_with_subcomponents.j
|
|
26
26
|
export { default as TableWithSubcomponentsAsDivs } from './_table_with_subcomponents_as_divs.jsx'
|
27
27
|
export { default as TableOuterPadding } from './_table_outer_padding.jsx'
|
28
28
|
export { default as TableStickyLeftColumns } from './_table_sticky_left_columns.jsx'
|
29
|
-
export { default as TableWithCollapsible } from './_table_with_collapsible.jsx'
|
30
|
-
export { default as TableWithCollapsibleWithCustomContent } from './_table_with_collapsible_with_custom_content.jsx'
|
31
|
-
export { default as TableWithCollapsibleWithNestedTable } from './_table_with_collapsible_with_nested_table.jsx'
|
32
|
-
export { default as TableWithCollapsibleWithNestedRows } from './_table_with_collapsible_with_nested_rows.jsx'
|
33
|
-
export { default as TableWithCollapsibleWithCustomClick } from './_table_with_collapsible_with_custom_click.jsx'
|
@@ -7,21 +7,14 @@ import {
|
|
7
7
|
buildHtmlProps,
|
8
8
|
} from "../../utilities/props";
|
9
9
|
import { globalProps } from "../../utilities/globalProps";
|
10
|
-
import Collapsible from "../../pb_collapsible/_collapsible";
|
11
|
-
import useCollapsible from "../../pb_collapsible/useCollapsible";
|
12
10
|
|
13
11
|
type TableRowPropTypes = {
|
14
12
|
aria?: { [key: string]: string };
|
15
13
|
children: React.ReactNode[] | React.ReactNode;
|
16
14
|
className: string;
|
17
|
-
collapsible?: boolean;
|
18
|
-
collapsibleContent?: React.ReactNode[] | React.ReactNode;
|
19
|
-
collapsibleSideHighlight?: boolean;
|
20
15
|
data?: { [key: string]: string };
|
21
|
-
dark?: boolean;
|
22
16
|
htmlOptions?: { [key: string]: string | number | boolean | (() => void) };
|
23
17
|
id?: string;
|
24
|
-
toggleCellId?: string;
|
25
18
|
sideHighlightColor: string;
|
26
19
|
tag?: "table" | "div";
|
27
20
|
};
|
@@ -30,15 +23,10 @@ const TableRow = (props: TableRowPropTypes): React.ReactElement => {
|
|
30
23
|
const {
|
31
24
|
aria = {},
|
32
25
|
children,
|
33
|
-
collapsible,
|
34
|
-
collapsibleContent,
|
35
|
-
collapsibleSideHighlight = true,
|
36
26
|
className,
|
37
27
|
data = {},
|
38
|
-
dark = false,
|
39
28
|
htmlOptions = {},
|
40
29
|
id,
|
41
|
-
toggleCellId,
|
42
30
|
sideHighlightColor = "none",
|
43
31
|
tag = "table",
|
44
32
|
} = props;
|
@@ -48,110 +36,17 @@ const TableRow = (props: TableRowPropTypes): React.ReactElement => {
|
|
48
36
|
const htmlProps = buildHtmlProps(htmlOptions);
|
49
37
|
const sideHighlightClass =
|
50
38
|
sideHighlightColor != "" ? `side_highlight_${sideHighlightColor}` : null;
|
51
|
-
|
52
|
-
const [isCollapsed, setIsCollapsed] = useCollapsible(true);
|
53
|
-
|
54
|
-
const collapsibleRow = collapsible && isCollapsed === true ? "collapsible_table_row" : null;
|
55
39
|
const classes = classnames(
|
56
40
|
buildCss("pb_table_row_kit", sideHighlightClass),
|
57
41
|
"pb_table_tr",
|
58
|
-
collapsibleRow,
|
59
42
|
globalProps(props),
|
60
43
|
className
|
61
44
|
);
|
62
45
|
const isTableTag = tag === "table";
|
63
46
|
|
64
|
-
// const [isCollapsed, setIsCollapsed] = useCollapsible(true);
|
65
|
-
|
66
|
-
const colSpan = React.Children.count(children);
|
67
|
-
|
68
|
-
const handleRowClick = (event: React.MouseEvent) => {
|
69
|
-
if (toggleCellId) {
|
70
|
-
const target = event.target as HTMLElement;
|
71
|
-
const clickedCell = target.closest(`#${toggleCellId}`);
|
72
|
-
const isIconClick =
|
73
|
-
target instanceof SVGElement &&
|
74
|
-
(target.matches("svg.pb_custom_icon") || target.closest("svg.pb_custom_icon"));
|
75
|
-
|
76
|
-
if (clickedCell || isIconClick) {
|
77
|
-
setIsCollapsed(!isCollapsed);
|
78
|
-
}
|
79
|
-
} else {
|
80
|
-
setIsCollapsed(!isCollapsed);
|
81
|
-
}
|
82
|
-
};
|
83
|
-
|
84
47
|
return (
|
85
48
|
<>
|
86
|
-
{
|
87
|
-
isTableTag ? (
|
88
|
-
<>
|
89
|
-
<tr
|
90
|
-
{...ariaProps}
|
91
|
-
{...dataProps}
|
92
|
-
{...htmlProps}
|
93
|
-
className={classes}
|
94
|
-
id={id}
|
95
|
-
onClick={(e)=>handleRowClick(e)}
|
96
|
-
style={{ cursor: toggleCellId ? "default" : "pointer" }}
|
97
|
-
>
|
98
|
-
{children}
|
99
|
-
</tr>
|
100
|
-
<tr>
|
101
|
-
<Collapsible
|
102
|
-
collapsed={isCollapsed}
|
103
|
-
dark={dark}
|
104
|
-
htmlOptions={{ colSpan: colSpan }}
|
105
|
-
padding="none"
|
106
|
-
tag="td"
|
107
|
-
>
|
108
|
-
<tr/>
|
109
|
-
<Collapsible.Content
|
110
|
-
className={collapsibleSideHighlight ? `table_collapsible_side_highlight` : ''}
|
111
|
-
dark={dark}
|
112
|
-
margin="none"
|
113
|
-
padding="none"
|
114
|
-
>
|
115
|
-
{collapsibleContent}
|
116
|
-
</Collapsible.Content>
|
117
|
-
</Collapsible>
|
118
|
-
</tr>
|
119
|
-
</>
|
120
|
-
) : (
|
121
|
-
<>
|
122
|
-
<div
|
123
|
-
{...ariaProps}
|
124
|
-
{...dataProps}
|
125
|
-
{...htmlProps}
|
126
|
-
className={classes}
|
127
|
-
id={id}
|
128
|
-
onClick={handleRowClick}
|
129
|
-
style={{ cursor: "pointer" }}
|
130
|
-
>
|
131
|
-
{children}
|
132
|
-
</div>
|
133
|
-
<tr>
|
134
|
-
<Collapsible
|
135
|
-
collapsed={isCollapsed}
|
136
|
-
dark={dark}
|
137
|
-
htmlOptions={{ colSpan: colSpan }}
|
138
|
-
padding="none"
|
139
|
-
tag="td"
|
140
|
-
>
|
141
|
-
<tr/>
|
142
|
-
<Collapsible.Content
|
143
|
-
className={collapsibleSideHighlight ? `table_collapsible_side_highlight` : ''}
|
144
|
-
dark={dark}
|
145
|
-
margin="none"
|
146
|
-
padding="none"
|
147
|
-
>
|
148
|
-
{collapsibleContent}
|
149
|
-
</Collapsible.Content>
|
150
|
-
</Collapsible>
|
151
|
-
</tr>
|
152
|
-
</>
|
153
|
-
)
|
154
|
-
) : isTableTag ? (
|
49
|
+
{isTableTag ? (
|
155
50
|
<tr
|
156
51
|
{...ariaProps}
|
157
52
|
{...dataProps}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import React, { forwardRef } from 'react'
|
1
|
+
import React, { forwardRef, ChangeEvent } from 'react'
|
2
2
|
import classnames from 'classnames'
|
3
3
|
|
4
4
|
import { globalProps, GlobalProps, domSafeProps } from '../utilities/globalProps'
|
@@ -10,6 +10,8 @@ import Caption from '../pb_caption/_caption'
|
|
10
10
|
import Body from '../pb_body/_body'
|
11
11
|
import Icon from '../pb_icon/_icon'
|
12
12
|
|
13
|
+
import { INPUTMASKS } from './inputMask'
|
14
|
+
|
13
15
|
type TextInputProps = {
|
14
16
|
aria?: { [key: string]: string },
|
15
17
|
className?: string,
|
@@ -22,6 +24,7 @@ type TextInputProps = {
|
|
22
24
|
inline?: boolean,
|
23
25
|
name: string,
|
24
26
|
label: string,
|
27
|
+
mask?: 'currency' | 'zipCode' | 'postalCode' | 'ssn',
|
25
28
|
onChange: (e: React.FormEvent<HTMLInputElement>) => void,
|
26
29
|
placeholder: string,
|
27
30
|
required?: boolean,
|
@@ -47,6 +50,7 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
|
|
47
50
|
htmlOptions = {},
|
48
51
|
id,
|
49
52
|
inline = false,
|
53
|
+
mask = null,
|
50
54
|
name,
|
51
55
|
label,
|
52
56
|
onChange = () => { void 0 },
|
@@ -90,6 +94,33 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
|
|
90
94
|
/>
|
91
95
|
)
|
92
96
|
|
97
|
+
const isMaskedInput = mask && mask in INPUTMASKS
|
98
|
+
|
99
|
+
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
100
|
+
if (isMaskedInput) {
|
101
|
+
const inputValue = e.target.value
|
102
|
+
|
103
|
+
let cursorPosition = e.target.selectionStart;
|
104
|
+
const isAtEnd = cursorPosition === inputValue.length;
|
105
|
+
|
106
|
+
const formattedValue = INPUTMASKS[mask].format(inputValue)
|
107
|
+
e.target.value = formattedValue
|
108
|
+
|
109
|
+
// Keep cursor position
|
110
|
+
if (!isAtEnd) {
|
111
|
+
// Account for extra characters (e.g., commas added/removed in currency)
|
112
|
+
if (formattedValue.length - inputValue.length === 1) {
|
113
|
+
cursorPosition = cursorPosition + 1
|
114
|
+
} else if (mask === "currency" && formattedValue.length - inputValue.length === -1) {
|
115
|
+
cursorPosition = cursorPosition - 1
|
116
|
+
}
|
117
|
+
e.target.selectionStart = e.target.selectionEnd = cursorPosition
|
118
|
+
}
|
119
|
+
}
|
120
|
+
|
121
|
+
onChange(e)
|
122
|
+
}
|
123
|
+
|
93
124
|
const childInput = children ? children.type === "input" : undefined
|
94
125
|
|
95
126
|
const textInput = (
|
@@ -101,8 +132,9 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
|
|
101
132
|
id={id}
|
102
133
|
key={id}
|
103
134
|
name={name}
|
104
|
-
onChange={onChange}
|
105
|
-
|
135
|
+
onChange={isMaskedInput ? handleChange : onChange}
|
136
|
+
pattern={isMaskedInput ? INPUTMASKS[mask]?.pattern : undefined}
|
137
|
+
placeholder={placeholder || (isMaskedInput ? INPUTMASKS[mask]?.placeholder : undefined)}
|
106
138
|
ref={ref}
|
107
139
|
required={required}
|
108
140
|
type={type}
|
@@ -0,0 +1,88 @@
|
|
1
|
+
import React, { useState } from 'react'
|
2
|
+
|
3
|
+
import Caption from '../../pb_caption/_caption'
|
4
|
+
import TextInput from '../../pb_text_input/_text_input'
|
5
|
+
import Title from '../../pb_title/_title'
|
6
|
+
|
7
|
+
const TextInputMask = (props) => {
|
8
|
+
const [ssn, setSSN] = useState('')
|
9
|
+
const handleOnChangeSSN = ({ target }) => {
|
10
|
+
setSSN(target.value)
|
11
|
+
}
|
12
|
+
const ref = React.createRef()
|
13
|
+
|
14
|
+
const [formFields, setFormFields] = useState({
|
15
|
+
currency: '',
|
16
|
+
zipCode: '',
|
17
|
+
postalCode: '',
|
18
|
+
ssn: '',
|
19
|
+
})
|
20
|
+
|
21
|
+
const handleOnChangeFormField = ({ target }) => {
|
22
|
+
const { name, value } = target
|
23
|
+
setFormFields({ ...formFields, [name]: value })
|
24
|
+
}
|
25
|
+
|
26
|
+
return (
|
27
|
+
<div>
|
28
|
+
<TextInput
|
29
|
+
label="Currency"
|
30
|
+
mask="currency"
|
31
|
+
name="currency"
|
32
|
+
onChange={handleOnChangeFormField}
|
33
|
+
value={formFields.currency}
|
34
|
+
{...props}
|
35
|
+
/>
|
36
|
+
<TextInput
|
37
|
+
label="Zip Code"
|
38
|
+
mask="zipCode"
|
39
|
+
name="zipCode"
|
40
|
+
onChange={handleOnChangeFormField}
|
41
|
+
value={formFields.zipCode}
|
42
|
+
{...props}
|
43
|
+
/>
|
44
|
+
<TextInput
|
45
|
+
label="Postal Code"
|
46
|
+
mask="postalCode"
|
47
|
+
name="postalCode"
|
48
|
+
onChange={handleOnChangeFormField}
|
49
|
+
value={formFields.postalCode}
|
50
|
+
{...props}
|
51
|
+
/>
|
52
|
+
<TextInput
|
53
|
+
label="SSN"
|
54
|
+
mask="ssn"
|
55
|
+
name="ssn"
|
56
|
+
onChange={handleOnChangeFormField}
|
57
|
+
value={formFields.ssn}
|
58
|
+
{...props}
|
59
|
+
/>
|
60
|
+
|
61
|
+
<br />
|
62
|
+
<br />
|
63
|
+
|
64
|
+
<Title>{'Event Handler Props'}</Title>
|
65
|
+
|
66
|
+
<br />
|
67
|
+
<Caption>{'onChange'}</Caption>
|
68
|
+
|
69
|
+
<br />
|
70
|
+
|
71
|
+
<TextInput
|
72
|
+
label="SSN"
|
73
|
+
mask="ssn"
|
74
|
+
onChange={handleOnChangeSSN}
|
75
|
+
placeholder="Enter SSN"
|
76
|
+
ref={ref}
|
77
|
+
value={ssn}
|
78
|
+
{...props}
|
79
|
+
/>
|
80
|
+
|
81
|
+
{ssn !== '' && (
|
82
|
+
<React.Fragment>{`SSN is: ${ssn}`}</React.Fragment>
|
83
|
+
)}
|
84
|
+
</div>
|
85
|
+
)
|
86
|
+
}
|
87
|
+
|
88
|
+
export default TextInputMask
|
@@ -5,3 +5,4 @@ export { default as TextInputDisabled } from './_text_input_disabled.jsx'
|
|
5
5
|
export { default as TextInputAddOn } from './_text_input_add_on.jsx'
|
6
6
|
export { default as TextInputInline } from './_text_input_inline.jsx'
|
7
7
|
export { default as TextInputNoLabel } from './_text_input_no_label.jsx'
|
8
|
+
export { default as TextInputMask } from './_text_input_mask.jsx'
|
@@ -0,0 +1,64 @@
|
|
1
|
+
type InputMask = {
|
2
|
+
format: (value: string) => string
|
3
|
+
pattern: string
|
4
|
+
placeholder: string
|
5
|
+
}
|
6
|
+
|
7
|
+
type InputMaskDictionary = {
|
8
|
+
[key in 'currency' | 'zipCode' | 'postalCode' | 'ssn']: InputMask
|
9
|
+
}
|
10
|
+
|
11
|
+
const formatCurrency = (value: string): string => {
|
12
|
+
const numericValue = value.replace(/[^0-9]/g, '').slice(0, 15)
|
13
|
+
|
14
|
+
if (!numericValue) return ''
|
15
|
+
|
16
|
+
const dollars = parseFloat((parseInt(numericValue) / 100).toFixed(2))
|
17
|
+
if (dollars === 0) return ''
|
18
|
+
|
19
|
+
return new Intl.NumberFormat('en-US', {
|
20
|
+
style: 'currency',
|
21
|
+
currency: 'USD',
|
22
|
+
maximumFractionDigits: 2,
|
23
|
+
}).format(dollars)
|
24
|
+
}
|
25
|
+
|
26
|
+
const formatBasicPostal = (value: string): string => {
|
27
|
+
return value.replace(/\D/g, '').slice(0, 5)
|
28
|
+
}
|
29
|
+
|
30
|
+
const formatExtendedPostal = (value: string): string => {
|
31
|
+
const cleaned = value.replace(/\D/g, '').slice(0, 9)
|
32
|
+
return cleaned.replace(/(\d{5})(?=\d)/, '$1-')
|
33
|
+
}
|
34
|
+
|
35
|
+
const formatSSN = (value: string): string => {
|
36
|
+
const cleaned = value.replace(/\D/g, '').slice(0, 9)
|
37
|
+
return cleaned
|
38
|
+
.replace(/(\d{5})(?=\d)/, '$1-')
|
39
|
+
.replace(/(\d{3})(?=\d)/, '$1-')
|
40
|
+
}
|
41
|
+
|
42
|
+
export const INPUTMASKS: InputMaskDictionary = {
|
43
|
+
currency: {
|
44
|
+
format: formatCurrency,
|
45
|
+
// eslint-disable-next-line no-useless-escape
|
46
|
+
pattern: '^\$\d{1,3}(?:,\d{3})*(?:\.\d{2})?$',
|
47
|
+
placeholder: '$0.00',
|
48
|
+
},
|
49
|
+
zipCode: {
|
50
|
+
format: formatBasicPostal,
|
51
|
+
pattern: '\\d{5}',
|
52
|
+
placeholder: '12345',
|
53
|
+
},
|
54
|
+
postalCode: {
|
55
|
+
format: formatExtendedPostal,
|
56
|
+
pattern: '\\d{5}-\\d{4}',
|
57
|
+
placeholder: '12345-6789',
|
58
|
+
},
|
59
|
+
ssn: {
|
60
|
+
format: formatSSN,
|
61
|
+
pattern: '\\d{3}-\\d{2}-\\d{4}',
|
62
|
+
placeholder: '123-45-6789',
|
63
|
+
},
|
64
|
+
}
|
@@ -1,5 +1,5 @@
|
|
1
|
-
import React from 'react'
|
2
|
-
import { render, screen } from '../utilities/test-utils'
|
1
|
+
import React, { useState } from 'react'
|
2
|
+
import { render, screen, fireEvent, within } from '../utilities/test-utils'
|
3
3
|
|
4
4
|
import TextInput from './_text_input'
|
5
5
|
|
@@ -89,3 +89,140 @@ test('returns additional class name', () => {
|
|
89
89
|
const kit = screen.getByTestId(testId)
|
90
90
|
expect(kit).toHaveClass(`${kitClass} mb_lg`)
|
91
91
|
})
|
92
|
+
|
93
|
+
|
94
|
+
const TextInputCurrencyMask = (props) => {
|
95
|
+
const [currency, setValue] = useState('')
|
96
|
+
const handleOnChange = ({ target }) => {
|
97
|
+
setValue(target.value)
|
98
|
+
}
|
99
|
+
|
100
|
+
return (
|
101
|
+
<TextInput
|
102
|
+
mask="currency"
|
103
|
+
onChange={handleOnChange}
|
104
|
+
value={currency}
|
105
|
+
{...props}
|
106
|
+
/>
|
107
|
+
)
|
108
|
+
}
|
109
|
+
|
110
|
+
test('returns masked currency value', () => {
|
111
|
+
render(
|
112
|
+
<TextInputCurrencyMask
|
113
|
+
data={{ testid: testId }}
|
114
|
+
/>
|
115
|
+
)
|
116
|
+
|
117
|
+
const kit = screen.getByTestId(testId)
|
118
|
+
|
119
|
+
const input = within(kit).getByRole('textbox');
|
120
|
+
|
121
|
+
fireEvent.change(input, { target: { value: '123456' } });
|
122
|
+
|
123
|
+
expect(input.value).toBe('$1,234.56')
|
124
|
+
|
125
|
+
fireEvent.change(input, { target: { value: '1' } });
|
126
|
+
|
127
|
+
expect(input.value).toBe('$0.01')
|
128
|
+
|
129
|
+
fireEvent.change(input, { target: { value: '' } });
|
130
|
+
|
131
|
+
expect(input.value).toBe('')
|
132
|
+
})
|
133
|
+
|
134
|
+
const TextInputZipCodeMask = (props) => {
|
135
|
+
const [zipCode, setValue] = useState('')
|
136
|
+
const handleOnChange = ({ target }) => {
|
137
|
+
setValue(target.value)
|
138
|
+
}
|
139
|
+
|
140
|
+
return (
|
141
|
+
<TextInput
|
142
|
+
mask="zipCode"
|
143
|
+
onChange={handleOnChange}
|
144
|
+
value={zipCode}
|
145
|
+
{...props}
|
146
|
+
/>
|
147
|
+
)
|
148
|
+
}
|
149
|
+
|
150
|
+
test('returns masked zip code value', () => {
|
151
|
+
render(
|
152
|
+
<TextInputZipCodeMask
|
153
|
+
data={{ testid: testId }}
|
154
|
+
/>
|
155
|
+
)
|
156
|
+
|
157
|
+
const kit = screen.getByTestId(testId)
|
158
|
+
|
159
|
+
const input = within(kit).getByRole('textbox');
|
160
|
+
|
161
|
+
fireEvent.change(input, { target: { value: '123456' } });
|
162
|
+
|
163
|
+
expect(input.value).toBe('12345')
|
164
|
+
})
|
165
|
+
|
166
|
+
const TextInputPostalCodeMask = (props) => {
|
167
|
+
const [postalCode, setValue] = useState('')
|
168
|
+
const handleOnChange = ({ target }) => {
|
169
|
+
setValue(target.value)
|
170
|
+
}
|
171
|
+
|
172
|
+
return (
|
173
|
+
<TextInput
|
174
|
+
mask="postalCode"
|
175
|
+
onChange={handleOnChange}
|
176
|
+
value={postalCode}
|
177
|
+
{...props}
|
178
|
+
/>
|
179
|
+
)
|
180
|
+
}
|
181
|
+
|
182
|
+
test('returns masked postal code value', () => {
|
183
|
+
render(
|
184
|
+
<TextInputPostalCodeMask
|
185
|
+
data={{ testid: testId }}
|
186
|
+
/>
|
187
|
+
)
|
188
|
+
|
189
|
+
const kit = screen.getByTestId(testId)
|
190
|
+
|
191
|
+
const input = within(kit).getByRole('textbox');
|
192
|
+
|
193
|
+
fireEvent.change(input, { target: { value: '123456789' } });
|
194
|
+
|
195
|
+
expect(input.value).toBe('12345-6789')
|
196
|
+
})
|
197
|
+
|
198
|
+
const TextInputSSNMask = (props) => {
|
199
|
+
const [ssn, setValue] = useState('')
|
200
|
+
const handleOnChange = ({ target }) => {
|
201
|
+
setValue(target.value)
|
202
|
+
}
|
203
|
+
|
204
|
+
return (
|
205
|
+
<TextInput
|
206
|
+
mask="ssn"
|
207
|
+
onChange={handleOnChange}
|
208
|
+
value={ssn}
|
209
|
+
{...props}
|
210
|
+
/>
|
211
|
+
)
|
212
|
+
}
|
213
|
+
|
214
|
+
test('returns masked ssn value', () => {
|
215
|
+
render(
|
216
|
+
<TextInputSSNMask
|
217
|
+
data={{ testid: testId }}
|
218
|
+
/>
|
219
|
+
)
|
220
|
+
|
221
|
+
const kit = screen.getByTestId(testId)
|
222
|
+
|
223
|
+
const input = within(kit).getByRole('textbox');
|
224
|
+
|
225
|
+
fireEvent.change(input, { target: { value: '123456789' } });
|
226
|
+
|
227
|
+
expect(input.value).toBe('123-45-6789')
|
228
|
+
})
|