Skip to content

Commit

Permalink
Port over changes from HoverCard PR
Browse files Browse the repository at this point in the history
  • Loading branch information
gwyneplaine committed Mar 15, 2024
1 parent 697613b commit 08c0259
Show file tree
Hide file tree
Showing 5 changed files with 572 additions and 127 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
:root {
// stylelint-disable-next-line polaris/conventions/polaris/custom-property-allowed-list -- PositionedOverlay CSS Properties; Supports consuming component styles tying into changes in order to animate without hacky JavaScript timers and forcing reflow
// stylelint-disable polaris/conventions/polaris/custom-property-allowed-list -- PositionedOverlay CSS Properties; Supports consuming component styles tying into changes in order to animate without hacky JavaScript timers and forcing reflow
--pc-positioned-overlay-top: initial;
--pc-positioned-overlay-bottom: initial;
// stylelint-enable
}

.PositionedOverlay {
position: absolute;
z-index: var(--p-z-index-2);
// stylelint-disable-next-line polaris/conventions/polaris/custom-property-allowed-list -- PositionedOverlay CSS Properties
top: var(--pc-positioned-overlay-top);
// stylelint-disable-next-line polaris/conventions/polaris/custom-property-allowed-list -- PositionedOverlay CSS Properties
bottom: var(--pc-positioned-overlay-bottom);
}

.fixed {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import styles from './PositionedOverlay.module.scss';

type Positioning = 'above' | 'below' | 'left' | 'right';

interface OverlayDetails {
export interface OverlayDetails {
top?: number;
bottom?: number;
left?: number;
right?: number;
desiredHeight: number;
Expand Down Expand Up @@ -50,7 +52,8 @@ interface State {
activatorRect: Rect;
left?: number;
right?: number;
top: number;
top?: number;
bottom?: number;
height: number;
width: number | null;
positioning: Positioning;
Expand Down Expand Up @@ -129,7 +132,7 @@ export class PositionedOverlay extends PureComponent<
}

render() {
const {left, right, top, zIndex, width} = this.state;
const {left, right, top, bottom, zIndex, width} = this.state;
const {
render,
fixed,
Expand All @@ -142,10 +145,13 @@ export class PositionedOverlay extends PureComponent<
const nextLeft = left == null || isNaN(left) ? undefined : left;
const nextRight = right == null || isNaN(right) ? undefined : right;
const nextWidth = width == null || isNaN(width) ? undefined : width;
const nextBottom = bottom == null || isNaN(bottom) ? undefined : bottom;

const style = {
'--pc-positioned-overlay-top': nextTop,
'--pc-positioned-overlay-top': nextTop ?? 'initial',
'--pc-positioned-overlay-bottom': nextBottom ?? 'initial',
top: nextTop,
bottom: nextBottom,
left: nextLeft,
right: nextRight,
width: nextWidth,
Expand Down Expand Up @@ -182,6 +188,8 @@ export class PositionedOverlay extends PureComponent<
private overlayDetails = (): OverlayDetails => {
const {
measuring,
top,
bottom,
left,
right,
positioning,
Expand All @@ -193,6 +201,8 @@ export class PositionedOverlay extends PureComponent<

return {
measuring,
top,
bottom,
left,
right,
desiredHeight: height,
Expand Down Expand Up @@ -312,7 +322,7 @@ export class PositionedOverlay extends PureComponent<
const zIndexForLayer = getZIndexForLayerFromNode(activator);
const zIndex =
zIndexForLayer == null ? zIndexForLayer : zIndexForLayer + 1;
const verticalPosition = calculateVerticalPosition(
const calculatedVerticalPosition = calculateVerticalPosition(
activatorRect,
overlayRect,
overlayMargins,
Expand All @@ -323,19 +333,23 @@ export class PositionedOverlay extends PureComponent<
topBarOffset,
);

const verticalPosition =
calculatedVerticalPosition?.top ?? calculatedVerticalPosition?.bottom;

const positionedHorizontal =
preferredPosition === 'left' || preferredPosition === 'right';

const calculatedHorizontalPosition = calculateHorizontalPosition(
const calculatedHorizontalPosition = calculateHorizontalPosition({
activatorRect,
overlayRect,
containerRect,
overlayMargins,
preferredAlignment,
scrollableContainerRect,
positionedHorizontal ? preferredPosition : undefined,
preferredHorizontalPosition: positionedHorizontal
? preferredPosition
: undefined,
overlayMinWidth,
);
});

const horizontalPosition =
calculatedHorizontalPosition.left ??
Expand Down Expand Up @@ -366,11 +380,15 @@ export class PositionedOverlay extends PureComponent<
(positionedHorizontal && calculatedHorizontalPosition.right)
? horizontalPosition
: undefined,
top: lockPosition ? top : verticalPosition.top,
top: lockPosition ? top : calculatedVerticalPosition.top,
bottom:
positionedHorizontal && calculatedVerticalPosition.bottom
? verticalPosition
: undefined,
lockPosition: Boolean(fixed),
height: verticalPosition.height || 0,
height: calculatedVerticalPosition.height || 0,
width,
positioning: verticalPosition.positioning as Positioning,
positioning: calculatedVerticalPosition.positioning as Positioning,
outsideScrollableContainer:
onScrollOut != null &&
rectIsOutsideOfRect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,46 @@ import {mountWithApp} from 'tests/utilities';

// eslint-disable-next-line import/no-deprecated
import {EventListener} from '../../EventListener';
import type {OverlayDetails} from '../PositionedOverlay';
import {PositionedOverlay} from '../PositionedOverlay';
import {Box} from '../../Box';
import * as mathModule from '../utilities/math';
import * as geometry from '../../../utilities/geometry';
import styles from '../PositionedOverlay.module.scss';

describe('<PositionedOverlay />', () => {
const mockRender = jest.fn((overlayDetails: OverlayDetails) => (
<Box
width={
overlayDetails.desiredWidth
? `${overlayDetails.desiredWidth}`
: undefined
}
>
<span>overlay content</span>
</Box>
));

const mockRenderMinWidth = jest.fn((overlayDetails: OverlayDetails) => (
<Box
width={
overlayDetails.desiredWidth
? `${overlayDetails.desiredWidth}`
: undefined
}
minWidth="100px"
>
<span>overlay content</span>
</Box>
));

const activatorText = document.createTextNode('Activator');
const activator = document.createElement('div');
activator.appendChild(activatorText);

const mockProps = {
active: true,
activator: document.createElement('div'),
activator,
render: mockRender,
};

Expand Down Expand Up @@ -79,55 +110,121 @@ describe('<PositionedOverlay />', () => {
});

describe('preferredPosition', () => {
let calculateVerticalPositionMock: jest.SpyInstance;

beforeEach(() => {
calculateVerticalPositionMock = jest.spyOn(
mathModule,
'calculateVerticalPosition',
);
calculateVerticalPositionMock.mockReturnValue({
height: 0,
top: 0,
positioning: 'above',
describe('vertical position', () => {
it('positions above if it is the preferredPosition', () => {
mountWithApp(
<PositionedOverlay {...mockProps} preferredPosition="above" />,
);

expect(mockRender).toHaveBeenCalledWith({
measuring: true,
top: 0,
bottom: undefined,
left: undefined,
right: undefined,
desiredHeight: 0,
desiredWidth: undefined,
positioning: 'below',
activatorRect: {
top: 0,
bottom: 0,
left: 0,
right: 0,
width: 0,
height: 0,
},
chevronOffset: 0,
});
});
});

afterEach(() => {
calculateVerticalPositionMock.mockRestore();
it('positions below if no preferredPosition is given', () => {
mountWithApp(<PositionedOverlay {...mockProps} />);

expect(mockRender).toHaveBeenCalledWith({
measuring: true,
top: 0,
bottom: undefined,
left: undefined,
right: undefined,
desiredHeight: 0,
desiredWidth: undefined,
positioning: 'below',
activatorRect: {
top: 0,
bottom: 0,
left: 0,
right: 0,
width: 0,
height: 0,
},
chevronOffset: 0,
});
});
});

it('positions above if preferredPosition is given', () => {
const spy = jest.fn();
mountWithApp(
<PositionedOverlay
{...mockProps}
preferredPosition="above"
render={spy}
/>,
);

expect(spy).toHaveBeenCalledWith({
activatorRect: {height: 0, left: 0, top: 0, right: 0, width: 0},
desiredHeight: 0,
left: 0,
measuring: false,
positioning: 'above',
chevronOffset: 0,
describe('horizontal position', () => {
it('positions left if it is the preferredPosition', () => {
const positionedOverlay = mountWithApp(
<PositionedOverlay
{...mockProps}
preferredPosition="left"
render={mockRenderMinWidth}
/>,
);

expect(positionedOverlay).toContainReactComponent('div', {
style: expect.objectContaining({left: undefined, right: undefined}),
});

expect(mockRenderMinWidth).toHaveBeenCalledWith({
measuring: true,
top: 0,
bottom: undefined,
left: undefined,
right: undefined,
desiredHeight: 0,
desiredWidth: undefined,
positioning: 'below',
activatorRect: {
top: 0,
bottom: 0,
left: 0,
right: 0,
width: 0,
height: 0,
},
chevronOffset: 0,
});
});
});

it('positions below if no preferredPosition is given', () => {
const spy = jest.fn();
mountWithApp(<PositionedOverlay {...mockProps} render={spy} />);

expect(spy).toHaveBeenCalledWith({
activatorRect: {height: 0, left: 0, top: 0, right: 0, width: 0},
desiredHeight: 0,
left: undefined,
measuring: true,
positioning: 'below',
chevronOffset: 0,
it('positions right if it is the preferredPosition', () => {
const positionedOverlay = mountWithApp(
<PositionedOverlay {...mockProps} render={mockRenderMinWidth} />,
);

expect(positionedOverlay).toContainReactComponent('div', {
style: expect.objectContaining({left: 0, right: undefined}),
});

expect(mockRenderMinWidth).toHaveBeenCalledWith({
measuring: true,
top: 0,
bottom: undefined,
left: undefined,
right: undefined,
desiredHeight: 0,
desiredWidth: undefined,
positioning: 'below',
activatorRect: {
top: 0,
bottom: 0,
left: 0,
right: 0,
width: 0,
height: 0,
},
chevronOffset: 0,
});
});
});
});
Expand Down Expand Up @@ -356,7 +453,3 @@ describe('<PositionedOverlay />', () => {
});
});
});

function mockRender() {
return <span>overlay content</span>;
}
Loading

0 comments on commit 08c0259

Please sign in to comment.