diff --git a/lib/osf-components/addon/components/new-project-modal/component.ts b/lib/osf-components/addon/components/new-project-modal/component.ts
index 07a840328..57ac64514 100644
--- a/lib/osf-components/addon/components/new-project-modal/component.ts
+++ b/lib/osf-components/addon/components/new-project-modal/component.ts
@@ -215,4 +215,20 @@ export default class NewProjectModal extends Component {
searchNodes(this: NewProjectModal, searchTerm: string) {
return this.get('searchUserNodesTask').perform(searchTerm);
}
+
+ @action
+ handleKeydown(_: string, event: KeyboardEvent & { isComposing?: boolean }) {
+ if (!event) {
+ return;
+ }
+
+ const isIME = event.isComposing || event.keyCode === 229;
+ if (isIME) {
+ return;
+ }
+ if (event.key === 'Enter' && this.nodeTitle && this.nodeTitle.trim()) {
+ event.preventDefault();
+ this.create();
+ }
+ }
}
diff --git a/lib/osf-components/addon/components/new-project-modal/template.hbs b/lib/osf-components/addon/components/new-project-modal/template.hbs
index ccf72b616..aacb0bbae 100644
--- a/lib/osf-components/addon/components/new-project-modal/template.hbs
+++ b/lib/osf-components/addon/components/new-project-modal/template.hbs
@@ -41,7 +41,7 @@
@required={{true}}
@value={{this.nodeTitle}}
@type='text'
- @enter={{action this.create}}
+ @key-down={{action this.handleKeydown}}
/>
diff --git a/tests/integration/components/new-project-modal/component-test.ts b/tests/integration/components/new-project-modal/component-test.ts
index 6db8a2e06..90c60498a 100644
--- a/tests/integration/components/new-project-modal/component-test.ts
+++ b/tests/integration/components/new-project-modal/component-test.ts
@@ -1,27 +1,129 @@
import { A } from '@ember/array';
+import EmberObject, { action } from '@ember/object';
import Service from '@ember/service';
-import { render } from '@ember/test-helpers';
+import { fillIn, render, triggerEvent, triggerKeyEvent } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupRenderingTest } from 'ember-qunit';
import { TestContext } from 'ember-test-helpers';
import { module, test } from 'qunit';
+import Promise from 'rsvp';
+import sinon from 'sinon';
-const currentUserStub = Service.extend({
- user: Object.freeze({
- institutions: A([]),
- }),
-});
+import NewProjectModal from 'osf-components/components/new-project-modal/component';
+
+interface LocalTestContext extends TestContext {
+ createActionSpy: sinon.SinonSpy;
+}
+
+let createSpyPlaceholder: sinon.SinonSpy | undefined;
+
+class MockNewProjectModal extends NewProjectModal {
+ @action
+ create(this: any, ...args: any[]) {
+ if (createSpyPlaceholder) {
+ createSpyPlaceholder(...args);
+ }
+ }
+}
module('Integration | Component | new-project-modal', hooks => {
setupRenderingTest(hooks);
- hooks.beforeEach(function(this: TestContext) {
- this.owner.register('service:current-user', currentUserStub);
+ hooks.beforeEach(function(this: LocalTestContext) {
+ this.createActionSpy = sinon.spy();
+ createSpyPlaceholder = this.createActionSpy;
+
+ this.owner.register('component:new-project-modal', MockNewProjectModal);
+
+ const mockUser = EmberObject.create({
+ institutions: A([EmberObject.create({ id: 'mock' })]),
+ defaultRegion: EmberObject.create({ id: 'us' }),
+ });
+
+ this.owner.register('service:current-user', Service.extend({ user: mockUser }));
+ this.owner.register('service:features', Service.extend({ isEnabled: () => false }));
+ this.owner.register('service:intl', Service.extend({ t: (key: string) => key }));
+ this.owner.register('service:toast', Service.extend({ error: sinon.spy(), success: sinon.spy() }));
+ this.owner.register('service:analytics', Service.extend({ click: sinon.spy() }));
+ this.owner.register('service:store', Service.extend({
+ findAll() { return Promise.resolve(A([])); },
+ }));
+ });
+
+ hooks.afterEach(() => {
+ createSpyPlaceholder = undefined;
});
test('it renders', async assert => {
- await render(hbs``);
+ await render(hbs`
+
+ `);
assert.dom('.modal').exists();
assert.dom('.modal-title').hasText('Create new project');
});
+
+ test('create button is disabled initially', async assert => {
+ await render(hbs`
+
+ `);
+ assert.dom('[data-test-create-project-submit]').isDisabled();
+ });
+
+ test('it renders and initializes correctly', async assert => {
+ await render(hbs`
+
+ `);
+
+ assert.dom('[data-test-new-project-title]').exists('Input fields for project title exists');
+ });
+
+ test(
+ 'Enter key triggers create action through DOM binding when not composing',
+ async function(this: LocalTestContext, assert) {
+ await render(hbs`
+
+ `);
+
+ const inputSelector = '[data-test-new-project-title]';
+ assert.dom(inputSelector).exists('Input field must exist');
+
+ await fillIn(inputSelector, 'My Awesome New Project');
+
+ await triggerKeyEvent(inputSelector, 'keydown', 'Enter');
+
+ assert.ok(
+ this.createActionSpy.calledOnce,
+ 'The create() action on the component should be triggered.',
+ );
+ },
+ );
+
+ test(
+ 'Enter key does NOT trigger create action when Japanese IME is composing',
+ async function(this: LocalTestContext, assert) {
+ await render(hbs`
+
+ `);
+
+ const inputSelector = '[data-test-new-project-title]';
+ assert.dom(inputSelector).exists();
+
+ await fillIn(inputSelector, 'プロジェクト');
+
+ await triggerEvent(inputSelector, 'keydown', {
+ key: 'Enter',
+ code: 'Enter',
+ keyCode: 229,
+ which: 229,
+ isComposing: true,
+ bubbles: true,
+ cancelable: true,
+ } as any);
+
+ assert.notOk(
+ this.createActionSpy.called,
+ 'The create() action must NOT be triggered during IME composition.',
+ );
+ },
+ );
});