From ef75d4819c20d1854743d0a3f1b5df69023f41ff Mon Sep 17 00:00:00 2001 From: Alquen Sarmiento Date: Tue, 18 Aug 2026 11:10:50 +0800 Subject: [PATCH 1/4] fix: add an accessibility label input to the link settings --- src/block-components/block-link/index.js | 1 + src/block-components/helpers/link/attributes.js | 15 +++++++++++++++ src/block-components/helpers/link/edit.js | 9 +++++++++ src/block-components/link/index.js | 2 +- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/block-components/block-link/index.js b/src/block-components/block-link/index.js index da8d354dc8..c34725ea00 100644 --- a/src/block-components/block-link/index.js +++ b/src/block-components/block-link/index.js @@ -29,6 +29,7 @@ BlockLink.Content = props => { target={ attributes.blockLinkNewTab ? '_blank' : '' } rel={ attributes.blockLinkRel || undefined } title={ attributes.blockLinkTitle || undefined } + aria-label={ attributes.blockLinkAriaLabel || undefined } aria-hidden={ isHidden ? 'true' : undefined } tabindex={ isHidden ? '-1' : undefined } /> diff --git a/src/block-components/helpers/link/attributes.js b/src/block-components/helpers/link/attributes.js index 4a47452857..25dac23be8 100644 --- a/src/block-components/helpers/link/attributes.js +++ b/src/block-components/helpers/link/attributes.js @@ -50,4 +50,19 @@ export const addLinkAttributes = ( attrObject, attrNameTemplate = '%s', selector versionAdded: '3.0.0', versionDeprecated: '', } ) + + attrObject.add( { + attributes: { + ariaLabel: { + type: 'string', + source: 'attribute', + selector, + attribute: 'aria-label', + default: '', + }, + }, + attrNameTemplate, + versionAdded: '3.20.1', + versionDeprecated: '', + } ) } diff --git a/src/block-components/helpers/link/edit.js b/src/block-components/helpers/link/edit.js index 4274745cc6..552ea6f51b 100644 --- a/src/block-components/helpers/link/edit.js +++ b/src/block-components/helpers/link/edit.js @@ -29,6 +29,7 @@ export const LinkControls = props => { const hasLightboxAttr = useAttributeValue( 'hasLightbox', props.attrNameTemplate ) const rel = useAttributeValue( 'rel', props.attrNameTemplate ) const title = useAttributeValue( 'title', props.attrNameTemplate ) + const ariaLabel = useAttributeValue( 'ariaLabel', props.attrNameTemplate ) const hasTitleAttr = useAttributeValue( 'hasTitle', props.attrNameTemplate ) const showGoogleMapHint = hasLightboxAttr && @@ -76,6 +77,14 @@ export const LinkControls = props => { value={ rel } onChange={ updateAttributeHandler( 'rel' ) } /> + { ( hasTitle || hasTitleAttr ) && ( { target={ attributes.linkNewTab ? '_blank' : undefined } rel={ attributes.linkRel || undefined } title={ attributes.linkTitle || undefined } + aria-label={ attributes.linkAriaLabel || undefined } > { props.children } @@ -64,4 +65,3 @@ Link.Content = props => { Link.InspectorControls = Edit Link.addAttributes = addAttributes - From 31246d1411bcc73f90f4c1ba5e376f173a4fabe9 Mon Sep 17 00:00:00 2001 From: Arukuen Date: Thu, 17 Sep 2026 11:13:38 +0800 Subject: [PATCH 2/4] fix: differentiate Block and Link Custom Attributes --- src/block-components/block-link/index.js | 4 +- .../custom-attributes/attributes.js | 10 ++++- .../custom-attributes/edit.js | 45 +++++++++++++++++-- .../custom-attributes/index.js | 9 ++-- .../custom-attributes/readme.md | 13 +++++- .../helpers/link/attributes.js | 14 ++---- src/block-components/helpers/link/edit.js | 9 ---- src/block-components/link/index.js | 4 +- src/block/call-to-action/edit.js | 2 +- src/block/card/edit.js | 2 +- src/block/column/edit.js | 2 +- src/block/icon-box/edit.js | 2 +- src/block/icon/edit.js | 2 +- src/block/image-box/edit.js | 2 +- src/block/image/edit.js | 2 +- src/block/notification/edit.js | 2 +- src/block/pricing-box/edit.js | 2 +- src/block/team-member/edit.js | 2 +- src/block/testimonial/edit.js | 2 +- .../__test__/index.test.js | 5 ++- .../custom-attributes-control/index.js | 26 ++--------- src/util/attributes/index.js | 2 + src/util/blocks.js | 4 +- 23 files changed, 97 insertions(+), 70 deletions(-) diff --git a/src/block-components/block-link/index.js b/src/block-components/block-link/index.js index c34725ea00..ec079ccc5e 100644 --- a/src/block-components/block-link/index.js +++ b/src/block-components/block-link/index.js @@ -8,6 +8,7 @@ import { Edit } from './edit' * External dependencies */ import { Link } from '~stackable/components' +import { CustomAttributes } from '../custom-attributes' export const BlockLink = () => { return null @@ -21,15 +22,16 @@ BlockLink.Content = props => { if ( ! attributes.blockLinkUrl ) { return null } + const customAttributes = CustomAttributes.getCustomAttributes( attributes, 'blockLinkCustomAttributes' ) return ( diff --git a/src/block-components/custom-attributes/attributes.js b/src/block-components/custom-attributes/attributes.js index 52efd71c0f..b2f4b4ca82 100644 --- a/src/block-components/custom-attributes/attributes.js +++ b/src/block-components/custom-attributes/attributes.js @@ -1,4 +1,9 @@ -export const addAttributes = attrObject => { +export const addAttributes = ( attrObject, options = {} ) => { + const { + attrNameTemplate = '%s', + versionAdded = '3.0.0', + } = options + attrObject.add( { attributes: { customAttributes: { @@ -6,7 +11,8 @@ export const addAttributes = attrObject => { default: [], }, }, - versionAdded: '3.0.0', + attrNameTemplate, + versionAdded, versionDeprecated: '', } ) } diff --git a/src/block-components/custom-attributes/edit.js b/src/block-components/custom-attributes/edit.js index 4f54187a1f..40f0cb3f40 100644 --- a/src/block-components/custom-attributes/edit.js +++ b/src/block-components/custom-attributes/edit.js @@ -17,10 +17,31 @@ import { useBlockAttributesContext, useBlockSetAttributesContext } from '~stacka * WordPress dependencies */ import { __ } from '@wordpress/i18n' +import { ExternalLink } from '@wordpress/components' import { INVALID_HTML_ATTRIBUTES } from '.' -export const Edit = () => { - const customAttributes = useBlockAttributesContext( attributes => attributes.customAttributes ) +const CustomAttributesHelp = ( { hasLink } ) => { + return

+ { hasLink + ? __( 'You can type in custom HTML attributes for this block or its link in the fields above. Examples:', i18n ) + : __( 'You can type in custom HTML attributes for this block in the field above. Example:', i18n ) } +
+ data-id="my-title" + { hasLink && <> +
+ aria-label="Open product details" + } +

+} + +export const Edit = props => { + const { + customAttributes, + linkCustomAttributes, + } = useBlockAttributesContext( attributes => ( { + customAttributes: attributes.customAttributes, + linkCustomAttributes: props.linkAttributeName ? attributes[ props.linkAttributeName ] : undefined, + } ) ) const setAttributes = useBlockSetAttributesContext() return ( @@ -30,12 +51,30 @@ export const Edit = () => { id="custom-attributes" > setAttributes( { customAttributes } ) } /> + { props.linkAttributeName && setAttributes( { [ props.linkAttributeName ]: linkCustomAttributes } ) } + /> + } + + + { __( 'Learn more about Custom Attributes', i18n ) } + ) } + +Edit.defaultProps = { + linkAttributeName: '', +} diff --git a/src/block-components/custom-attributes/index.js b/src/block-components/custom-attributes/index.js index ca0c353348..c6e01727ca 100644 --- a/src/block-components/custom-attributes/index.js +++ b/src/block-components/custom-attributes/index.js @@ -26,12 +26,13 @@ CustomAttributes.addAttributes = addAttributes // CustomAttributes.Style = Style -CustomAttributes.getCustomAttributes = attributes => { - if ( ! Array.isArray( attributes.customAttributes ) || attributes.customAttributes.length === 0 ) { +CustomAttributes.getCustomAttributes = ( attributes, attributeName = 'customAttributes' ) => { + if ( ! Array.isArray( attributes[ attributeName ] ) || attributes[ attributeName ].length === 0 ) { return {} } - const customAttributes = Object.fromEntries( attributes.customAttributes ) + const customAttributes = Object.fromEntries( attributes[ attributeName ] ) + const invalidBlockAttributes = [ ...INVALID_BLOCK_ATTRIBUTES, attributeName ] Object.keys( customAttributes ).forEach( key => { // Unescape the value, since we're storing them as escaped strings. let value = unescape( customAttributes[ key ] ) @@ -44,7 +45,7 @@ CustomAttributes.getCustomAttributes = attributes => { dynamicAttributeMatch.forEach( _match => { const match = _match.substr( 1, _match.length - 2 ) if ( - ! INVALID_BLOCK_ATTRIBUTES.includes( match ) && + ! invalidBlockAttributes.includes( match ) && attributes.hasOwnProperty( match ) && ! isUndefined( attributes[ match ] ) ) { diff --git a/src/block-components/custom-attributes/readme.md b/src/block-components/custom-attributes/readme.md index f14bbf6034..6f8de06d25 100644 --- a/src/block-components/custom-attributes/readme.md +++ b/src/block-components/custom-attributes/readme.md @@ -1,10 +1,11 @@ # Custom Attributes Block Component -Gives the ability to add your own custom attributes which will be placed on the block. +Gives the ability to add custom attributes to a block or its generated link. ## Usage -The custom attribute implementation is already handled by the Block Div Block Component +The block custom attribute implementation is already handled by the Block Div Block Component. +For a link, render the attributes with `CustomAttributes.getCustomAttributes( attributes, 'linkCustomAttributes' )` or its block-link equivalent. ### Adding inspector controls in `edit.js` @@ -12,8 +13,16 @@ The custom attribute implementation is already handled by the Block Div Block Co ``` +For a block with a configurable link, pass the link attribute name to add a second field below the block attributes. + +``` + +``` + ### Adding attributes in `schema.js` ``` CustomAttributes.addAttributes( attrObject ) ``` + +Link attributes are registered through `Link.addAttributes` or `BlockLink.addAttributes`. diff --git a/src/block-components/helpers/link/attributes.js b/src/block-components/helpers/link/attributes.js index 25dac23be8..7ed2671c73 100644 --- a/src/block-components/helpers/link/attributes.js +++ b/src/block-components/helpers/link/attributes.js @@ -1,3 +1,5 @@ +import { addAttributes as addCustomAttributes } from '../../custom-attributes/attributes' + export const linkAttributes = ( selector = 'a' ) => { return { hasLink: { @@ -51,18 +53,8 @@ export const addLinkAttributes = ( attrObject, attrNameTemplate = '%s', selector versionDeprecated: '', } ) - attrObject.add( { - attributes: { - ariaLabel: { - type: 'string', - source: 'attribute', - selector, - attribute: 'aria-label', - default: '', - }, - }, + addCustomAttributes( attrObject, { attrNameTemplate, versionAdded: '3.20.1', - versionDeprecated: '', } ) } diff --git a/src/block-components/helpers/link/edit.js b/src/block-components/helpers/link/edit.js index 552ea6f51b..4274745cc6 100644 --- a/src/block-components/helpers/link/edit.js +++ b/src/block-components/helpers/link/edit.js @@ -29,7 +29,6 @@ export const LinkControls = props => { const hasLightboxAttr = useAttributeValue( 'hasLightbox', props.attrNameTemplate ) const rel = useAttributeValue( 'rel', props.attrNameTemplate ) const title = useAttributeValue( 'title', props.attrNameTemplate ) - const ariaLabel = useAttributeValue( 'ariaLabel', props.attrNameTemplate ) const hasTitleAttr = useAttributeValue( 'hasTitle', props.attrNameTemplate ) const showGoogleMapHint = hasLightboxAttr && @@ -77,14 +76,6 @@ export const LinkControls = props => { value={ rel } onChange={ updateAttributeHandler( 'rel' ) } /> - { ( hasTitle || hasTitleAttr ) && ( { linkProps = {}, attributes, } = props + const customAttributes = CustomAttributes.getCustomAttributes( attributes, 'linkCustomAttributes' ) if ( ! attributes.linkHasLink ) { return props.children @@ -49,13 +51,13 @@ Link.Content = props => { return ( { props.children } diff --git a/src/block/call-to-action/edit.js b/src/block/call-to-action/edit.js index 4245dcd3fd..c7e5037148 100644 --- a/src/block/call-to-action/edit.js +++ b/src/block/call-to-action/edit.js @@ -137,7 +137,7 @@ const InspectorControls = memo( () => { - + diff --git a/src/block/card/edit.js b/src/block/card/edit.js index b572b41610..9fe4799b5a 100644 --- a/src/block/card/edit.js +++ b/src/block/card/edit.js @@ -191,7 +191,7 @@ const InspectorControls = memo( props => { - + diff --git a/src/block/column/edit.js b/src/block/column/edit.js index c82c9ae5e1..e100a12ac9 100644 --- a/src/block/column/edit.js +++ b/src/block/column/edit.js @@ -201,7 +201,7 @@ const InspectorControls = memo( props => { - + diff --git a/src/block/icon-box/edit.js b/src/block/icon-box/edit.js index 19fcc3cc48..cf241b4206 100644 --- a/src/block/icon-box/edit.js +++ b/src/block/icon-box/edit.js @@ -138,7 +138,7 @@ const InspectorControls = memo( () => { - + diff --git a/src/block/icon/edit.js b/src/block/icon/edit.js index 8b452eee56..7d88557cc9 100644 --- a/src/block/icon/edit.js +++ b/src/block/icon/edit.js @@ -120,7 +120,7 @@ const InspectorControls = memo( props => { - + diff --git a/src/block/image-box/edit.js b/src/block/image-box/edit.js index 322bac4697..be66fb42f6 100644 --- a/src/block/image-box/edit.js +++ b/src/block/image-box/edit.js @@ -135,7 +135,7 @@ const InspectorControls = memo( () => { - + diff --git a/src/block/image/edit.js b/src/block/image/edit.js index 4cdd12fa77..bcb12a6d85 100644 --- a/src/block/image/edit.js +++ b/src/block/image/edit.js @@ -153,7 +153,7 @@ const InspectorControls = memo( props => { - + diff --git a/src/block/notification/edit.js b/src/block/notification/edit.js index 9daf7e4e7d..0bbc6e2244 100644 --- a/src/block/notification/edit.js +++ b/src/block/notification/edit.js @@ -206,7 +206,7 @@ const InspectorControls = memo( props => { - + diff --git a/src/block/pricing-box/edit.js b/src/block/pricing-box/edit.js index 443656815f..9cc007a1aa 100644 --- a/src/block/pricing-box/edit.js +++ b/src/block/pricing-box/edit.js @@ -129,7 +129,7 @@ const InspectorControls = memo( () => { - + diff --git a/src/block/team-member/edit.js b/src/block/team-member/edit.js index 745f1582aa..5c4d1a967c 100644 --- a/src/block/team-member/edit.js +++ b/src/block/team-member/edit.js @@ -134,7 +134,7 @@ const InspectorControls = memo( () => { - + diff --git a/src/block/testimonial/edit.js b/src/block/testimonial/edit.js index b1757afdf3..e2a5110873 100644 --- a/src/block/testimonial/edit.js +++ b/src/block/testimonial/edit.js @@ -132,7 +132,7 @@ const InspectorControls = memo( () => { - + diff --git a/src/components/custom-attributes-control/__test__/index.test.js b/src/components/custom-attributes-control/__test__/index.test.js index 7c20bf5c95..c565faa072 100644 --- a/src/components/custom-attributes-control/__test__/index.test.js +++ b/src/components/custom-attributes-control/__test__/index.test.js @@ -22,10 +22,13 @@ describe( 'CustomAttributesControl', () => { } const { - getByTestId, getByText, + getByTestId, getByText, rerender, } = render( ) expect( getByText( 'Custom Attributes' ) ).toBeTruthy() + rerender( ) + expect( getByText( 'Link Custom Attributes' ) ).toBeTruthy() + const invalidInputs = [ `test`, `key=value`, diff --git a/src/components/custom-attributes-control/index.js b/src/components/custom-attributes-control/index.js index 0682985f67..105f97471c 100644 --- a/src/components/custom-attributes-control/index.js +++ b/src/components/custom-attributes-control/index.js @@ -13,13 +13,11 @@ import { createRoot } from '~stackable/util' * WordPress dependencies */ import { - Fragment, useState, unmountComponentAtNode, useRef, } from '@wordpress/element' import { __ } from '@wordpress/i18n' -import { ExternalLink } from '@wordpress/components' import AdvancedTextControl from '../advanced-text-control' const sanitizeString = str => { @@ -87,36 +85,18 @@ const CustomAttributesControl = props => { setCustomAttributes( createAttributeString( props.value ) ) } } + const help = hasError && { __( 'There is an error in your custom attribute', i18n ) } return ( - { __( 'You can type in custom HTML attributes for this block in the field above.', i18n ) } -   - { __( 'Example:', i18n ) } -
- { - // eslint-disable-next-line react/jsx-curly-brace-presence - `data-id="my-title"` - } -
- - { __( 'Learn more about Custom Attributes', i18n ) } - - { hasError && { __( 'There is an error in your custom attribute', i18n ) } } - - ) } + help={ help } /> ) } diff --git a/src/util/attributes/index.js b/src/util/attributes/index.js index c9758836d2..825bdb0016 100644 --- a/src/util/attributes/index.js +++ b/src/util/attributes/index.js @@ -232,6 +232,7 @@ export const CONTENT_ATTRIBUTES = [ 'linkRel', 'linkHasTitle', 'linkTitle', + 'linkCustomAttributes', // Block-level link 'blockLinkHasLink', 'blockLinkUrl', @@ -239,4 +240,5 @@ export const CONTENT_ATTRIBUTES = [ 'blockLinkRel', 'blockLinkHasTitle', 'blockLinkTitle', + 'blockLinkCustomAttributes', ] diff --git a/src/util/blocks.js b/src/util/blocks.js index 7d291976b0..57f1784cca 100644 --- a/src/util/blocks.js +++ b/src/util/blocks.js @@ -43,14 +43,14 @@ export const STACKABLE_FILTERS = { 'stackable/card-group': [], 'stackable/card': [ 'imageUrl', 'imageId', 'imageAlt' ], 'stackable/button-group': [], - 'stackable/button': [ 'text', 'icon', 'linkHasLink', 'linkUrl', 'linkNewTab', 'linkRel', 'linkHasTitle', 'linkTitle' ], + 'stackable/button': [ 'text', 'icon', 'linkHasLink', 'linkUrl', 'linkNewTab', 'linkRel', 'linkHasTitle', 'linkTitle', 'linkCustomAttributes' ], 'stackable/text': [ 'text' ], 'stackable/subtitle': [ 'text' ], 'stackable/heading': [ 'text' ], 'stackable/number-box': [ 'text' ], 'stackable/image': [ 'imageUrl', 'imageId', 'imageAlt' ], 'stackable/icon': [ 'icon' ], - 'stackable/icon-button': [ 'icon', 'linkHasLink', 'linkUrl', 'linkNewTab', 'linkRel', 'linkHasTitle', 'linkTitle' ], + 'stackable/icon-button': [ 'icon', 'linkHasLink', 'linkUrl', 'linkNewTab', 'linkRel', 'linkHasTitle', 'linkTitle', 'linkCustomAttributes' ], 'stackable/icon-list': [ 'icon', 'text' ], 'stackable/icon-list-item': [ 'text' ], 'stackable/progress-bar': [ 'text', 'progressValue', 'progressValuePrefix', 'progressValueSuffix', 'progressInnerText', 'progressMax' ], From 30e31dc9317f9b03342056a5496c706d0fd46b88 Mon Sep 17 00:00:00 2001 From: Arukuen Date: Thu, 17 Sep 2026 11:26:44 +0800 Subject: [PATCH 3/4] fix: description styling --- .../custom-attributes/edit.js | 22 ++++++++++--------- .../custom-attributes-control/index.js | 6 ++++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/block-components/custom-attributes/edit.js b/src/block-components/custom-attributes/edit.js index 40f0cb3f40..65499ffc89 100644 --- a/src/block-components/custom-attributes/edit.js +++ b/src/block-components/custom-attributes/edit.js @@ -21,7 +21,7 @@ import { ExternalLink } from '@wordpress/components' import { INVALID_HTML_ATTRIBUTES } from '.' const CustomAttributesHelp = ( { hasLink } ) => { - return

+ return <> { hasLink ? __( 'You can type in custom HTML attributes for this block or its link in the fields above. Examples:', i18n ) : __( 'You can type in custom HTML attributes for this block in the field above. Example:', i18n ) } @@ -29,9 +29,16 @@ const CustomAttributesHelp = ( { hasLink } ) => { data-id="my-title" { hasLink && <>
- aria-label="Open product details" + aria-label="Learn more" } -

+
+ + { __( 'Learn more about Custom Attributes', i18n ) } + + } export const Edit = props => { @@ -55,21 +62,16 @@ export const Edit = props => { value={ customAttributes } invalidHtmlAttributes={ INVALID_HTML_ATTRIBUTES } onChange={ customAttributes => setAttributes( { customAttributes } ) } + help={ ! props.linkAttributeName && } /> { props.linkAttributeName && setAttributes( { [ props.linkAttributeName ]: linkCustomAttributes } ) } + help={ } /> } - - - { __( 'Learn more about Custom Attributes', i18n ) } - ) diff --git a/src/components/custom-attributes-control/index.js b/src/components/custom-attributes-control/index.js index 105f97471c..48877d75d7 100644 --- a/src/components/custom-attributes-control/index.js +++ b/src/components/custom-attributes-control/index.js @@ -85,7 +85,10 @@ const CustomAttributesControl = props => { setCustomAttributes( createAttributeString( props.value ) ) } } - const help = hasError && { __( 'There is an error in your custom attribute', i18n ) } + const help = ( props.help || hasError ) && <> + { props.help } + { hasError && { __( 'There is an error in your custom attribute', i18n ) } } + return ( { CustomAttributesControl.defaultProps = { label: '', + help: null, value: [], invalidHtmlAttributes: [], onChange: () => {}, From ed6676b8497cb12207730b57a2c8e06b63238508 Mon Sep 17 00:00:00 2001 From: Arukuen Date: Thu, 17 Sep 2026 11:53:05 +0800 Subject: [PATCH 4/4] test: add e2e for new custom attr --- .../custom-attributes/__test__/index.test.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/block-components/custom-attributes/__test__/index.test.js diff --git a/src/block-components/custom-attributes/__test__/index.test.js b/src/block-components/custom-attributes/__test__/index.test.js new file mode 100644 index 0000000000..a0a9859e35 --- /dev/null +++ b/src/block-components/custom-attributes/__test__/index.test.js @@ -0,0 +1,30 @@ +import { CustomAttributes } from '..' + +describe( 'CustomAttributes.getCustomAttributes', () => { + it( 'resolves a named custom attribute list', () => { + const attributes = { + text: 'Product details', + linkCustomAttributes: [ + [ 'aria-label', 'Open %text%' ], + [ 'data-link-id', 'details' ], + ], + } + + expect( CustomAttributes.getCustomAttributes( attributes, 'linkCustomAttributes' ) ).toEqual( { + 'aria-label': 'Open Product details', + 'data-link-id': 'details', + } ) + } ) + + it( 'does not resolve the custom attribute list into itself', () => { + const attributes = { + linkCustomAttributes: [ + [ 'data-value', '%linkCustomAttributes%' ], + ], + } + + expect( CustomAttributes.getCustomAttributes( attributes, 'linkCustomAttributes' ) ).toEqual( { + 'data-value': '%linkCustomAttributes%', + } ) + } ) +} )