primer_view_components 0.12.0 → 0.13.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -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_element.js +10 -3
- data/app/components/primer/alpha/action_bar_element.ts +10 -1
- 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 +18 -12
- data/app/components/primer/alpha/action_menu/action_menu_element.ts +24 -12
- 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.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 -68
- 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.js +17 -26
- data/app/components/primer/alpha/tool_tip.ts +17 -26
- 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 +1 -1
- data/previews/primer/alpha/action_menu_preview.rb +8 -2
- data/previews/primer/alpha/check_box_preview.rb +0 -2
- data/previews/primer/alpha/dialog_preview/autofocus_element.html.erb +8 -0
- data/previews/primer/alpha/dialog_preview/with_text_input.html.erb +2 -1
- data/previews/primer/alpha/dialog_preview.rb +5 -0
- data/previews/primer/alpha/radio_button_preview.rb +0 -2
- data/previews/primer/alpha/tooltip_preview.rb +2 -2
- data/previews/primer/beta/button_group_preview.rb +6 -6
- data/static/classes.json +12 -0
- data/static/constants.json +12 -1
- data/static/info_arch.json +28 -2
- data/static/previews.json +26 -0
- metadata +4 -310
@@ -15,7 +15,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
15
15
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
16
16
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
17
17
|
};
|
18
|
-
var _ActionBarElement_instances, _ActionBarElement_initialBarWidth, _ActionBarElement_previousBarWidth, _ActionBarElement_focusZoneAbortController, _ActionBarElement_itemGap, _ActionBarElement_availableSpace, _ActionBarElement_menuSpace, _ActionBarElement_shrink, _ActionBarElement_grow, _ActionBarElement_showItem, _ActionBarElement_hideItem, _ActionBarElement_menuItems_get;
|
18
|
+
var _ActionBarElement_instances, _ActionBarElement_initialBarWidth, _ActionBarElement_previousBarWidth, _ActionBarElement_focusZoneAbortController, _ActionBarElement_isVisible, _ActionBarElement_itemGap, _ActionBarElement_availableSpace, _ActionBarElement_menuSpace, _ActionBarElement_shrink, _ActionBarElement_grow, _ActionBarElement_showItem, _ActionBarElement_hideItem, _ActionBarElement_menuItems_get;
|
19
19
|
import { controller, targets, target } from '@github/catalyst';
|
20
20
|
import { focusZone, FocusKeys } from '@primer/behaviors';
|
21
21
|
const instersectionObserver = new IntersectionObserver(entries => {
|
@@ -94,12 +94,19 @@ let ActionBarElement = class ActionBarElement extends HTMLElement {
|
|
94
94
|
bindKeys: FocusKeys.ArrowHorizontal | FocusKeys.HomeAndEnd,
|
95
95
|
focusOutBehavior: 'wrap',
|
96
96
|
focusableElementFilter: element => {
|
97
|
-
return
|
97
|
+
return __classPrivateFieldGet(this, _ActionBarElement_instances, "m", _ActionBarElement_isVisible).call(this, element);
|
98
98
|
}
|
99
99
|
}), "f");
|
100
100
|
}
|
101
101
|
};
|
102
|
-
_ActionBarElement_initialBarWidth = new WeakMap(), _ActionBarElement_previousBarWidth = new WeakMap(), _ActionBarElement_focusZoneAbortController = new WeakMap(), _ActionBarElement_instances = new WeakSet(),
|
102
|
+
_ActionBarElement_initialBarWidth = new WeakMap(), _ActionBarElement_previousBarWidth = new WeakMap(), _ActionBarElement_focusZoneAbortController = new WeakMap(), _ActionBarElement_instances = new WeakSet(), _ActionBarElement_isVisible = function _ActionBarElement_isVisible(element) {
|
103
|
+
// Safari doesn't support `checkVisibility` yet.
|
104
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
105
|
+
// @ts-ignore
|
106
|
+
if (typeof element.checkVisibility === 'function')
|
107
|
+
return element.checkVisibility();
|
108
|
+
return Boolean(element.offsetParent || element.offsetWidth || element.offsetHeight);
|
109
|
+
}, _ActionBarElement_itemGap = function _ActionBarElement_itemGap() {
|
103
110
|
var _a;
|
104
111
|
return parseInt((_a = window.getComputedStyle(this.itemContainer)) === null || _a === void 0 ? void 0 : _a.columnGap, 10) || 0;
|
105
112
|
}, _ActionBarElement_availableSpace = function _ActionBarElement_availableSpace() {
|
@@ -86,11 +86,20 @@ class ActionBarElement extends HTMLElement {
|
|
86
86
|
bindKeys: FocusKeys.ArrowHorizontal | FocusKeys.HomeAndEnd,
|
87
87
|
focusOutBehavior: 'wrap',
|
88
88
|
focusableElementFilter: element => {
|
89
|
-
return
|
89
|
+
return this.#isVisible(element)
|
90
90
|
}
|
91
91
|
})
|
92
92
|
}
|
93
93
|
|
94
|
+
#isVisible(element: HTMLElement): boolean {
|
95
|
+
// Safari doesn't support `checkVisibility` yet.
|
96
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
97
|
+
// @ts-ignore
|
98
|
+
if (typeof element.checkVisibility === 'function') return element.checkVisibility()
|
99
|
+
|
100
|
+
return Boolean(element.offsetParent || element.offsetWidth || element.offsetHeight)
|
101
|
+
}
|
102
|
+
|
94
103
|
#itemGap(): number {
|
95
104
|
return parseInt(window.getComputedStyle(this.itemContainer)?.columnGap, 10) || 0
|
96
105
|
}
|
@@ -1,7 +1,9 @@
|
|
1
1
|
<% if form_required? %>
|
2
2
|
<%= form_with(url: @action, method: @http_method, **@form_arguments) do %>
|
3
|
-
<% if
|
4
|
-
|
3
|
+
<% if render_inputs? %>
|
4
|
+
<% @inputs.each do |input_arguments| %>
|
5
|
+
<%= render(Primer::BaseComponent.new(tag: :input, **input_arguments)) %>
|
6
|
+
<% end %>
|
5
7
|
<% end %>
|
6
8
|
<%= content %>
|
7
9
|
<% end %>
|
@@ -24,14 +24,25 @@ module Primer
|
|
24
24
|
|
25
25
|
name = @form_arguments.delete(:name)
|
26
26
|
value = @form_arguments.delete(:value) || name
|
27
|
+
inputs = @form_arguments.delete(:inputs) || []
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
29
|
+
# For the older version of this component that only allowed you to
|
30
|
+
# specify a single input
|
31
|
+
if inputs.empty?
|
32
|
+
inputs << {
|
33
|
+
name: name,
|
34
|
+
value: value,
|
35
|
+
**(@form_arguments.delete(:input_arguments) || {})
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
@inputs = inputs.map do |input_data|
|
40
|
+
input_data = input_data.dup
|
41
|
+
input_data[:type] ||= :hidden
|
42
|
+
input_data[:data] ||= {}
|
43
|
+
input_data[:data][:list_item_input] = true
|
44
|
+
input_data
|
45
|
+
end
|
35
46
|
end
|
36
47
|
|
37
48
|
def get?
|
@@ -42,8 +53,8 @@ module Primer
|
|
42
53
|
@action && !get?
|
43
54
|
end
|
44
55
|
|
45
|
-
def
|
46
|
-
@
|
56
|
+
def render_inputs?
|
57
|
+
@inputs.present?
|
47
58
|
end
|
48
59
|
|
49
60
|
private
|
@@ -15,7 +15,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
15
15
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
16
16
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
17
17
|
};
|
18
|
-
var _ActionMenuElement_instances, _ActionMenuElement_abortController, _ActionMenuElement_originalLabel, _ActionMenuElement_inputName, _ActionMenuElement_invokerBeingClicked, _ActionMenuElement_softDisableItems, _ActionMenuElement_potentiallyDisallowActivation, _ActionMenuElement_isKeyboardActivation, _ActionMenuElement_isMouseActivation, _ActionMenuElement_isActivation, _ActionMenuElement_handleInvokerActivated, _ActionMenuElement_handleDialogItemActivated, _ActionMenuElement_handleItemActivated, _ActionMenuElement_activateItem, _ActionMenuElement_handleIncludeFragmentReplaced, _ActionMenuElement_handleFocusOut, _ActionMenuElement_show, _ActionMenuElement_hide, _ActionMenuElement_isOpen, _ActionMenuElement_setDynamicLabel, _ActionMenuElement_updateInput, _ActionMenuElement_firstItem_get, _ActionMenuElement_items_get;
|
18
|
+
var _ActionMenuElement_instances, _ActionMenuElement_abortController, _ActionMenuElement_originalLabel, _ActionMenuElement_inputName, _ActionMenuElement_invokerBeingClicked, _ActionMenuElement_softDisableItems, _ActionMenuElement_potentiallyDisallowActivation, _ActionMenuElement_isKeyboardActivation, _ActionMenuElement_isKeyboardActivationViaEnter, _ActionMenuElement_isKeyboardActivationViaSpace, _ActionMenuElement_isMouseActivation, _ActionMenuElement_isActivation, _ActionMenuElement_handleInvokerActivated, _ActionMenuElement_handleDialogItemActivated, _ActionMenuElement_handleItemActivated, _ActionMenuElement_activateItem, _ActionMenuElement_handleIncludeFragmentReplaced, _ActionMenuElement_handleFocusOut, _ActionMenuElement_show, _ActionMenuElement_hide, _ActionMenuElement_isOpen, _ActionMenuElement_setDynamicLabel, _ActionMenuElement_updateInput, _ActionMenuElement_firstItem_get, _ActionMenuElement_items_get;
|
19
19
|
import { controller, target } from '@github/catalyst';
|
20
20
|
import '@oddbird/popover-polyfill';
|
21
21
|
const validSelectors = ['[role="menuitem"]', '[role="menuitemcheckbox"]', '[role="menuitemradio"]'];
|
@@ -109,7 +109,7 @@ let ActionMenuElement = class ActionMenuElement extends HTMLElement {
|
|
109
109
|
__classPrivateFieldGet(this, _ActionMenuElement_abortController, "f").abort();
|
110
110
|
}
|
111
111
|
handleEvent(event) {
|
112
|
-
var _a, _b
|
112
|
+
var _a, _b;
|
113
113
|
const targetIsInvoker = (_a = this.invokerElement) === null || _a === void 0 ? void 0 : _a.contains(event.target);
|
114
114
|
const eventIsActivation = __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_isActivation).call(this, event);
|
115
115
|
if (targetIsInvoker && event.type === 'mousedown') {
|
@@ -127,10 +127,6 @@ let ActionMenuElement = class ActionMenuElement extends HTMLElement {
|
|
127
127
|
__classPrivateFieldSet(this, _ActionMenuElement_invokerBeingClicked, false, "f");
|
128
128
|
return;
|
129
129
|
}
|
130
|
-
// Ignore events within dialogs within menus
|
131
|
-
if (((_b = event.target) === null || _b === void 0 ? void 0 : _b.closest('dialog')) || ((_c = event.target) === null || _c === void 0 ? void 0 : _c.closest('modal-dialog'))) {
|
132
|
-
return;
|
133
|
-
}
|
134
130
|
if (event.type === 'focusout') {
|
135
131
|
if (__classPrivateFieldGet(this, _ActionMenuElement_invokerBeingClicked, "f"))
|
136
132
|
return;
|
@@ -155,6 +151,14 @@ let ActionMenuElement = class ActionMenuElement extends HTMLElement {
|
|
155
151
|
}
|
156
152
|
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_activateItem).call(this, event, item);
|
157
153
|
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_handleItemActivated).call(this, event, item);
|
154
|
+
// Pressing the space key on a button will cause the page to scroll unless preventDefault()
|
155
|
+
// is called. Unfortunately, calling preventDefault() will also skip form submission. The
|
156
|
+
// code below therefore only calls preventDefault() if the button submits a form and the
|
157
|
+
// button is being activated by the space key.
|
158
|
+
if (item.getAttribute('type') === 'submit' && __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_isKeyboardActivationViaSpace).call(this, event)) {
|
159
|
+
event.preventDefault();
|
160
|
+
(_b = item.closest('form')) === null || _b === void 0 ? void 0 : _b.submit();
|
161
|
+
}
|
158
162
|
return;
|
159
163
|
}
|
160
164
|
if (event.type === 'include-fragment-replaced') {
|
@@ -180,10 +184,17 @@ _ActionMenuElement_abortController = new WeakMap(), _ActionMenuElement_originalL
|
|
180
184
|
event.stopImmediatePropagation();
|
181
185
|
}
|
182
186
|
}, _ActionMenuElement_isKeyboardActivation = function _ActionMenuElement_isKeyboardActivation(event) {
|
187
|
+
return __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_isKeyboardActivationViaEnter).call(this, event) || __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_isKeyboardActivationViaSpace).call(this, event);
|
188
|
+
}, _ActionMenuElement_isKeyboardActivationViaEnter = function _ActionMenuElement_isKeyboardActivationViaEnter(event) {
|
183
189
|
return (event instanceof KeyboardEvent &&
|
184
190
|
event.type === 'keydown' &&
|
185
191
|
!(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) &&
|
186
|
-
|
192
|
+
event.key === 'Enter');
|
193
|
+
}, _ActionMenuElement_isKeyboardActivationViaSpace = function _ActionMenuElement_isKeyboardActivationViaSpace(event) {
|
194
|
+
return (event instanceof KeyboardEvent &&
|
195
|
+
event.type === 'keydown' &&
|
196
|
+
!(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) &&
|
197
|
+
event.key === ' ');
|
187
198
|
}, _ActionMenuElement_isMouseActivation = function _ActionMenuElement_isMouseActivation(event) {
|
188
199
|
return event instanceof MouseEvent && event.type === 'click';
|
189
200
|
}, _ActionMenuElement_isActivation = function _ActionMenuElement_isActivation(event) {
|
@@ -249,11 +260,6 @@ _ActionMenuElement_abortController = new WeakMap(), _ActionMenuElement_originalL
|
|
249
260
|
item.setAttribute('aria-checked', `${checked}`);
|
250
261
|
}
|
251
262
|
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_updateInput).call(this);
|
252
|
-
if (event instanceof KeyboardEvent && event.target instanceof HTMLButtonElement) {
|
253
|
-
// prevent buttons from being clicked twice
|
254
|
-
event.preventDefault();
|
255
|
-
return;
|
256
|
-
}
|
257
263
|
}, _ActionMenuElement_activateItem = function _ActionMenuElement_activateItem(event, item) {
|
258
264
|
const eventWillActivateByDefault = (event instanceof MouseEvent && event.type === 'click') ||
|
259
265
|
(event instanceof KeyboardEvent &&
|
@@ -134,11 +134,24 @@ export class ActionMenuElement extends HTMLElement {
|
|
134
134
|
}
|
135
135
|
|
136
136
|
#isKeyboardActivation(event: Event): boolean {
|
137
|
+
return this.#isKeyboardActivationViaEnter(event) || this.#isKeyboardActivationViaSpace(event)
|
138
|
+
}
|
139
|
+
|
140
|
+
#isKeyboardActivationViaEnter(event: Event): boolean {
|
141
|
+
return (
|
142
|
+
event instanceof KeyboardEvent &&
|
143
|
+
event.type === 'keydown' &&
|
144
|
+
!(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) &&
|
145
|
+
event.key === 'Enter'
|
146
|
+
)
|
147
|
+
}
|
148
|
+
|
149
|
+
#isKeyboardActivationViaSpace(event: Event): boolean {
|
137
150
|
return (
|
138
151
|
event instanceof KeyboardEvent &&
|
139
152
|
event.type === 'keydown' &&
|
140
153
|
!(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) &&
|
141
|
-
|
154
|
+
event.key === ' '
|
142
155
|
)
|
143
156
|
}
|
144
157
|
|
@@ -172,11 +185,6 @@ export class ActionMenuElement extends HTMLElement {
|
|
172
185
|
return
|
173
186
|
}
|
174
187
|
|
175
|
-
// Ignore events within dialogs within menus
|
176
|
-
if ((event.target as Element)?.closest('dialog') || (event.target as Element)?.closest('modal-dialog')) {
|
177
|
-
return
|
178
|
-
}
|
179
|
-
|
180
188
|
if (event.type === 'focusout') {
|
181
189
|
if (this.#invokerBeingClicked) return
|
182
190
|
|
@@ -207,6 +215,16 @@ export class ActionMenuElement extends HTMLElement {
|
|
207
215
|
|
208
216
|
this.#activateItem(event, item)
|
209
217
|
this.#handleItemActivated(event, item)
|
218
|
+
|
219
|
+
// Pressing the space key on a button will cause the page to scroll unless preventDefault()
|
220
|
+
// is called. Unfortunately, calling preventDefault() will also skip form submission. The
|
221
|
+
// code below therefore only calls preventDefault() if the button submits a form and the
|
222
|
+
// button is being activated by the space key.
|
223
|
+
if (item.getAttribute('type') === 'submit' && this.#isKeyboardActivationViaSpace(event)) {
|
224
|
+
event.preventDefault()
|
225
|
+
item.closest('form')?.submit()
|
226
|
+
}
|
227
|
+
|
210
228
|
return
|
211
229
|
}
|
212
230
|
|
@@ -283,12 +301,6 @@ export class ActionMenuElement extends HTMLElement {
|
|
283
301
|
}
|
284
302
|
|
285
303
|
this.#updateInput()
|
286
|
-
|
287
|
-
if (event instanceof KeyboardEvent && event.target instanceof HTMLButtonElement) {
|
288
|
-
// prevent buttons from being clicked twice
|
289
|
-
event.preventDefault()
|
290
|
-
return
|
291
|
-
}
|
292
304
|
}
|
293
305
|
|
294
306
|
#activateItem(event: Event, item: Element) {
|
@@ -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
|
@@ -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}
|
@@ -2,7 +2,20 @@
|
|
2
2
|
"name": "alpha/segmented_control",
|
3
3
|
"selectors": [
|
4
4
|
".SegmentedControl",
|
5
|
+
".SegmentedControl--iconOnly .Button--iconOnly.Button--large",
|
6
|
+
".SegmentedControl--iconOnly .Button--iconOnly.Button--medium",
|
7
|
+
".SegmentedControl--iconOnly .Button--iconOnly.Button--small",
|
8
|
+
".SegmentedControl--small",
|
9
|
+
".SegmentedControl--small .SegmentedControl-item",
|
10
|
+
".SegmentedControl--small .SegmentedControl-item .Button",
|
11
|
+
".SegmentedControl--small.SegmentedControl--iconOnly .SegmentedControl-item",
|
12
|
+
".SegmentedControl--medium .SegmentedControl-item",
|
13
|
+
".SegmentedControl--medium.SegmentedControl--iconOnly .SegmentedControl-item",
|
14
|
+
".SegmentedControl--large .SegmentedControl-item",
|
15
|
+
".SegmentedControl--large .SegmentedControl-item .Button",
|
16
|
+
".SegmentedControl--large.SegmentedControl--iconOnly .SegmentedControl-item",
|
5
17
|
".SegmentedControl-item",
|
18
|
+
".SegmentedControl-item .Button-withTooltip",
|
6
19
|
".SegmentedControl-item .Button--invisible:hover:not(:disabled)",
|
7
20
|
".SegmentedControl-item .Button--invisible:active:not(:disabled)",
|
8
21
|
".SegmentedControl-item.SegmentedControl-item--selected",
|
@@ -14,20 +27,8 @@
|
|
14
27
|
".SegmentedControl-item:not(:first-child):before",
|
15
28
|
".SegmentedControl-item .Button",
|
16
29
|
".SegmentedControl-item .Button:focus-visible",
|
17
|
-
".SegmentedControl-item .Button--small",
|
18
|
-
".SegmentedControl-item .Button--small.Button--iconOnly",
|
19
|
-
".SegmentedControl-item .Button--small.Button--iconOnly:before",
|
20
|
-
".SegmentedControl-item .Button--medium",
|
21
|
-
".SegmentedControl-item .Button--medium.Button--iconOnly",
|
22
|
-
".SegmentedControl-item .Button--medium.Button--iconOnly:before",
|
23
|
-
".SegmentedControl-item .Button--large",
|
24
|
-
".SegmentedControl-item .Button--large.Button--iconOnly",
|
25
|
-
".SegmentedControl-item .Button--large.Button--iconOnly:before",
|
26
|
-
".SegmentedControl-item .Button--iconOnly",
|
27
30
|
".SegmentedControl-item .Button--invisible.Button--invisible-noVisuals .Button-label",
|
28
31
|
".SegmentedControl--fullWidth",
|
29
|
-
".SegmentedControl--fullWidth .SegmentedControl-item"
|
30
|
-
".SegmentedControl--fullWidth .Button--iconOnly",
|
31
|
-
".SegmentedControl--fullWidth .Button-withTooltip"
|
32
|
+
".SegmentedControl--fullWidth .SegmentedControl-item"
|
32
33
|
]
|
33
34
|
}
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"sources":["segmented_control.pcss"
|
1
|
+
{"version":3,"sources":["segmented_control.pcss"],"names":[],"mappings":"AAEA,kBACE,yEAAkE,CAIlE,mFAAqF,CACrF,gDAAyC,CAHzC,mBAAoB,CACpB,eAGF,CAGE,qLAIE,0BAA4B,CAD5B,UAEF,CAKF,yBACE,2EAeF,CAbE,gDACE,wCAKF,CAHE,wDACE,4GACF,CAIA,2EACE,uCACF,CAKF,iDACE,sCACF,CAGE,4EACE,qCACF,CAKF,gDACE,uCAKF,CAHE,wDACE,4GACF,CAIA,2EACE,sCACF,CAMJ,uBAIE,4DAAiD,CACjD,gDAAyC,CAHzC,mBAAoB,CAIpB,sCAAkC,CAHlC,sBAAuB,CAIvB,4CAA6C,CAN7C,iBAoFF,CA5EE,2CACE,UACF,CAIE,+DACE,iGACF,CAEA,gEACE,mGACF,CAIF,uDACE,yFAA2F,CAC3F,sGAiBF,CAfE,+DACE,gDAKF,CAHE,qEACE,wBACF,CAGF,8DACE,4BACF,CAEA,qFACE,kBACF,CAIF,0DAKE,0BAA2B,CAJ3B,aAAc,CAEd,gDAA6C,CAD7C,QAAS,CAET,iBAEF,CAIE,gDAME,kHAAqE,CADrE,UAAW,CAHX,gBAAiB,CAEjB,wDAAiD,CADjD,qDAA8C,CAF9C,iBAMF,CAIF,+BAGE,QAAS,CAET,+FAA0F,CAD1F,8CAA2C,CAH3C,WAAY,CAKZ,6GAAuG,CAJvG,UAUF,CAJE,6CAEE,+FAA0F,CAD1F,sGAEF,CAGF,oFACE,8DACF,CAIF,6BACE,YAMF,CAJE,oDACE,MAAO,CACP,sBACF","file":"segmented_control.css","sourcesContent":["/* SegmentedControl */\n\n.SegmentedControl {\n --segmentedControl-item-padding: var(--control-small-paddingBlock);\n\n display: inline-flex;\n list-style: none;\n background-color: var(--controlTrack-bgColor-rest, var(--color-segmented-control-bg));\n border-radius: var(--borderRadius-medium);\n}\n\n.SegmentedControl--iconOnly {\n & .Button--iconOnly.Button--small,\n & .Button--iconOnly.Button--medium,\n & .Button--iconOnly.Button--large {\n width: 100%;\n padding-inline: 0 !important;\n }\n}\n\n/* sizes */\n\n.SegmentedControl--small {\n --segmentedControl-item-padding: var(--control-xsmall-paddingBlock);\n\n & .SegmentedControl-item {\n height: var(--control-small-size);\n\n & .Button {\n padding-inline: calc(var(--control-xsmall-paddingInline-normal) - var(--segmentedControl-item-padding));\n }\n }\n\n &.SegmentedControl--iconOnly {\n & .SegmentedControl-item {\n width: var(--control-small-size);\n }\n }\n}\n\n.SegmentedControl--medium {\n & .SegmentedControl-item {\n height: var(--control-medium-size);\n }\n\n &.SegmentedControl--iconOnly {\n & .SegmentedControl-item {\n width: var(--control-medium-size);\n }\n }\n}\n\n.SegmentedControl--large {\n & .SegmentedControl-item {\n height: var(--control-large-size);\n\n & .Button {\n padding-inline: calc(var(--control-large-paddingInline-normal) - var(--segmentedControl-item-padding));\n }\n }\n\n &.SegmentedControl--iconOnly {\n & .SegmentedControl-item {\n width: var(--control-large-size);\n }\n }\n}\n\n/* item */\n\n.SegmentedControl-item {\n position: relative;\n display: inline-flex;\n justify-content: center;\n border: var(--borderWidth-thin) solid transparent;\n border-radius: var(--borderRadius-medium);\n height: var(--control-medium-size);\n padding: var(--segmentedControl-item-padding);\n\n & .Button-withTooltip {\n width: 100%;\n }\n\n /* button color overrides */\n & .Button--invisible {\n &:hover:not(:disabled) {\n background-color: var(--controlTrack-bgColor-hover, var(--color-action-list-item-default-hover-bg));\n }\n\n &:active:not(:disabled) {\n background-color: var(--controlTrack-bgColor-active, var(--color-action-list-item-default-active-bg));\n }\n }\n\n /* Selected ---------------------------------------- */\n &.SegmentedControl-item--selected {\n background-color: var(--controlKnob-bgColor-rest, var(--color-segmented-control-button-bg));\n border-color: var(--controlKnob-borderColor-rest, var(--color-segmented-control-button-selected-border));\n\n & .Button {\n font-weight: var(--base-text-weight-semibold);\n\n &:hover {\n background-color: transparent;\n }\n }\n\n &::before {\n border-color: transparent !important;\n }\n\n & + .SegmentedControl-item::before {\n border-color: transparent;\n }\n }\n\n /* renders a visibly hidden \"copy\" of the text in bold, reserving box space for when text becomes bold on selected */\n & .Button-label[data-content]::before {\n display: block;\n height: 0;\n font-weight: var(--base-text-weight-semibold);\n visibility: hidden;\n content: attr(data-content);\n }\n\n /* Separator lines */\n &:not(:first-child) {\n &::before {\n position: absolute;\n inset: 0 0 0 -1px;\n margin-top: var(--control-medium-paddingBlock);\n margin-bottom: var(--control-medium-paddingBlock);\n content: '';\n border-left: var(--borderWidth-thin) solid var(--borderColor-default);\n }\n }\n\n /* Button ----------------------------------------- */\n & .Button {\n height: 100%;\n width: 100%;\n border: 0;\n font-weight: var(--base-text-weight-normal);\n border-radius: calc(var(--borderRadius-medium) - var(--segmentedControl-item-padding) / 2);\n padding-inline: calc(var(--control-medium-paddingInline-normal) - var(--segmentedControl-item-padding));\n\n &:focus-visible {\n outline-offset: calc(var(--segmentedControl-item-padding) - var(--borderWidth-thin));\n border-radius: calc(var(--borderRadius-medium) - var(--segmentedControl-item-padding) / 1);\n }\n }\n\n & .Button--invisible.Button--invisible-noVisuals .Button-label {\n color: var(--button-default-fgColor-rest);\n }\n}\n\n/* fullWidth */\n.SegmentedControl--fullWidth {\n display: flex;\n\n & .SegmentedControl-item {\n flex: 1;\n justify-content: center;\n }\n}\n"]}
|