@primer/view-components
Version:
ViewComponents for the Primer Design System
495 lines (494 loc) • 25.1 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
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");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _ActionMenuElement_instances, _ActionMenuElement_abortController, _ActionMenuElement_originalLabel, _ActionMenuElement_inputName, _ActionMenuElement_invokerBeingClicked, _ActionMenuElement_intersectionObserver, _ActionMenuElement_softDisableItems, _ActionMenuElement_potentiallyDisallowActivation, _ActionMenuElement_isAnchorActivationViaSpace, _ActionMenuElement_isActivation, _ActionMenuElement_handleInvokerActivated, _ActionMenuElement_handleDialogItemActivated, _ActionMenuElement_handleItemActivated, _ActionMenuElement_handleIncludeFragmentReplaced, _ActionMenuElement_handleFocusOut, _ActionMenuElement_show, _ActionMenuElement_hide, _ActionMenuElement_isOpen, _ActionMenuElement_setDynamicLabel, _ActionMenuElement_updateInput, _ActionMenuElement_firstItem_get;
import { controller, target } from '@github/catalyst';
import '@oddbird/popover-polyfill';
import { observeMutationsUntilConditionMet } from '../../utils';
const validSelectors = ['[role="menuitem"]', '[role="menuitemcheckbox"]', '[role="menuitemradio"]'];
const menuItemSelectors = validSelectors.map(selector => `:not([hidden]) > ${selector}`);
let ActionMenuElement = class ActionMenuElement extends HTMLElement {
constructor() {
super(...arguments);
_ActionMenuElement_instances.add(this);
_ActionMenuElement_abortController.set(this, void 0);
_ActionMenuElement_originalLabel.set(this, '');
_ActionMenuElement_inputName.set(this, '');
_ActionMenuElement_invokerBeingClicked.set(this, false);
_ActionMenuElement_intersectionObserver.set(this, void 0);
}
get selectVariant() {
return this.getAttribute('data-select-variant');
}
set selectVariant(variant) {
if (variant) {
this.setAttribute('data-select-variant', variant);
}
else {
this.removeAttribute('variant');
}
}
get dynamicLabelPrefix() {
const prefix = this.getAttribute('data-dynamic-label-prefix');
if (!prefix)
return '';
return `${prefix}:`;
}
set dynamicLabelPrefix(value) {
this.setAttribute('data-dynamic-label', value);
}
get dynamicLabel() {
return this.hasAttribute('data-dynamic-label');
}
set dynamicLabel(value) {
this.toggleAttribute('data-dynamic-label', value);
}
get popoverElement() {
return this.invokerElement?.popoverTargetElement || null;
}
get invokerElement() {
const id = this.querySelector('[role=menu]')?.id;
if (!id)
return null;
for (const el of this.querySelectorAll(`[aria-controls]`)) {
if (el.getAttribute('aria-controls') === id) {
return el;
}
}
return null;
}
get invokerLabel() {
if (!this.invokerElement)
return null;
return this.invokerElement.querySelector('.Button-label');
}
get selectedItems() {
const selectedItems = this.querySelectorAll('[aria-checked=true]');
const results = [];
for (const selectedItem of selectedItems) {
const labelEl = selectedItem.querySelector('.ActionListItem-label');
results.push({
label: labelEl?.textContent,
value: selectedItem?.getAttribute('data-value'),
element: selectedItem,
});
}
return results;
}
connectedCallback() {
const { signal } = (__classPrivateFieldSet(this, _ActionMenuElement_abortController, new AbortController(), "f"));
this.addEventListener('keydown', this, { signal });
this.addEventListener('click', this, { signal });
this.addEventListener('mouseover', this, { signal });
this.addEventListener('focusout', this, { signal });
this.addEventListener('mousedown', this, { signal });
this.popoverElement?.addEventListener('toggle', this, { signal });
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_setDynamicLabel).call(this);
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_updateInput).call(this);
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_softDisableItems).call(this);
if (this.includeFragment) {
this.includeFragment.addEventListener('include-fragment-replaced', this, {
signal,
});
}
// The code below updates the menu (i.e. overlay) position whenever the invoker button
// changes position within its scroll container.
//
// See: https://github.com/primer/view_components/issues/3175
const scrollUpdater = () => {
if (__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_isOpen).call(this)) {
this.overlay?.update();
}
};
__classPrivateFieldSet(this, _ActionMenuElement_intersectionObserver, new IntersectionObserver(entries => {
for (const entry of entries) {
const elem = entry.target;
if (elem === this.invokerElement) {
if (entry.isIntersecting) {
// eslint-disable-next-line github/prefer-observers
window.addEventListener('scroll', scrollUpdater, { capture: true });
}
else {
window.removeEventListener('scroll', scrollUpdater, { capture: true });
}
}
}
}), "f");
observeMutationsUntilConditionMet(this, () => Boolean(this.invokerElement), () => __classPrivateFieldGet(this, _ActionMenuElement_intersectionObserver, "f").observe(this.invokerElement));
// If there's no include fragment, then no async fetching will occur and we can
// mark the component as ready.
if (!this.includeFragment) {
this.setAttribute('data-ready', 'true');
}
}
disconnectedCallback() {
__classPrivateFieldGet(this, _ActionMenuElement_abortController, "f").abort();
}
handleEvent(event) {
const targetIsInvoker = this.invokerElement?.contains(event.target);
const eventIsActivation = __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_isActivation).call(this, event);
if (event.type === 'toggle' && event.newState === 'open') {
window.requestAnimationFrame(() => {
__classPrivateFieldGet(this, _ActionMenuElement_instances, "a", _ActionMenuElement_firstItem_get)?.focus();
});
}
if (targetIsInvoker && event.type === 'mousedown') {
__classPrivateFieldSet(this, _ActionMenuElement_invokerBeingClicked, true, "f");
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) {
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_handleInvokerActivated).call(this, event);
__classPrivateFieldSet(this, _ActionMenuElement_invokerBeingClicked, false, "f");
return;
}
if (event.type === 'focusout') {
if (__classPrivateFieldGet(this, _ActionMenuElement_invokerBeingClicked, "f"))
return;
// Give the browser time to focus the next element
requestAnimationFrame(() => {
if (!this.contains(document.activeElement) || document.activeElement === this.invokerElement) {
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_handleFocusOut).call(this);
}
});
return;
}
const item = event.target.closest(menuItemSelectors.join(','));
const targetIsItem = item !== null;
if (targetIsItem && eventIsActivation) {
if (__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_potentiallyDisallowActivation).call(this, event))
return;
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)) {
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_handleDialogItemActivated).call(this, event, dialog);
return;
}
}
// Pressing the space key on a link will cause the page to scroll unless preventDefault() is called.
// We then click it manually to navigate.
if (__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_isAnchorActivationViaSpace).call(this, event)) {
event.preventDefault();
item.click();
}
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_handleItemActivated).call(this, item);
return;
}
if (event.type === 'include-fragment-replaced') {
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_handleIncludeFragmentReplaced).call(this);
}
}
get items() {
return Array.from(this.querySelectorAll(menuItemSelectors.join(',')));
}
getItemById(itemId) {
return this.querySelector(`li[data-item-id="${itemId}"`);
}
isItemDisabled(item) {
if (item) {
return item.classList.contains('ActionListItem--disabled');
}
else {
return false;
}
}
disableItem(item) {
if (item) {
item.classList.add('ActionListItem--disabled');
item.querySelector('.ActionListContent').setAttribute('aria-disabled', 'true');
}
}
enableItem(item) {
if (item) {
item.classList.remove('ActionListItem--disabled');
item.querySelector('.ActionListContent').removeAttribute('aria-disabled');
}
}
isItemHidden(item) {
if (item) {
return item.hasAttribute('hidden');
}
else {
return false;
}
}
hideItem(item) {
if (item) {
item.setAttribute('hidden', 'hidden');
}
}
showItem(item) {
if (item) {
item.removeAttribute('hidden');
}
}
isItemChecked(item) {
if (item) {
return item.querySelector('.ActionListContent').getAttribute('aria-checked') === 'true';
}
else {
return false;
}
}
checkItem(item) {
if (item && (this.selectVariant === 'single' || this.selectVariant === 'multiple')) {
const itemContent = item.querySelector('.ActionListContent');
const ariaChecked = itemContent.getAttribute('aria-checked') === 'true';
if (!ariaChecked) {
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_handleItemActivated).call(this, itemContent);
}
}
}
uncheckItem(item) {
if (item && (this.selectVariant === 'single' || this.selectVariant === 'multiple')) {
const itemContent = item.querySelector('.ActionListContent');
const ariaChecked = itemContent.getAttribute('aria-checked') === 'true';
if (ariaChecked) {
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_handleItemActivated).call(this, itemContent);
}
}
}
};
_ActionMenuElement_abortController = new WeakMap();
_ActionMenuElement_originalLabel = new WeakMap();
_ActionMenuElement_inputName = new WeakMap();
_ActionMenuElement_invokerBeingClicked = new WeakMap();
_ActionMenuElement_intersectionObserver = new WeakMap();
_ActionMenuElement_instances = new WeakSet();
_ActionMenuElement_softDisableItems = function _ActionMenuElement_softDisableItems() {
const { signal } = __classPrivateFieldGet(this, _ActionMenuElement_abortController, "f");
for (const item of this.querySelectorAll(validSelectors.join(','))) {
item.addEventListener('click', __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_potentiallyDisallowActivation).bind(this), { signal });
item.addEventListener('keydown', __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_potentiallyDisallowActivation).bind(this), { signal });
}
};
_ActionMenuElement_potentiallyDisallowActivation = function _ActionMenuElement_potentiallyDisallowActivation(event) {
if (!__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_isActivation).call(this, event))
return false;
const item = event.target.closest(menuItemSelectors.join(','));
if (!item)
return false;
if (item.getAttribute('aria-disabled')) {
event.preventDefault();
/* eslint-disable-next-line no-restricted-syntax */
event.stopPropagation();
/* eslint-disable-next-line no-restricted-syntax */
event.stopImmediatePropagation();
return true;
}
return false;
};
_ActionMenuElement_isAnchorActivationViaSpace = function _ActionMenuElement_isAnchorActivationViaSpace(event) {
return (event.target instanceof HTMLAnchorElement &&
event instanceof KeyboardEvent &&
event.type === 'keydown' &&
!(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) &&
event.key === ' ');
};
_ActionMenuElement_isActivation = function _ActionMenuElement_isActivation(event) {
// Some browsers fire MouseEvents (Firefox) and others fire PointerEvents (Chrome). Activating an item via
// enter or space counterintuitively fires one of these rather than a KeyboardEvent. Since PointerEvent
// inherits from MouseEvent, it is enough to check for MouseEvent here.
return (event instanceof MouseEvent && event.type === 'click') || __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_isAnchorActivationViaSpace).call(this, event);
};
_ActionMenuElement_handleInvokerActivated = function _ActionMenuElement_handleInvokerActivated(event) {
event.preventDefault();
/* eslint-disable-next-line no-restricted-syntax */
event.stopPropagation();
if (__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_isOpen).call(this)) {
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_hide).call(this);
}
else {
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_show).call(this);
}
};
_ActionMenuElement_handleDialogItemActivated = function _ActionMenuElement_handleDialogItemActivated(event, dialog) {
if (this.contains(dialog)) {
this.querySelector('.ActionListWrap').style.display = 'none';
}
const dialog_controller = new AbortController();
const { signal } = dialog_controller;
const handleDialogClose = () => {
dialog_controller.abort();
if (this.contains(dialog)) {
this.querySelector('.ActionListWrap').style.display = '';
if (__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_isOpen).call(this)) {
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_hide).call(this);
}
}
const activeElement = this.ownerDocument.activeElement;
const lostFocus = this.ownerDocument.activeElement === this.ownerDocument.body;
const focusInClosedMenu = this.contains(activeElement);
const focusInDialog = dialog.contains(activeElement);
if (lostFocus || focusInClosedMenu || focusInDialog) {
setTimeout(() => {
// if the activeElement has changed after a task, then it's likely
// that other JS has tried to shift focus. We should respect that
// focus shift as long as it's not back at the document.
const newActiveElement = this.ownerDocument.activeElement;
if (newActiveElement === activeElement || newActiveElement === this.ownerDocument.body) {
this.invokerElement?.focus();
}
}, 0);
}
};
// a modal <dialog> element will close all popovers
dialog.addEventListener('close', handleDialogClose, { signal });
dialog.addEventListener('cancel', handleDialogClose, { signal });
};
_ActionMenuElement_handleItemActivated = function _ActionMenuElement_handleItemActivated(item) {
// 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 (__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_isOpen).call(this)) {
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_hide).call(this);
}
});
}
// 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;
const ariaChecked = item.getAttribute('aria-checked');
const checked = ariaChecked !== 'true';
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');
}
}
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_setDynamicLabel).call(this);
}
else {
// multi-select mode allows unchecking a checked item
item.setAttribute('aria-checked', `${checked}`);
}
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_updateInput).call(this);
this.dispatchEvent(new CustomEvent('itemActivated', {
bubbles: true,
detail: { item: item.parentElement, checked: this.isItemChecked(item.parentElement) },
}));
};
_ActionMenuElement_handleIncludeFragmentReplaced = function _ActionMenuElement_handleIncludeFragmentReplaced() {
__classPrivateFieldGet(this, _ActionMenuElement_instances, "a", _ActionMenuElement_firstItem_get)?.focus();
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_softDisableItems).call(this);
// async items have loaded, so component is ready
this.setAttribute('data-ready', 'true');
};
_ActionMenuElement_handleFocusOut = function _ActionMenuElement_handleFocusOut() {
__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_hide).call(this);
};
_ActionMenuElement_show = function _ActionMenuElement_show() {
this.popoverElement?.showPopover();
};
_ActionMenuElement_hide = function _ActionMenuElement_hide() {
this.popoverElement?.hidePopover();
};
_ActionMenuElement_isOpen = function _ActionMenuElement_isOpen() {
return this.popoverElement?.matches(':popover-open');
};
_ActionMenuElement_setDynamicLabel = function _ActionMenuElement_setDynamicLabel() {
if (this.selectVariant !== 'single')
return;
if (!this.dynamicLabel)
return;
const invokerLabel = this.invokerLabel;
if (!invokerLabel)
return;
__classPrivateFieldSet(this, _ActionMenuElement_originalLabel, __classPrivateFieldGet(this, _ActionMenuElement_originalLabel, "f") || (invokerLabel.textContent || ''), "f");
const itemLabel = this.querySelector('[aria-checked=true] .ActionListItem-label');
if (itemLabel && this.dynamicLabel) {
const prefixSpan = document.createElement('span');
prefixSpan.classList.add('color-fg-muted');
const contentSpan = document.createElement('span');
prefixSpan.textContent = this.dynamicLabelPrefix;
contentSpan.textContent = itemLabel.textContent || '';
invokerLabel.replaceChildren(prefixSpan, contentSpan);
}
else {
invokerLabel.textContent = __classPrivateFieldGet(this, _ActionMenuElement_originalLabel, "f");
}
};
_ActionMenuElement_updateInput = function _ActionMenuElement_updateInput() {
if (this.selectVariant === 'single') {
const input = this.querySelector(`[data-list-inputs=true] input`);
if (!input)
return;
const selectedItem = this.selectedItems[0];
if (selectedItem) {
input.value = (selectedItem.value || selectedItem.label || '').trim();
input.removeAttribute('disabled');
}
else {
input.setAttribute('disabled', 'disabled');
}
}
else if (this.selectVariant !== 'none') {
// multiple select variant
const inputList = this.querySelector('[data-list-inputs=true]');
if (!inputList)
return;
const inputs = inputList.querySelectorAll('input');
if (inputs.length > 0) {
__classPrivateFieldSet(this, _ActionMenuElement_inputName, __classPrivateFieldGet(this, _ActionMenuElement_inputName, "f") || inputs[0].name, "f");
}
for (const selectedItem of this.selectedItems) {
const newInput = document.createElement('input');
newInput.setAttribute('data-list-input', 'true');
newInput.type = 'hidden';
newInput.autocomplete = 'off';
newInput.name = __classPrivateFieldGet(this, _ActionMenuElement_inputName, "f");
newInput.value = (selectedItem.value || selectedItem.label || '').trim();
inputList.append(newInput);
}
for (const input of inputs) {
input.remove();
}
}
};
_ActionMenuElement_firstItem_get = function _ActionMenuElement_firstItem_get() {
return this.querySelector(menuItemSelectors.join(','));
};
__decorate([
target
], ActionMenuElement.prototype, "includeFragment", void 0);
__decorate([
target
], ActionMenuElement.prototype, "overlay", void 0);
ActionMenuElement = __decorate([
controller
], ActionMenuElement);
export { ActionMenuElement };
if (!window.customElements.get('action-menu')) {
window.ActionMenuElement = ActionMenuElement;
window.customElements.define('action-menu', ActionMenuElement);
}