Widen front component crossing attributes to aria-*, data-* and draggable (#22614)

Only a closed allow-list of props crossed the front-component
worker→host boundary (`id, className, style, title, tabIndex, role,
aria-label, aria-hidden, data-testid`), so arbitrary `aria-*`/`data-*`
attributes and `draggable` never reached the host DOM. That breaks
headless UI libraries (Radix, cmdk, react-aria) that drive styling/state
through those attributes.

This widens the crossing set to all `aria-*`, all `data-*`, and
`draggable`:
- `draggable` becomes an enumerated remote property (it's a DOM IDL
property React may set as a property, bypassing `setAttribute`, so it
can't ride the prefix path).
- Arbitrary `aria-*`/`data-*` are forwarded in the worker by patching
`setAttribute`/`removeAttribute` through remote-dom's attribute channel,
only for names not already synced as observed attributes.

Security: only inert `aria-*`/`data-*`/`draggable` cross, and they still
route through the host `filterProps` guards (non-function `on*` dropped,
`javascript:` URLs denied) — nothing bypasses them. The enumerated
`aria-label`/`aria-hidden`/`data-testid` keep their existing path.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22614?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Raphaël Bosi
2026-07-07 12:42:03 +02:00
committed by GitHub
parent 46d281f0cb
commit 3bacf7a24b
10 changed files with 247 additions and 48 deletions
@@ -60,4 +60,33 @@ describe('filterProps', () => {
expect(result.href).toBe('https://twenty.com');
});
it('should forward arbitrary aria-* and data-* attributes', () => {
const result = filter(
{
'aria-selected': 'true',
'aria-activedescendant': 'item-2',
'data-state': 'open',
'data-count': '3',
},
'div',
);
expect(result['aria-selected']).toBe('true');
expect(result['aria-activedescendant']).toBe('item-2');
expect(result['data-state']).toBe('open');
expect(result['data-count']).toBe('3');
});
it('should forward the draggable attribute', () => {
expect(filter({ draggable: 'true' }, 'div').draggable).toBe('true');
expect(filter({ draggable: true }, 'div').draggable).toBe(true);
});
it('should still drop a non-function on* handler smuggled as a data-adjacent prop', () => {
const result = filter({ onClick: 'alert(1)', 'data-state': 'open' }, 'div');
expect('onClick' in result).toBe(false);
expect(result['data-state']).toBe('open');
});
});