primer_view_components 0.0.66 → 0.0.69

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -1
  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 +95 -0
  9. data/app/components/primer/alpha/tooltip.ts +377 -0
  10. data/app/components/primer/base_component.rb +2 -2
  11. data/app/components/primer/beta/auto_complete/auto_complete.html.erb +11 -3
  12. data/app/components/primer/beta/auto_complete.rb +20 -95
  13. data/app/components/primer/beta/blankslate.html.erb +6 -2
  14. data/app/components/primer/beta/blankslate.rb +8 -11
  15. data/app/components/primer/beta/truncate.rb +1 -0
  16. data/app/components/primer/content.rb +12 -0
  17. data/app/components/primer/details_component.rb +1 -1
  18. data/app/components/primer/dropdown.rb +2 -2
  19. data/app/components/primer/icon_button.rb +1 -1
  20. data/app/components/primer/markdown.rb +1 -1
  21. data/app/components/primer/popover_component.rb +15 -20
  22. data/app/components/primer/primer.d.ts +1 -0
  23. data/app/components/primer/primer.js +1 -0
  24. data/app/components/primer/primer.ts +1 -0
  25. data/app/components/primer/subhead_component.html.erb +1 -1
  26. data/app/components/primer/subhead_component.rb +1 -1
  27. data/app/components/primer/tooltip.rb +1 -1
  28. data/app/lib/primer/test_selector_helper.rb +1 -1
  29. data/lib/primer/classify/utilities.yml +20 -0
  30. data/lib/primer/view_components/linters/button_component_migration_counter.rb +1 -1
  31. data/lib/primer/view_components/version.rb +1 -1
  32. data/lib/rubocop/cop/primer/component_name_migration.rb +35 -0
  33. data/lib/rubocop/cop/primer/primer_octicon.rb +1 -1
  34. data/lib/tasks/docs.rake +19 -11
  35. data/lib/tasks/utilities.rake +1 -1
  36. data/static/arguments.yml +34 -1
  37. data/static/audited_at.json +2 -1
  38. data/static/classes.yml +5 -4
  39. data/static/constants.json +20 -8
  40. data/static/statuses.json +3 -2
  41. metadata +17 -11
@@ -0,0 +1,377 @@
1
+ import type {AnchorAlignment, AnchorSide} from '@primer/behaviors'
2
+ import {getAnchoredPosition} from '@primer/behaviors'
3
+
4
+ const TOOLTIP_OPEN_CLASS = 'tooltip-open'
5
+
6
+ type Direction = 'n' | 's' | 'e' | 'w' | 'ne' | 'se' | 'nw' | 'sw'
7
+
8
+ const DIRECTION_CLASSES = [
9
+ 'tooltip-n',
10
+ 'tooltip-s',
11
+ 'tooltip-e',
12
+ 'tooltip-w',
13
+ 'tooltip-ne',
14
+ 'tooltip-se',
15
+ 'tooltip-nw',
16
+ 'tooltip-sw'
17
+ ]
18
+
19
+ class TooltipElement extends HTMLElement {
20
+ styles() {
21
+ return `
22
+ :host {
23
+ position: absolute;
24
+ z-index: 1000000;
25
+ padding: .5em .75em;
26
+ font: normal normal 11px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
27
+ -webkit-font-smoothing: subpixel-antialiased;
28
+ color: var(--color-fg-on-emphasis);
29
+ text-align: center;
30
+ text-decoration: none;
31
+ text-shadow: none;
32
+ text-transform: none;
33
+ letter-spacing: normal;
34
+ word-wrap: break-word;
35
+ white-space: pre;
36
+ background: var(--color-neutral-emphasis-plus);
37
+ border-radius: 6px;
38
+ opacity: 0;
39
+ max-width: 250px;
40
+ word-wrap: break-word;
41
+ white-space: normal
42
+ }
43
+
44
+ :host:before{
45
+ position: absolute;
46
+ z-index: 1000001;
47
+ color: var(--color-neutral-emphasis-plus);
48
+ content: "";
49
+ border: 6px solid transparent;
50
+ opacity: 0
51
+ }
52
+
53
+ @keyframes tooltip-appear {
54
+ from {
55
+ opacity: 0
56
+ }
57
+ to {
58
+ opacity: 1
59
+ }
60
+ }
61
+
62
+ :host:after{
63
+ position: absolute;
64
+ display: block;
65
+ right: 0;
66
+ left: 0;
67
+ height: 12px;
68
+ content: ""
69
+ }
70
+
71
+ :host(.${TOOLTIP_OPEN_CLASS}),
72
+ :host(.${TOOLTIP_OPEN_CLASS}):before {
73
+ animation-name: tooltip-appear;
74
+ animation-duration: .1s;
75
+ animation-fill-mode: forwards;
76
+ animation-timing-function: ease-in;
77
+ animation-delay: .4s
78
+ }
79
+
80
+ :host(.tooltip-s):before,
81
+ :host(.tooltip-se):before,
82
+ :host(.tooltip-sw):before {
83
+ right: 50%;
84
+ bottom: 100%;
85
+ margin-right: -6px;
86
+ border-bottom-color: var(--color-neutral-emphasis-plus)
87
+ }
88
+
89
+ :host(.tooltip-s):after,
90
+ :host(.tooltip-se):after,
91
+ :host(.tooltip-sw):after {
92
+ bottom: 100%
93
+ }
94
+
95
+ :host(.tooltip-n):before,
96
+ :host(.tooltip-ne):before,
97
+ :host(.tooltip-nw):before {
98
+ top: 100%;
99
+ right: 50%;
100
+ margin-right: -6px;
101
+ border-top-color: var(--color-neutral-emphasis-plus)
102
+ }
103
+
104
+ :host(.tooltip-n):after,
105
+ :host(.tooltip-ne):after,
106
+ :host(.tooltip-nw):after {
107
+ top: 100%
108
+ }
109
+
110
+ :host(.tooltip-se):before,
111
+ :host(.tooltip-ne):before {
112
+ right: auto
113
+ }
114
+
115
+ :host(.tooltip-sw):before,
116
+ :host(.tooltip-nw):before {
117
+ right: 0;
118
+ margin-right: 6px
119
+ }
120
+
121
+ :host(.tooltip-w):before {
122
+ top: 50%;
123
+ bottom: 50%;
124
+ left: 100%;
125
+ margin-top: -6px;
126
+ border-left-color: var(--color-neutral-emphasis-plus)
127
+ }
128
+
129
+ :host(.tooltip-e):before {
130
+ top: 50%;
131
+ right: 100%;
132
+ bottom: 50%;
133
+ margin-top: -6px;
134
+ border-right-color: var(--color-neutral-emphasis-plus)
135
+ }
136
+ `
137
+ }
138
+
139
+ #abortController: AbortController | undefined
140
+ #align: AnchorAlignment = 'center'
141
+ #side: AnchorSide = 'outside-bottom'
142
+ #allowUpdatePosition = false
143
+
144
+ get htmlFor(): string {
145
+ return this.getAttribute('for') || ''
146
+ }
147
+
148
+ set htmlFor(value: string) {
149
+ this.setAttribute('for', value)
150
+ }
151
+
152
+ get type(): 'description' | 'label' {
153
+ const type = this.getAttribute('data-type')
154
+ return type === 'label' ? 'label' : 'description'
155
+ }
156
+
157
+ set type(value: 'description' | 'label') {
158
+ this.setAttribute('data-type', value)
159
+ }
160
+
161
+ get direction(): Direction {
162
+ return (this.getAttribute('data-direction') || 's') as Direction
163
+ }
164
+
165
+ set direction(value: Direction) {
166
+ this.setAttribute('data-direction', value)
167
+ }
168
+
169
+ get control(): HTMLElement | null {
170
+ return this.ownerDocument.getElementById(this.htmlFor)
171
+ }
172
+
173
+ constructor() {
174
+ super()
175
+ const shadow = this.attachShadow({mode: 'open'})
176
+ shadow.innerHTML = `
177
+ <style>
178
+ ${this.styles()}
179
+ </style>
180
+ <slot></slot>
181
+ `
182
+ }
183
+
184
+ connectedCallback() {
185
+ this.hidden = true
186
+ this.#allowUpdatePosition = true
187
+
188
+ if (!this.id) {
189
+ this.id = `tooltip-${Date.now()}-${(Math.random() * 10000).toFixed(0)}`
190
+ }
191
+
192
+ if (!this.control) return
193
+
194
+ this.setAttribute('role', 'tooltip')
195
+
196
+ this.#abortController?.abort()
197
+ this.#abortController = new AbortController()
198
+ const {signal} = this.#abortController
199
+
200
+ this.addEventListener('mouseleave', this, {signal})
201
+ this.control.addEventListener('mouseenter', this, {signal})
202
+ this.control.addEventListener('mouseleave', this, {signal})
203
+ this.control.addEventListener('focus', this, {signal})
204
+ this.control.addEventListener('blur', this, {signal})
205
+ this.ownerDocument.addEventListener('keydown', this, {signal})
206
+ }
207
+
208
+ disconnectedCallback() {
209
+ this.#abortController?.abort()
210
+ }
211
+
212
+ handleEvent(event: Event) {
213
+ if (!this.control) return
214
+
215
+ // Ensures that tooltip stays open when hovering between tooltip and element
216
+ // WCAG Success Criterion 1.4.13 Hoverable
217
+ if ((event.type === 'mouseenter' || event.type === 'focus') && this.hidden) {
218
+ this.hidden = false
219
+ } else if (event.type === 'blur') {
220
+ this.hidden = true
221
+ } else if (
222
+ event.type === 'mouseleave' &&
223
+ (event as MouseEvent).relatedTarget !== this.control &&
224
+ (event as MouseEvent).relatedTarget !== this
225
+ ) {
226
+ this.hidden = true
227
+ } else if (event.type === 'keydown' && (event as KeyboardEvent).key === 'Escape' && !this.hidden) {
228
+ this.hidden = true
229
+ }
230
+ }
231
+
232
+ static observedAttributes = ['data-type', 'data-direction', 'id', 'hidden']
233
+
234
+ attributeChangedCallback(name: string) {
235
+ if (name === 'id' || name === 'data-type') {
236
+ if (!this.id || !this.control) return
237
+ if (this.type === 'label') {
238
+ this.control.setAttribute('aria-labelledby', this.id)
239
+ } else {
240
+ let describedBy = this.control.getAttribute('aria-describedby')
241
+ describedBy ? (describedBy = `${describedBy} ${this.id}`) : (describedBy = this.id)
242
+ this.control.setAttribute('aria-describedby', describedBy)
243
+ }
244
+ } else if (name === 'hidden') {
245
+ if (this.hidden) {
246
+ this.classList.remove(TOOLTIP_OPEN_CLASS, ...DIRECTION_CLASSES)
247
+ } else {
248
+ this.classList.add(TOOLTIP_OPEN_CLASS)
249
+ for (const tooltip of this.ownerDocument.querySelectorAll<HTMLElement>(this.tagName)) {
250
+ if (tooltip !== this) tooltip.hidden = true
251
+ }
252
+ this.#updatePosition()
253
+ }
254
+ } else if (name === 'data-direction') {
255
+ this.classList.remove(...DIRECTION_CLASSES)
256
+ const direction = this.direction
257
+ if (direction === 'n') {
258
+ this.#align = 'center'
259
+ this.#side = 'outside-top'
260
+ } else if (direction === 'ne') {
261
+ this.#align = 'start'
262
+ this.#side = 'outside-top'
263
+ } else if (direction === 'e') {
264
+ this.#align = 'center'
265
+ this.#side = 'outside-right'
266
+ } else if (direction === 'se') {
267
+ this.#align = 'start'
268
+ this.#side = 'outside-bottom'
269
+ } else if (direction === 's') {
270
+ this.#align = 'center'
271
+ this.#side = 'outside-bottom'
272
+ } else if (direction === 'sw') {
273
+ this.#align = 'end'
274
+ this.#side = 'outside-bottom'
275
+ } else if (direction === 'w') {
276
+ this.#align = 'center'
277
+ this.#side = 'outside-left'
278
+ } else if (direction === 'nw') {
279
+ this.#align = 'end'
280
+ this.#side = 'outside-top'
281
+ }
282
+ }
283
+ }
284
+
285
+ // `getAnchoredPosition` may calibrate `anchoredSide` but does not recalibrate `align`.
286
+ // Therefore, we need to determine which `align` is best based on the initial `getAnchoredPosition` calcluation.
287
+ // Related: https://github.com/primer/behaviors/issues/63
288
+ #adjustedAnchorAlignment(anchorSide: AnchorSide): AnchorAlignment | undefined {
289
+ if (!this.control) return
290
+
291
+ const tooltipPosition = this.getBoundingClientRect()
292
+ const targetPosition = this.control.getBoundingClientRect()
293
+ const tooltipWidth = tooltipPosition.width
294
+
295
+ const tooltipCenter = tooltipPosition.left + tooltipWidth / 2
296
+ const targetCenter = targetPosition.x + targetPosition.width / 2
297
+
298
+ if (Math.abs(tooltipCenter - targetCenter) < 2 || anchorSide === 'outside-left' || anchorSide === 'outside-right') {
299
+ return 'center'
300
+ } else if (tooltipPosition.left === targetPosition.left) {
301
+ return 'start'
302
+ } else if (tooltipPosition.right === targetPosition.right) {
303
+ return 'end'
304
+ } else if (tooltipCenter < targetCenter) {
305
+ if (tooltipPosition.left === 0) return 'start'
306
+ return 'end'
307
+ } else {
308
+ if (tooltipPosition.right === 0) return 'end'
309
+ return 'start'
310
+ }
311
+ }
312
+
313
+ #updatePosition() {
314
+ if (!this.control) return
315
+ if (!this.#allowUpdatePosition || this.hidden) return
316
+
317
+ const TOOLTIP_OFFSET = 10
318
+
319
+ this.style.left = `0px` // Ensures we have reliable tooltip width in `getAnchoredPosition`
320
+ let position = getAnchoredPosition(this, this.control, {
321
+ side: this.#side,
322
+ align: this.#align,
323
+ anchorOffset: TOOLTIP_OFFSET
324
+ })
325
+ let anchorSide = position.anchorSide
326
+
327
+ // We need to set tooltip position in order to determine ideal align.
328
+ this.style.top = `${position.top}px`
329
+ this.style.left = `${position.left}px`
330
+ let direction: Direction = 's'
331
+
332
+ const align = this.#adjustedAnchorAlignment(anchorSide)
333
+ if (!align) return
334
+
335
+ this.style.left = `0px` // Reset tooltip position again to ensure accurate width in `getAnchoredPosition`
336
+ position = getAnchoredPosition(this, this.control, {side: anchorSide, align, anchorOffset: TOOLTIP_OFFSET})
337
+ anchorSide = position.anchorSide
338
+
339
+ this.style.top = `${position.top}px`
340
+ this.style.left = `${position.left}px`
341
+
342
+ if (anchorSide === 'outside-left') {
343
+ direction = 'w'
344
+ } else if (anchorSide === 'outside-right') {
345
+ direction = 'e'
346
+ } else if (anchorSide === 'outside-top') {
347
+ if (align === 'center') {
348
+ direction = 'n'
349
+ } else if (align === 'start') {
350
+ direction = 'ne'
351
+ } else {
352
+ direction = 'nw'
353
+ }
354
+ } else {
355
+ if (align === 'center') {
356
+ direction = 's'
357
+ } else if (align === 'start') {
358
+ direction = 'se'
359
+ } else {
360
+ direction = 'sw'
361
+ }
362
+ }
363
+
364
+ this.classList.add(`tooltip-${direction}`)
365
+ }
366
+ }
367
+
368
+ if (!window.customElements.get('tool-tip')) {
369
+ window.TooltipElement = TooltipElement
370
+ window.customElements.define('tool-tip', TooltipElement)
371
+ }
372
+
373
+ declare global {
374
+ interface Window {
375
+ TooltipElement: typeof TooltipElement
376
+ }
377
+ }
@@ -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,14 @@
1
- <%= label %>
2
1
  <%= render Primer::BaseComponent.new(**@system_arguments) do %>
3
- <%= input %>
4
- <%= icon %>
2
+ <label for="<%= @input_id %>">
3
+ <% if @is_label_visible %>
4
+ <%= @label_text %>
5
+ <% else %>
6
+ <span class="sr-only"><%= @label_text %></span>
7
+ <% end %>
8
+ <% if icon.present? %>
9
+ <%= icon %>
10
+ <% end %>
11
+ </label>
12
+ <input id="<%= @input_id %>" name="<%= @input_id %>" type="text" class="form-control" autocomplete="off">
5
13
  <%= results %>
6
14
  <% end %>
@@ -7,41 +7,14 @@ 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
16
 
18
- # Optionally render a visible label. See <%= link_to_accessibility %>
19
- #
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
17
  # Optional icon to be rendered before the input. Has the same arguments as <%= link_to_component(Primer::OcticonComponent) %>.
44
- #
45
18
  renders_one :icon, Primer::OcticonComponent
46
19
 
47
20
  # Customizable results list.
@@ -56,34 +29,17 @@ module Primer
56
29
  system_arguments[:classes]
57
30
  )
58
31
 
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
32
  Primer::BaseComponent.new(**system_arguments)
64
33
  }
65
34
 
66
35
  # @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 %>
36
+ # <%= render(Primer::Beta::AutoComplete.new(label_text: "Fruits", src: "/auto_complete", input_id: "fruits-input-1", list_id: "fruits-popup-1", position: :relative)) %>
71
37
  #
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 %>
76
- #
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") %>
81
- # <% end %>
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)) %>
82
40
  #
83
- # @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) %>
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| %>
87
43
  # <% c.results(classes: "custom-class") do %>
88
44
  # <%= render(Primer::Beta::AutoComplete::Item.new(selected: true, value: "apple")) do |c| %>
89
45
  # Apple
@@ -95,22 +51,25 @@ module Primer
95
51
  # <% end %>
96
52
  #
97
53
  # @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) %>
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| %>
101
55
  # <% c.icon(icon: :search) %>
102
56
  # <% end %>
103
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
+ # @param label_text [String] The label of the input.
104
63
  # @param src [String] The route to query.
105
64
  # @param input_id [String] Id of the input element.
106
65
  # @param list_id [String] Id of the list element.
66
+ # @param is_label_visible [Boolean] Controls if the label is visible. If `false`, screen reader only text will be added.
107
67
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
108
- def initialize(src:, list_id:, input_id:, **system_arguments)
68
+ def initialize(label_text:, src:, list_id:, input_id:, is_label_visible: true, **system_arguments)
69
+ @label_text = label_text
109
70
  @list_id = list_id
110
71
  @input_id = input_id
111
- @aria_label = aria("label", system_arguments)
112
-
113
- system_arguments.delete(:"aria-label") && system_arguments[:aria]&.delete(:label)
72
+ @is_label_visible = is_label_visible
114
73
 
115
74
  @system_arguments = deny_tag_argument(**system_arguments)
116
75
  @system_arguments[:tag] = "auto-complete"
@@ -118,44 +77,10 @@ module Primer
118
77
  @system_arguments[:for] = list_id
119
78
  end
120
79
 
121
- # add `results` without needing to explicitly call it in the view
80
+ # add `results` without needing to explicitly call them in the view
122
81
  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
82
  results(classes: "") unless results
127
83
  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
158
- end
159
84
  end
160
85
  end
161
86
  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 %>
@@ -5,9 +5,11 @@ module Primer
5
5
  # Use `Blankslate` when there is a lack of content within a page or section. Use as placeholder to tell users why something isn't there.
6
6
  #
7
7
  # @accessibility
8
- # - Set the `heading` level based on what is appropriate for your page hierarchy. <%= link_to_heading_practices %>
9
- # - `secondary_action` can be set to provide more information that is relevant in the context of the `Blankslate`.
8
+ # - The blankslate uses a semantic heading that must be set at the appropriate level based on the hierarchy of the page.
9
+ # - All blankslate visuals have been programmed as decorative images, using `aria-hidden=”true”` and `img alt=””`, which will hide the visual from screen reader users.
10
+ # - The blankslate supports a primary and secondary action. Both actions have been built as semantic links with primary and secondary styling.
10
11
  # - `secondary_action` text should be meaningful out of context and clearly describe the destination. Avoid using vague text like, "Learn more" or "Click here".
12
+ # - The blankslate can leverage the spinner component, which will communicate to screen reader users that the content is still loading.
11
13
  class Blankslate < Primer::Component
12
14
  status :beta
13
15
 
@@ -24,20 +26,19 @@ module Primer
24
26
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
25
27
  renders_one :visual, types: {
26
28
  icon: lambda { |**system_arguments|
27
- system_arguments[:mb] = 3
28
29
  system_arguments[:size] ||= :medium
29
30
  system_arguments[:classes] = class_names("blankslate-icon", system_arguments[:classes])
30
31
 
31
32
  Primer::OcticonComponent.new(**system_arguments)
32
33
  },
33
34
  spinner: lambda { |**system_arguments|
34
- system_arguments[:mb] = 3
35
+ system_arguments[:classes] = class_names("blankslate-image", system_arguments[:classes])
35
36
 
36
37
  Primer::SpinnerComponent.new(**system_arguments)
37
38
  },
38
39
  image: lambda { |**system_arguments|
39
- system_arguments[:mb] = 3
40
40
  system_arguments[:size] = "56x56"
41
+ system_arguments[:classes] = class_names("blankslate-image", system_arguments[:classes])
41
42
 
42
43
  Primer::Image.new(**system_arguments)
43
44
  }
@@ -50,8 +51,7 @@ module Primer
50
51
  renders_one :heading, lambda { |tag:, **system_arguments|
51
52
  deny_tag_argument(**system_arguments)
52
53
  system_arguments[:tag] = tag
53
- system_arguments[:mb] = 1
54
- system_arguments[:classes] = class_names("h2", system_arguments[:classes])
54
+ system_arguments[:classes] = class_names("blankslate-heading", system_arguments[:classes])
55
55
 
56
56
  Primer::HeadingComponent.new(**system_arguments)
57
57
  }
@@ -64,7 +64,7 @@ module Primer
64
64
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
65
65
  renders_one :description, lambda { |**system_arguments|
66
66
  deny_tag_argument(**system_arguments)
67
- system_arguments[:tag] = :div
67
+ system_arguments[:tag] = :p
68
68
 
69
69
  Primer::BaseComponent.new(**system_arguments)
70
70
  }
@@ -80,7 +80,6 @@ module Primer
80
80
  deny_tag_argument(**system_arguments)
81
81
  system_arguments[:tag] = :a
82
82
  system_arguments[:href] = href
83
- system_arguments[:mt] = 5
84
83
  system_arguments[:size] = :medium
85
84
  system_arguments[:scheme] ||= :primary
86
85
 
@@ -96,8 +95,6 @@ module Primer
96
95
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
97
96
  renders_one :secondary_action, lambda { |href:, **system_arguments|
98
97
  system_arguments[:href] = href
99
- # Padding is needed to increase touch area.
100
- system_arguments[:p] = 3
101
98
 
102
99
  Primer::LinkComponent.new(**system_arguments)
103
100
  }
@@ -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
  )