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

Commit

Permalink
refactor: move handle resize to its own hook (#342)
Browse files Browse the repository at this point in the history
  • Loading branch information
johanlahti authored Aug 17, 2023
1 parent 3621785 commit ca15cca
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 55 deletions.
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

0 comments on commit ca15cca

Please sign in to comment.