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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +133 -2
  3. data/README.md +1 -1
  4. data/app/assets/javascripts/primer_view_components.js +1 -1
  5. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  6. data/app/components/primer/alpha/tooltip.d.ts +24 -0
  7. data/app/components/primer/alpha/tooltip.js +381 -0
  8. data/app/components/primer/alpha/tooltip.rb +103 -0
  9. data/app/components/primer/alpha/tooltip.ts +383 -0
  10. data/app/components/primer/base_component.rb +2 -2
  11. data/app/components/primer/beta/auto_complete/auto_complete.html.erb +22 -4
  12. data/app/components/primer/beta/auto_complete.rb +86 -99
  13. data/app/components/primer/beta/blankslate.html.erb +6 -2
  14. data/app/components/primer/beta/blankslate.rb +4 -9
  15. data/app/components/primer/beta/truncate.rb +1 -0
  16. data/app/components/primer/button_component.html.erb +1 -0
  17. data/app/components/primer/button_component.rb +29 -0
  18. data/app/components/primer/component.rb +9 -2
  19. data/app/components/primer/details_component.rb +1 -1
  20. data/app/components/primer/icon_button.rb +1 -1
  21. data/app/components/primer/link_component.erb +4 -0
  22. data/app/components/primer/link_component.rb +29 -4
  23. data/app/components/primer/markdown.rb +1 -1
  24. data/app/components/primer/popover_component.rb +5 -9
  25. data/app/components/primer/primer.d.ts +1 -0
  26. data/app/components/primer/primer.js +1 -0
  27. data/app/components/primer/primer.ts +1 -0
  28. data/app/components/primer/subhead_component.html.erb +1 -1
  29. data/app/components/primer/subhead_component.rb +1 -1
  30. data/app/components/primer/tooltip.rb +1 -1
  31. data/app/lib/primer/test_selector_helper.rb +1 -1
  32. data/lib/primer/classify/utilities.yml +28 -0
  33. data/lib/primer/view_components/linters/button_component_migration_counter.rb +1 -1
  34. data/lib/primer/view_components/version.rb +1 -1
  35. data/lib/rubocop/cop/primer/component_name_migration.rb +35 -0
  36. data/lib/rubocop/cop/primer/primer_octicon.rb +1 -1
  37. data/lib/tasks/docs.rake +12 -7
  38. data/lib/tasks/utilities.rake +1 -1
  39. data/static/arguments.yml +52 -1
  40. data/static/audited_at.json +1 -1
  41. data/static/classes.yml +9 -4
  42. data/static/constants.json +18 -8
  43. data/static/statuses.json +2 -2
  44. metadata +13 -9
  45. data/app/components/primer/auto_complete/auto_complete.d.ts +0 -1
  46. 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/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,6 +1,24 @@
1
- <%= label %>
2
1
  <%= render Primer::BaseComponent.new(**@system_arguments) do %>
3
- <%= input %>
4
- <%= icon %>
5
- <%= results %>
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
- # * Set the `label` slot to render a visible label. Alternatively, associate an existing visible text element
11
- # as a label by setting `aria-labelledby`.
12
- # * If you must use a non-visible label, set `:"aria-label"` on `AutoComplete` and Primer
13
- # will apply it to the correct elements. However, please note that a visible label should almost
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
- # <%= render(Primer::Beta::AutoComplete.new(src: "/auto_complete", input_id: "fruits-input-1", list_id: "fruits-popup-1", position: :relative)) do |c| %>
68
- # <% c.label(classes:"").with_content("Fruits") %>
69
- # <% c.input(type: :text) %>
70
- # <% end %>
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 `aria-label`
73
- # <%= render(Primer::Beta::AutoComplete.new("aria-label": "Fruits", src: "/auto_complete", input_id: "fruits-input-2", list_id: "fruits-popup-2", position: :relative)) do |c| %>
74
- # <% c.input(type: :text) %>
75
- # <% end %>
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 `aria-labelledby`
78
- # <%= render(Primer::HeadingComponent.new(tag: :h2, id: "search-1")) { "Search" } %>
79
- # <%= render(Primer::Beta::AutoComplete.new(src: "/auto_complete", input_id: "fruits-input-3", list_id: "fruits-popup-2", position: :relative)) do |c| %>
80
- # <% c.input("aria-labelledby": "search-1") %>
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-4", list_id: "fruits-popup-3", position: :relative)) do |c| %>
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
- # @example With Icon
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
- @aria_label = aria("label", system_arguments)
112
-
113
- system_arguments.delete(:"aria-label") && system_arguments[:aria]&.delete(:label)
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 it in the view
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
- end
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
- <%= primary_action %>
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="mt-3">
14
+ <div class="blankslate-action">
11
15
  <%= secondary_action %>
12
16
  </div>
13
17
  <% end %>