playbook_ui 14.9.0 → 14.10.0.pre.alpha.PBNTR662stickyrightcolumnreact5160

Sign up to get free protection for your applications and to get access to all the features.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/Components/CollapsibleTrail.tsx +1 -6
  3. data/app/pb_kits/playbook/pb_advanced_table/Components/TableHeaderCell.tsx +32 -19
  4. data/app/pb_kits/playbook/pb_advanced_table/SubKits/TableBody.tsx +3 -1
  5. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.scss +48 -3
  6. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.tsx +94 -36
  7. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +2 -2
  8. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_collapsible_trail_rails.html.erb +36 -0
  9. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_collapsible_trail_rails.md +1 -0
  10. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_headers.jsx +60 -0
  11. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_headers.md +1 -0
  12. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_headers_multiple.jsx +74 -0
  13. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_headers_multiple.md +1 -0
  14. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pagination.jsx +50 -0
  15. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pagination.md +1 -0
  16. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pagination_with_props.jsx +57 -0
  17. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pagination_with_props.md +5 -0
  18. data/app/pb_kits/playbook/pb_advanced_table/docs/advanced_table_pagination_mock_data.json +5600 -0
  19. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +5 -0
  20. data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +4 -0
  21. data/app/pb_kits/playbook/pb_advanced_table/table_body.rb +4 -2
  22. data/app/pb_kits/playbook/pb_advanced_table/table_header.html.erb +5 -3
  23. data/app/pb_kits/playbook/pb_advanced_table/table_row.html.erb +8 -0
  24. data/app/pb_kits/playbook/pb_advanced_table/table_row.rb +2 -0
  25. data/app/pb_kits/playbook/pb_advanced_table/table_subrow_header.html.erb +8 -0
  26. data/app/pb_kits/playbook/pb_advanced_table/table_subrow_header.rb +2 -0
  27. data/app/pb_kits/playbook/pb_avatar/_avatar.scss +0 -2
  28. data/app/pb_kits/playbook/pb_bar_graph/docs/_bar_graph_custom.jsx +53 -49
  29. data/app/pb_kits/playbook/pb_bar_graph/docs/_bar_graph_custom_rails.html.erb +29 -36
  30. data/app/pb_kits/playbook/pb_body/_body.scss +14 -13
  31. data/app/pb_kits/playbook/pb_body/_body_mixins.scss +22 -16
  32. data/app/pb_kits/playbook/pb_bread_crumbs/docs/_bread_crumbs_default.jsx +6 -0
  33. data/app/pb_kits/playbook/pb_caption/_caption_mixin.scss +2 -1
  34. data/app/pb_kits/playbook/pb_card/_card.tsx +7 -7
  35. data/app/pb_kits/playbook/pb_card/_card_mixin.scss +2 -2
  36. data/app/pb_kits/playbook/pb_card/docs/_card_header.html.erb +21 -1
  37. data/app/pb_kits/playbook/pb_card/docs/_card_header.jsx +50 -0
  38. data/app/pb_kits/playbook/pb_card/docs/_card_header.md +1 -1
  39. data/app/pb_kits/playbook/pb_circle_icon_button/_circle_icon_button.tsx +3 -0
  40. data/app/pb_kits/playbook/pb_circle_icon_button/circle_icon_button.html.erb +1 -1
  41. data/app/pb_kits/playbook/pb_circle_icon_button/circle_icon_button.rb +1 -0
  42. data/app/pb_kits/playbook/pb_circle_icon_button/docs/_circle_icon_button_link.html.erb +8 -0
  43. data/app/pb_kits/playbook/pb_circle_icon_button/docs/_circle_icon_button_link.jsx +9 -0
  44. data/app/pb_kits/playbook/pb_circle_icon_button/docs/_circle_icon_button_link.md +1 -0
  45. data/app/pb_kits/playbook/pb_collapsible/_collapsible.tsx +9 -4
  46. data/app/pb_kits/playbook/pb_collapsible/child_kits/CollapsibleContent.tsx +2 -2
  47. data/app/pb_kits/playbook/pb_collapsible/child_kits/CollapsibleMain.tsx +2 -2
  48. data/app/pb_kits/playbook/pb_drawer/_drawer.tsx +2 -2
  49. data/app/pb_kits/playbook/pb_file_upload/_file_upload.scss +5 -0
  50. data/app/pb_kits/playbook/pb_file_upload/_file_upload.tsx +7 -2
  51. data/app/pb_kits/playbook/pb_file_upload/file_upload.html.erb +8 -2
  52. data/app/pb_kits/playbook/pb_form_group/_error_state_mixin.scss +57 -0
  53. data/app/pb_kits/playbook/pb_form_group/_form_group.scss +11 -2
  54. data/app/pb_kits/playbook/pb_gauge/_gauge.scss +31 -1
  55. data/app/pb_kits/playbook/pb_gauge/_gauge.tsx +3 -3
  56. data/app/pb_kits/playbook/pb_gauge/docs/_gauge_complex.jsx +25 -5
  57. data/app/pb_kits/playbook/pb_home_address_street/_home_address_street.tsx +17 -1
  58. data/app/pb_kits/playbook/pb_home_address_street/docs/_home_address_street_emphasis.html.erb +17 -1
  59. data/app/pb_kits/playbook/pb_home_address_street/docs/_home_address_street_emphasis.jsx +15 -0
  60. data/app/pb_kits/playbook/pb_home_address_street/docs/_home_address_street_emphasis.md +2 -1
  61. data/app/pb_kits/playbook/pb_home_address_street/home_address_street.rb +15 -1
  62. data/app/pb_kits/playbook/pb_home_address_street/none_emphasis.html.erb +32 -0
  63. data/app/pb_kits/playbook/pb_home_address_street/none_emphasis.rb +29 -0
  64. data/app/pb_kits/playbook/pb_layout/_layout.tsx +30 -11
  65. data/app/pb_kits/playbook/pb_link/_link.scss +3 -3
  66. data/app/pb_kits/playbook/pb_nav/_bold_mixin.scss +11 -1
  67. data/app/pb_kits/playbook/pb_nav/_collapsible_nav.scss +16 -2
  68. data/app/pb_kits/playbook/pb_nav/_vertical_nav.scss +1 -1
  69. data/app/pb_kits/playbook/pb_pagination/_pagination.tsx +2 -2
  70. data/app/pb_kits/playbook/pb_selectable_card/_selectable_card.scss +4 -4
  71. data/app/pb_kits/playbook/pb_selectable_card/selectable_card.html.erb +1 -1
  72. data/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_filter.jsx +166 -0
  73. data/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_height_width.jsx +2 -0
  74. data/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_user.jsx +89 -0
  75. data/app/pb_kits/playbook/pb_skeleton_loading/docs/example.yml +2 -1
  76. data/app/pb_kits/playbook/pb_skeleton_loading/docs/index.js +2 -0
  77. data/app/pb_kits/playbook/pb_stat_change/_stat_change.tsx +44 -36
  78. data/app/pb_kits/playbook/pb_stat_change/stat_change.html.erb +4 -4
  79. data/app/pb_kits/playbook/pb_table/_table.tsx +73 -24
  80. data/app/pb_kits/playbook/pb_table/docs/_table_sticky_columns.jsx +88 -0
  81. data/app/pb_kits/playbook/pb_table/docs/_table_sticky_columns_react.md +3 -0
  82. data/app/pb_kits/playbook/pb_table/docs/_table_sticky_left_columns.html.erb +77 -0
  83. data/app/pb_kits/playbook/pb_table/docs/_table_sticky_left_columns.jsx +1 -1
  84. data/app/pb_kits/playbook/pb_table/docs/_table_sticky_left_columns_rails.md +3 -0
  85. data/app/pb_kits/playbook/pb_table/docs/_table_sticky_left_columns_react.md +4 -1
  86. data/app/pb_kits/playbook/pb_table/docs/_table_sticky_right_columns.jsx +87 -0
  87. data/app/pb_kits/playbook/pb_table/docs/_table_sticky_right_columns_react.md +5 -0
  88. data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible.jsx +75 -0
  89. data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible.md +1 -0
  90. data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_custom_click.jsx +108 -0
  91. data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_custom_click.md +2 -0
  92. data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_custom_content.jsx +94 -0
  93. data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_custom_content.md +0 -0
  94. data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_nested_rows.jsx +83 -0
  95. data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_nested_rows.md +3 -0
  96. data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_nested_table.jsx +120 -0
  97. data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_with_nested_table.md +1 -0
  98. data/app/pb_kits/playbook/pb_table/docs/example.yml +7 -0
  99. data/app/pb_kits/playbook/pb_table/docs/index.js +5 -0
  100. data/app/pb_kits/playbook/pb_table/index.ts +102 -26
  101. data/app/pb_kits/playbook/pb_table/styles/_all.scss +2 -1
  102. data/app/pb_kits/playbook/pb_table/styles/_collapsible.scss +35 -0
  103. data/app/pb_kits/playbook/pb_table/styles/_scroll.scss +44 -3
  104. data/app/pb_kits/playbook/pb_table/styles/_sticky_columns.scss +17 -4
  105. data/app/pb_kits/playbook/pb_table/subcomponents/_table_row.tsx +106 -1
  106. data/app/pb_kits/playbook/pb_table/table.html.erb +8 -2
  107. data/app/pb_kits/playbook/pb_table/table.rb +21 -2
  108. data/app/pb_kits/playbook/pb_text_input/_text_input.tsx +35 -3
  109. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_mask.jsx +88 -0
  110. data/app/pb_kits/playbook/pb_text_input/docs/example.yml +1 -0
  111. data/app/pb_kits/playbook/pb_text_input/docs/index.js +1 -0
  112. data/app/pb_kits/playbook/pb_text_input/inputMask.ts +64 -0
  113. data/app/pb_kits/playbook/pb_text_input/text_input.test.js +139 -2
  114. data/app/pb_kits/playbook/pb_title/_title.scss +6 -5
  115. data/app/pb_kits/playbook/pb_title/_title_mixin.scss +13 -0
  116. data/app/pb_kits/playbook/pb_typeahead/_typeahead.scss +115 -46
  117. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_with_highlight.jsx +9 -2
  118. data/app/pb_kits/playbook/pb_typeahead/typeahead.html.erb +3 -2
  119. data/app/pb_kits/playbook/tokens/_titles.scss +0 -8
  120. data/app/pb_kits/playbook/utilities/_hover.scss +11 -2
  121. data/app/pb_kits/playbook/utilities/globalProps.ts +2 -0
  122. data/app/pb_kits/playbook/utilities/test/globalProps/hover.test.js +15 -0
  123. data/dist/chunks/_typeahead-aym7Ky_O.js +22 -0
  124. data/dist/chunks/_weekday_stacked-BZj1pop-.js +45 -0
  125. data/dist/chunks/lazysizes-B7xYodB-.js +1 -0
  126. data/dist/chunks/{lib-SyD3buPZ.js → lib-B7sgJtGS.js} +3 -3
  127. data/dist/chunks/{pb_form_validation-Dt8UJgrJ.js → pb_form_validation-C5Cc0-1v.js} +1 -1
  128. data/dist/chunks/vendor.js +1 -1
  129. data/dist/menu.yml +1 -1
  130. data/dist/playbook-doc.js +1 -1
  131. data/dist/playbook-rails-react-bindings.js +1 -1
  132. data/dist/playbook-rails.js +1 -1
  133. data/dist/playbook.css +1 -1
  134. data/lib/playbook/hover.rb +7 -1
  135. data/lib/playbook/version.rb +2 -2
  136. metadata +46 -10
  137. data/dist/chunks/_typeahead-B8fkIeXA.js +0 -22
  138. data/dist/chunks/_weekday_stacked-CVYxBYHe.js +0 -45
  139. data/dist/chunks/lazysizes-DHz07jlL.js +0 -1
@@ -1,4 +1,45 @@
1
- .table-responsive-scroll {
2
- display: block;
3
- overflow-x: auto;
1
+ @import "../../tokens/screen_sizes";
2
+
3
+ .table-responsive-scroll {
4
+ display: block;
5
+ overflow-x: scroll;
6
+
7
+ // Responsive Styles
8
+ @media (max-width: 1600px) {
9
+ &[class*="table-responsive-scroll"] {
10
+ border-radius: 4px;
11
+ box-shadow: 1px 0 0 0px $border_light,
12
+ -1px 0 0 0px $border_light
13
+ }
14
+
15
+ &[class^=pb_table].table-sm.table-card thead tr th:first-child,
16
+ &[class^=pb_table].table-sm:not(.no-hover).table-card tbody tr td:first-child {
17
+ border-left-width: 0px;
18
+ }
19
+
20
+ &[class^=pb_table].table-md.table-card thead tr th:first-child,
21
+ &[class^=pb_table].table-md:not(.no-hover).table-card tbody tr td:first-child {
22
+ border-left-width: 0px;
23
+ }
24
+
25
+ &[class^=pb_table].table-lg.table-card thead tr th:first-child,
26
+ &[class^=pb_table].table-lg:not(.no-hover).table-card tbody tr td:first-child {
27
+ border-left-width: 0px;
28
+ }
29
+
30
+ &[class^=pb_table].table-sm.table-card thead tr th:last-child,
31
+ &[class^=pb_table].table-sm:not(.no-hover).table-card tbody tr td:last-child {
32
+ border-right-width: 0px;
33
+ }
34
+
35
+ &[class^=pb_table].table-md.table-card thead tr th:last-child,
36
+ &[class^=pb_table].table-md:not(.no-hover).table-card tbody tr td:last-child {
37
+ border-right-width: 0px;
38
+ }
39
+
40
+ &[class^=pb_table].table-lg.table-card thead tr th:last-child,
41
+ &[class^=pb_table].table-lg:not(.no-hover).table-card tbody tr td:last-child {
42
+ border-right-width: 0px;
43
+ }
4
44
  }
45
+ }
@@ -3,16 +3,29 @@
3
3
  [class^="pb_table"] {
4
4
  .sticky {
5
5
  position: sticky !important;
6
- left: 0;
7
6
  z-index: 1;
8
7
  background-color: white;
9
8
  }
10
9
 
11
- .with-border {
10
+ // For use with sticky left columns
11
+ .sticky-left-shadow {
12
+ box-shadow: 4px 0 10px rgba(60, 106, 172, 0.16) !important;
13
+ left: 0;
14
+ }
15
+
16
+ .with-border-right {
12
17
  border-right: 1px solid $border_light !important;
18
+ left: 0;
13
19
  }
14
20
 
15
- .sticky-shadow {
16
- box-shadow: 4px 0 10px rgba(60, 106, 172, 0.16) !important;
21
+ // For use with sticky right columns
22
+ .sticky-right-shadow {
23
+ box-shadow: -4px 0 10px rgba(60, 106, 172, 0.16) !important;
24
+ right: 0;
25
+ }
26
+
27
+ .with-border-left {
28
+ border-left: 1px solid $border_light !important;
29
+ right: 0;
17
30
  }
18
31
  }
@@ -7,14 +7,21 @@ 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";
10
12
 
11
13
  type TableRowPropTypes = {
12
14
  aria?: { [key: string]: string };
13
15
  children: React.ReactNode[] | React.ReactNode;
14
16
  className: string;
17
+ collapsible?: boolean;
18
+ collapsibleContent?: React.ReactNode[] | React.ReactNode;
19
+ collapsibleSideHighlight?: boolean;
15
20
  data?: { [key: string]: string };
21
+ dark?: boolean;
16
22
  htmlOptions?: { [key: string]: string | number | boolean | (() => void) };
17
23
  id?: string;
24
+ toggleCellId?: string;
18
25
  sideHighlightColor: string;
19
26
  tag?: "table" | "div";
20
27
  };
@@ -23,10 +30,15 @@ const TableRow = (props: TableRowPropTypes): React.ReactElement => {
23
30
  const {
24
31
  aria = {},
25
32
  children,
33
+ collapsible,
34
+ collapsibleContent,
35
+ collapsibleSideHighlight = true,
26
36
  className,
27
37
  data = {},
38
+ dark = false,
28
39
  htmlOptions = {},
29
40
  id,
41
+ toggleCellId,
30
42
  sideHighlightColor = "none",
31
43
  tag = "table",
32
44
  } = props;
@@ -36,17 +48,110 @@ const TableRow = (props: TableRowPropTypes): React.ReactElement => {
36
48
  const htmlProps = buildHtmlProps(htmlOptions);
37
49
  const sideHighlightClass =
38
50
  sideHighlightColor != "" ? `side_highlight_${sideHighlightColor}` : null;
51
+
52
+ const [isCollapsed, setIsCollapsed] = useCollapsible(true);
53
+
54
+ const collapsibleRow = collapsible && isCollapsed === true ? "collapsible_table_row" : null;
39
55
  const classes = classnames(
40
56
  buildCss("pb_table_row_kit", sideHighlightClass),
41
57
  "pb_table_tr",
58
+ collapsibleRow,
42
59
  globalProps(props),
43
60
  className
44
61
  );
45
62
  const isTableTag = tag === "table";
46
63
 
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
+
47
84
  return (
48
85
  <>
49
- {isTableTag ? (
86
+ {collapsible ? (
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 ? (
50
155
  <tr
51
156
  {...ariaProps}
52
157
  {...dataProps}
@@ -1,4 +1,10 @@
1
- <%= content_tag(:div) do %>
1
+ <% if object.responsive_classname %>
2
+ <% responsive_class = object.responsive_classname %>
3
+ <% else %>
4
+ <% responsive_class = nil %>
5
+ <% end %>
6
+
7
+ <%= content_tag(:div, class: responsive_class) do %>
2
8
  <% if object.tag == "table" %>
3
9
  <%= content_tag(:table,
4
10
  aria: object.aria,
@@ -18,4 +24,4 @@
18
24
  <%= content.presence %>
19
25
  <% end %>
20
26
  <% end %>
21
- <% end %>
27
+ <% end %>
@@ -23,6 +23,8 @@ module Playbook
23
23
  prop :text
24
24
  prop :sticky, type: Playbook::Props::Boolean,
25
25
  default: false
26
+ prop :sticky_left_column, type: Playbook::Props::Array,
27
+ default: []
26
28
  prop :vertical_border, type: Playbook::Props::Boolean,
27
29
  default: false
28
30
  prop :striped, type: Playbook::Props::Boolean,
@@ -37,12 +39,16 @@ module Playbook
37
39
  def classname
38
40
  generate_classname(
39
41
  "pb_table", "table-#{size}", single_line_class, dark_class,
40
- disable_hover_class, container_class, data_table_class, sticky_class, collapse_class,
41
- vertical_border_class, striped_class, outer_padding_class,
42
+ disable_hover_class, container_class, data_table_class, sticky_class, sticky_left_column_class,
43
+ collapse_class, vertical_border_class, striped_class, outer_padding_class,
42
44
  "table-responsive-#{responsive}", separator: " "
43
45
  )
44
46
  end
45
47
 
48
+ def responsive_classname
49
+ responsive ? "table-responsive-#{responsive}" : nil
50
+ end
51
+
46
52
  private
47
53
 
48
54
  def dark_class
@@ -73,6 +79,19 @@ module Playbook
73
79
  sticky ? "sticky-header" : nil
74
80
  end
75
81
 
82
+ def sticky_left_column_class
83
+ if sticky_left_column.empty?
84
+ nil
85
+ else
86
+ sticky_col_classname = "sticky-left-column sticky-columns"
87
+ sticky_left_column.each do |id|
88
+ sticky_col_classname += "-#{id}"
89
+ end
90
+
91
+ sticky_col_classname
92
+ end
93
+ end
94
+
76
95
  def striped_class
77
96
  striped ? "striped" : nil
78
97
  end
@@ -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
- placeholder={placeholder}
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
@@ -16,6 +16,7 @@ examples:
16
16
  - text_input_add_on: Add On
17
17
  - text_input_inline: Inline
18
18
  - text_input_no_label: No Label
19
+ - text_input_mask: Mask
19
20
 
20
21
  swift:
21
22
  - text_input_default_swift: Default
@@ -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
+ })