Convert GrStorage from a Polymer element to a class

Change-Id: I4b1d7da81d07d34d3a216188c656e59d5ad4f826
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
index ec985af..23718fa 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
@@ -734,7 +734,6 @@
     on-reload-diff-preference="_handleReloadingDiffPreference"
   >
   </gr-diff-preferences-dialog>
-  <gr-storage id="storage"></gr-storage>
   <gr-diff-cursor id="diffCursor"></gr-diff-cursor>
   <gr-cursor-manager
     id="fileCursor"
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index a0906de..253b5ff 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -168,7 +168,6 @@
     labelScores: GrLabelScores;
     textarea: GrTextarea;
     reviewerConfirmationOverlay: GrOverlay;
-    storage: GrStorage;
   };
 }
 
@@ -376,6 +375,8 @@
 
   private readonly restApiService = appContext.restApiService;
 
+  private readonly storage = new GrStorage();
+
   get keyBindings() {
     return {
       esc: '_handleEscKey',
@@ -1317,7 +1318,7 @@
   }
 
   _loadStoredDraft() {
-    const draft = this.$.storage.getDraftComment(this._getStorageLocation());
+    const draft = this.storage.getDraftComment(this._getStorageLocation());
     return draft?.message ?? '';
   }
 
@@ -1336,12 +1337,9 @@
         if (!newDraft.length && oldDraft) {
           // If the draft has been modified to be empty, then erase the storage
           // entry.
-          this.$.storage.eraseDraftComment(this._getStorageLocation());
+          this.storage.eraseDraftComment(this._getStorageLocation());
         } else if (newDraft.length) {
-          this.$.storage.setDraftComment(
-            this._getStorageLocation(),
-            this.draft
-          );
+          this.storage.setDraftComment(this._getStorageLocation(), this.draft);
         }
       },
       STORAGE_DEBOUNCE_INTERVAL_MS
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index 2fca13d..5409243 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
@@ -613,5 +613,4 @@
     </div>
   </div>
   <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
-  <gr-storage id="storage"></gr-storage>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
index e38742a..4547e53 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
@@ -114,10 +114,9 @@
       ],
     };
 
-    getDraftCommentStub = sinon.stub(element.$.storage, 'getDraftComment');
-    setDraftCommentStub = sinon.stub(element.$.storage, 'setDraftComment');
-    eraseDraftCommentStub = sinon.stub(element.$.storage,
-        'eraseDraftComment');
+    getDraftCommentStub = sinon.stub(element.storage, 'getDraftComment');
+    setDraftCommentStub = sinon.stub(element.storage, 'setDraftComment');
+    eraseDraftCommentStub = sinon.stub(element.storage, 'eraseDraftComment');
 
     // sinon.stub(patchSetUtilMockProxy, 'fetchChangeUpdates')
     //     .returns(Promise.resolve({isLatest: true}));
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
index 4f57c50..a00927c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
@@ -425,7 +425,6 @@
     on-reload-diff-preference="_handleReloadingDiffPreference"
   >
   </gr-diff-preferences-dialog>
-  <gr-storage id="storage"></gr-storage>
   <gr-diff-cursor
     id="cursor"
     on-navigate-to-next-unreviewed-file="_handleNextUnreviewedFile"
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 4be8edf..047abe3 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -55,11 +55,6 @@
 
 const STORAGE_DEBOUNCE_INTERVAL_MS = 100;
 
-export interface GrEditorView {
-  $: {
-    storage: GrStorage;
-  };
-}
 @customElement('gr-editor-view')
 export class GrEditorView extends KeyboardShortcutMixin(
   GestureEventListeners(LegacyElementMixin(PolymerElement))
@@ -124,6 +119,8 @@
 
   private readonly restApiService = appContext.restApiService;
 
+  private readonly storage = new GrStorage();
+
   reporting = appContext.reportingService;
 
   get keyBindings() {
@@ -237,9 +234,7 @@
     if (patchNum === undefined) {
       return Promise.reject(new Error('patchNum undefined'));
     }
-    const storedContent = this.$.storage.getEditableContentItem(
-      this.storageKey
-    );
+    const storedContent = this.storage.getEditableContentItem(this.storageKey);
 
     return this.restApiService
       .getFileContent(changeNum, path, patchNum)
@@ -275,7 +270,7 @@
     }
     this._saving = true;
     this._showAlert(SAVING_MESSAGE);
-    this.$.storage.eraseEditableContentItem(this.storageKey);
+    this.storage.eraseEditableContentItem(this.storageKey);
     if (!this._newContent)
       return Promise.reject(new Error('new content undefined'));
     return this.restApiService
@@ -359,9 +354,9 @@
         const content = e.detail.value;
         if (content) {
           this.set('_newContent', e.detail.value);
-          this.$.storage.setEditableContentItem(this.storageKey, content);
+          this.storage.setEditableContentItem(this.storageKey, content);
         } else {
-          this.$.storage.eraseEditableContentItem(this.storageKey);
+          this.storage.eraseEditableContentItem(this.storageKey);
         }
       },
       STORAGE_DEBOUNCE_INTERVAL_MS
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.ts
index 0b03856..0df04cb 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.ts
@@ -133,5 +133,4 @@
       ></gr-default-editor>
     </gr-endpoint-decorator>
   </div>
-  <gr-storage id="storage"></gr-storage>
 `;
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
index 03e0b4c..10f7921 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
@@ -103,7 +103,7 @@
   });
 
   test('reacts to content-change event', () => {
-    const storeStub = sinon.spy(element.$.storage, 'setEditableContentItem');
+    const storeStub = sinon.spy(element.storage, 'setEditableContentItem');
     element._newContent = 'test';
     element.$.editorEndpoint.dispatchEvent(new CustomEvent('content-change', {
       bubbles: true, composed: true,
@@ -136,7 +136,7 @@
 
     test('file modification and save, !ok response', () => {
       const saveSpy = sinon.spy(element, '_saveEdit');
-      const eraseStub = sinon.stub(element.$.storage,
+      const eraseStub = sinon.stub(element.storage,
           'eraseEditableContentItem');
       const alertStub = sinon.stub(element, '_showAlert');
       saveFileStub.returns(Promise.resolve({ok: false}));
@@ -248,7 +248,7 @@
       element._newContent = 'initial';
       element._content = 'initial';
       element._type = 'initial';
-      sinon.stub(element.$.storage, 'getEditableContentItem').returns(null);
+      sinon.stub(element.storage, 'getEditableContentItem').returns(null);
     });
 
     test('res.ok', () => {
@@ -377,7 +377,7 @@
 
   suite('gr-storage caching', () => {
     test('local edit exists', () => {
-      sinon.stub(element.$.storage, 'getEditableContentItem')
+      sinon.stub(element.storage, 'getEditableContentItem')
           .returns({message: 'pending edit'});
       stubRestApi('getFileContent')
           .returns(Promise.resolve({
@@ -400,7 +400,7 @@
     });
 
     test('local edit exists, is same as remote edit', () => {
-      sinon.stub(element.$.storage, 'getEditableContentItem')
+      sinon.stub(element.storage, 'getEditableContentItem')
           .returns({message: 'pending edit'});
       stubRestApi('getFileContent')
           .returns(Promise.resolve({
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index dd48556..c8b3be2 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -56,7 +56,6 @@
 
 export interface GrCommentThread {
   $: {
-    storage: GrStorage;
     replyBtn: GrButton;
     quoteBtn: GrButton;
   };
@@ -187,6 +186,8 @@
 
   flagsService = appContext.flagsService;
 
+  readonly storage = new GrStorage();
+
   readonly restApiService = appContext.restApiService;
 
   /** @override */
@@ -575,7 +576,7 @@
           path: changeComment.path,
           line: changeComment.line,
         };
-        this.$.storage.setDraftComment(
+        this.storage.setDraftComment(
           commentLocation,
           changeComment.message ?? ''
         );
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
index dd9f9e8..ba541be 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
@@ -167,5 +167,4 @@
       </div>
     </div>
   </div>
-  <gr-storage id="storage"></gr-storage>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
index b70a55a..e1c6bff 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
@@ -652,7 +652,7 @@
           __draft: true,
         },
       ];
-      const storageStub = sinon.stub(element.$.storage, 'setDraftComment');
+      const storageStub = sinon.stub(element.storage, 'setDraftComment');
       flush();
 
       const draftEl = element.root?.querySelectorAll('gr-comment')[1];
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 644b0ae..5d5f4f3 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -95,7 +95,6 @@
 
 export interface GrComment {
   $: {
-    storage: GrStorage;
     container: HTMLDivElement;
     resolvedCheckbox: HTMLInputElement;
   };
@@ -271,6 +270,8 @@
 
   private readonly restApiService = appContext.restApiService;
 
+  private readonly storage = new GrStorage();
+
   reporting = appContext.reportingService;
 
   /** @override */
@@ -332,7 +333,7 @@
     if (!editing) return;
     // visibility based on cache this will make sure we only and always show
     // a tip once every Math.max(a day, period between creating comments)
-    const cachedVisibilityOfRespectfulTip = this.$.storage.getRespectfulTipVisibility();
+    const cachedVisibilityOfRespectfulTip = this.storage.getRespectfulTipVisibility();
     if (!cachedVisibilityOfRespectfulTip) {
       // we still want to show the tip with a probability of 30%
       if (this.getRandomNum(0, 3) >= 1) return;
@@ -343,7 +344,7 @@
         tip: this._respectfulReviewTip,
       });
       // update cache
-      this.$.storage.setRespectfulTipVisibility();
+      this.storage.setRespectfulTipVisibility();
     }
   }
 
@@ -362,7 +363,7 @@
       tip: this._respectfulReviewTip,
     });
     // add a 14-day delay to the tip cache
-    this.$.storage.setRespectfulTipVisibility(/* delayDays= */ 14);
+    this.storage.setRespectfulTipVisibility(/* delayDays= */ 14);
   }
 
   _onRespectfulReadMoreClick() {
@@ -491,7 +492,7 @@
     if (this.changeNum === undefined) {
       throw new Error('undefined changeNum');
     }
-    this.$.storage.eraseDraftComment({
+    this.storage.eraseDraftComment({
       changeNum: this.changeNum,
       patchNum: this._getPatchNum(),
       path: this.comment.path,
@@ -663,9 +664,9 @@
           if ((!message || !message.length) && oldValue) {
             // If the draft has been modified to be empty, then erase the storage
             // entry.
-            this.$.storage.eraseDraftComment(commentLocation);
+            this.storage.eraseDraftComment(commentLocation);
           } else {
-            this.$.storage.setDraftComment(commentLocation, message);
+            this.storage.setDraftComment(commentLocation, message);
           }
         },
         STORAGE_DEBOUNCE_INTERVAL
@@ -945,7 +946,7 @@
       return;
     }
 
-    const draft = this.$.storage.getDraftComment({
+    const draft = this.storage.getDraftComment({
       changeNum,
       patchNum: this._getPatchNum(),
       path: comment.path,
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
index d6992c8..23dd71a 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
@@ -494,5 +494,4 @@
       </gr-dialog>
     </gr-overlay>
   </template>
-  <gr-storage id="storage"></gr-storage>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
index ea46ea1..6c925b0 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
@@ -115,7 +115,7 @@
     });
 
     test('message is not retrieved from storage when other edits', done => {
-      const storageStub = sinon.stub(element.$.storage, 'getDraftComment');
+      const storageStub = sinon.stub(element.storage, 'getDraftComment');
       const loadSpy = sinon.spy(element, '_loadLocalDraft');
 
       element.changeNum = 1;
@@ -135,7 +135,7 @@
     });
 
     test('message is retrieved from storage when no other edits', done => {
-      const storageStub = sinon.stub(element.$.storage, 'getDraftComment');
+      const storageStub = sinon.stub(element.storage, 'getDraftComment');
       const loadSpy = sinon.spy(element, '_loadLocalDraft');
 
       element.changeNum = 1;
@@ -458,10 +458,8 @@
         },
       }));
       stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
-      stub('gr-storage', {
-        getDraftComment() { return null; },
-      });
       element = draftFixture.instantiate();
+      sinon.stub(element.storage, 'getDraftComment').returns(null);
       element.changeNum = 42;
       element.patchNum = 1;
       element.editing = false;
@@ -727,7 +725,7 @@
       assert.isTrue(element.editing);
 
       element._messageText = 'hello world';
-      const eraseMessageDraftSpy = sinon.spy(element.$.storage,
+      const eraseMessageDraftSpy = sinon.spy(element.storage,
           'eraseDraftComment');
       const mockEvent = {preventDefault: sinon.stub()};
       element._handleSave(mockEvent);
@@ -1026,8 +1024,8 @@
 
     test('cancelling an unsaved draft discards, persists in storage', () => {
       const discardSpy = sinon.spy(element, '_fireDiscard');
-      const storeStub = sinon.stub(element.$.storage, 'setDraftComment');
-      const eraseStub = sinon.stub(element.$.storage, 'eraseDraftComment');
+      const storeStub = sinon.stub(element.storage, 'setDraftComment');
+      const eraseStub = sinon.stub(element.storage, 'eraseDraftComment');
       element._messageText = 'test text';
       flush();
       element.flushDebouncer('store');
@@ -1042,7 +1040,7 @@
     test('cancelling edit on a saved draft does not store', () => {
       element.comment.id = 'foo';
       const discardSpy = sinon.spy(element, '_fireDiscard');
-      const storeStub = sinon.stub(element.$.storage, 'setDraftComment');
+      const storeStub = sinon.stub(element.storage, 'setDraftComment');
       element._messageText = 'test text';
       flush();
       element.flushDebouncer('store');
@@ -1240,15 +1238,12 @@
     });
 
     test('show tip when no cached record', done => {
-      // fake stub for storage
-      const respectfulGetStub = sinon.stub();
-      const respectfulSetStub = sinon.stub();
-      stub('gr-storage', {
-        getRespectfulTipVisibility() { return respectfulGetStub(); },
-        setRespectfulTipVisibility() { return respectfulSetStub(); },
-      });
-      respectfulGetStub.returns(null);
       element = draftFixture.instantiate();
+      const respectfulGetStub =
+          sinon.stub(element.storage, 'getRespectfulTipVisibility');
+      const respectfulSetStub =
+          sinon.stub(element.storage, 'setRespectfulTipVisibility');
+      respectfulGetStub.returns(null);
       // fake random
       element.getRandomNum = () => 0;
       element.comment = {__editing: true, __draft: true};
@@ -1263,15 +1258,12 @@
     });
 
     test('add 14-day delays once dismissed', done => {
-      // fake stub for storage
-      const respectfulGetStub = sinon.stub();
-      const respectfulSetStub = sinon.stub();
-      stub('gr-storage', {
-        getRespectfulTipVisibility() { return respectfulGetStub(); },
-        setRespectfulTipVisibility(days) { return respectfulSetStub(days); },
-      });
-      respectfulGetStub.returns(null);
       element = draftFixture.instantiate();
+      const respectfulGetStub =
+          sinon.stub(element.storage, 'getRespectfulTipVisibility');
+      const respectfulSetStub =
+          sinon.stub(element.storage, 'setRespectfulTipVisibility');
+      respectfulGetStub.returns(null);
       // fake random
       element.getRandomNum = () => 0;
       element.comment = {__editing: true, __draft: true};
@@ -1292,15 +1284,12 @@
     });
 
     test('do not show tip when fall out of probability', done => {
-      // fake stub for storage
-      const respectfulGetStub = sinon.stub();
-      const respectfulSetStub = sinon.stub();
-      stub('gr-storage', {
-        getRespectfulTipVisibility() { return respectfulGetStub(); },
-        setRespectfulTipVisibility() { return respectfulSetStub(); },
-      });
-      respectfulGetStub.returns(null);
       element = draftFixture.instantiate();
+      const respectfulGetStub =
+          sinon.stub(element.storage, 'getRespectfulTipVisibility');
+      const respectfulSetStub =
+          sinon.stub(element.storage, 'setRespectfulTipVisibility');
+      respectfulGetStub.returns(null);
       // fake random
       element.getRandomNum = () => 3;
       element.comment = {__editing: true, __draft: true};
@@ -1315,15 +1304,12 @@
     });
 
     test('show tip when editing changed to true', done => {
-      // fake stub for storage
-      const respectfulGetStub = sinon.stub();
-      const respectfulSetStub = sinon.stub();
-      stub('gr-storage', {
-        getRespectfulTipVisibility() { return respectfulGetStub(); },
-        setRespectfulTipVisibility() { return respectfulSetStub(); },
-      });
-      respectfulGetStub.returns(null);
       element = draftFixture.instantiate();
+      const respectfulGetStub =
+          sinon.stub(element.storage, 'getRespectfulTipVisibility');
+      const respectfulSetStub =
+          sinon.stub(element.storage, 'setRespectfulTipVisibility');
+      respectfulGetStub.returns(null);
       // fake random
       element.getRandomNum = () => 0;
       element.comment = {__editing: false};
@@ -1347,15 +1333,12 @@
     });
 
     test('no tip when cached record', done => {
-      // fake stub for storage
-      const respectfulGetStub = sinon.stub();
-      const respectfulSetStub = sinon.stub();
-      stub('gr-storage', {
-        getRespectfulTipVisibility() { return respectfulGetStub(); },
-        setRespectfulTipVisibility() { return respectfulSetStub(); },
-      });
-      respectfulGetStub.returns({});
       element = draftFixture.instantiate();
+      const respectfulGetStub =
+          sinon.stub(element.storage, 'getRespectfulTipVisibility');
+      const respectfulSetStub =
+          sinon.stub(element.storage, 'setRespectfulTipVisibility');
+      respectfulGetStub.returns({});
       // fake random
       element.getRandomNum = () => 0;
       element.comment = {__editing: true, __draft: true};
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
index 10e8916..2537c1a 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
@@ -35,12 +35,6 @@
   }
 }
 
-export interface GrEditableContent {
-  $: {
-    storage: GrStorage;
-  };
-}
-
 @customElement('gr-editable-content')
 export class GrEditableContent extends GestureEventListeners(
   LegacyElementMixin(PolymerElement)
@@ -92,6 +86,8 @@
   @property({type: String, observer: '_newContentChanged'})
   _newContent?: string;
 
+  private readonly storage = new GrStorage();
+
   _contentChanged() {
     /* A changed content means that either a different change has been loaded
      * or new content was saved. Either way, let's reset the component.
@@ -112,14 +108,14 @@
       'store',
       () => {
         if (newContent.length) {
-          this.$.storage.setEditableContentItem(storageKey, newContent);
+          this.storage.setEditableContentItem(storageKey, newContent);
         } else {
           // This does not really happen, because we don't clear newContent
           // after saving (see below). So this only occurs when the user clears
-          // all the content in the editable textarea. But <gr-storage> cleans
+          // all the content in the editable textarea. But GrStorage cleans
           // up itself after one day, so we are not so concerned about leaving
           // some garbage behind.
-          this.$.storage.eraseEditableContentItem(storageKey);
+          this.storage.eraseEditableContentItem(storageKey);
         }
       },
       STORAGE_DEBOUNCE_INTERVAL_MS
@@ -145,7 +141,7 @@
 
     let content;
     if (this.storageKey) {
-      const storedContent = this.$.storage.getEditableContentItem(
+      const storedContent = this.storage.getEditableContentItem(
         this.storageKey
       );
       if (storedContent?.message) {
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
index 85fbba6..fa18761 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
@@ -71,5 +71,4 @@
       >
     </div>
   </div>
-  <gr-storage id="storage"></gr-storage>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js
index 129fda8..a0481ae 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js
@@ -99,7 +99,7 @@
     });
 
     test('editing toggled to true, has stored data', () => {
-      sinon.stub(element.$.storage, 'getEditableContentItem')
+      sinon.stub(element.storage, 'getEditableContentItem')
           .returns({message: 'stored content'});
       element.editing = true;
 
@@ -109,7 +109,7 @@
     });
 
     test('editing toggled to true, has no stored data', () => {
-      sinon.stub(element.$.storage, 'getEditableContentItem')
+      sinon.stub(element.storage, 'getEditableContentItem')
           .returns({});
       element.editing = true;
 
@@ -119,9 +119,9 @@
 
     test('edits are cached', () => {
       const storeStub =
-          sinon.stub(element.$.storage, 'setEditableContentItem');
+          sinon.stub(element.storage, 'setEditableContentItem');
       const eraseStub =
-          sinon.stub(element.$.storage, 'eraseEditableContentItem');
+          sinon.stub(element.storage, 'eraseEditableContentItem');
       element.editing = true;
 
       element._newContent = 'new content';
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.ts b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.ts
index 1369a17..a86d8f2 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.ts
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.ts
@@ -14,10 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {customElement, property} from '@polymer/decorators';
 import {CommentRange, PatchSetNum} from '../../../types/common';
 
 export interface StorageLocation {
@@ -43,28 +39,12 @@
 CLEANUP_PREFIXES_MAX_AGE_MAP.set('draft', DURATION_DAY);
 CLEANUP_PREFIXES_MAX_AGE_MAP.set('editablecontent', DURATION_DAY);
 
-declare global {
-  interface HTMLElementTagNameMap {
-    'gr-storage': GrStorage;
-  }
-}
+export class GrStorage {
+  private lastCleanup = 0;
 
-export interface GrStorage {
-  $: {};
-}
+  private readonly storage = window.localStorage;
 
-@customElement('gr-storage')
-export class GrStorage extends GestureEventListeners(
-  LegacyElementMixin(PolymerElement)
-) {
-  @property({type: Number})
-  _lastCleanup = 0;
-
-  @property({type: Object})
-  _storage = window.localStorage;
-
-  @property({type: Boolean})
-  _exceededQuota = false;
+  private exceededQuota = false;
 
   getDraftComment(location: StorageLocation): StorageObject | null {
     this._cleanupItems();
@@ -78,7 +58,7 @@
 
   eraseDraftComment(location: StorageLocation) {
     const key = this._getDraftKey(location);
-    this._storage.removeItem(key);
+    this.storage.removeItem(key);
   }
 
   getEditableContentItem(key: string): StorageObject | null {
@@ -106,7 +86,7 @@
   }
 
   eraseEditableContentItem(key: string) {
-    this._storage.removeItem(this._getEditableContentKey(key));
+    this.storage.removeItem(this._getEditableContentKey(key));
   }
 
   _getDraftKey(location: StorageLocation): string {
@@ -134,20 +114,20 @@
   _cleanupItems() {
     // Throttle cleanup to the throttle interval.
     if (
-      this._lastCleanup &&
-      Date.now() - this._lastCleanup < CLEANUP_THROTTLE_INTERVAL
+      this.lastCleanup &&
+      Date.now() - this.lastCleanup < CLEANUP_THROTTLE_INTERVAL
     ) {
       return;
     }
-    this._lastCleanup = Date.now();
+    this.lastCleanup = Date.now();
 
-    Object.keys(this._storage).forEach(key => {
+    Object.keys(this.storage).forEach(key => {
       const entries = CLEANUP_PREFIXES_MAX_AGE_MAP.entries();
       for (const [prefix, expiration] of entries) {
         if (key.startsWith(prefix)) {
           const item = this._getObject(key);
           if (!item || Date.now() - item.updated > expiration) {
-            this._storage.removeItem(key);
+            this.storage.removeItem(key);
           }
         }
       }
@@ -155,7 +135,7 @@
   }
 
   _getObject(key: string): StorageObject | null {
-    const serial = this._storage.getItem(key);
+    const serial = this.storage.getItem(key);
     if (!serial) {
       return null;
     }
@@ -163,16 +143,16 @@
   }
 
   _setObject(key: string, obj: StorageObject) {
-    if (this._exceededQuota) {
+    if (this.exceededQuota) {
       return;
     }
     try {
-      this._storage.setItem(key, JSON.stringify(obj));
+      this.storage.setItem(key, JSON.stringify(obj));
     } catch (exc) {
       // Catch for QuotaExceededError and disable writes on local storage the
       // first time that it occurs.
       if (exc.code === 22) {
-        this._exceededQuota = true;
+        this.exceededQuota = true;
         console.warn('Local storage quota exceeded: disabling');
         return;
       } else {
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.js
index 99f953f..64d3750 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.js
@@ -16,12 +16,10 @@
  */
 
 import '../../../test/common-test-setup-karma.js';
-import './gr-storage.js';
-
-const basicFixture = fixtureFromElement('gr-storage');
+import {GrStorage} from './gr-storage.js';
 
 suite('gr-storage tests', () => {
-  let element;
+  let grStorage;
 
   function mockStorage(opt_quotaExceeded) {
     return {
@@ -36,9 +34,8 @@
   }
 
   setup(() => {
-    element = basicFixture.instantiate();
-
-    element._storage = mockStorage();
+    grStorage = new GrStorage();
+    grStorage.storage = mockStorage();
   });
 
   test('storing, retrieving and erasing drafts', () => {
@@ -54,23 +51,23 @@
     };
 
     // The key is in the expected format.
-    const key = element._getDraftKey(location);
+    const key = grStorage._getDraftKey(location);
     assert.equal(key, ['draft', changeNum, patchNum, path, line].join(':'));
 
     // There should be no draft initially.
-    const draft = element.getDraftComment(location);
+    const draft = grStorage.getDraftComment(location);
     assert.isNotOk(draft);
 
     // Setting the draft stores it under the expected key.
-    element.setDraftComment(location, 'my comment');
-    assert.isOk(element._storage.getItem(key));
-    assert.equal(JSON.parse(element._storage.getItem(key)).message,
+    grStorage.setDraftComment(location, 'my comment');
+    assert.isOk(grStorage.storage.getItem(key));
+    assert.equal(JSON.parse(grStorage.storage.getItem(key)).message,
         'my comment');
-    assert.isOk(JSON.parse(element._storage.getItem(key)).updated);
+    assert.isOk(JSON.parse(grStorage.storage.getItem(key)).updated);
 
     // Erasing the draft removes the key.
-    element.eraseDraftComment(location);
-    assert.isNotOk(element._storage.getItem(key));
+    grStorage.eraseDraftComment(location);
+    assert.isNotOk(grStorage.storage.getItem(key));
   });
 
   test('automatically removes old drafts', () => {
@@ -85,25 +82,25 @@
       line,
     };
 
-    const key = element._getDraftKey(location);
+    const key = grStorage._getDraftKey(location);
 
     // Make sure that the call to cleanup doesn't get throttled.
-    element._lastCleanup = 0;
+    grStorage.lastCleanup = 0;
 
-    const cleanupSpy = sinon.spy(element, '_cleanupItems');
+    const cleanupSpy = sinon.spy(grStorage, '_cleanupItems');
 
     // Create a message with a timestamp that is a second behind the max age.
-    element._storage.setItem(key, JSON.stringify({
+    grStorage.storage.setItem(key, JSON.stringify({
       message: 'old message',
       updated: Date.now() - 24 * 60 * 60 * 1000 - 1000,
     }));
 
     // Getting the draft should cause it to be removed.
-    const draft = element.getDraftComment(location);
+    const draft = grStorage.getDraftComment(location);
 
     assert.isTrue(cleanupSpy.called);
     assert.isNotOk(draft);
-    assert.isNotOk(element._storage.getItem(key));
+    assert.isNotOk(grStorage.storage.getItem(key));
   });
 
   test('_getDraftKey', () => {
@@ -118,7 +115,7 @@
       line,
     };
     let expectedResult = 'draft:1234:5:my_source_file.js:123';
-    assert.equal(element._getDraftKey(location), expectedResult);
+    assert.equal(grStorage._getDraftKey(location), expectedResult);
     location.range = {
       start_character: 1,
       start_line: 1,
@@ -126,12 +123,12 @@
       end_line: 2,
     };
     expectedResult = 'draft:1234:5:my_source_file.js:123:1-1-1-2';
-    assert.equal(element._getDraftKey(location), expectedResult);
+    assert.equal(grStorage._getDraftKey(location), expectedResult);
   });
 
   test('exceeded quota disables storage', () => {
-    element._storage = mockStorage(true);
-    assert.isFalse(element._exceededQuota);
+    grStorage.storage = mockStorage(true);
+    assert.isFalse(grStorage.exceededQuota);
 
     const changeNum = 1234;
     const patchNum = 5;
@@ -143,37 +140,37 @@
       path,
       line,
     };
-    const key = element._getDraftKey(location);
-    element.setDraftComment(location, 'my comment');
-    assert.isTrue(element._exceededQuota);
-    assert.isNotOk(element._storage.getItem(key));
+    const key = grStorage._getDraftKey(location);
+    grStorage.setDraftComment(location, 'my comment');
+    assert.isTrue(grStorage.exceededQuota);
+    assert.isNotOk(grStorage.storage.getItem(key));
   });
 
   test('editable content items', () => {
-    const cleanupStub = sinon.stub(element, '_cleanupItems');
+    const cleanupStub = sinon.stub(grStorage, '_cleanupItems');
     const key = 'testKey';
-    const computedKey = element._getEditableContentKey(key);
+    const computedKey = grStorage._getEditableContentKey(key);
     // Key correctly computed.
     assert.equal(computedKey, 'editablecontent:testKey');
 
-    element.setEditableContentItem(key, 'my content');
+    grStorage.setEditableContentItem(key, 'my content');
 
     // Setting the draft stores it under the expected key.
-    let item = element._storage.getItem(computedKey);
+    let item = grStorage.storage.getItem(computedKey);
     assert.isOk(item);
     assert.equal(JSON.parse(item).message, 'my content');
     assert.isOk(JSON.parse(item).updated);
 
     // getEditableContentItem performs as expected.
-    item = element.getEditableContentItem(key);
+    item = grStorage.getEditableContentItem(key);
     assert.isOk(item);
     assert.equal(item.message, 'my content');
     assert.isOk(item.updated);
     assert.isTrue(cleanupStub.called);
 
     // eraseEditableContentItem performs as expected.
-    element.eraseEditableContentItem(key);
-    assert.isNotOk(element._storage.getItem(computedKey));
+    grStorage.eraseEditableContentItem(key);
+    assert.isNotOk(grStorage.storage.getItem(computedKey));
   });
 });