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.', + ); + }, + ); });