From bf1edfaf8b3ed42c30673d8fed9ce3449cda4c70 Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Wed, 6 Dec 2023 15:49:49 +0100 Subject: [PATCH] Using our own `createContext()` + QoL changes (#1687) Co-authored-by: Ole Martin Handeland --- src/components/form/Panel.test.tsx | 16 +++++++--- src/components/form/Panel.tsx | 6 ++-- src/components/form/SoftValidations.test.tsx | 16 +++++++--- src/core/contexts/context.tsx | 1 + .../NodeInspector/NodeInspectorContext.ts | 31 +++++++------------ .../layoutValidation/useLayoutValidation.tsx | 15 ++++++--- src/features/language/useLanguage.ts | 6 ++-- src/layout/FormComponentContext.tsx | 19 ++++++++++++ src/layout/GenericComponent.tsx | 14 +++++---- .../Group/RepeatingGroupsFocusContext.tsx | 31 ++++++++----------- src/layout/index.ts | 17 +--------- tsconfig.json | 2 +- 12 files changed, 95 insertions(+), 79 deletions(-) create mode 100644 src/layout/FormComponentContext.tsx diff --git a/src/components/form/Panel.test.tsx b/src/components/form/Panel.test.tsx index d934df6bef..342acfd81e 100644 --- a/src/components/form/Panel.test.tsx +++ b/src/components/form/Panel.test.tsx @@ -4,11 +4,12 @@ import { screen } from '@testing-library/react'; import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; import { getVariant, Panel, PanelVariant } from 'src/components/form/Panel'; -import { FormComponentContext } from 'src/layout'; +import { FormComponentContextProvider } from 'src/layout/FormComponentContext'; import { renderWithInstanceAndLayout } from 'src/test/renderWithProviders'; import type { IPanelProps } from 'src/components/form/Panel'; -import type { IFormComponentContext } from 'src/layout'; +import type { IFormComponentContext } from 'src/layout/FormComponentContext'; import type { IRuntimeState } from 'src/types'; +import type { LayoutNode } from 'src/utils/layout/LayoutNode'; describe('Panel', () => { it('should show icon when showIcon is true', async () => { @@ -74,9 +75,16 @@ const render = async ( await renderWithInstanceAndLayout({ renderer: () => ( - + - + ), reduxState: { ...getInitialStateMock(), diff --git a/src/components/form/Panel.tsx b/src/components/form/Panel.tsx index a17dae8251..90d44b50bf 100644 --- a/src/components/form/Panel.tsx +++ b/src/components/form/Panel.tsx @@ -1,10 +1,10 @@ -import React, { useContext } from 'react'; +import React from 'react'; import { Panel as PanelDesignSystem, PanelVariant } from '@altinn/altinn-design-system'; import { ConditionalWrapper } from 'src/components/ConditionalWrapper'; import { FullWidthWrapper } from 'src/components/form/FullWidthWrapper'; -import { FormComponentContext } from 'src/layout'; +import { useFormComponentCtx } from 'src/layout/FormComponentContext'; import { assertUnreachable } from 'src/types'; import type { IPanelBase } from 'src/layout/common.generated'; @@ -41,7 +41,7 @@ export interface IPanelProps { } export const Panel = ({ children, variant, showIcon, title, showPointer }: IPanelProps) => { - const { grid, baseComponentId } = useContext(FormComponentContext); + const { grid, baseComponentId } = useFormComponentCtx() || {}; const shouldHaveFullWidth = !grid && !baseComponentId; return ( diff --git a/src/components/form/SoftValidations.test.tsx b/src/components/form/SoftValidations.test.tsx index c1213ab3f7..9a5cee84c6 100644 --- a/src/components/form/SoftValidations.test.tsx +++ b/src/components/form/SoftValidations.test.tsx @@ -4,11 +4,12 @@ import { screen } from '@testing-library/react'; import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; import { SoftValidations } from 'src/components/form/SoftValidations'; -import { FormComponentContext } from 'src/layout'; +import { FormComponentContextProvider } from 'src/layout/FormComponentContext'; import { renderWithInstanceAndLayout } from 'src/test/renderWithProviders'; import type { ISoftValidationProps, SoftValidationVariant } from 'src/components/form/SoftValidations'; -import type { IFormComponentContext } from 'src/layout'; +import type { IFormComponentContext } from 'src/layout/FormComponentContext'; import type { IRuntimeState } from 'src/types'; +import type { LayoutNode } from 'src/utils/layout/LayoutNode'; const render = async ( props: Partial = {}, @@ -23,9 +24,16 @@ const render = async ( await renderWithInstanceAndLayout({ renderer: () => ( - + - + ), reduxState: { ...getInitialStateMock(), diff --git a/src/core/contexts/context.tsx b/src/core/contexts/context.tsx index 24bc16894e..557bb6280d 100644 --- a/src/core/contexts/context.tsx +++ b/src/core/contexts/context.tsx @@ -41,6 +41,7 @@ export function createContext({ name, required, ...rest }: CreateContextProps innerValue: defaultValue, provided: false, }); + Context.displayName = name; const useHasProvider = () => Boolean(React.useContext(Context).provided); diff --git a/src/features/devtools/components/NodeInspector/NodeInspectorContext.ts b/src/features/devtools/components/NodeInspector/NodeInspectorContext.ts index 52e26e3eeb..cf36cb2420 100644 --- a/src/features/devtools/components/NodeInspector/NodeInspectorContext.ts +++ b/src/features/devtools/components/NodeInspector/NodeInspectorContext.ts @@ -1,23 +1,16 @@ -import { createContext, useContext } from 'react'; - +import { createContext } from 'src/core/contexts/context'; import type { LayoutNode } from 'src/utils/layout/LayoutNode'; -export type NodeInspectorContextValue = - | { - selectedNodeId: string | undefined; - selectNode: (id: string) => void; - node: LayoutNode | undefined; - } - | undefined; - -const NodeInspectorContext = createContext(undefined); +interface NodeInspectorContextValue { + selectedNodeId: string | undefined; + selectNode: (id: string) => void; + node: LayoutNode | undefined; +} -export const useNodeInspectorContext = () => { - const context = useContext(NodeInspectorContext); - if (!context) { - throw new Error('useNodeInspectorContext must be used within a NodeInspectorContextProvider'); - } - return context; -}; +const { Provider, useCtx } = createContext({ + name: 'NodeInspectorContext', + required: true, +}); -export const NodeInspectorContextProvider = NodeInspectorContext.Provider; +export const useNodeInspectorContext = () => useCtx(); +export const NodeInspectorContextProvider = Provider; diff --git a/src/features/devtools/layoutValidation/useLayoutValidation.tsx b/src/features/devtools/layoutValidation/useLayoutValidation.tsx index f611ceea8e..3938e87954 100644 --- a/src/features/devtools/layoutValidation/useLayoutValidation.tsx +++ b/src/features/devtools/layoutValidation/useLayoutValidation.tsx @@ -1,6 +1,7 @@ -import React, { createContext, useContext, useMemo } from 'react'; +import React, { useMemo } from 'react'; import type { PropsWithChildren } from 'react'; +import { createContext } from 'src/core/contexts/context'; import { dotNotationToPointer } from 'src/features/datamodel/notations'; import { lookupBindingInSchema } from 'src/features/datamodel/SimpleSchemaTraversal'; import { useCurrentDataModelSchema, useCurrentDataModelType } from 'src/features/datamodel/useBindingSchema'; @@ -125,9 +126,13 @@ function useDataModelBindingsValidation(props: LayoutValidationProps) { }, [layoutSetId, schema, layoutLoaded, dataType, nodes, logErrors]); } -const Context = createContext(undefined); +const { Provider, useCtx } = createContext({ + name: 'LayoutValidation', + required: false, + default: undefined, +}); -export const useLayoutValidation = () => useContext(Context); +export const useLayoutValidation = () => useCtx(); export const useLayoutValidationForPage = () => { const ctx = useLayoutValidation(); const layoutSetId = useCurrentLayoutSetId() || 'default'; @@ -145,9 +150,9 @@ export function LayoutValidationProvider({ children }: PropsWithChildren) { const dataModelBindingsValidations = useDataModelBindingsValidation({ logErrors: true }); if (!layoutSchemaValidations) { - return {children}; + return {children}; } const value = mergeValidationErrors(dataModelBindingsValidations, layoutSchemaValidations); - return {children}; + return {children}; } diff --git a/src/features/language/useLanguage.ts b/src/features/language/useLanguage.ts index 6ddbec34b7..36d3843445 100644 --- a/src/features/language/useLanguage.ts +++ b/src/features/language/useLanguage.ts @@ -1,4 +1,4 @@ -import { Children, isValidElement, useContext, useMemo } from 'react'; +import { Children, isValidElement, useMemo } from 'react'; import type { JSX, ReactNode } from 'react'; import { useLaxInstanceData } from 'src/features/instance/InstanceContext'; @@ -8,7 +8,7 @@ import { useTextResources } from 'src/features/language/textResources/TextResour import { useAppSelector } from 'src/hooks/useAppSelector'; import { getLanguageFromCode } from 'src/language/languages'; import { getParsedLanguageFromText } from 'src/language/sharedLanguage'; -import { FormComponentContext } from 'src/layout'; +import { useFormComponentCtx } from 'src/layout/FormComponentContext'; import { getKeyWithoutIndexIndicators } from 'src/utils/databindings'; import { transposeDataBinding } from 'src/utils/databindings/DataBinding'; import { buildInstanceDataSources } from 'src/utils/instanceDataSources'; @@ -74,7 +74,7 @@ export type ValidLanguageKey = ObjectToDotNotation; export function useLanguage(node?: LayoutNode) { const textResources = useTextResources(); const selectedAppLanguage = useCurrentLanguage(); - const componentCtx = useContext(FormComponentContext); + const componentCtx = useFormComponentCtx(); const nearestNode = node || componentCtx?.node; const formData = useAppSelector((state) => state.formData.formData); const applicationSettings = useAppSelector((state) => state.applicationSettings.applicationSettings); diff --git a/src/layout/FormComponentContext.tsx b/src/layout/FormComponentContext.tsx new file mode 100644 index 0000000000..55edd873a4 --- /dev/null +++ b/src/layout/FormComponentContext.tsx @@ -0,0 +1,19 @@ +import { createContext } from 'src/core/contexts/context'; +import type { IGrid } from 'src/layout/common.generated'; +import type { LayoutNode } from 'src/utils/layout/LayoutNode'; + +export interface IFormComponentContext { + id: string; + baseComponentId: string | undefined; + node: LayoutNode; + grid?: IGrid; +} + +const { Provider, useCtx } = createContext({ + name: 'FormComponent', + required: false, + default: undefined, +}); + +export const useFormComponentCtx = () => useCtx(); +export const FormComponentContextProvider = Provider; diff --git a/src/layout/GenericComponent.tsx b/src/layout/GenericComponent.tsx index a040107198..55809c54b0 100644 --- a/src/layout/GenericComponent.tsx +++ b/src/layout/GenericComponent.tsx @@ -14,14 +14,16 @@ import { useLanguage } from 'src/features/language/useLanguage'; import { useAppDispatch } from 'src/hooks/useAppDispatch'; import { useAppSelector } from 'src/hooks/useAppSelector'; import { Triggers } from 'src/layout/common.generated'; -import { FormComponentContext, shouldComponentRenderLabel } from 'src/layout/index'; +import { FormComponentContextProvider } from 'src/layout/FormComponentContext'; +import { shouldComponentRenderLabel } from 'src/layout/index'; import { SummaryComponent } from 'src/layout/Summary/SummaryComponent'; import { makeGetFocus } from 'src/selectors/getLayoutData'; import { gridBreakpoints, pageBreakStyles } from 'src/utils/formComponentUtils'; import { renderValidationMessagesForComponent } from 'src/utils/render'; import type { ISingleFieldValidation } from 'src/features/formData/formDataTypes'; import type { IGridStyling } from 'src/layout/common.generated'; -import type { IComponentProps, IFormComponentContext, PropsFromGenericComponent } from 'src/layout/index'; +import type { IFormComponentContext } from 'src/layout/FormComponentContext'; +import type { IComponentProps, PropsFromGenericComponent } from 'src/layout/index'; import type { CompInternal, CompTypes, ITextResourceBindings } from 'src/layout/layout'; import type { LayoutComponent } from 'src/layout/LayoutComponent'; import type { IComponentFormData } from 'src/utils/formComponentUtils'; @@ -298,14 +300,14 @@ export function GenericComponent({ if (layoutComponent.directRender(componentProps) || overrideDisplay?.directRender) { return ( - + - + ); } return ( - + ({ {showValidationMessages && renderValidationMessagesForComponent(filterValidationErrors(), id)} - + ); } diff --git a/src/layout/Group/RepeatingGroupsFocusContext.tsx b/src/layout/Group/RepeatingGroupsFocusContext.tsx index fe43477171..2061a35d08 100644 --- a/src/layout/Group/RepeatingGroupsFocusContext.tsx +++ b/src/layout/Group/RepeatingGroupsFocusContext.tsx @@ -1,4 +1,6 @@ -import React, { createContext, useContext, useMemo, useRef } from 'react'; +import React, { useMemo, useRef } from 'react'; + +import { createContext } from 'src/core/contexts/context'; type FocusableHTMLElement = HTMLElement & HTMLButtonElement & @@ -15,19 +17,16 @@ interface Context { triggerFocus: FocusTrigger; } -const RepeatingGroupsFocusContext = createContext(null); - -export function useRepeatingGroupsFocusContext(): Context { - const context = useContext(RepeatingGroupsFocusContext); - if (!context) { - return { - refSetter: () => undefined, - triggerFocus: () => undefined, - }; - } +const { Provider, useCtx } = createContext({ + name: 'RepeatingGroupsFocus', + required: false, + default: { + refSetter: () => undefined, + triggerFocus: () => undefined, + }, +}); - return context; -} +export const useRepeatingGroupsFocusContext = () => useCtx(); export function RepeatingGroupsFocusProvider(props: { children: React.ReactNode }) { const elementRefs = useMemo(() => new Map(), []); @@ -67,11 +66,7 @@ export function RepeatingGroupsFocusProvider(props: { children: React.ReactNode } }; - return ( - - {props.children} - - ); + return {props.children}; } function isFocusable(element: FocusableHTMLElement) { diff --git a/src/layout/index.ts b/src/layout/index.ts index 87423137a6..93d586c6a3 100644 --- a/src/layout/index.ts +++ b/src/layout/index.ts @@ -1,4 +1,4 @@ -import { createContext, useMemo } from 'react'; +import { useMemo } from 'react'; import { useAttachments } from 'src/features/attachments/AttachmentsContext'; import { useCurrentLanguage } from 'src/features/language/LanguageProvider'; @@ -9,7 +9,6 @@ import { ComponentConfigs } from 'src/layout/components.generated'; import type { IAttachments } from 'src/features/attachments'; import type { IFormData } from 'src/features/formData'; import type { AllOptionsMap } from 'src/features/options/useAllOptions'; -import type { IGrid } from 'src/layout/common.generated'; import type { IGenericComponentProps } from 'src/layout/GenericComponent'; import type { CompInternal, CompRendersLabel, CompTypes } from 'src/layout/layout'; import type { AnyComponent, LayoutComponent } from 'src/layout/LayoutComponent'; @@ -66,20 +65,6 @@ export interface PropsFromGenericComponent exte overrideDisplay?: IGenericComponentProps['overrideDisplay']; } -export interface IFormComponentContext { - grid?: IGrid; - id?: string; - baseComponentId?: string; - node?: LayoutNode; -} - -export const FormComponentContext = createContext({ - grid: undefined, - id: undefined, - baseComponentId: undefined, - node: undefined, -}); - export function getLayoutComponentObject(type: T): CompClassMap[T] { if (type && type in ComponentConfigs) { return ComponentConfigs[type as keyof typeof ComponentConfigs].def as any; diff --git a/tsconfig.json b/tsconfig.json index 6ad076a547..d2bdaef687 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "skipLibCheck": true, "resolveJsonModule": true, "strictNullChecks": true, - "noUnusedLocals": true, + "noUnusedLocals": false, "esModuleInterop": true, "strictBindCallApply": true, "allowUnusedLabels": false,