Merge changes I49159738,I0358f77a

* changes:
  Convert gr-create-group-dialog to lit
  Convert gr-create-group-dialog_test.js to typescript
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
index ea70d7e..83063a1 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
@@ -185,4 +185,8 @@
   computeLoadingClass(loading: boolean) {
     return loading ? 'loading' : '';
   }
+
+  _handleHasNewGroupName() {
+    this._hasNewGroupName = !!this.$.createNewModal.name;
+  }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.ts
index 91863a9..fdde399 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.ts
@@ -70,8 +70,8 @@
       <div class="header" slot="header">Create Group</div>
       <div class="main" slot="main">
         <gr-create-group-dialog
-          has-new-group-name="{{_hasNewGroupName}}"
           id="createNewModal"
+          on-has-new-group-name="_handleHasNewGroupName"
         ></gr-create-group-dialog>
       </div>
     </gr-dialog>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts
index 180e60a..ce07804 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts
@@ -17,61 +17,95 @@
 import '@polymer/iron-input/iron-input';
 import '../../../styles/gr-form-styles';
 import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-create-group-dialog_html';
 import {encodeURL, getBaseUrl} from '../../../utils/url-util';
 import {page} from '../../../utils/page-wrapper-utils';
-import {customElement, property, observe} from '@polymer/decorators';
 import {GroupName} from '../../../types/common';
 import {appContext} from '../../../services/app-context';
-
-@customElement('gr-create-group-dialog')
-export class GrCreateGroupDialog extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
-
-  @property({type: Boolean, notify: true})
-  hasNewGroupName = false;
-
-  @property({type: String})
-  _name: GroupName | '' = '';
-
-  @property({type: Boolean})
-  _groupCreated = false;
-
-  private readonly restApiService = appContext.restApiService;
-
-  _computeGroupUrl(groupId: string) {
-    return getBaseUrl() + '/admin/groups/' + encodeURL(groupId, true);
-  }
-
-  @observe('_name')
-  _updateGroupName(name: string) {
-    this.hasNewGroupName = !!name;
-  }
-
-  override focus() {
-    this.shadowRoot?.querySelector('input')?.focus();
-  }
-
-  handleCreateGroup() {
-    const name = this._name as GroupName;
-    return this.restApiService.createGroup({name}).then(groupRegistered => {
-      if (groupRegistered.status !== 201) {
-        return;
-      }
-      this._groupCreated = true;
-      return this.restApiService.getGroupConfig(name).then(group => {
-        // TODO(TS): should group always defined ?
-        page.show(this._computeGroupUrl(String(group!.group_id!)));
-      });
-    });
-  }
-}
+import {formStyles} from '../../../styles/gr-form-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, query, property} from 'lit/decorators';
+import {BindValueChangeEvent} from '../../../types/events';
+import {fireEvent} from '../../../utils/event-util';
 
 declare global {
   interface HTMLElementTagNameMap {
     'gr-create-group-dialog': GrCreateGroupDialog;
   }
 }
+
+@customElement('gr-create-group-dialog')
+export class GrCreateGroupDialog extends LitElement {
+  @query('input') private input!: HTMLInputElement;
+
+  @property({type: String})
+  name: GroupName | '' = '';
+
+  private readonly restApiService = appContext.restApiService;
+
+  static override get styles() {
+    return [
+      formStyles,
+      sharedStyles,
+      css`
+        :host {
+          display: inline-block;
+        }
+        input {
+          width: 20em;
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    return html`
+      <div class="gr-form-styles">
+        <div id="form">
+          <section>
+            <span class="title">Group name</span>
+            <iron-input
+              .bindValue=${this.name}
+              @bind-value-changed=${this.handleGroupNameBindValueChanged}
+            >
+              <input />
+            </iron-input>
+          </section>
+        </div>
+      </div>
+    `;
+  }
+
+  override updated(changedProperties: PropertyValues) {
+    if (changedProperties.has('name')) {
+      this.updateGroupName();
+    }
+  }
+
+  private updateGroupName() {
+    fireEvent(this, 'has-new-group-name');
+  }
+
+  private computeGroupUrl(groupId: string) {
+    return getBaseUrl() + '/admin/groups/' + encodeURL(groupId, true);
+  }
+
+  override focus() {
+    this.input.focus();
+  }
+
+  handleCreateGroup() {
+    const name = this.name as GroupName;
+    return this.restApiService.createGroup({name}).then(groupRegistered => {
+      if (groupRegistered.status !== 201) return;
+      return this.restApiService.getGroupConfig(name).then(group => {
+        if (!group) return;
+        page.show(this.computeGroupUrl(String(group.group_id!)));
+      });
+    });
+  }
+
+  private handleGroupNameBindValueChanged(e: BindValueChangeEvent) {
+    this.name = e.detail.value as GroupName;
+  }
+}
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.ts b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.ts
deleted file mode 100644
index daf8780..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style include="shared-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
-  <style include="gr-form-styles">
-    :host {
-      display: inline-block;
-    }
-    input {
-      width: 20em;
-    }
-  </style>
-  <div class="gr-form-styles">
-    <div id="form">
-      <section>
-        <span class="title">Group name</span>
-        <iron-input bind-value="{{_name}}">
-          <input is="iron-input" bind-value="{{_name}}" />
-        </iron-input>
-      </section>
-    </div>
-  </div>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js
deleted file mode 100644
index 321f069..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-create-group-dialog.js';
-import {page} from '../../../utils/page-wrapper-utils.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-create-group-dialog');
-
-suite('gr-create-group-dialog tests', () => {
-  let element;
-
-  const GROUP_NAME = 'test-group';
-
-  setup(() => {
-    element = basicFixture.instantiate();
-  });
-
-  test('name is updated correctly', async () => {
-    assert.isFalse(element.hasNewGroupName);
-
-    const inputEl = element.root.querySelector('iron-input');
-    inputEl.bindValue = GROUP_NAME;
-
-    await new Promise(resolve => setTimeout(resolve));
-    assert.isTrue(element.hasNewGroupName);
-    assert.deepEqual(element._name, GROUP_NAME);
-  });
-
-  test('test for redirecting to group on successful creation', async () => {
-    stubRestApi('createGroup').returns(Promise.resolve({status: 201}));
-    stubRestApi('getGroupConfig').returns(Promise.resolve({group_id: 551}));
-
-    const showStub = sinon.stub(page, 'show');
-    await element.handleCreateGroup();
-    assert.isTrue(showStub.calledWith('/admin/groups/551'));
-  });
-
-  test('test for unsuccessful group creation', async () => {
-    stubRestApi('createGroup').returns(Promise.resolve({status: 409}));
-    stubRestApi('getGroupConfig').returns(Promise.resolve({group_id: 551}));
-
-    const showStub = sinon.stub(page, 'show');
-    await element.handleCreateGroup();
-    assert.isFalse(showStub.called);
-  });
-});
-
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.ts
new file mode 100644
index 0000000..f84c76c
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.ts
@@ -0,0 +1,82 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-create-group-dialog';
+import {GrCreateGroupDialog} from './gr-create-group-dialog';
+import {page} from '../../../utils/page-wrapper-utils';
+import {
+  mockPromise,
+  queryAndAssert,
+  stubRestApi,
+} from '../../../test/test-utils';
+import {IronInputElement} from '@polymer/iron-input';
+import {GroupId} from '../../../types/common';
+
+const basicFixture = fixtureFromElement('gr-create-group-dialog');
+
+suite('gr-create-group-dialog tests', () => {
+  let element: GrCreateGroupDialog;
+
+  const GROUP_NAME = 'test-group';
+
+  setup(async () => {
+    element = basicFixture.instantiate();
+    await element.updateComplete;
+  });
+
+  test('name is updated correctly', async () => {
+    const promise = mockPromise();
+    element.addEventListener('has-new-group-name', () => {
+      promise.resolve();
+    });
+
+    const inputEl = queryAndAssert<IronInputElement>(element, 'iron-input');
+    inputEl.bindValue = GROUP_NAME;
+    inputEl.dispatchEvent(new Event('input', {bubbles: true, composed: true}));
+
+    await promise;
+
+    assert.deepEqual(element.name, GROUP_NAME);
+  });
+
+  test('test for redirecting to group on successful creation', async () => {
+    stubRestApi('createGroup').returns(
+      Promise.resolve({status: 201} as Response)
+    );
+    stubRestApi('getGroupConfig').returns(
+      Promise.resolve({id: 'testId551' as GroupId, group_id: 551})
+    );
+
+    const showStub = sinon.stub(page, 'show');
+    await element.handleCreateGroup();
+    assert.isTrue(showStub.calledWith('/admin/groups/551'));
+  });
+
+  test('test for unsuccessful group creation', async () => {
+    stubRestApi('createGroup').returns(
+      Promise.resolve({status: 409} as Response)
+    );
+    stubRestApi('getGroupConfig').returns(
+      Promise.resolve({id: 'testId551' as GroupId, group_id: 551})
+    );
+
+    const showStub = sinon.stub(page, 'show');
+    await element.handleCreateGroup();
+    assert.isFalse(showStub.called);
+  });
+});