diff --git a/packages/sn-filter-pane/src/components/FoldedListbox/FoldedListbox.tsx b/packages/sn-filter-pane/src/components/FoldedListbox/FoldedListbox.tsx index 0d375d81..758d6620 100644 --- a/packages/sn-filter-pane/src/components/FoldedListbox/FoldedListbox.tsx +++ b/packages/sn-filter-pane/src/components/FoldedListbox/FoldedListbox.tsx @@ -12,7 +12,7 @@ import KEYS from '../keys'; import getSizes from './get-sizes'; export interface FoldedListboxClickEvent { - event: React.MouseEvent; + event: React.MouseEvent; resource: IListboxResource; } export interface FoldedListboxProps { diff --git a/packages/sn-filter-pane/src/components/ListboxGrid/ListboxGrid.tsx b/packages/sn-filter-pane/src/components/ListboxGrid/ListboxGrid.tsx index 8579081f..6448a995 100644 --- a/packages/sn-filter-pane/src/components/ListboxGrid/ListboxGrid.tsx +++ b/packages/sn-filter-pane/src/components/ListboxGrid/ListboxGrid.tsx @@ -1,41 +1,31 @@ import React, { - useCallback, useEffect, useRef, useState, useSyncExternalStore, + useEffect, useRef, useSyncExternalStore, } from 'react'; import { Grid } from '@mui/material'; import debounce from 'lodash/debounce'; import { stardust } from '@nebula.js/stardust'; -import getWidthHeight from './get-size'; import { IListboxResource } from '../../hooks/types'; import ListboxContainer from '../ListboxContainer'; import 'react-resizable/css/styles.css'; import ElementResizeListener from '../ElementResizeListener'; -import { - setDefaultValues, balanceColumns, calculateColumns, calculateExpandPriority, mergeColumnsAndResources, hasHeader, -} from './distribute-resources'; -import { ExpandProps, IColumn, ISize } from './interfaces'; + +import { IColumn } from './interfaces'; import { ColumnGrid } from './grid-components/ColumnGrid'; import { Column } from './grid-components/Column'; import { ColumnItem } from './grid-components/ColumnItem'; import type { IStores } from '../../store'; import { ListboxPopoverContainer } from '../ListboxPopoverContainer'; import useHandleActive, { ActiveOnly } from './use-handle-active'; +import useHandleResize from './use-handle-resize'; import KEYS from '../keys'; -import { RenderTrackerService } from '../../services/render-tracker'; import useFocusListener from '../../hooks/use-focus-listener'; import findNextIndex from './find-next-index'; import { IEnv } from '../../types/types'; -const prepareRenderTracker = (listboxCount: number, renderTracker?: RenderTrackerService) => { - renderTracker?.setNumberOfListboxes(listboxCount); - if (listboxCount === 0) { - renderTracker?.renderedCallback(); - } -}; - function ListboxGrid({ stores }: { stores: IStores }) { const { store, resourceStore } = stores; const { - env = {}, selections, keyboard, renderTracker, + env = {}, selections, keyboard, renderTracker, options, } = store.getState(); const { sense } = env as IEnv; @@ -43,35 +33,17 @@ function ListboxGrid({ stores }: { stores: IStores }) { // Subscribe to the resourceStore outside of react and re-render on store change. const { resources = [] } = useSyncExternalStore(resourceStore.subscribe, resourceStore.getState); - const gridRef = useRef(); - const [columns, setColumns] = useState([]); - const [overflowingResources, setOverflowingResources] = useState([]); - const isInSense = typeof (sense?.isSmallDevice) === 'function'; - const { options } = stores.store.getState(); + const gridRef = useRef(); - const handleResize = useCallback(() => { - if (!resources?.length) { - return; - } - const { width, height } = getWidthHeight(gridRef); - const size: ISize = { width, height, dimensionCount: resources.length }; - store.setState({ ...store.getState(), containerSize: size }); - const isSmallDevice = sense?.isSmallDevice?.() ?? false; - const isSingleItem = resources.length === 1; - const expandProps: ExpandProps = { - isSingleGridLayout: isSingleItem && resources[0].layout?.layoutOptions?.dataLayout === 'grid', - hasHeader: hasHeader(resources[0]), - }; - const calculatedColumns = calculateColumns(size, [], isSmallDevice, expandProps); - const balancedColumns = balanceColumns(size, calculatedColumns, isSmallDevice, expandProps); - const resourcesWithDefaultValues = setDefaultValues(resources); - const { columns: mergedColumnsAndResources, overflowing } = mergeColumnsAndResources(balancedColumns, resourcesWithDefaultValues); - setOverflowingResources(overflowing); - const { columns: expandedAndCollapsedColumns, expandedItemsCount } = calculateExpandPriority(mergedColumnsAndResources, size, expandProps); - setColumns(expandedAndCollapsedColumns); - prepareRenderTracker(expandedItemsCount, renderTracker); - }, [resources]); + const isInSense = typeof (sense?.isSmallDevice) === 'function'; + const { handleResize, overflowingResources, columns } = useHandleResize({ + resources, + gridRef, + store, + env, + renderTracker, + }); const dHandleResize = useRef(debounce(handleResize, isInSense ? 0 : 10)); const preventDefaultBehavior = (event: React.KeyboardEvent | MouseEvent | React.MouseEvent) => { @@ -128,7 +100,7 @@ function ListboxGrid({ stores }: { stores: IStores }) { }, [resources]); useEffect(() => { - const firstChild = gridRef?.current?.querySelector?.('.listbox-container,.listbox-popover-container') as HTMLDivElement; + const firstChild = gridRef?.current?.querySelector?.('.listbox-container,.listbox-popover-container') as HTMLObjectElement; if (keyboard?.active) { firstChild?.setAttribute('tabIndex', '-1'); firstChild?.focus(); @@ -148,7 +120,7 @@ function ListboxGrid({ stores }: { stores: IStores }) { onKeyDown={handleKeyDown} sx={{ flexDirection: isRtl ? 'row-reverse' : 'row' }} columns={columns?.length} - ref={gridRef as unknown as () => HTMLDivElement} + ref={gridRef as unknown as () => HTMLObjectElement} spacing={0} height='100%' overflow="hidden" diff --git a/packages/sn-filter-pane/src/components/ListboxGrid/__tests__/use-handle-resize.spec.ts b/packages/sn-filter-pane/src/components/ListboxGrid/__tests__/use-handle-resize.spec.ts new file mode 100644 index 00000000..31428571 --- /dev/null +++ b/packages/sn-filter-pane/src/components/ListboxGrid/__tests__/use-handle-resize.spec.ts @@ -0,0 +1,63 @@ +import { MutableRefObject } from 'react'; +import { act, waitFor, renderHook } from '@testing-library/react'; +import useHandleResize from '../use-handle-resize'; +import { IStore } from '../../../store'; +import { IListboxResource } from '../../../hooks/types'; +import { RenderTrackerService } from '../../../services/render-tracker'; +import * as distResourcesMod from '../distribute-resources'; + +describe('use-handle-resize', () => { + beforeAll(() => { + jest.spyOn(distResourcesMod, 'calculateExpandPriority').mockReturnValue({ columns: [{}, {}, {}], expandedItemsCount: 3 }); + }); + + it('should return a resize handler which, upon call, returns columns and overflowingResources, calculated by other funcs', async () => { + const resources = [{ + layout: {}, + }] as unknown as IListboxResource[]; + + const gridRef = { + current: {}, + } as unknown as MutableRefObject; + + const store = { + getState: jest.fn().mockReturnValue({ heyHey: 1 }), + setState: jest.fn(), + } as unknown as IStore; + + const env = { + sense: { + isSmallDevice: () => false, + }, + }; + + const renderTracker = { + setNumberOfListboxes: jest.fn(), + renderedCallback: jest.fn(), + } as unknown as RenderTrackerService; + + const { result, unmount } = renderHook(() => useHandleResize({ + resources, + gridRef, + store, + env, + renderTracker, + })); + + const { handleResize } = result.current; + + expect(result.current.columns).toEqual([]); + expect(result.current.overflowingResources).toEqual([]); + + await act(() => { + handleResize(); + }); + + await waitFor(() => { + expect(result.current.columns).toHaveLength(3); + expect(result.current.overflowingResources).toHaveLength(0); + }); + + unmount(); + }); +}); diff --git a/packages/sn-filter-pane/src/components/ListboxGrid/get-size.ts b/packages/sn-filter-pane/src/components/ListboxGrid/get-size.ts index f62c2d3a..46bbc2a1 100644 --- a/packages/sn-filter-pane/src/components/ListboxGrid/get-size.ts +++ b/packages/sn-filter-pane/src/components/ListboxGrid/get-size.ts @@ -1,4 +1,4 @@ -const getWidthHeight = (ref: React.MutableRefObject) => { +const getWidthHeight = (ref: React.MutableRefObject) => { const width = ref?.current?.offsetWidth ?? 0; const height = ref?.current?.offsetHeight ?? 0; return { width, height }; diff --git a/packages/sn-filter-pane/src/components/ListboxGrid/use-handle-resize.ts b/packages/sn-filter-pane/src/components/ListboxGrid/use-handle-resize.ts new file mode 100644 index 00000000..d4dca308 --- /dev/null +++ b/packages/sn-filter-pane/src/components/ListboxGrid/use-handle-resize.ts @@ -0,0 +1,71 @@ +import { + MutableRefObject, useCallback, useState, +} from 'react'; +import { RenderTrackerService } from '../../services/render-tracker'; +import getWidthHeight from './get-size'; +import { + setDefaultValues, balanceColumns, calculateColumns, calculateExpandPriority, mergeColumnsAndResources, hasHeader, +} from './distribute-resources'; +import { ExpandProps, IColumn, ISize } from './interfaces'; +import { IEnv } from '../../types/types'; +import { IListboxResource } from '../../hooks/types'; +import { IStore } from '../../store'; + +const prepareRenderTracker = (listboxCount: number, renderTracker?: RenderTrackerService) => { + renderTracker?.setNumberOfListboxes(listboxCount); + if (listboxCount === 0) { + renderTracker?.renderedCallback(); + } +}; + +interface IUseHandleResize { + resources: IListboxResource[]; + gridRef: MutableRefObject; + store: IStore; + env: IEnv; + renderTracker?: RenderTrackerService; +} + +export default function useHandleResize({ + resources, + gridRef, + store, + env, + renderTracker, +}: IUseHandleResize) { + const { sense } = env as IEnv; + + const [overflowingResources, setOverflowingResources] = useState([]); + const [columns, setColumns] = useState([]); + + const handleResize = () => { + if (!resources?.length) { + return; + } + const { width, height } = getWidthHeight(gridRef); + const size: ISize = { width, height, dimensionCount: resources.length }; + store.setState({ ...store.getState(), containerSize: size }); + const isSmallDevice = sense?.isSmallDevice?.() ?? false; + const isSingleItem = resources.length === 1; + const expandProps: ExpandProps = { + isSingleGridLayout: isSingleItem && resources[0].layout?.layoutOptions?.dataLayout === 'grid', + hasHeader: hasHeader(resources[0]), + }; + const calculatedColumns = calculateColumns(size, [], isSmallDevice, expandProps); + const balancedColumns = balanceColumns(size, calculatedColumns, isSmallDevice, expandProps); + const resourcesWithDefaultValues = setDefaultValues(resources); + const { columns: mergedColumnsAndResources, overflowing } = mergeColumnsAndResources(balancedColumns, resourcesWithDefaultValues); + setOverflowingResources(overflowing); + const { columns: expandedAndCollapsedColumns, expandedItemsCount } = calculateExpandPriority(mergedColumnsAndResources, size, expandProps); + setColumns(expandedAndCollapsedColumns); + prepareRenderTracker(expandedItemsCount, renderTracker); + }; + + const handleResizeMemo = useCallback(() => handleResize(), [resources]); + + return { + handleResize: handleResizeMemo, + overflowingResources, + columns, + }; +} diff --git a/packages/sn-filter-pane/src/components/ListboxPopoverContainer.tsx b/packages/sn-filter-pane/src/components/ListboxPopoverContainer.tsx index 8aa3eab2..902a978b 100644 --- a/packages/sn-filter-pane/src/components/ListboxPopoverContainer.tsx +++ b/packages/sn-filter-pane/src/components/ListboxPopoverContainer.tsx @@ -221,7 +221,7 @@ export const ListboxPopoverContainer = ({ resources, stores }: FoldedPopoverProp vertical: 'bottom', horizontal: 'left', }} - PaperProps={popoverPaperProps(!!selectedResource, isSmallDevice, stardustTheme, muiTheme)} + slotProps={{ paper: popoverPaperProps(!!selectedResource, isSmallDevice, stardustTheme, muiTheme) }} anchorReference= {isSmallDevice ? 'anchorPosition' : 'anchorEl'} anchorPosition= {{ left: 0, top: 0 }} marginThreshold={isSmallDevice ? 0 : 16} diff --git a/packages/sn-filter-pane/src/ext/property-panel/data/data-panel/presentation/index.ts b/packages/sn-filter-pane/src/ext/property-panel/data/data-panel/presentation/index.ts index 85939615..51c3b51b 100644 --- a/packages/sn-filter-pane/src/ext/property-panel/data/data-panel/presentation/index.ts +++ b/packages/sn-filter-pane/src/ext/property-panel/data/data-panel/presentation/index.ts @@ -54,7 +54,7 @@ export default function getPresentation(env: IEnv) { defaultValue: frequencies.FREQUENCY_NONE, show(_properties: unknown, _handler: unknown, args: { app: { layout: INxAppLayout } }) { const isDQ = isDirectQueryEnabled({ env, appLayout: args?.app?.layout }); - return !isDQ && isEnabled('LIST_BOX_FREQUENCY_COUNT'); + return !isDQ && isEnabled?.('LIST_BOX_FREQUENCY_COUNT'); }, translation: 'properties.frequencyCountMode', options: [ diff --git a/packages/sn-filter-pane/src/ext/property-panel/settings.ts b/packages/sn-filter-pane/src/ext/property-panel/settings.ts index 6e3aeff8..eecb8b02 100644 --- a/packages/sn-filter-pane/src/ext/property-panel/settings.ts +++ b/packages/sn-filter-pane/src/ext/property-panel/settings.ts @@ -17,7 +17,7 @@ function getSettings(env: IEnv) { simpleLabels: null, }, }; - if (flags.isEnabled('IM_4073_FILTERPANE_STYLING')) { + if (flags?.isEnabled('IM_4073_FILTERPANE_STYLING')) { Object.assign(settings.items, { presentation: { grouped: true, diff --git a/packages/sn-filter-pane/src/hooks/types/index.d.ts b/packages/sn-filter-pane/src/hooks/types/index.d.ts index 054fd0e4..4dd0f482 100644 --- a/packages/sn-filter-pane/src/hooks/types/index.d.ts +++ b/packages/sn-filter-pane/src/hooks/types/index.d.ts @@ -9,7 +9,7 @@ export type IPage = { qMatrix: object[]; }; -export interface IContainerElement extends HTMLDivElement { +export interface IContainerElement extends HTMLObjectElement { current: HTMLElement } diff --git a/packages/sn-filter-pane/src/hooks/use-focus-listener.ts b/packages/sn-filter-pane/src/hooks/use-focus-listener.ts index a29e7a7e..6a0cf2ca 100644 --- a/packages/sn-filter-pane/src/hooks/use-focus-listener.ts +++ b/packages/sn-filter-pane/src/hooks/use-focus-listener.ts @@ -26,7 +26,7 @@ const handleFocusoutEvent = ( }; const useFocusListener = ( - filterpaneWrapperRef: React.MutableRefObject, + filterpaneWrapperRef: React.MutableRefObject, keyboard: stardust.Keyboard | undefined, ) => { useEffect(() => { diff --git a/packages/sn-filter-pane/src/store/index.ts b/packages/sn-filter-pane/src/store/index.ts index 04459959..a41e58d4 100644 --- a/packages/sn-filter-pane/src/store/index.ts +++ b/packages/sn-filter-pane/src/store/index.ts @@ -9,7 +9,7 @@ import { RenderTrackerService } from '../services/render-tracker'; import createStore from './state-store'; import { ISize } from '../components/ListboxGrid/interfaces'; -export interface IStore { +export interface IStoreState { app?: EngineAPI.IApp; model?: EngineAPI.IGenericObject; fpLayout?: IFilterPaneLayout; @@ -26,11 +26,19 @@ export interface IStore { containerSize?: ISize; } +export type Listener = () => void; + +export interface IStore { + getState: () => IStoreState; + setState: (obj: IStoreState) => void; + subscribe: (listener: Listener) => void; +} + interface ResourceState { resources: IListboxResource[]; } -const initStoreState: IStore = { +const initStoreState: IStoreState = { options: {}, }; diff --git a/packages/sn-filter-pane/src/store/state-store.ts b/packages/sn-filter-pane/src/store/state-store.ts index 46a8e107..6ea878d8 100644 --- a/packages/sn-filter-pane/src/store/state-store.ts +++ b/packages/sn-filter-pane/src/store/state-store.ts @@ -1,6 +1,7 @@ +import { Listener } from '.'; + const createStore = (createState: () => T) => { let state = createState(); - type Listener = () => void; let listeners: Listener[] = []; const emitChange = () => { diff --git a/packages/sn-filter-pane/src/types/types.d.ts b/packages/sn-filter-pane/src/types/types.d.ts index eb1cdaa9..1a2630f4 100644 --- a/packages/sn-filter-pane/src/types/types.d.ts +++ b/packages/sn-filter-pane/src/types/types.d.ts @@ -3,7 +3,7 @@ interface ISense { } export interface IEnv { - flags: { + flags?: { isEnabled: (flag?: string) => boolean; }, sense?: ISense,