Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/complexselector-above-placement-gap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] ComplexSelector: the popup keeps its clearance when opening upward (`placement="above"`). The popup layer only set the gap on its block-start edge, which spaces a downward-opening popup but left an upward-opening one flush against the trigger. Both block edges now carry the gap, matching Popover.

@AKnassa
45 changes: 45 additions & 0 deletions packages/core/src/ComplexSelector/ComplexSelector.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,24 @@
import {describe, expect, it, vi} from 'vitest';
import {render, screen, waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as stylex from '@stylexjs/stylex';
import {spacingVars} from '../theme/tokens.stylex';
import {ComplexSelector} from './ComplexSelector';

// StyleX emits one deterministic atomic class per property/value pair, so an
// element carries a probe's class exactly when it has the same declaration.
// The dev-mode debug class (contains "__") varies by source location and is
// excluded from the comparison.
const probe = stylex.create({
blockStartGap: {marginBlockStart: spacingVars['--spacing-1']},
blockEndGap: {marginBlockEnd: spacingVars['--spacing-1']},
});

function atomicClasses(style: (typeof probe)[keyof typeof probe]): string[] {
const {className = ''} = stylex.props(style);
return className.split(' ').filter(c => c !== '' && !c.includes('__'));
}

type FruitValue = {
fruit: 'Apple' | 'Banana';
ripeness: 'Crisp' | 'Ripe' | 'Juicy';
Expand Down Expand Up @@ -153,4 +169,33 @@ describe('ComplexSelector', () => {
await user.click(screen.getByRole('button', {name: 'Done', ...h}));
expect(trigger).toHaveAttribute('aria-expanded', 'false');
});

it('keeps the popup gap on both block edges so placement="above" clears the trigger', async () => {
// #4803: the popup layer only set marginBlockStart, which spaces a popup
// opening downward but leaves an upward-opening one flush against the
// trigger. Both block edges must carry the gap, mirroring Popover's `gap`
// style, so the clearance holds whichever way the layer opens.
const user = userEvent.setup();
render(
<ComplexSelector
label="Fruit blend"
value="Apple"
triggerLabel="Apple"
placement="above">
{() => <div>Surface</div>}
</ComplexSelector>,
);
await user.click(screen.getByRole('button', {name: 'Fruit blend'}));

const layer = document.querySelector('[popover]');
expect(layer).not.toBeNull();
const startGapClasses = atomicClasses(probe.blockStartGap);
const endGapClasses = atomicClasses(probe.blockEndGap);
// Guard against a vacuous pass if the probe ever compiles to no classes.
expect(startGapClasses.length).toBeGreaterThan(0);
expect(endGapClasses.length).toBeGreaterThan(0);
for (const cls of [...startGapClasses, ...endGapClasses]) {
expect(layer!.classList.contains(cls)).toBe(true);
}
});
});
3 changes: 3 additions & 0 deletions packages/core/src/ComplexSelector/ComplexSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ const styles = stylex.create({
},
popover: {
minWidth: 'anchor-size(width)',
// Both block edges carry the gap so the popup keeps its clearance
// whichever way it opens (placement="above" spaces via the end edge).
marginBlockStart: spacingVars['--spacing-1'],
marginBlockEnd: spacingVars['--spacing-1'],
},
content: {
boxSizing: 'border-box',
Expand Down
Loading