playbook_ui 14.9.0.pre.alpha.PBNTR702stickyleftcolrails4806 → 14.9.0.pre.alpha.PLAY1731inputmasking4866

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/table_header.html.erb +5 -3
  3. data/app/pb_kits/playbook/pb_avatar/_avatar.scss +0 -2
  4. data/app/pb_kits/playbook/pb_body/_body.scss +14 -13
  5. data/app/pb_kits/playbook/pb_body/_body_mixins.scss +22 -16
  6. data/app/pb_kits/playbook/pb_bread_crumbs/docs/_bread_crumbs_default.jsx +6 -0
  7. data/app/pb_kits/playbook/pb_caption/_caption_mixin.scss +2 -1
  8. data/app/pb_kits/playbook/pb_card/_card_mixin.scss +1 -1
  9. data/app/pb_kits/playbook/pb_file_upload/_file_upload.scss +5 -0
  10. data/app/pb_kits/playbook/pb_file_upload/_file_upload.tsx +7 -2
  11. data/app/pb_kits/playbook/pb_file_upload/file_upload.html.erb +8 -2
  12. data/app/pb_kits/playbook/pb_nav/_bold_mixin.scss +11 -1
  13. data/app/pb_kits/playbook/pb_nav/_collapsible_nav.scss +16 -2
  14. data/app/pb_kits/playbook/pb_nav/_vertical_nav.scss +1 -1
  15. data/app/pb_kits/playbook/pb_selectable_card/_selectable_card.scss +4 -4
  16. data/app/pb_kits/playbook/pb_selectable_card/selectable_card.html.erb +1 -1
  17. data/app/pb_kits/playbook/pb_stat_change/_stat_change.tsx +44 -36
  18. data/app/pb_kits/playbook/pb_stat_change/stat_change.html.erb +4 -4
  19. data/app/pb_kits/playbook/pb_table/index.ts +26 -100
  20. data/app/pb_kits/playbook/pb_table/table.html.erb +1 -1
  21. data/app/pb_kits/playbook/pb_table/table.rb +2 -17
  22. data/app/pb_kits/playbook/pb_text_input/_text_input.tsx +35 -3
  23. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_mask.jsx +88 -0
  24. data/app/pb_kits/playbook/pb_text_input/docs/example.yml +1 -0
  25. data/app/pb_kits/playbook/pb_text_input/docs/index.js +1 -0
  26. data/app/pb_kits/playbook/pb_text_input/inputMask.ts +64 -0
  27. data/app/pb_kits/playbook/pb_text_input/text_input.test.js +139 -2
  28. data/app/pb_kits/playbook/pb_title/_title.scss +6 -5
  29. data/app/pb_kits/playbook/pb_title/_title_mixin.scss +13 -0
  30. data/app/pb_kits/playbook/tokens/_titles.scss +0 -8
  31. data/app/pb_kits/playbook/utilities/_hover.scss +11 -2
  32. data/app/pb_kits/playbook/utilities/globalProps.ts +2 -0
  33. data/app/pb_kits/playbook/utilities/test/globalProps/hover.test.js +15 -0
  34. data/dist/chunks/_typeahead-Cx7J1O_I.js +22 -0
  35. data/dist/chunks/_weekday_stacked-Z6CSak-K.js +45 -0
  36. data/dist/chunks/{lib-CVPInSs5.js → lib-SyD3buPZ.js} +1 -1
  37. data/dist/chunks/{pb_form_validation-CDLJ5eAG.js → pb_form_validation-Dt8UJgrJ.js} +1 -1
  38. data/dist/chunks/vendor.js +1 -1
  39. data/dist/playbook-doc.js +1 -1
  40. data/dist/playbook-rails-react-bindings.js +1 -1
  41. data/dist/playbook-rails.js +1 -1
  42. data/dist/playbook.css +1 -1
  43. data/lib/playbook/hover.rb +7 -1
  44. data/lib/playbook/version.rb +1 -1
  45. metadata +8 -7
  46. data/app/pb_kits/playbook/pb_table/docs/_table_sticky_left_columns.html.erb +0 -95
  47. data/dist/chunks/_typeahead-CCDoUmRR.js +0 -22
  48. data/dist/chunks/_weekday_stacked-CxjKLoMr.js +0 -45
@@ -1,106 +1,32 @@
1
1
  import PbEnhancedElement from '../pb_enhanced_element'
2
2
 
3
3
  export default class PbTable extends PbEnhancedElement {
4
- private stickyLeftColumns: string[] = [];
5
- private handleStickyColumnsRef: () => void;
6
-
7
- static get selector(): string {
8
- return '.table-responsive-collapse'
9
- }
10
-
11
- connect(): void {
12
- const tables = document.querySelectorAll('.table-responsive-collapse');
13
- // Each Table
14
- [].forEach.call(tables, (table: HTMLTableElement) => {
15
- // Header Titles
16
- const headers: string[] = [];
17
- [].forEach.call(table.querySelectorAll('th'), (header: HTMLTableCellElement) => {
18
- const colSpan = header.colSpan
19
- for (let i = 0; i < colSpan; i++) {
20
- headers.push(header.textContent.replace(/\r?\n|\r/, ''));
21
- }
22
- });
23
- // for each row in tbody
24
- [].forEach.call(table.querySelectorAll('tbody tr'), (row: HTMLTableRowElement) => {
25
- // for each cell
26
- [].forEach.call(row.cells, (cell: HTMLTableCellElement, headerIndex: number) => {
27
- // apply the attribute
28
- cell.setAttribute('data-title', headers[headerIndex])
29
- })
30
- })
31
- });
32
-
33
- // New sticky columns logic
34
- this.initStickyColumns();
35
- }
36
-
37
- private initStickyColumns(): void {
38
- // Find tables with sticky-left-column class
39
- const tables = document.querySelectorAll('.sticky-left-column');
40
-
41
- tables.forEach((table) => {
42
- // Extract sticky left column IDs by looking at the component's class
43
- const classList = Array.from(table.classList);
44
-
45
- // Look for classes in the format sticky-left-column-{ids}
46
- const stickyColumnClass = classList.find(cls => cls.startsWith('sticky-columns-'));
47
- if (stickyColumnClass) {
48
- // Extract the IDs from the class name
49
- this.stickyLeftColumns = stickyColumnClass
50
- .replace('sticky-columns-', '')
51
- .split('-');
52
-
53
- if (this.stickyLeftColumns.length > 0) {
54
- this.handleStickyColumnsRef = this.handleStickyColumns.bind(this);
55
- this.handleStickyColumns();
56
- window.addEventListener('resize', this.handleStickyColumnsRef);
57
- }
4
+ static get selector(): string {
5
+ return '.table-responsive-collapse'
6
+ }
7
+
8
+ connect(): void {
9
+ const tables = document.querySelectorAll('.table-responsive-collapse');
10
+
11
+ // Each Table
12
+ [].forEach.call(tables, (table: HTMLTableElement) => {
13
+ // Header Titles
14
+ const headers: string[] = [];
15
+ [].forEach.call(table.querySelectorAll('th'), (header: HTMLTableCellElement) => {
16
+ const colSpan = header.colSpan
17
+ for (let i = 0; i < colSpan; i++) {
18
+ headers.push(header.textContent.replace(/\r?\n|\r/, ''));
58
19
  }
59
20
  });
60
- }
61
-
62
- private handleStickyColumns(): void {
63
- let accumulatedWidth = 0;
64
21
 
65
- this.stickyLeftColumns.forEach((colId, index) => {
66
- const isLastColumn = index === this.stickyLeftColumns.length - 1;
67
- const header = document.querySelector(`th[id="${colId}"]`);
68
- const cells = document.querySelectorAll(`td[id="${colId}"]`);
69
-
70
- if (header) {
71
- header.classList.add('sticky');
72
- (header as HTMLElement).style.left = `${accumulatedWidth}px`;
73
-
74
- if (!isLastColumn) {
75
- header.classList.add('with-border');
76
- header.classList.remove('sticky-shadow');
77
- } else {
78
- header.classList.remove('with-border');
79
- header.classList.add('sticky-shadow');
80
- }
81
-
82
- accumulatedWidth += (header as HTMLElement).offsetWidth;
83
- }
84
-
85
- cells.forEach((cell) => {
86
- cell.classList.add('sticky');
87
- (cell as HTMLElement).style.left = `${accumulatedWidth - (header as HTMLElement).offsetWidth}px`;
88
-
89
- if (!isLastColumn) {
90
- cell.classList.add('with-border');
91
- cell.classList.remove('sticky-shadow');
92
- } else {
93
- cell.classList.remove('with-border');
94
- cell.classList.add('sticky-shadow');
95
- }
96
- });
97
- });
98
- }
99
-
100
- // Cleanup method to remove event listener
101
- disconnect(): void {
102
- if (this.handleStickyColumnsRef) {
103
- window.removeEventListener('resize', this.handleStickyColumnsRef);
104
- }
105
- }
106
- }
22
+ // for each row in tbody
23
+ [].forEach.call(table.querySelectorAll('tbody tr'), (row: HTMLTableRowElement) => {
24
+ // for each cell
25
+ [].forEach.call(row.cells, (cell: HTMLTableCellElement, headerIndex: number) => {
26
+ // apply the attribute
27
+ cell.setAttribute('data-title', headers[headerIndex])
28
+ })
29
+ })
30
+ })
31
+ }
32
+ }
@@ -18,4 +18,4 @@
18
18
  <%= content.presence %>
19
19
  <% end %>
20
20
  <% end %>
21
- <% end %>
21
+ <% end %>
@@ -23,8 +23,6 @@ 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: []
28
26
  prop :vertical_border, type: Playbook::Props::Boolean,
29
27
  default: false
30
28
  prop :striped, type: Playbook::Props::Boolean,
@@ -39,8 +37,8 @@ module Playbook
39
37
  def classname
40
38
  generate_classname(
41
39
  "pb_table", "table-#{size}", single_line_class, dark_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,
40
+ disable_hover_class, container_class, data_table_class, sticky_class, collapse_class,
41
+ vertical_border_class, striped_class, outer_padding_class,
44
42
  "table-responsive-#{responsive}", separator: " "
45
43
  )
46
44
  end
@@ -75,19 +73,6 @@ module Playbook
75
73
  sticky ? "sticky-header" : nil
76
74
  end
77
75
 
78
- def sticky_left_column_class
79
- if sticky_left_column.empty?
80
- nil
81
- else
82
- sticky_col_classname = "sticky-left-column sticky-columns"
83
- sticky_left_column.each do |id|
84
- sticky_col_classname += "-#{id}"
85
- end
86
-
87
- sticky_col_classname
88
- end
89
- end
90
-
91
76
  def striped_class
92
77
  striped ? "striped" : nil
93
78
  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
+ })
@@ -49,10 +49,11 @@
49
49
  }
50
50
 
51
51
  &.dark {
52
- @include pb_title_dark;
53
- }
54
-
55
- &.dark[class*=_link] {
56
- @include pb_title_dark_link;
52
+ @include title_dark;
53
+ @each $name, $color in $pb_dark_title_colors {
54
+ &[class*="_#{$name}"] {
55
+ color: $color;
56
+ }
57
+ }
57
58
  }
58
59
  }
@@ -9,6 +9,15 @@ $pb_title_colors: (
9
9
  link: $primary
10
10
  );
11
11
 
12
+ $pb_dark_title_colors: (
13
+ default: $text_dk_default,
14
+ light: $text_dk_light,
15
+ lighter: $text_dk_lighter,
16
+ success: $success,
17
+ error: $error_dark,
18
+ link: $active_dark
19
+ );
20
+
12
21
  @mixin title_colors {
13
22
  @each $name, $color in $pb_title_colors {
14
23
  &[class*=_#{$name}] {
@@ -16,3 +25,7 @@ $pb_title_colors: (
16
25
  }
17
26
  }
18
27
  }
28
+
29
+ @mixin title_dark {
30
+ color: $text_dk_default;
31
+ }
@@ -33,14 +33,6 @@
33
33
  @include pb_title($heading_4, $bolder, $letterSpacing: $lspace_normal);
34
34
  }
35
35
 
36
- @mixin pb_title_dark {
37
- color: $text_dk_default;
38
- }
39
-
40
- @mixin pb_title_dark_link {
41
- color: $active_dark;
42
- }
43
-
44
36
  @mixin pb_title_bold {
45
37
  font-weight: $bolder;
46
38
  }
@@ -20,6 +20,13 @@
20
20
  }
21
21
  }
22
22
 
23
+ @mixin hover-underline {
24
+ .hover_underline:hover {
25
+ text-decoration: underline;
26
+ transition: text-decoration $transition-speed ease;
27
+ }
28
+ }
29
+
23
30
  @mixin hover-color-classes($colors-list) {
24
31
  @each $name, $color in $colors-list {
25
32
  .hover_background-#{"" + $name}:hover {
@@ -32,7 +39,9 @@
32
39
  }
33
40
  }
34
41
  }
35
-
42
+
43
+
44
+ @include hover-underline;
36
45
  @include hover-scale-classes($scales);
37
46
  @include hover-shadow-classes($box_shadows);
38
47
  @include hover-color-classes($product_colors);
@@ -64,4 +73,4 @@
64
73
  .group_hover.hover_visibility {
65
74
  opacity: 1;
66
75
  }
67
- }
76
+ }
@@ -64,6 +64,7 @@ type Hover = Shadow & {
64
64
  background?: string,
65
65
  color?: string,
66
66
  scale?: "sm" | "md" | "lg",
67
+ underline?: boolean,
67
68
  visibility?: boolean,
68
69
  }
69
70
 
@@ -236,6 +237,7 @@ const PROP_CATEGORIES: {[key:string]: (props: {[key: string]: any}) => string} =
236
237
  if (!hover) return css;
237
238
  css += hover.shadow ? `hover_shadow_${hover.shadow} ` : '';
238
239
  css += hover.background ? `hover_background-${hover.background } ` : '';
240
+ css += hover.underline ? `hover_underline ` : '';
239
241
  css += hover.scale ? `hover_scale_${hover.scale} ` : '';
240
242
  css += hover.color ? `hover_color-${hover.color } ` : '';
241
243
  css += hover.visibility ? `hover_visibility` : '';