diff --git a/app/javascript/packs/admin/users/memberIndex.js b/app/javascript/packs/admin/users/memberIndex.js new file mode 100644 index 000000000..b79b88342 --- /dev/null +++ b/app/javascript/packs/admin/users/memberIndex.js @@ -0,0 +1,107 @@ +import { openDropdown, closeDropdown } from '@utilities/dropdownUtils'; +import { copyToClipboard } from '@utilities/runtime'; + +// We present up to 50 users in the UI at once, and for performance reasons we don't want to add individual click listeners to each dropdown menu or inner menu item +// Instead we listen for click events anywhere in the table, and identify required actions based on data attributes of the target +document + .getElementById('member-index-content') + ?.addEventListener('click', ({ target }) => { + const { + dataset: { copyEmail, toggleDropdown }, + } = target; + + if (copyEmail) { + handleEmailCopy(copyEmail); + return; + } + + if (toggleDropdown) { + handleDropdownToggle({ + triggerElementId: target.getAttribute('id'), + dropdownContentId: toggleDropdown, + }); + return; + } + }); + +// We keep track of the currently opened dropdown to make sure we only ever have one open at a time +let currentlyOpenDropdownId; + +const handleDropdownToggle = ({ triggerElementId, dropdownContentId }) => { + const triggerButton = document.getElementById(triggerElementId); + + const isCurrentlyOpen = + triggerButton.getAttribute('aria-expanded') === 'true'; + + if (isCurrentlyOpen) { + closeDropdown({ triggerElementId, dropdownContentId }); + triggerButton.focus(); + currentlyOpenDropdownId = null; + } else { + closeCurrentlyOpenDropdown(); + openDropdown({ triggerElementId, dropdownContentId }); + currentlyOpenDropdownId = dropdownContentId; + } +}; + +/** + * Make sure any currently opened dropdown is closed + */ +const closeCurrentlyOpenDropdown = (focusTrigger = false) => { + if (!currentlyOpenDropdownId) { + return; + } + const triggerButton = document.querySelector( + `[aria-controls='${currentlyOpenDropdownId}']`, + ); + + closeDropdown({ + dropdownContentId: currentlyOpenDropdownId, + triggerElementId: triggerButton?.getAttribute('id'), + }); + + if (focusTrigger) { + triggerButton.focus(); + } +}; + +// We listen for key up events on this page to make sure users can close the dropdowns via keyboard +document.addEventListener('keyup', ({ key }) => { + if (key === 'Escape') { + closeCurrentlyOpenDropdown(true); + return; + } + if (key === 'Tab') { + // If we're not inside a dropdown any more, let's close it + const closestDropdown = document.activeElement.closest('.crayons-dropdown'); + if (!closestDropdown) { + closeCurrentlyOpenDropdown(); + } + } +}); + +/** + * Helper function to copy the given text to the clipboard and display a snackbar confirmation. + * + * @param {string} copyEmail The email to copy + */ +const handleEmailCopy = (copyEmail) => { + copyToClipboard(copyEmail) + .then(() => { + document.dispatchEvent( + new CustomEvent('snackbar:add', { + detail: { message: 'Copied to clipboard' }, + }), + ); + closeCurrentlyOpenDropdown(true); + }) + .catch(() => { + document.dispatchEvent( + new CustomEvent('snackbar:add', { + detail: { + message: 'Unable to copy the text. Try reloading the page', + }, + }), + ); + }); +}; diff --git a/app/javascript/utilities/dropdownUtils.js b/app/javascript/utilities/dropdownUtils.js index 4ab8bd402..b46387b7e 100644 --- a/app/javascript/utilities/dropdownUtils.js +++ b/app/javascript/utilities/dropdownUtils.js @@ -63,7 +63,7 @@ export const INTERACTIVE_ELEMENTS_QUERY = * @param {string} args.triggerElementId The id of the button which activates the dropdown * @param {string} args.dropdownContent The id of the dropdown content element */ -const openDropdown = ({ triggerElementId, dropdownContentId }) => { +export const openDropdown = ({ triggerElementId, dropdownContentId }) => { const dropdownContent = document.getElementById(dropdownContentId); const triggerElement = document.getElementById(triggerElementId); @@ -84,7 +84,11 @@ const openDropdown = ({ triggerElementId, dropdownContentId }) => { * @param {string} args.dropdownContent The id of the dropdown content element * @param {Function} args.onClose Optional function for any side-effects which should occur on dropdown close */ -const closeDropdown = ({ triggerElementId, dropdownContentId, onClose }) => { +export const closeDropdown = ({ + triggerElementId, + dropdownContentId, + onClose, +}) => { const dropdownContent = document.getElementById(dropdownContentId); if (!dropdownContent) { diff --git a/app/views/admin/users/_member_index.html.erb b/app/views/admin/users/_member_index.html.erb index a7420517e..2f64622ca 100644 --- a/app/views/admin/users/_member_index.html.erb +++ b/app/views/admin/users/_member_index.html.erb @@ -1,4 +1,5 @@ -