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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/app/assets/javascripts/app/components/primer/alpha/action_menu/action_menu_element.d.ts +0 -9
- data/app/assets/javascripts/app/components/primer/alpha/select_panel_element.d.ts +64 -0
- data/app/assets/javascripts/app/components/primer/aria_live.d.ts +8 -0
- data/app/assets/javascripts/app/components/primer/primer.d.ts +4 -0
- data/app/assets/javascripts/app/components/primer/shared_events.d.ts +9 -0
- data/app/assets/javascripts/primer_view_components.js +1 -1
- data/app/assets/javascripts/primer_view_components.js.map +1 -1
- data/app/assets/styles/primer_view_components.css +1 -1
- data/app/assets/styles/primer_view_components.css.map +1 -1
- data/app/components/primer/alpha/action_list.css +1 -1
- data/app/components/primer/alpha/action_list.css.map +1 -1
- data/app/components/primer/alpha/action_list.pcss +1 -0
- data/app/components/primer/alpha/action_menu/action_menu_element.d.ts +0 -9
- data/app/components/primer/alpha/action_menu/action_menu_element.ts +0 -11
- data/app/components/primer/alpha/action_menu.rb +13 -6
- data/app/components/primer/alpha/select_panel.html.erb +100 -0
- data/app/components/primer/alpha/select_panel.rb +486 -0
- data/app/components/primer/alpha/select_panel_element.d.ts +64 -0
- data/app/components/primer/alpha/select_panel_element.js +927 -0
- data/app/components/primer/alpha/select_panel_element.ts +1049 -0
- data/app/components/primer/aria_live.d.ts +8 -0
- data/app/components/primer/aria_live.js +38 -0
- data/app/components/primer/aria_live.ts +41 -0
- data/app/components/primer/base_component.rb +1 -1
- data/app/components/primer/primer.d.ts +4 -0
- data/app/components/primer/primer.js +4 -0
- data/app/components/primer/primer.ts +4 -0
- data/app/components/primer/shared_events.d.ts +9 -0
- data/app/components/primer/shared_events.js +1 -0
- data/app/components/primer/shared_events.ts +10 -0
- data/app/forms/example_toggle_switch_form/example_field_caption.html.erb +1 -1
- data/lib/primer/forms/toggle_switch.html.erb +1 -2
- data/lib/primer/static/generate_info_arch.rb +3 -2
- data/lib/primer/view_components/version.rb +2 -2
- data/lib/primer/yard/component_manifest.rb +2 -0
- data/previews/primer/alpha/select_panel_preview/_interaction_subject_js.html.erb +25 -0
- data/previews/primer/alpha/select_panel_preview/eventually_local_fetch.html.erb +16 -0
- data/previews/primer/alpha/select_panel_preview/eventually_local_fetch_initial_failure.html.erb +12 -0
- data/previews/primer/alpha/select_panel_preview/eventually_local_fetch_no_results.html.erb +16 -0
- data/previews/primer/alpha/select_panel_preview/footer_buttons.html.erb +23 -0
- data/previews/primer/alpha/select_panel_preview/local_fetch.html.erb +19 -0
- data/previews/primer/alpha/select_panel_preview/local_fetch_no_results.html.erb +15 -0
- data/previews/primer/alpha/select_panel_preview/multiselect.html.erb +17 -0
- data/previews/primer/alpha/select_panel_preview/multiselect_form.html.erb +31 -0
- data/previews/primer/alpha/select_panel_preview/playground.html.erb +23 -0
- data/previews/primer/alpha/select_panel_preview/remote_fetch.html.erb +16 -0
- data/previews/primer/alpha/select_panel_preview/remote_fetch_filter_failure.html.erb +13 -0
- data/previews/primer/alpha/select_panel_preview/remote_fetch_initial_failure.html.erb +12 -0
- data/previews/primer/alpha/select_panel_preview/remote_fetch_no_results.html.erb +16 -0
- data/previews/primer/alpha/select_panel_preview/single_select.html.erb +20 -0
- data/previews/primer/alpha/select_panel_preview/single_select_form.html.erb +33 -0
- data/previews/primer/alpha/select_panel_preview/with_avatar_items.html.erb +19 -0
- data/previews/primer/alpha/select_panel_preview/with_dynamic_label.html.erb +23 -0
- data/previews/primer/alpha/select_panel_preview/with_dynamic_label_and_aria_prefix.html.erb +24 -0
- data/previews/primer/alpha/select_panel_preview/with_leading_icons.html.erb +31 -0
- data/previews/primer/alpha/select_panel_preview/with_subtitle.html.erb +25 -0
- data/previews/primer/alpha/select_panel_preview/with_trailing_icons.html.erb +19 -0
- data/previews/primer/alpha/select_panel_preview.rb +239 -0
- data/static/arguments.json +140 -0
- data/static/audited_at.json +2 -0
- data/static/constants.json +18 -0
- data/static/info_arch.json +949 -96
- data/static/previews.json +294 -0
- data/static/statuses.json +2 -0
- 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 {};
|