From 7d7be62280403740dd08d3d297ca68a74d5c7069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ula=C5=9F=20Turan?= Date: Tue, 4 Jul 2023 18:02:55 +0300 Subject: [PATCH] Refactor #4602 - Add unstyled feature --- components/lib/api/PrimeReactContext.js | 5 +- components/lib/api/api.d.ts | 1 + components/lib/componentbase/ComponentBase.js | 46 +++++++++++++-- components/lib/hooks/Hooks.js | 2 + components/lib/hooks/hooks.d.ts | 46 +++++++++++++++ components/lib/hooks/useStyle.js | 59 +++++++++++++++++++ components/lib/utils/DomHandler.js | 4 ++ components/lib/utils/utils.d.ts | 1 + 8 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 components/lib/hooks/useStyle.js diff --git a/components/lib/api/PrimeReactContext.js b/components/lib/api/PrimeReactContext.js index be605deccb..75b5998a39 100644 --- a/components/lib/api/PrimeReactContext.js +++ b/components/lib/api/PrimeReactContext.js @@ -21,6 +21,7 @@ export const PrimeReactProvider = (props) => { toast: 1200 }); const [pt, setPt] = useState(undefined); + const [unstyled, setUnstyled] = useState(false); const [filterMatchModeOptions, setFilterMatchModeOptions] = useState({ text: [FilterMatchMode.STARTS_WITH, FilterMatchMode.CONTAINS, FilterMatchMode.NOT_CONTAINS, FilterMatchMode.ENDS_WITH, FilterMatchMode.EQUALS, FilterMatchMode.NOT_EQUALS], numeric: [FilterMatchMode.EQUALS, FilterMatchMode.NOT_EQUALS, FilterMatchMode.LESS_THAN, FilterMatchMode.LESS_THAN_OR_EQUAL_TO, FilterMatchMode.GREATER_THAN, FilterMatchMode.GREATER_THAN_OR_EQUAL_TO], @@ -70,7 +71,9 @@ export const PrimeReactProvider = (props) => { pt, setPt, filterMatchModeOptions, - setFilterMatchModeOptions + setFilterMatchModeOptions, + unstyled, + setUnstyled }; return {props.children}; diff --git a/components/lib/api/api.d.ts b/components/lib/api/api.d.ts index 778aa9a4f5..b5a89cd176 100644 --- a/components/lib/api/api.d.ts +++ b/components/lib/api/api.d.ts @@ -206,6 +206,7 @@ export interface APIOptions { * This option allows to direct implementation of all relevant attributes (e.g., style, classnames) within the respective HTML tag. */ pt?: PrimeReactPTOptions; + unstyled?: boolean; /** * This method is used to change the theme dynamically. * @param {string} theme - The name of the theme to be applied. diff --git a/components/lib/componentbase/ComponentBase.js b/components/lib/componentbase/ComponentBase.js index 16eddd9453..4897ca4dae 100644 --- a/components/lib/componentbase/ComponentBase.js +++ b/components/lib/componentbase/ComponentBase.js @@ -3,11 +3,27 @@ import { ObjectUtils } from '../utils/Utils'; export const ComponentBase = { defaultProps: { - pt: undefined + pt: undefined, + unstyled: false }, context: undefined, + classes: {}, + styles: "", extend: (props = {}) => { + const css = props.css; const defaultProps = { ...props.defaultProps, ...ComponentBase.defaultProps }; + const inlineStyles = { + hiddenAccessible: { + border: '0', + clip: 'rect(0 0 0 0)', + height: '1px', + margin: '-1px', + overflow: 'hidden', + padding: '0', + position: 'absolute', + width: '1px' + } + }; const getProps = (props, context = {}) => { ComponentBase.context = context; @@ -29,7 +45,6 @@ export const ComponentBase = { const datasetPrefix = 'data-pc-'; const componentName = (params.props && params.props.__TYPE && ObjectUtils.convertToFlatCase(params.props.__TYPE)) || ''; const pt = ComponentBase.context.pt || PrimeReact.pt || {}; - const defaultPT = (key) => pt && getOptionValue(pt[componentName], key); const self = ObjectUtils.getPropValue(obj, key, params)[key]; const globalPT = defaultPT(key); @@ -53,10 +68,33 @@ export const ComponentBase = { }; const setMetaData = (metadata = {}) => { - const ptm = (key = '', params = {}) => ptmo((metadata.props || {}).pt, key, { ...metadata, ...params }); + const { props, state } = metadata; + const ptm = (key = '', params = {}) => ptmo((props || {}).pt, key, { ...metadata, ...params }); const ptmo = (obj = {}, key = '', params = {}) => getPTValue(obj, key, params); - return { ptm, ptmo }; + const isUnstyled = () => { + return ComponentBase.context.unstyled || PrimeReact.unstyled || props.unstyled; + }; + + const cx = (key = '', params = {}) => { + return !isUnstyled() ? getOptionValue(css && css.classes, key, { props, state, ...params }) : undefined; + }; + + const sx = (key = '', when = true, params = {}) => { + if (when) { + const self = getOptionValue(css && css.inlineStyles, key, { props, state, ...params }); + const base = getOptionValue(inlineStyles, key, { props: props || {}, state, ...params }); + let merged = { + ...ObjectUtils.getMergedProps(base, self) + }; + + return merged; + } + + return undefined; + } + + return { ptm, ptmo, sx, cx }; }; return { diff --git a/components/lib/hooks/Hooks.js b/components/lib/hooks/Hooks.js index b4305c05d8..28e6b2875c 100644 --- a/components/lib/hooks/Hooks.js +++ b/components/lib/hooks/Hooks.js @@ -17,6 +17,7 @@ import { useLocalStorage, useSessionStorage, useStorage } from './useStorage'; import { useTimeout } from './useTimeout'; import { useUnmountEffect } from './useUnmountEffect'; import { useUpdateEffect } from './useUpdateEffect'; +import { useStyle } from './useStyle'; export { usePrevious, @@ -30,6 +31,7 @@ export { useIntersectionObserver, useInterval, useStorage, + useStyle, useLocalStorage, useSessionStorage, useTimeout, diff --git a/components/lib/hooks/hooks.d.ts b/components/lib/hooks/hooks.d.ts index aeda7abfcc..84cc4c2a08 100644 --- a/components/lib/hooks/hooks.d.ts +++ b/components/lib/hooks/hooks.d.ts @@ -21,6 +21,45 @@ interface MousePositionOptions { y: number; } +/** + * Custom UseStyleOptions + */ +interface UseStyleOptions { + document?: Document; + immediate: boolean; + manual: boolean; + name: string; + media: string; +} + +/** + * Custom StyleOptions + */ +interface StyleOptions { + /** + * Defines data-pc-name attribute of the style tag. + */ + name: string; + /** + * The css object. + */ + css: React.RefObject; + /** + * This option is used to load the style tag by the name. + * @returns {void} + */ + load: () => void; + /** + * This method is used to remove the style tag from the head. + * @returns {void} + */ + unload: () => void; + /** + * Whether the style is loaded or not. + */ + isLoaded: boolean; +} + /** * Custom MouseDataOptions */ @@ -220,6 +259,13 @@ export declare function useMouse(): MouseDataOptions; * @param {MousePositionOptions} initialValue - The initial value. */ export declare function useMove(mode: 'horizontal' | 'vertical' | 'both', initialValue: MousePositionOptions): MouseMoveOptions; + +/** + * Custom hook to use to get style options. + * @param {string} css - The style text content. + * @param {UseStyleOptions} options - The options of the style. + */ +export declare function useStyle(css: string, options?: UseStyleOptions): StyleOptions; /** * Custom hook to use change the current favicon. * @param {string} newIcon - The new favicon url to set. diff --git a/components/lib/hooks/useStyle.js b/components/lib/hooks/useStyle.js new file mode 100644 index 0000000000..d883d00aa2 --- /dev/null +++ b/components/lib/hooks/useStyle.js @@ -0,0 +1,59 @@ +import { useEffect, useRef, useState } from 'react'; +import { DomHandler, ObjectUtils } from "../utils/Utils"; + +let _id = 0; + +export const useStyle = (css = {}, options = {}) => { + const [isLoaded, setIsLoaded] = useState(false); + + const cssRef = useRef(null); + const defaultDocument = DomHandler.isClient() ? window.document : undefined; + const { document = defaultDocument, immediate = true, manual = false, name = `primereact_style_${++_id}`, media } = options; + + useEffect(() => { + cssRef.current = css; + }, [css]); + + const load = () => { + if (!document) return; + + const el = document.querySelector(`[data-pc-name="${name}"]`) || document.createElement('style'); + + if (ObjectUtils.isNotEmpty(el) || !el.isConnected) { + el.type = 'text/css'; + el.setAttribute('data-pc-name', name); + if (media) el.media = media; + document.head.appendChild(el); + } + + if (isLoaded) return; + + el.textContent = cssRef.current; + setIsLoaded(true); + }; + + const unload = () => { + if (!document || !isLoaded) return; + const node = document.querySelector(`[data-pc-name="${name}"]`); + + if (node && node.isConnected) { + document.head.removeChild(node); + setIsLoaded(false); + } + }; + + useEffect(() => { + if (immediate && !manual) load(); + + return () => unload(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [immediate, manual]); + + return { + name, + css: cssRef, + unload, + load, + isLoaded + }; +} diff --git a/components/lib/utils/DomHandler.js b/components/lib/utils/DomHandler.js index 82f65670f6..64e1364bd3 100644 --- a/components/lib/utils/DomHandler.js +++ b/components/lib/utils/DomHandler.js @@ -669,6 +669,10 @@ export default class DomHandler { return /(chrome)/i.test(navigator.userAgent); } + static isClient() { + return !!(typeof window !== 'undefined' && window.document && window.document.createElement); + } + static isTouchDevice() { return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; } diff --git a/components/lib/utils/utils.d.ts b/components/lib/utils/utils.d.ts index 13232cbd7a..fac45ac245 100644 --- a/components/lib/utils/utils.d.ts +++ b/components/lib/utils/utils.d.ts @@ -59,6 +59,7 @@ export declare class DomHandler { static getUserAgent(): string; static isIOS(): boolean; static isAndroid(): boolean; + static isClient(): boolean; static isTouchDevice(): boolean; static isFunction(obj: any): boolean; static appendChild(el: HTMLElement, target: HTMLElement): void;