Repository: actualbudget/actual
Pull request #4912: Allow-return-to-trigger-default-actions
Author: login: SamBobBarnes
Labels: name: :sparkles: merged
Created: 2025-05-02T05:45:04Z | Merged: 2025-05-16T19:28:31Z

This change should fix The InitialFocus component so that buttons and inputs correctly focus by default.

This will resolve any issues where one would expect the return key to execute the default action and will continue to focus text inputs correctly as well as enabling a new feature to optionally have the focused text input have the text selected by default.


Diff:
diff --git a/packages/component-library/package.json b/packages/component-library/package.json
index 01bafe442ae..a3ba9e3ad7e 100644
--- a/packages/component-library/package.json
+++ b/packages/component-library/package.json
@@ -15,7 +15,8 @@
     "@svgr/cli": "^8.1.0",
     "@types/react": "^19.1.0",
     "react": "19.1.0",
-    "react-dom": "19.1.0"
+    "react-dom": "19.1.0",
+    "vitest": "^3.0.2"
   },
   "exports": {
     "./hooks/*": "./src/hooks/*.ts",
@@ -50,6 +51,8 @@
     "./view": "./src/View.tsx"
   },
   "scripts": {
-    "generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . ."
+    "generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .",
+    "test": "npm-run-all -cp 'test:*'",
+    "test:web": "ENV=web vitest -c vitest.web.config.ts"
   }
 }
diff --git a/packages/component-library/src/InitialFocus.ts b/packages/component-library/src/InitialFocus.ts
index c7ee965b08e..81b01e8a23f 100644
--- a/packages/component-library/src/InitialFocus.ts
+++ b/packages/component-library/src/InitialFocus.ts
@@ -1,19 +1,32 @@
 import {
-  type ReactElement,
-  type Ref,
+  Children,
   cloneElement,
+  isValidElement,
+  type ReactElement,
+  Ref,
   useEffect,
   useRef,
 } from 'react';
 
+type FocusableElement = HTMLElement;
+
 type InitialFocusProps = {
+  /**
+   * The child element to focus when the component mounts. This can be either a single React element or a function that returns a React element.
+   */
   children:
-    | ReactElement<{ inputRef: Ref<HTMLInputElement> }>
-    | ((node: Ref<HTMLInputElement>) => ReactElement);
+    | ReactElement<{ ref: Ref<HTMLElement> }>
+    | ((node: Ref<HTMLElement>) => ReactElement);
 };
 
+/**
+ * InitialFocus sets focus on its child element
+ * when it mounts.
+ * @param {Object} props - The component props.
+ * @param {ReactElement | function} props.children - A single React element or a function that returns a React element.
+ */
 export function InitialFocus({ children }: InitialFocusProps) {
-  const node = useRef<HTMLInputElement>(null);
+  const node = useRef<FocusableElement>(null);
 
   useEffect(() => {
     if (node.current) {
@@ -23,7 +36,12 @@ export function InitialFocus({ children }: InitialFocusProps) {
       setTimeout(() => {
         if (node.current) {
           node.current.focus();
-          node.current.setSelectionRange(0, 10000);
+          if (
+            node.current instanceof HTMLInputElement ||
+            node.current instanceof HTMLTextAreaElement
+          ) {
+            node.current.setSelectionRange(0, 10000);
+          }
         }
       }, 0);
     }
@@ -32,5 +50,12 @@ export function InitialFocus({ children }: InitialFocusProps) {
   if (typeof children === 'function') {
     return children(node);
   }
-  return cloneElement(children, { inputRef: node });
+
+  const child = Children.only(children);
+  if (isValidElement(child)) {
+    return cloneElement(child, { ref: node });
+  }
+  throw new Error(
+    'InitialFocus expects a single valid React element as its child.',
+  );
 }
diff --git a/packages/component-library/src/InitialFocus.web.test.tsx b/packages/component-library/src/InitialFocus.web.test.tsx
new file mode 100644
index 00000000000..3ce1df74f81
--- /dev/null
+++ b/packages/component-library/src/InitialFocus.web.test.tsx
@@ -0,0 +1,117 @@
+import * as React from 'react';
+import { forwardRef, Ref } from 'react';
+
+import { render } from '@testing-library/react';
+
+import { InitialFocus } from './InitialFocus';
+import { View } from './View';
+
+describe('InitialFocus', () => {
+  it('should focus a text input', async () => {
+    const component = render(
+      <View>
+        <InitialFocus>
+          <input type="text" title="focused" />
+        </InitialFocus>
+        <input type="text" title="unfocused" />
+      </View>,
+    );
+
+    // This is needed bc of the `setTimeout` in the `InitialFocus` component.
+    await new Promise(resolve => setTimeout(resolve, 0));
+
+    const input = component.getByTitle('focused') as HTMLInputElement;
+    const unfocusedInput = component.getByTitle(
+      'unfocused',
+    ) as HTMLInputElement;
+    expect(document.activeElement).toBe(input);
+    expect(document.activeElement).not.toBe(unfocusedInput);
+  });
+
+  it('should focus a textarea', async () => {
+    const component = render(
+      <View>
+        <InitialFocus>
+          <textarea title="focused" />
+        </InitialFocus>
+        <textarea title="unfocused" />
+      </View>,
+    );
+
+    // This is needed bc of the `setTimeout` in the `InitialFocus` component.
+    await new Promise(resolve => setTimeout(resolve, 0));
+
+    const textarea = component.getByTitle('focused') as HTMLTextAreaElement;
+    const unfocusedTextarea = component.getByTitle(
+      'unfocused',
+    ) as HTMLTextAreaElement;
+    expect(document.activeElement).toBe(textarea);
+    expect(document.activeElement).not.toBe(unfocusedTextarea);
+  });
+
+  it('should select text in an input', async () => {
+    const component = render(
+      <View>
+        <InitialFocus>
+          <input type="text" title="focused" defaultValue="Hello World" />
+        </InitialFocus>
+        <input type="text" title="unfocused" />
+      </View>,
+    );
+
+    // This is needed bc of the `setTimeout` in the `InitialFocus` component.
+    await new Promise(resolve => setTimeout(resolve, 0));
+
+    const input = component.getByTitle('focused') as HTMLInputElement;
+    expect(document.activeElement).toBe(input);
+    expect(input.selectionStart).toBe(0);
+    expect(input.selectionEnd).toBe(11); // Length of "Hello World"
+  });
+
+  it('should focus a button', async () => {
+    const component = render(
+      <View>
+        <InitialFocus>
+          <button title="focused">Click me</button>
+        </InitialFocus>
+        <button title="unfocused">Do not click me</button>
+      </View>,
+    );
+
+    // This is needed bc of the `setTimeout` in the `InitialFocus` component.
+    await new Promise(resolve => setTimeout(resolve, 0));
+
+    const button = component.getByTitle('focused') as HTMLButtonElement;
+    const unfocusedButton = component.getByTitle(
+      'unfocused',
+    ) as HTMLButtonElement;
+    expect(document.activeElement).toBe(button);
+    expect(document.activeElement).not.toBe(unfocusedButton);
+  });
+
+  it('should focus a custom component with ref forwarding', async () => {
+    const CustomInput = forwardRef<HTMLInputElement>((props, ref) => (
+      <input type="text" ref={ref} {...props} title="focused" />
+    ));
+    CustomInput.displayName = 'CustomInput';
+
+    const component = render(
+      <View>
+        <InitialFocus>
+          {node => <CustomInput ref={node as Ref<HTMLInputElement>} />}
+        </InitialFocus>
+        <input type="text" title="unfocused" />
+      </View>,
+    );
+
+    // This is needed bc of the `setTimeout` in the `InitialFocus` component.
+    await new Promise(resolve => setTimeout(resolve, 0));
+
+    const input = component.getByTitle('focused') as HTMLInputElement;
+    const unfocusedInput = component.getByTitle(
+      'unfocused',
+    ) as HTMLInputElement;
+    expect(document.activeElement).toBe(input);
+    expect(document.activeElement).not.toBe(unfocusedInput);
+  });
+});
diff --git a/packages/component-library/vitest.web.config.ts b/packages/component-library/vitest.web.config.ts
new file mode 100644
index 00000000000..1b2a9ee7c72
--- /dev/null
+++ b/packages/component-library/vitest.web.config.ts
@@ -0,0 +1,35 @@
+import path from 'path';
+
+import peggyLoader from 'vite-plugin-peggy-loader';
+import { defineConfig } from 'vitest/config';
+
+const resolveExtensions = [
+  '.testing.ts',
+  '.web.ts',
+  '.mjs',
+  '.js',
+  '.mts',
+  '.ts',
+  '.jsx',
+  '.tsx',
+  '.json',
+  '.wasm',
+];
+
+export default defineConfig({
+  test: {
+    environment: 'jsdom',
+    globals: true,
+    include: ['src/**/*.web.test.(js|jsx|ts|tsx)'],
+  },
+  resolve: {
+    alias: [
+      {
+        find: /^@actual-app\/crdt(\/.*)?$/,
+        replacement: path.resolve('../../../crdt/src$1'),
+      },
+    ],
+    extensions: resolveExtensions,
+  },
+  plugins: [peggyLoader()],
+});
diff --git a/packages/desktop-client/src/components/accounts/Header.tsx b/packages/desktop-client/src/components/accounts/Header.tsx
index d7bdbdf5461..ac70c442b1e 100644
--- a/packages/desktop-client/src/components/accounts/Header.tsx
+++ b/packages/desktop-client/src/components/accounts/Header.tsx
@@ -1,9 +1,9 @@
 import React, {
-  useState,
-  useRef,
+  type ComponentProps,
   Fragment,
   type ReactNode,
-  type ComponentProps,
+  useRef,
+  useState,
 } from 'react';
 import { Dialog, DialogTrigger } from 'react-aria-components';
 import { useHotkeys } from 'react-hotkeys-hook';
@@ -50,7 +50,7 @@ import { SelectedTransactionsButton } from '../transactions/SelectedTransactions
 
 import { type TableRef } from './Account';
 import { Balances } from './Balance';
-import { ReconcilingMessage, ReconcileMenu } from './Reconcile';
+import { ReconcileMenu, ReconcilingMessage } from './Reconcile';
 
 import { useLocale } from '@desktop-client/hooks/useLocale';
 import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
diff --git a/packages/desktop-client/src/components/budget/envelope/CoverMenu.tsx b/packages/desktop-client/src/components/budget/envelope/CoverMenu.tsx
index cecf9de3bbe..ecdd82637ec 100644
--- a/packages/desktop-client/src/components/budget/envelope/CoverMenu.tsx
+++ b/packages/desktop-client/src/components/budget/envelope/CoverMenu.tsx
@@ -1,4 +1,4 @@
-import React, { useMemo, useState } from 'react';
+import React, { type Ref, useMemo, useState } from 'react';
 import { Trans, useTranslation } from 'react-i18next';
 
 import { Button } from '@actual-app/components/button';
@@ -47,6 +47,7 @@ export function CoverMenu({
     }
     onClose();
   }
+
   return (
     <View style={{ padding: 10 }}>
       <View style={{ marginBottom: 5 }}>
@@ -61,7 +62,7 @@ export function CoverMenu({
             openOnFocus={true}
             onSelect={(id: string | undefined) => setFromCategoryId(id || null)}
             inputProps={{
-              inputRef: node,
+              inputRef: node as Ref<HTMLInputElement>,
               onEnter: event => !event.defaultPrevented && submit(),
               placeholder: t('(none)'),
             }}
diff --git a/packages/desktop-client/src/components/sidebar/Account.tsx b/packages/desktop-client/src/components/sidebar/Account.tsx
index 5277123c268..eef0c40c7d6 100644
--- a/packages/desktop-client/src/components/sidebar/Account.tsx
+++ b/packages/desktop-client/src/components/sidebar/Account.tsx
@@ -25,13 +25,13 @@ import { useDispatch } from '../../redux';
 import { Link } from '../common/Link';
 import { Notes } from '../Notes';
 import {
-  useDraggable,
-  useDroppable,
   DropHighlight,
   type OnDragChangeCallback,
   type OnDropCallback,
+  useDraggable,
+  useDroppable,
 } from '../sort';
-import { type SheetFields, type Binding } from '../spreadsheet';
+import { type Binding, type SheetFields } from '../spreadsheet';
 import { CellValue } from '../spreadsheet/CellValue';
 
 import { useContextMenu } from '@desktop-client/hooks/useContextMenu';
diff --git a/upcoming-release-notes/4912.md b/upcoming-release-notes/4912.md
new file mode 100644
index 00000000000..e363cab9a84
--- /dev/null
+++ b/upcoming-release-notes/4912.md
@@ -0,0 +1,6 @@
+---
+category: Bugfix
+authors: [ sambobbarnes ]
+---
+
+Fix autofocus of `Button` elements when enclosed in the `InitialFocus` component
diff --git a/yarn.lock b/yarn.lock
index f027b88c88b..2caf6dd41ff 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -46,6 +46,7 @@ __metadata:
     react-aria-components: "npm:^1.7.1"
     react-dom: "npm:19.1.0"
     usehooks-ts: "npm:^3.1.1"
+    vitest: "npm:^3.0.2"
   peerDependencies:
     react: ">=18.2"
     react-dom: ">=18.2"


Reviews:
user: login: joel-jeremy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T15:55:57Z
user: login: joel-jeremy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T19:18:14Z
user: login: SamBobBarnes
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T13:42:01Z
user: login: joel-jeremy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-11T15:46:13Z
user: login: joel-jeremy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T15:36:59Z
user: login: joel-jeremy
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-16T19:26:48Z
user: login: joel-jeremy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T19:26:54Z

Review comments:
diff_hunk: @@ -1,19 +1,36 @@
 import {
-  type ReactElement,
-  type Ref,
+  Children,
   cloneElement,
+  isValidElement,
+  type ReactElement,
+  Ref,
   useEffect,
   useRef,
 } from 'react';
 
+type FocusableElement = HTMLElement;
+
 type InitialFocusProps = {
-  children:
-    | ReactElement<{ inputRef: Ref<HTMLInputElement> }>
-    | ((node: Ref<HTMLInputElement>) => ReactElement);
+  /**
+   * The child element to focus when the component mounts. This can be either a single React element or a function that returns a React element.
+   */
+  children: ReactElement | ((node: Ref<HTMLInputElement>) => ReactElement);
path: packages/component-library/src/InitialFocus.ts
user: login: joel-jeremy
body: To fix ref error below and update to allow any element not only inputs:
```suggestion
  children: ReactElement<{ ref: Ref<HTMLElement> }> | ((node: Ref<HTMLElement>) => ReactElement);
```
created_at: 2025-05-02T15:53:26Z
author_association: MEMBER
diff_hunk: @@ -1,19 +1,36 @@
 import {
-  type ReactElement,
-  type Ref,
+  Children,
   cloneElement,
+  isValidElement,
+  type ReactElement,
+  Ref,
   useEffect,
   useRef,
 } from 'react';
 
+type FocusableElement = HTMLElement;
+
 type InitialFocusProps = {
-  children:
-    | ReactElement<{ inputRef: Ref<HTMLInputElement> }>
-    | ((node: Ref<HTMLInputElement>) => ReactElement);
+  /**
+   * The child element to focus when the component mounts. This can be either a single React element or a function that returns a React element.
+   */
+  children: ReactElement | ((node: Ref<HTMLInputElement>) => ReactElement);
+  /**
+   * Whether to select the text in the input or textarea element when focused.
+   */
+  selectText?: boolean;
path: packages/component-library/src/InitialFocus.ts
user: login: joel-jeremy
body: IMO this is not needed. It should always auto select for inputs.
created_at: 2025-05-02T15:54:14Z
author_association: MEMBER
diff_hunk: @@ -23,14 +40,28 @@ export function InitialFocus({ children }: InitialFocusProps) {
       setTimeout(() => {
         if (node.current) {
           node.current.focus();
-          node.current.setSelectionRange(0, 10000);
+          if (
+            selectText &&
+            (node.current instanceof HTMLInputElement ||
+              node.current instanceof HTMLTextAreaElement)
+          ) {
+            node.current.setSelectionRange(0, 10000);
+          }
         }
       }, 0);
     }
-  }, []);
+  }, [selectText]);
 
   if (typeof children === 'function') {
-    return children(node);
+    return children(node as Ref<HTMLInputElement>);
path: packages/component-library/src/InitialFocus.ts
user: login: joel-jeremy
body: No need to cast if type update above is applied
```suggestion
    return children(node);
```
created_at: 2025-05-02T15:55:36Z
author_association: MEMBER
diff_hunk: @@ -1,19 +1,36 @@
 import {
-  type ReactElement,
-  type Ref,
+  Children,
   cloneElement,
+  isValidElement,
+  type ReactElement,
+  Ref,
   useEffect,
   useRef,
 } from 'react';
 
+type FocusableElement = HTMLElement;
+
 type InitialFocusProps = {
-  children:
-    | ReactElement<{ inputRef: Ref<HTMLInputElement> }>
-    | ((node: Ref<HTMLInputElement>) => ReactElement);
+  /**
+   * The child element to focus when the component mounts. This can be either a single React element or a function that returns a React element.
+   */
+  children: ReactElement | ((node: Ref<HTMLInputElement>) => ReactElement);
path: packages/component-library/src/InitialFocus.ts
user: login: joel-jeremy
body: This is still not resolved
created_at: 2025-05-06T19:18:14Z
author_association: MEMBER
diff_hunk: @@ -1,19 +1,36 @@
 import {
-  type ReactElement,
-  type Ref,
+  Children,
   cloneElement,
+  isValidElement,
+  type ReactElement,
+  Ref,
   useEffect,
   useRef,
 } from 'react';
 
+type FocusableElement = HTMLElement;
+
 type InitialFocusProps = {
-  children:
-    | ReactElement<{ inputRef: Ref<HTMLInputElement> }>
-    | ((node: Ref<HTMLInputElement>) => ReactElement);
+  /**
+   * The child element to focus when the component mounts. This can be either a single React element or a function that returns a React element.
+   */
+  children: ReactElement | ((node: Ref<HTMLInputElement>) => ReactElement);
path: packages/component-library/src/InitialFocus.ts
user: login: SamBobBarnes
body: dont know how I missed that.
created_at: 2025-05-08T13:42:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,5 +50,13 @@ export function InitialFocus({ children }: InitialFocusProps) {
   if (typeof children === 'function') {
     return children(node);
   }
-  return cloneElement(children, { inputRef: node });
+
+  const child = Children.only(children);
+  if (isValidElement(child)) {
+    // @ts-ignore `ref` is not a valid prop for the type that `cloneElement` expects, but this feature doesn't work without it.
path: packages/component-library/src/InitialFocus.ts
user: login: joel-jeremy
body: This comment can now be removed
```suggestion
    // @ts-ignore `ref` is not a valid prop for the type that `cloneElement` expects, but this feature doesn't work without it.
```
created_at: 2025-05-11T15:46:13Z
author_association: MEMBER
diff_hunk: @@ -23,7 +36,12 @@ export function InitialFocus({ children }: InitialFocusProps) {
       setTimeout(() => {
path: packages/component-library/src/InitialFocus.ts
user: login: joel-jeremy
body: I don't think we use ScopeTab in the code anymore so maybe this setTimeout can now be removed to simplify the tests
created_at: 2025-05-14T15:36:58Z
author_association: MEMBER
diff_hunk: @@ -23,7 +36,12 @@ export function InitialFocus({ children }: InitialFocusProps) {
       setTimeout(() => {
path: packages/component-library/src/InitialFocus.ts
user: login: joel-jeremy
body: This can be done on a separate PR
created_at: 2025-05-16T19:26:54Z
author_association: MEMBER

PR comments:
user: login: netlify[bot]
created_at: 2025-05-02T05:45:22Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *actualbudget* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | ff9bf020419fd336bc3ed1aca7d8fb7e440fd5f7 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/actualbudget/deploys/6823582b5d287e000878e671 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-4912.demo.actualbudget.org](https://deploy-preview-4912.demo.actualbudget.org) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTQ5MTIuZGVtby5hY3R1YWxidWRnZXQub3JnIn0.Q8A5O_vzWMzp71T9GsNO8kO981OHB0We2k0Hxs90tpU)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [actualbudget Preview](https://deploy-preview-4912.demo.actualbudget.org) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/actualbudget/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-02T05:47:34Z
body: 
### Bundle Stats — desktop-client

Hey there, this message comes from a [GitHub action](https://github.com/twk3/rollup-size-compare-action) that helps you and reviewers to understand how these changes affect the size of this project's bundle.

As this PR is updated, I'll keep you updated on how the bundle size is impacted.

**Total**

Files count | Total bundle size | % Changed
----------- | ----------------- | ---------
18 | 9.67 MB → 9.67 MB (+305 B) | +0.00%

<details>
<summary>Changeset</summary>

File | Δ | Size
---- | - | ----
`home/runner/work/actual/actual/packages/component-library/src/InitialFocus.ts` | 📈 +305 B (+67.03%) | 455 B → 760 B

</details>


<details>
<summary>View detailed bundle breakdown</summary>

<div>

**Added**

No assets were added

**Removed**

No assets were removed

**Bigger**

Asset | File Size | % Changed
----- | --------- | ---------
static/js/index.js | 6.56 MB → 6.56 MB (+305 B) | +0.00%

**Smaller**

No assets were smaller

**Unchanged**

Asset | File Size | % Changed
----- | --------- | ---------
static/js/de.js | 120.53 kB | 0%
static/js/en-GB.js | 5.93 kB | 0%
static/js/en.js | 115.92 kB | 0%
static/js/es.js | 68.91 kB | 0%
static/js/fr.js | 124.09 kB | 0%
static/js/nl.js | 100.82 kB | 0%
static/js/pt-BR.js | 117.2 kB | 0%
static/js/uk.js | 121.43 kB | 0%
static/js/indexeddb-main-thread-worker-e59fee74.js | 12.94 kB | 0%
static/js/workbox-window.prod.es5.js | 5.64 kB | 0%
static/js/resize-observer.js | 18.37 kB | 0%
static/js/BackgroundImage.js | 122.29 kB | 0%
static/js/ReportRouter.js | 1.69 MB | 0%
static/js/narrow.js | 391.09 kB | 0%
static/js/wide.js | 113.55 kB | 0%
static/js/AppliedFilters.js | 10.87 kB | 0%
static/js/useAccountPreviewTransactions.js | 1.69 kB | 0%

</div>
</details>

<!--- bundlestats-action-comment key:desktop-client --->

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-02T05:47:35Z
body: 
### Bundle Stats — loot-core

Hey there, this message comes from a [GitHub action](https://github.com/github/webpack-bundlesize-compare-action) that helps you and reviewers to understand how these changes affect the size of this project's bundle.

As this PR is updated, I'll keep you updated on how the bundle size is impacted.

**Total**

Files count | Total bundle size | % Changed
----------- | ----------------- | ---------
1 | 2.29 MB | 0%

Changeset

No files were changed

<details>
<summary>View detailed bundle breakdown</summary>

<div>

**Added**

No assets were added

**Removed**

No assets were removed

**Bigger**

No assets were bigger

**Smaller**

No assets were smaller

**Unchanged**

Asset | File Size | % Changed
----- | --------- | ---------
kcab.worker.js | 2.29 MB | 0%

</div>
</details>

<!--- bundlestats-action-comment key:loot-core --->

author_association: CONTRIBUTOR