openproject-primer_view_components 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/app/assets/javascripts/app/components/primer/alpha/tool_tip.d.ts +1 -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_bar/item.rb +7 -4
- data/app/components/primer/alpha/action_bar.rb +2 -2
- data/app/components/primer/alpha/action_bar_element.js +9 -4
- data/app/components/primer/alpha/action_bar_element.ts +9 -2
- data/app/components/primer/alpha/action_list/form_wrapper.html.erb +4 -2
- data/app/components/primer/alpha/action_list/form_wrapper.rb +20 -9
- data/app/components/primer/alpha/action_menu/action_menu_element.js +160 -85
- data/app/components/primer/alpha/action_menu/action_menu_element.ts +195 -81
- data/app/components/primer/alpha/action_menu/list.rb +0 -2
- data/app/components/primer/alpha/action_menu.rb +120 -3
- data/app/components/primer/alpha/modal_dialog.js +10 -13
- data/app/components/primer/alpha/modal_dialog.ts +10 -13
- data/app/components/primer/alpha/segmented_control/item.html.erb +1 -8
- data/app/components/primer/alpha/segmented_control/item.rb +38 -4
- data/app/components/primer/alpha/segmented_control.css +1 -1
- data/app/components/primer/alpha/segmented_control.css.json +14 -13
- data/app/components/primer/alpha/segmented_control.css.map +1 -1
- data/app/components/primer/alpha/segmented_control.pcss +75 -66
- data/app/components/primer/alpha/segmented_control.rb +10 -0
- data/app/components/primer/alpha/text_field.css +1 -1
- data/app/components/primer/alpha/tool_tip.d.ts +1 -0
- data/app/components/primer/alpha/tool_tip.js +26 -93
- data/app/components/primer/alpha/tool_tip.ts +25 -91
- data/app/components/primer/beta/base_button.rb +4 -0
- data/app/components/primer/beta/button.css +1 -1
- data/app/components/primer/beta/button.css.map +1 -1
- data/app/components/primer/beta/button.pcss +6 -2
- data/app/components/primer/focus_group.js +28 -3
- data/app/components/primer/focus_group.ts +27 -1
- data/lib/primer/accessibility.rb +3 -1
- data/lib/primer/static/generate_info_arch.rb +86 -5
- data/lib/primer/view_components/version.rb +2 -2
- data/previews/primer/alpha/action_menu_preview/single_select_form_items.html.erb +31 -0
- data/previews/primer/alpha/action_menu_preview/with_actions.html.erb +6 -5
- data/previews/primer/alpha/action_menu_preview.rb +10 -1
- data/previews/primer/alpha/check_box_preview.rb +1 -0
- data/previews/primer/alpha/dialog_preview/autofocus_element.html.erb +8 -0
- data/previews/primer/alpha/dialog_preview.rb +5 -0
- data/previews/primer/alpha/radio_button_preview.rb +1 -0
- data/previews/primer/alpha/segmented_control_preview.rb +35 -0
- data/previews/primer/alpha/tooltip_preview/tooltip_with_dialog_moving_focus_to_input.html.erb +23 -0
- data/previews/primer/alpha/tooltip_preview.rb +6 -1
- data/previews/primer/beta/button_group_preview.rb +6 -6
- data/previews/primer/beta/button_preview.rb +20 -2
- data/previews/primer/beta/icon_button_preview.rb +3 -0
- data/static/arguments.json +18 -1
- data/static/classes.json +12 -0
- data/static/constants.json +12 -1
- data/static/info_arch.json +137 -161
- data/static/previews.json +91 -172
- metadata +5 -310
@@ -20,6 +20,7 @@ export class ActionMenuElement extends HTMLElement {
|
|
20
20
|
#abortController: AbortController
|
21
21
|
#originalLabel = ''
|
22
22
|
#inputName = ''
|
23
|
+
#invokerBeingClicked = false
|
23
24
|
|
24
25
|
get selectVariant(): SelectVariant {
|
25
26
|
return this.getAttribute('data-select-variant') as SelectVariant
|
@@ -52,7 +53,7 @@ export class ActionMenuElement extends HTMLElement {
|
|
52
53
|
}
|
53
54
|
|
54
55
|
get popoverElement(): HTMLElement | null {
|
55
|
-
return this.invokerElement?.popoverTargetElement || null
|
56
|
+
return (this.invokerElement?.popoverTargetElement as HTMLElement) || null
|
56
57
|
}
|
57
58
|
|
58
59
|
get invokerElement(): HTMLButtonElement | null {
|
@@ -94,8 +95,10 @@ export class ActionMenuElement extends HTMLElement {
|
|
94
95
|
this.addEventListener('click', this, {signal})
|
95
96
|
this.addEventListener('mouseover', this, {signal})
|
96
97
|
this.addEventListener('focusout', this, {signal})
|
98
|
+
this.addEventListener('mousedown', this, {signal})
|
97
99
|
this.#setDynamicLabel()
|
98
100
|
this.#updateInput()
|
101
|
+
this.#softDisableItems()
|
99
102
|
|
100
103
|
if (this.includeFragment) {
|
101
104
|
this.includeFragment.addEventListener('include-fragment-replaced', this, {
|
@@ -104,102 +107,218 @@ export class ActionMenuElement extends HTMLElement {
|
|
104
107
|
}
|
105
108
|
}
|
106
109
|
|
110
|
+
#softDisableItems() {
|
111
|
+
const {signal} = this.#abortController
|
112
|
+
|
113
|
+
for (const item of this.#items) {
|
114
|
+
item.addEventListener('click', this.#potentiallyDisallowActivation.bind(this), {signal})
|
115
|
+
item.addEventListener('keydown', this.#potentiallyDisallowActivation.bind(this), {signal})
|
116
|
+
}
|
117
|
+
}
|
118
|
+
|
119
|
+
#potentiallyDisallowActivation(event: Event) {
|
120
|
+
if (!this.#isActivation(event)) return
|
121
|
+
|
122
|
+
const item = (event.target as HTMLElement).closest(menuItemSelectors.join(','))
|
123
|
+
if (!item) return
|
124
|
+
|
125
|
+
if (item.getAttribute('aria-disabled')) {
|
126
|
+
event.preventDefault()
|
127
|
+
event.stopPropagation()
|
128
|
+
event.stopImmediatePropagation()
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
107
132
|
disconnectedCallback() {
|
108
133
|
this.#abortController.abort()
|
109
134
|
}
|
110
135
|
|
136
|
+
#isKeyboardActivation(event: Event): boolean {
|
137
|
+
return (
|
138
|
+
event instanceof KeyboardEvent &&
|
139
|
+
event.type === 'keydown' &&
|
140
|
+
!(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) &&
|
141
|
+
(event.key === 'Enter' || event.key === ' ')
|
142
|
+
)
|
143
|
+
}
|
144
|
+
|
145
|
+
#isMouseActivation(event: Event): boolean {
|
146
|
+
return event instanceof MouseEvent && event.type === 'click'
|
147
|
+
}
|
148
|
+
|
149
|
+
#isActivation(event: Event): boolean {
|
150
|
+
return this.#isMouseActivation(event) || this.#isKeyboardActivation(event)
|
151
|
+
}
|
152
|
+
|
111
153
|
handleEvent(event: Event) {
|
112
|
-
const
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
154
|
+
const targetIsInvoker = this.invokerElement?.contains(event.target as HTMLElement)
|
155
|
+
const eventIsActivation = this.#isActivation(event)
|
156
|
+
|
157
|
+
if (targetIsInvoker && event.type === 'mousedown') {
|
158
|
+
this.#invokerBeingClicked = true
|
159
|
+
return
|
160
|
+
}
|
161
|
+
|
162
|
+
// Prevent safari bug that dismisses menu on mousedown instead of allowing
|
163
|
+
// the click event to propagate to the button
|
164
|
+
if (event.type === 'mousedown') {
|
165
|
+
event.preventDefault()
|
166
|
+
return
|
120
167
|
}
|
121
168
|
|
122
|
-
|
123
|
-
|
169
|
+
if (targetIsInvoker && eventIsActivation) {
|
170
|
+
this.#handleInvokerActivated(event)
|
171
|
+
this.#invokerBeingClicked = false
|
124
172
|
return
|
125
173
|
}
|
126
174
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
this.querySelector<HTMLElement>('.ActionListWrap')!.style.display = 'none'
|
135
|
-
const dialog_controller = new AbortController()
|
136
|
-
const {signal} = dialog_controller
|
137
|
-
const handleDialogClose = () => {
|
138
|
-
dialog_controller.abort()
|
139
|
-
this.querySelector<HTMLElement>('.ActionListWrap')!.style.display = ''
|
140
|
-
if (this.popoverElement?.matches(':popover-open')) {
|
141
|
-
this.popoverElement?.hidePopover()
|
142
|
-
}
|
175
|
+
if (event.type === 'focusout') {
|
176
|
+
if (this.#invokerBeingClicked) return
|
177
|
+
|
178
|
+
// Give the browser time to focus the next element
|
179
|
+
requestAnimationFrame(() => {
|
180
|
+
if (!this.contains(document.activeElement) || document.activeElement === this.invokerElement) {
|
181
|
+
this.#handleFocusOut()
|
143
182
|
}
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
}
|
183
|
+
})
|
184
|
+
|
185
|
+
return
|
148
186
|
}
|
149
187
|
|
150
|
-
|
188
|
+
const item = (event.target as Element).closest(menuItemSelectors.join(','))
|
189
|
+
const targetIsItem = item !== null
|
151
190
|
|
152
|
-
if (
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
setTimeout(() => {
|
163
|
-
if (this.popoverElement?.matches(':popover-open')) {
|
164
|
-
this.popoverElement?.hidePopover()
|
165
|
-
}
|
166
|
-
})
|
191
|
+
if (targetIsItem && eventIsActivation) {
|
192
|
+
const dialogInvoker = item.closest('[data-show-dialog-id]')
|
193
|
+
|
194
|
+
if (dialogInvoker) {
|
195
|
+
const dialog = this.ownerDocument.getElementById(dialogInvoker.getAttribute('data-show-dialog-id') || '')
|
196
|
+
|
197
|
+
if (dialog && this.contains(dialogInvoker) && this.contains(dialog)) {
|
198
|
+
this.#handleDialogItemActivated(event, dialog)
|
199
|
+
return
|
200
|
+
}
|
167
201
|
}
|
168
202
|
|
169
|
-
|
170
|
-
|
171
|
-
|
203
|
+
this.#activateItem(event, item)
|
204
|
+
this.#handleItemActivated(event, item)
|
205
|
+
return
|
206
|
+
}
|
172
207
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
208
|
+
if (event.type === 'include-fragment-replaced') {
|
209
|
+
this.#handleIncludeFragmentReplaced()
|
210
|
+
}
|
211
|
+
}
|
177
212
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
item.setAttribute('aria-checked', 'true')
|
182
|
-
}
|
213
|
+
#handleInvokerActivated(event: Event) {
|
214
|
+
event.preventDefault()
|
215
|
+
event.stopPropagation()
|
183
216
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
217
|
+
if (this.#isOpen()) {
|
218
|
+
this.#hide()
|
219
|
+
} else {
|
220
|
+
this.#show()
|
221
|
+
this.#firstItem?.focus()
|
222
|
+
}
|
223
|
+
}
|
189
224
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
225
|
+
#handleDialogItemActivated(event: Event, dialog: HTMLElement) {
|
226
|
+
this.querySelector<HTMLElement>('.ActionListWrap')!.style.display = 'none'
|
227
|
+
const dialog_controller = new AbortController()
|
228
|
+
const {signal} = dialog_controller
|
229
|
+
const handleDialogClose = () => {
|
230
|
+
dialog_controller.abort()
|
231
|
+
this.querySelector<HTMLElement>('.ActionListWrap')!.style.display = ''
|
232
|
+
if (this.#isOpen()) {
|
233
|
+
this.#hide()
|
194
234
|
}
|
235
|
+
}
|
236
|
+
dialog.addEventListener('close', handleDialogClose, {signal})
|
237
|
+
dialog.addEventListener('cancel', handleDialogClose, {signal})
|
238
|
+
}
|
239
|
+
|
240
|
+
#handleItemActivated(event: Event, item: Element) {
|
241
|
+
// Hide popover after current event loop to prevent changes in focus from
|
242
|
+
// altering the target of the event. Not doing this specifically affects
|
243
|
+
// <a> tags. It causes the event to be sent to the currently focused element
|
244
|
+
// instead of the anchor, which effectively prevents navigation, i.e. it
|
245
|
+
// appears as if hitting enter does nothing. Curiously, clicking instead
|
246
|
+
// works fine.
|
247
|
+
if (this.selectVariant !== 'multiple') {
|
248
|
+
setTimeout(() => {
|
249
|
+
if (this.#isOpen()) {
|
250
|
+
this.#hide()
|
251
|
+
}
|
252
|
+
})
|
253
|
+
}
|
195
254
|
|
196
|
-
|
255
|
+
// The rest of the code below deals with single/multiple selection behavior, and should not
|
256
|
+
// interfere with events fired by menu items whose behavior is specified outside the library.
|
257
|
+
if (this.selectVariant !== 'multiple' && this.selectVariant !== 'single') return
|
197
258
|
|
198
|
-
|
199
|
-
|
200
|
-
|
259
|
+
const ariaChecked = item.getAttribute('aria-checked')
|
260
|
+
const checked = ariaChecked !== 'true'
|
261
|
+
|
262
|
+
if (this.selectVariant === 'single') {
|
263
|
+
// Only check, never uncheck here. Single-select mode does not allow unchecking a checked item.
|
264
|
+
if (checked) {
|
265
|
+
item.setAttribute('aria-checked', 'true')
|
266
|
+
}
|
267
|
+
|
268
|
+
for (const checkedItem of this.querySelectorAll('[aria-checked]')) {
|
269
|
+
if (checkedItem !== item) {
|
270
|
+
checkedItem.setAttribute('aria-checked', 'false')
|
271
|
+
}
|
201
272
|
}
|
273
|
+
|
274
|
+
this.#setDynamicLabel()
|
275
|
+
} else {
|
276
|
+
// multi-select mode allows unchecking a checked item
|
277
|
+
item.setAttribute('aria-checked', `${checked}`)
|
202
278
|
}
|
279
|
+
|
280
|
+
this.#updateInput()
|
281
|
+
}
|
282
|
+
|
283
|
+
#activateItem(event: Event, item: Element) {
|
284
|
+
const eventWillActivateByDefault =
|
285
|
+
(event instanceof MouseEvent && event.type === 'click') ||
|
286
|
+
(event instanceof KeyboardEvent &&
|
287
|
+
event.type === 'keydown' &&
|
288
|
+
!(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) &&
|
289
|
+
event.key === 'Enter')
|
290
|
+
|
291
|
+
// if the event will result in activating the current item by default, i.e. is a
|
292
|
+
// mouse click or keyboard enter, bail out
|
293
|
+
if (eventWillActivateByDefault) return
|
294
|
+
|
295
|
+
// otherwise, event will not result in activation by default, so we stop it and
|
296
|
+
// simulate a click
|
297
|
+
event.stopPropagation()
|
298
|
+
const elem = item as HTMLElement
|
299
|
+
elem.click()
|
300
|
+
}
|
301
|
+
|
302
|
+
#handleIncludeFragmentReplaced() {
|
303
|
+
if (this.#firstItem) this.#firstItem.focus()
|
304
|
+
this.#softDisableItems()
|
305
|
+
}
|
306
|
+
|
307
|
+
// Close when focus leaves menu
|
308
|
+
#handleFocusOut() {
|
309
|
+
this.#hide()
|
310
|
+
}
|
311
|
+
|
312
|
+
#show() {
|
313
|
+
this.popoverElement?.showPopover()
|
314
|
+
}
|
315
|
+
|
316
|
+
#hide() {
|
317
|
+
this.popoverElement?.hidePopover()
|
318
|
+
}
|
319
|
+
|
320
|
+
#isOpen() {
|
321
|
+
return this.popoverElement?.matches(':popover-open')
|
203
322
|
}
|
204
323
|
|
205
324
|
#setDynamicLabel() {
|
@@ -261,18 +380,13 @@ export class ActionMenuElement extends HTMLElement {
|
|
261
380
|
}
|
262
381
|
}
|
263
382
|
|
264
|
-
#isActivationKeydown(event: Event): boolean {
|
265
|
-
return (
|
266
|
-
event instanceof KeyboardEvent &&
|
267
|
-
event.type === 'keydown' &&
|
268
|
-
!(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) &&
|
269
|
-
(event.key === 'Enter' || event.key === ' ')
|
270
|
-
)
|
271
|
-
}
|
272
|
-
|
273
383
|
get #firstItem(): HTMLElement | null {
|
274
384
|
return this.querySelector(menuItemSelectors.join(','))
|
275
385
|
}
|
386
|
+
|
387
|
+
get #items(): HTMLElement[] {
|
388
|
+
return Array.from(this.querySelectorAll(menuItemSelectors.join(',')))
|
389
|
+
}
|
276
390
|
}
|
277
391
|
|
278
392
|
if (!window.customElements.get('action-menu')) {
|
@@ -3,14 +3,131 @@
|
|
3
3
|
|
4
4
|
module Primer
|
5
5
|
module Alpha
|
6
|
-
# ActionMenu is used for actions, navigation, to display secondary options, or single/multi select lists. They appear when
|
6
|
+
# ActionMenu is used for actions, navigation, to display secondary options, or single/multi select lists. They appear when
|
7
|
+
# users interact with buttons, actions, or other controls.
|
7
8
|
#
|
8
9
|
# The only allowed elements for the `Item` components are: `:a`, `:button`, and `:clipboard-copy`. The default is `:button`.
|
9
10
|
#
|
11
|
+
# ### Select variants
|
12
|
+
#
|
13
|
+
# While `ActionMenu`s default to a list of buttons that can link to other pages, copy text to the clipboard, etc, they also support
|
14
|
+
# `single` and `multiple` select variants. The single select variant allows a single item to be "selected" (i.e. marked "active")
|
15
|
+
# when clicked, which will cause a check mark to appear to the left of the item text. When the `multiple` select variant is chosen,
|
16
|
+
# multiple items may be selected and check marks will appear next to each selected item.
|
17
|
+
#
|
18
|
+
# Use the `select_variant:` option to control which variant the `ActionMenu` uses. For more information, see the documentation on
|
19
|
+
# supported arguments below.
|
20
|
+
#
|
21
|
+
# ### Dynamic labels
|
22
|
+
#
|
23
|
+
# When using the `single` select variant, an optional label indicating the selected item can be displayed inside the menu button.
|
24
|
+
# Dynamic labels can also be prefixed with custom text.
|
25
|
+
#
|
26
|
+
# Pass `dynamic_label: true` to enable dynamic label behavior, and pass `dynamic_label_prefix: "<string>"` to set a custom prefix.
|
27
|
+
# For more information, see the documentation on supported arguments below.
|
28
|
+
#
|
29
|
+
# ### `ActionMenu`s as form inputs
|
30
|
+
#
|
31
|
+
# When using either the `single` or `multiple` select variants, `ActionMenu`s can be used as form inputs. They behave very
|
32
|
+
# similarly to how HTML `<select>` boxes behave, and play nicely with Rails' built-in form mechanisms. Pass arguments via the
|
33
|
+
# `form_arguments:` argument, including the Rails form builder object and the name of the field:
|
34
|
+
#
|
35
|
+
# ```erb
|
36
|
+
# <% form_with(url: update_merge_strategy_path) do |f| %>
|
37
|
+
# <%= render(Primer::Alpha::ActionMenu.new(form_arguments: { builder: f, name: "merge_strategy" })) do |menu| %>
|
38
|
+
# <% menu.with_item(label: "Fast forward", data: { value: "fast_forward" }) %>
|
39
|
+
# <% menu.with_item(label: "Recursive", data: { value: "recursive" }) %>
|
40
|
+
# <% menu.with_item(label: "Ours", data: { value: "ours" }) %>
|
41
|
+
# <% menu.with_item(label: "Theirs", data: { value: "theirs" }) %>
|
42
|
+
# <% end %>
|
43
|
+
# <% end %>
|
44
|
+
# ```
|
45
|
+
#
|
46
|
+
# The value of the `data: { value: ... }` argument is sent to the server on submit, keyed using the name provided above
|
47
|
+
# (eg. `"merge_strategy"`). If no value is provided for an item, the value of that item is the item's label. Here's the
|
48
|
+
# corresponding `MergeStrategyController` that might be written to handle the form above:
|
49
|
+
#
|
50
|
+
# ```ruby
|
51
|
+
# class MergeStrategyController < ApplicationController
|
52
|
+
# def update
|
53
|
+
# puts "You chose #{merge_strategy_params[:merge_strategy]}"
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# private
|
57
|
+
#
|
58
|
+
# def merge_strategy_params
|
59
|
+
# params.permit(:merge_strategy)
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
# ```
|
63
|
+
#
|
64
|
+
# ### `ActionMenu` items that submit forms
|
65
|
+
#
|
66
|
+
# Whereas `ActionMenu` items normally permit navigation via `<a>` tags which make HTTP `get` requests, `ActionMenu` items
|
67
|
+
# also permit navigation via `POST` requests. To enable this behavior, include the `href:` argument as normal, but also pass
|
68
|
+
# the `form_arguments:` argument to the appropriate item:
|
69
|
+
#
|
70
|
+
# ```erb
|
71
|
+
# <%= render(Primer::Alpha::ActionMenu.new) do |menu| %>
|
72
|
+
# <% menu.with_item(
|
73
|
+
# label: "Repository",
|
74
|
+
# href: update_repo_grouping_path,
|
75
|
+
# form_arguments: {
|
76
|
+
# method: :post,
|
77
|
+
# name: "group_by",
|
78
|
+
# value: "repository"
|
79
|
+
# }
|
80
|
+
# ) %>
|
81
|
+
# <% end %>
|
82
|
+
# ```
|
83
|
+
#
|
84
|
+
# Make sure to specify `method: :post`, as the default is `:get`. When clicked, the list item will submit a POST request to
|
85
|
+
# the URL passed in the `href:` argument, including a parameter named `"group_by"` with a value of `"repository"`. If no value
|
86
|
+
# is given, the name, eg. `"group_by"`, will be used as the value.
|
87
|
+
#
|
88
|
+
# It is possible to include multiple fields on submit. Instead of passing the `name:` and `value:` arguments, pass an array via
|
89
|
+
# the `inputs:` argument:
|
90
|
+
#
|
91
|
+
# ```erb
|
92
|
+
# <%= render(Primer::Alpha::ActionMenu.new) do |menu| %>
|
93
|
+
# <% menu.with_show_button { "Group By" } %>
|
94
|
+
# <% menu.with_item(
|
95
|
+
# label: "Repository",
|
96
|
+
# href: update_repo_grouping_path,
|
97
|
+
# form_arguments: {
|
98
|
+
# method: :post,
|
99
|
+
# inputs: [{
|
100
|
+
# name: "group_by",
|
101
|
+
# value: "repository"
|
102
|
+
# }, {
|
103
|
+
# name: "some_other_field",
|
104
|
+
# value: "some value",
|
105
|
+
# }],
|
106
|
+
# }
|
107
|
+
# ) %>
|
108
|
+
# <% end %>
|
109
|
+
# ```
|
110
|
+
#
|
111
|
+
# ### Form arguments
|
112
|
+
#
|
113
|
+
# The following table summarizes the arguments allowed in the `form_arguments:` hash mentioned above.
|
114
|
+
#
|
115
|
+
# |Name |Type |Default|Description|
|
116
|
+
# |:----------------|:-------------|:------|:----------|
|
117
|
+
# |`method` |`Symbol` |`:get` |The HTTP request method to use to submit the form. One of `:get`, `:post`, `:patch`, `:put`, `:delete`, or `:head`|
|
118
|
+
# |`name` |`String` |`nil` |The name of the field that will be sent to the server on submit.|
|
119
|
+
# |`value` |`String` |`nil` |The value of the field that will be sent to the server on submit.|
|
120
|
+
# |`input_arguments`|`Hash` |`{}` |Additional key/value pairs to emit as HTML attributes on the `<input type="hidden">` element.|
|
121
|
+
# |`inputs` |`Array<Hash>` |`[]` |An array of hashes representing HTML `<input type="hidden">` elements. Must contain at least `name:` and `value:` keys. If additional key/value pairs are provided, they are emitted as HTML attributes on the `<input>` element. This argument supercedes the `name:`, `value:`, and `:input_arguments` arguments listed above.|
|
122
|
+
#
|
123
|
+
# The elements of the `inputs:` array will be emitted as HTML `<input type="hidden">` elements.
|
124
|
+
#
|
10
125
|
# @accessibility
|
11
|
-
# The action for the menu item needs to be on the element with `role="menuitem"`. Semantics are removed for everything
|
126
|
+
# The action for the menu item needs to be on the element with `role="menuitem"`. Semantics are removed for everything
|
127
|
+
# nested inside of it. When a menu item is selected, the menu will close immediately.
|
12
128
|
#
|
13
|
-
# Additional information around the keyboard functionality and implementation can be found on the
|
129
|
+
# Additional information around the keyboard functionality and implementation can be found on the
|
130
|
+
# [WAI-ARIA Authoring Practices](https://www.w3.org/TR/wai-aria-practices-1.2/#menu).
|
14
131
|
class ActionMenu < Primer::Component
|
15
132
|
status :alpha
|
16
133
|
|
@@ -38,19 +38,16 @@ function clickHandler(event) {
|
|
38
38
|
return;
|
39
39
|
}
|
40
40
|
}
|
41
|
-
|
42
|
-
const topLevelDialog = overlayStack[overlayStack.length - 1];
|
43
|
-
if (!topLevelDialog)
|
41
|
+
if (!overlayStack.length)
|
44
42
|
return;
|
45
|
-
dialogId = button.getAttribute('data-close-dialog-id');
|
46
|
-
if (dialogId
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
topLevelDialog.close(true);
|
43
|
+
dialogId = button.getAttribute('data-close-dialog-id') || button.getAttribute('data-submit-dialog-id');
|
44
|
+
if (dialogId) {
|
45
|
+
const dialog = document.getElementById(dialogId);
|
46
|
+
if (dialog instanceof ModalDialogElement) {
|
47
|
+
const dialogIndex = overlayStack.findIndex(ele => ele.id === dialogId);
|
48
|
+
overlayStack.splice(dialogIndex, 1);
|
49
|
+
dialog.close(button.hasAttribute('data-submit-dialog-id'));
|
50
|
+
}
|
54
51
|
}
|
55
52
|
}
|
56
53
|
function keydownHandler(event) {
|
@@ -107,7 +104,7 @@ export class ModalDialogElement extends HTMLElement {
|
|
107
104
|
if (__classPrivateFieldGet(this, _ModalDialogElement_focusAbortController, "f").signal.aborted) {
|
108
105
|
__classPrivateFieldSet(this, _ModalDialogElement_focusAbortController, new AbortController(), "f");
|
109
106
|
}
|
110
|
-
focusTrap(this,
|
107
|
+
focusTrap(this, this.querySelector('[autofocus]'), __classPrivateFieldGet(this, _ModalDialogElement_focusAbortController, "f").signal);
|
111
108
|
overlayStack.push(this);
|
112
109
|
}
|
113
110
|
else {
|
@@ -30,20 +30,17 @@ function clickHandler(event: Event) {
|
|
30
30
|
return
|
31
31
|
}
|
32
32
|
}
|
33
|
-
// Find the top level dialog that is open.
|
34
|
-
const topLevelDialog = overlayStack[overlayStack.length - 1]
|
35
|
-
if (!topLevelDialog) return
|
36
33
|
|
37
|
-
|
38
|
-
if (dialogId === topLevelDialog.id) {
|
39
|
-
overlayStack.pop()
|
40
|
-
topLevelDialog.close()
|
41
|
-
}
|
34
|
+
if (!overlayStack.length) return
|
42
35
|
|
43
|
-
dialogId = button.getAttribute('data-submit-dialog-id')
|
44
|
-
if (dialogId
|
45
|
-
|
46
|
-
|
36
|
+
dialogId = button.getAttribute('data-close-dialog-id') || button.getAttribute('data-submit-dialog-id')
|
37
|
+
if (dialogId) {
|
38
|
+
const dialog = document.getElementById(dialogId)
|
39
|
+
if (dialog instanceof ModalDialogElement) {
|
40
|
+
const dialogIndex = overlayStack.findIndex(ele => ele.id === dialogId)
|
41
|
+
overlayStack.splice(dialogIndex, 1)
|
42
|
+
dialog.close(button.hasAttribute('data-submit-dialog-id'))
|
43
|
+
}
|
47
44
|
}
|
48
45
|
}
|
49
46
|
|
@@ -107,7 +104,7 @@ export class ModalDialogElement extends HTMLElement {
|
|
107
104
|
if (this.#focusAbortController.signal.aborted) {
|
108
105
|
this.#focusAbortController = new AbortController()
|
109
106
|
}
|
110
|
-
focusTrap(this,
|
107
|
+
focusTrap(this, this.querySelector('[autofocus]') as HTMLElement, this.#focusAbortController.signal)
|
111
108
|
overlayStack.push(this)
|
112
109
|
} else {
|
113
110
|
if (!this.open) return
|
@@ -2,12 +2,5 @@
|
|
2
2
|
"SegmentedControl-item",
|
3
3
|
"SegmentedControl-item--selected": @selected
|
4
4
|
) %>" role="listitem" data-targets="segmented-control.items">
|
5
|
-
|
6
|
-
<%= render Primer::Beta::IconButton.new(icon: @icon, "aria-label": @label, **@system_arguments) %>
|
7
|
-
<% else %>
|
8
|
-
<%= render Primer::Beta::Button.new(**@system_arguments) do |button| %>
|
9
|
-
<% button.with_leading_visual_icon(icon: @icon) unless @icon.nil? %>
|
10
|
-
<%= @label %>
|
11
|
-
<% end %>
|
12
|
-
<% end %>
|
5
|
+
<%= render @button %>
|
13
6
|
</li>
|
@@ -13,16 +13,50 @@ module Primer
|
|
13
13
|
# @param selected [Boolean] Whether the item is selected
|
14
14
|
# @param icon [Symbol] The icon to use
|
15
15
|
# @param hide_labels [Symbol] Whether to only show the icon
|
16
|
-
def initialize(
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
def initialize(
|
17
|
+
label:,
|
18
|
+
selected: false,
|
19
|
+
icon: nil,
|
20
|
+
hide_labels: false,
|
21
|
+
**system_arguments
|
22
|
+
)
|
20
23
|
@selected = selected
|
21
24
|
|
22
25
|
@system_arguments = system_arguments
|
23
26
|
@system_arguments[:"data-action"] = "click:segmented-control#select" if system_arguments[:href].nil?
|
24
27
|
@system_arguments[:"aria-current"] = selected
|
25
28
|
@system_arguments[:scheme] = :invisible
|
29
|
+
|
30
|
+
if hide_labels
|
31
|
+
@button = Primer::Beta::IconButton.new(
|
32
|
+
icon: icon,
|
33
|
+
"aria-label": label,
|
34
|
+
**@system_arguments
|
35
|
+
)
|
36
|
+
else
|
37
|
+
@button = Primer::Beta::Button.new(**@system_arguments)
|
38
|
+
@button.with_leading_visual_icon(icon: icon) if icon
|
39
|
+
@button.with_content(label)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @!parse
|
44
|
+
# # Optional trailing Label
|
45
|
+
# #
|
46
|
+
# # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Beta::Button) %>'s `with_trailing_visual_label` slot.
|
47
|
+
# renders_one(:trailing_visual_label)
|
48
|
+
|
49
|
+
# Optional trailing label.
|
50
|
+
#
|
51
|
+
# @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Beta::Button) %>'s `with_trailing_visual_label` slot.
|
52
|
+
def with_trailing_visual_label(**system_arguments, &block)
|
53
|
+
@button.with_trailing_visual_label(**system_arguments, &block)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def before_render
|
59
|
+
content
|
26
60
|
end
|
27
61
|
end
|
28
62
|
end
|
@@ -1 +1 @@
|
|
1
|
-
.SegmentedControl{background-color:var(--controlTrack-bgColor-rest,var(--color-segmented-control-bg));border-radius:var(--borderRadius-medium,.375rem);display:inline-flex;list-style:none}.SegmentedControl-item{border:var(--borderWidth-thin,max(1px,.0625rem)) solid #0000;border-radius:var(--borderRadius-medium,.375rem);display:inline-flex;
|
1
|
+
.SegmentedControl{--segmentedControl-item-padding:var(--control-small-paddingBlock,0.25rem);background-color:var(--controlTrack-bgColor-rest,var(--color-segmented-control-bg));border-radius:var(--borderRadius-medium,.375rem);display:inline-flex;list-style:none}.SegmentedControl--iconOnly .Button--iconOnly.Button--large,.SegmentedControl--iconOnly .Button--iconOnly.Button--medium,.SegmentedControl--iconOnly .Button--iconOnly.Button--small{padding-inline:0!important;width:100%}.SegmentedControl--small{--segmentedControl-item-padding:var(--control-xsmall-paddingBlock,0.125rem)}.SegmentedControl--small .SegmentedControl-item{height:var(--control-small-size,1.75rem)}.SegmentedControl--small .SegmentedControl-item .Button{padding-inline:calc(var(--control-xsmall-paddingInline-normal,.5rem) - var(--segmentedControl-item-padding))}.SegmentedControl--small.SegmentedControl--iconOnly .SegmentedControl-item{width:var(--control-small-size,1.75rem)}.SegmentedControl--medium .SegmentedControl-item{height:var(--control-medium-size,2rem)}.SegmentedControl--medium.SegmentedControl--iconOnly .SegmentedControl-item{width:var(--control-medium-size,2rem)}.SegmentedControl--large .SegmentedControl-item{height:var(--control-large-size,2.5rem)}.SegmentedControl--large .SegmentedControl-item .Button{padding-inline:calc(var(--control-large-paddingInline-normal,.75rem) - var(--segmentedControl-item-padding))}.SegmentedControl--large.SegmentedControl--iconOnly .SegmentedControl-item{width:var(--control-large-size,2.5rem)}.SegmentedControl-item{border:var(--borderWidth-thin,max(1px,.0625rem)) solid #0000;border-radius:var(--borderRadius-medium,.375rem);display:inline-flex;height:var(--control-medium-size,2rem);justify-content:center;padding:var(--segmentedControl-item-padding);position:relative}.SegmentedControl-item .Button-withTooltip{width:100%}.SegmentedControl-item .Button--invisible:hover:not(:disabled){background-color:var(--controlTrack-bgColor-hover,var(--color-action-list-item-default-hover-bg))}.SegmentedControl-item .Button--invisible:active:not(:disabled){background-color:var(--controlTrack-bgColor-active,var(--color-action-list-item-default-active-bg))}.SegmentedControl-item.SegmentedControl-item--selected{background-color:var(--controlKnob-bgColor-rest,var(--color-segmented-control-button-bg));border-color:var(--controlKnob-borderColor-rest,var(--color-segmented-control-button-selected-border))}.SegmentedControl-item.SegmentedControl-item--selected .Button{font-weight:var(--base-text-weight-semibold,600)}.SegmentedControl-item.SegmentedControl-item--selected .Button:hover{background-color:initial}.SegmentedControl-item.SegmentedControl-item--selected:before{border-color:#0000!important}.SegmentedControl-item.SegmentedControl-item--selected+.SegmentedControl-item:before{border-color:#0000}.SegmentedControl-item .Button-label[data-content]:before{content:attr(data-content);display:block;font-weight:var(--base-text-weight-semibold,600);height:0;visibility:hidden}.SegmentedControl-item:not(:first-child):before{border-left:var(--borderWidth-thin,max(1px,.0625rem)) solid var(--borderColor-default,var(--color-border-default));content:"";inset:0 0 0 -1px;margin-bottom:var(--control-medium-paddingBlock,.375rem);margin-top:var(--control-medium-paddingBlock,.375rem);position:absolute}.SegmentedControl-item .Button{border:0;border-radius:calc(var(--borderRadius-medium,.375rem) - var(--segmentedControl-item-padding)/2);font-weight:var(--base-text-weight-normal,400);height:100%;padding-inline:calc(var(--control-medium-paddingInline-normal,.75rem) - var(--segmentedControl-item-padding));width:100%}.SegmentedControl-item .Button:focus-visible{border-radius:calc(var(--borderRadius-medium,.375rem) - var(--segmentedControl-item-padding)/1);outline-offset:calc(var(--segmentedControl-item-padding) - var(--borderWidth-thin,max(1px, .0625rem)))}.SegmentedControl-item .Button--invisible.Button--invisible-noVisuals .Button-label{color:var(--button-default-fgColor-rest,var(--color-btn-text))}.SegmentedControl--fullWidth{display:flex}.SegmentedControl--fullWidth .SegmentedControl-item{flex:1;justify-content:center}
|