playbook_ui 16.13.0.pre.rc.2 → 16.13.0.pre.rc.3
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_form_group/_form_group.scss +24 -10
- data/app/pb_kits/playbook/pb_form_group/docs/_form_group_typeahead.html.erb +19 -14
- data/app/pb_kits/playbook/pb_form_group/docs/example.yml +2 -2
- data/app/pb_kits/playbook/pb_highlight/_highlight.tsx +31 -13
- data/app/pb_kits/playbook/pb_highlight/_highlightChunks.ts +89 -0
- data/app/pb_kits/playbook/pb_highlight/highlight.test.jsx +263 -0
- data/app/pb_kits/playbook/pb_pagination/pagination.html.erb +1 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.html.erb +1 -1
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.scss +3 -1
- data/app/pb_kits/playbook/pb_typeahead/typeahead.html.erb +1 -0
- data/app/pb_kits/playbook/pb_user_badge/user_badge.rb +5 -1
- data/dist/chunks/{_typeahead-BrQbahtd.js → _typeahead-BcKnlPk3.js} +1 -1
- data/dist/chunks/vendor.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/engine.rb +2 -0
- data/lib/playbook/version.rb +1 -1
- metadata +15 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 064a383e9d8fb6d5bb028d1343d3f2ba9dd4bf6eaed25dba6e74761d62f972ec
|
|
4
|
+
data.tar.gz: 668a7db3525d0b9f67e4b190f07dcb5108aec8d189e1980c2ea02ba23938e71f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 858dbdb201bb04117d664d5052fa441d057ab42ae7cf7ce76df084428e1b3cec8f3b8f64a55a77433a2d168614f55c4663e35b97310c736113d99b1e340f5688
|
|
7
|
+
data.tar.gz: 0bcec11c51a3da8a729a931da49a416d06d729de3ad508bfba63e6817857fe3a8b9faa84c982163bf8bd979979054d80f267ac514d72177695b873578805fd42
|
|
@@ -202,23 +202,37 @@
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
& > [class^=pb_typeahead_kit]
|
|
205
|
+
& > [class^=pb_typeahead_kit],
|
|
206
|
+
& > [data-pb-react-component] > [class^=pb_typeahead_kit] {
|
|
206
207
|
min-width: 200px;
|
|
207
208
|
}
|
|
208
209
|
|
|
209
|
-
& > [class^=pb_typeahead_kit]:not(:last-child)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
& > [class^=pb_typeahead_kit]:not(:last-child),
|
|
211
|
+
& > [data-pb-react-component]:not(:last-child) > [class^=pb_typeahead_kit] {
|
|
212
|
+
[class^=pb_text_input_kit] .text_input_wrapper {
|
|
213
|
+
input,
|
|
214
|
+
.text_input,
|
|
215
|
+
.typeahead-kit-select__control {
|
|
216
|
+
border-bottom-right-radius: 0 !important;
|
|
217
|
+
border-top-right-radius: 0 !important;
|
|
218
|
+
border-right-width: 0 !important;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
[class^=pb_text_input_kit] .text_input_wrapper div.text_input {
|
|
214
223
|
min-height: 26px;
|
|
215
224
|
}
|
|
216
225
|
}
|
|
217
226
|
|
|
218
|
-
& > [class^=pb_typeahead_kit]:not(:first-child)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
227
|
+
& > [class^=pb_typeahead_kit]:not(:first-child),
|
|
228
|
+
& > [data-pb-react-component]:not(:first-child) > [class^=pb_typeahead_kit] {
|
|
229
|
+
[class^=pb_text_input_kit] .text_input_wrapper {
|
|
230
|
+
input,
|
|
231
|
+
.text_input,
|
|
232
|
+
.typeahead-kit-select__control {
|
|
233
|
+
border-bottom-left-radius: 0 !important;
|
|
234
|
+
border-top-left-radius: 0 !important;
|
|
235
|
+
}
|
|
222
236
|
}
|
|
223
237
|
}
|
|
224
238
|
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
<div>
|
|
2
|
+
|
|
3
|
+
<%
|
|
4
|
+
options = [
|
|
5
|
+
{ label: 'Orange', value: '#FFA500' },
|
|
6
|
+
{ label: 'Red', value: '#FF0000' },
|
|
7
|
+
{ label: 'Green', value: '#00FF00' },
|
|
8
|
+
{ label: 'Blue', value: '#0000FF' },
|
|
9
|
+
]
|
|
10
|
+
%>
|
|
2
11
|
<%= pb_rails("form_group") do %>
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
id: "form-group-typeahead-example",
|
|
14
|
-
label: "Products",
|
|
15
|
-
options: options,
|
|
16
|
-
}) %>
|
|
12
|
+
|
|
13
|
+
<%= pb_rails("typeahead", props: {
|
|
14
|
+
id: "typeahead-default",
|
|
15
|
+
placeholder: "All Colors",
|
|
16
|
+
options: options,
|
|
17
|
+
label: "Colors",
|
|
18
|
+
name: :foo,
|
|
19
|
+
is_multi: false,
|
|
20
|
+
})
|
|
21
|
+
%>
|
|
17
22
|
<%= pb_rails("button", props: {
|
|
18
23
|
id: "form-group-button",
|
|
19
24
|
text: "Add",
|
|
@@ -5,7 +5,7 @@ examples:
|
|
|
5
5
|
- form_group_button: Button
|
|
6
6
|
- form_group_full_width: Full Width
|
|
7
7
|
- form_group_date_picker: Date Picker
|
|
8
|
-
|
|
8
|
+
- form_group_typeahead: Typeahead
|
|
9
9
|
- form_group_select: Select
|
|
10
10
|
- form_group_selectable_card: Selectable Card
|
|
11
11
|
- form_group_selectable_card_icon: Selectable Card Icon
|
|
@@ -16,7 +16,7 @@ examples:
|
|
|
16
16
|
- form_group_button: Button
|
|
17
17
|
- form_group_full_width: Full Width
|
|
18
18
|
- form_group_date_picker: Date Picker
|
|
19
|
-
|
|
19
|
+
- form_group_typeahead: Typeahead
|
|
20
20
|
- form_group_select: Select
|
|
21
21
|
- form_group_selectable_card: Selectable Card
|
|
22
22
|
- form_group_selectable_card_icon: Selectable Card Icon
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
/* eslint-disable react/no-multi-comp, flowtype/space-before-type-colon */
|
|
2
|
-
import Highlighter from 'react-highlight-words'
|
|
3
1
|
import React from 'react'
|
|
4
2
|
import classnames from 'classnames'
|
|
5
3
|
import { globalProps, GlobalProps } from '../utilities/globalProps'
|
|
6
|
-
import { buildHtmlProps } from '../utilities/props'
|
|
4
|
+
import { buildDataProps, buildHtmlProps } from '../utilities/props'
|
|
5
|
+
import { findHighlightChunks } from './_highlightChunks'
|
|
7
6
|
|
|
8
7
|
type HighlightProps = {
|
|
9
8
|
className?: string,
|
|
@@ -26,20 +25,39 @@ const Highlight = (props: HighlightProps): React.ReactElement => {
|
|
|
26
25
|
text = '',
|
|
27
26
|
} = props
|
|
28
27
|
|
|
28
|
+
const dataProps = buildDataProps(data)
|
|
29
29
|
const htmlProps = buildHtmlProps(htmlOptions)
|
|
30
|
-
const highlightContent:
|
|
30
|
+
const highlightContent: string = (text || children || '') as string
|
|
31
|
+
const highlightClassName = classnames(globalProps(props), className)
|
|
32
|
+
const chunks = findHighlightChunks(highlightContent, highlightedText)
|
|
31
33
|
|
|
32
34
|
return (
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
data={data}
|
|
36
|
-
highlightClassName={classnames(globalProps(props), className)}
|
|
37
|
-
highlightTag="mark"
|
|
38
|
-
id={id}
|
|
39
|
-
searchWords={highlightedText}
|
|
40
|
-
textToHighlight={highlightContent}
|
|
35
|
+
<span
|
|
36
|
+
{...dataProps}
|
|
41
37
|
{...htmlProps}
|
|
42
|
-
|
|
38
|
+
id={id}
|
|
39
|
+
>
|
|
40
|
+
{chunks.map((chunk, index) => {
|
|
41
|
+
const chunkText = highlightContent.substring(chunk.start, chunk.end)
|
|
42
|
+
|
|
43
|
+
if (chunk.highlight) {
|
|
44
|
+
return (
|
|
45
|
+
<mark
|
|
46
|
+
className={highlightClassName}
|
|
47
|
+
key={index}
|
|
48
|
+
>
|
|
49
|
+
{chunkText}
|
|
50
|
+
</mark>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<span key={index}>
|
|
56
|
+
{chunkText}
|
|
57
|
+
</span>
|
|
58
|
+
)
|
|
59
|
+
})}
|
|
60
|
+
</span>
|
|
43
61
|
)
|
|
44
62
|
}
|
|
45
63
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
type HighlightChunk = {
|
|
2
|
+
start: number,
|
|
3
|
+
end: number,
|
|
4
|
+
highlight: boolean,
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const escapeRegExp = (value: string): string =>
|
|
8
|
+
value.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Finds all case-insensitive, literal (auto-escaped) matches for the given
|
|
12
|
+
* search terms, merges overlapping/adjacent ranges, and returns a full set of
|
|
13
|
+
* highlight and non-highlight chunks covering the text.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors highlight-words-core behavior used by react-highlight-words with
|
|
16
|
+
* autoEscape enabled and caseSensitive disabled.
|
|
17
|
+
*/
|
|
18
|
+
export const findHighlightChunks = (
|
|
19
|
+
textToHighlight: string,
|
|
20
|
+
searchWords: string[],
|
|
21
|
+
): HighlightChunk[] => {
|
|
22
|
+
const text = textToHighlight || ''
|
|
23
|
+
|
|
24
|
+
const matchRanges = searchWords
|
|
25
|
+
.filter((searchWord) => searchWord)
|
|
26
|
+
.reduce<{ start: number, end: number }[]>((chunks, searchWord) => {
|
|
27
|
+
const regex = new RegExp(escapeRegExp(searchWord), 'gi')
|
|
28
|
+
let match: RegExpExecArray | null
|
|
29
|
+
|
|
30
|
+
while ((match = regex.exec(text))) {
|
|
31
|
+
const start = match.index
|
|
32
|
+
const end = regex.lastIndex
|
|
33
|
+
|
|
34
|
+
if (end > start) {
|
|
35
|
+
chunks.push({ start, end })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Prevent zero-length match infinite loops (e.g. Firefox)
|
|
39
|
+
if (match.index === regex.lastIndex) {
|
|
40
|
+
regex.lastIndex++
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return chunks
|
|
45
|
+
}, [])
|
|
46
|
+
|
|
47
|
+
const combined = matchRanges
|
|
48
|
+
.sort((first, second) => first.start - second.start)
|
|
49
|
+
.reduce<{ start: number, end: number }[]>((processedChunks, nextChunk) => {
|
|
50
|
+
if (processedChunks.length === 0) {
|
|
51
|
+
return [nextChunk]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const prevChunk = processedChunks.pop() as { start: number, end: number }
|
|
55
|
+
|
|
56
|
+
// Merge overlapping or adjacent ranges (start <= prev end)
|
|
57
|
+
if (nextChunk.start <= prevChunk.end) {
|
|
58
|
+
processedChunks.push({
|
|
59
|
+
start: prevChunk.start,
|
|
60
|
+
end: Math.max(prevChunk.end, nextChunk.end),
|
|
61
|
+
})
|
|
62
|
+
} else {
|
|
63
|
+
processedChunks.push(prevChunk, nextChunk)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return processedChunks
|
|
67
|
+
}, [])
|
|
68
|
+
|
|
69
|
+
const allChunks: HighlightChunk[] = []
|
|
70
|
+
const append = (start: number, end: number, highlight: boolean) => {
|
|
71
|
+
if (end - start > 0) {
|
|
72
|
+
allChunks.push({ start, end, highlight })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (combined.length === 0) {
|
|
77
|
+
append(0, text.length, false)
|
|
78
|
+
} else {
|
|
79
|
+
let lastIndex = 0
|
|
80
|
+
combined.forEach((chunk) => {
|
|
81
|
+
append(lastIndex, chunk.start, false)
|
|
82
|
+
append(chunk.start, chunk.end, true)
|
|
83
|
+
lastIndex = chunk.end
|
|
84
|
+
})
|
|
85
|
+
append(lastIndex, text.length, false)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return allChunks
|
|
89
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, screen } from '../utilities/test-utils'
|
|
3
|
+
|
|
4
|
+
import Highlight from './_highlight'
|
|
5
|
+
import Body from '../pb_body/_body'
|
|
6
|
+
import { findHighlightChunks } from './_highlightChunks'
|
|
7
|
+
|
|
8
|
+
describe('findHighlightChunks', () => {
|
|
9
|
+
test('returns a single non-highlight chunk when there are no matches', () => {
|
|
10
|
+
expect(findHighlightChunks('hello world', ['missing'])).toEqual([
|
|
11
|
+
{ start: 0, end: 11, highlight: false },
|
|
12
|
+
])
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('finds a single search term case-insensitively', () => {
|
|
16
|
+
expect(findHighlightChunks('Hello World', ['world'])).toEqual([
|
|
17
|
+
{ start: 0, end: 6, highlight: false },
|
|
18
|
+
{ start: 6, end: 11, highlight: true },
|
|
19
|
+
])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('finds multiple matches of the same term', () => {
|
|
23
|
+
expect(findHighlightChunks('foo bar foo', ['foo'])).toEqual([
|
|
24
|
+
{ start: 0, end: 3, highlight: true },
|
|
25
|
+
{ start: 3, end: 8, highlight: false },
|
|
26
|
+
{ start: 8, end: 11, highlight: true },
|
|
27
|
+
])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('finds multiple search terms', () => {
|
|
31
|
+
expect(findHighlightChunks('red and blue', ['red', 'blue'])).toEqual([
|
|
32
|
+
{ start: 0, end: 3, highlight: true },
|
|
33
|
+
{ start: 3, end: 8, highlight: false },
|
|
34
|
+
{ start: 8, end: 12, highlight: true },
|
|
35
|
+
])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('merges overlapping matches into one highlight chunk', () => {
|
|
39
|
+
expect(findHighlightChunks('highlight', ['high', 'highlight'])).toEqual([
|
|
40
|
+
{ start: 0, end: 9, highlight: true },
|
|
41
|
+
])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('merges adjacent matches into one highlight chunk', () => {
|
|
45
|
+
expect(findHighlightChunks('foobar', ['foo', 'bar'])).toEqual([
|
|
46
|
+
{ start: 0, end: 6, highlight: true },
|
|
47
|
+
])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('matches regex special characters literally', () => {
|
|
51
|
+
expect(findHighlightChunks('cost is $5.00 (approx)', ['$5.00', '(approx)'])).toEqual([
|
|
52
|
+
{ start: 0, end: 8, highlight: false },
|
|
53
|
+
{ start: 8, end: 13, highlight: true },
|
|
54
|
+
{ start: 13, end: 14, highlight: false },
|
|
55
|
+
{ start: 14, end: 22, highlight: true },
|
|
56
|
+
])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('ignores empty search terms', () => {
|
|
60
|
+
expect(findHighlightChunks('hello', ['', 'hello'])).toEqual([
|
|
61
|
+
{ start: 0, end: 5, highlight: true },
|
|
62
|
+
])
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('Highlight', () => {
|
|
67
|
+
test('highlights a single search term from the text prop', () => {
|
|
68
|
+
render(
|
|
69
|
+
<Highlight
|
|
70
|
+
data={{ testid: 'highlight-single' }}
|
|
71
|
+
highlightedText={['Highlight']}
|
|
72
|
+
text="This is the Highlight Kit."
|
|
73
|
+
/>
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const kit = screen.getByTestId('highlight-single')
|
|
77
|
+
const marks = kit.querySelectorAll('mark')
|
|
78
|
+
|
|
79
|
+
expect(marks).toHaveLength(1)
|
|
80
|
+
expect(marks[0]).toHaveTextContent('Highlight')
|
|
81
|
+
expect(marks[0]).toHaveClass('pb_highlight_kit')
|
|
82
|
+
expect(kit).toHaveTextContent('This is the Highlight Kit.')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('highlights content passed via children', () => {
|
|
86
|
+
render(
|
|
87
|
+
<Highlight
|
|
88
|
+
data={{ testid: 'highlight-children' }}
|
|
89
|
+
highlightedText={['children']}
|
|
90
|
+
>
|
|
91
|
+
Content via children works.
|
|
92
|
+
</Highlight>
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const kit = screen.getByTestId('highlight-children')
|
|
96
|
+
const marks = kit.querySelectorAll('mark')
|
|
97
|
+
|
|
98
|
+
expect(marks).toHaveLength(1)
|
|
99
|
+
expect(marks[0]).toHaveTextContent('children')
|
|
100
|
+
expect(kit).toHaveTextContent('Content via children works.')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('highlights multiple search terms and multiple matches', () => {
|
|
104
|
+
render(
|
|
105
|
+
<Highlight
|
|
106
|
+
data={{ testid: 'highlight-multi' }}
|
|
107
|
+
highlightedText={['cat', 'dog']}
|
|
108
|
+
text="cat and dog and cat"
|
|
109
|
+
/>
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const kit = screen.getByTestId('highlight-multi')
|
|
113
|
+
const marks = Array.from(kit.querySelectorAll('mark')).map((mark) => mark.textContent)
|
|
114
|
+
|
|
115
|
+
expect(marks).toEqual(['cat', 'dog', 'cat'])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('merges overlapping or adjacent matches into one mark', () => {
|
|
119
|
+
render(
|
|
120
|
+
<Highlight
|
|
121
|
+
data={{ testid: 'highlight-merge' }}
|
|
122
|
+
highlightedText={['foo', 'bar']}
|
|
123
|
+
text="foobar"
|
|
124
|
+
/>
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const kit = screen.getByTestId('highlight-merge')
|
|
128
|
+
const marks = kit.querySelectorAll('mark')
|
|
129
|
+
|
|
130
|
+
expect(marks).toHaveLength(1)
|
|
131
|
+
expect(marks[0]).toHaveTextContent('foobar')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('renders without marks when there are no matches', () => {
|
|
135
|
+
render(
|
|
136
|
+
<Highlight
|
|
137
|
+
data={{ testid: 'highlight-none' }}
|
|
138
|
+
highlightedText={['missing']}
|
|
139
|
+
text="nothing to see"
|
|
140
|
+
/>
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const kit = screen.getByTestId('highlight-none')
|
|
144
|
+
|
|
145
|
+
expect(kit.querySelectorAll('mark')).toHaveLength(0)
|
|
146
|
+
expect(kit).toHaveTextContent('nothing to see')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('matches case-insensitively', () => {
|
|
150
|
+
render(
|
|
151
|
+
<Highlight
|
|
152
|
+
data={{ testid: 'highlight-case' }}
|
|
153
|
+
highlightedText={['highlight']}
|
|
154
|
+
text="HiGhLiGhT text"
|
|
155
|
+
/>
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
const kit = screen.getByTestId('highlight-case')
|
|
159
|
+
const marks = kit.querySelectorAll('mark')
|
|
160
|
+
|
|
161
|
+
expect(marks).toHaveLength(1)
|
|
162
|
+
expect(marks[0]).toHaveTextContent('HiGhLiGhT')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('matches regex special characters literally', () => {
|
|
166
|
+
render(
|
|
167
|
+
<Highlight
|
|
168
|
+
data={{ testid: 'highlight-regex' }}
|
|
169
|
+
highlightedText={['a+b', '(x)', 'file.js']}
|
|
170
|
+
text="use a+b or (x) in file.js"
|
|
171
|
+
/>
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
const kit = screen.getByTestId('highlight-regex')
|
|
175
|
+
const marks = Array.from(kit.querySelectorAll('mark')).map((mark) => mark.textContent)
|
|
176
|
+
|
|
177
|
+
expect(marks).toEqual(['a+b', '(x)', 'file.js'])
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('passes id and htmlOptions through to the wrapper', () => {
|
|
181
|
+
render(
|
|
182
|
+
<Highlight
|
|
183
|
+
data={{ testid: 'highlight-attrs' }}
|
|
184
|
+
htmlOptions={{ 'aria-label': 'highlighted content' }}
|
|
185
|
+
id="highlight-id"
|
|
186
|
+
text="highlight me"
|
|
187
|
+
/>
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const kit = screen.getByTestId('highlight-attrs')
|
|
191
|
+
|
|
192
|
+
expect(kit).toHaveAttribute('id', 'highlight-id')
|
|
193
|
+
expect(kit).toHaveAttribute('aria-label', 'highlighted content')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('uses the default highlightedText of highlight when omitted', () => {
|
|
197
|
+
render(
|
|
198
|
+
<Highlight
|
|
199
|
+
data={{ testid: 'highlight-default' }}
|
|
200
|
+
text="default highlight behavior"
|
|
201
|
+
/>
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
const kit = screen.getByTestId('highlight-default')
|
|
205
|
+
const marks = kit.querySelectorAll('mark')
|
|
206
|
+
|
|
207
|
+
expect(marks).toHaveLength(1)
|
|
208
|
+
expect(marks[0]).toHaveTextContent('highlight')
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe('Body with highlighting', () => {
|
|
213
|
+
test('renders Highlight for matching text via text prop', () => {
|
|
214
|
+
render(
|
|
215
|
+
<Body
|
|
216
|
+
data={{ testid: 'body-highlight' }}
|
|
217
|
+
highlightedText={['highlighted', 'Body Kit']}
|
|
218
|
+
highlighting
|
|
219
|
+
text="This is text highlighted in the Body Kit."
|
|
220
|
+
/>
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
const kit = screen.getByTestId('body-highlight')
|
|
224
|
+
const marks = Array.from(kit.querySelectorAll('mark')).map((mark) => mark.textContent)
|
|
225
|
+
|
|
226
|
+
expect(kit).toHaveClass('pb_body_kit')
|
|
227
|
+
expect(marks).toEqual(['highlighted', 'Body Kit'])
|
|
228
|
+
expect(kit).toHaveTextContent('This is text highlighted in the Body Kit.')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('renders Highlight for matching text via children', () => {
|
|
232
|
+
render(
|
|
233
|
+
<Body
|
|
234
|
+
data={{ testid: 'body-highlight-children' }}
|
|
235
|
+
highlightedText={['children']}
|
|
236
|
+
highlighting
|
|
237
|
+
>
|
|
238
|
+
Body with children highlight
|
|
239
|
+
</Body>
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
const kit = screen.getByTestId('body-highlight-children')
|
|
243
|
+
const marks = kit.querySelectorAll('mark')
|
|
244
|
+
|
|
245
|
+
expect(marks).toHaveLength(1)
|
|
246
|
+
expect(marks[0]).toHaveTextContent('children')
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('does not highlight when highlighting is false', () => {
|
|
250
|
+
render(
|
|
251
|
+
<Body
|
|
252
|
+
data={{ testid: 'body-no-highlight' }}
|
|
253
|
+
highlightedText={['highlighted']}
|
|
254
|
+
text="This is text highlighted in the Body Kit."
|
|
255
|
+
/>
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
const kit = screen.getByTestId('body-no-highlight')
|
|
259
|
+
|
|
260
|
+
expect(kit.querySelectorAll('mark')).toHaveLength(0)
|
|
261
|
+
expect(kit).toHaveTextContent('This is text highlighted in the Body Kit.')
|
|
262
|
+
})
|
|
263
|
+
})
|
|
@@ -158,5 +158,5 @@
|
|
|
158
158
|
|
|
159
159
|
<%# Module script: rich_text_editor_rails.js %>
|
|
160
160
|
<script type="module">
|
|
161
|
-
<%=
|
|
161
|
+
<%= File.read(Playbook.kit_path("rich_text_editor", "rich_text_editor_rails.js")).html_safe %>
|
|
162
162
|
</script>
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
color: $input_text_active;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
// React select control only — Rails <input class="text_input"> must keep max-height: 45px
|
|
48
|
+
div.text_input {
|
|
48
49
|
max-height: none;
|
|
49
50
|
}
|
|
50
51
|
& > input:first-child {
|
|
@@ -142,6 +143,7 @@
|
|
|
142
143
|
&__control {
|
|
143
144
|
&--is-disabled {
|
|
144
145
|
background-color: $input_background_disabled;
|
|
146
|
+
border-color: $input_border_disabled;
|
|
145
147
|
}
|
|
146
148
|
&--is-focused {
|
|
147
149
|
@include pb_textarea_focus;
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
required_indicator: object.required_indicator,
|
|
25
25
|
id: object.input_options[:id],
|
|
26
26
|
dark: object.dark,
|
|
27
|
+
disabled: object.disabled,
|
|
27
28
|
}) %>
|
|
28
29
|
<%= pb_rails("list", props: { ordered: false, borderless: true, xpadding: true, role: "status", aria: { live: "polite" }, data: { pb_typeahead_kit_results: true } }) do %>
|
|
29
30
|
<% end %>
|
|
@@ -16,7 +16,11 @@ module Playbook
|
|
|
16
16
|
|
|
17
17
|
def display_badge
|
|
18
18
|
@display_badge ||= begin
|
|
19
|
-
badge_path =
|
|
19
|
+
badge_path = File.join(
|
|
20
|
+
File.dirname(self.class.source_location),
|
|
21
|
+
"badges",
|
|
22
|
+
"#{badge}.svg"
|
|
23
|
+
)
|
|
20
24
|
File.read(badge_path).html_safe
|
|
21
25
|
end
|
|
22
26
|
end
|