Skip to content
Snippets Groups Projects
Unverified Commit f81d8542 authored by Paul Slaughter's avatar Paul Slaughter
Browse files

Settings search bar to remember expansion state

- This way the sections that only the sections expanded
  before we start a search, are expanded when the search
  is cleared. This helps prevent the userr from losing their
  place.

https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53581#note_503655640
parent e6d13d49
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -3,20 +3,37 @@ import { GlSearchBoxByType } from '@gitlab/ui';
import { uniq } from 'lodash';
import { EXCLUDED_NODES, HIDE_CLASS, HIGHLIGHT_CLASS, TYPING_DELAY } from '../constants';
 
const origExpansions = new Map();
const findSettingsSection = (sectionSelector, node) => {
return node.parentElement.closest(sectionSelector);
};
 
const resetSections = ({ sectionSelector, expandSection, collapseSection }) => {
document.querySelectorAll(sectionSelector).forEach((section, index) => {
section.classList.remove(HIDE_CLASS);
if (index === 0) {
const restoreExpansionState = ({ expandSection, collapseSection }) => {
origExpansions.forEach((isExpanded, section) => {
if (isExpanded) {
expandSection(section);
} else {
collapseSection(section);
}
});
origExpansions.clear();
};
const saveExpansionState = (sections, { isExpanded }) => {
// If we've saved expansions before, don't override it.
if (origExpansions.size > 0) {
return;
}
sections.forEach((section) => origExpansions.set(section, isExpanded(section)));
};
const resetSections = ({ sectionSelector }) => {
document.querySelectorAll(sectionSelector).forEach((section) => {
section.classList.remove(HIDE_CLASS);
});
};
 
const clearHighlights = () => {
Loading
Loading
@@ -85,6 +102,12 @@ export default {
type: String,
required: true,
},
isExpandedFn: {
type: Function,
required: false,
// default to a function that returns false
default: () => () => false,
},
},
data() {
return {
Loading
Loading
@@ -97,6 +120,7 @@ export default {
sectionSelector: this.sectionSelector,
expandSection: this.expandSection,
collapseSection: this.collapseSection,
isExpanded: this.isExpandedFn,
};
 
this.searchTerm = value;
Loading
Loading
@@ -104,7 +128,11 @@ export default {
clearResults(displayOptions);
 
if (value.length) {
saveExpansionState(document.querySelectorAll(this.sectionSelector), displayOptions);
displayResults(displayOptions, search(this.searchRoot, value));
} else {
restoreExpansionState(displayOptions);
}
},
expandSection(section) {
Loading
Loading
import Vue from 'vue';
import $ from 'jquery';
import { expandSection, closeSection } from '~/settings_panels';
import { expandSection, closeSection, isExpanded } from '~/settings_panels';
import SearchSettings from '~/search_settings/components/search_settings.vue';
 
const mountSearch = ({ el }) =>
Loading
Loading
@@ -12,10 +11,11 @@ const mountSearch = ({ el }) =>
props: {
searchRoot: document.querySelector('#content-body'),
sectionSelector: 'section.settings',
isExpandedFn: isExpanded,
},
on: {
collapse: (section) => closeSection($(section)),
expand: (section) => expandSection($(section)),
collapse: closeSection,
expand: expandSection,
},
}),
});
Loading
Loading
import $ from 'jquery';
import { __ } from './locale';
 
export function expandSection($section) {
/**
* Returns true if the given section is expanded or not
*
* For legacy consistency, it supports both jQuery and DOM elements
*
* @param {jQuery | Element} section
*/
export function isExpanded(sectionArg) {
const section = sectionArg instanceof $ ? sectionArg[0] : sectionArg;
return section.classList.contains('expanded');
}
export function expandSection(sectionArg) {
const $section = $(sectionArg);
$section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Collapse'));
// eslint-disable-next-line @gitlab/no-global-event-off
$section.find('.settings-content').off('scroll.expandSection').scrollTop(0);
Loading
Loading
@@ -13,7 +28,9 @@ export function expandSection($section) {
}
}
 
export function closeSection($section) {
export function closeSection(sectionArg) {
const $section = $(sectionArg);
$section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Expand'));
$section.find('.settings-content').on('scroll.expandSection', () => expandSection($section));
$section.removeClass('expanded');
Loading
Loading
Loading
Loading
@@ -2,6 +2,7 @@ import { GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SearchSettings from '~/search_settings/components/search_settings.vue';
import { HIGHLIGHT_CLASS, HIDE_CLASS } from '~/search_settings/constants';
import { isExpanded, expandSection, closeSection } from '~/settings_panels';
 
describe('search_settings/components/search_settings.vue', () => {
const ROOT_ID = 'content-body';
Loading
Loading
@@ -9,29 +10,44 @@ describe('search_settings/components/search_settings.vue', () => {
const SEARCH_TERM = 'Delete project';
const GENERAL_SETTINGS_ID = 'js-general-settings';
const ADVANCED_SETTINGS_ID = 'js-advanced-settings';
const EXTRA_SETTINGS_ID = 'js-extra-settings';
let wrapper;
let expandSpy;
let collapseSpy;
 
const buildWrapper = () => {
wrapper = shallowMount(SearchSettings, {
propsData: {
searchRoot: document.querySelector(`#${ROOT_ID}`),
sectionSelector: SECTION_SELECTOR,
isExpandedFn: isExpanded,
},
// We use "listeners" sometimes instead of "wrapper.emitted" so we can clear calls
listeners: {
expand: expandSpy,
collapse: collapseSpy,
},
});
};
const sections = () => Array.from(document.querySelectorAll(SECTION_SELECTOR));
const sectionsCount = () => sections().length;
const visibleSectionsCount = () =>
document.querySelectorAll(`${SECTION_SELECTOR}:not(.${HIDE_CLASS})`).length;
const highlightedElementsCount = () => document.querySelectorAll(`.${HIGHLIGHT_CLASS}`).length;
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
const findExpandedSections = () => sections().filter(isExpanded);
const findCollapsedSections = () => sections().filter((x) => !isExpanded(x));
const search = (term) => {
findSearchBox().vm.$emit('input', term);
};
const clearSearch = () => search('');
 
beforeEach(() => {
// We use the actual expand/close methods so that we can test what happens when expanding changes.
expandSpy = jest.fn().mockImplementation(expandSection);
collapseSpy = jest.fn().mockImplementation(closeSection);
setFixtures(`
<div>
<div class="js-search-app"></div>
Loading
Loading
@@ -39,9 +55,12 @@ describe('search_settings/components/search_settings.vue', () => {
<section id="${GENERAL_SETTINGS_ID}" class="settings">
<span>General</span>
</section>
<section id="${ADVANCED_SETTINGS_ID}" class="settings">
<section id="${ADVANCED_SETTINGS_ID}" class="settings expanded">
<span>${SEARCH_TERM}</span>
</section>
<section id="${EXTRA_SETTINGS_ID}" class="settings">
<span>Silly</span>
</section>
</div>
</div>
`);
Loading
Loading
@@ -52,17 +71,6 @@ describe('search_settings/components/search_settings.vue', () => {
wrapper.destroy();
});
 
it('expands first section and collapses the rest', () => {
clearSearch();
const [firstSection, ...otherSections] = sections();
expect(wrapper.emitted()).toEqual({
expand: [[firstSection]],
collapse: otherSections.map((x) => [x]),
});
});
it('hides sections that do not match the search term', () => {
const hiddenSection = document.querySelector(`#${GENERAL_SETTINGS_ID}`);
search(SEARCH_TERM);
Loading
Loading
@@ -77,7 +85,7 @@ describe('search_settings/components/search_settings.vue', () => {
search(SEARCH_TERM);
 
// Last called because expand is always called once to reset the page state
expect(wrapper.emitted().expand[1][0]).toBe(section);
expect(wrapper.emitted('expand')).toEqual([[section]]);
});
 
it('highlight elements that match the search term', () => {
Loading
Loading
@@ -87,7 +95,13 @@ describe('search_settings/components/search_settings.vue', () => {
});
 
describe('when search term is cleared', () => {
let expanded;
let collapsed;
beforeEach(() => {
expanded = findExpandedSections();
collapsed = findCollapsedSections();
search(SEARCH_TERM);
});
 
Loading
Loading
@@ -102,5 +116,41 @@ describe('search_settings/components/search_settings.vue', () => {
clearSearch();
expect(highlightedElementsCount()).toBe(0);
});
// This tests that we don't overwrite our state once it's saved.
const caseMultipleSearches = () => {
search('General');
search(SEARCH_TERM);
};
// This tests that we clear out the saved expansion state after every clearSearch.
const caseExpandAfterCleaer = () => {
clearSearch();
const count = sectionsCount();
expanded = sections().slice(0, count - 1);
collapsed = sections().slice(count - 1);
expanded.forEach(expandSection);
collapsed.forEach(closeSection);
search(SEARCH_TERM);
};
it.each`
case | caseFn
${'after multiple searches'} | ${caseMultipleSearches}
${'after clear and user changes expanded sections'} | ${caseExpandAfterCleaer}
`('$case, clear resets to most recent expanded sections', ({ caseFn }) => {
caseFn();
expandSpy.mockClear();
collapseSpy.mockClear();
clearSearch();
expect(expandSpy.mock.calls).toEqual(expanded.map((x) => [x]));
expect(collapseSpy.mock.calls).toEqual(collapsed.map((x) => [x]));
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment