Skip to content
This repository has been archived by the owner on Oct 21, 2024. It is now read-only.

refactor: move handle resize to its own hook #342

Merged
merged 3 commits into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import KEYS from '../keys';
import getSizes from './get-sizes';

export interface FoldedListboxClickEvent {
event: React.MouseEvent<HTMLDivElement>;
event: React.MouseEvent<HTMLObjectElement | HTMLDivElement>;
resource: IListboxResource;
}
export interface FoldedListboxProps {
Expand Down
60 changes: 16 additions & 44 deletions packages/sn-filter-pane/src/components/ListboxGrid/ListboxGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,49 @@
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;

// Subscribe to the resourceStore outside of react and re-render on store change.
const { resources = [] } = useSyncExternalStore(resourceStore.subscribe, resourceStore.getState);

const gridRef = useRef<HTMLDivElement>();
const [columns, setColumns] = useState<IColumn[]>([]);
const [overflowingResources, setOverflowingResources] = useState<IListboxResource[]>([]);
const isInSense = typeof (sense?.isSmallDevice) === 'function';
const { options } = stores.store.getState();
const gridRef = useRef<HTMLObjectElement>();

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<HTMLLIElement>) => {
Expand Down Expand Up @@ -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();
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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<HTMLObjectElement>;

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();
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const getWidthHeight = (ref: React.MutableRefObject<HTMLDivElement | undefined>) => {
const getWidthHeight = (ref: React.MutableRefObject<HTMLObjectElement | undefined>) => {
const width = ref?.current?.offsetWidth ?? 0;
const height = ref?.current?.offsetHeight ?? 0;
return { width, height };
Expand Down
Original file line number Diff line number Diff line change
@@ -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<HTMLObjectElement | undefined>;
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<IListboxResource[]>([]);
const [columns, setColumns] = useState<IColumn[]>([]);

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,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
2 changes: 1 addition & 1 deletion packages/sn-filter-pane/src/ext/property-panel/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/sn-filter-pane/src/hooks/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type IPage = {
qMatrix: object[];
};

export interface IContainerElement extends HTMLDivElement {
export interface IContainerElement extends HTMLObjectElement {
current: HTMLElement
}

Expand Down
2 changes: 1 addition & 1 deletion packages/sn-filter-pane/src/hooks/use-focus-listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const handleFocusoutEvent = (
};

const useFocusListener = (
filterpaneWrapperRef: React.MutableRefObject<HTMLDivElement | undefined>,
filterpaneWrapperRef: React.MutableRefObject<HTMLObjectElement | undefined>,
keyboard: stardust.Keyboard | undefined,
) => {
useEffect(() => {
Expand Down
12 changes: 10 additions & 2 deletions packages/sn-filter-pane/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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: {},
};

Expand Down
3 changes: 2 additions & 1 deletion packages/sn-filter-pane/src/store/state-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Listener } from '.';

const createStore = <T>(createState: () => T) => {
let state = createState();
type Listener = () => void;
let listeners: Listener[] = [];

const emitChange = () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/sn-filter-pane/src/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ interface ISense {
}

export interface IEnv {
flags: {
flags?: {
isEnabled: (flag?: string) => boolean;
},
sense?: ISense,
Expand Down
Loading