fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

View File

@@ -0,0 +1,27 @@
/**
* WordPress dependencies
*/
import { createContext, useContext } from '@wordpress/element';
/**
* Internal dependencies
*/
const noop = () => undefined;
export const ToolsPanelContext = createContext({
menuItems: {
default: {},
optional: {}
},
hasMenuItems: false,
isResetting: false,
shouldRenderPlaceholderItems: false,
registerPanelItem: noop,
deregisterPanelItem: noop,
flagItemCustomization: noop,
registerResetAllFilter: noop,
deregisterResetAllFilter: noop,
areAllOptionalControlsHidden: true
});
export const useToolsPanelContext = () => useContext(ToolsPanelContext);
//# sourceMappingURL=context.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["createContext","useContext","noop","undefined","ToolsPanelContext","menuItems","default","optional","hasMenuItems","isResetting","shouldRenderPlaceholderItems","registerPanelItem","deregisterPanelItem","flagItemCustomization","registerResetAllFilter","deregisterResetAllFilter","areAllOptionalControlsHidden","useToolsPanelContext"],"sources":["@wordpress/components/src/tools-panel/context.ts"],"sourcesContent":["/**\n * WordPress dependencies\n */\nimport { createContext, useContext } from '@wordpress/element';\n\n/**\n * Internal dependencies\n */\nimport type { ToolsPanelContext as ToolsPanelContextType } from './types';\n\nconst noop = () => undefined;\n\nexport const ToolsPanelContext = createContext< ToolsPanelContextType >( {\n\tmenuItems: { default: {}, optional: {} },\n\thasMenuItems: false,\n\tisResetting: false,\n\tshouldRenderPlaceholderItems: false,\n\tregisterPanelItem: noop,\n\tderegisterPanelItem: noop,\n\tflagItemCustomization: noop,\n\tregisterResetAllFilter: noop,\n\tderegisterResetAllFilter: noop,\n\tareAllOptionalControlsHidden: true,\n} );\n\nexport const useToolsPanelContext = () =>\n\tuseContext< ToolsPanelContextType >( ToolsPanelContext );\n"],"mappings":"AAAA;AACA;AACA;AACA,SAASA,aAAa,EAAEC,UAAU,QAAQ,oBAAoB;;AAE9D;AACA;AACA;;AAGA,MAAMC,IAAI,GAAGA,CAAA,KAAMC,SAAS;AAE5B,OAAO,MAAMC,iBAAiB,GAAGJ,aAAa,CAA2B;EACxEK,SAAS,EAAE;IAAEC,OAAO,EAAE,CAAC,CAAC;IAAEC,QAAQ,EAAE,CAAC;EAAE,CAAC;EACxCC,YAAY,EAAE,KAAK;EACnBC,WAAW,EAAE,KAAK;EAClBC,4BAA4B,EAAE,KAAK;EACnCC,iBAAiB,EAAET,IAAI;EACvBU,mBAAmB,EAAEV,IAAI;EACzBW,qBAAqB,EAAEX,IAAI;EAC3BY,sBAAsB,EAAEZ,IAAI;EAC5Ba,wBAAwB,EAAEb,IAAI;EAC9Bc,4BAA4B,EAAE;AAC/B,CAAE,CAAC;AAEH,OAAO,MAAMC,oBAAoB,GAAGA,CAAA,KACnChB,UAAU,CAA2BG,iBAAkB,CAAC"}

View File

@@ -0,0 +1,4 @@
export { default as ToolsPanel } from './tools-panel';
export { default as ToolsPanelItem } from './tools-panel-item';
export { ToolsPanelContext } from './context';
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["default","ToolsPanel","ToolsPanelItem","ToolsPanelContext"],"sources":["@wordpress/components/src/tools-panel/index.ts"],"sourcesContent":["export { default as ToolsPanel } from './tools-panel';\nexport { default as ToolsPanelItem } from './tools-panel-item';\nexport { ToolsPanelContext } from './context';\n"],"mappings":"AAAA,SAASA,OAAO,IAAIC,UAAU,QAAQ,eAAe;AACrD,SAASD,OAAO,IAAIE,cAAc,QAAQ,oBAAoB;AAC9D,SAASC,iBAAiB,QAAQ,WAAW"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,166 @@
import { createElement, Fragment } from "react";
/**
* External dependencies
*/
/**
* WordPress dependencies
*/
import { speak } from '@wordpress/a11y';
import { check, moreVertical, plus } from '@wordpress/icons';
import { __, _x, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import DropdownMenu from '../../dropdown-menu';
import MenuGroup from '../../menu-group';
import MenuItem from '../../menu-item';
import { HStack } from '../../h-stack';
import { Heading } from '../../heading';
import { useToolsPanelHeader } from './hook';
import { contextConnect } from '../../context';
import { ResetLabel } from '../styles';
const DefaultControlsGroup = ({
itemClassName,
items,
toggleItem
}) => {
if (!items.length) {
return null;
}
const resetSuffix = createElement(ResetLabel, {
"aria-hidden": true
}, __('Reset'));
return createElement(Fragment, null, items.map(([label, hasValue]) => {
if (hasValue) {
return createElement(MenuItem, {
key: label,
className: itemClassName,
role: "menuitem",
label: sprintf(
// translators: %s: The name of the control being reset e.g. "Padding".
__('Reset %s'), label),
onClick: () => {
toggleItem(label);
speak(sprintf(
// translators: %s: The name of the control being reset e.g. "Padding".
__('%s reset to default'), label), 'assertive');
},
suffix: resetSuffix
}, label);
}
return createElement(MenuItem, {
key: label,
icon: check,
className: itemClassName,
role: "menuitemcheckbox",
isSelected: true,
"aria-disabled": true
}, label);
}));
};
const OptionalControlsGroup = ({
items,
toggleItem
}) => {
if (!items.length) {
return null;
}
return createElement(Fragment, null, items.map(([label, isSelected]) => {
const itemLabel = isSelected ? sprintf(
// translators: %s: The name of the control being hidden and reset e.g. "Padding".
__('Hide and reset %s'), label) : sprintf(
// translators: %s: The name of the control to display e.g. "Padding".
__('Show %s'), label);
return createElement(MenuItem, {
key: label,
icon: isSelected ? check : null,
isSelected: isSelected,
label: itemLabel,
onClick: () => {
if (isSelected) {
speak(sprintf(
// translators: %s: The name of the control being reset e.g. "Padding".
__('%s hidden and reset to default'), label), 'assertive');
} else {
speak(sprintf(
// translators: %s: The name of the control being reset e.g. "Padding".
__('%s is now visible'), label), 'assertive');
}
toggleItem(label);
},
role: "menuitemcheckbox"
}, label);
}));
};
const ToolsPanelHeader = (props, forwardedRef) => {
const {
areAllOptionalControlsHidden,
defaultControlsItemClassName,
dropdownMenuClassName,
hasMenuItems,
headingClassName,
headingLevel = 2,
label: labelText,
menuItems,
resetAll,
toggleItem,
dropdownMenuProps,
...headerProps
} = useToolsPanelHeader(props);
if (!labelText) {
return null;
}
const defaultItems = Object.entries(menuItems?.default || {});
const optionalItems = Object.entries(menuItems?.optional || {});
const dropDownMenuIcon = areAllOptionalControlsHidden ? plus : moreVertical;
const dropDownMenuLabelText = sprintf(
// translators: %s: The name of the tool e.g. "Color" or "Typography".
_x('%s options', 'Button label to reveal tool panel options'), labelText);
const dropdownMenuDescriptionText = areAllOptionalControlsHidden ? __('All options are currently hidden') : undefined;
const canResetAll = [...defaultItems, ...optionalItems].some(([, isSelected]) => isSelected);
return createElement(HStack, {
...headerProps,
ref: forwardedRef
}, createElement(Heading, {
level: headingLevel,
className: headingClassName
}, labelText), hasMenuItems && createElement(DropdownMenu, {
...dropdownMenuProps,
icon: dropDownMenuIcon,
label: dropDownMenuLabelText,
menuProps: {
className: dropdownMenuClassName
},
toggleProps: {
isSmall: true,
describedBy: dropdownMenuDescriptionText
}
}, () => createElement(Fragment, null, createElement(MenuGroup, {
label: labelText
}, createElement(DefaultControlsGroup, {
items: defaultItems,
toggleItem: toggleItem,
itemClassName: defaultControlsItemClassName
}), createElement(OptionalControlsGroup, {
items: optionalItems,
toggleItem: toggleItem
})), createElement(MenuGroup, null, createElement(MenuItem, {
"aria-disabled": !canResetAll
// @ts-expect-error - TODO: If this "tertiary" style is something we really want to allow on MenuItem,
// we should rename it and explicitly allow it as an official API. All the other Button variants
// don't make sense in a MenuItem context, and should be disallowed.
,
variant: 'tertiary',
onClick: () => {
if (canResetAll) {
resetAll();
speak(__('All options reset'), 'assertive');
}
}
}, __('Reset all'))))));
};
const ConnectedToolsPanelHeader = contextConnect(ToolsPanelHeader, 'ToolsPanelHeader');
export default ConnectedToolsPanelHeader;
//# sourceMappingURL=component.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,49 @@
/**
* WordPress dependencies
*/
import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
import * as styles from '../styles';
import { useToolsPanelContext } from '../context';
import { useContextSystem } from '../../context';
import { useCx } from '../../utils/hooks/use-cx';
export function useToolsPanelHeader(props) {
const {
className,
headingLevel = 2,
...otherProps
} = useContextSystem(props, 'ToolsPanelHeader');
const cx = useCx();
const classes = useMemo(() => {
return cx(styles.ToolsPanelHeader, className);
}, [className, cx]);
const dropdownMenuClassName = useMemo(() => {
return cx(styles.DropdownMenu);
}, [cx]);
const headingClassName = useMemo(() => {
return cx(styles.ToolsPanelHeading);
}, [cx]);
const defaultControlsItemClassName = useMemo(() => {
return cx(styles.DefaultControlsItem);
}, [cx]);
const {
menuItems,
hasMenuItems,
areAllOptionalControlsHidden
} = useToolsPanelContext();
return {
...otherProps,
areAllOptionalControlsHidden,
defaultControlsItemClassName,
dropdownMenuClassName,
hasMenuItems,
headingClassName,
headingLevel,
menuItems,
className: classes
};
}
//# sourceMappingURL=hook.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["useMemo","styles","useToolsPanelContext","useContextSystem","useCx","useToolsPanelHeader","props","className","headingLevel","otherProps","cx","classes","ToolsPanelHeader","dropdownMenuClassName","DropdownMenu","headingClassName","ToolsPanelHeading","defaultControlsItemClassName","DefaultControlsItem","menuItems","hasMenuItems","areAllOptionalControlsHidden"],"sources":["@wordpress/components/src/tools-panel/tools-panel-header/hook.ts"],"sourcesContent":["/**\n * WordPress dependencies\n */\nimport { useMemo } from '@wordpress/element';\n\n/**\n * Internal dependencies\n */\nimport * as styles from '../styles';\nimport { useToolsPanelContext } from '../context';\nimport type { WordPressComponentProps } from '../../context';\nimport { useContextSystem } from '../../context';\nimport { useCx } from '../../utils/hooks/use-cx';\nimport type { ToolsPanelHeaderProps } from '../types';\n\nexport function useToolsPanelHeader(\n\tprops: WordPressComponentProps< ToolsPanelHeaderProps, 'h2' >\n) {\n\tconst {\n\t\tclassName,\n\t\theadingLevel = 2,\n\t\t...otherProps\n\t} = useContextSystem( props, 'ToolsPanelHeader' );\n\n\tconst cx = useCx();\n\tconst classes = useMemo( () => {\n\t\treturn cx( styles.ToolsPanelHeader, className );\n\t}, [ className, cx ] );\n\n\tconst dropdownMenuClassName = useMemo( () => {\n\t\treturn cx( styles.DropdownMenu );\n\t}, [ cx ] );\n\n\tconst headingClassName = useMemo( () => {\n\t\treturn cx( styles.ToolsPanelHeading );\n\t}, [ cx ] );\n\n\tconst defaultControlsItemClassName = useMemo( () => {\n\t\treturn cx( styles.DefaultControlsItem );\n\t}, [ cx ] );\n\n\tconst { menuItems, hasMenuItems, areAllOptionalControlsHidden } =\n\t\tuseToolsPanelContext();\n\n\treturn {\n\t\t...otherProps,\n\t\tareAllOptionalControlsHidden,\n\t\tdefaultControlsItemClassName,\n\t\tdropdownMenuClassName,\n\t\thasMenuItems,\n\t\theadingClassName,\n\t\theadingLevel,\n\t\tmenuItems,\n\t\tclassName: classes,\n\t};\n}\n"],"mappings":"AAAA;AACA;AACA;AACA,SAASA,OAAO,QAAQ,oBAAoB;;AAE5C;AACA;AACA;AACA,OAAO,KAAKC,MAAM,MAAM,WAAW;AACnC,SAASC,oBAAoB,QAAQ,YAAY;AAEjD,SAASC,gBAAgB,QAAQ,eAAe;AAChD,SAASC,KAAK,QAAQ,0BAA0B;AAGhD,OAAO,SAASC,mBAAmBA,CAClCC,KAA6D,EAC5D;EACD,MAAM;IACLC,SAAS;IACTC,YAAY,GAAG,CAAC;IAChB,GAAGC;EACJ,CAAC,GAAGN,gBAAgB,CAAEG,KAAK,EAAE,kBAAmB,CAAC;EAEjD,MAAMI,EAAE,GAAGN,KAAK,CAAC,CAAC;EAClB,MAAMO,OAAO,GAAGX,OAAO,CAAE,MAAM;IAC9B,OAAOU,EAAE,CAAET,MAAM,CAACW,gBAAgB,EAAEL,SAAU,CAAC;EAChD,CAAC,EAAE,CAAEA,SAAS,EAAEG,EAAE,CAAG,CAAC;EAEtB,MAAMG,qBAAqB,GAAGb,OAAO,CAAE,MAAM;IAC5C,OAAOU,EAAE,CAAET,MAAM,CAACa,YAAa,CAAC;EACjC,CAAC,EAAE,CAAEJ,EAAE,CAAG,CAAC;EAEX,MAAMK,gBAAgB,GAAGf,OAAO,CAAE,MAAM;IACvC,OAAOU,EAAE,CAAET,MAAM,CAACe,iBAAkB,CAAC;EACtC,CAAC,EAAE,CAAEN,EAAE,CAAG,CAAC;EAEX,MAAMO,4BAA4B,GAAGjB,OAAO,CAAE,MAAM;IACnD,OAAOU,EAAE,CAAET,MAAM,CAACiB,mBAAoB,CAAC;EACxC,CAAC,EAAE,CAAER,EAAE,CAAG,CAAC;EAEX,MAAM;IAAES,SAAS;IAAEC,YAAY;IAAEC;EAA6B,CAAC,GAC9DnB,oBAAoB,CAAC,CAAC;EAEvB,OAAO;IACN,GAAGO,UAAU;IACbY,4BAA4B;IAC5BJ,4BAA4B;IAC5BJ,qBAAqB;IACrBO,YAAY;IACZL,gBAAgB;IAChBP,YAAY;IACZW,SAAS;IACTZ,SAAS,EAAEI;EACZ,CAAC;AACF"}

View File

@@ -0,0 +1,2 @@
export { default } from './component';
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["default"],"sources":["@wordpress/components/src/tools-panel/tools-panel-header/index.ts"],"sourcesContent":["export { default } from './component';\n"],"mappings":"AAAA,SAASA,OAAO,QAAQ,aAAa"}

View File

@@ -0,0 +1,34 @@
import { createElement } from "react";
/**
* External dependencies
*/
/**
* Internal dependencies
*/
import { useToolsPanelItem } from './hook';
import { View } from '../../view';
import { contextConnect } from '../../context';
// This wraps controls to be conditionally displayed within a tools panel. It
// prevents props being applied to HTML elements that would make them invalid.
const UnconnectedToolsPanelItem = (props, forwardedRef) => {
const {
children,
isShown,
shouldRenderPlaceholder,
...toolsPanelItemProps
} = useToolsPanelItem(props);
if (!isShown) {
return shouldRenderPlaceholder ? createElement(View, {
...toolsPanelItemProps,
ref: forwardedRef
}) : null;
}
return createElement(View, {
...toolsPanelItemProps,
ref: forwardedRef
}, children);
};
export const ToolsPanelItem = contextConnect(UnconnectedToolsPanelItem, 'ToolsPanelItem');
export default ToolsPanelItem;
//# sourceMappingURL=component.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["useToolsPanelItem","View","contextConnect","UnconnectedToolsPanelItem","props","forwardedRef","children","isShown","shouldRenderPlaceholder","toolsPanelItemProps","createElement","ref","ToolsPanelItem"],"sources":["@wordpress/components/src/tools-panel/tools-panel-item/component.tsx"],"sourcesContent":["/**\n * External dependencies\n */\nimport type { ForwardedRef } from 'react';\n\n/**\n * Internal dependencies\n */\nimport { useToolsPanelItem } from './hook';\nimport { View } from '../../view';\nimport type { WordPressComponentProps } from '../../context';\nimport { contextConnect } from '../../context';\nimport type { ToolsPanelItemProps } from '../types';\n\n// This wraps controls to be conditionally displayed within a tools panel. It\n// prevents props being applied to HTML elements that would make them invalid.\nconst UnconnectedToolsPanelItem = (\n\tprops: WordPressComponentProps< ToolsPanelItemProps, 'div' >,\n\tforwardedRef: ForwardedRef< any >\n) => {\n\tconst {\n\t\tchildren,\n\t\tisShown,\n\t\tshouldRenderPlaceholder,\n\t\t...toolsPanelItemProps\n\t} = useToolsPanelItem( props );\n\n\tif ( ! isShown ) {\n\t\treturn shouldRenderPlaceholder ? (\n\t\t\t<View { ...toolsPanelItemProps } ref={ forwardedRef } />\n\t\t) : null;\n\t}\n\n\treturn (\n\t\t<View { ...toolsPanelItemProps } ref={ forwardedRef }>\n\t\t\t{ children }\n\t\t</View>\n\t);\n};\n\nexport const ToolsPanelItem = contextConnect(\n\tUnconnectedToolsPanelItem,\n\t'ToolsPanelItem'\n);\n\nexport default ToolsPanelItem;\n"],"mappings":";AAAA;AACA;AACA;;AAGA;AACA;AACA;AACA,SAASA,iBAAiB,QAAQ,QAAQ;AAC1C,SAASC,IAAI,QAAQ,YAAY;AAEjC,SAASC,cAAc,QAAQ,eAAe;AAG9C;AACA;AACA,MAAMC,yBAAyB,GAAGA,CACjCC,KAA4D,EAC5DC,YAAiC,KAC7B;EACJ,MAAM;IACLC,QAAQ;IACRC,OAAO;IACPC,uBAAuB;IACvB,GAAGC;EACJ,CAAC,GAAGT,iBAAiB,CAAEI,KAAM,CAAC;EAE9B,IAAK,CAAEG,OAAO,EAAG;IAChB,OAAOC,uBAAuB,GAC7BE,aAAA,CAACT,IAAI;MAAA,GAAMQ,mBAAmB;MAAGE,GAAG,EAAGN;IAAc,CAAE,CAAC,GACrD,IAAI;EACT;EAEA,OACCK,aAAA,CAACT,IAAI;IAAA,GAAMQ,mBAAmB;IAAGE,GAAG,EAAGN;EAAc,GAClDC,QACG,CAAC;AAET,CAAC;AAED,OAAO,MAAMM,cAAc,GAAGV,cAAc,CAC3CC,yBAAyB,EACzB,gBACD,CAAC;AAED,eAAeS,cAAc"}

View File

@@ -0,0 +1,139 @@
/**
* WordPress dependencies
*/
import { usePrevious } from '@wordpress/compose';
import { useCallback, useEffect, useLayoutEffect, useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
import * as styles from '../styles';
import { useToolsPanelContext } from '../context';
import { useContextSystem } from '../../context';
import { useCx } from '../../utils/hooks/use-cx';
const noop = () => {};
export function useToolsPanelItem(props) {
const {
className,
hasValue,
isShownByDefault = false,
label,
panelId,
resetAllFilter = noop,
onDeselect,
onSelect,
...otherProps
} = useContextSystem(props, 'ToolsPanelItem');
const {
panelId: currentPanelId,
menuItems,
registerResetAllFilter,
deregisterResetAllFilter,
registerPanelItem,
deregisterPanelItem,
flagItemCustomization,
isResetting,
shouldRenderPlaceholderItems: shouldRenderPlaceholder,
firstDisplayedItem,
lastDisplayedItem,
__experimentalFirstVisibleItemClass,
__experimentalLastVisibleItemClass
} = useToolsPanelContext();
// hasValue is a new function on every render, so do not add it as a
// dependency to the useCallback hook! If needed, we should use a ref.
// eslint-disable-next-line react-hooks/exhaustive-deps
const hasValueCallback = useCallback(hasValue, [panelId]);
// resetAllFilter is a new function on every render, so do not add it as a
// dependency to the useCallback hook! If needed, we should use a ref.
// eslint-disable-next-line react-hooks/exhaustive-deps
const resetAllFilterCallback = useCallback(resetAllFilter, [panelId]);
const previousPanelId = usePrevious(currentPanelId);
const hasMatchingPanel = currentPanelId === panelId || currentPanelId === null;
// Registering the panel item allows the panel to include it in its
// automatically generated menu and determine its initial checked status.
//
// This is performed in a layout effect to ensure that the panel item
// is registered before it is rendered preventing a rendering glitch.
// See: https://github.com/WordPress/gutenberg/issues/56470
useLayoutEffect(() => {
if (hasMatchingPanel && previousPanelId !== null) {
registerPanelItem({
hasValue: hasValueCallback,
isShownByDefault,
label,
panelId
});
}
return () => {
if (previousPanelId === null && !!currentPanelId || currentPanelId === panelId) {
deregisterPanelItem(label);
}
};
}, [currentPanelId, hasMatchingPanel, isShownByDefault, label, hasValueCallback, panelId, previousPanelId, registerPanelItem, deregisterPanelItem]);
useEffect(() => {
if (hasMatchingPanel) {
registerResetAllFilter(resetAllFilterCallback);
}
return () => {
if (hasMatchingPanel) {
deregisterResetAllFilter(resetAllFilterCallback);
}
};
}, [registerResetAllFilter, deregisterResetAllFilter, resetAllFilterCallback, hasMatchingPanel]);
// Note: `label` is used as a key when building menu item state in
// `ToolsPanel`.
const menuGroup = isShownByDefault ? 'default' : 'optional';
const isMenuItemChecked = menuItems?.[menuGroup]?.[label];
const wasMenuItemChecked = usePrevious(isMenuItemChecked);
const isRegistered = menuItems?.[menuGroup]?.[label] !== undefined;
const isValueSet = hasValue();
const wasValueSet = usePrevious(isValueSet);
const newValueSet = isValueSet && !wasValueSet;
// Notify the panel when an item's value has been set.
useEffect(() => {
if (!newValueSet) {
return;
}
flagItemCustomization(label, menuGroup);
}, [newValueSet, menuGroup, label, flagItemCustomization]);
// Determine if the panel item's corresponding menu is being toggled and
// trigger appropriate callback if it is.
useEffect(() => {
// We check whether this item is currently registered as items rendered
// via fills can persist through the parent panel being remounted.
// See: https://github.com/WordPress/gutenberg/pull/45673
if (!isRegistered || isResetting || !hasMatchingPanel) {
return;
}
if (isMenuItemChecked && !isValueSet && !wasMenuItemChecked) {
onSelect?.();
}
if (!isMenuItemChecked && wasMenuItemChecked) {
onDeselect?.();
}
}, [hasMatchingPanel, isMenuItemChecked, isRegistered, isResetting, isValueSet, wasMenuItemChecked, onSelect, onDeselect]);
// The item is shown if it is a default control regardless of whether it
// has a value. Optional items are shown when they are checked or have
// a value.
const isShown = isShownByDefault ? menuItems?.[menuGroup]?.[label] !== undefined : isMenuItemChecked;
const cx = useCx();
const classes = useMemo(() => {
const shouldApplyPlaceholderStyles = shouldRenderPlaceholder && !isShown;
const firstItemStyle = firstDisplayedItem === label && __experimentalFirstVisibleItemClass;
const lastItemStyle = lastDisplayedItem === label && __experimentalLastVisibleItemClass;
return cx(styles.ToolsPanelItem, shouldApplyPlaceholderStyles && styles.ToolsPanelItemPlaceholder, !shouldApplyPlaceholderStyles && className, firstItemStyle, lastItemStyle);
}, [isShown, shouldRenderPlaceholder, className, cx, firstDisplayedItem, lastDisplayedItem, __experimentalFirstVisibleItemClass, __experimentalLastVisibleItemClass, label]);
return {
...otherProps,
isShown,
shouldRenderPlaceholder,
className: classes
};
}
//# sourceMappingURL=hook.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
export { default } from './component';
export { useToolsPanelItem } from './hook';
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["default","useToolsPanelItem"],"sources":["@wordpress/components/src/tools-panel/tools-panel-item/index.ts"],"sourcesContent":["export { default } from './component';\nexport { useToolsPanelItem } from './hook';\n"],"mappings":"AAAA,SAASA,OAAO,QAAQ,aAAa;AACrC,SAASC,iBAAiB,QAAQ,QAAQ"}

View File

@@ -0,0 +1,93 @@
import { createElement } from "react";
/**
* External dependencies
*/
/**
* Internal dependencies
*/
import ToolsPanelHeader from '../tools-panel-header';
import { ToolsPanelContext } from '../context';
import { useToolsPanel } from './hook';
import { Grid } from '../../grid';
import { contextConnect } from '../../context';
const UnconnectedToolsPanel = (props, forwardedRef) => {
const {
children,
label,
panelContext,
resetAllItems,
toggleItem,
headingLevel,
dropdownMenuProps,
...toolsPanelProps
} = useToolsPanel(props);
return createElement(Grid, {
...toolsPanelProps,
columns: 2,
ref: forwardedRef
}, createElement(ToolsPanelContext.Provider, {
value: panelContext
}, createElement(ToolsPanelHeader, {
label: label,
resetAll: resetAllItems,
toggleItem: toggleItem,
headingLevel: headingLevel,
dropdownMenuProps: dropdownMenuProps
}), children));
};
/**
* The `ToolsPanel` is a container component that displays its children preceded
* by a header. The header includes a dropdown menu which is automatically
* generated from the panel's inner `ToolsPanelItems`.
*
* ```jsx
* import { __ } from '@wordpress/i18n';
* import {
* __experimentalToolsPanel as ToolsPanel,
* __experimentalToolsPanelItem as ToolsPanelItem,
* __experimentalUnitControl as UnitControl
* } from '@wordpress/components';
*
* function Example() {
* const [ height, setHeight ] = useState();
* const [ width, setWidth ] = useState();
*
* const resetAll = () => {
* setHeight();
* setWidth();
* }
*
* return (
* <ToolsPanel label={ __( 'Dimensions' ) } resetAll={ resetAll }>
* <ToolsPanelItem
* hasValue={ () => !! height }
* label={ __( 'Height' ) }
* onDeselect={ () => setHeight() }
* >
* <UnitControl
* label={ __( 'Height' ) }
* onChange={ setHeight }
* value={ height }
* />
* </ToolsPanelItem>
* <ToolsPanelItem
* hasValue={ () => !! width }
* label={ __( 'Width' ) }
* onDeselect={ () => setWidth() }
* >
* <UnitControl
* label={ __( 'Width' ) }
* onChange={ setWidth }
* value={ width }
* />
* </ToolsPanelItem>
* </ToolsPanel>
* );
* }
* ```
*/
export const ToolsPanel = contextConnect(UnconnectedToolsPanel, 'ToolsPanel');
export default ToolsPanel;
//# sourceMappingURL=component.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["ToolsPanelHeader","ToolsPanelContext","useToolsPanel","Grid","contextConnect","UnconnectedToolsPanel","props","forwardedRef","children","label","panelContext","resetAllItems","toggleItem","headingLevel","dropdownMenuProps","toolsPanelProps","createElement","columns","ref","Provider","value","resetAll","ToolsPanel"],"sources":["@wordpress/components/src/tools-panel/tools-panel/component.tsx"],"sourcesContent":["/**\n * External dependencies\n */\nimport type { ForwardedRef } from 'react';\n\n/**\n * Internal dependencies\n */\nimport ToolsPanelHeader from '../tools-panel-header';\nimport { ToolsPanelContext } from '../context';\nimport { useToolsPanel } from './hook';\nimport { Grid } from '../../grid';\nimport type { WordPressComponentProps } from '../../context';\nimport { contextConnect } from '../../context';\nimport type { ToolsPanelProps } from '../types';\n\nconst UnconnectedToolsPanel = (\n\tprops: WordPressComponentProps< ToolsPanelProps, 'div' >,\n\tforwardedRef: ForwardedRef< any >\n) => {\n\tconst {\n\t\tchildren,\n\t\tlabel,\n\t\tpanelContext,\n\t\tresetAllItems,\n\t\ttoggleItem,\n\t\theadingLevel,\n\t\tdropdownMenuProps,\n\t\t...toolsPanelProps\n\t} = useToolsPanel( props );\n\n\treturn (\n\t\t<Grid { ...toolsPanelProps } columns={ 2 } ref={ forwardedRef }>\n\t\t\t<ToolsPanelContext.Provider value={ panelContext }>\n\t\t\t\t<ToolsPanelHeader\n\t\t\t\t\tlabel={ label }\n\t\t\t\t\tresetAll={ resetAllItems }\n\t\t\t\t\ttoggleItem={ toggleItem }\n\t\t\t\t\theadingLevel={ headingLevel }\n\t\t\t\t\tdropdownMenuProps={ dropdownMenuProps }\n\t\t\t\t/>\n\t\t\t\t{ children }\n\t\t\t</ToolsPanelContext.Provider>\n\t\t</Grid>\n\t);\n};\n\n/**\n * The `ToolsPanel` is a container component that displays its children preceded\n * by a header. The header includes a dropdown menu which is automatically\n * generated from the panel's inner `ToolsPanelItems`.\n *\n * ```jsx\n * import { __ } from '@wordpress/i18n';\n * import {\n * __experimentalToolsPanel as ToolsPanel,\n * __experimentalToolsPanelItem as ToolsPanelItem,\n * __experimentalUnitControl as UnitControl\n * } from '@wordpress/components';\n *\n * function Example() {\n * const [ height, setHeight ] = useState();\n * const [ width, setWidth ] = useState();\n *\n * const resetAll = () => {\n * setHeight();\n * setWidth();\n * }\n *\n * return (\n * <ToolsPanel label={ __( 'Dimensions' ) } resetAll={ resetAll }>\n * <ToolsPanelItem\n * hasValue={ () => !! height }\n * label={ __( 'Height' ) }\n * onDeselect={ () => setHeight() }\n * >\n * <UnitControl\n * label={ __( 'Height' ) }\n * onChange={ setHeight }\n * value={ height }\n * />\n * </ToolsPanelItem>\n * <ToolsPanelItem\n * hasValue={ () => !! width }\n * label={ __( 'Width' ) }\n * onDeselect={ () => setWidth() }\n * >\n * <UnitControl\n * label={ __( 'Width' ) }\n * onChange={ setWidth }\n * value={ width }\n * />\n * </ToolsPanelItem>\n * </ToolsPanel>\n * );\n * }\n * ```\n */\nexport const ToolsPanel = contextConnect( UnconnectedToolsPanel, 'ToolsPanel' );\n\nexport default ToolsPanel;\n"],"mappings":";AAAA;AACA;AACA;;AAGA;AACA;AACA;AACA,OAAOA,gBAAgB,MAAM,uBAAuB;AACpD,SAASC,iBAAiB,QAAQ,YAAY;AAC9C,SAASC,aAAa,QAAQ,QAAQ;AACtC,SAASC,IAAI,QAAQ,YAAY;AAEjC,SAASC,cAAc,QAAQ,eAAe;AAG9C,MAAMC,qBAAqB,GAAGA,CAC7BC,KAAwD,EACxDC,YAAiC,KAC7B;EACJ,MAAM;IACLC,QAAQ;IACRC,KAAK;IACLC,YAAY;IACZC,aAAa;IACbC,UAAU;IACVC,YAAY;IACZC,iBAAiB;IACjB,GAAGC;EACJ,CAAC,GAAGb,aAAa,CAAEI,KAAM,CAAC;EAE1B,OACCU,aAAA,CAACb,IAAI;IAAA,GAAMY,eAAe;IAAGE,OAAO,EAAG,CAAG;IAACC,GAAG,EAAGX;EAAc,GAC9DS,aAAA,CAACf,iBAAiB,CAACkB,QAAQ;IAACC,KAAK,EAAGV;EAAc,GACjDM,aAAA,CAAChB,gBAAgB;IAChBS,KAAK,EAAGA,KAAO;IACfY,QAAQ,EAAGV,aAAe;IAC1BC,UAAU,EAAGA,UAAY;IACzBC,YAAY,EAAGA,YAAc;IAC7BC,iBAAiB,EAAGA;EAAmB,CACvC,CAAC,EACAN,QACyB,CACvB,CAAC;AAET,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMc,UAAU,GAAGlB,cAAc,CAAEC,qBAAqB,EAAE,YAAa,CAAC;AAE/E,eAAeiB,UAAU"}

View File

@@ -0,0 +1,269 @@
/**
* WordPress dependencies
*/
import { useCallback, useEffect, useMemo, useRef, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import * as styles from '../styles';
import { useContextSystem } from '../../context';
import { useCx } from '../../utils/hooks/use-cx';
const DEFAULT_COLUMNS = 2;
const generateMenuItems = ({
panelItems,
shouldReset,
currentMenuItems,
menuItemOrder
}) => {
const newMenuItems = {
default: {},
optional: {}
};
const menuItems = {
default: {},
optional: {}
};
panelItems.forEach(({
hasValue,
isShownByDefault,
label
}) => {
const group = isShownByDefault ? 'default' : 'optional';
// If a menu item for this label has already been flagged as customized
// (for default controls), or toggled on (for optional controls), do not
// overwrite its value as those controls would lose that state.
const existingItemValue = currentMenuItems?.[group]?.[label];
const value = existingItemValue ? existingItemValue : hasValue();
newMenuItems[group][label] = shouldReset ? false : value;
});
// Loop the known, previously registered items first to maintain menu order.
menuItemOrder.forEach(key => {
if (newMenuItems.default.hasOwnProperty(key)) {
menuItems.default[key] = newMenuItems.default[key];
}
if (newMenuItems.optional.hasOwnProperty(key)) {
menuItems.optional[key] = newMenuItems.optional[key];
}
});
// Loop newMenuItems object adding any that aren't in the known items order.
Object.keys(newMenuItems.default).forEach(key => {
if (!menuItems.default.hasOwnProperty(key)) {
menuItems.default[key] = newMenuItems.default[key];
}
});
Object.keys(newMenuItems.optional).forEach(key => {
if (!menuItems.optional.hasOwnProperty(key)) {
menuItems.optional[key] = newMenuItems.optional[key];
}
});
return menuItems;
};
const isMenuItemTypeEmpty = obj => obj && Object.keys(obj).length === 0;
export function useToolsPanel(props) {
const {
className,
headingLevel = 2,
resetAll,
panelId,
hasInnerWrapper = false,
shouldRenderPlaceholderItems = false,
__experimentalFirstVisibleItemClass,
__experimentalLastVisibleItemClass,
...otherProps
} = useContextSystem(props, 'ToolsPanel');
const isResetting = useRef(false);
const wasResetting = isResetting.current;
// `isResetting` is cleared via this hook to effectively batch together
// the resetAll task. Without this, the flag is cleared after the first
// control updates and forces a rerender with subsequent controls then
// believing they need to reset, unfortunately using stale data.
useEffect(() => {
if (wasResetting) {
isResetting.current = false;
}
}, [wasResetting]);
// Allow panel items to register themselves.
const [panelItems, setPanelItems] = useState([]);
const [menuItemOrder, setMenuItemOrder] = useState([]);
const [resetAllFilters, setResetAllFilters] = useState([]);
const registerPanelItem = useCallback(item => {
// Add item to panel items.
setPanelItems(items => {
const newItems = [...items];
// If an item with this label has already been registered, remove it
// first. This can happen when an item is moved between the default
// and optional groups.
const existingIndex = newItems.findIndex(oldItem => oldItem.label === item.label);
if (existingIndex !== -1) {
newItems.splice(existingIndex, 1);
}
return [...newItems, item];
});
// Track the initial order of item registration. This is used for
// maintaining menu item order later.
setMenuItemOrder(items => {
if (items.includes(item.label)) {
return items;
}
return [...items, item.label];
});
}, [setPanelItems, setMenuItemOrder]);
// Panels need to deregister on unmount to avoid orphans in menu state.
// This is an issue when panel items are being injected via SlotFills.
const deregisterPanelItem = useCallback(label => {
// When switching selections between components injecting matching
// controls, e.g. both panels have a "padding" control, the
// deregistration of the first panel doesn't occur until after the
// registration of the next.
setPanelItems(items => {
const newItems = [...items];
const index = newItems.findIndex(item => item.label === label);
if (index !== -1) {
newItems.splice(index, 1);
}
return newItems;
});
}, [setPanelItems]);
const registerResetAllFilter = useCallback(newFilter => {
setResetAllFilters(filters => {
return [...filters, newFilter];
});
}, [setResetAllFilters]);
const deregisterResetAllFilter = useCallback(filterToRemove => {
setResetAllFilters(filters => {
return filters.filter(filter => filter !== filterToRemove);
});
}, [setResetAllFilters]);
// Manage and share display state of menu items representing child controls.
const [menuItems, setMenuItems] = useState({
default: {},
optional: {}
});
// Setup menuItems state as panel items register themselves.
useEffect(() => {
setMenuItems(prevState => {
const items = generateMenuItems({
panelItems,
shouldReset: false,
currentMenuItems: prevState,
menuItemOrder
});
return items;
});
}, [panelItems, setMenuItems, menuItemOrder]);
// Force a menu item to be checked.
// This is intended for use with default panel items. They are displayed
// separately to optional items and have different display states,
// we need to update that when their value is customized.
const flagItemCustomization = useCallback((label, group = 'default') => {
setMenuItems(items => {
const newState = {
...items,
[group]: {
...items[group],
[label]: true
}
};
return newState;
});
}, [setMenuItems]);
// Whether all optional menu items are hidden or not must be tracked
// in order to later determine if the panel display is empty and handle
// conditional display of a plus icon to indicate the presence of further
// menu items.
const [areAllOptionalControlsHidden, setAreAllOptionalControlsHidden] = useState(false);
useEffect(() => {
if (isMenuItemTypeEmpty(menuItems?.default) && !isMenuItemTypeEmpty(menuItems?.optional)) {
const allControlsHidden = !Object.entries(menuItems.optional).some(([, isSelected]) => isSelected);
setAreAllOptionalControlsHidden(allControlsHidden);
}
}, [menuItems, setAreAllOptionalControlsHidden]);
const cx = useCx();
const classes = useMemo(() => {
const wrapperStyle = hasInnerWrapper && styles.ToolsPanelWithInnerWrapper(DEFAULT_COLUMNS);
const emptyStyle = isMenuItemTypeEmpty(menuItems?.default) && areAllOptionalControlsHidden && styles.ToolsPanelHiddenInnerWrapper;
return cx(styles.ToolsPanel(DEFAULT_COLUMNS), wrapperStyle, emptyStyle, className);
}, [areAllOptionalControlsHidden, className, cx, hasInnerWrapper, menuItems]);
// Toggle the checked state of a menu item which is then used to determine
// display of the item within the panel.
const toggleItem = useCallback(label => {
const currentItem = panelItems.find(item => item.label === label);
if (!currentItem) {
return;
}
const menuGroup = currentItem.isShownByDefault ? 'default' : 'optional';
const newMenuItems = {
...menuItems,
[menuGroup]: {
...menuItems[menuGroup],
[label]: !menuItems[menuGroup][label]
}
};
setMenuItems(newMenuItems);
}, [menuItems, panelItems, setMenuItems]);
// Resets display of children and executes resetAll callback if available.
const resetAllItems = useCallback(() => {
if (typeof resetAll === 'function') {
isResetting.current = true;
resetAll(resetAllFilters);
}
// Turn off display of all non-default items.
const resetMenuItems = generateMenuItems({
panelItems,
menuItemOrder,
shouldReset: true
});
setMenuItems(resetMenuItems);
}, [panelItems, resetAllFilters, resetAll, setMenuItems, menuItemOrder]);
// Assist ItemGroup styling when there are potentially hidden placeholder
// items by identifying first & last items that are toggled on for display.
const getFirstVisibleItemLabel = items => {
const optionalItems = menuItems.optional || {};
const firstItem = items.find(item => item.isShownByDefault || !!optionalItems[item.label]);
return firstItem?.label;
};
const firstDisplayedItem = getFirstVisibleItemLabel(panelItems);
const lastDisplayedItem = getFirstVisibleItemLabel([...panelItems].reverse());
const panelContext = useMemo(() => ({
areAllOptionalControlsHidden,
deregisterPanelItem,
deregisterResetAllFilter,
firstDisplayedItem,
flagItemCustomization,
hasMenuItems: !!panelItems.length,
isResetting: isResetting.current,
lastDisplayedItem,
menuItems,
panelId,
registerPanelItem,
registerResetAllFilter,
shouldRenderPlaceholderItems,
__experimentalFirstVisibleItemClass,
__experimentalLastVisibleItemClass
}), [areAllOptionalControlsHidden, deregisterPanelItem, deregisterResetAllFilter, firstDisplayedItem, flagItemCustomization, lastDisplayedItem, menuItems, panelId, panelItems, registerResetAllFilter, registerPanelItem, shouldRenderPlaceholderItems, __experimentalFirstVisibleItemClass, __experimentalLastVisibleItemClass]);
return {
...otherProps,
headingLevel,
panelContext,
resetAllItems,
toggleItem,
className: classes
};
}
//# sourceMappingURL=hook.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
export { default } from './component';
export { useToolsPanel } from './hook';
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["default","useToolsPanel"],"sources":["@wordpress/components/src/tools-panel/tools-panel/index.ts"],"sourcesContent":["export { default } from './component';\nexport { useToolsPanel } from './hook';\n"],"mappings":"AAAA,SAASA,OAAO,QAAQ,aAAa;AACrC,SAASC,aAAa,QAAQ,QAAQ"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=types.js.map

File diff suppressed because one or more lines are too long