Commit d719c3cf authored by Paul Slaughter's avatar Paul Slaughter

Merge branch...

Merge branch '198460-date-time-picker-custom-dates-make-the-date-field-too-long-truncate-it-and-add-a-tooltip' into 'master'

Add tooltip when dates in date picker are too long

Closes #198460

See merge request gitlab-org/gitlab!24664
parents 30f3844f c43156cd
...@@ -5,6 +5,7 @@ import { __, sprintf } from '~/locale'; ...@@ -5,6 +5,7 @@ import { __, sprintf } from '~/locale';
import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range'; import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import DateTimePickerInput from './date_time_picker_input.vue'; import DateTimePickerInput from './date_time_picker_input.vue';
import { import {
defaultTimeRanges, defaultTimeRanges,
...@@ -24,6 +25,7 @@ const events = { ...@@ -24,6 +25,7 @@ const events = {
export default { export default {
components: { components: {
Icon, Icon,
TooltipOnTruncate,
DateTimePickerInput, DateTimePickerInput,
GlFormGroup, GlFormGroup,
GlButton, GlButton,
...@@ -149,12 +151,18 @@ export default { ...@@ -149,12 +151,18 @@ export default {
}; };
</script> </script>
<template> <template>
<tooltip-on-truncate
:title="timeWindowText"
:truncate-target="elem => elem.querySelector('.date-time-picker-toggle')"
placement="top"
class="d-inline-block"
>
<gl-dropdown <gl-dropdown
:text="timeWindowText" :text="timeWindowText"
class="date-time-picker"
menu-class="date-time-picker-menu"
v-bind="$attrs" v-bind="$attrs"
toggle-class="w-100 text-truncate" class="date-time-picker w-100"
menu-class="date-time-picker-menu"
toggle-class="date-time-picker-toggle text-truncate"
> >
<div class="d-flex justify-content-between gl-p-2"> <div class="d-flex justify-content-between gl-p-2">
<gl-form-group <gl-form-group
...@@ -206,4 +214,5 @@ export default { ...@@ -206,4 +214,5 @@ export default {
</gl-form-group> </gl-form-group>
</div> </div>
</gl-dropdown> </gl-dropdown>
</tooltip-on-truncate>
</template> </template>
<script> <script>
import _ from 'underscore'; import { isFunction } from 'lodash';
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
export default { export default {
...@@ -28,16 +28,18 @@ export default { ...@@ -28,16 +28,18 @@ export default {
showTooltip: false, showTooltip: false,
}; };
}, },
watch: {
title() {
// Wait on $nextTick in case of slot width changes
this.$nextTick(this.updateTooltip);
},
},
mounted() { mounted() {
const target = this.selectTarget(); this.updateTooltip();
if (target && target.scrollWidth > target.offsetWidth) {
this.showTooltip = true;
}
}, },
methods: { methods: {
selectTarget() { selectTarget() {
if (_.isFunction(this.truncateTarget)) { if (isFunction(this.truncateTarget)) {
return this.truncateTarget(this.$el); return this.truncateTarget(this.$el);
} else if (this.truncateTarget === 'child') { } else if (this.truncateTarget === 'child') {
return this.$el.childNodes[0]; return this.$el.childNodes[0];
...@@ -45,6 +47,10 @@ export default { ...@@ -45,6 +47,10 @@ export default {
return this.$el; return this.$el;
}, },
updateTooltip() {
const target = this.selectTarget();
this.showTooltip = Boolean(target && target.scrollWidth > target.offsetWidth);
},
}, },
}; };
</script> </script>
......
---
title: Add tooltip when dates in date picker are too long
merge_request: 24664
author:
type: added
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
const TEST_TITLE = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do'; const TEXT_SHORT = 'lorem';
const STYLE_TRUNCATED = 'display: inline-block; max-width: 20px;'; const TEXT_LONG = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do';
const STYLE_NORMAL = 'display: inline-block; max-width: 1000px;';
const localVue = createLocalVue(); const TEXT_TRUNCATE = 'white-space: nowrap; overflow:hidden;';
const STYLE_NORMAL = `${TEXT_TRUNCATE} display: inline-block; max-width: 1000px;`; // does not overflows
const STYLE_OVERFLOWED = `${TEXT_TRUNCATE} display: inline-block; max-width: 50px;`; // overflowed when text is long
const createElementWithStyle = (style, content) => `<a href="#" style="${style}">${content}</a>`; const createElementWithStyle = (style, content) => `<a href="#" style="${style}">${content}</a>`;
describe('TooltipOnTruncate component', () => { describe('TooltipOnTruncate component', () => {
let wrapper; let wrapper;
let parent;
const createComponent = ({ propsData, ...options } = {}) => { const createComponent = ({ propsData, ...options } = {}) => {
wrapper = shallowMount(localVue.extend(TooltipOnTruncate), { wrapper = shallowMount(TooltipOnTruncate, {
localVue,
attachToDocument: true, attachToDocument: true,
propsData: { propsData: {
title: TEST_TITLE,
...propsData, ...propsData,
}, },
attrs: {
style: STYLE_OVERFLOWED,
},
...options, ...options,
}); });
}; };
const createWrappedComponent = ({ propsData, ...options }) => {
// set a parent around the tested component
parent = mount(
{
props: {
title: { default: '' },
},
template: `
<TooltipOnTruncate :title="title" truncate-target="child" style="${STYLE_OVERFLOWED}">
<div>{{title}}</div>
</TooltipOnTruncate>
`,
components: {
TooltipOnTruncate,
},
},
{
propsData: { ...propsData },
attachToDocument: true,
...options,
},
);
wrapper = parent.find(TooltipOnTruncate);
};
const hasTooltip = () => wrapper.classes('js-show-tooltip');
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
const hasTooltip = () => wrapper.classes('js-show-tooltip');
describe('with default target', () => { describe('with default target', () => {
it('renders tooltip if truncated', done => { it('renders tooltip if truncated', () => {
createComponent({ createComponent({
attrs: { propsData: {
style: STYLE_TRUNCATED, title: TEXT_LONG,
}, },
slots: { slots: {
default: [TEST_TITLE], default: [TEXT_LONG],
}, },
}); });
wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(true); expect(hasTooltip()).toBe(true);
expect(wrapper.attributes('data-original-title')).toEqual(TEST_TITLE); expect(wrapper.attributes('data-original-title')).toEqual(TEXT_LONG);
expect(wrapper.attributes('data-placement')).toEqual('top'); expect(wrapper.attributes('data-placement')).toEqual('top');
}) });
.then(done)
.catch(done.fail);
}); });
it('does not render tooltip if normal', done => { it('does not render tooltip if normal', () => {
createComponent({ createComponent({
attrs: { propsData: {
style: STYLE_NORMAL, title: TEXT_SHORT,
}, },
slots: { slots: {
default: [TEST_TITLE], default: [TEXT_SHORT],
}, },
}); });
wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(false); expect(hasTooltip()).toBe(false);
}) });
.then(done)
.catch(done.fail);
}); });
}); });
describe('with child target', () => { describe('with child target', () => {
it('renders tooltip if truncated', done => { it('renders tooltip if truncated', () => {
createComponent({ createComponent({
attrs: { attrs: {
style: STYLE_NORMAL, style: STYLE_NORMAL,
}, },
propsData: { propsData: {
title: TEXT_LONG,
truncateTarget: 'child', truncateTarget: 'child',
}, },
slots: { slots: {
default: createElementWithStyle(STYLE_TRUNCATED, TEST_TITLE), default: createElementWithStyle(STYLE_OVERFLOWED, TEXT_LONG),
}, },
}); });
wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(true); expect(hasTooltip()).toBe(true);
}) });
.then(done)
.catch(done.fail);
}); });
it('does not render tooltip if normal', done => { it('does not render tooltip if normal', () => {
createComponent({ createComponent({
propsData: { propsData: {
truncateTarget: 'child', truncateTarget: 'child',
}, },
slots: { slots: {
default: createElementWithStyle(STYLE_NORMAL, TEST_TITLE), default: createElementWithStyle(STYLE_NORMAL, TEXT_LONG),
}, },
}); });
wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(false); expect(hasTooltip()).toBe(false);
}) });
.then(done)
.catch(done.fail);
}); });
}); });
describe('with fn target', () => { describe('with fn target', () => {
it('renders tooltip if truncated', done => { it('renders tooltip if truncated', () => {
createComponent({ createComponent({
attrs: { attrs: {
style: STYLE_NORMAL, style: STYLE_NORMAL,
}, },
propsData: { propsData: {
title: TEXT_LONG,
truncateTarget: el => el.childNodes[1], truncateTarget: el => el.childNodes[1],
}, },
slots: { slots: {
default: [ default: [
createElementWithStyle('', TEST_TITLE), createElementWithStyle('', TEXT_LONG),
createElementWithStyle(STYLE_TRUNCATED, TEST_TITLE), createElementWithStyle(STYLE_OVERFLOWED, TEXT_LONG),
], ],
}, },
}); });
wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(true); expect(hasTooltip()).toBe(true);
}) });
.then(done)
.catch(done.fail);
}); });
}); });
describe('placement', () => { describe('placement', () => {
it('sets data-placement when tooltip is rendered', done => { it('sets data-placement when tooltip is rendered', () => {
const placement = 'bottom'; const placement = 'bottom';
createComponent({ createComponent({
...@@ -151,21 +162,75 @@ describe('TooltipOnTruncate component', () => { ...@@ -151,21 +162,75 @@ describe('TooltipOnTruncate component', () => {
placement, placement,
}, },
attrs: { attrs: {
style: STYLE_TRUNCATED, style: STYLE_OVERFLOWED,
}, },
slots: { slots: {
default: TEST_TITLE, default: TEXT_LONG,
}, },
}); });
wrapper.vm return wrapper.vm.$nextTick().then(() => {
expect(hasTooltip()).toBe(true);
expect(wrapper.attributes('data-placement')).toEqual(placement);
});
});
});
describe('updates when title and slot content changes', () => {
describe('is initialized with a long text', () => {
beforeEach(() => {
createWrappedComponent({
propsData: { title: TEXT_LONG },
});
return parent.vm.$nextTick();
});
it('renders tooltip', () => {
expect(hasTooltip()).toBe(true);
expect(wrapper.attributes('data-original-title')).toEqual(TEXT_LONG);
expect(wrapper.attributes('data-placement')).toEqual('top');
});
it('does not render tooltip after updated to a short text', () => {
parent.setProps({
title: TEXT_SHORT,
});
return wrapper.vm
.$nextTick()
.then(() => wrapper.vm.$nextTick()) // wait 2 times to get an updated slot
.then(() => {
expect(hasTooltip()).toBe(false);
});
});
});
describe('is initialized with a short text', () => {
beforeEach(() => {
createWrappedComponent({
propsData: { title: TEXT_SHORT },
});
return wrapper.vm.$nextTick();
});
it('does not render tooltip', () => {
expect(hasTooltip()).toBe(false);
});
it('renders tooltip after updated to a long text', () => {
parent.setProps({
title: TEXT_LONG,
});
return wrapper.vm
.$nextTick() .$nextTick()
.then(() => wrapper.vm.$nextTick()) // wait 2 times to get an updated slot
.then(() => { .then(() => {
expect(hasTooltip()).toBe(true); expect(hasTooltip()).toBe(true);
expect(wrapper.attributes('data-placement')).toEqual(placement); expect(wrapper.attributes('data-original-title')).toEqual(TEXT_LONG);
}) expect(wrapper.attributes('data-placement')).toEqual('top');
.then(done) });
.catch(done.fail); });
}); });
}); });
}); });
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