From fdbc5dd4c34c0f557ba2707697ca8261697a3b0f Mon Sep 17 00:00:00 2001 From: melloware Date: Wed, 28 Dec 2022 09:31:20 -0500 Subject: [PATCH] Fix #3693: Datatable allow responsive stack and scrollable --- components/lib/datatable/DataTable.js | 48 ++++++++++++++++----------- components/lib/hooks/Hooks.js | 15 +++++---- components/lib/hooks/hooks.d.ts | 1 + components/lib/hooks/useMediaQuery.js | 44 ++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 27 deletions(-) create mode 100644 components/lib/hooks/useMediaQuery.js diff --git a/components/lib/datatable/DataTable.js b/components/lib/datatable/DataTable.js index 34eaa6cdb9..569f558d83 100644 --- a/components/lib/datatable/DataTable.js +++ b/components/lib/datatable/DataTable.js @@ -1,6 +1,7 @@ import * as React from 'react'; import PrimeReact, { FilterMatchMode, FilterOperator, FilterService } from '../api/Api'; import { useEventListener, useMountEffect, useUnmountEffect, useUpdateEffect } from '../hooks/Hooks'; +import { useMediaQuery } from '../hooks/useMediaQuery'; import { Paginator } from '../paginator/Paginator'; import { DomHandler, ObjectUtils, UniqueComponentId, classNames } from '../utils/Utils'; import { VirtualScroller } from '../virtualscroller/VirtualScroller'; @@ -15,12 +16,14 @@ export const DataTable = React.forwardRef((props, ref) => { const [sortOrderState, setSortOrderState] = React.useState(props.sortOrder); const [multiSortMetaState, setMultiSortMetaState] = React.useState(props.multiSortMeta); const [filtersState, setFiltersState] = React.useState(props.filters); + const [scrollableState, setScrollableState] = React.useState(props.scrollable); const [columnOrderState, setColumnOrderState] = React.useState([]); const [groupRowsSortMetaState, setGroupRowsSortMetaState] = React.useState(null); const [editingMetaState, setEditingMetaState] = React.useState({}); const [attributeSelectorState, setAttributeSelectorState] = React.useState(null); const [d_rowsState, setD_rowsState] = React.useState(props.rows); const [d_filtersState, setD_filtersState] = React.useState({}); + const matchesResponsiveBreakpoint = useMediaQuery(`(max-width: ${props.breakpoint})`); const elementRef = React.useRef(null); const tableRef = React.useRef(null); const wrapperRef = React.useRef(null); @@ -81,7 +84,7 @@ export const DataTable = React.forwardRef((props, ref) => { }; const isVirtualScrollerDisabled = () => { - return ObjectUtils.isEmpty(props.virtualScrollerOptions) || !props.scrollable; + return ObjectUtils.isEmpty(props.virtualScrollerOptions) || !scrollableState; }; const isEquals = (data1, data2) => { @@ -367,7 +370,7 @@ export const DataTable = React.forwardRef((props, ref) => { let innerHTML = ''; widths.forEach((width, index) => { - let style = props.scrollable ? `flex: 1 1 ${width}px !important` : `width: ${width}px !important`; + let style = scrollableState ? `flex: 1 1 ${width}px !important` : `width: ${width}px !important`; innerHTML += ` .p-datatable[${attributeSelectorState}] .p-datatable-thead > tr > th:nth-child(${index + 1}), @@ -563,7 +566,7 @@ export const DataTable = React.forwardRef((props, ref) => { widths.forEach((width, index) => { let colWidth = index === colIndex ? newColumnWidth : nextColumnWidth && index === colIndex + 1 ? nextColumnWidth : width; - let style = props.scrollable ? `flex: 1 1 ${colWidth}px !important` : `width: ${colWidth}px !important`; + let style = scrollableState ? `flex: 1 1 ${colWidth}px !important` : `width: ${colWidth}px !important`; innerHTML += ` .p-datatable[${attributeSelectorState}] .p-datatable-thead > tr > th:nth-child(${index + 1}), @@ -1308,14 +1311,6 @@ export const DataTable = React.forwardRef((props, ref) => { } }); - useUpdateEffect(() => { - elementRef.current.setAttribute(attributeSelectorState, ''); - - if (props.responsiveLayout === 'stack' && !props.scrollable) { - createResponsiveStyle(); - } - }, [attributeSelectorState]); - useUpdateEffect(() => { const filters = cloneFilters(props.filters); @@ -1334,12 +1329,25 @@ export const DataTable = React.forwardRef((props, ref) => { }); useUpdateEffect(() => { + elementRef.current.setAttribute(attributeSelectorState, ''); + destroyResponsiveStyle(); - if (props.responsiveLayout === 'stack' && !props.scrollable) { + if (props.responsiveLayout === 'stack') { createResponsiveStyle(); } - }, [props.responsiveLayout, props.scrollable]); + }, [props.responsiveLayout, attributeSelectorState]); + + useUpdateEffect(() => { + let isScrollable = props.scrollable; + + // #3693 disable scrolling if responsive stack breakpoint is hit + if (props.responsiveLayout === 'stack' && isScrollable) { + isScrollable = !matchesResponsiveBreakpoint; + } + + setScrollableState(isScrollable); + }, [props.responsiveLayout, props.scrollable, matchesResponsiveBreakpoint]); useUpdateEffect(() => { if (props.globalFilter) { @@ -1492,7 +1500,7 @@ export const DataTable = React.forwardRef((props, ref) => { editingRows={props.editingRows} onRowReorder={props.onRowReorder} reorderableRows={props.reorderableRows} - scrollable={props.scrollable} + scrollable={scrollableState} rowGroupMode={props.rowGroupMode} groupRowsBy={props.groupRowsBy} expandableRowGroups={props.expandableRowGroups} @@ -1566,7 +1574,7 @@ export const DataTable = React.forwardRef((props, ref) => { editingRows={props.editingRows} onRowReorder={props.onRowReorder} reorderableRows={props.reorderableRows} - scrollable={props.scrollable} + scrollable={scrollableState} rowGroupMode={props.rowGroupMode} groupRowsBy={props.groupRowsBy} expandableRowGroups={props.expandableRowGroups} @@ -1741,11 +1749,11 @@ export const DataTable = React.forwardRef((props, ref) => { 'p-datatable-auto-layout': props.autoLayout, 'p-datatable-resizable': props.resizableColumns, 'p-datatable-resizable-fit': props.resizableColumns && props.columnResizeMode === 'fit', - 'p-datatable-scrollable': props.scrollable, - 'p-datatable-scrollable-vertical': props.scrollable && props.scrollDirection === 'vertical', - 'p-datatable-scrollable-horizontal': props.scrollable && props.scrollDirection === 'horizontal', - 'p-datatable-scrollable-both': props.scrollable && props.scrollDirection === 'both', - 'p-datatable-flex-scrollable': props.scrollable && props.scrollHeight === 'flex', + 'p-datatable-scrollable': scrollableState, + 'p-datatable-scrollable-vertical': scrollableState && props.scrollDirection === 'vertical', + 'p-datatable-scrollable-horizontal': scrollableState && props.scrollDirection === 'horizontal', + 'p-datatable-scrollable-both': scrollableState && props.scrollDirection === 'both', + 'p-datatable-flex-scrollable': scrollableState && props.scrollHeight === 'flex', 'p-datatable-responsive-stack': props.responsiveLayout === 'stack', 'p-datatable-responsive-scroll': props.responsiveLayout === 'scroll', 'p-datatable-striped': props.stripedRows, diff --git a/components/lib/hooks/Hooks.js b/components/lib/hooks/Hooks.js index 0327f1710e..082a188120 100644 --- a/components/lib/hooks/Hooks.js +++ b/components/lib/hooks/Hooks.js @@ -1,13 +1,14 @@ -import { usePrevious } from './usePrevious'; -import { useMountEffect } from './useMountEffect'; -import { useUpdateEffect } from './useUpdateEffect'; -import { useUnmountEffect } from './useUnmountEffect'; import { useEventListener } from './useEventListener'; +import { useInterval } from './useInterval'; +import { useMediaQuery } from './useMediaQuery'; +import { useMountEffect } from './useMountEffect'; import { useOverlayListener } from './useOverlayListener'; import { useOverlayScrollListener } from './useOverlayScrollListener'; +import { usePrevious } from './usePrevious'; import { useResizeListener } from './useResizeListener'; -import { useInterval } from './useInterval'; -import { useStorage, useLocalStorage, useSessionStorage } from './useStorage'; +import { useLocalStorage, useSessionStorage, useStorage } from './useStorage'; import { useTimeout } from './useTimeout'; +import { useUnmountEffect } from './useUnmountEffect'; +import { useUpdateEffect } from './useUpdateEffect'; -export { usePrevious, useMountEffect, useUpdateEffect, useUnmountEffect, useEventListener, useOverlayListener, useOverlayScrollListener, useResizeListener, useInterval, useStorage, useLocalStorage, useSessionStorage, useTimeout }; +export { usePrevious, useMediaQuery, useMountEffect, useUpdateEffect, useUnmountEffect, useEventListener, useOverlayListener, useOverlayScrollListener, useResizeListener, useInterval, useStorage, useLocalStorage, useSessionStorage, useTimeout }; diff --git a/components/lib/hooks/hooks.d.ts b/components/lib/hooks/hooks.d.ts index 01aa810bf9..640ab52210 100644 --- a/components/lib/hooks/hooks.d.ts +++ b/components/lib/hooks/hooks.d.ts @@ -27,6 +27,7 @@ export declare function useMountEffect(effect: React.EffectCallback): void; export declare function useUpdateEffect(effect: React.EffectCallback, deps?: React.DependencyList): void; export declare function useUnmountEffect(effect: React.EffectCallback): void; export declare function useEventListener(options: EventOptions): any[]; +export declare function useMediaQuery(query: string, initialValue?: boolean): boolean; export declare function useOverlayListener(options: OverlayEventOptions): any[]; export declare function useOverlayScrollListener(options: EventOptions): any[]; export declare function useResizeListener(options: ResizeEventOptions): any[]; diff --git a/components/lib/hooks/useMediaQuery.js b/components/lib/hooks/useMediaQuery.js new file mode 100644 index 0000000000..79cf4548a4 --- /dev/null +++ b/components/lib/hooks/useMediaQuery.js @@ -0,0 +1,44 @@ +import { useEffect, useRef, useState } from 'react'; + +/** + * Hook to notify when media queries are satisfied. + * + * @param {*} query the media query like '(min-width: 900px)' + * @param {*} initialValue override the initial value if you don't want it detected + * @returns boolean if the media query matches + */ +export function useMediaQuery(query, initialValue) { + const [matches, setMatches] = useState(getInitialValue(query, initialValue)); + const queryRef = useRef(); + + useEffect(() => { + if ('matchMedia' in window) { + queryRef.current = window.matchMedia(query); + setMatches(queryRef.current.matches); + + return attachMediaListener(queryRef.current, (event) => setMatches(event.matches)); + } + + return undefined; + }, [query]); + + return matches; +} + +function attachMediaListener(query, callback) { + query.addEventListener('change', callback); + + return () => query.removeEventListener('change', callback); +} + +function getInitialValue(query, initialValue) { + if (typeof initialValue === 'boolean') { + return initialValue; + } + + if (typeof window !== 'undefined' && 'matchMedia' in window) { + return window.matchMedia(query).matches; + } + + return false; +}