import type { DeepPartial, UseFormProps, UseFormReturn, WatchObserver, } from '@graphcommerce/ecommerce-ui' import { FormAutoSubmit, useForm } from '@graphcommerce/ecommerce-ui' import { useMatchMediaMotionValue, useMemoObject } from '@graphcommerce/next-ui' import type { Theme } from '@mui/material' // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { useEventCallback, useMediaQuery, useTheme } from '@mui/material' import { m, useTransform } from 'framer-motion' import { useRouter } from 'next/router' import type { BaseSyntheticEvent, MutableRefObject } from 'react' import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react' import { productListLinkFromFilter } from '../../hooks/useProductListLink' import type { ProductListFiltersFragment } from '../ProductListFilters/ProductListFilters.gql' import type { ProductFilterParams, ProductListParams } from '../ProductListItems/filterTypes' import { toFilterParams } from '../ProductListItems/filterTypes' type Subscription = { unsubscribe: () => void } type DataProps = { filterTypes: Record appliedAggregations?: ProductListFiltersFragment['aggregations'] } & ProductListFiltersFragment export type ProductFiltersProContext = DataProps & { /** * Watch and formState are known to cause performance issues. * * - `watch` -> `useWatch` * - `formState` -> `useFormState` */ form: Omit, 'formState' | 'watch'> & { watch: ( callback: WatchObserver, defaultValues?: DeepPartial, ) => Subscription } /** * Parameters of the currently displayed items. * * To get active form values use `useWatch`. */ params: ProductFilterParams submit: (e?: BaseSyntheticEvent | undefined) => Promise } const FilterFormContext = createContext(null) export const globalFormContextRef: MutableRefObject = { current: null, } export function useProductFiltersPro(optional: true): ProductFiltersProContext | null export function useProductFiltersPro(optional?: false): ProductFiltersProContext export function useProductFiltersPro(optional: boolean = false) { const context = useContext(FilterFormContext) if (!optional && !context) throw Error('useProductFiltersPro should be used inside ProductFiltersPro') return context } export type FilterFormProviderProps = Omit< UseFormProps, 'values' | 'defaultValues' > & { children: React.ReactNode params: ProductListParams /** Whether the filter should scroll to the products list and whether to submit the form on change. */ autoSubmitMd?: boolean handleSubmit?: ( formValues: ProductFilterParams, next: (shallow?: boolean, replace?: boolean) => Promise, ) => Promise | void } & DataProps function AutoSubmitSidebarDesktop() { const { form, submit } = useProductFiltersPro() // We only need to auto-submit when the layout is not sidebar and we're viewing on desktop const autoSubmitDisabled = useMediaQuery((t) => t.breakpoints.down('md'), { defaultMatches: false, }) return ( ) } export function ProductFiltersPro(props: FilterFormProviderProps) { const { children, params, aggregations, appliedAggregations, filterTypes, autoSubmitMd = false, handleSubmit, ...formProps } = props const defaultValues = useMemoObject(toFilterParams(params)) const form = useForm({ defaultValues, ...formProps }) const ref = useRef(null) const router = useRouter() const theme = useTheme() const isDesktop = useMatchMediaMotionValue('up', 'md') const scrollMarginTop = useTransform(() => (isDesktop.get() ? 0 : theme.appShell.headerHeightSm)) const scroll = useTransform(() => !autoSubmitMd || isDesktop.get()) const submit = useEventCallback( form.handleSubmit(async (formValues) => { const path = productListLinkFromFilter({ ...formValues, currentPage: 1 }) if (router.asPath === path) return false const isSearch = router.asPath.startsWith('/search') const isFilter = (router.query.url ?? []).includes('q') const next = async (shallow = false, replace: boolean = isSearch || isFilter) => { const opts = { shallow, scroll: scroll.get() } await (replace ? router.replace(path, path, opts) : router.push(path, path, opts)) } if (handleSubmit) return handleSubmit(formValues, next) return next() }), ) const filterFormContext = useMemo(() => { const ctx: ProductFiltersProContext = { form, params: defaultValues, submit, appliedAggregations, filterTypes, aggregations: aggregations ?? appliedAggregations, } globalFormContextRef.current = ctx return ctx }, [form, defaultValues, submit, appliedAggregations, filterTypes, aggregations]) // When the component unmounts, we want to clear the global filter form useEffect( () => () => { globalFormContextRef.current = null }, [], ) return ( {children} {autoSubmitMd && } ) }