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';
import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
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 {
defaultTimeRanges,
......@@ -24,6 +25,7 @@ const events = {
export default {
components: {
Icon,
TooltipOnTruncate,
DateTimePickerInput,
GlFormGroup,
GlButton,
......@@ -149,61 +151,68 @@ export default {
};
</script>
<template>
<gl-dropdown
:text="timeWindowText"
class="date-time-picker"
menu-class="date-time-picker-menu"
v-bind="$attrs"
toggle-class="w-100 text-truncate"
<tooltip-on-truncate
:title="timeWindowText"
:truncate-target="elem => elem.querySelector('.date-time-picker-toggle')"
placement="top"
class="d-inline-block"
>
<div class="d-flex justify-content-between gl-p-2">
<gl-form-group
:label="__('Custom range')"
label-for="custom-from-time"
label-class="gl-pb-1"
class="custom-time-range-form-group col-md-7 gl-pl-1 gl-pr-0 m-0"
>
<div class="gl-pt-2">
<date-time-picker-input
id="custom-time-from"
v-model="startInput"
:label="__('From')"
:state="startInputValid"
/>
<date-time-picker-input
id="custom-time-to"
v-model="endInput"
:label="__('To')"
: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-dropdown
:text="timeWindowText"
v-bind="$attrs"
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">
<gl-form-group
:label="__('Custom range')"
label-for="custom-from-time"
label-class="gl-pb-1"
class="custom-time-range-form-group col-md-7 gl-pl-1 gl-pr-0 m-0"
>
<div class="gl-pt-2">
<date-time-picker-input
id="custom-time-from"
v-model="startInput"
:label="__('From')"
:state="startInputValid"
/>
<date-time-picker-input
id="custom-time-to"
v-model="endInput"
:label="__('To')"
: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 label-for="group-id-dropdown" class="col-md-5 gl-pl-1 gl-pr-1 m-0">
<template #label>
<span class="gl-pl-5">{{ __('Quick range') }}</span>
</template>
<gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-pl-1 gl-pr-1 m-0">
<template #label>
<span class="gl-pl-5">{{ __('Quick range') }}</span>
</template>
<gl-dropdown-item
v-for="(option, index) in options"
:key="index"
:active="isOptionActive(option)"
active-class="active"
@click="setQuickRange(option)"
>
<icon
name="mobile-issue-close"
class="align-bottom"
:class="{ invisible: !isOptionActive(option) }"
/>
{{ option.label }}
</gl-dropdown-item>
</gl-form-group>
</div>
</gl-dropdown>
<gl-dropdown-item
v-for="(option, index) in options"
:key="index"
:active="isOptionActive(option)"
active-class="active"
@click="setQuickRange(option)"
>
<icon
name="mobile-issue-close"
class="align-bottom"
:class="{ invisible: !isOptionActive(option) }"
/>
{{ option.label }}
</gl-dropdown-item>
</gl-form-group>
</div>
</gl-dropdown>
</tooltip-on-truncate>
</template>
<script>
import _ from 'underscore';
import { isFunction } from 'lodash';
import tooltip from '../directives/tooltip';
export default {
......@@ -28,16 +28,18 @@ export default {
showTooltip: false,
};
},
watch: {
title() {
// Wait on $nextTick in case of slot width changes
this.$nextTick(this.updateTooltip);
},
},
mounted() {
const target = this.selectTarget();
if (target && target.scrollWidth > target.offsetWidth) {
this.showTooltip = true;
}
this.updateTooltip();
},
methods: {
selectTarget() {
if (_.isFunction(this.truncateTarget)) {
if (isFunction(this.truncateTarget)) {
return this.truncateTarget(this.$el);
} else if (this.truncateTarget === 'child') {
return this.$el.childNodes[0];
......@@ -45,6 +47,10 @@ export default {
return this.$el;
},
updateTooltip() {
const target = this.selectTarget();
this.showTooltip = Boolean(target && target.scrollWidth > target.offsetWidth);
},
},
};
</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';
const TEST_TITLE = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do';
const STYLE_TRUNCATED = 'display: inline-block; max-width: 20px;';
const STYLE_NORMAL = 'display: inline-block; max-width: 1000px;';
const TEXT_SHORT = 'lorem';
const TEXT_LONG = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do';
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>`;
describe('TooltipOnTruncate component', () => {
let wrapper;
let parent;
const createComponent = ({ propsData, ...options } = {}) => {
wrapper = shallowMount(localVue.extend(TooltipOnTruncate), {
localVue,
wrapper = shallowMount(TooltipOnTruncate, {
attachToDocument: true,
propsData: {
title: TEST_TITLE,
...propsData,
},
attrs: {
style: STYLE_OVERFLOWED,
},
...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(() => {
wrapper.destroy();
});
const hasTooltip = () => wrapper.classes('js-show-tooltip');
describe('with default target', () => {
it('renders tooltip if truncated', done => {
it('renders tooltip if truncated', () => {
createComponent({
attrs: {
style: STYLE_TRUNCATED,
propsData: {
title: TEXT_LONG,
},
slots: {
default: [TEST_TITLE],
default: [TEXT_LONG],
},
});
wrapper.vm
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(true);
expect(wrapper.attributes('data-original-title')).toEqual(TEST_TITLE);
expect(wrapper.attributes('data-placement')).toEqual('top');
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
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 if normal', done => {
it('does not render tooltip if normal', () => {
createComponent({
attrs: {
style: STYLE_NORMAL,
propsData: {
title: TEXT_SHORT,
},
slots: {
default: [TEST_TITLE],
default: [TEXT_SHORT],
},
});
wrapper.vm
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(false);
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
expect(hasTooltip()).toBe(false);
});
});
});
describe('with child target', () => {
it('renders tooltip if truncated', done => {
it('renders tooltip if truncated', () => {
createComponent({
attrs: {
style: STYLE_NORMAL,
},
propsData: {
title: TEXT_LONG,
truncateTarget: 'child',
},
slots: {
default: createElementWithStyle(STYLE_TRUNCATED, TEST_TITLE),
default: createElementWithStyle(STYLE_OVERFLOWED, TEXT_LONG),
},
});
wrapper.vm
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(true);
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
expect(hasTooltip()).toBe(true);
});
});
it('does not render tooltip if normal', done => {
it('does not render tooltip if normal', () => {
createComponent({
propsData: {
truncateTarget: 'child',
},
slots: {
default: createElementWithStyle(STYLE_NORMAL, TEST_TITLE),
default: createElementWithStyle(STYLE_NORMAL, TEXT_LONG),
},
});
wrapper.vm
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(false);
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
expect(hasTooltip()).toBe(false);
});
});
});
describe('with fn target', () => {
it('renders tooltip if truncated', done => {
it('renders tooltip if truncated', () => {
createComponent({
attrs: {
style: STYLE_NORMAL,
},
propsData: {
title: TEXT_LONG,
truncateTarget: el => el.childNodes[1],
},
slots: {
default: [
createElementWithStyle('', TEST_TITLE),
createElementWithStyle(STYLE_TRUNCATED, TEST_TITLE),
createElementWithStyle('', TEXT_LONG),
createElementWithStyle(STYLE_OVERFLOWED, TEXT_LONG),
],
},
});
wrapper.vm
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(true);
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
expect(hasTooltip()).toBe(true);
});
});
});
describe('placement', () => {
it('sets data-placement when tooltip is rendered', done => {
it('sets data-placement when tooltip is rendered', () => {
const placement = 'bottom';
createComponent({
......@@ -151,21 +162,75 @@ describe('TooltipOnTruncate component', () => {
placement,
},
attrs: {
style: STYLE_TRUNCATED,
style: STYLE_OVERFLOWED,
},
slots: {
default: TEST_TITLE,
default: TEXT_LONG,
},
});
wrapper.vm
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(true);
expect(wrapper.attributes('data-placement')).toEqual(placement);
})
.then(done)
.catch(done.fail);
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()
.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