Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bump/primer upstream #41

Merged
merged 60 commits into from
Oct 20, 2023
Merged
Changes from 1 commit
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
f423db6
Release Tracking (#2243)
primer[bot] Sep 28, 2023
b584a6b
Several ActionMenu a11y fixes (#2260)
camertron Sep 29, 2023
ff242c5
Bump @primer/css from 21.0.8 to 21.0.9 in /demo (#2264)
dependabot[bot] Oct 4, 2023
c957996
Bump view_component from 3.5.0 to 3.6.0 in /demo (#2272)
dependabot[bot] Oct 4, 2023
65e2250
Bump actions/cache from 3.3.1 to 3.3.2 (#2266)
dependabot[bot] Oct 4, 2023
c34eba3
Bump tj-actions/changed-files from 38.2.0 to 39.2.0 (#2267)
dependabot[bot] Oct 4, 2023
5c74037
Bump actions/checkout from 3 to 4 (#2270)
dependabot[bot] Oct 4, 2023
50a22a4
Bump octicons from 19.7.0 to 19.8.0 in /demo (#2273)
dependabot[bot] Oct 4, 2023
2795825
Bump @primer/primitives from 7.14.0 to 7.14.1 in /demo (#2268)
dependabot[bot] Oct 4, 2023
70b096f
Bump @rails/actioncable from 7.0.7 to 7.0.8 in /demo (#2263)
dependabot[bot] Oct 4, 2023
7184d76
Adding an optional items_arguments to ActionBar::Item (#2276)
jonrohan Oct 4, 2023
29ea5c9
Generating static files
primer-css Oct 4, 2023
6e2fa37
Bump sprockets from 4.2.0 to 4.2.1 in /demo (#2271)
dependabot[bot] Oct 5, 2023
0da0a82
Bump puma from 6.3.1 to 6.4.0 in /demo (#2269)
dependabot[bot] Oct 5, 2023
e91da0c
Bump @rails/ujs from 7.0.7 to 7.0.8 in /demo (#2265)
dependabot[bot] Oct 5, 2023
dadb984
Bump puma from 6.3.1 to 6.4.0 (#2275)
dependabot[bot] Oct 5, 2023
eb58c73
Bump kuby-kind from 0.2.1 to 0.2.3 in /demo (#2274)
dependabot[bot] Oct 6, 2023
4cb0655
Update kuby.gemfile.lock
jonrohan Oct 6, 2023
a2fe613
Add overflow menu (`ActionMenu`) to focus zone in `ActionBar` (#2259)
TylerJDev Oct 6, 2023
d92acd9
Release Tracking (#2262)
primer[bot] Oct 9, 2023
83b70dd
Add trailing_visual_label to SegmentedControlItem (#2278)
gwwar Oct 10, 2023
47d4c23
Generating static files
primer-css Oct 10, 2023
da3bdb2
Adds 'inactive' state to buttons (#2283)
mperrotti Oct 13, 2023
ecd54c3
Generating static files
primer-css Oct 13, 2023
0e7bc4d
remove release notification (#2285)
gr2m Oct 16, 2023
46d5d9c
Fix tooltips opening when focus is removed while displaying (#2281)
keithamus Oct 17, 2023
2e3a47d
Generating static files
primer-css Oct 17, 2023
3e3d46b
Release Tracking (#2279)
primer[bot] Oct 17, 2023
caf0996
Fix issue where dialog is unable to be closed (#2288)
TylerJDev Oct 18, 2023
02e7f78
`SegmentedControl` alignment fixes (#2286)
langermank Oct 18, 2023
f456256
Generating static files
primer-css Oct 18, 2023
f33eed3
Respect autofocus attributes inside of a dialog when opening a dialog…
jonrohan Oct 18, 2023
ec0e854
Generating static files
primer-css Oct 18, 2023
725bbd9
Allow ActionMenu items to submit multiple values on form submission; …
camertron Oct 18, 2023
6d580e3
Generating static files
primer-css Oct 18, 2023
d7eafca
Fix multi-select behavior when ActionMenus are embedded in dialogs (#…
camertron Oct 18, 2023
46e3ff0
Fix action bar issue where keys don't loop around to end (#2292)
jonrohan Oct 18, 2023
374d10f
Update `Tooltip` design + fix v8 color tokens (#2284)
langermank Oct 19, 2023
f1b3a27
Updating accessibility configs to comply with scorecard (#2294)
jonrohan Oct 19, 2023
f6c084f
Generating static files
primer-css Oct 19, 2023
ab0cb6f
Bump github/accessibility-alt-text-bot from 1.3.0 to 1.4.0 (#2297)
dependabot[bot] Oct 19, 2023
0f4d551
Bump postcss from 8.4.24 to 8.4.31 (#2298)
dependabot[bot] Oct 19, 2023
b970d49
Bump stefanzweifel/git-auto-commit-action from 4 to 5 (#2301)
dependabot[bot] Oct 19, 2023
5c0372d
Bump tj-actions/changed-files from 39.2.0 to 39.2.3 (#2300)
dependabot[bot] Oct 19, 2023
3089def
Bump @primer/primitives from 7.14.1 to 7.15.0 in /demo (#2302)
dependabot[bot] Oct 19, 2023
e4ea39e
Bump @primer/behaviors from 1.3.5 to 1.3.6 (#2304)
dependabot[bot] Oct 19, 2023
1710e69
Bump @rails/actioncable from 7.0.8 to 7.1.1 in /demo (#2299)
dependabot[bot] Oct 19, 2023
c2341bb
Bump markdownlint-cli2 from 0.5.1 to 0.10.0 (#2305)
dependabot[bot] Oct 19, 2023
3c0cdc0
Bump hotwire-livereload from 1.2.2 to 1.3.0 in /demo (#2306)
dependabot[bot] Oct 19, 2023
e94d734
Bump stimulus-rails from 1.1.1 to 1.3.0 in /demo (#2309)
dependabot[bot] Oct 19, 2023
1cc7818
Bump simplecov from 0.21.2 to 0.22.0 (#2310)
dependabot[bot] Oct 19, 2023
78fb09d
Bump importmap-rails from 1.1.5 to 1.2.1 in /demo (#2312)
dependabot[bot] Oct 19, 2023
7dbec9f
Bump spring from 4.1.0 to 4.1.1 in /demo (#2315)
dependabot[bot] Oct 19, 2023
4ec8f57
Bump mocha from 2.0.4 to 2.1.0 (#2316)
dependabot[bot] Oct 19, 2023
951155a
Bump sprockets from 4.2.0 to 4.2.1 (#2317)
dependabot[bot] Oct 19, 2023
fe1bb45
Bump thor from 1.2.1 to 1.3.0 (#2313)
dependabot[bot] Oct 19, 2023
a348e74
Bump cssnano from 5.1.15 to 6.0.1 (#2308)
dependabot[bot] Oct 19, 2023
af6f345
Bump @rails/ujs from 7.0.8 to 7.1.1 in /demo (#2303)
dependabot[bot] Oct 19, 2023
5117b75
Bump turbo-rails from 1.3.2 to 1.5.0 in /demo (#2314)
dependabot[bot] Oct 19, 2023
e2cb8ff
Merge branch 'bump/primer-upstream-ref' into bump/primer-upstream
HDinger Oct 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/sour-colts-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@primer/view-components': patch
---

ActionMenu: hide the menu when focus leaves the component; focus the first list item when the menu is activated with the mouse; allow disabling list items while still permitting them to be focused with the keyboard

<!-- Changed components: Primer::Alpha::ActionMenu -->
283 changes: 204 additions & 79 deletions app/components/primer/alpha/action_menu/action_menu_element.ts
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ export class ActionMenuElement extends HTMLElement {
#abortController: AbortController
#originalLabel = ''
#inputName = ''
#invokerBeingClicked = false

get selectVariant(): SelectVariant {
return this.getAttribute('data-select-variant') as SelectVariant
@@ -52,7 +53,7 @@ export class ActionMenuElement extends HTMLElement {
}

get popoverElement(): HTMLElement | null {
return this.invokerElement?.popoverTargetElement || null
return (this.invokerElement?.popoverTargetElement as HTMLElement) || null
}

get invokerElement(): HTMLButtonElement | null {
@@ -94,8 +95,10 @@ export class ActionMenuElement extends HTMLElement {
this.addEventListener('click', this, {signal})
this.addEventListener('mouseover', this, {signal})
this.addEventListener('focusout', this, {signal})
this.addEventListener('mousedown', this, {signal})
this.#setDynamicLabel()
this.#updateInput()
this.#softDisableItems()

if (this.includeFragment) {
this.includeFragment.addEventListener('include-fragment-replaced', this, {
@@ -104,104 +107,231 @@ export class ActionMenuElement extends HTMLElement {
}
}

#softDisableItems() {
const {signal} = this.#abortController

for (const item of this.#items) {
item.addEventListener('click', this.#potentiallyDisallowActivation.bind(this), {signal})
item.addEventListener('keydown', this.#potentiallyDisallowActivation.bind(this), {signal})
}
}

#potentiallyDisallowActivation(event: Event) {
if (!this.#isActivation(event)) return

const item = (event.target as HTMLElement).closest(menuItemSelectors.join(','))
if (!item) return

if (item.getAttribute('aria-disabled')) {
event.preventDefault()
event.stopPropagation()
event.stopImmediatePropagation()
}
}

disconnectedCallback() {
this.#abortController.abort()
}

#isKeyboardActivation(event: Event): boolean {
return (
event instanceof KeyboardEvent &&
event.type === 'keydown' &&
!(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) &&
(event.key === 'Enter' || event.key === ' ')
)
}

#isMouseActivation(event: Event): boolean {
return event instanceof MouseEvent && event.type === 'click'
}

#isActivation(event: Event): boolean {
return this.#isMouseActivation(event) || this.#isKeyboardActivation(event)
}

handleEvent(event: Event) {
const activation = this.#isActivationKeydown(event)
if (event.target === this.invokerElement && activation) {
if (this.#firstItem) {
event.preventDefault()
this.popoverElement?.showPopover()
this.#firstItem.focus()
return
}
const targetIsInvoker = this.invokerElement?.contains(event.target as HTMLElement)
const eventIsActivation = this.#isActivation(event)

if (targetIsInvoker && event.type === 'mousedown') {
this.#invokerBeingClicked = true
return
}

// Prevent safari bug that dismisses menu on mousedown instead of allowing
// the click event to propagate to the button
if (event.type === 'mousedown') {
event.preventDefault()
return
}

if (targetIsInvoker && eventIsActivation) {
this.#handleInvokerActivated(event)
this.#invokerBeingClicked = false
return
}

// Ignore events within dialogs within menus
if ((event.target as Element)?.closest('dialog') || (event.target as Element)?.closest('modal-dialog')) {
return
}

// If a dialog has been rendered within the menu, we do not want to hide
// the entire menu, as that will also hide the Dialog. Instead we want to
// show the Dialog while hiding just the visible part of the menu.
if ((activation || event.type === 'click') && (event.target as HTMLElement)?.closest('[data-show-dialog-id]')) {
const dialogInvoker = (event.target as HTMLElement)!.closest('[data-show-dialog-id]')
const dialog = this.ownerDocument.getElementById(dialogInvoker?.getAttribute('data-show-dialog-id') || '')
if (dialogInvoker && dialog && this.contains(dialogInvoker) && this.contains(dialog)) {
this.querySelector<HTMLElement>('.ActionListWrap')!.style.display = 'none'
const dialog_controller = new AbortController()
const {signal} = dialog_controller
const handleDialogClose = () => {
dialog_controller.abort()
this.querySelector<HTMLElement>('.ActionListWrap')!.style.display = ''
if (this.popoverElement?.matches(':popover-open')) {
this.popoverElement?.hidePopover()
}
if (event.type === 'focusout') {
if (this.#invokerBeingClicked) return

// Give the browser time to focus the next element
requestAnimationFrame(() => {
if (!this.contains(document.activeElement) || document.activeElement === this.invokerElement) {
this.#handleFocusOut()
}
dialog.addEventListener('close', handleDialogClose, {signal})
dialog.addEventListener('cancel', handleDialogClose, {signal})
return
}
})

return
}

if (!this.popoverElement?.matches(':popover-open')) return
const item = (event.target as Element).closest(menuItemSelectors.join(','))
const targetIsItem = item !== null

if (event.type === 'include-fragment-replaced') {
if (this.#firstItem) this.#firstItem.focus()
} else if (activation || (event instanceof MouseEvent && event.type === 'click')) {
// Hide popover after current event loop to prevent changes in focus from
// altering the target of the event. Not doing this specifically affects
// <a> tags. It causes the event to be sent to the currently focused element
// instead of the anchor, which effectively prevents navigation, i.e. it
// appears as if hitting enter does nothing. Curiously, clicking instead
// works fine.
if (this.selectVariant !== 'multiple') {
setTimeout(() => {
if (this.popoverElement?.matches(':popover-open')) {
this.popoverElement?.hidePopover()
}
})
if (targetIsItem && eventIsActivation) {
const dialogInvoker = item.closest('[data-show-dialog-id]')

if (dialogInvoker) {
const dialog = this.ownerDocument.getElementById(dialogInvoker.getAttribute('data-show-dialog-id') || '')

if (dialog && this.contains(dialogInvoker) && this.contains(dialog)) {
this.#handleDialogItemActivated(event, dialog)
return
}
}

// The rest of the code below deals with single/multiple selection behavior, and should not
// interfere with events fired by menu items whose behavior is specified outside the library.
if (this.selectVariant !== 'multiple' && this.selectVariant !== 'single') return
this.#activateItem(event, item)
this.#handleItemActivated(event, item)
return
}

const item = (event.target as Element).closest(menuItemSelectors.join(','))
if (!item) return
const ariaChecked = item.getAttribute('aria-checked')
const checked = ariaChecked !== 'true'
if (event.type === 'include-fragment-replaced') {
this.#handleIncludeFragmentReplaced()
}
}

if (this.selectVariant === 'single') {
// Only check, never uncheck here. Single-select mode does not allow unchecking a checked item.
if (checked) {
item.setAttribute('aria-checked', 'true')
}
#handleInvokerActivated(event: Event) {
event.preventDefault()
event.stopPropagation()

for (const checkedItem of this.querySelectorAll('[aria-checked]')) {
if (checkedItem !== item) {
checkedItem.setAttribute('aria-checked', 'false')
}
}
if (this.#isOpen()) {
this.#hide()
} else {
this.#show()
this.#firstItem?.focus()
}
}

this.#setDynamicLabel()
} else {
// multi-select mode allows unchecking a checked item
item.setAttribute('aria-checked', `${checked}`)
#handleDialogItemActivated(event: Event, dialog: HTMLElement) {
this.querySelector<HTMLElement>('.ActionListWrap')!.style.display = 'none'
const dialog_controller = new AbortController()
const {signal} = dialog_controller
const handleDialogClose = () => {
dialog_controller.abort()
this.querySelector<HTMLElement>('.ActionListWrap')!.style.display = ''
if (this.#isOpen()) {
this.#hide()
}
}
dialog.addEventListener('close', handleDialogClose, {signal})
dialog.addEventListener('cancel', handleDialogClose, {signal})
}

#handleItemActivated(event: Event, item: Element) {
// Hide popover after current event loop to prevent changes in focus from
// altering the target of the event. Not doing this specifically affects
// <a> tags. It causes the event to be sent to the currently focused element
// instead of the anchor, which effectively prevents navigation, i.e. it
// appears as if hitting enter does nothing. Curiously, clicking instead
// works fine.
if (this.selectVariant !== 'multiple') {
setTimeout(() => {
if (this.#isOpen()) {
this.#hide()
}
})
}

// The rest of the code below deals with single/multiple selection behavior, and should not
// interfere with events fired by menu items whose behavior is specified outside the library.
if (this.selectVariant !== 'multiple' && this.selectVariant !== 'single') return

this.#updateInput()
const ariaChecked = item.getAttribute('aria-checked')
const checked = ariaChecked !== 'true'

if (event instanceof KeyboardEvent && event.target instanceof HTMLButtonElement) {
// prevent buttons from being clicked twice
event.preventDefault()
if (this.selectVariant === 'single') {
// Only check, never uncheck here. Single-select mode does not allow unchecking a checked item.
if (checked) {
item.setAttribute('aria-checked', 'true')
}

for (const checkedItem of this.querySelectorAll('[aria-checked]')) {
if (checkedItem !== item) {
checkedItem.setAttribute('aria-checked', 'false')
}
}

this.#setDynamicLabel()
} else {
// multi-select mode allows unchecking a checked item
item.setAttribute('aria-checked', `${checked}`)
}

this.#updateInput()

if (event instanceof KeyboardEvent && event.target instanceof HTMLButtonElement) {
// prevent buttons from being clicked twice
event.preventDefault()
return
}
}

#activateItem(event: Event, item: Element) {
const eventWillActivateByDefault =
(event instanceof MouseEvent && event.type === 'click') ||
(event instanceof KeyboardEvent &&
event.type === 'keydown' &&
!(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) &&
event.key === 'Enter')

// if the event will result in activating the current item by default, i.e. is a
// mouse click or keyboard enter, bail out
if (eventWillActivateByDefault) return

// otherwise, event will not result in activation by default, so we stop it and
// simulate a click
event.stopPropagation()
const elem = item as HTMLElement
elem.click()
}

#handleIncludeFragmentReplaced() {
if (this.#firstItem) this.#firstItem.focus()
this.#softDisableItems()
}

// Close when focus leaves menu
#handleFocusOut() {
this.#hide()
}

#show() {
this.popoverElement?.showPopover()
}

#hide() {
this.popoverElement?.hidePopover()
}

#isOpen() {
return this.popoverElement?.matches(':popover-open')
}

#setDynamicLabel() {
if (!this.dynamicLabel) return
const invokerLabel = this.invokerLabel
@@ -261,18 +391,13 @@ export class ActionMenuElement extends HTMLElement {
}
}

#isActivationKeydown(event: Event): boolean {
return (
event instanceof KeyboardEvent &&
event.type === 'keydown' &&
!(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) &&
(event.key === 'Enter' || event.key === ' ')
)
}

get #firstItem(): HTMLElement | null {
return this.querySelector(menuItemSelectors.join(','))
}

get #items(): HTMLElement[] {
return Array.from(this.querySelectorAll(menuItemSelectors.join(',')))
}
}

if (!window.customElements.get('action-menu')) {
2 changes: 0 additions & 2 deletions app/components/primer/alpha/action_menu/list.rb
Original file line number Diff line number Diff line change
@@ -111,8 +111,6 @@ def organize_arguments(data: {}, **system_arguments)
system_arguments,
{ aria: { disabled: true } }
)

content_arguments[:disabled] = "" if content_arguments[:tag] == :button
end

{ data: data, **system_arguments, content_arguments: content_arguments }
14 changes: 7 additions & 7 deletions package-lock.json
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@
"@github/auto-check-element": "^5.2.0",
"@github/auto-complete-element": "^3.3.4",
"@github/catalyst": "^1.6.0",
"@github/clipboard-copy-element": "^1.1.2",
"@github/clipboard-copy-element": "^1.3.0",
"@github/details-menu-element": "^1.0.12",
"@github/image-crop-element": "^5.0.0",
"@github/include-fragment-element": "^6.1.1",
5 changes: 4 additions & 1 deletion previews/primer/alpha/action_menu_preview.rb
Original file line number Diff line number Diff line change
@@ -216,7 +216,10 @@ def with_deferred_preloaded_content

# @label With actions
#
def with_actions; end
# @param disable_items toggle
def with_actions(disable_items: false)
render_with_template(locals: { disable_items: disable_items })
end

# @label Single select form
#
Original file line number Diff line number Diff line change
@@ -8,14 +8,15 @@

<%= render(Primer::Alpha::ActionMenu.new) do |component| %>
<% component.with_show_button { "Trigger" } %>
<% component.with_item(label: "Alert", tag: :button, id: "alert-item") %>
<% component.with_item(label: "Navigate", tag: :a, content_arguments: { href: action_menu_landing_path }) %>
<% component.with_item(label: "Copy text", tag: :"clipboard-copy", content_arguments: { value: "Text to copy" }) %>
<% component.with_item(label: "Alert", tag: :button, id: "alert-item", disabled: disable_items) %>
<% component.with_item(label: "Navigate", tag: :a, content_arguments: { href: action_menu_landing_path }, disabled: disable_items) %>
<% component.with_item(label: "Copy text", tag: :"clipboard-copy", content_arguments: { value: "Text to copy" }, disabled: disable_items) %>
<% component.with_item(
label: "Submit form",
href: action_menu_form_action_path,
form_arguments: {
name: "foo", value: "bar", method: :post
}
},
disabled: disable_items
) %>
<% end %>
2 changes: 1 addition & 1 deletion test/components/alpha/action_menu_test.rb
Original file line number Diff line number Diff line change
@@ -115,7 +115,7 @@ def test_disabled
render_preview(:with_disabled_items)

assert_selector("li[aria-disabled=true]") do
assert_selector("button.ActionListContent[aria-disabled=true][disabled=disabled]", text: "Does something")
assert_selector("button.ActionListContent[aria-disabled=true]", text: "Does something")
assert_selector("a.ActionListContent[aria-disabled=true]", text: "Site")
end
end
412 changes: 319 additions & 93 deletions test/system/alpha/action_menu_test.rb

Large diffs are not rendered by default.