diff --git a/app/assets/javascripts/discourse/components/date-input.js.es6 b/app/assets/javascripts/discourse/components/date-input.js.es6
new file mode 100644
index 00000000000..d29962c02c4
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/date-input.js.es6
@@ -0,0 +1,101 @@
+/* global Pikaday:true */
+import loadScript from "discourse/lib/load-script";
+import {
+ default as computed,
+ on
+} from "ember-addons/ember-computed-decorators";
+
+export default Ember.Component.extend({
+ classNames: ["d-date-input"],
+ date: null,
+ _picker: null,
+
+ @computed("site.mobileView")
+ inputType(mobileView) {
+ return mobileView ? "date" : "text";
+ },
+
+ @on("didInsertElement")
+ _loadDatePicker() {
+ const container = this.element.querySelector(`#${this.containerId}`);
+
+ if (this.site.mobileView) {
+ this._loadNativePicker(container);
+ } else {
+ this._loadPikadayPicker(container);
+ }
+ },
+
+ didUpdateAttrs() {
+ this._super(...arguments);
+
+ if (this._picker) {
+ this._picker.setDate(this.date, true);
+ }
+ },
+
+ _loadPikadayPicker(container) {
+ loadScript("/javascripts/pikaday.js").then(() => {
+ Ember.run.next(() => {
+ const default_opts = {
+ field: this.element.querySelector(".date-picker"),
+ container: container || this.element,
+ bound: container === null,
+ format: "LL",
+ firstDay: 1,
+ i18n: {
+ previousMonth: I18n.t("dates.previous_month"),
+ nextMonth: I18n.t("dates.next_month"),
+ months: moment.months(),
+ weekdays: moment.weekdays(),
+ weekdaysShort: moment.weekdaysShort()
+ },
+ onSelect: date => this._handleSelection(date)
+ };
+
+ this._picker = new Pikaday(Object.assign(default_opts, this._opts()));
+ this._picker.setDate(this.date, true);
+ });
+ });
+ },
+
+ _loadNativePicker(container) {
+ const wrapper = container || this.element;
+ const picker = wrapper.querySelector("input.date-picker");
+ picker.onchange = () => this._handleSelection(picker.value);
+ picker.hide = () => {
+ /* do nothing for native */
+ };
+ picker.destroy = () => {
+ /* do nothing for native */
+ };
+ this._picker = picker;
+ },
+
+ _handleSelection(value) {
+ if (!this.element || this.isDestroying || this.isDestroyed) return;
+
+ this._picker && this._picker.hide();
+
+ if (this.onChange) {
+ this.onChange(moment(value).toDate());
+ }
+ },
+
+ @on("willDestroyElement")
+ _destroy() {
+ if (this._picker) {
+ this._picker.destroy();
+ }
+ this._picker = null;
+ },
+
+ @computed()
+ placeholder() {
+ return I18n.t("dates.placeholder");
+ },
+
+ _opts() {
+ return null;
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/date-time-input-range.js.es6 b/app/assets/javascripts/discourse/components/date-time-input-range.js.es6
new file mode 100644
index 00000000000..3754be93cd2
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/date-time-input-range.js.es6
@@ -0,0 +1,51 @@
+export default Ember.Component.extend({
+ classNames: ["d-date-time-input-range"],
+
+ from: null,
+ to: null,
+ onChangeTo: null,
+ onChangeFrom: null,
+ currentPanel: "from",
+ showFromTime: true,
+ showToTime: true,
+ error: null,
+
+ fromPanelActive: Ember.computed.equal("currentPanel", "from"),
+ toPanelActive: Ember.computed.equal("currentPanel", "to"),
+
+ _valid(state) {
+ if (state.to < state.from) {
+ return I18n.t("date_time_picker.errors.to_before_from");
+ }
+
+ return true;
+ },
+
+ actions: {
+ _onChange(options, value) {
+ if (this.onChange) {
+ const state = {
+ from: this.from,
+ to: this.to
+ };
+
+ const diff = {};
+ diff[options.prop] = value;
+
+ const newState = Object.assign(state, diff);
+
+ const validation = this._valid(newState);
+ if (validation === true) {
+ this.set("error", null);
+ this.onChange(newState);
+ } else {
+ this.set("error", validation);
+ }
+ }
+ },
+
+ onChangePanel(panel) {
+ this.set("currentPanel", panel);
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/date-time-input.js.es6 b/app/assets/javascripts/discourse/components/date-time-input.js.es6
new file mode 100644
index 00000000000..ce173e3d422
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/date-time-input.js.es6
@@ -0,0 +1,35 @@
+export default Ember.Component.extend({
+ classNames: ["d-date-time-input"],
+ date: null,
+ showTime: true,
+
+ _hours: Ember.computed("date", function() {
+ return this.date ? this.date.getHours() : null;
+ }),
+
+ _minutes: Ember.computed("date", function() {
+ return this.date ? this.date.getMinutes() : null;
+ }),
+
+ actions: {
+ onChangeTime(time) {
+ if (this.onChange) {
+ const year = this.date.getFullYear();
+ const month = this.date.getMonth();
+ const day = this.date.getDate();
+ this.onChange(new Date(year, month, day, time.hours, time.minutes));
+ }
+ },
+
+ onChangeDate(date) {
+ if (this.onChange) {
+ const year = date.getFullYear();
+ const month = date.getMonth();
+ const day = date.getDate();
+ this.onChange(
+ new Date(year, month, day, this._hours || 0, this._minutes || 0)
+ );
+ }
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/time-input.js.es6 b/app/assets/javascripts/discourse/components/time-input.js.es6
new file mode 100644
index 00000000000..bdeb1b3a722
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/time-input.js.es6
@@ -0,0 +1,73 @@
+import { isNumeric } from "discourse/lib/utilities";
+
+export default Ember.Component.extend({
+ classNames: ["d-time-input"],
+ hours: null,
+ minutes: null,
+ _hours: Ember.computed.oneWay("hours"),
+ _minutes: Ember.computed.oneWay("minutes"),
+ isSafari: Ember.computed.oneWay("capabilities.isSafari"),
+ isMobile: Ember.computed.oneWay("site.mobileView"),
+ nativePicker: Ember.computed.or("isSafari", "isMobile"),
+
+ actions: {
+ onInput(options, event) {
+ event.preventDefault();
+
+ if (this.onChange) {
+ let value = event.target.value;
+
+ if (!isNumeric(value)) {
+ value = 0;
+ } else {
+ value = parseInt(value, 10);
+ }
+
+ if (options.prop === "hours") {
+ value = Math.max(0, Math.min(value, 23))
+ .toString()
+ .padStart(2, "0");
+ this._processHoursChange(value);
+ } else {
+ value = Math.max(0, Math.min(value, 59))
+ .toString()
+ .padStart(2, "0");
+ this._processMinutesChange(value);
+ }
+
+ Ember.run.schedule("afterRender", () => (event.target.value = value));
+ }
+ },
+
+ onFocusIn(value, event) {
+ if (value && event.target) {
+ event.target.select();
+ }
+ },
+
+ onChangeTime(event) {
+ const time = event.target.value;
+
+ if (time && this.onChange) {
+ this.onChange({
+ hours: time.split(":")[0],
+ minutes: time.split(":")[1]
+ });
+ }
+ }
+ },
+
+ _processHoursChange(hours) {
+ this.onChange({
+ hours,
+ minutes: this._minutes || "00"
+ });
+ },
+
+ _processMinutesChange(minutes) {
+ this.onChange({
+ hours: this._hours || "00",
+ minutes
+ });
+ }
+});
diff --git a/app/assets/javascripts/discourse/templates/components/date-input.hbs b/app/assets/javascripts/discourse/templates/components/date-input.hbs
new file mode 100644
index 00000000000..e29eb0e73af
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/date-input.hbs
@@ -0,0 +1,5 @@
+{{input
+ type=inputType
+ class="date-picker"
+ placeholder=placeholder
+ value=value}}
diff --git a/app/assets/javascripts/discourse/templates/components/date-time-input-range.hbs b/app/assets/javascripts/discourse/templates/components/date-time-input-range.hbs
new file mode 100644
index 00000000000..e4476ae9486
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/date-time-input-range.hbs
@@ -0,0 +1,34 @@
+
+ -
+ {{d-button
+ label="date_time_picker.from"
+ class="from-panel"
+ action=(action "onChangePanel" "from")}}
+
+ -
+ {{d-button
+ label="date_time_picker.to"
+ class="to-panel"
+ action=(action "onChangePanel" "to")}}
+
+
+
+{{#if error}}
+ {{error}}
+{{/if}}
+
+
+ {{date-time-input
+ date=from
+ onChange=(action "_onChange" (hash prop="from"))
+ showTime=showFromTime
+ }}
+
+
+
+ {{date-time-input
+ date=to
+ onChange=(action "_onChange" (hash prop="to"))
+ showTime=showToTime
+ }}
+
diff --git a/app/assets/javascripts/discourse/templates/components/date-time-input.hbs b/app/assets/javascripts/discourse/templates/components/date-time-input.hbs
new file mode 100644
index 00000000000..a2da31c28c8
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/date-time-input.hbs
@@ -0,0 +1,9 @@
+{{date-input date=date onChange=(action "onChangeDate")}}
+
+{{#if showTime}}
+ {{time-input
+ hours=_hours
+ minutes=_minutes
+ onChange=(action "onChangeTime")
+ }}
+{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/time-input.hbs b/app/assets/javascripts/discourse/templates/components/time-input.hbs
new file mode 100644
index 00000000000..51ada91bf64
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/time-input.hbs
@@ -0,0 +1,40 @@
+
+ {{#if nativePicker}}
+ {{input
+ class="field time"
+ type="time"
+ value=(concat _hours ":" _minutes)
+ change=(action "onChangeTime")
+ }}
+ {{else}}
+ {{input
+ class="field hours"
+ type="number"
+ title="Hours"
+ minlength=2
+ maxlength=2
+ max="23"
+ min="0"
+ placeholder="00"
+ value=_hours
+ input=(action "onInput" (hash prop="hours"))
+ focus-in=(action "onFocusIn")
+ }}
+
+
:
+
+ {{input
+ class="field minutes"
+ title="Minutes"
+ type="number"
+ minlength=2
+ maxlength=2
+ max="59"
+ min="0"
+ placeholder="00"
+ value=_minutes
+ input=(action "onInput" (hash prop="minutes"))
+ focus-in=(action "onFocusIn")
+ }}
+ {{/if}}
+
diff --git a/app/assets/javascripts/polyfills.js b/app/assets/javascripts/polyfills.js
index ab97aebb351..5c115208f74 100644
--- a/app/assets/javascripts/polyfills.js
+++ b/app/assets/javascripts/polyfills.js
@@ -189,4 +189,40 @@ if (RegExp.prototype.flags === undefined) {
});
}
+// https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
+if (!String.prototype.padStart) {
+ String.prototype.padStart = function padStart(targetLength, padString) {
+ targetLength = targetLength >> 0; //truncate if number, or convert non-number to 0;
+ padString = String(typeof padString !== "undefined" ? padString : " ");
+ if (this.length >= targetLength) {
+ return String(this);
+ } else {
+ targetLength = targetLength - this.length;
+ if (targetLength > padString.length) {
+ padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
+ }
+ return padString.slice(0, targetLength) + String(this);
+ }
+ };
+}
+
+// https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd
+if (!String.prototype.padEnd) {
+ String.prototype.padEnd = function padEnd(targetLength, padString) {
+ targetLength = targetLength >> 0; //floor if number or convert non-number to 0;
+ padString = String(typeof padString !== "undefined" ? padString : " ");
+ if (this.length > targetLength) {
+ return String(this);
+ } else {
+ targetLength = targetLength - this.length;
+ if (targetLength > padString.length) {
+ padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
+ }
+ return String(this) + padString.slice(0, targetLength);
+ }
+ };
+}
+
/* eslint-enable */
diff --git a/app/assets/stylesheets/common/components/date-input.scss b/app/assets/stylesheets/common/components/date-input.scss
new file mode 100644
index 00000000000..583039a0017
--- /dev/null
+++ b/app/assets/stylesheets/common/components/date-input.scss
@@ -0,0 +1,13 @@
+.d-date-input {
+ .date-picker {
+ margin: 0;
+ text-align: left;
+ width: 100%;
+ outline: none;
+ box-shadow: none !important;
+ }
+
+ .pika-single {
+ margin-left: -1px;
+ }
+}
diff --git a/app/assets/stylesheets/common/components/date-time-input-range.scss b/app/assets/stylesheets/common/components/date-time-input-range.scss
new file mode 100644
index 00000000000..438de7742e0
--- /dev/null
+++ b/app/assets/stylesheets/common/components/date-time-input-range.scss
@@ -0,0 +1,42 @@
+.d-date-time-input-range {
+ padding: 0.5em;
+ background: whitesmole;
+ border: 1px solid $primary-low;
+ width: 300px;
+ display: flex;
+ flex-direction: column;
+
+ .panels {
+ display: inline-flex;
+ list-style: none;
+ margin: 0 0 0.5em 0;
+ flex: 1;
+
+ &.from {
+ .from-panel {
+ background: $danger;
+ color: $secondary;
+ }
+ }
+
+ &.to {
+ .to-panel {
+ background: $danger;
+ color: $secondary;
+ }
+ }
+
+ .btn {
+ margin-right: 0.5em;
+ }
+ }
+
+ .panel {
+ display: none;
+ flex: 1;
+
+ &.visible {
+ display: flex;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/common/components/date-time-input.scss b/app/assets/stylesheets/common/components/date-time-input.scss
new file mode 100644
index 00000000000..634bc219610
--- /dev/null
+++ b/app/assets/stylesheets/common/components/date-time-input.scss
@@ -0,0 +1,15 @@
+.d-date-time-input {
+ display: flex;
+ align-items: center;
+ border: 1px solid $primary-low;
+ width: 258px;
+ box-sizing: border-box;
+ position: relative;
+ flex: 1;
+ justify-content: space-between;
+
+ .date-picker,
+ .fields {
+ border: 0;
+ }
+}
diff --git a/app/assets/stylesheets/common/components/time-input.scss b/app/assets/stylesheets/common/components/time-input.scss
new file mode 100644
index 00000000000..d11e2b7c7d9
--- /dev/null
+++ b/app/assets/stylesheets/common/components/time-input.scss
@@ -0,0 +1,39 @@
+.d-time-input {
+ box-sizing: border-box;
+
+ .fields {
+ display: flex;
+ align-items: center;
+ border: 1px solid $primary-low;
+
+ .field {
+ text-align: center;
+ width: auto;
+ margin: 0;
+ border: none;
+ outline: none;
+ box-shadow: none;
+ width: 32px;
+
+ &.time {
+ width: 100%;
+ text-align: left;
+ }
+
+ &.hours,
+ &.minutes {
+ text-align: center;
+ width: 45px;
+ }
+
+ &.hours {
+ padding-right: 0;
+ }
+
+ &.minutes {
+ padding-left: 10px;
+ width: 55px;
+ }
+ }
+ }
+}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 032cbe7f366..a728f185d2a 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -1518,6 +1518,12 @@ en:
one: "Select at least {{count}} item."
other: "Select at least {{count}} items."
+ date_time_picker:
+ from: From
+ to: To
+ errors:
+ to_before_from: "To date must be later than from date."
+
emoji_picker:
filter_placeholder: Search for emoji
smileys_&_emotion: Smileys and Emotion
diff --git a/test/javascripts/components/date-input-test.js.es6 b/test/javascripts/components/date-input-test.js.es6
new file mode 100644
index 00000000000..64686aba06d
--- /dev/null
+++ b/test/javascripts/components/date-input-test.js.es6
@@ -0,0 +1,65 @@
+import componentTest from "helpers/component-test";
+
+moduleForComponent("date-input", { integration: true });
+
+function dateInput() {
+ return find(".date-picker");
+}
+
+function setDate(date) {
+ this.set("date", date);
+}
+
+async function pika(year, month, day) {
+ await click(
+ `.pika-button.pika-day[data-pika-year="${year}"][data-pika-month="${month}"][data-pika-day="${day}"]`
+ );
+}
+
+function noop() {}
+
+const DEFAULT_DATE = new Date(2019, 0, 29);
+
+componentTest("default", {
+ template: `{{date-input date=date}}`,
+
+ beforeEach() {
+ this.setProperties({ date: DEFAULT_DATE });
+ },
+
+ test(assert) {
+ assert.equal(dateInput().val(), "January 29, 2019");
+ }
+});
+
+componentTest("prevents mutations", {
+ template: `{{date-input date=date onChange=onChange}}`,
+
+ beforeEach() {
+ this.setProperties({ date: DEFAULT_DATE });
+ this.set("onChange", noop);
+ },
+
+ async test(assert) {
+ await click(dateInput());
+ await pika(2019, 0, 2);
+
+ assert.ok(this.date.getTime() === DEFAULT_DATE.getTime());
+ }
+});
+
+componentTest("allows mutations through actions", {
+ template: `{{date-input date=date onChange=onChange}}`,
+
+ beforeEach() {
+ this.setProperties({ date: DEFAULT_DATE });
+ this.set("onChange", setDate);
+ },
+
+ async test(assert) {
+ await click(dateInput());
+ await pika(2019, 0, 2);
+
+ assert.ok(this.date.getTime() === new Date(2019, 0, 2).getTime());
+ }
+});
diff --git a/test/javascripts/components/date-time-input-range-test.js.es6 b/test/javascripts/components/date-time-input-range-test.js.es6
new file mode 100644
index 00000000000..8176943b991
--- /dev/null
+++ b/test/javascripts/components/date-time-input-range-test.js.es6
@@ -0,0 +1,102 @@
+import componentTest from "helpers/component-test";
+
+moduleForComponent("date-time-input-range", { integration: true });
+
+function fromDateInput() {
+ return find(".from .date-picker");
+}
+
+function fromHoursInput() {
+ return find(".from .field.hours");
+}
+
+function fromMinutesInput() {
+ return find(".from .field.minutes");
+}
+
+function toDateInput() {
+ return find(".to .date-picker");
+}
+
+function toHoursInput() {
+ return find(".to .field.hours");
+}
+
+function toMinutesInput() {
+ return find(".to .field.minutes");
+}
+
+function setDates(dates) {
+ this.setProperties(dates);
+}
+
+async function pika(year, month, day) {
+ await click(
+ `.pika-button.pika-day[data-pika-year="${year}"][data-pika-month="${month}"][data-pika-day="${day}"]`
+ );
+}
+
+const DEFAULT_DATE_TIME = new Date(2019, 0, 29, 14, 45);
+
+componentTest("default", {
+ template: `{{date-time-input-range from=date to=to}}`,
+
+ beforeEach() {
+ this.setProperties({ date: DEFAULT_DATE_TIME, to: null });
+ },
+
+ test(assert) {
+ assert.equal(fromDateInput().val(), "January 29, 2019");
+ assert.equal(fromHoursInput().val(), "14");
+ assert.equal(fromMinutesInput().val(), "45");
+
+ assert.equal(toDateInput().val(), "");
+ assert.equal(toHoursInput().val(), "");
+ assert.equal(toMinutesInput().val(), "");
+ }
+});
+
+componentTest("can switch panels", {
+ template: `{{date-time-input-range}}`,
+
+ async test(assert) {
+ assert.ok(exists(".panel.from.visible"));
+ assert.notOk(exists(".panel.to.visible"));
+
+ await click(".panels .to-panel");
+
+ assert.ok(exists(".panel.to.visible"));
+ assert.notOk(exists(".panel.from.visible"));
+ }
+});
+
+componentTest("prevents toDate to be before fromDate", {
+ template: `{{date-time-input-range from=from to=to onChange=onChange}}`,
+
+ beforeEach() {
+ this.setProperties({
+ from: DEFAULT_DATE_TIME,
+ to: DEFAULT_DATE_TIME,
+ onChange: setDates
+ });
+ },
+
+ async test(assert) {
+ assert.notOk(exists(".error"));
+
+ await click(toDateInput());
+ await pika(2019, 0, 1);
+
+ assert.ok(exists(".error"));
+ assert.ok(
+ this.to.getTime() === DEFAULT_DATE_TIME.getTime(),
+ "it didnt trigger a mutation"
+ );
+
+ await click(toDateInput());
+ await pika(2019, 0, 30);
+
+ assert.notOk(exists(".error"));
+ assert.ok(this.to.getTime() === new Date(2019, 0, 30, 14, 45).getTime());
+ }
+});
diff --git a/test/javascripts/components/date-time-input-test.js.es6 b/test/javascripts/components/date-time-input-test.js.es6
new file mode 100644
index 00000000000..2dd4ac8fcf4
--- /dev/null
+++ b/test/javascripts/components/date-time-input-test.js.es6
@@ -0,0 +1,84 @@
+import componentTest from "helpers/component-test";
+
+moduleForComponent("date-time-input", { integration: true });
+
+function dateInput() {
+ return find(".date-picker");
+}
+
+function hoursInput() {
+ return find(".field.hours");
+}
+
+function minutesInput() {
+ return find(".field.minutes");
+}
+
+function setDate(date) {
+ this.set("date", date);
+}
+
+async function pika(year, month, day) {
+ await click(
+ `.pika-button.pika-day[data-pika-year="${year}"][data-pika-month="${month}"][data-pika-day="${day}"]`
+ );
+}
+
+const DEFAULT_DATE_TIME = new Date(2019, 0, 29, 14, 45);
+
+componentTest("default", {
+ template: `{{date-time-input date=date}}`,
+
+ beforeEach() {
+ this.setProperties({ date: DEFAULT_DATE_TIME });
+ },
+
+ test(assert) {
+ assert.equal(dateInput().val(), "January 29, 2019");
+ assert.equal(hoursInput().val(), "14");
+ assert.equal(minutesInput().val(), "45");
+ }
+});
+
+componentTest("prevents mutations", {
+ template: `{{date-time-input date=date}}`,
+
+ beforeEach() {
+ this.setProperties({ date: DEFAULT_DATE_TIME });
+ },
+
+ async test(assert) {
+ await click(dateInput());
+ await pika(2019, 0, 2);
+
+ assert.ok(this.date.getTime() === DEFAULT_DATE_TIME.getTime());
+ }
+});
+
+componentTest("allows mutations through actions", {
+ template: `{{date-time-input date=date onChange=onChange}}`,
+
+ beforeEach() {
+ this.setProperties({ date: DEFAULT_DATE_TIME });
+ this.set("onChange", setDate);
+ },
+
+ async test(assert) {
+ await click(dateInput());
+ await pika(2019, 0, 2);
+
+ assert.ok(this.date.getTime() === new Date(2019, 0, 2, 14, 45).getTime());
+ }
+});
+
+componentTest("can hide time", {
+ template: `{{date-time-input date=date showTime=false}}`,
+
+ beforeEach() {
+ this.setProperties({ date: DEFAULT_DATE_TIME });
+ },
+
+ async test(assert) {
+ assert.notOk(exists(hoursInput()));
+ }
+});
diff --git a/test/javascripts/components/time-input-test.js.es6 b/test/javascripts/components/time-input-test.js.es6
new file mode 100644
index 00000000000..edbe7574c82
--- /dev/null
+++ b/test/javascripts/components/time-input-test.js.es6
@@ -0,0 +1,97 @@
+import componentTest from "helpers/component-test";
+
+moduleForComponent("time-input", { integration: true });
+
+function hoursInput() {
+ return find(".field.hours");
+}
+
+function minutesInput() {
+ return find(".field.minutes");
+}
+
+function setTime(time) {
+ this.setProperties(time);
+}
+
+function noop() {}
+
+componentTest("default", {
+ template: `{{time-input hours=hours minutes=minutes}}`,
+
+ beforeEach() {
+ this.setProperties({ hours: "14", minutes: "58" });
+ },
+
+ test(assert) {
+ assert.equal(hoursInput().val(), "14");
+ assert.equal(minutesInput().val(), "58");
+ }
+});
+
+componentTest("prevents mutations", {
+ template: `{{time-input hours=hours minutes=minutes}}`,
+
+ beforeEach() {
+ this.setProperties({ hours: "14", minutes: "58" });
+ },
+
+ async test(assert) {
+ await fillIn(hoursInput(), "12");
+ assert.ok(this.hours === "14");
+
+ await fillIn(minutesInput(), "36");
+ assert.ok(this.minutes === "58");
+ }
+});
+
+componentTest("allows mutations through actions", {
+ template: `{{time-input hours=hours minutes=minutes onChange=onChange}}`,
+
+ beforeEach() {
+ this.setProperties({ hours: "14", minutes: "58" });
+ this.set("onChange", setTime);
+ },
+
+ async test(assert) {
+ await fillIn(hoursInput(), "12");
+ assert.ok(this.hours === "12");
+
+ await fillIn(minutesInput(), "36");
+ assert.ok(this.minutes === "36");
+ }
+});
+
+componentTest("hours and minutes have boundaries", {
+ template: `{{time-input hours=14 minutes=58 onChange=onChange}}`,
+
+ beforeEach() {
+ this.set("onChange", noop);
+ },
+
+ async test(assert) {
+ await fillIn(hoursInput(), "2");
+ assert.equal(hoursInput().val(), "02");
+
+ await fillIn(hoursInput(), "@");
+ assert.equal(hoursInput().val(), "00");
+
+ await fillIn(hoursInput(), "24");
+ assert.equal(hoursInput().val(), "23");
+
+ await fillIn(hoursInput(), "-1");
+ assert.equal(hoursInput().val(), "00");
+
+ await fillIn(minutesInput(), "@");
+ assert.equal(minutesInput().val(), "00");
+
+ await fillIn(minutesInput(), "2");
+ assert.equal(minutesInput().val(), "02");
+
+ await fillIn(minutesInput(), "60");
+ assert.equal(minutesInput().val(), "59");
+
+ await fillIn(minutesInput(), "-1");
+ assert.equal(minutesInput().val(), "00");
+ }
+});