diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-01 15:09:50 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-01 15:09:50 +0300 |
commit | 02f6aecd47c847bb2a7d101678813fe5077ae299 (patch) | |
tree | 886df070fd7a3b3f0d364e879b89ea5eb31aeada /app/assets/javascripts | |
parent | 5ffb2b7bcde1c76f939c5dca2ff65bac3404a88f (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
4 files changed, 178 insertions, 50 deletions
diff --git a/app/assets/javascripts/super_sidebar/components/create_menu.vue b/app/assets/javascripts/super_sidebar/components/create_menu.vue index cea87a70878..97d11e74482 100644 --- a/app/assets/javascripts/super_sidebar/components/create_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/create_menu.vue @@ -1,7 +1,7 @@ <script> import { GlDisclosureDropdown, - GlTooltip, + GlTooltipDirective, GlDisclosureDropdownGroup, GlDisclosureDropdownItem, } from '@gitlab/ui'; @@ -22,9 +22,11 @@ export default { GlDisclosureDropdown, GlDisclosureDropdownGroup, GlDisclosureDropdownItem, - GlTooltip, InviteMembersTrigger, }, + directives: { + GlTooltip: GlTooltipDirective, + }, i18n: { createNew: __('Create new...'), }, @@ -59,45 +61,37 @@ export default { </script> <template> - <div> - <gl-disclosure-dropdown - category="tertiary" - icon="plus" - no-caret - text-sr-only - :toggle-text="$options.i18n.createNew" - :toggle-id="$options.toggleId" - :dropdown-offset="dropdownOffset" - data-qa-selector="new_menu_toggle" - data-testid="new-menu-toggle" - @shown="dropdownOpen = true" - @hidden="dropdownOpen = false" - > - <gl-disclosure-dropdown-group - v-for="(group, index) in groups" - :key="group.name" - :bordered="index !== 0" - :group="group" - > - <template v-for="groupItem in group.items"> - <invite-members-trigger - v-if="isInvitedMembers(groupItem)" - :key="`${groupItem.text}-trigger`" - trigger-source="top_nav" - :trigger-element="$options.TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN" - /> - <gl-disclosure-dropdown-item v-else :key="groupItem.text" :item="groupItem" /> - </template> - </gl-disclosure-dropdown-group> - </gl-disclosure-dropdown> - <gl-tooltip - v-if="!dropdownOpen" - :target="`#${$options.toggleId}`" - placement="bottom" - container="#super-sidebar" - noninteractive + <gl-disclosure-dropdown + v-gl-tooltip:super-sidebar.hover.noninteractive.ds500.bottom=" + dropdownOpen ? '' : $options.i18n.createNew + " + category="tertiary" + icon="plus" + no-caret + text-sr-only + :toggle-text="$options.i18n.createNew" + :toggle-id="$options.toggleId" + :dropdown-offset="dropdownOffset" + data-qa-selector="new_menu_toggle" + data-testid="new-menu-toggle" + @shown="dropdownOpen = true" + @hidden="dropdownOpen = false" + > + <gl-disclosure-dropdown-group + v-for="(group, index) in groups" + :key="group.name" + :bordered="index !== 0" + :group="group" > - {{ $options.i18n.createNew }} - </gl-tooltip> - </div> + <template v-for="groupItem in group.items"> + <invite-members-trigger + v-if="isInvitedMembers(groupItem)" + :key="`${groupItem.text}-trigger`" + trigger-source="top_nav" + :trigger-element="$options.TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN" + /> + <gl-disclosure-dropdown-item v-else :key="groupItem.text" :item="groupItem" /> + </template> + </gl-disclosure-dropdown-group> + </gl-disclosure-dropdown> </template> diff --git a/app/assets/javascripts/super_sidebar/components/flyout_menu.vue b/app/assets/javascripts/super_sidebar/components/flyout_menu.vue index fa7960da2f4..441b19cfe23 100644 --- a/app/assets/javascripts/super_sidebar/components/flyout_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/flyout_menu.vue @@ -2,6 +2,23 @@ import { computePosition, autoUpdate, offset, flip, shift } from '@floating-ui/dom'; import NavItem from './nav_item.vue'; +// Flyout menus are shown when the MenuSection's title is hovered with the mouse. +// Their position is dynamically calculated with floating-ui. +// +// Since flyout menus show all NavItems of a section, they can be very long and +// a user might want to move their mouse diagonally from the section title down +// to last nav item in the flyout. But this mouse movement over other sections +// would loose hover and close the flyout, opening another section's flyout. +// To avoid this annoyance, our flyouts come with a "diagonal tolerance". This +// is an area between the current mouse position and the top- and bottom-left +// corner of the flyout itself. While the mouse stays within this area and +// reaches the flyout before a timer expires, the native browser hover stays +// within the component. +// This is done with an transparent SVG positioned left of the flyout menu, +// overlapping the sidebar. The SVG itself ignores pointer events but its two +// triangles, one above the section title, one below, do listen to events, +// keeping hover. + export default { name: 'FlyoutMenu', components: { NavItem }, @@ -15,13 +32,45 @@ export default { required: true, }, }, + data() { + return { + currentMouseX: 0, + flyoutX: 0, + flyoutY: 0, + flyoutHeight: 0, + hoverTimeoutId: null, + showSVG: true, + targetRect: null, + }; + }, cleanupFunction: undefined, + computed: { + topSVGPoints() { + const x = (this.currentMouseX / this.targetRect.width) * 100; + let y = ((this.targetRect.top - this.flyoutY) / this.flyoutHeight) * 100; + y += 1; // overlap title to not loose hover + + return `${x}, ${y} 100, 0 100, ${y}`; + }, + bottomSVGPoints() { + const x = (this.currentMouseX / this.targetRect.width) * 100; + let y = ((this.targetRect.bottom - this.flyoutY) / this.flyoutHeight) * 100; + y -= 1; // overlap title to not loose hover + + return `${x}, ${y} 100, ${y} 100, 100`; + }, + }, + created() { + const target = document.querySelector(`#${this.targetId}`); + target.addEventListener('mousemove', this.onMouseMove); + }, mounted() { const target = document.querySelector(`#${this.targetId}`); const flyout = document.querySelector(`#${this.targetId}-flyout`); + const sidebar = document.querySelector('#super-sidebar'); - function updatePosition() { - return computePosition(target, flyout, { + const updatePosition = () => + computePosition(target, flyout, { middleware: [offset({ alignmentAxis: -12 }), flip(), shift()], placement: 'right-start', strategy: 'fixed', @@ -30,13 +79,46 @@ export default { left: `${x}px`, top: `${y}px`, }); + this.flyoutX = x; + this.flyoutY = y; + this.flyoutHeight = flyout.clientHeight; + + // Flyout coordinates are relative to the sidebar which can be + // shifted down by the performance-bar etc. + // Adjust viewport coordinates from getBoundingClientRect: + const targetRect = target.getBoundingClientRect(); + const sidebarRect = sidebar.getBoundingClientRect(); + this.targetRect = { + top: targetRect.top - sidebarRect.top, + bottom: targetRect.bottom - sidebarRect.top, + width: targetRect.width, + }; }); - } this.$options.cleanupFunction = autoUpdate(target, flyout, updatePosition); }, beforeUnmount() { this.$options.cleanupFunction(); + clearTimeout(this.hoverTimeoutId); + }, + beforeDestroy() { + const target = document.querySelector(`#${this.targetId}`); + target.removeEventListener('mousemove', this.onMouseMove); + }, + methods: { + startHoverTimeout() { + this.hoverTimeoutId = setTimeout(() => { + this.showSVG = false; + this.$emit('mouseleave'); + }, 1000); + }, + stopHoverTimeout() { + clearTimeout(this.hoverTimeoutId); + }, + onMouseMove(e) { + // add some wiggle room to the left of mouse cursor + this.currentMouseX = Math.max(0, e.clientX - 20); + }, }, }; </script> @@ -49,8 +131,8 @@ export default { @mouseleave="$emit('mouseleave')" > <ul - v-if="items.length > 0" class="gl-min-w-20 gl-max-w-34 gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-100 gl-shadow-md gl-bg-white gl-p-2 gl-pb-1 gl-list-style-none" + @mouseenter="showSVG = false" > <nav-item v-for="item of items" @@ -61,5 +143,44 @@ export default { @pin-remove="(itemId) => $emit('pin-remove', itemId)" /> </ul> + <svg + v-if="targetRect && showSVG" + :width="flyoutX" + :height="flyoutHeight" + viewBox="0 0 100 100" + preserveAspectRatio="none" + :style="{ + top: flyoutY + 'px', + }" + > + <polygon + ref="topSVG" + :points="topSVGPoints" + fill="transparent" + @mouseenter="startHoverTimeout" + @mouseleave="stopHoverTimeout" + /> + <polygon + ref="bottomSVG" + :points="bottomSVGPoints" + fill="transparent" + @mouseenter="startHoverTimeout" + @mouseleave="stopHoverTimeout" + /> + </svg> </div> </template> + +<style scoped> +svg { + pointer-events: none; + + position: fixed; + right: 0; +} + +svg polygon, +svg rect { + pointer-events: auto; +} +</style> diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue index d2d45ca7b6e..6b5002e1aa8 100644 --- a/app/assets/javascripts/super_sidebar/components/menu_section.vue +++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue @@ -79,15 +79,26 @@ export default { isExpanded(newIsExpanded) { this.$emit('collapse-toggle', newIsExpanded); this.keepFlyoutClosed = !this.newIsExpanded; + if (!newIsExpanded) { + this.isMouseOverFlyout = false; + } }, }, methods: { handlePointerover(e) { + if (!this.hasFlyout) return; + this.isMouseOverSection = e.pointerType === 'mouse'; }, handlePointerleave() { - this.isMouseOverSection = false; + if (!this.hasFlyout) return; + this.keepFlyoutClosed = false; + // delay state change. otherwise the flyout menu gets removed before it + // has a chance to emit its mouseover event. + setTimeout(() => { + this.isMouseOverSection = false; + }, 5); }, }, }; @@ -129,8 +140,7 @@ export default { </button> <flyout-menu - v-if="hasFlyout" - v-show="isMouseOver && !isExpanded && !keepFlyoutClosed" + v-if="hasFlyout && isMouseOver && !isExpanded && !keepFlyoutClosed && item.items.length > 0" :target-id="`menu-section-button-${itemId}`" :items="item.items" @mouseover="isMouseOverFlyout = true" diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue index ca4412fc504..8112b479c19 100644 --- a/app/assets/javascripts/super_sidebar/components/user_bar.vue +++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue @@ -127,7 +127,10 @@ export default { tooltip-container="super-sidebar" data-testid="super-sidebar-collapse-button" /> - <create-menu v-if="sidebarData.is_logged_in" :groups="sidebarData.create_new_menu_groups" /> + <create-menu + v-if="sidebarData.is_logged_in && sidebarData.create_new_menu_groups.length > 0" + :groups="sidebarData.create_new_menu_groups" + /> <user-menu v-if="sidebarData.is_logged_in" :data="sidebarData" /> |