mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-30 18:20:24 -05:00
Compare commits
3 Commits
master
...
jfdoming/0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95a931a014 | ||
|
|
8f9c27b447 | ||
|
|
ff7fb9544b |
@@ -6,6 +6,7 @@ export class MobileTransactionEntryPage {
|
|||||||
readonly page: Page;
|
readonly page: Page;
|
||||||
readonly header: Locator;
|
readonly header: Locator;
|
||||||
readonly amountField: Locator;
|
readonly amountField: Locator;
|
||||||
|
readonly globalAmountField: Locator;
|
||||||
readonly transactionForm: Locator;
|
readonly transactionForm: Locator;
|
||||||
readonly footer: Locator;
|
readonly footer: Locator;
|
||||||
readonly addTransactionButton: Locator;
|
readonly addTransactionButton: Locator;
|
||||||
@@ -15,6 +16,7 @@ export class MobileTransactionEntryPage {
|
|||||||
this.header = page.getByRole('heading');
|
this.header = page.getByRole('heading');
|
||||||
this.transactionForm = page.getByTestId('transaction-form');
|
this.transactionForm = page.getByTestId('transaction-form');
|
||||||
this.amountField = this.transactionForm.getByTestId('amount-input');
|
this.amountField = this.transactionForm.getByTestId('amount-input');
|
||||||
|
this.globalAmountField = page.getByTestId('navigable-focus-input');
|
||||||
this.footer = page.getByTestId('transaction-form-footer');
|
this.footer = page.getByTestId('transaction-form-footer');
|
||||||
this.addTransactionButton = this.footer.getByRole('button', {
|
this.addTransactionButton = this.footer.getByRole('button', {
|
||||||
name: 'Add transaction',
|
name: 'Add transaction',
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ test.describe('Mobile Transactions', () => {
|
|||||||
|
|
||||||
await expect(transactionEntryPage.header).toHaveText('New Transaction');
|
await expect(transactionEntryPage.header).toHaveText('New Transaction');
|
||||||
|
|
||||||
await transactionEntryPage.amountField.fill('12.34');
|
await expect(transactionEntryPage.globalAmountField).toBeFocused();
|
||||||
|
await transactionEntryPage.globalAmountField.fill('12.34');
|
||||||
// Click anywhere to cancel active edit.
|
// Click anywhere to cancel active edit.
|
||||||
await transactionEntryPage.header.click();
|
await transactionEntryPage.header.click();
|
||||||
await transactionEntryPage.fillField(
|
await transactionEntryPage.fillField(
|
||||||
@@ -62,7 +63,8 @@ test.describe('Mobile Transactions', () => {
|
|||||||
await expect(transactionEntryPage.header).toHaveText('New Transaction');
|
await expect(transactionEntryPage.header).toHaveText('New Transaction');
|
||||||
await expect(page).toMatchThemeScreenshots();
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
|
||||||
await transactionEntryPage.amountField.fill('12.34');
|
await expect(transactionEntryPage.globalAmountField).toBeFocused();
|
||||||
|
await transactionEntryPage.globalAmountField.fill('12.34');
|
||||||
// Click anywhere to cancel active edit.
|
// Click anywhere to cancel active edit.
|
||||||
await transactionEntryPage.header.click();
|
await transactionEntryPage.header.click();
|
||||||
await transactionEntryPage.fillField(
|
await transactionEntryPage.fillField(
|
||||||
@@ -84,7 +86,9 @@ test.describe('Mobile Transactions', () => {
|
|||||||
test('creates an uncategorized transaction from `/categories/uncategorized` page', async () => {
|
test('creates an uncategorized transaction from `/categories/uncategorized` page', async () => {
|
||||||
// Create uncategorized transaction
|
// Create uncategorized transaction
|
||||||
let transactionEntryPage = await navigation.goToTransactionEntryPage();
|
let transactionEntryPage = await navigation.goToTransactionEntryPage();
|
||||||
await transactionEntryPage.amountField.fill('12.35');
|
|
||||||
|
await expect(transactionEntryPage.globalAmountField).toBeFocused();
|
||||||
|
await transactionEntryPage.globalAmountField.fill('12.35');
|
||||||
// Click anywhere to cancel active edit.
|
// Click anywhere to cancel active edit.
|
||||||
await transactionEntryPage.header.click();
|
await transactionEntryPage.header.click();
|
||||||
await transactionEntryPage.fillField(
|
await transactionEntryPage.fillField(
|
||||||
@@ -117,7 +121,9 @@ test.describe('Mobile Transactions', () => {
|
|||||||
test('creates a categorized transaction from `/categories/uncategorized` page', async () => {
|
test('creates a categorized transaction from `/categories/uncategorized` page', async () => {
|
||||||
// Create uncategorized transaction
|
// Create uncategorized transaction
|
||||||
let transactionEntryPage = await navigation.goToTransactionEntryPage();
|
let transactionEntryPage = await navigation.goToTransactionEntryPage();
|
||||||
await transactionEntryPage.amountField.fill('12.35');
|
|
||||||
|
await expect(transactionEntryPage.globalAmountField).toBeFocused();
|
||||||
|
await transactionEntryPage.globalAmountField.fill('12.35');
|
||||||
// Click anywhere to cancel active edit.
|
// Click anywhere to cancel active edit.
|
||||||
await transactionEntryPage.header.click();
|
await transactionEntryPage.header.click();
|
||||||
await transactionEntryPage.fillField(
|
await transactionEntryPage.fillField(
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type ReactNode,
|
||||||
|
type InputHTMLAttributes,
|
||||||
|
type RefObject,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
type InputMode = InputHTMLAttributes<HTMLInputElement>['inputMode'];
|
||||||
|
|
||||||
|
type FocusOptions = {
|
||||||
|
inputmode: InputMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FocusInput = (opts: FocusOptions) => void;
|
||||||
|
|
||||||
|
const NavigableFocusFunctionContext = createContext<FocusInput | null>(null);
|
||||||
|
|
||||||
|
const NavigableFocusValueContext =
|
||||||
|
createContext<RefObject<HTMLInputElement | null> | null>(null);
|
||||||
|
|
||||||
|
type ProviderProps = { children: ReactNode };
|
||||||
|
|
||||||
|
export function NavigableFocusProvider({ children }: ProviderProps) {
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const inputModeRef = useRef<InputMode>('text');
|
||||||
|
const [focusedValue, setFocusedValue] = useState('');
|
||||||
|
|
||||||
|
const focusInput = useCallback((opts: FocusOptions) => {
|
||||||
|
const el = inputRef.current;
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (opts?.inputmode) {
|
||||||
|
inputModeRef.current = opts.inputmode;
|
||||||
|
el.setAttribute('inputmode', opts.inputmode);
|
||||||
|
}
|
||||||
|
el.value = ''; // Reset previous state
|
||||||
|
el.setSelectionRange(0, 0); // Move cursor to start
|
||||||
|
el.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavigableFocusFunctionContext.Provider value={focusInput}>
|
||||||
|
<NavigableFocusValueContext.Provider value={inputRef}>
|
||||||
|
<input
|
||||||
|
data-testid="navigable-focus-input"
|
||||||
|
type="text"
|
||||||
|
ref={inputRef}
|
||||||
|
value={focusedValue}
|
||||||
|
inputMode={inputModeRef.current}
|
||||||
|
onChange={e => setFocusedValue(e.target.value)}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
opacity: 0,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
// Don't zoom in on iOS
|
||||||
|
fontSize: '16px',
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</NavigableFocusValueContext.Provider>
|
||||||
|
</NavigableFocusFunctionContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNavigableFocusFunction() {
|
||||||
|
const focusInput = useContext(NavigableFocusFunctionContext);
|
||||||
|
if (focusInput == null) {
|
||||||
|
throw new Error(
|
||||||
|
'useNavigableFocusFunction must be used within NavigableFocusFunctionProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return focusInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNavigableFocusRef() {
|
||||||
|
const ref = useContext(NavigableFocusValueContext);
|
||||||
|
if (ref == null) {
|
||||||
|
throw new Error(
|
||||||
|
'useNavigableFocusRef must be used within NavigableFocusRefProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import { theme } from '@actual-app/components/theme';
|
|||||||
import { View } from '@actual-app/components/view';
|
import { View } from '@actual-app/components/view';
|
||||||
import { useDrag } from '@use-gesture/react';
|
import { useDrag } from '@use-gesture/react';
|
||||||
|
|
||||||
|
import { useNavigableFocusFunction } from '@desktop-client/components/NavigableFocusProvider';
|
||||||
import { useScrollListener } from '@desktop-client/components/ScrollProvider';
|
import { useScrollListener } from '@desktop-client/components/ScrollProvider';
|
||||||
|
|
||||||
const COLUMN_COUNT = 3;
|
const COLUMN_COUNT = 3;
|
||||||
@@ -67,6 +68,8 @@ export function MobileNavTabs() {
|
|||||||
[api, OPEN_FULL_Y],
|
[api, OPEN_FULL_Y],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const focusInput = useNavigableFocusFunction();
|
||||||
|
|
||||||
const openDefault = useCallback(
|
const openDefault = useCallback(
|
||||||
(velocity = 0) => {
|
(velocity = 0) => {
|
||||||
setNavbarState('default');
|
setNavbarState('default');
|
||||||
@@ -103,6 +106,7 @@ export function MobileNavTabs() {
|
|||||||
path: '/transactions/new',
|
path: '/transactions/new',
|
||||||
style: navTabStyle,
|
style: navTabStyle,
|
||||||
Icon: SvgAdd,
|
Icon: SvgAdd,
|
||||||
|
focus: { inputmode: 'decimal' as const },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('Accounts'),
|
name: t('Accounts'),
|
||||||
@@ -141,7 +145,16 @@ export function MobileNavTabs() {
|
|||||||
Icon: SvgCog,
|
Icon: SvgCog,
|
||||||
},
|
},
|
||||||
].map(tab => (
|
].map(tab => (
|
||||||
<NavTab key={tab.path} onClick={() => openDefault()} {...tab} />
|
<NavTab
|
||||||
|
key={tab.path}
|
||||||
|
onClick={() => {
|
||||||
|
if (tab.focus) {
|
||||||
|
focusInput(tab.focus);
|
||||||
|
}
|
||||||
|
openDefault();
|
||||||
|
}}
|
||||||
|
{...tab}
|
||||||
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
const bufferTabsCount = COLUMN_COUNT - (navTabs.length % COLUMN_COUNT);
|
const bufferTabsCount = COLUMN_COUNT - (navTabs.length % COLUMN_COUNT);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Button } from '@actual-app/components/button';
|
import { Button } from '@actual-app/components/button';
|
||||||
import { SvgAdd } from '@actual-app/components/icons/v1';
|
import { SvgAdd } from '@actual-app/components/icons/v1';
|
||||||
|
|
||||||
|
import { useNavigableFocusFunction } from '@desktop-client/components/NavigableFocusProvider';
|
||||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
||||||
|
|
||||||
type AddTransactionButtonProps = {
|
type AddTransactionButtonProps = {
|
||||||
@@ -18,13 +19,16 @@ export function AddTransactionButton({
|
|||||||
categoryId,
|
categoryId,
|
||||||
}: AddTransactionButtonProps) {
|
}: AddTransactionButtonProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const focusInput = useNavigableFocusFunction();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="bare"
|
variant="bare"
|
||||||
aria-label={t('Add transaction')}
|
aria-label={t('Add transaction')}
|
||||||
style={{ margin: 10 }}
|
style={{ margin: 10 }}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
focusInput({ inputmode: 'decimal' });
|
||||||
navigate(to, { state: { accountId, categoryId } });
|
navigate(to, { state: { accountId, categoryId } });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
type CSSProperties,
|
type CSSProperties,
|
||||||
|
type ChangeEvent,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { Button } from '@actual-app/components/button';
|
import { Button } from '@actual-app/components/button';
|
||||||
@@ -14,6 +15,7 @@ import { Text } from '@actual-app/components/text';
|
|||||||
import { theme } from '@actual-app/components/theme';
|
import { theme } from '@actual-app/components/theme';
|
||||||
import { View } from '@actual-app/components/view';
|
import { View } from '@actual-app/components/view';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
import { useEventCallback } from 'usehooks-ts';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
amountToCurrency,
|
amountToCurrency,
|
||||||
@@ -23,6 +25,7 @@ import {
|
|||||||
} from 'loot-core/shared/util';
|
} from 'loot-core/shared/util';
|
||||||
|
|
||||||
import { makeAmountFullStyle } from '@desktop-client/components/budget/util';
|
import { makeAmountFullStyle } from '@desktop-client/components/budget/util';
|
||||||
|
import { useNavigableFocusRef } from '@desktop-client/components/NavigableFocusProvider';
|
||||||
import { useMergedRefs } from '@desktop-client/hooks/useMergedRefs';
|
import { useMergedRefs } from '@desktop-client/hooks/useMergedRefs';
|
||||||
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
||||||
|
|
||||||
@@ -59,11 +62,17 @@ const AmountInput = memo(function AmountInput({
|
|||||||
|
|
||||||
const initialValue = Math.abs(props.value);
|
const initialValue = Math.abs(props.value);
|
||||||
|
|
||||||
|
const globalFocusedRef = useNavigableFocusRef();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (focused) {
|
if (focused) {
|
||||||
inputRef.current?.focus();
|
// Focus requested
|
||||||
|
if (globalFocusedRef.current !== document.activeElement) {
|
||||||
|
// Global focusable element not focused, attempt to focus input
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [focused]);
|
}, [focused, globalFocusedRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
@@ -71,21 +80,24 @@ const AmountInput = memo(function AmountInput({
|
|||||||
setValue(initialValue);
|
setValue(initialValue);
|
||||||
}, [initialValue]);
|
}, [initialValue]);
|
||||||
|
|
||||||
const onKeyUp: HTMLProps<HTMLInputElement>['onKeyUp'] = e => {
|
const onKeyUp: HTMLProps<HTMLInputElement>['onKeyUp'] = useEventCallback(
|
||||||
if (e.key === 'Backspace' && text === '') {
|
e => {
|
||||||
setEditing(true);
|
if (e.key === 'Backspace' && text === '') {
|
||||||
} else if (e.key === 'Enter') {
|
setEditing(true);
|
||||||
props.onEnter?.(e);
|
} else if (e.key === 'Enter') {
|
||||||
if (!e.defaultPrevented) {
|
props.onEnter?.(e);
|
||||||
onUpdate(e.currentTarget.value);
|
if (!e.defaultPrevented) {
|
||||||
|
onUpdate(e.currentTarget.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
);
|
||||||
|
|
||||||
const applyText = () => {
|
const applyText = () => {
|
||||||
const parsed = currencyToAmount(text) || 0;
|
const parsed = currencyToAmount(text) || 0;
|
||||||
const newValue = editing ? parsed : value;
|
const newValue = editing ? parsed : value;
|
||||||
|
|
||||||
|
globalFocusedRef.current?.blur();
|
||||||
setValue(Math.abs(newValue));
|
setValue(Math.abs(newValue));
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
setText('');
|
setText('');
|
||||||
@@ -106,20 +118,44 @@ const AmountInput = memo(function AmountInput({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBlur: HTMLProps<HTMLInputElement>['onBlur'] = e => {
|
const onBlur: HTMLProps<HTMLInputElement>['onBlur'] = useEventCallback(e => {
|
||||||
props.onBlur?.(e);
|
props.onBlur?.(e);
|
||||||
if (!e.defaultPrevented) {
|
if (!e.defaultPrevented) {
|
||||||
onUpdate(e.target.value);
|
onUpdate(e.target.value);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const onChangeText = (text: string) => {
|
const onChangeText = useEventCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
let text = e.target.value;
|
||||||
text = reapplyThousandSeparators(text);
|
text = reapplyThousandSeparators(text);
|
||||||
text = appendDecimals(text, String(hideFraction) === 'true');
|
text = appendDecimals(text, String(hideFraction) === 'true');
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
setText(text);
|
setText(text);
|
||||||
props.onChangeValue?.(text);
|
props.onChangeValue?.(text);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el: HTMLInputElement | null = globalFocusedRef.current;
|
||||||
|
if (el === document.activeElement) {
|
||||||
|
// @ts-expect-error addEventListener missing types
|
||||||
|
el.addEventListener('blur', onBlur);
|
||||||
|
// @ts-expect-error addEventListener missing types
|
||||||
|
el.addEventListener('input', onChangeText);
|
||||||
|
// @ts-expect-error addEventListener missing types
|
||||||
|
el.addEventListener('keyup', onKeyUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (el) {
|
||||||
|
// @ts-expect-error addEventListener missing types
|
||||||
|
el.removeEventListener('blur', onBlur);
|
||||||
|
// @ts-expect-error addEventListener missing types
|
||||||
|
el.removeEventListener('input', onChangeText);
|
||||||
|
// @ts-expect-error addEventListener missing types
|
||||||
|
el.removeEventListener('keyup', onKeyUp);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [onBlur, onChangeText, onKeyUp, globalFocusedRef]);
|
||||||
|
|
||||||
const input = (
|
const input = (
|
||||||
<input
|
<input
|
||||||
@@ -128,7 +164,7 @@ const AmountInput = memo(function AmountInput({
|
|||||||
value={text}
|
value={text}
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
onChange={e => onChangeText(e.target.value)}
|
onChange={onChangeText}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onKeyUp={onKeyUp}
|
onKeyUp={onKeyUp}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import {
|
|||||||
|
|
||||||
import { send } from 'loot-core/platform/client/fetch';
|
import { send } from 'loot-core/platform/client/fetch';
|
||||||
import * as monthUtils from 'loot-core/shared/months';
|
import * as monthUtils from 'loot-core/shared/months';
|
||||||
import * as Platform from 'loot-core/shared/platform';
|
|
||||||
import { q } from 'loot-core/shared/query';
|
import { q } from 'loot-core/shared/query';
|
||||||
import { getStatusLabel } from 'loot-core/shared/schedules';
|
import { getStatusLabel } from 'loot-core/shared/schedules';
|
||||||
import {
|
import {
|
||||||
@@ -510,7 +509,7 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
|||||||
const [totalAmountFocused, setTotalAmountFocused] = useState(
|
const [totalAmountFocused, setTotalAmountFocused] = useState(
|
||||||
// iOS does not support automatically opening up the keyboard for the
|
// iOS does not support automatically opening up the keyboard for the
|
||||||
// total amount field. Hence we should not focus on it on page render.
|
// total amount field. Hence we should not focus on it on page render.
|
||||||
!Platform.isIOSAgent,
|
true,
|
||||||
);
|
);
|
||||||
const childTransactionElementRefMap = useRef({});
|
const childTransactionElementRefMap = useRef({});
|
||||||
const hasAccountChanged = useRef(false);
|
const hasAccountChanged = useRef(false);
|
||||||
@@ -528,7 +527,7 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
|||||||
const isInitialMount = useInitialMount();
|
const isInitialMount = useInitialMount();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isInitialMount && isAdding && !Platform.isIOSAgent) {
|
if (isInitialMount && isAdding) {
|
||||||
onTotalAmountEdit();
|
onTotalAmountEdit();
|
||||||
}
|
}
|
||||||
}, [isAdding, isInitialMount, onTotalAmountEdit]);
|
}, [isAdding, isInitialMount, onTotalAmountEdit]);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import * as budgetfilesSlice from './budgetfiles/budgetfilesSlice';
|
|||||||
// focus outline appear from keyboard events.
|
// focus outline appear from keyboard events.
|
||||||
import 'focus-visible';
|
import 'focus-visible';
|
||||||
import { App } from './components/App';
|
import { App } from './components/App';
|
||||||
|
import { NavigableFocusProvider } from './components/NavigableFocusProvider';
|
||||||
import { ServerProvider } from './components/ServerContext';
|
import { ServerProvider } from './components/ServerContext';
|
||||||
import * as modalsSlice from './modals/modalsSlice';
|
import * as modalsSlice from './modals/modalsSlice';
|
||||||
import * as notificationsSlice from './notifications/notificationsSlice';
|
import * as notificationsSlice from './notifications/notificationsSlice';
|
||||||
@@ -94,7 +95,9 @@ root.render(
|
|||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ServerProvider>
|
<ServerProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<App />
|
<NavigableFocusProvider>
|
||||||
|
<App />
|
||||||
|
</NavigableFocusProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ServerProvider>
|
</ServerProvider>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
|
|||||||
6
upcoming-release-notes/5595.md
Normal file
6
upcoming-release-notes/5595.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
category: Enhancements
|
||||||
|
authors: [jfdoming]
|
||||||
|
---
|
||||||
|
|
||||||
|
Auto-focus on navigate in all browsers
|
||||||
Reference in New Issue
Block a user