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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -1
  3. data/app/assets/javascripts/primer_view_components.js +1 -1
  4. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  5. data/app/components/primer/alpha/navigation_list_element.d.ts +11 -0
  6. data/app/components/primer/alpha/navigation_list_element.js +42 -0
  7. data/app/components/primer/alpha/tooltip.d.ts +24 -0
  8. data/app/components/primer/alpha/tooltip.js +381 -0
  9. data/app/components/primer/alpha/tooltip.rb +103 -0
  10. data/app/components/primer/alpha/tooltip.ts +383 -0
  11. data/app/components/primer/base_component.rb +2 -2
  12. data/app/components/primer/beta/auto_complete/auto_complete.html.erb +16 -6
  13. data/app/components/primer/beta/auto_complete.rb +82 -20
  14. data/app/components/primer/beta/truncate.rb +1 -0
  15. data/app/components/primer/button_component.html.erb +1 -0
  16. data/app/components/primer/button_component.rb +29 -0
  17. data/app/components/primer/component.rb +9 -2
  18. data/app/components/primer/details_component.rb +1 -1
  19. data/app/components/primer/icon_button.rb +1 -1
  20. data/app/components/primer/link_component.erb +4 -0
  21. data/app/components/primer/link_component.rb +29 -4
  22. data/app/components/primer/markdown.rb +1 -1
  23. data/app/components/primer/primer.d.ts +1 -0
  24. data/app/components/primer/primer.js +1 -0
  25. data/app/components/primer/primer.ts +1 -0
  26. data/app/components/primer/subhead_component.html.erb +1 -1
  27. data/app/components/primer/subhead_component.rb +1 -1
  28. data/lib/primer/classify/utilities.yml +40 -0
  29. data/lib/primer/view_components/linters/button_component_migration_counter.rb +1 -1
  30. data/lib/primer/view_components/version.rb +1 -1
  31. data/lib/rubocop/cop/primer/primer_octicon.rb +1 -1
  32. data/lib/tasks/custom_utilities.yml +12 -0
  33. data/lib/tasks/docs.rake +9 -5
  34. data/lib/tasks/utilities.rake +1 -1
  35. data/static/arguments.yml +43 -1
  36. data/static/audited_at.json +1 -0
  37. data/static/classes.yml +5 -0
  38. data/static/constants.json +18 -0
  39. data/static/statuses.json +1 -0
  40. 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/app/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).
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 | Wether to assign the `clearfix` class. |
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
- <input id="<%= @input_id %>" name="<%= @input_id %>" type="text" class="form-control" autocomplete="off">
13
- <%= results %>
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
- # <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", input_id: "fruits-input-1", list_id: "fruits-popup-1", position: :relative)) %>
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 Non-Visible Label
39
- # <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", input_id: "fruits-input-2", list_id: "fruits-popup-2", is_label_visible: false, position: :relative)) %>
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 Custom Classes for the Results
42
- # <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", input_id: "fruits-input-3", list_id: "fruits-popup-3", position: :relative)) do |c| %>
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
@@ -92,6 +92,7 @@ module Primer
92
92
  @system_arguments[:tag] = system_arguments[:tag] || :span
93
93
  @system_arguments[:classes] = class_names(
94
94
  "Truncate-text",
95
+ system_arguments[:classes],
95
96
  "Truncate-text--primary": priority,
96
97
  "Truncate-text--expandable": expandable
97
98
  )
@@ -1,3 +1,4 @@
1
1
  <%= render Primer::BaseButton.new(**@system_arguments) do -%>
2
2
  <%= leading_visual %><%= trimmed_content %><%= trailing_visual %><%= primer_octicon("triangle-down", ml: 2, mr: -1) if @dropdown %>
3
+ <%= tooltip %>
3
4
  <% 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
- raise ArgumentError, "Don't use `aria-label` on `#{tag}` elements. See https://www.tpgi.com/short-note-on-aria-label-aria-labelledby-and-aria-describedby/" if should_raise_aria_error?
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] Defatuls to false. If set to true, it will remove the default caret and remove style from the summary element
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 invokves a search action, the `aria-label` should be
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