openproject-primer_view_components 0.37.1 → 0.39.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/app/assets/javascripts/app/components/primer/alpha/action_menu/action_menu_element.d.ts +0 -9
  4. data/app/assets/javascripts/app/components/primer/alpha/select_panel_element.d.ts +64 -0
  5. data/app/assets/javascripts/app/components/primer/aria_live.d.ts +8 -0
  6. data/app/assets/javascripts/app/components/primer/primer.d.ts +4 -0
  7. data/app/assets/javascripts/app/components/primer/shared_events.d.ts +9 -0
  8. data/app/assets/javascripts/primer_view_components.js +1 -1
  9. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  10. data/app/assets/styles/primer_view_components.css +1 -1
  11. data/app/assets/styles/primer_view_components.css.map +1 -1
  12. data/app/components/primer/alpha/action_list.css +1 -1
  13. data/app/components/primer/alpha/action_list.css.map +1 -1
  14. data/app/components/primer/alpha/action_list.pcss +1 -0
  15. data/app/components/primer/alpha/action_menu/action_menu_element.d.ts +0 -9
  16. data/app/components/primer/alpha/action_menu/action_menu_element.ts +0 -11
  17. data/app/components/primer/alpha/action_menu.rb +13 -6
  18. data/app/components/primer/alpha/select_panel.html.erb +100 -0
  19. data/app/components/primer/alpha/select_panel.rb +486 -0
  20. data/app/components/primer/alpha/select_panel_element.d.ts +64 -0
  21. data/app/components/primer/alpha/select_panel_element.js +927 -0
  22. data/app/components/primer/alpha/select_panel_element.ts +1049 -0
  23. data/app/components/primer/aria_live.d.ts +8 -0
  24. data/app/components/primer/aria_live.js +38 -0
  25. data/app/components/primer/aria_live.ts +41 -0
  26. data/app/components/primer/base_component.rb +1 -1
  27. data/app/components/primer/primer.d.ts +4 -0
  28. data/app/components/primer/primer.js +4 -0
  29. data/app/components/primer/primer.ts +4 -0
  30. data/app/components/primer/shared_events.d.ts +9 -0
  31. data/app/components/primer/shared_events.js +1 -0
  32. data/app/components/primer/shared_events.ts +10 -0
  33. data/app/forms/example_toggle_switch_form/example_field_caption.html.erb +1 -1
  34. data/lib/primer/forms/toggle_switch.html.erb +1 -2
  35. data/lib/primer/static/generate_info_arch.rb +3 -2
  36. data/lib/primer/view_components/version.rb +2 -2
  37. data/lib/primer/yard/component_manifest.rb +2 -0
  38. data/previews/primer/alpha/select_panel_preview/_interaction_subject_js.html.erb +25 -0
  39. data/previews/primer/alpha/select_panel_preview/eventually_local_fetch.html.erb +16 -0
  40. data/previews/primer/alpha/select_panel_preview/eventually_local_fetch_initial_failure.html.erb +12 -0
  41. data/previews/primer/alpha/select_panel_preview/eventually_local_fetch_no_results.html.erb +16 -0
  42. data/previews/primer/alpha/select_panel_preview/footer_buttons.html.erb +23 -0
  43. data/previews/primer/alpha/select_panel_preview/local_fetch.html.erb +19 -0
  44. data/previews/primer/alpha/select_panel_preview/local_fetch_no_results.html.erb +15 -0
  45. data/previews/primer/alpha/select_panel_preview/multiselect.html.erb +17 -0
  46. data/previews/primer/alpha/select_panel_preview/multiselect_form.html.erb +31 -0
  47. data/previews/primer/alpha/select_panel_preview/playground.html.erb +23 -0
  48. data/previews/primer/alpha/select_panel_preview/remote_fetch.html.erb +16 -0
  49. data/previews/primer/alpha/select_panel_preview/remote_fetch_filter_failure.html.erb +13 -0
  50. data/previews/primer/alpha/select_panel_preview/remote_fetch_initial_failure.html.erb +12 -0
  51. data/previews/primer/alpha/select_panel_preview/remote_fetch_no_results.html.erb +16 -0
  52. data/previews/primer/alpha/select_panel_preview/single_select.html.erb +20 -0
  53. data/previews/primer/alpha/select_panel_preview/single_select_form.html.erb +33 -0
  54. data/previews/primer/alpha/select_panel_preview/with_avatar_items.html.erb +19 -0
  55. data/previews/primer/alpha/select_panel_preview/with_dynamic_label.html.erb +23 -0
  56. data/previews/primer/alpha/select_panel_preview/with_dynamic_label_and_aria_prefix.html.erb +24 -0
  57. data/previews/primer/alpha/select_panel_preview/with_leading_icons.html.erb +31 -0
  58. data/previews/primer/alpha/select_panel_preview/with_subtitle.html.erb +25 -0
  59. data/previews/primer/alpha/select_panel_preview/with_trailing_icons.html.erb +19 -0
  60. data/previews/primer/alpha/select_panel_preview.rb +239 -0
  61. data/static/arguments.json +140 -0
  62. data/static/audited_at.json +2 -0
  63. data/static/constants.json +18 -0
  64. data/static/info_arch.json +949 -96
  65. data/static/previews.json +294 -0
  66. data/static/statuses.json +2 -0
  67. metadata +41 -4
@@ -0,0 +1,486 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ module Alpha
5
+ # Select panels allow for selecting from a large number of options and can be thought of as a more capable
6
+ # version of the traditional HTML `<select>` element.
7
+ #
8
+ # Select panels:
9
+ #
10
+ # 1. feature an input field at the top that allows an end user to filter the list of results.
11
+ # 1. can render their items statically or dynamically by fetching results from the server.
12
+ # 1. allow selecting a single item or multiple items.
13
+ # 1. permit leading visuals like Octicons, avatars, and custom SVGs.
14
+ # 1. can be used as form inputs in Rails forms.
15
+ #
16
+ # ## Static list items
17
+ #
18
+ # The Rails `SelectPanel` component allows items to be provided statically or loaded dynamically from the
19
+ # server. Providing items statically is done using a fetch strategy of `:local` in combination with the
20
+ # `item` slot:
21
+ #
22
+ # ```erb
23
+ # <%= render(Primer::Alpha::SelectPanel.new(fetch_strategy: :local))) do |panel| %>
24
+ # <% panel.with_show_button { "Select item" } %>
25
+ # <% panel.with_item(label: "Item 1") %>
26
+ # <% panel.with_item(label: "Item 2") %>
27
+ # <% end %>
28
+ # ```
29
+ #
30
+ # ## Dynamic list items
31
+ #
32
+ # List items can also be fetched dynamically from the server and will require creating a Rails controller action
33
+ # to respond with the list of items in addition to rendering the `SelectPanel` instance. Render the instance as
34
+ # normal, providing your desired [fetch strategy](#fetch-strategies):
35
+ #
36
+ # ```erb
37
+ # <%= render(
38
+ # Primer::Alpha::SelectPanel.new(
39
+ # fetch_strategy: :remote,
40
+ # src: search_items_path # perhaps a Rails URL helper
41
+ # )
42
+ # ) %>
43
+ # ```
44
+ #
45
+ # Define a controller action to serve the list of items. The `SelectPanel` component passes any filter text in
46
+ # the `q=` URL parameter.
47
+ #
48
+ # ```ruby
49
+ # class SearchItemsController < ApplicationController
50
+ # def show
51
+ # # NOTE: params[:q] may be nil since there is no filter string available
52
+ # # when the panel is first opened
53
+ # @results = SomeModel.search(params[:q] || "")
54
+ # end
55
+ # end
56
+ # ```
57
+ #
58
+ # Responses must be HTML fragments, eg. have a content type of `text/html+fragment`. This content type isn't
59
+ # available by default in Rails, so you may have to register it eg. in an initializer:
60
+ #
61
+ # ```ruby
62
+ # Mime::Type.register("text/fragment+html", :html_fragment)
63
+ # ```
64
+ #
65
+ # Render a `Primer::Alpha::SelectPanel::ItemList` in the action's template, search_items/show.html_fragment.erb:
66
+ #
67
+ # ```erb
68
+ # <%= render(Primer::Alpha::SelectPanel::ItemList.new) do |list| %>
69
+ # <% @results.each do |result| %>
70
+ # <% list.with_item(label: result.title) do |item| %>
71
+ # <% item.with_description(result.description) %>
72
+ # <% end %>
73
+ # <% end %>
74
+ # <% end %>
75
+ # ```
76
+ #
77
+ # ### Selection consistency
78
+ #
79
+ # The `SelectPanel` component automatically "remembers" which items have been selected across item fetch requests,
80
+ # meaning the controller that renders dynamic list items does not (and should not) remember these selections or
81
+ # persist them until the user has confirmed them, either by submitting the form or otherwise indicating completion.
82
+ # The `SelectPanel` component does not include unconfirmed selection data in requests.
83
+ #
84
+ # ## Fetch strategies
85
+ #
86
+ # The list of items can be fetched from a remote URL, or provided as a static list, configured using the
87
+ # `fetch_strategy` attribute. Fetch strategies are summarized below.
88
+ #
89
+ # 1. `:remote`: a query is made to the URL in the `src` attribute every time the input field changes.
90
+ #
91
+ # 2. `:eventually_local`: a query is made to the URL in the `src` attribute when the panel is first opened. The
92
+ # results are "remembered" and filtered in-memory for all subsequent filter operations, i.e. when the input
93
+ # field changes.
94
+ #
95
+ # 3. `:local`: the list of items is provided statically ahead of time and filtered in-memory. No requests are made
96
+ # to the server.
97
+ #
98
+ # ## Customizing filter behavior
99
+ #
100
+ # If the fetch strategy is `:remote`, then filtering is handled server-side. The server should render a
101
+ # `Primer::Alpha::SelectPanel::ItemList` (an alias of <%= link_to_component(Primer::Alpha::ActionList) %>)
102
+ # in the response containing the filtered list of items. The component achieves remote fetching via the
103
+ # [remote-input-element](https://github.com/github/remote-input-element), which sends a request to the
104
+ # server with the filter string in the `q=` parameter. Responses must be HTML fragments, eg. have a content
105
+ # type of `text/html+fragment`.
106
+ #
107
+ # ### Local filtering
108
+ #
109
+ # If the fetch strategy is `:local` or `:eventually_local`, filtering is performed client-side. Filter behavior can
110
+ # be customized in JavaScript by setting the `filterFn` attribute on the instance of `SelectPanelElement`, eg:
111
+ #
112
+ # ```javascript
113
+ # document.querySelector("select-panel").filterFn = (item: HTMLElement, query: string): boolean => {
114
+ # // return true if the item should be displayed, false otherwise
115
+ # }
116
+ # ```
117
+ #
118
+ # The element's default filter function uses the value of the `data-filter-string` attribute, falling back to the
119
+ # element's `innerText` property. It performs a case-insensitive substring match against the filter string.
120
+ #
121
+ # ### `SelectPanel`s as form inputs
122
+ #
123
+ # `SelectPanel`s can be used as form inputs. They behave very similarly to how HTML `<select>` boxes behave, and
124
+ # play nicely with Rails' built-in form mechanisms. Pass arguments via the `form_arguments:` argument, including
125
+ # the Rails form builder object and the name of the field. Each list item must also have a value specified in
126
+ # `content_arguments: { data: { value: } }`.
127
+ #
128
+ # ```erb
129
+ # <% form_with(model: Address.new) do |f| %>
130
+ # <%= render(Primer::Alpha::SelectPanel.new(form_arguments: { builder: f, name: "country" })) do |menu| %>
131
+ # <% countries.each do |country|
132
+ # <% menu.with_item(label: country.name, content_arguments: { data: { value: country.code } }) %>
133
+ # <% end %>
134
+ # <% end %>
135
+ # <% end %>
136
+ # ```
137
+ #
138
+ # The value of the `data: { value: ... }` argument is sent to the server on submit, keyed using the name provided above
139
+ # (eg. `"country"`). If no value is provided for an item, the value of that item is the item's label. Here's the
140
+ # corresponding `AddressesController` that might be written to handle the form above:
141
+ #
142
+ # ```ruby
143
+ # class AddressesController < ApplicationController
144
+ # def create
145
+ # puts "You chose #{address_params[:country]} as your country"
146
+ # end
147
+ #
148
+ # private
149
+ #
150
+ # def address_params
151
+ # params.require(:address).permit(:country)
152
+ # end
153
+ # end
154
+ # ```
155
+ #
156
+ # If items are provided dynamically, things become a bit more complicated. The `form_for` or `form_with` method call
157
+ # happens in the view that renders the `SelectPanel`, which means the form builder object but isn't available in the
158
+ # view that renders the list items. In such a case, it can be useful to create an instance of the form builder maually:
159
+ #
160
+ # ```erb
161
+ # <% builder = ActionView::Helpers::FormBuilder.new(
162
+ # "address", # the name of the model, used to wrap input names, eg 'address[country_code]'
163
+ # nil, # object (eg. the Address instance, which we can omit)
164
+ # self, # template
165
+ # {} # options
166
+ # ) %>
167
+ # <%= render(Primer::Alpha::SelectPanel::ItemList.new(
168
+ # form_arguments: { builder: builder, name: "country" }
169
+ # )) do |list| %>
170
+ # <% countries.each do |country| %>
171
+ # <% menu.with_item(label: country.name, data: { value: country.code }) %>
172
+ # <% end %>
173
+ # <% end %>
174
+ # ```
175
+ #
176
+ # ### JavaScript API
177
+ #
178
+ # `SelectPanel`s render a `<select-panel>` custom element that exposes behavior to the client.
179
+ #
180
+ # #### Utility methods
181
+ #
182
+ # * `show()`: Manually open the panel. Under normal circumstances, a show button is used to show the panel, but this method exists to support unusual use-cases.
183
+ # * `hide()`: Manually hides (closes) the panel.
184
+ #
185
+ # #### Query methods
186
+ #
187
+ # * `getItemById(itemId: string): Element`: Returns the item's HTML `<li>` element. The return value can be passed as the `item` argument to the other methods listed below.
188
+ # * `isItemChecked(item: Element): boolean`: Returns `true` if the item is checked, `false` otherwise.
189
+ # * `isItemHidden(item: Element): boolean`: Returns `true` if the item is hidden, `false` otherwise.
190
+ # * `isItemDisabled(item: Element): boolean`: Returns `true` if the item is disabled, `false` otherwise.
191
+ #
192
+ # NOTE: Item IDs are special values provided by the user that are attached to `SelectPanel` list items as the `data-item-id`
193
+ # HTML attribute. Item IDs can be provided by passing an `item_id:` attribute when adding items to the panel, eg:
194
+ #
195
+ # ```erb
196
+ # <%= render(Primer::Alpha::SelectPanel.new) do |panel| %>
197
+ # <% panel.with_item(item_id: "my-id") %>
198
+ # <% end %>
199
+ # ```
200
+ #
201
+ # The same is true when rendering `ItemList`s:
202
+ #
203
+ # ```erb
204
+ # <%= render(Primer::Alpha::SelectPanel::ItemList.new) do |list| %>
205
+ # <% list.with_item(item_id: "my-id") %>
206
+ # <% end %>
207
+ # ```
208
+ #
209
+ # #### State methods
210
+ #
211
+ # * `enableItem(item: Element)`: Enables the item, i.e. makes it clickable by the mouse and keyboard.
212
+ # * `disableItem(item: Element)`: Disables the item, i.e. makes it unclickable by the mouse and keyboard.
213
+ # * `checkItem(item: Element)`: Checks the item. Only has an effect in single- and multi-select modes.
214
+ # * `uncheckItem(item: Element)`: Unchecks the item. Only has an effect in multi-select mode, since items cannot be unchecked in single-select mode.
215
+ #
216
+ # #### Events
217
+ #
218
+ # |Name |Type |Bubbles |Cancelable |
219
+ # |:--------------------|:------------------------------------------|:-------|:----------|
220
+ # |`itemActivated` |`CustomEvent<ItemActivatedEvent>` |Yes |No |
221
+ # |`beforeItemActivated`|`CustomEvent<ItemActivatedEvent>` |Yes |Yes |
222
+ # |`dialog:open` |`CustomEvent<{dialog: HTMLDialogElement}>` |No |No |
223
+ # |`panelClosed` |`CustomEvent<{panel: SelectPanelElement}>` |Yes |No |
224
+ #
225
+ # _Item activation_
226
+ #
227
+ # The `<select-panel>` element fires an `itemActivated` event whenever an item is activated (eg. clicked) via the mouse or keyboard.
228
+ #
229
+ # ```typescript
230
+ # document.querySelector("select-panel").addEventListener(
231
+ # "itemActivated",
232
+ # (event: CustomEvent<ItemActivatedEvent>) => {
233
+ # event.detail.item // Element: the <li> item that was activated
234
+ # event.detail.checked // boolean: whether or not the result of the activation checked the item
235
+ # }
236
+ # )
237
+ # ```
238
+ #
239
+ # The `beforeItemActivated` event fires before an item is activated. Canceling this event will prevent the item
240
+ # from being activated.
241
+ #
242
+ # ```typescript
243
+ # document.querySelector("select-panel").addEventListener(
244
+ # "beforeItemActivated",
245
+ # (event: CustomEvent<ItemActivatedEvent>) => {
246
+ # event.detail.item // Element: the <li> item that was activated
247
+ # event.detail.checked // boolean: whether or not the result of the activation checked the item
248
+ # event.preventDefault() // Cancel the event to prevent activation (eg. checking/unchecking)
249
+ # }
250
+ # )
251
+ # ```
252
+ class SelectPanel < Primer::Component
253
+ # The component that should be used to render the list of items in the body of a SelectPanel.
254
+ class ItemList < Primer::Alpha::ActionList
255
+ # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList) %>.
256
+ def initialize(**system_arguments)
257
+ select_variant = system_arguments[:select_variant] || Primer::Alpha::ActionList::DEFAULT_SELECT_VARIANT
258
+
259
+ super(
260
+ p: 2,
261
+ role: "listbox",
262
+ aria_selection_variant: select_variant == :single ? :selected : :checked,
263
+ **system_arguments
264
+ )
265
+ end
266
+ end
267
+
268
+ status :alpha
269
+
270
+ DEFAULT_PRELOAD = false
271
+
272
+ DEFAULT_FETCH_STRATEGY = :remote
273
+ FETCH_STRATEGIES = [
274
+ DEFAULT_FETCH_STRATEGY,
275
+ :eventually_local,
276
+ :local
277
+ ]
278
+
279
+ DEFAULT_SELECT_VARIANT = :single
280
+ SELECT_VARIANT_OPTIONS = [
281
+ DEFAULT_SELECT_VARIANT,
282
+ :multiple,
283
+ :none,
284
+ ].freeze
285
+
286
+ # The URL to fetch search results from.
287
+ #
288
+ # @return [String]
289
+ attr_reader :src
290
+
291
+ # The unique ID of the panel.
292
+ #
293
+ # @return [String]
294
+ attr_reader :panel_id
295
+
296
+ # The unique ID of the panel body.
297
+ #
298
+ # @return [String]
299
+ attr_reader :body_id
300
+
301
+ # <%= one_of(Primer::Alpha::ActionMenu::SELECT_VARIANT_OPTIONS) %>
302
+ #
303
+ # @return [Symbol]
304
+ attr_reader :select_variant
305
+
306
+ # <%= one_of(Primer::Alpha::SelectPanel::FETCH_STRATEGIES) %>
307
+ #
308
+ # @return [Symbol]
309
+ attr_reader :fetch_strategy
310
+
311
+ # Whether to preload search results when the page loads. If this option is false, results are loaded when the panel is opened.
312
+ #
313
+ # @return [Boolean]
314
+ attr_reader :preload
315
+
316
+ alias preload? preload
317
+
318
+ # Whether or not to show the filter input.
319
+ #
320
+ # @return [Boolean]
321
+ attr_reader :show_filter
322
+
323
+ alias show_filter? show_filter
324
+
325
+ # @param src [String] The URL to fetch search results from.
326
+ # @param title [String] The title that appears at the top of the panel.
327
+ # @param id [String] The unique ID of the panel.
328
+ # @param size [Symbol] The size of the panel. <%= one_of(Primer::Alpha::Overlay::SIZE_OPTIONS) %>
329
+ # @param select_variant [Symbol] <%= one_of(Primer::Alpha::ActionList::SELECT_VARIANT_OPTIONS) %>
330
+ # @param fetch_strategy [Symbol] <%= one_of(Primer::Alpha::SelectPanel::FETCH_STRATEGIES) %>
331
+ # @param no_results_label [String] The label to display when no results are found.
332
+ # @param preload [Boolean] Whether to preload search results when the page loads. If this option is false, results are loaded when the panel is opened.
333
+ # @param dynamic_label [Boolean] Whether or not to display the text of the currently selected item in the show button.
334
+ # @param dynamic_label_prefix [String] If provided, the prefix is prepended to the dynamic label and displayed in the show button.
335
+ # @param dynamic_aria_label_prefix [String] If provided, the prefix is prepended to the dynamic label and set as the value of the `aria-label` attribute on the show button.
336
+ # @param body_id [String] The unique ID of the panel body. If not provided, the body ID will be set to the panel ID with a "-body" suffix.
337
+ # @param list_arguments [Hash] Arguments to pass to the underlying <%= link_to_component(Primer::Alpha::ActionList) %> component. Only has an effect for the local fetch strategy.
338
+ # @param form_arguments [Hash] Form arguments to pass to the underlying <%= link_to_component(Primer::Alpha::ActionList) %> component. Only has an effect for the local fetch strategy.
339
+ # @param show_filter [Boolean] Whether or not to show the filter input.
340
+ # @param open_on_load [Boolean] Open the panel when the page loads.
341
+ # @param anchor_align [Symbol] The anchor alignment of the Overlay. <%= one_of(Primer::Alpha::Overlay::ANCHOR_ALIGN_OPTIONS) %>
342
+ # @param anchor_side [Symbol] The side to anchor the Overlay to. <%= one_of(Primer::Alpha::Overlay::ANCHOR_SIDE_OPTIONS) %>
343
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
344
+ def initialize(
345
+ src: nil,
346
+ title: "Menu",
347
+ id: self.class.generate_id,
348
+ size: :small,
349
+ select_variant: DEFAULT_SELECT_VARIANT,
350
+ fetch_strategy: DEFAULT_FETCH_STRATEGY,
351
+ no_results_label: "No results found",
352
+ preload: DEFAULT_PRELOAD,
353
+ dynamic_label: false,
354
+ dynamic_label_prefix: nil,
355
+ dynamic_aria_label_prefix: nil,
356
+ body_id: nil,
357
+ list_arguments: {},
358
+ form_arguments: {},
359
+ show_filter: true,
360
+ open_on_load: false,
361
+ anchor_align: Primer::Alpha::Overlay::DEFAULT_ANCHOR_ALIGN,
362
+ anchor_side: Primer::Alpha::Overlay::DEFAULT_ANCHOR_SIDE,
363
+ **system_arguments
364
+ )
365
+ if src.present?
366
+ url = URI(src)
367
+ query = url.query || ""
368
+ url.query = query.split("&").push("experimental=1").join("&")
369
+ @src = url
370
+ end
371
+
372
+ @panel_id = id
373
+ @body_id = body_id || "#{@panel_id}-body"
374
+ @preload = fetch_or_fallback_boolean(preload, DEFAULT_PRELOAD)
375
+ @select_variant = fetch_or_fallback(SELECT_VARIANT_OPTIONS, select_variant, DEFAULT_SELECT_VARIANT)
376
+ @fetch_strategy = fetch_or_fallback(FETCH_STRATEGIES, fetch_strategy, DEFAULT_FETCH_STRATEGY)
377
+ @no_results_label = no_results_label
378
+ @show_filter = show_filter
379
+ @dynamic_label = dynamic_label
380
+ @dynamic_label_prefix = dynamic_label_prefix
381
+ @dynamic_aria_label_prefix = dynamic_aria_label_prefix
382
+
383
+ @system_arguments = deny_tag_argument(**system_arguments)
384
+ @system_arguments[:id] = @panel_id
385
+ @system_arguments[:"anchor-align"] = fetch_or_fallback(Primer::Alpha::Overlay::ANCHOR_ALIGN_OPTIONS, anchor_align, Primer::Alpha::Overlay::DEFAULT_ANCHOR_ALIGN)
386
+ @system_arguments[:"anchor-side"] = Primer::Alpha::Overlay::ANCHOR_SIDE_MAPPINGS[fetch_or_fallback(Primer::Alpha::Overlay::ANCHOR_SIDE_OPTIONS, anchor_side, Primer::Alpha::Overlay::DEFAULT_ANCHOR_SIDE)]
387
+
388
+ @title = title
389
+ @system_arguments[:tag] = :"select-panel"
390
+ @system_arguments[:preload] = true if @src.present? && preload?
391
+
392
+ @system_arguments[:data] = merge_data(
393
+ system_arguments, {
394
+ data: { select_variant: @select_variant, fetch_strategy: @fetch_strategy, open_on_load: open_on_load }.tap do |data|
395
+ data[:dynamic_label] = dynamic_label if dynamic_label
396
+ data[:dynamic_label_prefix] = dynamic_label_prefix if dynamic_label_prefix.present?
397
+ data[:dynamic_aria_label_prefix] = dynamic_aria_label_prefix if dynamic_aria_label_prefix.present?
398
+ end
399
+ }
400
+ )
401
+
402
+ @dialog = Primer::BaseComponent.new(
403
+ id: "#{@panel_id}-dialog",
404
+ tag: :dialog,
405
+ data: { target: "select-panel.dialog" },
406
+ classes: class_names(
407
+ "Overlay",
408
+ "Overlay-whenNarrow",
409
+ Primer::Alpha::Dialog::SIZE_MAPPINGS[
410
+ fetch_or_fallback(Primer::Alpha::Dialog::SIZE_OPTIONS, size, Primer::Alpha::Dialog::DEFAULT_SIZE)
411
+ ],
412
+ ),
413
+ style: "position: absolute;",
414
+ )
415
+
416
+ @list = Primer::Alpha::SelectPanel::ItemList.new(
417
+ **list_arguments,
418
+ form_arguments: form_arguments,
419
+ id: "#{@panel_id}-list",
420
+ select_variant: @select_variant,
421
+ role: "listbox",
422
+ aria_selection_variant: @select_variant == :multiple ? :checked : :selected,
423
+ aria: {
424
+ label: "#{title} options"
425
+ },
426
+ p: 2
427
+ )
428
+ end
429
+
430
+ # @!parse
431
+ # # Adds an item to the list. Note that this method only has an effect for the local fetch strategy.
432
+ # #
433
+ # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList) %>'s `item` slot.
434
+ # def with_item(**system_arguments)
435
+ # end
436
+ #
437
+ # # Adds an avatar item to the list. Note that this method only has an effect for the local fetch strategy.
438
+ #
439
+ # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList) %>'s `item` slot.
440
+ # def with_avatar_item
441
+ # end
442
+
443
+ delegate :with_item, :with_avatar_item, to: :@list
444
+
445
+ # Renders content in a footer region below the list of items.
446
+ #
447
+ # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::Dialog::Footer) %>.
448
+ renders_one :footer, Primer::Alpha::Dialog::Footer
449
+
450
+ # Renders content underneath the title at the top of the panel.
451
+ #
452
+ # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::Dialog::Header) %>'s `subtitle` slot.
453
+ renders_one :subtitle
454
+
455
+ # Adds a show button (i.e. a button) that will open the panel when clicked.
456
+ #
457
+ # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Beta::Button) %>.
458
+ renders_one :show_button, lambda { |**system_arguments|
459
+ system_arguments[:id] = "#{@panel_id}-button"
460
+
461
+ system_arguments[:aria] = merge_aria(
462
+ system_arguments,
463
+ { aria: { controls: "#{@panel_id}-dialog" } }
464
+ )
465
+
466
+ Primer::Beta::Button.new(**system_arguments)
467
+ }
468
+
469
+ # Customizable content for the error message that appears when items are fetched for the first time. This message
470
+ # appears in place of the list of items.
471
+ # For more information, see the [documentation regarding SelectPanel error messaging](/components/selectpanel#errorwarning).
472
+ renders_one :preload_error_content
473
+
474
+ # Customizable content for the error message that appears when items are fetched as the result of a filter
475
+ # operation. This message appears as a banner above the previously fetched list of items.
476
+ # For more information, see the [documentation regarding SelectPanel error messaging](/components/selectpanel#errorwarning).
477
+ renders_one :error_content
478
+
479
+ private
480
+
481
+ def before_render
482
+ content
483
+ end
484
+ end
485
+ end
486
+ end
@@ -0,0 +1,64 @@
1
+ import { IncludeFragmentElement } from '@github/include-fragment-element';
2
+ import type { AnchorAlignment, AnchorSide } from '@primer/behaviors';
3
+ import '@oddbird/popover-polyfill';
4
+ type SelectVariant = 'none' | 'single' | 'multiple' | null;
5
+ type SelectedItem = {
6
+ label: string | null | undefined;
7
+ value: string | null | undefined;
8
+ inputName: string | null | undefined;
9
+ element: SelectPanelItem;
10
+ };
11
+ export type SelectPanelItem = HTMLLIElement;
12
+ export type FilterFn = (item: SelectPanelItem, query: string) => boolean;
13
+ export declare class SelectPanelElement extends HTMLElement {
14
+ #private;
15
+ includeFragment: IncludeFragmentElement;
16
+ dialog: HTMLDialogElement;
17
+ filterInputTextField: HTMLInputElement;
18
+ remoteInput: HTMLElement;
19
+ list: HTMLElement;
20
+ ariaLiveContainer: HTMLElement;
21
+ noResults: HTMLElement;
22
+ fragmentErrorElement: HTMLElement;
23
+ bannerErrorElement: HTMLElement;
24
+ bodySpinner: HTMLElement;
25
+ filterFn?: FilterFn;
26
+ get open(): boolean;
27
+ get selectVariant(): SelectVariant;
28
+ get ariaSelectionType(): string;
29
+ set selectVariant(variant: SelectVariant);
30
+ get dynamicLabelPrefix(): string;
31
+ get dynamicAriaLabelPrefix(): string;
32
+ set dynamicLabelPrefix(value: string);
33
+ get dynamicLabel(): boolean;
34
+ set dynamicLabel(value: boolean);
35
+ get invokerElement(): HTMLButtonElement | null;
36
+ get closeButton(): HTMLButtonElement | null;
37
+ get invokerLabel(): HTMLElement | null;
38
+ get selectedItems(): SelectedItem[];
39
+ get align(): AnchorAlignment;
40
+ get side(): AnchorSide;
41
+ updateAnchorPosition(): void;
42
+ connectedCallback(): void;
43
+ disconnectedCallback(): void;
44
+ handleEvent(event: Event): void;
45
+ show(): void;
46
+ hide(): void;
47
+ get visibleItems(): SelectPanelItem[];
48
+ get items(): SelectPanelItem[];
49
+ get focusableItem(): HTMLElement | undefined;
50
+ getItemById(itemId: string): SelectPanelItem | null;
51
+ isItemDisabled(item: SelectPanelItem | null): boolean;
52
+ disableItem(item: SelectPanelItem | null): void;
53
+ enableItem(item: SelectPanelItem | null): void;
54
+ isItemHidden(item: SelectPanelItem | null): boolean;
55
+ isItemChecked(item: SelectPanelItem | null): boolean;
56
+ checkItem(item: SelectPanelItem | null): void;
57
+ uncheckItem(item: SelectPanelItem | null): void;
58
+ }
59
+ declare global {
60
+ interface Window {
61
+ SelectPanelElement: typeof SelectPanelElement;
62
+ }
63
+ }
64
+ export {};