primer_view_components 0.0.68 → 0.0.71

Sign up to get free protection for your applications and to get access to all the features.
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