diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-13 00:10:52 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-13 00:10:52 +0300 |
commit | a60e53c7671c299432f0c255ffaf0e0c9fa9eeab (patch) | |
tree | 9682f6acc0c40bd80beb79b9feec645f6252e8e0 /app/assets/javascripts/super_sidebar | |
parent | 753eb533e509464184ad267fb894d2c08d0d1ba6 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/super_sidebar')
6 files changed, 187 insertions, 23 deletions
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_hover_peek_behavior.vue b/app/assets/javascripts/super_sidebar/components/sidebar_hover_peek_behavior.vue new file mode 100644 index 00000000000..df432a1928a --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/sidebar_hover_peek_behavior.vue @@ -0,0 +1,126 @@ +<script> +import { getCssClassDimensions } from '~/lib/utils/css_utils'; +import Tracking from '~/tracking'; +import { + JS_TOGGLE_EXPAND_CLASS, + SUPER_SIDEBAR_PEEK_OPEN_DELAY, + SUPER_SIDEBAR_PEEK_CLOSE_DELAY, + SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED, + SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN as STATE_WILL_OPEN, + SUPER_SIDEBAR_PEEK_STATE_OPEN as STATE_OPEN, + SUPER_SIDEBAR_PEEK_STATE_WILL_CLOSE as STATE_WILL_CLOSE, +} from '../constants'; + +export default { + name: 'SidebarHoverPeek', + mixins: [Tracking.mixin()], + props: { + isMouseOverSidebar: { + type: Boolean, + required: false, + default: false, + }, + }, + created() { + // Nothing needs to observe these properties, so they are not reactive. + this.state = null; + this.openTimer = null; + this.closeTimer = null; + this.xSidebarEdge = null; + this.isMouseWithinSidebarArea = false; + }, + async mounted() { + await this.$nextTick(); + this.xSidebarEdge = getCssClassDimensions('super-sidebar').width; + document.addEventListener('mousemove', this.onMouseMove); + document.documentElement.addEventListener('mouseleave', this.onDocumentLeave); + document + .querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`) + .addEventListener('mouseenter', this.onMouseEnter); + document + .querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`) + .addEventListener('mouseleave', this.onMouseLeave); + this.changeState(STATE_CLOSED); + }, + beforeDestroy() { + document.removeEventListener('mousemove', this.onMouseMove); + document.documentElement.removeEventListener('mouseleave', this.onDocumentLeave); + document + .querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`) + .removeEventListener('mouseenter', this.onMouseEnter); + document + .querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`) + .removeEventListener('mouseleave', this.onMouseLeave); + this.clearTimers(); + }, + methods: { + onMouseMove({ clientX }) { + if (clientX < this.xSidebarEdge) { + this.isMouseWithinSidebarArea = true; + } else { + this.isMouseWithinSidebarArea = false; + if (!this.isMouseOverSidebar && this.state === STATE_OPEN) { + this.willClose(); + } + } + }, + onDocumentLeave() { + this.isMouseWithinSidebarArea = false; + if (this.state === STATE_OPEN) { + this.willClose(); + } else if (this.state === STATE_WILL_OPEN) { + this.close(); + } + }, + onMouseEnter() { + clearTimeout(this.closeTimer); + this.willOpen(); + }, + onMouseLeave() { + clearTimeout(this.openTimer); + if (this.isMouseWithinSidebarArea || this.isMouseOverSidebar) return; + this.willClose(); + }, + willClose() { + this.changeState(STATE_WILL_CLOSE); + this.closeTimer = setTimeout(this.close, SUPER_SIDEBAR_PEEK_CLOSE_DELAY); + }, + willOpen() { + this.changeState(STATE_WILL_OPEN); + this.openTimer = setTimeout(this.open, SUPER_SIDEBAR_PEEK_OPEN_DELAY); + }, + open() { + this.changeState(STATE_OPEN); + this.clearTimers(); + this.track('nav_hover_peek', { + label: 'nav_sidebar_toggle', + property: 'nav_sidebar', + }); + }, + close() { + if (this.isMouseWithinSidebarArea) return; + this.changeState(STATE_CLOSED); + this.clearTimers(); + }, + clearTimers() { + clearTimeout(this.closeTimer); + clearTimeout(this.openTimer); + }, + /** + * Switches to the new state, and emits a change event. + * + * If the given state is the current state, do nothing. + * + * @param {string} state The state to transition to. + */ + changeState(state) { + if (this.state === state) return; + this.state = state; + this.$emit('change', state); + }, + }, + render() { + return null; + }, +}; +</script> diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue b/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue index ec728b4af9e..a20e37b945a 100644 --- a/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue +++ b/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue @@ -1,12 +1,14 @@ <script> import { getCssClassDimensions } from '~/lib/utils/css_utils'; import Tracking from '~/tracking'; -import { SUPER_SIDEBAR_PEEK_OPEN_DELAY, SUPER_SIDEBAR_PEEK_CLOSE_DELAY } from '../constants'; - -export const STATE_CLOSED = 'closed'; -export const STATE_WILL_OPEN = 'will-open'; -export const STATE_OPEN = 'open'; -export const STATE_WILL_CLOSE = 'will-close'; +import { + SUPER_SIDEBAR_PEEK_OPEN_DELAY, + SUPER_SIDEBAR_PEEK_CLOSE_DELAY, + SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED, + SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN as STATE_WILL_OPEN, + SUPER_SIDEBAR_PEEK_STATE_OPEN as STATE_OPEN, + SUPER_SIDEBAR_PEEK_STATE_WILL_CLOSE as STATE_WILL_CLOSE, +} from '../constants'; export default { name: 'SidebarPeek', diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue index fa366deeac8..2c939487784 100644 --- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue +++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue @@ -4,14 +4,20 @@ import { Mousetrap } from '~/lib/mousetrap'; import { keysFor, TOGGLE_SUPER_SIDEBAR } from '~/behaviors/shortcuts/keybindings'; import { __, s__ } from '~/locale'; import Tracking from '~/tracking'; -import { sidebarState } from '../constants'; +import { + sidebarState, + SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED, + SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN as STATE_WILL_OPEN, + SUPER_SIDEBAR_PEEK_STATE_OPEN as STATE_OPEN, +} from '../constants'; import { isCollapsed, toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager'; import { trackContextAccess } from '../utils'; import UserBar from './user_bar.vue'; import SidebarPortalTarget from './sidebar_portal_target.vue'; import HelpCenter from './help_center.vue'; import SidebarMenu from './sidebar_menu.vue'; -import SidebarPeekBehavior, { STATE_CLOSED, STATE_WILL_OPEN } from './sidebar_peek_behavior.vue'; +import SidebarPeekBehavior from './sidebar_peek_behavior.vue'; +import SidebarHoverPeekBehavior from './sidebar_hover_peek_behavior.vue'; export default { components: { @@ -20,6 +26,7 @@ export default { HelpCenter, SidebarMenu, SidebarPeekBehavior, + SidebarHoverPeekBehavior, SidebarPortalTarget, TrialStatusWidget: () => import('ee_component/contextual_sidebar/components/trial_status_widget.vue'), @@ -43,16 +50,21 @@ export default { sidebarState, showPeekHint: false, isMouseover: false, + breakpoint: null, }; }, computed: { + showOverlay() { + return this.sidebarState.isPeek || this.sidebarState.isHoverPeek; + }, menuItems() { return this.sidebarData.current_menu_items || []; }, peekClasses() { return { 'super-sidebar-peek-hint': this.showPeekHint, - 'super-sidebar-peek': this.sidebarState.isPeek, + 'super-sidebar-peek': this.showOverlay, + 'super-sidebar-has-peeked': this.sidebarState.hasPeeked, }; }, }, @@ -90,6 +102,7 @@ export default { this.sidebarState.isCollapsed = true; this.showPeekHint = false; } else if (state === STATE_WILL_OPEN) { + this.sidebarState.hasPeeked = true; this.sidebarState.isPeek = false; this.sidebarState.isCollapsed = true; this.showPeekHint = true; @@ -99,6 +112,16 @@ export default { this.showPeekHint = false; } }, + onHoverPeekChange(state) { + if (state === STATE_OPEN) { + this.sidebarState.hasPeeked = true; + this.sidebarState.isHoverPeek = true; + this.sidebarState.isCollapsed = false; + } else if (state === STATE_CLOSED) { + this.sidebarState.isHoverPeek = false; + this.sidebarState.isCollapsed = true; + } + }, }, }; </script> @@ -124,7 +147,7 @@ export default { @mouseenter="isMouseover = true" @mouseleave="isMouseover = false" > - <user-bar :has-collapse-button="!sidebarState.isPeek" :sidebar-data="sidebarData" /> + <user-bar :has-collapse-button="!showOverlay" :sidebar-data="sidebarData" /> <div v-if="showTrialStatusWidget" class="gl-px-2 gl-py-2"> <trial-status-widget class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-mb-1 gl-px-3 gl-line-height-normal gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! nav-item-link gl-py-3" @@ -165,13 +188,18 @@ export default { </a> <!-- - Only mount SidebarPeekBehavior if the sidebar is peekable, to avoid + Only mount peek behavior components if the sidebar is peekable, to avoid setting up event listeners unnecessarily. --> <sidebar-peek-behavior - v-if="sidebarState.isPeekable" + v-if="sidebarState.isPeekable && !sidebarState.isHoverPeek" :is-mouse-over-sidebar="isMouseover" @change="onPeekChange" /> + <sidebar-hover-peek-behavior + v-if="sidebarState.isPeekable && !sidebarState.isPeek" + :is-mouse-over-sidebar="isMouseover" + @change="onHoverPeekChange" + /> </div> </template> diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue index 49435310793..f3f7dd587db 100644 --- a/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue +++ b/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue @@ -27,19 +27,18 @@ export default { }, i18n: { collapseSidebar: __('Hide sidebar'), - expandSidebar: __('Show sidebar'), + expandSidebar: __('Keep sidebar visible'), primaryNavigationSidebar: __('Primary navigation sidebar'), }, data() { return sidebarState; }, computed: { + canOpen() { + return this.isCollapsed || this.isPeek || this.isHoverPeek; + }, tooltipTitle() { - if (this.isPeek) return ''; - - return this.isCollapsed - ? this.$options.i18n.expandSidebar - : this.$options.i18n.collapseSidebar; + return this.canOpen ? this.$options.i18n.expandSidebar : this.$options.i18n.collapseSidebar; }, tooltip() { return { @@ -49,21 +48,21 @@ export default { }; }, ariaExpanded() { - return String(!this.isCollapsed); + return String(!this.canOpen); }, }, methods: { toggle() { - this.track(this.isCollapsed ? 'nav_show' : 'nav_hide', { + this.track(this.canOpen ? 'nav_show' : 'nav_hide', { label: 'nav_toggle', property: 'nav_sidebar', }); - toggleSuperSidebarCollapsed(!this.isCollapsed, true); + toggleSuperSidebarCollapsed(!this.canOpen, true); this.focusOtherToggle(); }, focusOtherToggle() { this.$nextTick(() => { - const classSelector = this.isCollapsed ? JS_TOGGLE_EXPAND_CLASS : JS_TOGGLE_COLLAPSE_CLASS; + const classSelector = this.canOpen ? JS_TOGGLE_EXPAND_CLASS : JS_TOGGLE_COLLAPSE_CLASS; const otherToggle = document.querySelector(`.${classSelector}`); otherToggle?.focus(); }); @@ -80,7 +79,6 @@ export default { :aria-label="$options.i18n.primaryNavigationSidebar" icon="sidebar" category="tertiary" - :disabled="isPeek" @click="toggle" /> </template> diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js index 0abc459bc52..77bd8b4a734 100644 --- a/app/assets/javascripts/super_sidebar/constants.js +++ b/app/assets/javascripts/super_sidebar/constants.js @@ -14,8 +14,11 @@ export const portalState = Vue.observable({ export const sidebarState = Vue.observable({ isCollapsed: false, + hasPeeked: false, isPeek: false, isPeekable: false, + isHoverPeek: false, + wasHoverPeek: false, }); export const helpCenterState = Vue.observable({ @@ -27,6 +30,10 @@ export const MAX_FREQUENT_GROUPS_COUNT = 3; export const SUPER_SIDEBAR_PEEK_OPEN_DELAY = 200; export const SUPER_SIDEBAR_PEEK_CLOSE_DELAY = 500; +export const SUPER_SIDEBAR_PEEK_STATE_CLOSED = 'closed'; +export const SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN = 'will-open'; +export const SUPER_SIDEBAR_PEEK_STATE_OPEN = 'open'; +export const SUPER_SIDEBAR_PEEK_STATE_WILL_CLOSE = 'will-close'; export const TRACKING_UNKNOWN_ID = 'item_without_id'; export const TRACKING_UNKNOWN_PANEL = 'nav_panel_unknown'; diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js index feb7e274b07..9ee78a657b6 100644 --- a/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js +++ b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js @@ -26,6 +26,9 @@ export const toggleSuperSidebarCollapsed = (collapsed, saveCookie) => { sidebarState.isPeek = false; sidebarState.isPeekable = collapsed; + sidebarState.hasPeeked = false; + sidebarState.isHoverPeek = false; + sidebarState.wasHoverPeek = false; sidebarState.isCollapsed = collapsed; if (saveCookie && isDesktopBreakpoint()) { |