Developer reference for the Integration Features Block.
- Block Attributes
- TypeScript Types
- Utility Functions
- CSS Custom Properties
- Block Registration
- Extending the Block
The block uses the following attributes defined in block.json:
- Type:
string - Enum:
['free', 'pro', 'proplus'] - Default:
'free' - Description: Determines which tier badge is displayed
- Type:
string - Source:
htmlfrom.pm-integration-feature__label - Default:
'' - Description: The feature name/label displayed in the header
- Type:
boolean - Default:
false - Description: Controls accordion open state in editor (editor-only, not saved to post content)
- Type:
string - Enum:
['chevron', 'plus-minus'] - Default:
'chevron' - Description: Icon style for accordion indicator
chevron: Uses ▼/▲ charactersplus-minus: Uses +/− characters
- Type:
boolean - Default:
false - Description: Controls FREE tier badge visibility (hidden by default)
type TierType = 'free' | 'pro' | 'proplus';type IconStyleType = 'chevron' | 'plus-minus';interface IntegrationFeatureAttributes {
tier: TierType;
label: string;
isOpen: boolean;
iconStyle: IconStyleType;
showFreeBadge: boolean;
}interface TierConfig {
label: string; // Display label (e.g., 'PRO')
className: string; // CSS class for styling
icon: string; // Dashicon name for toolbar
}interface WPBlock {
name: string;
clientId: string;
attributes: Record<string, any>;
innerBlocks?: WPBlock[];
}interface EditProps {
attributes: IntegrationFeatureAttributes;
setAttributes: (attributes: Partial<IntegrationFeatureAttributes>) => void;
clientId: string;
}interface SaveProps {
attributes: IntegrationFeatureAttributes;
}Location: src/integration-features/lib/hasDescription.ts
Determines if inner blocks contain meaningful description content. This is the single source of truth for hasDescription computation, used by both Edit and Save components.
Parameters:
innerBlocks(WPBlock[] | null | undefined): Array of block objects from editor
Returns: boolean - true if at least one block has non-whitespace text
Supported Block Types:
core/paragraph: Checkscontentattributecore/heading: Checkscontentattributecore/list: Checksvaluesattribute- Other block types: Assumes content exists (future-proof)
Example:
import { computeHasDescription } from './lib/hasDescription';
const innerBlocks = useSelect(
(select) => {
const blockEditor = select('core/block-editor');
return blockEditor?.getBlocks(clientId);
},
[clientId]
);
const hasDescription = useMemo(
() => computeHasDescription(innerBlocks || []),
[innerBlocks]
);The block uses CSS custom properties for theme integration:
- Used for: PRO tier badge background
- Fallback:
#1dbe61(brand green) - Example:
.pm-tier-badge--pro { background-color: var(--action, #1dbe61); color: #ffffff; }
- Used for: PRO+ tier badge background
- Fallback:
#8267F6(brand purple) - Example:
.pm-tier-badge--proplus { background-color: var(--secondary, #8267F6); color: #ffffff; }
Add these CSS variables to your theme:
:root {
--action: #1dbe61; /* PRO tier - Brand primary green */
--secondary: #8267F6; /* PRO+ tier - Brand secondary purple */
}The block is registered using WordPress Block API v3:
import { registerBlockType } from '@wordpress/blocks';
import Edit from './edit';
import save from './save';
import metadata from './block.json';
import type { IntegrationFeatureAttributes } from './types';
registerBlockType<IntegrationFeatureAttributes>(metadata.name, {
edit: Edit,
save,
});-
Update
TierTypeintypes.ts:export type TierType = 'free' | 'pro' | 'proplus' | 'enterprise';
-
Add tier to
block.jsonenum:{ "tier": { "enum": ["free", "pro", "proplus", "enterprise"] } } -
Add tier config in
edit.tsxandsave.tsx:const TIER_CONFIG: Record<TierType, TierConfig> = { // ... existing tiers enterprise: { label: __('ENTERPRISE', 'popup-maker'), className: 'pm-tier-badge--enterprise', icon: 'building', }, };
-
Add tier styles in
style.scss:.pm-tier-badge--enterprise { background-color: var(--tertiary, #1a1a1a); color: #ffffff; }
-
Update
IconStyleTypeintypes.ts:export type IconStyleType = 'chevron' | 'plus-minus' | 'arrow';
-
Update enum in
block.json:{ "iconStyle": { "enum": ["chevron", "plus-minus", "arrow"] } } -
Update icon rendering logic in
edit.tsx:const getIcon = () => { switch (iconStyle) { case 'plus-minus': return isOpen ? '−' : '+'; case 'arrow': return isOpen ? '↑' : '↓'; case 'chevron': default: return isOpen ? '▲' : '▼'; } };
-
Update icon rendering in
save.tsx:const getIcon = () => { switch (iconStyle) { case 'plus-minus': return '+'; case 'arrow': return '↓'; case 'chevron': default: return '▼'; } };
-
Add toolbar control in
edit.tsx:{ title: __('Arrow (↓/↑)', 'popup-maker'), isActive: iconStyle === 'arrow', onClick: () => setAttributes({ iconStyle: 'arrow' }), }
Update allowedBlocks in edit.tsx:
const innerBlocksProps = useInnerBlocksProps(
{
className: 'pm-integration-feature__description',
},
{
template: [
['core/paragraph', { placeholder: __('Add description...', 'popup-maker') }],
],
templateLock: false,
allowedBlocks: [
'core/paragraph',
'core/list',
'core/heading',
'core/image', // Add image support
'core/quote', // Add quote support
'core/buttons', // Add buttons support
],
}
);WordPress provides filters for extending blocks:
// Add custom attributes
wp.hooks.addFilter(
'blocks.registerBlockType',
'my-plugin/integration-feature-custom-attributes',
(settings, name) => {
if (name === 'popup-maker/integration-feature') {
settings.attributes = {
...settings.attributes,
customAttribute: {
type: 'string',
default: '',
},
};
}
return settings;
}
);
// Modify block save output
wp.hooks.addFilter(
'blocks.getSaveElement',
'my-plugin/integration-feature-save-element',
(element, blockType, attributes) => {
if (blockType.name === 'popup-maker/integration-feature') {
// Modify save element
}
return element;
}
);The block includes comprehensive accessibility features:
- ARIA attributes:
aria-expanded,aria-controls,role="button",role="region" - Keyboard navigation: Enter and Space keys to toggle accordion
- Focus management: Visible focus outlines with 2px offset
- Screen reader support:
aria-hidden="true"on decorative icons - High contrast mode: Enhanced borders and outlines
- Reduced motion: Respects
prefers-reduced-motionpreference
The block uses a derived state pattern instead of useEffect for computing hasDescription:
// ✅ GOOD: Derived state with useMemo
const hasDescription = useMemo(
() => computeHasDescription(innerBlocks || []),
[innerBlocks]
);
// ❌ BAD: useEffect with setAttributes (feedback loop risk)
useEffect(() => {
const hasDesc = computeHasDescription(innerBlocks);
setAttributes({ hasDescription: hasDesc });
}, [innerBlocks]);Benefits:
- No useEffect feedback loops
- Single source of truth
- Better performance
- Eliminates race conditions
Use useMemo for expensive computations:
const hasDescription = useMemo(
() => computeHasDescription(innerBlocks || []),
[innerBlocks]
);See the specification document for comprehensive test coverage strategy:
- 150+ tests planned
- 82% coverage target
- Unit, integration, accessibility, and E2E tests