Commit ec86096e authored by Axel García's avatar Axel García

Add embedded variant to labels dropdown

This is to be used inside of a form.

It's different from "standalone" because it has
header, footer and custom styling.
parent d06a5ba7
export const DropdownVariant = {
Sidebar: 'sidebar',
Standalone: 'standalone',
Embedded: 'embedded',
};
export const LIST_BUFFER_SIZE = 5;
......@@ -8,12 +8,16 @@ export default {
GlIcon,
},
computed: {
...mapGetters(['dropdownButtonText', 'isDropdownVariantStandalone']),
...mapGetters([
'dropdownButtonText',
'isDropdownVariantStandalone',
'isDropdownVariantEmbedded',
]),
},
methods: {
...mapActions(['toggleDropdownContents']),
handleButtonClick(e) {
if (this.isDropdownVariantStandalone) {
if (this.isDropdownVariantStandalone || this.isDropdownVariantEmbedded) {
this.toggleDropdownContents();
e.stopPropagation();
}
......
......@@ -36,7 +36,7 @@ export default {
'footerCreateLabelTitle',
'footerManageLabelTitle',
]),
...mapGetters(['selectedLabelsList', 'isDropdownVariantSidebar']),
...mapGetters(['selectedLabelsList', 'isDropdownVariantSidebar', 'isDropdownVariantEmbedded']),
visibleLabels() {
if (this.searchKey) {
return this.labels.filter(label =>
......@@ -129,7 +129,10 @@ export default {
class="labels-fetch-loading position-absolute d-flex align-items-center w-100 h-100"
size="md"
/>
<div v-if="isDropdownVariantSidebar" class="dropdown-title d-flex align-items-center pt-0 pb-2">
<div
v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
class="dropdown-title d-flex align-items-center pt-0 pb-2"
>
<span class="flex-grow-1">{{ labelsListTitle }}</span>
<gl-button
:aria-label="__('Close')"
......@@ -165,7 +168,7 @@ export default {
</li>
</smart-virtual-list>
</div>
<div v-if="isDropdownVariantSidebar" class="dropdown-footer">
<div v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded" class="dropdown-footer">
<ul class="list-unstyled">
<li v-if="allowLabelCreate">
<gl-link
......
......@@ -74,6 +74,11 @@ export default {
required: false,
default: '',
},
dropdownButtonText: {
type: String,
required: false,
default: __('Label'),
},
labelsListTitle: {
type: String,
required: false,
......@@ -97,7 +102,11 @@ export default {
},
computed: {
...mapState(['showDropdownButton', 'showDropdownContents']),
...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantStandalone']),
...mapGetters([
'isDropdownVariantSidebar',
'isDropdownVariantStandalone',
'isDropdownVariantEmbedded',
]),
dropdownButtonVisible() {
return this.isDropdownVariantSidebar ? this.showDropdownButton : true;
},
......@@ -116,6 +125,7 @@ export default {
allowLabelCreate: this.allowLabelCreate,
allowMultiselect: this.allowMultiselect,
allowScopedLabels: this.allowScopedLabels,
dropdownButtonText: this.dropdownButtonText,
selectedLabels: this.selectedLabels,
labelsFetchPath: this.labelsFetchPath,
labelsManagePath: this.labelsManagePath,
......@@ -200,7 +210,10 @@ export default {
<template>
<div
class="labels-select-wrapper position-relative"
:class="{ 'is-standalone': isDropdownVariantStandalone }"
:class="{
'is-standalone': isDropdownVariantStandalone,
'is-embedded': isDropdownVariantEmbedded,
}"
>
<template v-if="isDropdownVariantSidebar">
<dropdown-value-collapsed
......@@ -221,7 +234,7 @@ export default {
ref="dropdownContents"
/>
</template>
<template v-if="isDropdownVariantStandalone">
<template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded">
<dropdown-button v-show="dropdownButtonVisible" />
<dropdown-contents
v-if="dropdownButtonVisible && showDropdownContents"
......
......@@ -13,7 +13,7 @@ export const dropdownButtonText = (state, getters) => {
: state.selectedLabels;
if (!selectedLabels.length) {
return __('Label');
return state.dropdownButtonText || __('Label');
} else if (selectedLabels.length > 1) {
return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
firstLabelName: selectedLabels[0].title,
......@@ -44,5 +44,12 @@ export const isDropdownVariantSidebar = state => state.variant === DropdownVaria
*/
export const isDropdownVariantStandalone = state => state.variant === DropdownVariant.Standalone;
/**
* Returns boolean representing whether dropdown variant
* is `embedded`
* @param {object} state
*/
export const isDropdownVariantEmbedded = state => state.variant === DropdownVariant.Embedded;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -6,6 +6,7 @@ export default () => ({
labelsCreateTitle: '',
footerCreateLabelTitle: '',
footerManageLabelTitle: '',
dropdownButtonText: '',
// Paths
namespace: '',
......
......@@ -193,7 +193,9 @@ export default {
"
:labels-manage-path="groupUrl('labels')"
:labels-filter-base-path="groupUrl('epics')"
variant="standalone"
:labels-list-title="__('Select label')"
:dropdown-button-text="__('Labels')"
variant="embedded"
class="block labels js-labels-block"
@updateSelectedLabels="handleUpdateSelectedLabels"
>{{ __('None') }}</labels-select-vue
......
......@@ -97,25 +97,20 @@
}
}
.labels-select-wrapper.is-standalone {
@include gl-bg-white;
@include gl-font-regular;
@include gl-font-base;
@include gl-line-height-normal;
@include gl-py-3;
@include gl-px-4;
@include gl-h-auto;
@include gl-text-left;
@include gl-border-none;
@include gl-inset-border-1-gray-400;
@include gl-rounded-base;
@include gl-white-space-nowrap;
.labels-select-wrapper.is-embedded {
.labels-select-dropdown-button {
@include gl-bg-none;
@include gl-bg-white;
@include gl-font-regular;
@include gl-font-base;
@include gl-line-height-normal;
@include gl-py-3;
@include gl-px-4;
@include gl-h-auto;
@include gl-text-left;
@include gl-border-none;
@include gl-shadow-none;
@include gl-p-0;
@include gl-inset-border-1-gray-400;
@include gl-rounded-base;
@include gl-white-space-nowrap;
.gl-button-text {
@include gl-text-gray-700;
......@@ -123,12 +118,37 @@
@include gl-justify-content-space-between;
@include gl-w-full;
}
.gl-icon {
@include gl-m-0;
}
}
.labels-select-dropdown-contents {
@include gl-left-0;
@include gl-shadow-x0-y2-b4-s0;
top: 100%;
bottom: 100%;
width: 300px !important;
margin-bottom: $gl-spacing-scale-6 !important;
a:not(.btn) {
@include gl-reset-color;
}
}
.dropdown-footer .list-unstyled {
@include gl-m-0;
}
.color-input-container {
.dropdown-label-color-preview[style] {
@include gl-border-none;
}
.gl-form-input {
@include gl-rounded-top-left-none;
@include gl-rounded-bottom-left-none;
}
}
}
......@@ -8,11 +8,12 @@ import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue
import { mockConfig } from './mock_data';
let store;
const localVue = createLocalVue();
localVue.use(Vuex);
const createComponent = (initialState = mockConfig) => {
const store = new Vuex.Store(labelSelectModule());
store = new Vuex.Store(labelSelectModule());
store.dispatch('setInitialState', initialState);
......@@ -35,24 +36,31 @@ describe('DropdownButton', () => {
describe('methods', () => {
describe('handleButtonClick', () => {
it('calls action `toggleDropdownContents` and stops event propagation when `state.variant` is "standalone"', () => {
const event = {
stopPropagation: jest.fn(),
};
wrapper = createComponent({
...mockConfig,
variant: 'standalone',
});
jest.spyOn(wrapper.vm, 'toggleDropdownContents');
wrapper.vm.handleButtonClick(event);
expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled();
expect(event.stopPropagation).toHaveBeenCalled();
wrapper.destroy();
});
it.each`
variant
${'standalone'}
${'embedded'}
`(
'calls action `toggleDropdownContents` and stops event propagation when `state.variant` is "$variant"',
({ variant }) => {
const event = {
stopPropagation: jest.fn(),
};
wrapper = createComponent({
...mockConfig,
variant,
});
jest.spyOn(wrapper.vm, 'toggleDropdownContents');
wrapper.vm.handleButtonClick(event);
expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled();
expect(event.stopPropagation).toHaveBeenCalled();
wrapper.destroy();
},
);
});
});
......@@ -61,13 +69,22 @@ describe('DropdownButton', () => {
expect(wrapper.is('gl-button-stub')).toBe(true);
});
it('renders button text element', () => {
it('renders default button text element', () => {
const dropdownTextEl = wrapper.find('.dropdown-toggle-text');
expect(dropdownTextEl.exists()).toBe(true);
expect(dropdownTextEl.text()).toBe('Label');
});
it('renders provided button text element', () => {
store.state.dropdownButtonText = 'Custom label';
const dropdownTextEl = wrapper.find('.dropdown-toggle-text');
return wrapper.vm.$nextTick().then(() => {
expect(dropdownTextEl.text()).toBe('Custom label');
});
});
it('renders chevron icon element', () => {
const iconEl = wrapper.find(GlIcon);
......
......@@ -44,6 +44,7 @@ const createComponent = (initialState = mockConfig) => {
describe('DropdownContentsLabelsView', () => {
let wrapper;
let wrapperStandalone;
let wrapperEmbedded;
beforeEach(() => {
wrapper = createComponent();
......@@ -51,11 +52,16 @@ describe('DropdownContentsLabelsView', () => {
...mockConfig,
variant: 'standalone',
});
wrapperEmbedded = createComponent({
...mockConfig,
variant: 'embedded',
});
});
afterEach(() => {
wrapper.destroy();
wrapperStandalone.destroy();
wrapperEmbedded.destroy();
});
describe('computed', () => {
......@@ -211,6 +217,10 @@ describe('DropdownContentsLabelsView', () => {
expect(wrapperStandalone.find('.dropdown-title').exists()).toBe(false);
});
it('renders dropdown title element when `state.variant` is "embedded"', () => {
expect(wrapperEmbedded.find('.dropdown-title').exists()).toBe(true);
});
it('renders dropdown close button element', () => {
const closeButtonEl = wrapper.find('.dropdown-title').find(GlButton);
......@@ -291,5 +301,9 @@ describe('DropdownContentsLabelsView', () => {
it('does not render footer list items when `state.variant` is "standalone"', () => {
expect(wrapperStandalone.find('.dropdown-footer').exists()).toBe(false);
});
it('renders footer list items when `state.variant` is "embedded"', () => {
expect(wrapperEmbedded.find('.dropdown-footer').exists()).toBe(true);
});
});
});
......@@ -89,18 +89,25 @@ describe('LabelsSelectRoot', () => {
expect(wrapper.attributes('class')).toContain('labels-select-wrapper position-relative');
});
it('renders component root element with CSS class `is-standalone` when `state.variant` is "standalone"', () => {
const wrapperStandalone = createComponent({
...mockConfig,
variant: 'standalone',
});
return wrapperStandalone.vm.$nextTick(() => {
expect(wrapperStandalone.classes()).toContain('is-standalone');
wrapperStandalone.destroy();
});
});
it.each`
variant | cssClass
${'standalone'} | ${'is-standalone'}
${'embedded'} | ${'is-embedded'}
`(
'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
({ variant, cssClass }) => {
const wrapperStandalone = createComponent({
...mockConfig,
variant,
});
return wrapperStandalone.vm.$nextTick(() => {
expect(wrapperStandalone.classes()).toContain(cssClass);
wrapperStandalone.destroy();
});
},
);
it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', () => {
expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
......
......@@ -2,13 +2,20 @@ import * as getters from '~/vue_shared/components/sidebar/labels_select_vue/stor
describe('LabelsSelect Getters', () => {
describe('dropdownButtonText', () => {
it('returns string "Label" when state.labels has no selected labels', () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
expect(getters.dropdownButtonText({ labels }, { isDropdownVariantSidebar: true })).toBe(
'Label',
);
});
it.each`
labelType | dropdownButtonText | expected
${'default'} | ${''} | ${'Label'}
${'custom'} | ${'Custom label'} | ${'Custom label'}
`(
'returns $labelType text when state.labels has no selected labels',
({ dropdownButtonText, expected }) => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
const selectedLabels = [];
const state = { labels, selectedLabels, dropdownButtonText };
expect(getters.dropdownButtonText(state, {})).toBe(expected);
},
);
it('returns label title when state.labels has only 1 label', () => {
const labels = [{ id: 1, title: 'Foobar', set: true }];
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment