- {{#if (and this.editing (not @loading))}}
-
- {{yield (hash value=@value update=this.update) to="editing"}}
-
+
+ {{#if (and this.editingIsEnabled (not @loading))}}
+ {{on-document "keydown" this.handleKeydown}}
+ {{yield
+ (hash
+ value=this.value
+ update=this.maybeUpdateValue
+ input=this.inputModifier
+ emptyValueErrorIsShown=this.emptyValueErrorIsShown
+ )
+ to="editing"
+ }}
{{else}}
{{#if @loading}}
{{/if}}
-
+ {{yield (hash value=this.value)}}
+
{{/if}}
diff --git a/web/app/components/editable-field.ts b/web/app/components/editable-field.ts
index 00e627700..6d3affcd2 100644
--- a/web/app/components/editable-field.ts
+++ b/web/app/components/editable-field.ts
@@ -1,8 +1,10 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
-import { scheduleOnce } from "@ember/runloop";
+import { schedule, scheduleOnce } from "@ember/runloop";
import { assert } from "@ember/debug";
+import { modifier } from "ember-modifier";
+import { ModifierLike } from "@glint/template";
export const FOCUSABLE =
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
@@ -14,74 +16,172 @@ interface EditableFieldComponentSignature {
onChange: (value: any) => void;
loading?: boolean;
disabled?: boolean;
+ isRequired?: boolean;
};
Blocks: {
- default: [];
+ default: [value: any];
editing: [
F: {
value: any;
update: (value: any) => void;
+ input: ModifierLike<{
+ Element: HTMLInputElement | HTMLTextAreaElement;
+ Return: void;
+ }>;
+ emptyValueErrorIsShown: boolean;
}
];
};
}
export default class EditableFieldComponent extends Component
{
- @tracked protected editing = false;
- @tracked protected el: HTMLElement | null = null;
- @tracked protected cachedValue = null;
+ /**
+ * The cached value of the field.
+ * Initially set to the value passed in; updated when committed.
+ * Required to handle both the PEOPLE and STRING fieldTypes,
+ * which are processed differently by their sub-components.
+ */
+ private cachedValue = this.args.value;
- @action protected captureElement(el: HTMLElement) {
- this.el = el;
- }
+ /**
+ * The value of the field. Initially set to the value passed in.
+ * Updated when the user commits their changes.
+ */
+ @tracked protected value = this.args.value;
- @action protected edit() {
- this.cachedValue = this.args.value;
- this.editing = true;
+ /**
+ * Whether the <:editing> block is enabled.
+ * Set true by clicking the <:default> content.
+ * Set false by the `disableEditing` action on blur.
+ */
+ @tracked protected editingIsEnabled = false;
- // Kinda gross, but this gives focus to the first focusable element in the
- // :editing block, which will typically be an input.
- scheduleOnce("afterRender", this, () => {
- if (this.el && !this.el.contains(document.activeElement)) {
- const firstInput = this.el.querySelector(FOCUSABLE);
- if (firstInput) (firstInput as HTMLElement).focus();
- }
- });
- }
+ /**
+ * Whether the empty-value error is shown.
+ * Set true when a required field is committed with an empty value.
+ * Yielded to the <:editing> block to show/hide the error.
+ */
+ @tracked protected emptyValueErrorIsShown = false;
+
+ /**
+ * Whether the user has cancelled their edit using the Escape key.
+ * Used on blur to determine whether to reset the value to the original.
+ * Set true by the `handleKeydown` task on Escape keydown.
+ */
+ @tracked private hasCancelled = false;
- @action protected cancel(ev: KeyboardEvent) {
- if (ev.key === "Escape") {
- ev.preventDefault();
- scheduleOnce("actions", this, () => {
- this.editing = false;
+ /**
+ * The input or textarea element, if using.
+ * Registered by the `inputModifier` action, used for focusing and blurring.
+ */
+ @tracked private inputElement: HTMLInputElement | HTMLTextAreaElement | null =
+ null;
+
+ /**
+ * The modifier passed to the `editing` block to apply to the input or textarea.
+ * Autofocuses the input and adds a blur listener to commit changes.
+ */
+ protected inputModifier = modifier((element: HTMLElement) => {
+ this.inputElement = element as HTMLInputElement | HTMLTextAreaElement;
+ this.inputElement.focus();
+ element.addEventListener("blur", this.onBlur);
+ return () => element.removeEventListener("blur", this.onBlur);
+ });
+
+ /**
+ * The action run when an `inputModifier`-registered input blurs.
+ * If blurring is the result of a cancel, the value is reset to the original,
+ * otherwise the value is passed to the `maybeUpdateValue` method.
+ */
+ @action private onBlur(event: FocusEvent) {
+ if (this.hasCancelled) {
+ this.value = this.args.value;
+ schedule("actions", () => {
+ this.hasCancelled = false;
});
+ return;
}
+
+ this.maybeUpdateValue(event);
}
- @action protected preventNewlines(ev: KeyboardEvent) {
- if (ev.key === "Enter") {
- ev.preventDefault();
- }
+ /**
+ * The action to enable the <:editing> block.
+ * Called when a user clicks the <:default> content.
+ */
+ @action protected enableEditing() {
+ this.editingIsEnabled = true;
}
- @action protected update(eventOrValue: Event | any) {
- scheduleOnce("actions", this, () => {
- this.editing = false;
- });
+ /**
+ * The action to disable the <:editing> block.
+ * Called when a user commits or cancels their edit.
+ */
+ @action protected disableEditing() {
+ this.editingIsEnabled = false;
+ }
+
+ /**
+ * The action to handle Enter and Escape keydowns while the <:editing> block is open.
+ * On Enter, the input blurs, causing `maybeUpdateValue` action to run.
+ * On Escape, we disable editing.
+ */
+ @action protected handleKeydown(ev: KeyboardEvent) {
+ switch (ev.key) {
+ case "Enter":
+ ev.preventDefault();
+ if (this.inputElement) {
+ this.inputElement.blur();
+ }
+ break;
+ case "Escape":
+ ev.preventDefault();
+ this.hasCancelled = true;
+ this.disableEditing();
+ break;
+ }
+ }
- let newValue = eventOrValue;
+ /**
+ * The action run when the <:editing> block is committed.
+ * Checks the value to see if it has changed, and if so, updates the value
+ * and resets the cached value. If the value is empty and the field is required,
+ * triggers the empty-value error.
+ */
+ @action protected maybeUpdateValue(eventOrValue: Event | any) {
+ let newValue: string | string[] | undefined;
if (eventOrValue instanceof Event) {
const target = eventOrValue.target;
assert("target must exist", target);
assert("value must exist in the target", "value" in target);
const value = target.value;
- newValue = value;
+ newValue = value as string | string[];
+ } else {
+ newValue = eventOrValue;
}
- if (newValue !== this.cachedValue) {
- this.args.onChange?.(newValue);
+ // Stringified values work for both arrays and strings.
+ if (JSON.stringify(newValue) !== JSON.stringify(this.cachedValue)) {
+ if (newValue === "") {
+ if (this.args.isRequired) {
+ this.emptyValueErrorIsShown = true;
+ return;
+ }
+ // Nothing has really changed, so we don't update the value.
+ if (this.args.value === undefined) {
+ this.disableEditing();
+ return;
+ }
+ }
+
+ this.cachedValue = this.value = newValue;
+ this.args.onChange?.(this.value);
}
+
+ scheduleOnce("actions", this, () => {
+ this.disableEditing();
+ });
}
}
diff --git a/web/app/components/inputs/people-select.ts b/web/app/components/inputs/people-select.ts
index b68a06140..74eee7c2a 100644
--- a/web/app/components/inputs/people-select.ts
+++ b/web/app/components/inputs/people-select.ts
@@ -16,7 +16,6 @@ interface InputsPeopleSelectComponentSignature {
Element: HTMLDivElement;
Args: {
selected: HermesUser[];
- onBlur?: () => void;
onChange: (people: HermesUser[]) => void;
};
}
@@ -51,9 +50,6 @@ export default class InputsPeopleSelectComponent extends Component .field-toggle {
all: unset;
diff --git a/web/tests/acceptance/authenticated/document-test.ts b/web/tests/acceptance/authenticated/document-test.ts
index bc9378bc9..826a06774 100644
--- a/web/tests/acceptance/authenticated/document-test.ts
+++ b/web/tests/acceptance/authenticated/document-test.ts
@@ -1,11 +1,11 @@
import {
click,
+ fillIn,
find,
findAll,
triggerEvent,
visit,
waitFor,
- waitUntil,
} from "@ember/test-helpers";
import { setupApplicationTest } from "ember-qunit";
import { module, test } from "qunit";
@@ -33,21 +33,15 @@ const DRAFT_VISIBILITY_TOGGLE_SELECTOR = "[data-test-draft-visibility-toggle]";
const COPY_URL_BUTTON_SELECTOR = "[data-test-sidebar-copy-url-button]";
const DRAFT_VISIBILITY_OPTION_SELECTOR = "[data-test-draft-visibility-option]";
const SECOND_DRAFT_VISIBILITY_LIST_ITEM_SELECTOR = `${DRAFT_VISIBILITY_DROPDOWN_SELECTOR} li:nth-child(2)`;
-const EDITABLE_TITLE_SELECTOR = "[data-test-document-title-editable]";
-const EDITABLE_SUMMARY_SELECTOR = "[data-test-document-summary-editable]";
+const TITLE_SELECTOR = "[data-test-document-title]";
+const SUMMARY_SELECTOR = "[data-test-document-summary]";
+const CONTRIBUTORS_SELECTOR = "[data-test-document-contributors]";
+const APPROVERS_SELECTOR = "[data-test-document-approvers]";
+
const EDITABLE_PRODUCT_AREA_SELECTOR =
"[data-test-document-product-area-editable]";
-const EDITABLE_CONTRIBUTORS_SELECTOR =
- "[data-test-document-contributors-editable]";
-const EDITABLE_APPROVERS_SELECTOR = "[data-test-document-approvers-editable]";
-
-const READ_ONLY_TITLE_SELECTOR = "[data-test-document-title-read-only]";
-const READ_ONLY_SUMMARY_SELECTOR = "[data-test-document-summary-read-only]";
const READ_ONLY_PRODUCT_AREA_SELECTOR =
"[data-test-document-product-area-read-only]";
-const READ_ONLY_CONTRIBUTORS_SELECTOR =
- "[data-test-document-contributors-read-only]";
-const READ_ONLY_APPROVERS_SELECTOR = "[data-test-document-approvers-read-only]";
const SIDEBAR_PUBLISH_FOR_REVIEW_BUTTON_SELECTOR =
"[data-test-sidebar-publish-for-review-button";
const PUBLISH_FOR_REVIEW_MODAL_SELECTOR =
@@ -65,20 +59,16 @@ const DOC_PUBLISHED_COPY_URL_BUTTON_SELECTOR =
"[data-test-doc-published-copy-url-button]";
const assertEditingIsDisabled = (assert: Assert) => {
- assert.dom(EDITABLE_TITLE_SELECTOR).doesNotExist();
- assert.dom(EDITABLE_SUMMARY_SELECTOR).doesNotExist();
- assert.dom(EDITABLE_PRODUCT_AREA_SELECTOR).doesNotExist();
- assert.dom(EDITABLE_CONTRIBUTORS_SELECTOR).doesNotExist();
- assert.dom(EDITABLE_APPROVERS_SELECTOR).doesNotExist();
+ assert.dom(TITLE_SELECTOR).doesNotHaveAttribute("data-test-editable");
+ assert.dom(SUMMARY_SELECTOR).doesNotHaveAttribute("data-test-editable");
+ assert.dom(CONTRIBUTORS_SELECTOR).doesNotHaveAttribute("data-test-editable");
+ assert.dom(APPROVERS_SELECTOR).doesNotHaveAttribute("data-test-editable");
+ assert.dom(EDITABLE_PRODUCT_AREA_SELECTOR).doesNotExist();
assert.dom(DRAFT_VISIBILITY_TOGGLE_SELECTOR).doesNotExist();
assert.dom(ADD_RELATED_RESOURCE_BUTTON_SELECTOR).doesNotExist();
- assert.dom(READ_ONLY_TITLE_SELECTOR).exists();
- assert.dom(READ_ONLY_SUMMARY_SELECTOR).exists();
assert.dom(READ_ONLY_PRODUCT_AREA_SELECTOR).exists();
- assert.dom(READ_ONLY_CONTRIBUTORS_SELECTOR).exists();
- assert.dom(READ_ONLY_APPROVERS_SELECTOR).exists();
};
interface AuthenticatedDocumentRouteTestContext extends MirageTestContext {}
@@ -363,20 +353,16 @@ module("Acceptance | authenticated/document", function (hooks) {
await visit("/document/1?draft=true");
- assert.dom(EDITABLE_TITLE_SELECTOR).exists();
- assert.dom(EDITABLE_SUMMARY_SELECTOR).exists();
- assert.dom(EDITABLE_PRODUCT_AREA_SELECTOR).exists();
- assert.dom(EDITABLE_CONTRIBUTORS_SELECTOR).exists();
- assert.dom(EDITABLE_APPROVERS_SELECTOR).exists();
+ assert.dom(TITLE_SELECTOR).hasAttribute("data-test-editable");
+ assert.dom(SUMMARY_SELECTOR).hasAttribute("data-test-editable");
+ assert.dom(CONTRIBUTORS_SELECTOR).hasAttribute("data-test-editable");
+ assert.dom(APPROVERS_SELECTOR).hasAttribute("data-test-editable");
+ assert.dom(EDITABLE_PRODUCT_AREA_SELECTOR).exists();
assert.dom(DRAFT_VISIBILITY_TOGGLE_SELECTOR).exists();
assert.dom(ADD_RELATED_RESOURCE_BUTTON_SELECTOR).exists();
- assert.dom(READ_ONLY_TITLE_SELECTOR).doesNotExist();
- assert.dom(READ_ONLY_SUMMARY_SELECTOR).doesNotExist();
assert.dom(READ_ONLY_PRODUCT_AREA_SELECTOR).doesNotExist();
- assert.dom(READ_ONLY_CONTRIBUTORS_SELECTOR).doesNotExist();
- assert.dom(READ_ONLY_APPROVERS_SELECTOR).doesNotExist();
});
test("owners can edit everything but the product area of a published doc", async function (this: AuthenticatedDocumentRouteTestContext, assert) {
@@ -388,20 +374,17 @@ module("Acceptance | authenticated/document", function (hooks) {
await visit("/document/1");
- assert.dom(EDITABLE_TITLE_SELECTOR).exists();
- assert.dom(EDITABLE_SUMMARY_SELECTOR).exists();
- assert.dom(EDITABLE_PRODUCT_AREA_SELECTOR).doesNotExist();
- assert.dom(EDITABLE_CONTRIBUTORS_SELECTOR).exists();
- assert.dom(EDITABLE_APPROVERS_SELECTOR).exists();
+ assert.dom(TITLE_SELECTOR).hasAttribute("data-test-editable");
+ assert.dom(SUMMARY_SELECTOR).hasAttribute("data-test-editable");
+ assert.dom(CONTRIBUTORS_SELECTOR).hasAttribute("data-test-editable");
+ assert.dom(APPROVERS_SELECTOR).hasAttribute("data-test-editable");
+ assert.dom(EDITABLE_PRODUCT_AREA_SELECTOR).doesNotExist();
assert.dom(DRAFT_VISIBILITY_TOGGLE_SELECTOR).doesNotExist();
+
assert.dom(ADD_RELATED_RESOURCE_BUTTON_SELECTOR).exists();
- assert.dom(READ_ONLY_TITLE_SELECTOR).doesNotExist();
- assert.dom(READ_ONLY_SUMMARY_SELECTOR).doesNotExist();
assert.dom(READ_ONLY_PRODUCT_AREA_SELECTOR).exists();
- assert.dom(READ_ONLY_CONTRIBUTORS_SELECTOR).doesNotExist();
- assert.dom(READ_ONLY_APPROVERS_SELECTOR).doesNotExist();
});
test("collaborators cannot edit the metadata of a draft", async function (this: AuthenticatedDocumentRouteTestContext, assert) {
@@ -524,4 +507,21 @@ module("Acceptance | authenticated/document", function (hooks) {
"the Continue button becomes the primary button when the copy link is hidden"
);
});
+
+ test("non-required values can be reset by saving an empty value", async function (this: AuthenticatedDocumentRouteTestContext, assert) {
+ this.server.create("document", {
+ objectID: 1,
+ summary: "foo bar baz",
+ });
+
+ await visit("/document/1?draft=true");
+
+ await click(`${SUMMARY_SELECTOR} button`);
+
+ await fillIn(`${SUMMARY_SELECTOR} textarea`, "");
+
+ await triggerEvent(`${SUMMARY_SELECTOR} textarea`, "blur");
+
+ assert.dom(SUMMARY_SELECTOR).hasText("Enter a summary");
+ });
});
diff --git a/web/tests/integration/components/editable-field-test.ts b/web/tests/integration/components/editable-field-test.ts
new file mode 100644
index 000000000..3c3f2f16f
--- /dev/null
+++ b/web/tests/integration/components/editable-field-test.ts
@@ -0,0 +1,360 @@
+import {
+ click,
+ fillIn,
+ render,
+ triggerEvent,
+ triggerKeyEvent,
+ waitUntil,
+} from "@ember/test-helpers";
+import { hbs } from "ember-cli-htmlbars";
+import { MirageTestContext } from "ember-cli-mirage/test-support";
+import { setupRenderingTest } from "ember-qunit";
+import { module, test } from "qunit";
+
+const EDITABLE_FIELD_SELECTOR = ".editable-field";
+const FIELD_TOGGLE_SELECTOR = ".editable-field .field-toggle";
+const LOADING_SPINNER_SELECTOR = ".loading-indicator";
+
+interface EditableFieldComponentTestContext extends MirageTestContext {
+ onChange: (value: any) => void;
+ isLoading: boolean;
+ value: string;
+ newArray: string[];
+}
+
+module("Integration | Component | editable-field", function (hooks) {
+ setupRenderingTest(hooks);
+
+ hooks.beforeEach(function (this: EditableFieldComponentTestContext) {
+ this.set("onChange", () => {});
+ });
+
+ test("it handles splattributes", async function (this: EditableFieldComponentTestContext, assert) {
+ await render(hbs`
+
+ `);
+
+ assert.dom(EDITABLE_FIELD_SELECTOR).hasClass("bar");
+ });
+
+ test("it toggles between default and editing modes", async function (this: EditableFieldComponentTestContext, assert) {
+ await render(hbs`
+
+ <:default>Foo
+ <:editing>Bar
+
+ `);
+
+ assert.dom(EDITABLE_FIELD_SELECTOR).exists({ count: 1 }).hasText("Foo");
+
+ await click(FIELD_TOGGLE_SELECTOR);
+
+ assert.dom(EDITABLE_FIELD_SELECTOR).exists({ count: 1 }).hasText("Bar");
+ });
+
+ test("it yields the expected value", async function (this: EditableFieldComponentTestContext, assert) {
+ await render(hbs`
+
+ <:default as |F|>{{F.value}} one
+ <:editing as |F|>{{F.value}} two
+
+ `);
+
+ assert.dom(EDITABLE_FIELD_SELECTOR).exists({ count: 1 }).hasText("foo one");
+
+ await click(FIELD_TOGGLE_SELECTOR);
+
+ assert.dom(EDITABLE_FIELD_SELECTOR).exists({ count: 1 }).hasText("foo two");
+ });
+
+ test("it can show a loading state", async function (this: EditableFieldComponentTestContext, assert) {
+ this.set("isLoading", true);
+
+ await render(hbs`
+
+ `);
+
+ assert.dom(FIELD_TOGGLE_SELECTOR).hasClass("loading").isDisabled();
+
+ assert.dom(LOADING_SPINNER_SELECTOR).exists();
+
+ this.set("isLoading", false);
+
+ assert
+ .dom(FIELD_TOGGLE_SELECTOR)
+ .doesNotHaveClass("loading")
+ .isNotDisabled();
+
+ assert.dom(LOADING_SPINNER_SELECTOR).doesNotExist();
+ });
+
+ test("it yields an emptyValueErrorIsShown property to the editing block", async function (this: EditableFieldComponentTestContext, assert) {
+ await render(hbs`
+
+ <:default>Foo
+ <:editing as |F|>
+
+
+ {{#if F.emptyValueErrorIsShown}}
+ Empty value error
+ {{/if}}
+
+
+ `);
+
+ assert.dom(".error").doesNotExist();
+
+ await click(FIELD_TOGGLE_SELECTOR);
+
+ assert.dom(".error").doesNotExist();
+
+ await fillIn("input", "");
+ await triggerEvent("input", "blur");
+
+ assert.dom(".error").exists();
+ });
+
+ test("the edit button can be disabled", async function (this: EditableFieldComponentTestContext, assert) {
+ await render(hbs`
+
+ <:default>Foo
+ <:editing>Bar
+
+ `);
+
+ assert.dom(FIELD_TOGGLE_SELECTOR).isDisabled();
+ });
+
+ test("it cancels when the escape key is pressed", async function (this: EditableFieldComponentTestContext, assert) {
+ const defaultText = "foo";
+
+ this.set("value", defaultText);
+
+ await render(hbs`
+
+ <:default as |F|>{{F.value}}
+ <:editing as |F|>
+
+
+
+ `);
+
+ await click(FIELD_TOGGLE_SELECTOR);
+
+ await fillIn("input", "Baz");
+
+ await triggerEvent("input", "keydown", {
+ key: "Escape",
+ });
+
+ assert
+ .dom(EDITABLE_FIELD_SELECTOR)
+ .hasText(defaultText, "value reverts when escape is pressed");
+ });
+
+ test("it runs the passed-in onChange action on blur", async function (this: EditableFieldComponentTestContext, assert) {
+ this.set("value", "foo");
+
+ this.set("onChange", (e: unknown) => {
+ this.set("value", e);
+ });
+
+ await render(hbs`
+
+ <:default as |F|>{{F.value}}
+ <:editing as |F|>
+
+
+
+ `);
+
+ await click(FIELD_TOGGLE_SELECTOR);
+
+ await fillIn("input", "bar");
+
+ // Keying "Enter" tests both `onBlur` and `handleKeydown`
+ // since `handleKeydown` ultimately calls `onBlur`.
+ await triggerKeyEvent("input", "keydown", "Enter");
+
+ assert.dom(EDITABLE_FIELD_SELECTOR).hasText("bar");
+ });
+
+ test("it yields an `update` (string) function to the editing block", async function (this: EditableFieldComponentTestContext, assert) {
+ this.set("value", "foo");
+ this.set("onChange", (newValue: string) => {
+ this.set("value", newValue);
+ });
+
+ await render(hbs`
+
+ <:default as |F|>
+ {{F.value}}
+
+ <:editing as |F|>
+
+ F.update
+
+
+
+ `);
+
+ assert.dom(EDITABLE_FIELD_SELECTOR).hasText("foo");
+ await click(FIELD_TOGGLE_SELECTOR);
+
+ await click("button");
+
+ assert.dom(EDITABLE_FIELD_SELECTOR).hasText("bar");
+ });
+
+ test("it yields an `update` (array) function to the editing block", async function (this: EditableFieldComponentTestContext, assert) {
+ this.set("value", ["foo"]);
+ this.set("onChange", (newValue: string[]) => {
+ this.set("value", newValue);
+ });
+
+ await render(hbs`
+
+ <:default as |F|>
+ {{F.value}}
+
+ <:editing as |F|>
+
+ F.update
+
+
+
+ `);
+
+ assert.dom(EDITABLE_FIELD_SELECTOR).hasText("foo");
+
+ await click(FIELD_TOGGLE_SELECTOR);
+ await click("button");
+
+ assert.dom(EDITABLE_FIELD_SELECTOR).hasText("bar");
+ });
+
+ test("onChange only runs if the textInput value has changed", async function (this: EditableFieldComponentTestContext, assert) {
+ let count = 0;
+ this.set("onChange", () => count++);
+
+ await render(hbs`
+
+ <:default as |F|>
+ {{F.value}}
+
+ <:editing as |F|>
+
+
+
+ `);
+
+ await click(FIELD_TOGGLE_SELECTOR);
+
+ await fillIn("input", "foo");
+ await triggerEvent("input", "blur");
+
+ assert.equal(count, 0, "onChange has not been called");
+
+ await click(FIELD_TOGGLE_SELECTOR);
+
+ await fillIn("input", "bar");
+ await triggerKeyEvent("input", "keydown", "Enter");
+
+ assert.equal(count, 1, "onChange has been called");
+ });
+
+ test("onChange only runs if the array value has changed", async function (this: EditableFieldComponentTestContext, assert) {
+ let count = 0;
+
+ this.set("onChange", () => count++);
+ this.set("newArray", ["foo"]);
+
+ await render(hbs`
+
+ <:default as |F|>
+ {{F.value}}
+
+ <:editing as |F|>
+
+
+
+
+ `);
+
+ await click(FIELD_TOGGLE_SELECTOR);
+ await click(".click-away");
+
+ assert.equal(count, 0, "onChange has not been called");
+
+ this.set("newArray", ["bar"]);
+
+ await click(FIELD_TOGGLE_SELECTOR);
+ await click(".click-away");
+
+ assert.equal(count, 1, "onChange has been called");
+ });
+
+ test("the input value resets on cancel", async function (this: EditableFieldComponentTestContext, assert) {
+ await render(hbs`
+
+ <:default as |F|>
+ {{F.value}}
+
+ <:editing as |F|>
+
+
+
+ `);
+
+ await click(FIELD_TOGGLE_SELECTOR);
+
+ await fillIn("input", "bar");
+ await triggerKeyEvent("input", "keydown", "Escape");
+
+ assert.dom(EDITABLE_FIELD_SELECTOR).hasText("foo");
+
+ await click(FIELD_TOGGLE_SELECTOR);
+
+ assert.dom("input").hasValue("foo");
+ });
+});