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,61 +151,68 @@ export default { ...@@ -149,61 +151,68 @@ export default {
}; };
</script> </script>
<template> <template>
<gl-dropdown <tooltip-on-truncate
:text="timeWindowText" :title="timeWindowText"
class="date-time-picker" :truncate-target="elem => elem.querySelector('.date-time-picker-toggle')"
menu-class="date-time-picker-menu" placement="top"
v-bind="$attrs" class="d-inline-block"
toggle-class="w-100 text-truncate"
> >
<div class="d-flex justify-content-between gl-p-2"> <gl-dropdown
<gl-form-group :text="timeWindowText"
:label="__('Custom range')" v-bind="$attrs"
label-for="custom-from-time" class="date-time-picker w-100"
label-class="gl-pb-1" menu-class="date-time-picker-menu"
class="custom-time-range-form-group col-md-7 gl-pl-1 gl-pr-0 m-0" toggle-class="date-time-picker-toggle text-truncate"
> >
<div class="gl-pt-2"> <div class="d-flex justify-content-between gl-p-2">
<date-time-picker-input <gl-form-group
id="custom-time-from" :label="__('Custom range')"
v-model="startInput" label-for="custom-from-time"
:label="__('From')" label-class="gl-pb-1"
:state="startInputValid" class="custom-time-range-form-group col-md-7 gl-pl-1 gl-pr-0 m-0"
/> >
<date-time-picker-input <div class="gl-pt-2">
id="custom-time-to" <date-time-picker-input
v-model="endInput" id="custom-time-from"
:label="__('To')" v-model="startInput"
:state="endInputValid" :label="__('From')"
/> :state="startInputValid"
</div> />
<gl-form-group> <date-time-picker-input
<gl-button @click="closeDropdown">{{ __('Cancel') }}</gl-button> id="custom-time-to"
<gl-button variant="success" :disabled="!isValid" @click="setFixedRange()"> v-model="endInput"
{{ __('Apply') }} :label="__('To')"
</gl-button> :state="endInputValid"
/>
</div>
<gl-form-group>
<gl-button @click="closeDropdown">{{ __('Cancel') }}</gl-button>
<gl-button variant="success" :disabled="!isValid" @click="setFixedRange()">
{{ __('Apply') }}
</gl-button>
</gl-form-group>
</gl-form-group> </gl-form-group>
</gl-form-group> <gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-pl-1 gl-pr-1 m-0">
<gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-pl-1 gl-pr-1 m-0"> <template #label>
<template #label> <span class="gl-pl-5">{{ __('Quick range') }}</span>
<span class="gl-pl-5">{{ __('Quick range') }}</span> </template>
</template>
<gl-dropdown-item <gl-dropdown-item
v-for="(option, index) in options" v-for="(option, index) in options"
:key="index" :key="index"
:active="isOptionActive(option)" :active="isOptionActive(option)"
active-class="active" active-class="active"
@click="setQuickRange(option)" @click="setQuickRange(option)"
> >
<icon <icon
name="mobile-issue-close" name="mobile-issue-close"
class="align-bottom" class="align-bottom"
:class="{ invisible: !isOptionActive(option) }" :class="{ invisible: !isOptionActive(option) }"
/> />
{{ option.label }} {{ option.label }}
</gl-dropdown-item> </gl-dropdown-item>
</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() expect(hasTooltip()).toBe(true);
.then(() => { expect(wrapper.attributes('data-original-title')).toEqual(TEXT_LONG);
expect(hasTooltip()).toBe(true); expect(wrapper.attributes('data-placement')).toEqual('top');
expect(wrapper.attributes('data-original-title')).toEqual(TEST_TITLE); });
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() expect(hasTooltip()).toBe(false);
.then(() => { });
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() expect(hasTooltip()).toBe(true);
.then(() => { });
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() expect(hasTooltip()).toBe(false);
.then(() => { });
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() expect(hasTooltip()).toBe(true);
.then(() => { });
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(() => {
.$nextTick() expect(hasTooltip()).toBe(true);
.then(() => { expect(wrapper.attributes('data-placement')).toEqual(placement);
expect(hasTooltip()).toBe(true); });
expect(wrapper.attributes('data-placement')).toEqual(placement); });
}) });
.then(done)
.catch(done.fail); 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()
.then(() => wrapper.vm.$nextTick()) // wait 2 times to get an updated slot
.then(() => {
expect(hasTooltip()).toBe(true);
expect(wrapper.attributes('data-original-title')).toEqual(TEXT_LONG);
expect(wrapper.attributes('data-placement')).toEqual('top');
});
});
}); });
}); });
}); });
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