primer_view_components 0.0.118 → 0.0.120

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  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/assets/styles/primer_view_components.css +1 -1
  6. data/app/assets/styles/primer_view_components.css.map +1 -1
  7. data/app/components/primer/alpha/action_list/item.rb +1 -1
  8. data/app/components/primer/alpha/action_list.rb +1 -1
  9. data/app/components/primer/alpha/dialog.rb +2 -2
  10. data/app/components/primer/alpha/modal_dialog.js +2 -0
  11. data/app/components/primer/alpha/modal_dialog.ts +2 -0
  12. data/app/components/primer/alpha/text_field.css +1 -1
  13. data/app/components/primer/alpha/text_field.css.json +1 -1
  14. data/app/components/primer/alpha/text_field.css.map +1 -1
  15. data/app/components/primer/alpha/text_field.pcss +6 -0
  16. data/app/components/primer/alpha/toggle_switch.css +1 -1
  17. data/app/components/primer/alpha/toggle_switch.css.json +1 -1
  18. data/app/components/primer/alpha/toggle_switch.css.map +1 -1
  19. data/app/components/primer/alpha/toggle_switch.html.erb +4 -2
  20. data/app/components/primer/alpha/toggle_switch.js +23 -11
  21. data/app/components/primer/alpha/toggle_switch.pcss +6 -0
  22. data/app/components/primer/alpha/toggle_switch.ts +27 -11
  23. data/app/components/primer/alpha/tool_tip.js +0 -3
  24. data/app/components/primer/alpha/tool_tip.ts +0 -4
  25. data/app/components/primer/alpha/tooltip.rb +4 -3
  26. data/app/components/primer/beta/icon_button.rb +1 -1
  27. data/app/components/primer/component.rb +4 -0
  28. data/app/components/primer/icon_button.rb +1 -1
  29. data/app/components/primer/primer.d.ts +1 -0
  30. data/app/components/primer/primer.js +1 -0
  31. data/app/components/primer/primer.ts +1 -0
  32. data/app/forms/example_toggle_switch_form.rb +1 -1
  33. data/lib/primer/deprecations.yml +0 -13
  34. data/lib/primer/forms/dsl/input.rb +1 -1
  35. data/lib/primer/forms/toggle_switch.html.erb +7 -2
  36. data/lib/primer/forms/toggle_switch.rb +2 -4
  37. data/lib/primer/forms/toggle_switch_input.d.ts +5 -0
  38. data/lib/primer/forms/toggle_switch_input.js +29 -0
  39. data/lib/primer/forms/toggle_switch_input.ts +19 -0
  40. data/lib/primer/view_components/linters/button_component_migration_counter.rb +2 -2
  41. data/lib/primer/view_components/version.rb +1 -1
  42. data/lib/primer/view_components.rb +5 -0
  43. data/lib/primer/yard/backend.rb +38 -0
  44. data/lib/primer/yard/component_manifest.rb +123 -0
  45. data/lib/primer/yard/docs_helper.rb +81 -0
  46. data/lib/primer/yard/legacy_gatsby_backend.rb +271 -0
  47. data/lib/primer/yard/registry.rb +146 -0
  48. data/lib/primer/yard/renders_many_handler.rb +23 -0
  49. data/lib/primer/yard/renders_one_handler.rb +23 -0
  50. data/lib/rubocop/config/default.yml +3 -0
  51. data/lib/rubocop/cop/primer/test_selector.rb +48 -0
  52. data/lib/tasks/docs.rake +37 -405
  53. data/previews/primer/alpha/dialog_preview/body_has_scrollbar_overflow.html.erb +9 -0
  54. data/previews/primer/alpha/dialog_preview.rb +15 -0
  55. data/previews/primer/alpha/tooltip_preview.rb +8 -8
  56. data/previews/primer/beta/clipboard_copy_preview.rb +1 -1
  57. data/previews/primer/forms/forms_preview/example_toggle_switch_form.html.erb +3 -1
  58. data/static/arguments.json +2 -34
  59. data/static/audited_at.json +0 -3
  60. data/static/constants.json +0 -20
  61. data/static/statuses.json +0 -3
  62. metadata +20 -29
  63. data/app/components/primer/box_component.rb +0 -7
  64. data/app/components/primer/clipboard_copy.rb +0 -7
  65. data/app/components/primer/dropdown_menu_component.html.erb +0 -8
  66. data/app/components/primer/dropdown_menu_component.rb +0 -58
  67. data/lib/yard/docs_helper.rb +0 -79
  68. data/lib/yard/renders_many_handler.rb +0 -19
  69. data/lib/yard/renders_one_handler.rb +0 -19
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Naming/MethodParameterName
4
+
5
+ # :nocov:
6
+
7
+ require "primer/yard/component_manifest"
8
+ require "primer/yard/backend"
9
+
10
+ module Primer
11
+ module YARD
12
+ # Backend that generates documentation for the legacy, Gatsby-powered PVC docsite.
13
+ class LegacyGatsbyBackend < Backend
14
+ class << self
15
+ def parse_example_tag(tag)
16
+ name = tag.name
17
+ description = nil
18
+ code = nil
19
+
20
+ if tag.text.include?("@description")
21
+ splitted = tag.text.split(/@description|@code/)
22
+ description = splitted.second.gsub(/^[ \t]{2}/, "").strip
23
+ code = splitted.last.gsub(/^[ \t]{2}/, "").strip
24
+ else
25
+ code = tag.text
26
+ end
27
+
28
+ [name, description, code]
29
+ end
30
+ end
31
+
32
+ attr_reader :registry
33
+
34
+ def initialize(registry)
35
+ @registry = registry
36
+ end
37
+
38
+ def generate
39
+ args_for_components = []
40
+ errors = []
41
+
42
+ each_component do |component|
43
+ docs = registry.find(component)
44
+ status_path = docs.status_module.nil? ? "" : "#{docs.status_module}/"
45
+
46
+ metadata = docs.metadata.merge(
47
+ source: source_url(component),
48
+ lookbook: lookbook_url(component),
49
+ path: "docs/content/components/#{status_path}#{docs.short_name.downcase}.md",
50
+ example_path: example_path(component),
51
+ require_js_path: require_js_path(component)
52
+ )
53
+
54
+ path = Pathname.new(metadata[:path])
55
+ path.dirname.mkpath unless path.dirname.exist?
56
+
57
+ File.open(path, "w") do |f|
58
+ f.puts("---")
59
+ f.puts("title: #{metadata[:title]}")
60
+ f.puts("componentId: #{metadata[:component_id]}")
61
+ f.puts("status: #{metadata[:status]}")
62
+ f.puts("source: #{metadata[:source]}")
63
+ f.puts("a11yReviewed: #{metadata[:a11y_reviewed]}")
64
+ f.puts("lookbook: #{metadata[:lookbook]}") if preview_exists?(component)
65
+ f.puts("---")
66
+ f.puts
67
+ f.puts("import Example from '#{metadata[:example_path]}'")
68
+
69
+ if docs.requires_js?
70
+ f.puts("import RequiresJSFlash from '#{metadata[:require_js_path]}'")
71
+ f.puts
72
+ f.puts("<RequiresJSFlash />")
73
+ end
74
+
75
+ f.puts
76
+ f.puts("<!-- Warning: AUTO-GENERATED file, do not edit. Add code comments to your Ruby instead <3 -->")
77
+ f.puts
78
+ f.puts(view_context.render(inline: docs.base_docstring))
79
+
80
+ if docs.tags(:deprecated).any?
81
+ f.puts
82
+ f.puts("## Deprecation")
83
+ docs.tags(:deprecated).each do |tag|
84
+ f.puts
85
+ f.puts view_context.render(inline: tag.text)
86
+ end
87
+ end
88
+
89
+ if docs.tags(:accessibility).any?
90
+ f.puts
91
+ f.puts("## Accessibility")
92
+ docs.tags(:accessibility).each do |tag|
93
+ f.puts
94
+ f.puts view_context.render(inline: tag.text)
95
+ end
96
+ end
97
+
98
+ errors << { component.name => { arguments: "No argument documentation found" } } unless docs.params.any?
99
+
100
+ f.puts
101
+ f.puts("## Arguments")
102
+ f.puts
103
+ f.puts("| Name | Type | Default | Description |")
104
+ f.puts("| :- | :- | :- | :- |")
105
+
106
+ documented_params = docs.params.map(&:name)
107
+ component_params = component.instance_method(:initialize).parameters.map { |p| p.last.to_s }
108
+
109
+ if (documented_params & component_params).size != component_params.size
110
+ err = { arguments: {} }
111
+ (component_params - documented_params).each do |arg|
112
+ err[:arguments][arg] = "Not documented"
113
+ end
114
+
115
+ errors << { component.name => err }
116
+ end
117
+
118
+ args = []
119
+ docs.params.each do |tag|
120
+ default_value = pretty_default_value(tag, component)
121
+
122
+ args << {
123
+ "name" => tag.name,
124
+ "type" => tag.types.join(", "),
125
+ "default" => default_value,
126
+ "description" => view_context.render(inline: tag.text.squish)
127
+ }
128
+
129
+ f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{default_value} | #{view_context.render(inline: tag.text.squish)} |")
130
+ end
131
+
132
+ component_args = {
133
+ "component" => metadata[:title],
134
+ "status" => component.status.to_s,
135
+ "source" => metadata[:source],
136
+ "lookbook" => metadata[:lookbook],
137
+ "parameters" => args
138
+ }
139
+
140
+ args_for_components << component_args
141
+
142
+ if docs.slot_methods.any?
143
+ f.puts
144
+ f.puts("## Slots")
145
+
146
+ docs.slot_methods.each do |slot_docs|
147
+ emit_method(slot_docs, component, f)
148
+ end
149
+ end
150
+
151
+ example_tags = docs.constructor.tags(:example)
152
+
153
+ if example_tags.any?
154
+ f.puts
155
+ f.puts("## Examples")
156
+
157
+ example_tags.each do |tag|
158
+ name, description, code = parse_example_tag(tag)
159
+ f.puts
160
+ f.puts("### #{name}")
161
+ if description
162
+ f.puts
163
+ f.puts(view_context.render(inline: description.squish))
164
+ end
165
+ f.puts
166
+ html = view_context.render(inline: code)
167
+ f.puts("<Example src=\"#{html.tr('"', "\'").delete("\n")}\" />")
168
+ f.puts
169
+ f.puts("```erb")
170
+ f.puts(code.to_s)
171
+ f.puts("```")
172
+ end
173
+ elsif manifest.components_with_examples.include?(component)
174
+ errors << { component.name => { example: "No examples found" } }
175
+ end
176
+ end
177
+ end
178
+
179
+ # Build system arguments docs from BaseComponent
180
+ system_args_docs = registry.find(Primer::BaseComponent)
181
+
182
+ File.open("docs/content/system-arguments.md", "w") do |f|
183
+ f.puts("---")
184
+ f.puts("title: System arguments")
185
+ f.puts("---")
186
+ f.puts
187
+ f.puts("<!-- Warning: AUTO-GENERATED file, do not edit. Add code comments to your Ruby instead <3 -->")
188
+ f.puts
189
+ f.puts(system_args_docs.base_docstring)
190
+ f.puts
191
+
192
+ f.puts(view_context.render(inline: system_args_docs.constructor.base_docstring))
193
+ end
194
+
195
+ [args_for_components, errors]
196
+ end
197
+
198
+ private
199
+
200
+ def emit_method(method_docs, component, f)
201
+ f.puts
202
+ f.puts("### `#{method_docs.name}`")
203
+
204
+ if method_docs.base_docstring.to_s.present?
205
+ f.puts
206
+ f.puts(view_context.render(inline: method_docs.base_docstring))
207
+ end
208
+
209
+ param_tags = method_docs.tags(:param)
210
+ if param_tags.any?
211
+ f.puts
212
+ f.puts("| Name | Type | Default | Description |")
213
+ f.puts("| :- | :- | :- | :- |")
214
+ end
215
+
216
+ param_tags.each do |tag|
217
+ f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{pretty_default_value(tag, component)} | #{view_context.render(inline: tag.text)} |")
218
+ end
219
+ end
220
+
221
+ def each_component(&block)
222
+ manifest.components_with_docs.sort_by(&:name).each(&block)
223
+ end
224
+
225
+ def manifest
226
+ Primer::YARD::ComponentManifest
227
+ end
228
+
229
+ def source_url(component)
230
+ path = component.name.split("::").map(&:underscore).join("/")
231
+
232
+ "https://github.com/primer/view_components/tree/main/app/components/#{path}.rb"
233
+ end
234
+
235
+ def lookbook_url(component)
236
+ path = component.name.underscore.gsub("_component", "")
237
+
238
+ "https://primer.style/view-components/lookbook/inspect/#{path}/default/"
239
+ end
240
+
241
+ def example_path(component)
242
+ example_path = "../../src/@primer/gatsby-theme-doctocat/components/example"
243
+ example_path = "../#{example_path}" if status_module?(component)
244
+ example_path
245
+ end
246
+
247
+ def require_js_path(component)
248
+ require_js_path = "../../src/@primer/gatsby-theme-doctocat/components/requires-js-flash"
249
+ require_js_path = "../#{require_js_path}" if status_module?(component)
250
+ require_js_path
251
+ end
252
+
253
+ def preview_exists?(component)
254
+ path = component.name.underscore
255
+
256
+ File.exist?("previews/#{path}_preview.rb")
257
+ end
258
+
259
+ def status_module?(component)
260
+ (%w[Alpha Beta] & component.name.split("::")).any?
261
+ end
262
+
263
+ def parse_example_tag(tag)
264
+ self.class.parse_example_tag(tag)
265
+ end
266
+ end
267
+ end
268
+ end
269
+ # :nocov:
270
+
271
+ # rubocop:enable Naming/MethodParameterName
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nocov:
4
+
5
+ require "primer/view_components"
6
+ require "primer/yard/docs_helper"
7
+ require "view_component/test_helpers"
8
+
9
+ module Primer
10
+ module YARD
11
+ # A wrapper around a YARD class reference that provides convenience methods
12
+ # for extracting component parameters, accessibility status, etc.
13
+ class RegistryEntry
14
+ include DocsHelper
15
+
16
+ attr_reader :component, :docs
17
+
18
+ delegate_missing_to :docs
19
+
20
+ def initialize(component, docs)
21
+ @component = component
22
+ @docs = docs
23
+ end
24
+
25
+ def metadata
26
+ @metadata ||= begin
27
+ status_module, short_name, class_name = status_module_and_short_name(component)
28
+ status = component.status.to_s
29
+ a11y_reviewed = component.audited_at.nil? ? "false" : "true"
30
+
31
+ {
32
+ title: class_name,
33
+ component_id: short_name.underscore,
34
+ status: status.capitalize,
35
+ status_module: status_module,
36
+ short_name: short_name,
37
+ a11y_reviewed: a11y_reviewed
38
+ }
39
+ end
40
+ end
41
+
42
+ def constructor
43
+ docs.meths.find(&:constructor?)
44
+ end
45
+
46
+ def params
47
+ constructor.tags(:param)
48
+ end
49
+
50
+ def slot_methods
51
+ public_methods.select { |mtd| slot_method?(mtd) }
52
+ end
53
+
54
+ def non_slot_methods
55
+ public_methods.reject { |mtd| slot_method?(mtd) }
56
+ end
57
+
58
+ def slot_method?(mtd)
59
+ mtd[:renders_one] || mtd[:renders_many]
60
+ end
61
+
62
+ def public_methods
63
+ # Returns: only public methods that belong to this class (i.e. no inherited methods)
64
+ # excluding the constructor
65
+ @public_methods ||=
66
+ docs.meths.reject { |mtd| mtd.tag(:private) || mtd.name == :initialize }
67
+ end
68
+
69
+ def title
70
+ metadata[:title]
71
+ end
72
+
73
+ def component_id
74
+ metadata[:component_id]
75
+ end
76
+
77
+ def status
78
+ metadata[:status]
79
+ end
80
+
81
+ def status_module
82
+ metadata[:status_module]
83
+ end
84
+
85
+ def short_name
86
+ metadata[:short_name]
87
+ end
88
+
89
+ def a11y_reviewed?
90
+ metadata[:a11y_reviewed]
91
+ end
92
+
93
+ def requires_js?
94
+ manifest.components_requiring_js.include?(component)
95
+ end
96
+
97
+ def includes_examples?
98
+ manifest.components_with_examples.include?(component)
99
+ end
100
+
101
+ private
102
+
103
+ def manifest
104
+ Primer::YARD::ComponentManifest
105
+ end
106
+ end
107
+
108
+ # Wrapper around an instance of YARD::Registry that provides easy access to component
109
+ # documentation.
110
+ class Registry
111
+ class << self
112
+ include ViewComponent::TestHelpers
113
+ include Primer::ViewHelper
114
+ include Primer::YARD::DocsHelper
115
+
116
+ def make
117
+ registry = ::YARD::RegistryStore.new
118
+ registry.load!(".yardoc")
119
+
120
+ new(registry)
121
+ end
122
+ end
123
+
124
+ attr_reader :yard_registry
125
+
126
+ def initialize(yard_registry)
127
+ @yard_registry = yard_registry
128
+ end
129
+
130
+ def find(component)
131
+ return entries[component] if entries.include?(component)
132
+
133
+ return unless (docs = yard_registry.get(component.name))
134
+
135
+ entries[component] = RegistryEntry.new(component, docs)
136
+ end
137
+
138
+ private
139
+
140
+ def entries
141
+ @entries ||= {}
142
+ end
143
+ end
144
+ end
145
+ end
146
+ # :nocov:
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nocov:
4
+ module Primer
5
+ module YARD
6
+ # YARD Handler to parse `renders_many` calls.
7
+ class RendersManyHandler < ::YARD::Handlers::Ruby::Base
8
+ handles method_call(:renders_many)
9
+ namespace_only
10
+
11
+ process do
12
+ name = statement.parameters.first.jump(:tstring_content, :ident).source
13
+ object = ::YARD::CodeObjects::MethodObject.new(namespace, name)
14
+ register(object)
15
+ parse_block(statement.last, owner: object)
16
+
17
+ object.dynamic = true
18
+ object[:renders_many] = true
19
+ end
20
+ end
21
+ end
22
+ end
23
+ # :nocov:
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nocov:
4
+ module Primer
5
+ module YARD
6
+ # YARD Handler to parse `renders_one` calls.
7
+ class RendersOneHandler < ::YARD::Handlers::Ruby::Base
8
+ handles method_call(:renders_one)
9
+ namespace_only
10
+
11
+ process do
12
+ name = statement.parameters.first.jump(:tstring_content, :ident).source
13
+ object = ::YARD::CodeObjects::MethodObject.new(namespace, name)
14
+ register(object)
15
+ parse_block(statement.last, owner: object)
16
+
17
+ object.dynamic = true
18
+ object[:renders_one] = true
19
+ end
20
+ end
21
+ end
22
+ end
23
+ # :nocov:
@@ -15,3 +15,6 @@ Primer/DeprecatedArguments:
15
15
 
16
16
  Primer/DeprecatedComponents:
17
17
  Enabled: true
18
+
19
+ Primer/TestSelector:
20
+ Enabled: true
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ # :nocov:
6
+ module RuboCop
7
+ module Cop
8
+ module Primer
9
+ # Prefer the `test_selector` argument over manually generating
10
+ # a `data-test-selector` attribute.
11
+ #
12
+ # Bad:
13
+ #
14
+ # Primer::BaseComponent.new(data: { "test-selector": "the-component" })
15
+ #
16
+ # Good:
17
+ #
18
+ # Primer::BaseComponent.new(test_selector: "the-component")
19
+ class TestSelector < BaseCop
20
+ INVALID_MESSAGE = <<~STR
21
+ Prefer the `test_selector` argument over manually generating a `data-test-selector` attribute: https://primer.style/view-components/system-arguments.
22
+ STR
23
+
24
+ def on_send(node)
25
+ return unless valid_node?(node)
26
+ return unless node.arguments?
27
+
28
+ kwargs = node.arguments.last
29
+ return unless kwargs.type == :hash
30
+
31
+ data_arg = kwargs.pairs.find { |kwarg| kwarg.key.value == :data }
32
+ return if data_arg.nil?
33
+ return unless data_arg.value.type == :hash
34
+
35
+ hash = data_arg.child_nodes.find { |arg| arg.type == :hash }
36
+ return unless hash
37
+
38
+ test_selector = hash.pairs.find do |pair|
39
+ pair.key.value == :"test-selector" || pair.key.value == "test-selector"
40
+ end
41
+ return unless test_selector
42
+
43
+ add_offense(data_arg, message: INVALID_MESSAGE)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end