diff --git a/.eslint_todo/vue-no-unused-properties.mjs b/.eslint_todo/vue-no-unused-properties.mjs
index 48b6d32b0b22ee8e838acc898ddc25319874bde6..7419aafde70b4532b7ed74b66e100fedc749f3ed 100644
--- a/.eslint_todo/vue-no-unused-properties.mjs
+++ b/.eslint_todo/vue-no-unused-properties.mjs
@@ -609,7 +609,6 @@ export default {
     'ee/app/assets/javascripts/work_items/components/work_item_links/work_item_rolled_up_health_status.vue',
     'ee/app/assets/javascripts/work_items/components/work_item_progress.vue',
     'ee/app/assets/javascripts/work_items/components/work_item_rolledup_dates.vue',
-    'ee/app/assets/javascripts/work_items/components/work_item_weight.vue',
     'ee/app/assets/javascripts/workspaces/common/components/workspaces_list/workspaces_table.vue',
     'ee/app/assets/javascripts/workspaces/dropdown_group/components/workspace_dropdown_item.vue',
     'ee/app/assets/javascripts/workspaces/user/pages/list.vue',
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_sidebar_widget.vue b/app/assets/javascripts/work_items/components/shared/work_item_sidebar_widget.vue
index fd438a5073e6d4cafbfbe7e7bc357215495f6dc3..a4767e4e6bb730863b253c01524108ee17e546db 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_sidebar_widget.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_sidebar_widget.vue
@@ -1,11 +1,12 @@
 <script>
-import { GlButton, GlOutsideDirective as Outside } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlOutsideDirective as Outside } from '@gitlab/ui';
 import { Mousetrap } from '~/lib/mousetrap';
 import { keysFor, SIDEBAR_CLOSE_WIDGET } from '~/behaviors/shortcuts/keybindings';
 
 export default {
   components: {
     GlButton,
+    GlLoadingIcon,
   },
   directives: {
     Outside,
@@ -56,6 +57,7 @@ export default {
       <h3 class="gl-heading-5 gl-mb-0">
         <slot name="title"></slot>
       </h3>
+      <gl-loading-icon v-if="isUpdating" />
       <gl-button
         v-if="canUpdate && !isEditing"
         key="edit-button"
diff --git a/ee/app/assets/javascripts/work_items/components/work_item_weight.vue b/ee/app/assets/javascripts/work_items/components/work_item_weight.vue
index d8624a10c982950c46925f42d4af55caa67fbc6b..036d99515190fb8636aed0dab996f611fbf086df 100644
--- a/ee/app/assets/javascripts/work_items/components/work_item_weight.vue
+++ b/ee/app/assets/javascripts/work_items/components/work_item_weight.vue
@@ -1,26 +1,25 @@
 <script>
-import { GlButton, GlForm, GlFormInput, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlFormInput, GlTooltipDirective } from '@gitlab/ui';
 import * as Sentry from '~/sentry/sentry_browser_wrapper';
 import Tracking from '~/tracking';
 import {
-  sprintfWorkItem,
   I18N_WORK_ITEM_ERROR_UPDATING,
+  sprintfWorkItem,
   TRACKING_CATEGORY_SHOW,
 } from '~/work_items/constants';
 import updateNewWorkItemMutation from '~/work_items/graphql/update_new_work_item.mutation.graphql';
 import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
 import { newWorkItemId } from '~/work_items/utils';
+import WorkItemSidebarWidget from '~/work_items/components/shared/work_item_sidebar_widget.vue';
 
 export default {
-  inputId: 'weight-widget-input',
   directives: {
     GlTooltip: GlTooltipDirective,
   },
   components: {
+    WorkItemSidebarWidget,
     GlButton,
-    GlForm,
     GlFormInput,
-    GlLoadingIcon,
   },
   mixins: [Tracking.mixin()],
   inject: ['hasIssueWeightsFeature'],
@@ -42,10 +41,6 @@ export default {
       type: String,
       required: true,
     },
-    workItemIid: {
-      type: String,
-      required: true,
-    },
     workItemType: {
       type: String,
       required: true,
@@ -53,9 +48,7 @@ export default {
   },
   data() {
     return {
-      isEditing: false,
-      clickingClearButton: false,
-      workItem: {},
+      dirtyWeight: this.widget.weight,
       isUpdating: false,
     };
   },
@@ -69,6 +62,7 @@ export default {
     showRemoveWeight() {
       return this.hasWeight && !this.isUpdating;
     },
+    // eslint-disable-next-line vue/no-unused-properties
     tracking() {
       return {
         category: TRACKING_CATEGORY_SHOW,
@@ -88,27 +82,19 @@ export default {
     },
   },
   methods: {
-    blurInput() {
-      this.$refs.input.$el.blur();
-    },
-    handleFocus() {
-      this.isEditing = true;
+    clearWeight(stopEditing) {
+      this.dirtyWeight = '';
+      stopEditing();
+      this.updateWeight();
     },
-    updateWeightFromInput(event) {
-      if (event.target.value === '') {
-        this.updateWeight(null);
+    updateWeight() {
+      if (!this.canUpdate) {
         return;
       }
 
-      const weight = Number(event.target.value);
-      this.updateWeight(weight);
-    },
-    updateWeight(weight) {
-      if (this.clickingClearButton) return;
-      if (!this.canUpdate) return;
+      const newWeight = this.dirtyWeight === '' ? null : Number(this.dirtyWeight);
 
-      if (this.weight === weight) {
-        this.isEditing = false;
+      if (this.weight === newWeight) {
         return;
       }
 
@@ -123,13 +109,12 @@ export default {
             input: {
               workItemType: this.workItemType,
               fullPath: this.fullPath,
-              weight,
+              weight: newWeight,
             },
           },
         });
 
         this.isUpdating = false;
-        this.isEditing = false;
         return;
       }
 
@@ -140,7 +125,7 @@ export default {
             input: {
               id: this.workItemId,
               weightWidget: {
-                weight,
+                weight: newWeight,
               },
             },
           },
@@ -157,7 +142,6 @@ export default {
         })
         .finally(() => {
           this.isUpdating = false;
-          this.isEditing = false;
         });
     },
   },
@@ -165,74 +149,49 @@ export default {
 </script>
 
 <template>
-  <div v-if="displayWeightWidget" data-testid="work-item-weight">
-    <div class="gl-flex gl-items-center gl-justify-between">
-      <!-- hide header when editing, since we then have a form label. Keep it reachable for screenreader nav  -->
-      <h3 :class="{ 'gl-sr-only': isEditing }" class="gl-heading-5 !gl-mb-0">
-        {{ __('Weight') }}
-      </h3>
-      <gl-button
-        v-if="canUpdate && !isEditing"
-        data-testid="edit-weight"
-        category="tertiary"
-        size="small"
-        @click="isEditing = true"
-        >{{ __('Edit') }}</gl-button
-      >
-    </div>
-    <gl-form v-if="isEditing" @submit.prevent="blurInput">
-      <div class="gl-flex gl-items-center">
-        <label :for="$options.inputId" class="gl-mb-0">{{ __('Weight') }}</label>
-        <gl-loading-icon v-if="isUpdating" size="sm" inline class="gl-ml-3" />
-        <gl-button
-          data-testid="apply-weight"
-          category="tertiary"
-          size="small"
-          class="gl-ml-auto"
-          :disabled="isUpdating"
-          @click="isEditing = false"
-          >{{ __('Apply') }}</gl-button
-        >
-      </div>
-      <!-- wrapper for the form input so the borders fit inside the sidebar -->
+  <work-item-sidebar-widget
+    v-if="displayWeightWidget"
+    :can-update="canUpdate"
+    :is-updating="isUpdating"
+    data-testid="work-item-weight"
+    @stopEditing="updateWeight"
+  >
+    <template #title>
+      {{ __('Weight') }}
+    </template>
+    <template #content>
+      <template v-if="hasWeight">
+        {{ weight }}
+      </template>
+      <span v-else class="gl-text-subtle">
+        {{ __('None') }}
+      </span>
+    </template>
+    <template #editing-content="{ stopEditing }">
       <div class="gl-relative gl-px-2">
         <gl-form-input
-          :id="$options.inputId"
-          ref="input"
+          v-model="dirtyWeight"
+          autofocus
           min="0"
-          class="hide-unfocused-input-decoration gl-block"
-          type="number"
-          :disabled="isUpdating"
           :placeholder="__('Enter a number')"
-          :value="weight"
-          autofocus
-          @blur="updateWeightFromInput"
-          @focus="handleFocus"
-          @keydown.exact.esc.stop="blurInput"
+          type="number"
+          :aria-label="__('Enter a number')"
+          @keydown.enter="stopEditing"
+          @keydown.exact.esc.stop="stopEditing"
         />
         <gl-button
           v-if="showRemoveWeight"
           v-gl-tooltip
-          data-testid="remove-weight"
-          variant="default"
+          class="gl-absolute gl-right-7 gl-top-2"
           category="tertiary"
-          size="small"
-          name="clear"
           icon="clear"
-          class="gl-clear-icon-button gl-absolute gl-right-7 gl-top-2"
+          size="small"
           :title="__('Remove weight')"
           :aria-label="__('Remove weight')"
-          @mousedown="clickingClearButton = true"
-          @mouseup="clickingClearButton = false"
-          @click="updateWeight(null)"
+          data-testid="remove-weight"
+          @click="clearWeight(stopEditing)"
         />
       </div>
-    </gl-form>
-    <template v-else-if="hasWeight">
-      <div>{{ weight }}</div>
-    </template>
-    <template v-else>
-      <div class="gl-text-subtle">{{ __('None') }}</div>
     </template>
-  </div>
+  </work-item-sidebar-widget>
 </template>
diff --git a/ee/spec/frontend/work_items/components/work_item_weight_spec.js b/ee/spec/frontend/work_items/components/work_item_weight_spec.js
index 2dbd70ded2a23ec08367c7f0cdf13847ffd470fd..479ed5034dc8b013a33413435758cd9c9e6b0cc0 100644
--- a/ee/spec/frontend/work_items/components/work_item_weight_spec.js
+++ b/ee/spec/frontend/work_items/components/work_item_weight_spec.js
@@ -1,14 +1,16 @@
-import { GlForm, GlFormInput, GlLoadingIcon } from '@gitlab/ui';
+import { GlFormInput } from '@gitlab/ui';
 import Vue, { nextTick } from 'vue';
 import VueApollo from 'vue-apollo';
 import WorkItemWeight from 'ee/work_items/components/work_item_weight.vue';
 import createMockApollo from 'helpers/mock_apollo_helper';
 import { mockTracking } from 'helpers/tracking_helper';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 import waitForPromises from 'helpers/wait_for_promises';
 import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
 import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
 import { updateWorkItemMutationResponse } from 'jest/work_items/mock_data';
+import WorkItemSidebarWidget from '~/work_items/components/shared/work_item_sidebar_widget.vue';
+import { ENTER_KEY, ESC_KEY } from '~/lib/utils/keys';
 
 describe('WorkItemWeight component', () => {
   Vue.use(VueApollo);
@@ -16,145 +18,60 @@ describe('WorkItemWeight component', () => {
   let wrapper;
 
   const workItemId = 'gid://gitlab/WorkItem/1';
-  const defaultWorkItemType = 'Task';
 
   const findHeader = () => wrapper.find('h3');
-  const findEditButton = () => wrapper.find('[data-testid="edit-weight"]');
-  const findApplyButton = () => wrapper.find('[data-testid="apply-weight"]');
-  const findLabel = () => wrapper.find('label');
-  const findForm = () => wrapper.findComponent(GlForm);
+  const findEditButton = () => wrapper.findByTestId('edit-button');
+  const findApplyButton = () => wrapper.findByTestId('apply-button');
   const findInput = () => wrapper.findComponent(GlFormInput);
-  const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
-  const findClearButton = () => wrapper.find('[data-testid="remove-weight"]');
+  const findClearButton = () => wrapper.findByTestId('remove-weight');
 
   const createComponent = ({
     canUpdate = true,
-    fullPath = 'gitlab-org/gitlab',
     hasIssueWeightsFeature = true,
     isEditing = false,
     weight = null,
     editable = true,
-    workItemIid = '1',
-    workItemType = defaultWorkItemType,
     mutationHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse),
   } = {}) => {
-    wrapper = mountExtended(WorkItemWeight, {
+    wrapper = shallowMountExtended(WorkItemWeight, {
       apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
       propsData: {
         canUpdate,
-        fullPath,
+        fullPath: 'gitlab-org/gitlab',
         widget: {
           weight,
           widgetDefinition: { editable },
         },
         workItemId,
-        workItemIid,
-        workItemType,
+        workItemType: 'Task',
       },
       provide: {
         hasIssueWeightsFeature,
       },
+      stubs: {
+        WorkItemSidebarWidget,
+      },
     });
 
     if (isEditing) {
-      findEditButton().trigger('click');
+      findEditButton().vm.$emit('click');
     }
   };
 
   describe('rendering widget', () => {
     it('renders nothing if license not available', async () => {
       createComponent({ hasIssueWeightsFeature: false });
-
       await nextTick();
 
       expect(findHeader().exists()).toBe(false);
-      expect(findForm().exists()).toBe(false);
     });
 
     // 'editable' property means if it's available for that work item type
     it('renders nothing if not editable', async () => {
       createComponent({ editable: false });
-
       await nextTick();
 
       expect(findHeader().exists()).toBe(false);
-      expect(findForm().exists()).toBe(false);
-    });
-  });
-
-  describe('label', () => {
-    it('shows header when not editing', () => {
-      createComponent();
-
-      expect(findHeader().exists()).toBe(true);
-      expect(findHeader().classes('gl-sr-only')).toBe(false);
-      expect(findLabel().exists()).toBe(false);
-    });
-
-    it('shows label and hides header while editing', async () => {
-      createComponent({ isEditing: true });
-
-      await nextTick();
-
-      expect(findLabel().exists()).toBe(true);
-      expect(findHeader().classes('gl-sr-only')).toBe(true);
-    });
-
-    it('shows loading spinner while updating', async () => {
-      createComponent({
-        isEditing: true,
-        weight: 0,
-        canUpdate: true,
-      });
-
-      await nextTick();
-
-      findInput().setValue('1');
-      findInput().trigger('blur');
-
-      await nextTick();
-
-      expect(findLoadingIcon().exists()).toBe(true);
-
-      await waitForPromises();
-
-      expect(findLoadingIcon().exists()).toBe(false);
-    });
-  });
-
-  describe('edit button', () => {
-    it('is not shown if user cannot edit', () => {
-      createComponent({ canUpdate: false });
-
-      expect(findEditButton().exists()).toBe(false);
-    });
-
-    it('is shown if user can edit', () => {
-      createComponent({ canUpdate: true });
-
-      expect(findEditButton().exists()).toBe(true);
-    });
-
-    it('triggers edit mode on click', async () => {
-      createComponent();
-
-      findEditButton().trigger('click');
-
-      await nextTick();
-
-      expect(findLabel().exists()).toBe(true);
-      expect(findForm().exists()).toBe(true);
-    });
-
-    it('is replaced by Apply button while editing', async () => {
-      createComponent();
-
-      findEditButton().trigger('click');
-
-      await nextTick();
-
-      expect(findEditButton().exists()).toBe(false);
-      expect(findApplyButton().exists()).toBe(true);
     });
   });
 
@@ -173,36 +90,22 @@ describe('WorkItemWeight component', () => {
     });
   });
 
-  describe('form', () => {
-    it('is not shown while not editing', async () => {
-      await createComponent();
-
-      expect(findForm().exists()).toBe(false);
-    });
-
-    it('is shown while editing', async () => {
-      await createComponent({ isEditing: true });
-
-      expect(findForm().exists()).toBe(true);
-    });
-  });
-
   describe('weight input', () => {
     it('is not shown while not editing', async () => {
-      await createComponent();
+      createComponent();
+      await nextTick();
 
       expect(findInput().exists()).toBe(false);
     });
 
-    it('has weight-y attributes', async () => {
-      await createComponent({ isEditing: true });
+    it('renders when editing', async () => {
+      createComponent({ isEditing: true });
+      await nextTick();
 
-      expect(findInput().attributes()).toEqual(
-        expect.objectContaining({
-          min: '0',
-          type: 'number',
-        }),
-      );
+      expect(findInput().attributes()).toMatchObject({
+        min: '0',
+        type: 'number',
+      });
     });
 
     it('clear button triggers mutation', async () => {
@@ -213,10 +116,9 @@ describe('WorkItemWeight component', () => {
         mutationHandler: mutationSpy,
         canUpdate: true,
       });
-
       await nextTick();
 
-      findClearButton().trigger('click');
+      findClearButton().vm.$emit('click');
 
       expect(mutationSpy).toHaveBeenCalledWith({
         input: {
@@ -236,11 +138,10 @@ describe('WorkItemWeight component', () => {
         mutationHandler: mutationSpy,
         canUpdate: true,
       });
-
       await nextTick();
 
-      findInput().setValue('1');
-      findInput().trigger('blur');
+      findInput().vm.$emit('input', '1');
+      findInput().vm.$emit('keydown', new KeyboardEvent('keydown', { key: ENTER_KEY }));
 
       expect(mutationSpy).toHaveBeenCalledWith({
         input: {
@@ -252,36 +153,19 @@ describe('WorkItemWeight component', () => {
       });
     });
 
-    it('is disabled while updating, and removed after', async () => {
+    it('does not call a mutation to update the weight when the input value is the same', async () => {
+      const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
       createComponent({
         isEditing: true,
         weight: 0,
+        mutationHandler: mutationSpy,
         canUpdate: true,
       });
-
-      await nextTick();
-
-      findInput().setValue('1');
-      findInput().trigger('blur');
-
-      await nextTick();
-
-      expect(findInput().attributes('disabled')).toBe('disabled');
-
-      await waitForPromises();
-
-      expect(findInput().exists()).toBe(false);
-    });
-
-    it('does not call a mutation to update the weight when the input value is the same', async () => {
-      const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
-      createComponent({ isEditing: true, mutationHandler: mutationSpy, canUpdate: true });
-
       await nextTick();
 
-      findInput().trigger('blur');
+      findInput().vm.$emit('keydown', new KeyboardEvent('keydown', { key: ESC_KEY }));
 
-      expect(mutationSpy).not.toHaveBeenCalledWith();
+      expect(mutationSpy).not.toHaveBeenCalled();
     });
 
     it('emits an error when there is a GraphQL error', async () => {
@@ -298,12 +182,10 @@ describe('WorkItemWeight component', () => {
         mutationHandler: jest.fn().mockResolvedValue(response),
         canUpdate: true,
       });
-
       await nextTick();
 
-      findInput().setValue('1');
-      findInput().trigger('blur');
-
+      findInput().vm.$emit('input', '1');
+      findApplyButton().vm.$emit('click');
       await waitForPromises();
 
       expect(wrapper.emitted('error')).toEqual([
@@ -317,12 +199,10 @@ describe('WorkItemWeight component', () => {
         mutationHandler: jest.fn().mockRejectedValue(new Error()),
         canUpdate: true,
       });
-
       await nextTick();
 
-      findInput().setValue('1');
-      findInput().trigger('blur');
-
+      findInput().vm.$emit('input', '1');
+      findApplyButton().vm.$emit('click');
       await waitForPromises();
 
       expect(wrapper.emitted('error')).toEqual([
@@ -333,11 +213,10 @@ describe('WorkItemWeight component', () => {
     it('tracks updating the weight', async () => {
       const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
       createComponent({ isEditing: true, canUpdate: true });
-
       await nextTick();
 
-      findInput().setValue('1');
-      findInput().trigger('blur');
+      findInput().vm.$emit('input', '1');
+      findApplyButton().vm.$emit('click');
 
       expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_weight', {
         category: TRACKING_CATEGORY_SHOW,
diff --git a/spec/frontend/work_items/components/shared/work_item_sidebar_widget_spec.js b/spec/frontend/work_items/components/shared/work_item_sidebar_widget_spec.js
index 68794022a1a3bfa43ba42003cd379461bfe69266..78ec7608aa5d0c983a0e07776d4a92a0a6bc0d79 100644
--- a/spec/frontend/work_items/components/shared/work_item_sidebar_widget_spec.js
+++ b/spec/frontend/work_items/components/shared/work_item_sidebar_widget_spec.js
@@ -1,3 +1,4 @@
+import { GlLoadingIcon } from '@gitlab/ui';
 import { nextTick } from 'vue';
 import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 import { Mousetrap } from '~/lib/mousetrap';
@@ -111,11 +112,17 @@ describe('WorkItemSidebarWidget component', () => {
     });
 
     describe('when updating', () => {
-      it('renders Edit button as disabled', () => {
+      beforeEach(() => {
         createComponent({ canUpdate: true, isUpdating: true });
+      });
 
+      it('renders Edit button as disabled', () => {
         expect(findEditButton().props('disabled')).toBe(true);
       });
+
+      it('shows loading icon', () => {
+        expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+      });
     });
   });