primer_view_components 0.0.67 → 0.0.70
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/CHANGELOG.md +133 -2
- data/README.md +1 -1
- data/app/assets/javascripts/primer_view_components.js +1 -1
- data/app/assets/javascripts/primer_view_components.js.map +1 -1
- data/app/components/primer/alpha/tooltip.d.ts +24 -0
- data/app/components/primer/alpha/tooltip.js +381 -0
- data/app/components/primer/alpha/tooltip.rb +103 -0
- data/app/components/primer/alpha/tooltip.ts +383 -0
- data/app/components/primer/base_component.rb +2 -2
- data/app/components/primer/beta/auto_complete/auto_complete.html.erb +22 -4
- data/app/components/primer/beta/auto_complete.rb +86 -99
- data/app/components/primer/beta/blankslate.html.erb +6 -2
- data/app/components/primer/beta/blankslate.rb +4 -9
- data/app/components/primer/beta/truncate.rb +1 -0
- data/app/components/primer/button_component.html.erb +1 -0
- data/app/components/primer/button_component.rb +29 -0
- data/app/components/primer/component.rb +9 -2
- data/app/components/primer/details_component.rb +1 -1
- data/app/components/primer/icon_button.rb +1 -1
- data/app/components/primer/link_component.erb +4 -0
- data/app/components/primer/link_component.rb +29 -4
- data/app/components/primer/markdown.rb +1 -1
- data/app/components/primer/popover_component.rb +5 -9
- data/app/components/primer/primer.d.ts +1 -0
- data/app/components/primer/primer.js +1 -0
- data/app/components/primer/primer.ts +1 -0
- data/app/components/primer/subhead_component.html.erb +1 -1
- data/app/components/primer/subhead_component.rb +1 -1
- data/app/components/primer/tooltip.rb +1 -1
- data/app/lib/primer/test_selector_helper.rb +1 -1
- data/lib/primer/classify/utilities.yml +28 -0
- data/lib/primer/view_components/linters/button_component_migration_counter.rb +1 -1
- data/lib/primer/view_components/version.rb +1 -1
- data/lib/rubocop/cop/primer/component_name_migration.rb +35 -0
- data/lib/rubocop/cop/primer/primer_octicon.rb +1 -1
- data/lib/tasks/docs.rake +12 -7
- data/lib/tasks/utilities.rake +1 -1
- data/static/arguments.yml +52 -1
- data/static/audited_at.json +1 -1
- data/static/classes.yml +9 -4
- data/static/constants.json +18 -8
- data/static/statuses.json +2 -2
- metadata +13 -9
- data/app/components/primer/auto_complete/auto_complete.d.ts +0 -1
- data/app/components/primer/auto_complete/auto_complete.js +0 -1
@@ -0,0 +1,383 @@
|
|
1
|
+
import type {AnchorAlignment, AnchorSide} from '@primer/behaviors'
|
2
|
+
import {getAnchoredPosition} from '@primer/behaviors'
|
3
|
+
|
4
|
+
const TOOLTIP_OPEN_CLASS = 'tooltip-open'
|
5
|
+
const TOOLTIP_ARROW_EDGE_OFFSET = 10
|
6
|
+
|
7
|
+
type Direction = 'n' | 's' | 'e' | 'w' | 'ne' | 'se' | 'nw' | 'sw'
|
8
|
+
|
9
|
+
const DIRECTION_CLASSES = [
|
10
|
+
'tooltip-n',
|
11
|
+
'tooltip-s',
|
12
|
+
'tooltip-e',
|
13
|
+
'tooltip-w',
|
14
|
+
'tooltip-ne',
|
15
|
+
'tooltip-se',
|
16
|
+
'tooltip-nw',
|
17
|
+
'tooltip-sw'
|
18
|
+
]
|
19
|
+
|
20
|
+
class TooltipElement extends HTMLElement {
|
21
|
+
styles() {
|
22
|
+
return `
|
23
|
+
:host {
|
24
|
+
position: absolute;
|
25
|
+
z-index: 1000000;
|
26
|
+
padding: .5em .75em;
|
27
|
+
font: normal normal 11px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
28
|
+
-webkit-font-smoothing: subpixel-antialiased;
|
29
|
+
color: var(--color-fg-on-emphasis);
|
30
|
+
text-align: center;
|
31
|
+
text-decoration: none;
|
32
|
+
text-shadow: none;
|
33
|
+
text-transform: none;
|
34
|
+
letter-spacing: normal;
|
35
|
+
word-wrap: break-word;
|
36
|
+
white-space: pre;
|
37
|
+
background: var(--color-neutral-emphasis-plus);
|
38
|
+
border-radius: 6px;
|
39
|
+
opacity: 0;
|
40
|
+
max-width: 250px;
|
41
|
+
word-wrap: break-word;
|
42
|
+
white-space: normal;
|
43
|
+
width: max-content;
|
44
|
+
}
|
45
|
+
|
46
|
+
:host:before{
|
47
|
+
position: absolute;
|
48
|
+
z-index: 1000001;
|
49
|
+
color: var(--color-neutral-emphasis-plus);
|
50
|
+
content: "";
|
51
|
+
border: 6px solid transparent;
|
52
|
+
opacity: 0
|
53
|
+
}
|
54
|
+
|
55
|
+
@keyframes tooltip-appear {
|
56
|
+
from {
|
57
|
+
opacity: 0
|
58
|
+
}
|
59
|
+
to {
|
60
|
+
opacity: 1
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
:host:after{
|
65
|
+
position: absolute;
|
66
|
+
display: block;
|
67
|
+
right: 0;
|
68
|
+
left: 0;
|
69
|
+
height: 12px;
|
70
|
+
content: ""
|
71
|
+
}
|
72
|
+
|
73
|
+
:host(.${TOOLTIP_OPEN_CLASS}),
|
74
|
+
:host(.${TOOLTIP_OPEN_CLASS}):before {
|
75
|
+
animation-name: tooltip-appear;
|
76
|
+
animation-duration: .1s;
|
77
|
+
animation-fill-mode: forwards;
|
78
|
+
animation-timing-function: ease-in;
|
79
|
+
animation-delay: .4s
|
80
|
+
}
|
81
|
+
|
82
|
+
:host(.tooltip-s):before,
|
83
|
+
:host(.tooltip-n):before {
|
84
|
+
right: 50%;
|
85
|
+
}
|
86
|
+
|
87
|
+
:host(.tooltip-s):before,
|
88
|
+
:host(.tooltip-se):before,
|
89
|
+
:host(.tooltip-sw):before {
|
90
|
+
bottom: 100%;
|
91
|
+
margin-right: -${TOOLTIP_ARROW_EDGE_OFFSET}px;
|
92
|
+
border-bottom-color: var(--color-neutral-emphasis-plus)
|
93
|
+
}
|
94
|
+
|
95
|
+
:host(.tooltip-s):after,
|
96
|
+
:host(.tooltip-se):after,
|
97
|
+
:host(.tooltip-sw):after {
|
98
|
+
bottom: 100%
|
99
|
+
}
|
100
|
+
|
101
|
+
:host(.tooltip-n):before,
|
102
|
+
:host(.tooltip-ne):before,
|
103
|
+
:host(.tooltip-nw):before {
|
104
|
+
top: 100%;
|
105
|
+
margin-right: -${TOOLTIP_ARROW_EDGE_OFFSET}px;
|
106
|
+
border-top-color: var(--color-neutral-emphasis-plus)
|
107
|
+
}
|
108
|
+
|
109
|
+
:host(.tooltip-n):after,
|
110
|
+
:host(.tooltip-ne):after,
|
111
|
+
:host(.tooltip-nw):after {
|
112
|
+
top: 100%
|
113
|
+
}
|
114
|
+
|
115
|
+
:host(.tooltip-se):before,
|
116
|
+
:host(.tooltip-ne):before {
|
117
|
+
left: 0;
|
118
|
+
margin-left: ${TOOLTIP_ARROW_EDGE_OFFSET}px;
|
119
|
+
}
|
120
|
+
|
121
|
+
:host(.tooltip-sw):before,
|
122
|
+
:host(.tooltip-nw):before {
|
123
|
+
right: 0;
|
124
|
+
margin-right: ${TOOLTIP_ARROW_EDGE_OFFSET}px;
|
125
|
+
}
|
126
|
+
|
127
|
+
:host(.tooltip-w):before {
|
128
|
+
top: 50%;
|
129
|
+
bottom: 50%;
|
130
|
+
left: 100%;
|
131
|
+
margin-top: -6px;
|
132
|
+
border-left-color: var(--color-neutral-emphasis-plus)
|
133
|
+
}
|
134
|
+
|
135
|
+
:host(.tooltip-e):before {
|
136
|
+
top: 50%;
|
137
|
+
right: 100%;
|
138
|
+
bottom: 50%;
|
139
|
+
margin-top: -6px;
|
140
|
+
border-right-color: var(--color-neutral-emphasis-plus)
|
141
|
+
}
|
142
|
+
`
|
143
|
+
}
|
144
|
+
|
145
|
+
#abortController: AbortController | undefined
|
146
|
+
#align: AnchorAlignment = 'center'
|
147
|
+
#side: AnchorSide = 'outside-bottom'
|
148
|
+
#allowUpdatePosition = false
|
149
|
+
|
150
|
+
get htmlFor(): string {
|
151
|
+
return this.getAttribute('for') || ''
|
152
|
+
}
|
153
|
+
|
154
|
+
set htmlFor(value: string) {
|
155
|
+
this.setAttribute('for', value)
|
156
|
+
}
|
157
|
+
|
158
|
+
get type(): 'description' | 'label' {
|
159
|
+
const type = this.getAttribute('data-type')
|
160
|
+
return type === 'label' ? 'label' : 'description'
|
161
|
+
}
|
162
|
+
|
163
|
+
set type(value: 'description' | 'label') {
|
164
|
+
this.setAttribute('data-type', value)
|
165
|
+
}
|
166
|
+
|
167
|
+
get direction(): Direction {
|
168
|
+
return (this.getAttribute('data-direction') || 's') as Direction
|
169
|
+
}
|
170
|
+
|
171
|
+
set direction(value: Direction) {
|
172
|
+
this.setAttribute('data-direction', value)
|
173
|
+
}
|
174
|
+
|
175
|
+
get control(): HTMLElement | null {
|
176
|
+
return this.ownerDocument.getElementById(this.htmlFor)
|
177
|
+
}
|
178
|
+
|
179
|
+
constructor() {
|
180
|
+
super()
|
181
|
+
const shadow = this.attachShadow({mode: 'open'})
|
182
|
+
shadow.innerHTML = `
|
183
|
+
<style>
|
184
|
+
${this.styles()}
|
185
|
+
</style>
|
186
|
+
<slot></slot>
|
187
|
+
`
|
188
|
+
}
|
189
|
+
|
190
|
+
connectedCallback() {
|
191
|
+
this.hidden = true
|
192
|
+
this.#allowUpdatePosition = true
|
193
|
+
|
194
|
+
if (!this.id) {
|
195
|
+
this.id = `tooltip-${Date.now()}-${(Math.random() * 10000).toFixed(0)}`
|
196
|
+
}
|
197
|
+
|
198
|
+
if (!this.control) return
|
199
|
+
|
200
|
+
this.setAttribute('role', 'tooltip')
|
201
|
+
|
202
|
+
this.#abortController?.abort()
|
203
|
+
this.#abortController = new AbortController()
|
204
|
+
const {signal} = this.#abortController
|
205
|
+
|
206
|
+
this.addEventListener('mouseleave', this, {signal})
|
207
|
+
this.control.addEventListener('mouseenter', this, {signal})
|
208
|
+
this.control.addEventListener('mouseleave', this, {signal})
|
209
|
+
this.control.addEventListener('focus', this, {signal})
|
210
|
+
this.control.addEventListener('blur', this, {signal})
|
211
|
+
this.ownerDocument.addEventListener('keydown', this, {signal})
|
212
|
+
}
|
213
|
+
|
214
|
+
disconnectedCallback() {
|
215
|
+
this.#abortController?.abort()
|
216
|
+
}
|
217
|
+
|
218
|
+
handleEvent(event: Event) {
|
219
|
+
if (!this.control) return
|
220
|
+
|
221
|
+
// Ensures that tooltip stays open when hovering between tooltip and element
|
222
|
+
// WCAG Success Criterion 1.4.13 Hoverable
|
223
|
+
if ((event.type === 'mouseenter' || event.type === 'focus') && this.hidden) {
|
224
|
+
this.hidden = false
|
225
|
+
} else if (event.type === 'blur') {
|
226
|
+
this.hidden = true
|
227
|
+
} else if (
|
228
|
+
event.type === 'mouseleave' &&
|
229
|
+
(event as MouseEvent).relatedTarget !== this.control &&
|
230
|
+
(event as MouseEvent).relatedTarget !== this
|
231
|
+
) {
|
232
|
+
this.hidden = true
|
233
|
+
} else if (event.type === 'keydown' && (event as KeyboardEvent).key === 'Escape' && !this.hidden) {
|
234
|
+
this.hidden = true
|
235
|
+
}
|
236
|
+
}
|
237
|
+
|
238
|
+
static observedAttributes = ['data-type', 'data-direction', 'id', 'hidden']
|
239
|
+
|
240
|
+
attributeChangedCallback(name: string) {
|
241
|
+
if (name === 'id' || name === 'data-type') {
|
242
|
+
if (!this.id || !this.control) return
|
243
|
+
if (this.type === 'label') {
|
244
|
+
this.control.setAttribute('aria-labelledby', this.id)
|
245
|
+
} else {
|
246
|
+
let describedBy = this.control.getAttribute('aria-describedby')
|
247
|
+
describedBy ? (describedBy = `${describedBy} ${this.id}`) : (describedBy = this.id)
|
248
|
+
this.control.setAttribute('aria-describedby', describedBy)
|
249
|
+
}
|
250
|
+
} else if (name === 'hidden') {
|
251
|
+
if (this.hidden) {
|
252
|
+
this.classList.remove(TOOLTIP_OPEN_CLASS, ...DIRECTION_CLASSES)
|
253
|
+
} else {
|
254
|
+
this.classList.add(TOOLTIP_OPEN_CLASS)
|
255
|
+
for (const tooltip of this.ownerDocument.querySelectorAll<HTMLElement>(this.tagName)) {
|
256
|
+
if (tooltip !== this) tooltip.hidden = true
|
257
|
+
}
|
258
|
+
this.#updatePosition()
|
259
|
+
}
|
260
|
+
} else if (name === 'data-direction') {
|
261
|
+
this.classList.remove(...DIRECTION_CLASSES)
|
262
|
+
const direction = this.direction
|
263
|
+
if (direction === 'n') {
|
264
|
+
this.#align = 'center'
|
265
|
+
this.#side = 'outside-top'
|
266
|
+
} else if (direction === 'ne') {
|
267
|
+
this.#align = 'start'
|
268
|
+
this.#side = 'outside-top'
|
269
|
+
} else if (direction === 'e') {
|
270
|
+
this.#align = 'center'
|
271
|
+
this.#side = 'outside-right'
|
272
|
+
} else if (direction === 'se') {
|
273
|
+
this.#align = 'start'
|
274
|
+
this.#side = 'outside-bottom'
|
275
|
+
} else if (direction === 's') {
|
276
|
+
this.#align = 'center'
|
277
|
+
this.#side = 'outside-bottom'
|
278
|
+
} else if (direction === 'sw') {
|
279
|
+
this.#align = 'end'
|
280
|
+
this.#side = 'outside-bottom'
|
281
|
+
} else if (direction === 'w') {
|
282
|
+
this.#align = 'center'
|
283
|
+
this.#side = 'outside-left'
|
284
|
+
} else if (direction === 'nw') {
|
285
|
+
this.#align = 'end'
|
286
|
+
this.#side = 'outside-top'
|
287
|
+
}
|
288
|
+
}
|
289
|
+
}
|
290
|
+
|
291
|
+
// `getAnchoredPosition` may calibrate `anchoredSide` but does not recalibrate `align`.
|
292
|
+
// Therefore, we need to determine which `align` is best based on the initial `getAnchoredPosition` calcluation.
|
293
|
+
// Related: https://github.com/primer/behaviors/issues/63
|
294
|
+
#adjustedAnchorAlignment(anchorSide: AnchorSide): AnchorAlignment | undefined {
|
295
|
+
if (!this.control) return
|
296
|
+
|
297
|
+
const tooltipPosition = this.getBoundingClientRect()
|
298
|
+
const targetPosition = this.control.getBoundingClientRect()
|
299
|
+
const tooltipWidth = tooltipPosition.width
|
300
|
+
|
301
|
+
const tooltipCenter = tooltipPosition.left + tooltipWidth / 2
|
302
|
+
const targetCenter = targetPosition.x + targetPosition.width / 2
|
303
|
+
|
304
|
+
if (Math.abs(tooltipCenter - targetCenter) < 2 || anchorSide === 'outside-left' || anchorSide === 'outside-right') {
|
305
|
+
return 'center'
|
306
|
+
} else if (tooltipPosition.left === targetPosition.left) {
|
307
|
+
return 'start'
|
308
|
+
} else if (tooltipPosition.right === targetPosition.right) {
|
309
|
+
return 'end'
|
310
|
+
} else if (tooltipCenter < targetCenter) {
|
311
|
+
if (tooltipPosition.left === 0) return 'start'
|
312
|
+
return 'end'
|
313
|
+
} else {
|
314
|
+
if (tooltipPosition.right === 0) return 'end'
|
315
|
+
return 'start'
|
316
|
+
}
|
317
|
+
}
|
318
|
+
|
319
|
+
#updatePosition() {
|
320
|
+
if (!this.control) return
|
321
|
+
if (!this.#allowUpdatePosition || this.hidden) return
|
322
|
+
|
323
|
+
const TOOLTIP_OFFSET = 10
|
324
|
+
|
325
|
+
this.style.left = `0px` // Ensures we have reliable tooltip width in `getAnchoredPosition`
|
326
|
+
let position = getAnchoredPosition(this, this.control, {
|
327
|
+
side: this.#side,
|
328
|
+
align: this.#align,
|
329
|
+
anchorOffset: TOOLTIP_OFFSET
|
330
|
+
})
|
331
|
+
let anchorSide = position.anchorSide
|
332
|
+
|
333
|
+
// We need to set tooltip position in order to determine ideal align.
|
334
|
+
this.style.top = `${position.top}px`
|
335
|
+
this.style.left = `${position.left}px`
|
336
|
+
let direction: Direction = 's'
|
337
|
+
|
338
|
+
const align = this.#adjustedAnchorAlignment(anchorSide)
|
339
|
+
if (!align) return
|
340
|
+
|
341
|
+
this.style.left = `0px` // Reset tooltip position again to ensure accurate width in `getAnchoredPosition`
|
342
|
+
position = getAnchoredPosition(this, this.control, {side: anchorSide, align, anchorOffset: TOOLTIP_OFFSET})
|
343
|
+
anchorSide = position.anchorSide
|
344
|
+
|
345
|
+
this.style.top = `${position.top}px`
|
346
|
+
this.style.left = `${position.left}px`
|
347
|
+
|
348
|
+
if (anchorSide === 'outside-left') {
|
349
|
+
direction = 'w'
|
350
|
+
} else if (anchorSide === 'outside-right') {
|
351
|
+
direction = 'e'
|
352
|
+
} else if (anchorSide === 'outside-top') {
|
353
|
+
if (align === 'center') {
|
354
|
+
direction = 'n'
|
355
|
+
} else if (align === 'start') {
|
356
|
+
direction = 'ne'
|
357
|
+
} else {
|
358
|
+
direction = 'nw'
|
359
|
+
}
|
360
|
+
} else {
|
361
|
+
if (align === 'center') {
|
362
|
+
direction = 's'
|
363
|
+
} else if (align === 'start') {
|
364
|
+
direction = 'se'
|
365
|
+
} else {
|
366
|
+
direction = 'sw'
|
367
|
+
}
|
368
|
+
}
|
369
|
+
|
370
|
+
this.classList.add(`tooltip-${direction}`)
|
371
|
+
}
|
372
|
+
}
|
373
|
+
|
374
|
+
if (!window.customElements.get('tool-tip')) {
|
375
|
+
window.TooltipElement = TooltipElement
|
376
|
+
window.customElements.define('tool-tip', TooltipElement)
|
377
|
+
}
|
378
|
+
|
379
|
+
declare global {
|
380
|
+
interface Window {
|
381
|
+
TooltipElement: typeof TooltipElement
|
382
|
+
}
|
383
|
+
}
|
@@ -5,7 +5,7 @@ require "primer/classify"
|
|
5
5
|
module Primer
|
6
6
|
# All Primer ViewComponents accept a standard set of options called system arguments, mimicking the [styled-system API](https://styled-system.com/table) used by [Primer React](https://primer.style/components/system-props).
|
7
7
|
#
|
8
|
-
# Under the hood, system arguments are [mapped](https://github.com/primer/view_components/blob/main/
|
8
|
+
# Under the hood, system arguments are [mapped](https://github.com/primer/view_components/blob/main/lib/primer/classify.rb) to Primer CSS classes, with any remaining options passed to Rails' [`content_tag`](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag).
|
9
9
|
#
|
10
10
|
# ## Responsive values
|
11
11
|
#
|
@@ -85,7 +85,7 @@ module Primer
|
|
85
85
|
#
|
86
86
|
# | Name | Type | Description |
|
87
87
|
# | :- | :- | :- |
|
88
|
-
# | `clearfix` | Boolean |
|
88
|
+
# | `clearfix` | Boolean | Whether to assign the `clearfix` class. |
|
89
89
|
# | `col` | Integer | Number of columns. <%= one_of(Primer::Classify::Utilities.mappings(:col)) %> |
|
90
90
|
# | `container` | Symbol | Size of the container. <%= one_of(Primer::Classify::Utilities.mappings(:container)) %> |
|
91
91
|
#
|
@@ -1,6 +1,24 @@
|
|
1
|
-
<%= label %>
|
2
1
|
<%= render Primer::BaseComponent.new(**@system_arguments) do %>
|
3
|
-
<%=
|
4
|
-
|
5
|
-
|
2
|
+
<label for="<%= @input_id %>" class="<%= @label_classes %>">
|
3
|
+
<% if @is_label_visible %>
|
4
|
+
<%= @label_text %>
|
5
|
+
<% else %>
|
6
|
+
<span class="sr-only"><%= @label_text %></span>
|
7
|
+
<% end %>
|
8
|
+
</label>
|
9
|
+
<span class="autocomplete-body">
|
10
|
+
<% if @with_icon %>
|
11
|
+
<div class="form-control autocomplete-embedded-icon-wrap">
|
12
|
+
<%= render Primer::OcticonComponent.new(:search) %>
|
13
|
+
<% end %>
|
14
|
+
<%= input %>
|
15
|
+
<% if @is_clearable %>
|
16
|
+
<button id="<%= @input_id %>-clear" class="btn-octicon" aria-label="Clear"><%= primer_octicon "x" %></button>
|
17
|
+
<% end %>
|
18
|
+
<% if @with_icon %>
|
19
|
+
</div>
|
20
|
+
<% end %>
|
21
|
+
<%= results %>
|
22
|
+
</span>
|
23
|
+
<div id="<%= @list_id %>-feedback" class="sr-only"></div>
|
6
24
|
<% end %>
|
@@ -7,43 +7,13 @@ module Primer
|
|
7
7
|
# @accessibility
|
8
8
|
# Always set an accessible label to help the user interact with the component.
|
9
9
|
#
|
10
|
-
# *
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# always be used unless there is compelling reason not to. A placeholder is not a label.
|
10
|
+
# * `label_text` is required and visible by default.
|
11
|
+
# * If you must use a non-visible label, set `is_label_visible` to `false`.
|
12
|
+
# However, please note that a visible label should almost always
|
13
|
+
# be used unless there is compelling reason not to. A placeholder is not a label.
|
15
14
|
class AutoComplete < Primer::Component
|
16
15
|
status :beta
|
17
|
-
|
18
|
-
# Optionally render a visible label. See <%= link_to_accessibility %>
|
19
16
|
#
|
20
|
-
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
21
|
-
renders_one :label, lambda { |**system_arguments|
|
22
|
-
deny_tag_argument(**system_arguments)
|
23
|
-
system_arguments[:for] = @input_id
|
24
|
-
system_arguments[:tag] = :label
|
25
|
-
Primer::BaseComponent.new(**system_arguments)
|
26
|
-
}
|
27
|
-
|
28
|
-
# Required input used to search for results
|
29
|
-
#
|
30
|
-
# @param type [Symbol] <%= one_of(Primer::Beta::AutoComplete::Input::TYPE_OPTIONS) %>
|
31
|
-
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
32
|
-
renders_one :input, lambda { |**system_arguments|
|
33
|
-
aria_label = aria("label", system_arguments) || @aria_label
|
34
|
-
if aria_label.present?
|
35
|
-
system_arguments[:"aria-label"] = aria_label
|
36
|
-
system_arguments[:aria]&.delete(:label)
|
37
|
-
end
|
38
|
-
|
39
|
-
name = system_arguments[:name] || @input_id
|
40
|
-
Input.new(id: @input_id, name: name, **system_arguments)
|
41
|
-
}
|
42
|
-
|
43
|
-
# Optional icon to be rendered before the input. Has the same arguments as <%= link_to_component(Primer::OcticonComponent) %>.
|
44
|
-
#
|
45
|
-
renders_one :icon, Primer::OcticonComponent
|
46
|
-
|
47
17
|
# Customizable results list.
|
48
18
|
#
|
49
19
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
@@ -56,34 +26,82 @@ module Primer
|
|
56
26
|
system_arguments[:classes]
|
57
27
|
)
|
58
28
|
|
59
|
-
aria_label = system_arguments[:"aria-label"] || system_arguments.dig(:aria, :label) || @aria_label
|
60
|
-
system_arguments[:"aria-label"] = aria_label if aria_label.present?
|
61
|
-
system_arguments[:aria]&.delete(:label)
|
62
|
-
|
63
29
|
Primer::BaseComponent.new(**system_arguments)
|
64
30
|
}
|
65
31
|
|
32
|
+
# Customizable input used to search for results.
|
33
|
+
# It is preferred to use this slot sparingly - it will be created by default if not explicity added.
|
34
|
+
#
|
35
|
+
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
36
|
+
renders_one :input, lambda { |**system_arguments|
|
37
|
+
sanitized_args = deny_tag_argument(**system_arguments)
|
38
|
+
sanitized_args = deny_single_argument(:autofocus, "autofocus is not allowed for accessibility reasons. See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus#accessibility_considerations for more information.", **sanitized_args)
|
39
|
+
deny_aria_key(
|
40
|
+
:label,
|
41
|
+
"instead of `aria-label`, include `label_text` and set `is_label_visible` to `false` on the component initializer.",
|
42
|
+
**sanitized_args
|
43
|
+
)
|
44
|
+
deny_single_argument(
|
45
|
+
:id,
|
46
|
+
"`id` will always be set to @input_id.",
|
47
|
+
**sanitized_args
|
48
|
+
)
|
49
|
+
deny_single_argument(
|
50
|
+
:name,
|
51
|
+
"Set @input_name on the component initializer instead with `input_name`.",
|
52
|
+
**sanitized_args
|
53
|
+
)
|
54
|
+
sanitized_args[:id] = @input_id
|
55
|
+
sanitized_args[:name] = @input_name
|
56
|
+
sanitized_args[:tag] = :input
|
57
|
+
sanitized_args[:autocomplete] = "off"
|
58
|
+
|
59
|
+
sanitized_args[:type] = :text
|
60
|
+
sanitized_args[:classes] = class_names(
|
61
|
+
"form-control",
|
62
|
+
sanitized_args[:classes]
|
63
|
+
)
|
64
|
+
|
65
|
+
Primer::BaseComponent.new(**sanitized_args)
|
66
|
+
}
|
67
|
+
|
66
68
|
# @example Default
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
69
|
+
# @description
|
70
|
+
# Labels are stacked by default.
|
71
|
+
# @code
|
72
|
+
# <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", input_id: "fruits-input--default", list_id: "fruits-popup--default")) %>
|
71
73
|
#
|
72
|
-
# @example With
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
74
|
+
# @example With inline label
|
75
|
+
# @description
|
76
|
+
# Labels can be inline by setting `is_label_inline: true`. However, labels will always become stacked on smaller screen sizes.
|
77
|
+
# @code
|
78
|
+
# <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", is_label_inline: true, input_id: "fruits-input--inline-label", list_id: "fruits-popup--inline-label")) %>
|
76
79
|
#
|
77
|
-
# @example With
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
80
|
+
# @example With non-visible label
|
81
|
+
# @description
|
82
|
+
# A non-visible label may be rendered with `is_label_visible: false`, but it is highly discouraged. See <%= link_to_accessibility %>.
|
83
|
+
# @code
|
84
|
+
# <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", input_id: "fruits-input--non-visible-label", list_id: "fruits-popup--non-visible-label", is_label_visible: false)) %>
|
85
|
+
#
|
86
|
+
# @example With icon
|
87
|
+
# @description
|
88
|
+
# To display a search icon, set `with_icon` to `true`.
|
89
|
+
# @code
|
90
|
+
# <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", list_id: "fruits-popup--icon", input_id: "fruits-input--icon", with_icon: true)) %>
|
91
|
+
#
|
92
|
+
# @example With icon and non-visible label
|
93
|
+
# <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", list_id: "fruits-popup--icon-no-label", input_id: "fruits-input--icon-no-label", with_icon: true, is_label_visible: false)) %>
|
94
|
+
#
|
95
|
+
# @example With clear button
|
96
|
+
# <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", input_id: "fruits-input--clear", list_id: "fruits-popup--clear", is_clearable: true)) %>
|
97
|
+
#
|
98
|
+
# @example With custom classes for the input
|
99
|
+
# <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", input_id: "fruits-input--custom-input", list_id: "fruits-popup--custom-input")) do |c| %>
|
100
|
+
# <% c.input(classes: "custom-class") %>
|
81
101
|
# <% end %>
|
82
102
|
#
|
83
103
|
# @example With custom classes for the results
|
84
|
-
# <%= render(Primer::Beta::AutoComplete.new(src: "/auto_complete", input_id: "fruits-input-
|
85
|
-
# <% c.label(classes:"").with_content("Fruits") %>
|
86
|
-
# <% c.input(type: :text) %>
|
104
|
+
# <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", input_id: "fruits-input--custom-results", list_id: "fruits-popup--custom-results")) do |c| %>
|
87
105
|
# <% c.results(classes: "custom-class") do %>
|
88
106
|
# <%= render(Primer::Beta::AutoComplete::Item.new(selected: true, value: "apple")) do |c| %>
|
89
107
|
# Apple
|
@@ -94,67 +112,36 @@ module Primer
|
|
94
112
|
# <% end %>
|
95
113
|
# <% end %>
|
96
114
|
#
|
97
|
-
# @
|
98
|
-
# <%= render(Primer::Beta::AutoComplete.new(src: "/auto_complete", list_id: "fruits-popup-4", input_id: "fruits-input-4", position: :relative)) do |c| %>
|
99
|
-
# <% c.label(classes:"").with_content("Fruits") %>
|
100
|
-
# <% c.input(type: :text) %>
|
101
|
-
# <% c.icon(icon: :search) %>
|
102
|
-
# <% end %>
|
103
|
-
#
|
115
|
+
# @param label_text [String] The label of the input.
|
104
116
|
# @param src [String] The route to query.
|
105
117
|
# @param input_id [String] Id of the input element.
|
118
|
+
# @param input_name [String] Optional name of the input element, defaults to `input_id` when not set.
|
106
119
|
# @param list_id [String] Id of the list element.
|
120
|
+
# @param with_icon [Boolean] Controls if a search icon is visible, defaults to `false`.
|
121
|
+
# @param is_label_visible [Boolean] Controls if the label is visible. If `false`, screen reader only text will be added.
|
122
|
+
# @param is_clearable [Boolean] Adds optional clear button.
|
123
|
+
# @param is_label_inline [Boolean] Controls if the label is inline. On smaller screens, label will always become stacked.
|
107
124
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
108
|
-
def initialize(src:, list_id:, input_id:, **system_arguments)
|
125
|
+
def initialize(label_text:, src:, list_id:, input_id:, input_name: nil, is_label_visible: true, is_label_inline: false, with_icon: false, is_clearable: false, **system_arguments)
|
126
|
+
@label_text = label_text
|
109
127
|
@list_id = list_id
|
110
128
|
@input_id = input_id
|
111
|
-
@
|
112
|
-
|
113
|
-
|
129
|
+
@input_name = input_name || input_id
|
130
|
+
@is_label_visible = is_label_visible
|
131
|
+
@with_icon = with_icon
|
132
|
+
@is_clearable = is_clearable
|
114
133
|
|
134
|
+
@label_classes = is_label_inline ? "autocomplete-label-inline" : "autocomplete-label-stacked"
|
115
135
|
@system_arguments = deny_tag_argument(**system_arguments)
|
116
136
|
@system_arguments[:tag] = "auto-complete"
|
117
137
|
@system_arguments[:src] = src
|
118
138
|
@system_arguments[:for] = list_id
|
119
139
|
end
|
120
140
|
|
121
|
-
# add `results` without needing to explicitly call
|
141
|
+
# add `input` and `results` without needing to explicitly call them in the view
|
122
142
|
def before_render
|
123
|
-
raise ArgumentError, "Missing `input` slot" if input.blank?
|
124
|
-
raise ArgumentError, "Accessible label is required." if label.blank? && input.missing_label?
|
125
|
-
|
126
143
|
results(classes: "") unless results
|
127
|
-
|
128
|
-
|
129
|
-
# This component is part of `Primer::Beta::AutoCompleteComponent` and should not be
|
130
|
-
# used as a standalone component.
|
131
|
-
class Input < Primer::Component
|
132
|
-
DEFAULT_TYPE = :text
|
133
|
-
TYPE_OPTIONS = [DEFAULT_TYPE, :search].freeze
|
134
|
-
|
135
|
-
# @param type [Symbol] <%= one_of(Primer::Beta::AutoComplete::Input::TYPE_OPTIONS) %>
|
136
|
-
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
137
|
-
def initialize(type: DEFAULT_TYPE, **system_arguments)
|
138
|
-
@system_arguments = deny_tag_argument(**system_arguments)
|
139
|
-
@system_arguments[:tag] = :input
|
140
|
-
|
141
|
-
@aria_label = system_arguments[:"aria-label"]
|
142
|
-
@aria_labelledby = system_arguments[:"aria-labelledby"] || system_arguments.dig(:aria, :labelledby)
|
143
|
-
|
144
|
-
@system_arguments[:type] = fetch_or_fallback(TYPE_OPTIONS, type, DEFAULT_TYPE)
|
145
|
-
@system_arguments[:classes] = class_names(
|
146
|
-
"form-control",
|
147
|
-
system_arguments[:classes]
|
148
|
-
)
|
149
|
-
end
|
150
|
-
|
151
|
-
def missing_label?
|
152
|
-
@aria_label.blank? && @aria_labelledby.blank?
|
153
|
-
end
|
154
|
-
|
155
|
-
def call
|
156
|
-
render(Primer::BaseComponent.new(**@system_arguments))
|
157
|
-
end
|
144
|
+
input(classes: "") unless input
|
158
145
|
end
|
159
146
|
end
|
160
147
|
end
|
@@ -5,9 +5,13 @@
|
|
5
5
|
<%= heading %>
|
6
6
|
<%= description %>
|
7
7
|
|
8
|
-
|
8
|
+
<% if primary_action.present? %>
|
9
|
+
<div class="blankslate-action">
|
10
|
+
<%= primary_action %>
|
11
|
+
</div>
|
12
|
+
<% end %>
|
9
13
|
<% if secondary_action.present? %>
|
10
|
-
<div class="
|
14
|
+
<div class="blankslate-action">
|
11
15
|
<%= secondary_action %>
|
12
16
|
</div>
|
13
17
|
<% end %>
|