primer_view_components 0.0.68 → 0.0.71
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 +96 -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/navigation_list_element.d.ts +11 -0
- data/app/components/primer/alpha/navigation_list_element.js +42 -0
- 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 +16 -6
- data/app/components/primer/beta/auto_complete.rb +82 -20
- 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/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/lib/primer/classify/utilities.yml +40 -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/primer_octicon.rb +1 -1
- data/lib/tasks/custom_utilities.yml +12 -0
- data/lib/tasks/docs.rake +9 -5
- data/lib/tasks/utilities.rake +1 -1
- data/static/arguments.yml +43 -1
- data/static/audited_at.json +1 -0
- data/static/classes.yml +5 -0
- data/static/constants.json +18 -0
- data/static/statuses.json +1 -0
- metadata +18 -11
@@ -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,14 +1,24 @@
|
|
1
1
|
<%= render Primer::BaseComponent.new(**@system_arguments) do %>
|
2
|
-
<label for="<%= @input_id %>">
|
2
|
+
<label for="<%= @input_id %>" class="<%= @label_classes %>">
|
3
3
|
<% if @is_label_visible %>
|
4
4
|
<%= @label_text %>
|
5
5
|
<% else %>
|
6
6
|
<span class="sr-only"><%= @label_text %></span>
|
7
7
|
<% end %>
|
8
|
-
<% if icon.present? %>
|
9
|
-
<%= icon %>
|
10
|
-
<% end %>
|
11
8
|
</label>
|
12
|
-
<
|
13
|
-
|
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>
|
14
24
|
<% end %>
|
@@ -13,10 +13,7 @@ module Primer
|
|
13
13
|
# be used unless there is compelling reason not to. A placeholder is not a label.
|
14
14
|
class AutoComplete < Primer::Component
|
15
15
|
status :beta
|
16
|
-
|
17
|
-
# Optional icon to be rendered before the input. Has the same arguments as <%= link_to_component(Primer::OcticonComponent) %>.
|
18
|
-
renders_one :icon, Primer::OcticonComponent
|
19
|
-
|
16
|
+
#
|
20
17
|
# Customizable results list.
|
21
18
|
#
|
22
19
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
@@ -32,14 +29,79 @@ module Primer
|
|
32
29
|
Primer::BaseComponent.new(**system_arguments)
|
33
30
|
}
|
34
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
|
+
|
35
68
|
# @example Default
|
36
|
-
#
|
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")) %>
|
73
|
+
#
|
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")) %>
|
79
|
+
#
|
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)) %>
|
37
94
|
#
|
38
|
-
# @example With
|
39
|
-
# <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", input_id: "fruits-input
|
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)) %>
|
40
97
|
#
|
41
|
-
# @example With
|
42
|
-
# <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", input_id: "fruits-input-
|
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") %>
|
101
|
+
# <% end %>
|
102
|
+
#
|
103
|
+
# @example With custom classes for the results
|
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| %>
|
43
105
|
# <% c.results(classes: "custom-class") do %>
|
44
106
|
# <%= render(Primer::Beta::AutoComplete::Item.new(selected: true, value: "apple")) do |c| %>
|
45
107
|
# Apple
|
@@ -50,36 +112,36 @@ module Primer
|
|
50
112
|
# <% end %>
|
51
113
|
# <% end %>
|
52
114
|
#
|
53
|
-
# @example With Icon
|
54
|
-
# <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", list_id: "fruits-popup-4", input_id: "fruits-input-4", position: :relative)) do |c| %>
|
55
|
-
# <% c.icon(icon: :search) %>
|
56
|
-
# <% end %>
|
57
|
-
#
|
58
|
-
# @example With Icon and Non-Visible Label
|
59
|
-
# <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", list_id: "fruits-popup-5", input_id: "fruits-input-5", is_label_visible: false, position: :relative)) do |c| %>
|
60
|
-
# <% c.icon(icon: :search) %>
|
61
|
-
# <% end %>
|
62
115
|
# @param label_text [String] The label of the input.
|
63
116
|
# @param src [String] The route to query.
|
64
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.
|
65
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`.
|
66
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.
|
67
124
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
68
|
-
def initialize(label_text:, src:, list_id:, input_id:, is_label_visible: true, **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)
|
69
126
|
@label_text = label_text
|
70
127
|
@list_id = list_id
|
71
128
|
@input_id = input_id
|
129
|
+
@input_name = input_name || input_id
|
72
130
|
@is_label_visible = is_label_visible
|
131
|
+
@with_icon = with_icon
|
132
|
+
@is_clearable = is_clearable
|
73
133
|
|
134
|
+
@label_classes = is_label_inline ? "autocomplete-label-inline" : "autocomplete-label-stacked"
|
74
135
|
@system_arguments = deny_tag_argument(**system_arguments)
|
75
136
|
@system_arguments[:tag] = "auto-complete"
|
76
137
|
@system_arguments[:src] = src
|
77
138
|
@system_arguments[:for] = list_id
|
78
139
|
end
|
79
140
|
|
80
|
-
# add `results` without needing to explicitly call them in the view
|
141
|
+
# add `input` and `results` without needing to explicitly call them in the view
|
81
142
|
def before_render
|
82
143
|
results(classes: "") unless results
|
144
|
+
input(classes: "") unless input
|
83
145
|
end
|
84
146
|
end
|
85
147
|
end
|
@@ -56,6 +56,23 @@ module Primer
|
|
56
56
|
}
|
57
57
|
alias counter trailing_visual_counter # remove alias when all buttons are migrated to new slot names
|
58
58
|
|
59
|
+
# `Tooltip` that appears on mouse hover or keyboard focus over the button. Use tooltips sparingly and as a last resort.
|
60
|
+
# **Important:** This tooltip defaults to `type: :description`. In a few scenarios, `type: :label` may be more appropriate.
|
61
|
+
# Consult the <%= link_to_component(Primer::Alpha::Tooltip) %> documentation for more information.
|
62
|
+
#
|
63
|
+
# @param type [Symbol] (:description) <%= one_of(Primer::Alpha::Tooltip::TYPE_OPTIONS) %>
|
64
|
+
# @param system_arguments [Hash] Same arguments as <%= link_to_component(Primer::Alpha::Tooltip) %>.
|
65
|
+
renders_one :tooltip, lambda { |**system_arguments|
|
66
|
+
raise ArgumentError, "Buttons with a tooltip must have a unique `id` set on the `Button`." if @id.blank? && !Rails.env.production?
|
67
|
+
|
68
|
+
@system_arguments = system_arguments
|
69
|
+
|
70
|
+
@system_arguments[:for_id] = @id
|
71
|
+
@system_arguments[:type] ||= :description
|
72
|
+
|
73
|
+
Primer::Alpha::Tooltip.new(**@system_arguments)
|
74
|
+
}
|
75
|
+
|
59
76
|
# @example Schemes
|
60
77
|
# <%= render(Primer::ButtonComponent.new) { "Default" } %>
|
61
78
|
# <%= render(Primer::ButtonComponent.new(scheme: :primary)) { "Primary" } %>
|
@@ -96,6 +113,15 @@ module Primer
|
|
96
113
|
# Button
|
97
114
|
# <% end %>
|
98
115
|
#
|
116
|
+
# @example With tooltip
|
117
|
+
# @description
|
118
|
+
# Use tooltips sparingly and as a last resort. Consult the <%= link_to_component(Primer::Alpha::Tooltip) %> documentation for more information.
|
119
|
+
# @code
|
120
|
+
# <%= render(Primer::ButtonComponent.new(id: "button-with-tooltip")) do |c| %>
|
121
|
+
# <% c.tooltip(text: "Tooltip text") %>
|
122
|
+
# Button
|
123
|
+
# <% end %>
|
124
|
+
#
|
99
125
|
# @param scheme [Symbol] <%= one_of(Primer::ButtonComponent::SCHEME_OPTIONS) %>
|
100
126
|
# @param variant [Symbol] DEPRECATED. <%= one_of(Primer::ButtonComponent::SIZE_OPTIONS) %>
|
101
127
|
# @param size [Symbol] <%= one_of(Primer::ButtonComponent::SIZE_OPTIONS) %>
|
@@ -118,6 +144,9 @@ module Primer
|
|
118
144
|
@dropdown = dropdown
|
119
145
|
|
120
146
|
@system_arguments = system_arguments
|
147
|
+
|
148
|
+
@id = @system_arguments[:id]
|
149
|
+
|
121
150
|
@system_arguments[:classes] = class_names(
|
122
151
|
system_arguments[:classes],
|
123
152
|
SCHEME_MAPPINGS[fetch_or_fallback(SCHEME_OPTIONS, scheme, DEFAULT_SCHEME)],
|
@@ -102,10 +102,17 @@ module Primer
|
|
102
102
|
def deny_aria_label(tag:, arguments:)
|
103
103
|
return arguments.except!(:skip_aria_label_check) if arguments[:skip_aria_label_check]
|
104
104
|
return if arguments[:role]
|
105
|
-
return unless aria(:label, arguments)
|
106
105
|
return unless INVALID_ARIA_LABEL_TAGS.include?(tag)
|
107
106
|
|
108
|
-
|
107
|
+
deny_aria_key(
|
108
|
+
:label,
|
109
|
+
"Don't use `aria-label` on `#{tag}` elements. See https://www.tpgi.com/short-note-on-aria-label-aria-labelledby-and-aria-describedby/",
|
110
|
+
**arguments
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
114
|
+
def deny_aria_key(key, help_text, **arguments)
|
115
|
+
raise ArgumentError, help_text if should_raise_aria_error? && aria(key, arguments)
|
109
116
|
end
|
110
117
|
|
111
118
|
def deny_tag_argument(**arguments)
|
@@ -49,7 +49,7 @@ module Primer
|
|
49
49
|
# <% end %>
|
50
50
|
#
|
51
51
|
# @param overlay [Symbol] Dictates the type of overlay to render with. <%= one_of(Primer::DetailsComponent::OVERLAY_MAPPINGS.keys) %>
|
52
|
-
# @param reset [Boolean]
|
52
|
+
# @param reset [Boolean] Defaults to false. If set to true, it will remove the default caret and remove style from the summary element
|
53
53
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
54
54
|
def initialize(overlay: NO_OVERLAY, reset: false, **system_arguments)
|
55
55
|
@system_arguments = deny_tag_argument(**system_arguments)
|
@@ -6,7 +6,7 @@ module Primer
|
|
6
6
|
# @accessibility
|
7
7
|
# `IconButton` requires an `aria-label`, which will provide assistive technologies with an accessible label.
|
8
8
|
# The `aria-label` should describe the action to be invoked rather than the icon itself. For instance,
|
9
|
-
# if your `IconButton` renders a magnifying glass icon and
|
9
|
+
# if your `IconButton` renders a magnifying glass icon and invokes a search action, the `aria-label` should be
|
10
10
|
# `"Search"` instead of `"Magnifying glass"`.
|
11
11
|
# [Learn more about best functional image practices (WAI Images)](https://www.w3.org/WAI/tutorials/images/functional)
|
12
12
|
class IconButton < Primer::Component
|