Skip to content

Commit

Permalink
feat: improve UX of default creation popup (#905) (#913)
Browse files Browse the repository at this point in the history
  • Loading branch information
adhrinae authored Oct 20, 2021
1 parent 9dee9ac commit d53bc05
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 28 deletions.
57 changes: 47 additions & 10 deletions src/js/view/popup/scheduleCreationPopup.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ function ScheduleCreationPopup(container, calendars, usageStatistics) {
this._toggleIsPrivate.bind(this),
this._onClickSaveSchedule.bind(this)
];
this._datepickerState = {
start: null,
end: null,
isAllDay: false
};

domevent.on(container, 'click', this._onClick, this);
}
Expand Down Expand Up @@ -79,6 +84,8 @@ ScheduleCreationPopup.prototype._onMouseDown = function(mouseDownEvent) {
ScheduleCreationPopup.prototype.destroy = function() {
this.layer.destroy();
this.layer = null;
this.rangePicker.destroy();
this.rangePicker = null;
domevent.off(this.container, 'click', this._onClick, this);
domevent.off(document.body, 'mousedown', this._onMouseDown, this);
View.prototype.destroy.call(this);
Expand Down Expand Up @@ -206,6 +213,11 @@ ScheduleCreationPopup.prototype._toggleIsAllday = function(target) {
checkbox = domutil.find(config.classname('.checkbox-square'), alldaySection);
checkbox.checked = !checkbox.checked;

this.rangePicker.destroy();
this.rangePicker = null;
this._setDatepickerState({isAllDay: checkbox.checked});
this._createDatepicker();

return true;
}

Expand Down Expand Up @@ -300,8 +312,7 @@ ScheduleCreationPopup.prototype._onClickSaveSchedule = function(target) {
ScheduleCreationPopup.prototype.render = function(viewModel) {
var calendars = this.calendars;
var layer = this.layer;
var self = this;
var boxElement, guideElements;
var boxElement, guideElements, defaultStartDate, defaultEndDate;

viewModel.zIndex = this.layer.zIndex + 5;
viewModel.calendars = calendars;
Expand All @@ -319,16 +330,31 @@ ScheduleCreationPopup.prototype.render = function(viewModel) {
boxElement = guideElements.length ? guideElements[0] : null;
}
layer.setContent(tmpl(viewModel));
this._createDatepicker(viewModel.start, viewModel.end, viewModel.isAllDay);

defaultStartDate = new TZDate(viewModel.start);
defaultEndDate = new TZDate(viewModel.end);
// NOTE: Setting default start/end time when editing all-day schedule first time.
// This logic refers to Apple calendar's behavior.
if (viewModel.isAllDay) {
defaultStartDate.setHours(12, 0, 0);
defaultEndDate.setHours(13, 0, 0);
}
this._setDatepickerState({
start: defaultStartDate,
end: defaultEndDate,
isAllDay: viewModel.isAllDay
});
this._createDatepicker();

layer.show();

if (boxElement) {
this._setPopupPositionAndArrowDirection(boxElement.getBoundingClientRect());
}

util.debounce(function() {
domevent.on(document.body, 'mousedown', self._onMouseDown, self);
})();
domevent.on(document.body, 'mousedown', this._onMouseDown, this);
}.bind(this))();
};

/**
Expand Down Expand Up @@ -376,6 +402,10 @@ ScheduleCreationPopup.prototype._makeEditModeData = function(viewModel) {
};
};

ScheduleCreationPopup.prototype._setDatepickerState = function(newState) {
util.extend(this._datepickerState, newState);
};

/**
* Set popup position and arrow direction to apear near guide element
* @param {MonthCreationGuide|TimeCreationGuide|DayGridCreationGuide} guideBound - creation guide element
Expand Down Expand Up @@ -588,12 +618,12 @@ ScheduleCreationPopup.prototype._setArrowDirection = function(arrow) {

/**
* Create date range picker using start date and end date
* @param {TZDate} start - start date
* @param {TZDate} end - end date
* @param {boolean} isAllDay - isAllDay
*/
ScheduleCreationPopup.prototype._createDatepicker = function(start, end, isAllDay) {
ScheduleCreationPopup.prototype._createDatepicker = function() {
var cssPrefix = config.cssPrefix;
var start = this._datepickerState.start;
var end = this._datepickerState.end;
var isAllDay = this._datepickerState.isAllDay;

this.rangePicker = DatePicker.createRangePicker({
startpicker: {
Expand All @@ -613,13 +643,20 @@ ScheduleCreationPopup.prototype._createDatepicker = function(start, end, isAllDa
},
usageStatistics: this._usageStatistics
});
this.rangePicker.on('change:start', function() {
this._setDatepickerState({start: this.rangePicker.getStartDate()});
}.bind(this));
this.rangePicker.on('change:end', function() {
this._setDatepickerState({end: this.rangePicker.getEndDate()});
}.bind(this));
};

/**
* Hide layer
*/
ScheduleCreationPopup.prototype.hide = function() {
this.layer.hide();
this.rangePicker.destroy();

if (this.guide) {
this.guide.clearGuideElement();
Expand Down Expand Up @@ -678,7 +715,7 @@ ScheduleCreationPopup.prototype._validateForm = function(title, startDate, endDa
*/
ScheduleCreationPopup.prototype._getRangeDate = function(startDate, endDate, isAllDay) {
var start = isAllDay ? datetime.start(startDate) : startDate;
var end = isAllDay ? datetime.renderEnd(startDate, endDate) : endDate;
var end = isAllDay ? datetime.renderEnd(startDate, datetime.end(endDate)) : endDate;

/**
* @typedef {object} RangeDate
Expand Down
115 changes: 97 additions & 18 deletions test/app/view/scheduleCreationPopup.spec.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,98 @@
'use strict';

var ScheduleCreationpPopup = require('../../../src/js/view/popup/scheduleCreationPopup');
var ScheduleCreationPopup = require('../../../src/js/view/popup/scheduleCreationPopup');
var TZDate = require('common/timezone').Date;

/**
* NOTE: Due to external dependency(tui-date-picker) and testing environment,
* couldn't add detailed test cases. Writing more intergrated test would draw a better coverage.
* e.g.) Changeing date range picker values
*/
describe('ScheduleCreationPopup date range picker', function() {
var container, popup;
var startPickerId = 'tui-full-calendar-schedule-start-date';
var endPickerId = 'tui-full-calendar-schedule-end-date';
var mockTimedViewModel = {
start: '2015-05-01T09:00:00',
end: '2015-05-01T10:00:00',
guide: []
};
var mockAllDayViewModel = {
start: '2015-05-01T09:00:00',
end: '2015-05-01T10:00:00',
isAllDay: true,
guide: []
};

function getInputValue(inputId) {
var input = document.getElementById(inputId);

return input ? input.value : null;
}

function clickAllDaySection() {
var allDaySectionClassName = 'tui-full-calendar-section-allday';
var clickEvent = new MouseEvent('click', {bubbles: true});

document.querySelector('.' + allDaySectionClassName).dispatchEvent(clickEvent);
}

beforeEach(function() {
fixture.load('view.html');
container = document.getElementById('container');
popup = new ScheduleCreationPopup(container, [], false);
});

afterEach(function() {
fixture.cleanup();
popup.destroy();
});

it('should render start & end dates on the date range picker', function() {
var popupClassName = 'tui-full-calendar-popup-container';
var popupNode = document.getElementsByClassName(popupClassName);
popup.render(mockTimedViewModel);

expect(popupNode).toBeDefined();
expect(getInputValue(startPickerId)).toBe('2015-05-01 09:00');
expect(getInputValue(endPickerId)).toBe('2015-05-01 10:00');
});

it('should not show time values for allday events at initial render', function() {
popup.render(mockAllDayViewModel);

expect(getInputValue(startPickerId)).toBe('2015-05-01');
expect(getInputValue(endPickerId)).toBe('2015-05-01');
});

it('should be able to switch between timed and allday range pickers', function() {
popup.render(mockTimedViewModel);
clickAllDaySection();

expect(getInputValue(startPickerId)).toBe('2015-05-01');
expect(getInputValue(endPickerId)).toBe('2015-05-01');

clickAllDaySection();

expect(getInputValue(startPickerId)).toBe('2015-05-01 09:00');
expect(getInputValue(endPickerId)).toBe('2015-05-01 10:00');
});

it('should set default start & end time when start editing allday events', function() {
var mockViewModel = Object.create(mockAllDayViewModel);
mockViewModel.schedule = {id: ''};

popup.render(mockAllDayViewModel);

clickAllDaySection();

expect(getInputValue(startPickerId)).toBe('2015-05-01 12:00');
expect(getInputValue(endPickerId)).toBe('2015-05-01 13:00');
});
});

/* eslint-disable object-property-newline */
describe('ScheduleCreationpPopup#_calcRenderingData', function() {
describe('ScheduleCreationPopup#_calcRenderingData', function() {
var popupSize, containerSize;

beforeEach(function() {
Expand All @@ -14,7 +102,7 @@ describe('ScheduleCreationpPopup#_calcRenderingData', function() {

it('it is usually placed at the top center of the guide element', function() {
var guideBound = {top: 200, left: 100, bottom: 200, right: 400};
var posData = ScheduleCreationpPopup.prototype._calcRenderingData(popupSize, containerSize, guideBound);
var posData = ScheduleCreationPopup.prototype._calcRenderingData(popupSize, containerSize, guideBound);

expect(posData.x).toBe(50);
expect(posData.y).toBe(67);
Expand All @@ -24,7 +112,7 @@ describe('ScheduleCreationpPopup#_calcRenderingData', function() {

it('if it overflows the top of the container, it should be placed under the guide element', function() {
var guideBound = {top: 100, left: 100, bottom: 200, right: 400};
var posData = ScheduleCreationpPopup.prototype._calcRenderingData(popupSize, containerSize, guideBound);
var posData = ScheduleCreationPopup.prototype._calcRenderingData(popupSize, containerSize, guideBound);

expect(posData.x).toBe(50);
expect(posData.y).toBe(153);
Expand All @@ -34,35 +122,26 @@ describe('ScheduleCreationpPopup#_calcRenderingData', function() {

it('when overflowing to the left of the container, the left value is set to 0 and the left value of the arrow is also set.', function() {
var guideBound = {top: 200, left: 50, bottom: 200, right: 200};
var posData = ScheduleCreationpPopup.prototype._calcRenderingData(popupSize, containerSize, guideBound);
var posData = ScheduleCreationPopup.prototype._calcRenderingData(popupSize, containerSize, guideBound);

expect(posData.x).toBe(0);
expect(posData.arrow.position).toBeDefined();
});

it('when it overflows to the right of the container, the popup is aligned to the right and the left value of the arrow should be set', function() {
var guideBound = {top: 200, left: 400, bottom: 200, right: 500};
var posData = ScheduleCreationpPopup.prototype._calcRenderingData(popupSize, containerSize, guideBound);
var posData = ScheduleCreationPopup.prototype._calcRenderingData(popupSize, containerSize, guideBound);

expect(posData.x).toBe(200);
expect(posData.arrow.position).toBeDefined();
});
});

describe('ScheduleCreationpPopup#_getRangeDate', function() {
describe('ScheduleCreationPopup#_getRangeDate', function() {
it('when it is an all-day schedule, set the hour and minute', function() {
var start = new TZDate('2020/04/24 10:00:00');
var end = new TZDate('2020/04/24 15:00:00');
var rangeDate = ScheduleCreationpPopup.prototype._getRangeDate(start, end, true);

expect(rangeDate.start).toEqual(new TZDate('2020/04/24 00:00:00'));
expect(rangeDate.end).toEqual(new TZDate('2020/04/24 23:59:59'));
});

it('when it is an all-day schedule, if the end date is the start time of the day, it is set as the last time of the previous day', function() {
var start = new TZDate('2020/04/24 00:00:00');
var end = new TZDate('2020/04/25 00:00:00');
var rangeDate = ScheduleCreationpPopup.prototype._getRangeDate(start, end, true);
var rangeDate = ScheduleCreationPopup.prototype._getRangeDate(start, end, true);

expect(rangeDate.start).toEqual(new TZDate('2020/04/24 00:00:00'));
expect(rangeDate.end).toEqual(new TZDate('2020/04/24 23:59:59'));
Expand All @@ -71,7 +150,7 @@ describe('ScheduleCreationpPopup#_getRangeDate', function() {
it('when it is not an all-day schedule, if the end date is entered user date', function() {
var start = new TZDate('2020/04/24 00:00:00');
var end = new TZDate('2020/04/25 00:00:00');
var rangeDate = ScheduleCreationpPopup.prototype._getRangeDate(start, end, false);
var rangeDate = ScheduleCreationPopup.prototype._getRangeDate(start, end, false);

expect(rangeDate.start).toEqual(new TZDate('2020/04/24 00:00:00'));
expect(rangeDate.end).toEqual(new TZDate('2020/04/25 00:00:00'));
Expand Down

0 comments on commit d53bc05

Please sign in to comment.