CB-4356 TabPanelValidation handler for switching tabs

This commit is contained in:
s.teleshev
2023-12-11 13:05:37 +01:00
parent 5ca9e46bec
commit 42bb20c158
5 changed files with 114 additions and 24 deletions
@@ -49,12 +49,12 @@ export function useExecutor<T>(options: IUseExecutorOptions<T>): void {
}
}
if (props.next) {
executor.next(props.next);
if (props.before) {
executor.before(props.before);
}
if (props.before) {
executor.next(props.before);
if (props.next) {
executor.next(props.next);
}
return () => {
@@ -74,5 +74,9 @@ export function useExecutor<T>(options: IUseExecutorOptions<T>): void {
executor.removeBefore(before);
}
};
}, [executor, props.handlers?.length, props.postHandlers?.length]);
}, [
executor, props.handlers?.length, props.postHandlers?.length,
props.next, props.before, props.handlers,
props.postHandlers,
]);
}
+18 -17
View File
@@ -9,44 +9,45 @@ import { observer } from 'mobx-react-lite';
import { useContext, useEffect, useMemo, useRef } from 'react';
import { TabPanel as BaseTabPanel, TabStateReturn } from 'reakit/Tab';
import { FormContext, getComputed, Loader } from '@cloudbeaver/core-blocks';
import { getComputed, Loader } from '@cloudbeaver/core-blocks';
import { TabContext } from './TabContext';
import type { TabPanelProps } from './TabPanelProps';
import { TabsContext } from './TabsContext';
import { TabPanelValidationHandlerContext } from './TabPanelValidationHandlerContext';
export const TabPanel: React.FC<TabPanelProps> = observer(function TabPanel({ tabId, children, className, lazy }) {
const state = useContext(TabsContext);
const formContext = useContext(FormContext);
const tabContextState = useContext(TabsContext);
const panelRef = useRef<HTMLDivElement>(null);
const tabPanelValidationHandlerContext = useContext(TabPanelValidationHandlerContext);
if (!state) {
if (!tabContextState) {
throw new Error('Tabs context was not provided');
}
if (!tabPanelValidationHandlerContext) {
throw new Error('TabPanelValidationHandlerContext context was not provided');
}
const tabContext = useMemo(() => ({ tabId }), [tabId]);
const selected = getComputed(() => state.state.selectedId === tabId);
const enabled = getComputed(() => (lazy || state.lazy) && !selected);
const selected = getComputed(() => tabContextState.state.selectedId === tabId);
const enabled = getComputed(() => (lazy || tabContextState.lazy) && !selected);
useEffect(() => {
if (formContext === null) {
if (tabPanelValidationHandlerContext === null) {
return;
}
async function trackValidity(event: Event) {
if (selected) {
return;
}
await state?.open(tabId);
formContext?.reportValidity();
tabPanelValidationHandlerContext?.addInvalidTab(tabId);
}
panelRef.current?.removeEventListener('invalid', trackValidity, true);
panelRef.current?.addEventListener('invalid', trackValidity, true);
return () => {
panelRef.current?.removeEventListener('invalid', trackValidity, true);
};
}, [formContext, selected, state, tabId]);
}, [tabId]);
if (enabled) {
return null;
@@ -55,8 +56,8 @@ export const TabPanel: React.FC<TabPanelProps> = observer(function TabPanel({ ta
if (typeof children === 'function') {
return (
<TabContext.Provider value={tabContext}>
<BaseTabPanel ref={panelRef} {...state.state} tabId={tabId} className={className}>
<Loader suspense>{(children as (state: TabStateReturn) => React.ReactNode)(state.state)}</Loader>
<BaseTabPanel ref={panelRef} {...tabContextState.state} tabId={tabId} className={className}>
<Loader suspense>{(children as (state: TabStateReturn) => React.ReactNode)(tabContextState.state)}</Loader>
</BaseTabPanel>
</TabContext.Provider>
);
@@ -64,7 +65,7 @@ export const TabPanel: React.FC<TabPanelProps> = observer(function TabPanel({ ta
return (
<TabContext.Provider value={tabContext}>
<BaseTabPanel ref={panelRef} {...state.state} tabId={tabId} className={className}>
<BaseTabPanel ref={panelRef} {...tabContextState.state} tabId={tabId} className={className}>
<Loader suspense>{children}</Loader>
</BaseTabPanel>
</TabContext.Provider>
@@ -17,6 +17,7 @@ import { BASE_TAB_STYLES } from './Tab/BASE_TAB_STYLES';
import { TabPanel } from './TabPanel';
import type { ITabInfo } from './TabsContainer/ITabsContainer';
import { TabsContext } from './TabsContext';
import { TabPanelValidationHandler } from './TabPanelValidationHandler';
interface Props extends React.PropsWithChildren {
style?: ComponentStyle;
@@ -42,7 +43,7 @@ export const TabPanelList = observer<Props>(function TabPanelList({ style, child
const displayed = state.container.getDisplayed(state.props);
return styled(styles)(
<>
<TabPanelValidationHandler>
{displayed
.map(
generateTabElement(
@@ -56,6 +57,6 @@ export const TabPanelList = observer<Props>(function TabPanelList({ style, child
)
.flat()}
{children}
</>,
</TabPanelValidationHandler>,
);
});
@@ -0,0 +1,76 @@
import { observer } from 'mobx-react-lite';
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { TabsContext } from './TabsContext';
import { FormContext, useExecutor } from '@cloudbeaver/core-blocks';
import { TabPanelValidationHandlerContext } from './TabPanelValidationHandlerContext';
import { SyncExecutor } from '@cloudbeaver/core-executor';
export interface ITabPanelHandlerProps {
children: React.ReactNode;
}
export const TabPanelValidationHandler = observer(function TabPanelValidationHandler({ children }: ITabPanelHandlerProps) {
const state = useContext(TabsContext);
const containerRef = useRef<HTMLDivElement>(null);
const formContext = useContext(FormContext);
const selectedTab = state?.state.selectedId;
const invalidTabs = useRef<Set<string>>(new Set());
const [resetExecutor] = useState(() => new SyncExecutor());
function addInvalidTab(tabId: string) {
invalidTabs.current.add(tabId);
};
function resetInvalidTabs() {
invalidTabs.current = new Set();
}
useExecutor({
executor: resetExecutor,
handlers: [resetInvalidTabs],
});
useExecutor({
executor: formContext?.onValidate,
before: resetExecutor,
});
useExecutor({
executor: formContext?.onValidate,
postHandlers: [() => formContext?.reportValidity()],
});
useEffect(() => {
async function goNextTab() {
const firstInvalidTab = invalidTabs.current.values().next().value;
if (
(selectedTab && invalidTabs.current.has(selectedTab)) ||
invalidTabs.current.size === 0 ||
!firstInvalidTab
) {
return;
}
await state?.open(firstInvalidTab);
resetInvalidTabs();
}
formContext?.parent?.ref?.removeEventListener('invalid', goNextTab, true);
formContext?.parent?.ref?.addEventListener('invalid', goNextTab, true);
return () => {
formContext?.parent?.ref?.removeEventListener('invalid', goNextTab, true);
};
}, []);
const value = useMemo(() => ({ addInvalidTab, invalidTabs }), []);
return (
<TabPanelValidationHandlerContext.Provider value={value}>
<div ref={containerRef}>
{children}
</div>
</TabPanelValidationHandlerContext.Provider>
);
});
@@ -0,0 +1,8 @@
import { MutableRefObject, createContext } from 'react';
export interface ITabPanelValidationHandlerContext {
invalidTabs: MutableRefObject<Set<string>>;
addInvalidTab: (tabId: string) => void;
}
export const TabPanelValidationHandlerContext = createContext<ITabPanelValidationHandlerContext | null>(null);