Skip to content

Commit

Permalink
Merge pull request #352 from TykTechnologies/TUI-41/floating-containe…
Browse files Browse the repository at this point in the history
…r-enhancements

floating container enhancements
  • Loading branch information
ifrim authored Jan 31, 2024
2 parents 8d29dcd + 64392aa commit 8494b22
Show file tree
Hide file tree
Showing 15 changed files with 125 additions and 95 deletions.
2 changes: 1 addition & 1 deletion lib/index.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/index.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/index.js.map

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@
top: -9999px;
overflow: auto;
z-index: $modal-z-index;

&__content-wrapper {
height: 100%;
}
}
2 changes: 1 addition & 1 deletion lib/tyk-ui.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/tyk-ui.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/tyk-ui.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/tyk-ui.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tyk-technologies/tyk-ui",
"version": "3.0.14",
"version": "3.0.15",
"description": "Tyk UI - ui reusable components",
"main": "lib/index.js",
"scripts": {
Expand Down
2 changes: 2 additions & 0 deletions src/components/FloatingContainer/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const onAxisChange = updateStateProp('displayAxis');
<label>Size:</label>
<label><input type="radio" name="size" value="auto" checked={state.size === 'auto'} onChange={onSizeChange} />auto</label>
<label><input type="radio" name="size" value="matchElement" checked={state.size === 'matchElement'} onChange={onSizeChange} />matchElement</label>
<label><input type="radio" name="size" value="matchStart" checked={state.size === 'matchStart'} onChange={onSizeChange} />matchStart</label>
<label><input type="radio" name="size" value="matchEnd" checked={state.size === 'matchEnd'} onChange={onSizeChange} />matchEnd</label>
</div>
<div>
<label>Offset:<input type="number" value={state.offset} onChange={e => updateState({offset: Number(e.target.value)})} /></label>
Expand Down
80 changes: 61 additions & 19 deletions src/components/FloatingContainer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { createPortal } from 'react-dom';
import PropTypes from 'prop-types';
import InfiniteScroller from '../InfiniteScroller';

const VIEWPORT_INITIAL_PADDING = 5;

/**
* It displays a container relative to another element.
* Meant to be used for dropdowns, tooltips, and other similar components.
Expand All @@ -23,6 +25,8 @@ function FloatingContainer({
const localRef = useRef(null);
const floatingContainerRef = ref || localRef;
const contentWrapperRef = useRef(null);
// offset calculated on first render to make sure the floating container is inside the viewport
const viewportOffset = useRef(null);

function determineDisplay() {
const target = element.current;
Expand Down Expand Up @@ -75,41 +79,61 @@ function FloatingContainer({
if (!target || !container) return;

const targetPosition = target.getBoundingClientRect();
const { left: vpLeft, top: vpTop } = viewportOffset.current ?? { left: 0, top: 0 };

if (display === 'top') {
const leftPos = {
auto: targetPosition.left + target.offsetWidth / 2 - container.offsetWidth / 2,
matchElement: targetPosition.left,
matchStart: targetPosition.left,
matchEnd: targetPosition.right - container.offsetWidth,
}[size];
const topPos = targetPosition.top - container.offsetHeight - offset;
container.style.top = `${topPos}px`;
container.style.left = (size === 'auto'
? `${targetPosition.left + target.offsetWidth / 2 - container.offsetWidth / 2}px`
: `${targetPosition.left}px`);
container.style.left = `${leftPos + vpLeft}px`;
container.style.top = `${topPos + vpTop}px`;

if (size === 'matchElement') {
container.style.width = `${target.offsetWidth}px`;
} else if (size === 'matchEnd') {
container.style.maxWidth = `${targetPosition.right - VIEWPORT_INITIAL_PADDING}px`;
} else if (typeof size === 'function') {
container.style.width = `${size(target.offsetWidth)}px`;
}
container.style.maxHeight = `${targetPosition.top - offset}px`;
}

if (display === 'bottom') {
container.style.top = `${targetPosition.bottom + offset}px`;
container.style.left = size === 'auto'
? `${targetPosition.left + target.offsetWidth / 2 - container.offsetWidth / 2}px`
: `${targetPosition.left}px`;
const leftPos = {
auto: targetPosition.left + target.offsetWidth / 2 - container.offsetWidth / 2,
matchElement: targetPosition.left,
matchStart: targetPosition.left,
matchEnd: targetPosition.right - container.offsetWidth,
}[size];
const topPos = targetPosition.bottom + offset;
container.style.left = `${leftPos + vpLeft}px`;
container.style.top = `${topPos + vpTop}px`;

if (size === 'matchElement') {
container.style.width = `${target.offsetWidth}px`;
} else if (size === 'matchEnd') {
container.style.maxWidth = `${targetPosition.right - VIEWPORT_INITIAL_PADDING}px`;
} else if (typeof size === 'function') {
container.style.width = `${size(target.offsetWidth)}px`;
}
container.style.maxHeight = `${window.innerHeight - targetPosition.bottom - offset}px`;
}

if (display === 'left') {
const topPos = targetPosition.top + target.offsetHeight / 2 - container.offsetHeight / 2;
container.style.top = size === 'auto'
? `${topPos}px`
: `${targetPosition.top}px`;
container.style.left = `${targetPosition.left - container.offsetWidth - offset}px`;
const leftPos = targetPosition.left - container.offsetWidth - offset;
const topPos = {
auto: targetPosition.top + target.offsetHeight / 2 - container.offsetHeight / 2,
matchElement: targetPosition.top,
matchStart: targetPosition.top,
matchEnd: targetPosition.bottom - container.offsetHeight,
}[size];
container.style.left = `${leftPos}px`;
container.style.top = `${topPos}px`;

if (size === 'matchElement') {
container.style.height = `${target.offsetHeight}px`;
} else if (typeof size === 'function') {
Expand All @@ -119,18 +143,36 @@ function FloatingContainer({
}

if (display === 'right') {
const topPos = targetPosition.top + target.offsetHeight / 2 - container.offsetHeight / 2;
container.style.top = size === 'auto'
? `${topPos}px`
: `${targetPosition.top}px`;
container.style.left = `${targetPosition.left + target.offsetWidth + offset}px`;
const leftPos = targetPosition.left + target.offsetWidth + offset;
const topPos = {
auto: targetPosition.top + target.offsetHeight / 2 - container.offsetHeight / 2,
matchElement: targetPosition.top,
matchStart: targetPosition.top,
matchEnd: targetPosition.bottom - container.offsetHeight,
}[size];
container.style.left = size === 'auto' ? `${leftPos + vpLeft}px` : `${leftPos}px`;
container.style.top = size === 'auto' ? `${topPos + vpTop}px` : `${topPos}px`;

if (size === 'matchElement') {
container.style.height = `${target.offsetHeight}px`;
} else if (typeof size === 'function') {
container.style.height = `${size(target.offsetHeight)}px`;
}
container.style.maxWidth = `${window.innerWidth - targetPosition.left - target.offsetWidth - offset}px`;
}

if (viewportOffset.current === null) {
if (size === 'matchEnd') {
viewportOffset.current = { left: 0, top: 0 };
} else {
const left = Number(container.style.left.replace('px', ''));
const top = Number(container.style.top.replace('px', ''));
viewportOffset.current = {
left: left < 0 ? VIEWPORT_INITIAL_PADDING - left : 0,
top: top < 0 ? VIEWPORT_INITIAL_PADDING - top : 0,
};
}
}
}

useEffect(() => {
Expand Down Expand Up @@ -173,7 +215,7 @@ FloatingContainer.propTypes = {
* or the height in pixels.
*/
size: PropTypes.oneOfType([
PropTypes.oneOf(['auto', 'matchElement']),
PropTypes.oneOf(['auto', 'matchElement', 'matchStart', 'matchEnd']),
PropTypes.func,
]),
/**
Expand Down
4 changes: 4 additions & 0 deletions src/components/FloatingContainer/sass/FloatingContainer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@
top: -9999px;
overflow: auto;
z-index: $modal-z-index;

&__content-wrapper {
height: 100%;
}
}
108 changes: 43 additions & 65 deletions src/form/components/Dropdown2/Dropdow2.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ describe('Dropdown2', () => {
.get('.dropdown__menu')
.should('not.exist')
.get(classes.dropdownTrigger)
.click()
.get('.dropdown__menu')
.click();
cy.get('.dropdown__menu')
.should('exist');
});

Expand All @@ -53,12 +53,12 @@ describe('Dropdown2', () => {
</Dropdown2>,
)
.get(classes.dropdownTrigger)
.click()
.get('.dropdown__menu')
.click();
cy.get('.dropdown__menu')
.should('exist')
.get(classes.dropdownTrigger)
.click()
.get('.dropdown__menu')
.click();
cy.get('.dropdown__menu')
.should('not.exist');
});

Expand All @@ -69,12 +69,12 @@ describe('Dropdown2', () => {
</Dropdown2>,
)
.get(classes.dropdownTrigger)
.click()
.get('.dropdown__menu')
.click();
cy.get('.dropdown__menu')
.should('exist')
.get('body')
.click()
.get('.dropdown__menu')
.click();
cy.get('.dropdown__menu')
.should('not.exist');
});

Expand All @@ -85,13 +85,13 @@ describe('Dropdown2', () => {
</Dropdown2>,
)
.get(classes.dropdownTrigger)
.click()
.get('.dropdown__menu')
.click();
cy.get('.dropdown__menu')
.should('exist')
.get(`${classes.buttonGroup} ${classes.btn}`)
.first()
.click()
.get('.dropdown__menu')
.click();
cy.get('.dropdown__menu')
.should('not.exist');
});

Expand All @@ -109,20 +109,14 @@ describe('Dropdown2', () => {

it('displays dropdown items when the trigger button is clicked', () => {
cy.mount(
<div
style={{
marginLeft: '40%',
}}
>
<Dropdown2 splitTrigger title="Dropdown" theme="primary">
<Dropdown2.Item>Item 1</Dropdown2.Item>
<Dropdown2.Item>Item 2</Dropdown2.Item>
</Dropdown2>
</div>,
<Dropdown2 splitTrigger title="Dropdown" theme="primary">
<Dropdown2.Item>Item 1</Dropdown2.Item>
<Dropdown2.Item>Item 2</Dropdown2.Item>
</Dropdown2>,
)
.get(classes.dropdownTrigger)
.click()
.get('.dropdown__menu')
.click();
cy.get('.dropdown__menu')
.should('exist')
.get(classes.dropdownListWrapper)
.should('exist')
Expand All @@ -131,40 +125,32 @@ describe('Dropdown2', () => {
});

it('calls the onChange method of the Dropdown, with the value of the clicked item', () => {
const onChange = cy.stub();
const onChange = cy.stub().as('onChange');
cy.mount(
<div
style={{
marginLeft: '40%',
}}
>
<Dropdown2 splitTrigger title="Dropdown" theme="primary" onChange={onChange}>
<Dropdown2.Item value="1">Item 1</Dropdown2.Item>
<Dropdown2.Item value="2">Item 2</Dropdown2.Item>
</Dropdown2>
</div>,
<Dropdown2 splitTrigger title="Dropdown" theme="primary" onChange={onChange}>
<Dropdown2.Item value="1">Item 1</Dropdown2.Item>
<Dropdown2.Item value="2">Item 2</Dropdown2.Item>
</Dropdown2>,
)
.get(classes.dropdownTrigger)
.click()
.get(`${classes.dropdownListWrapper} > li`)
.click();
cy.get(`${classes.dropdownListWrapper} > li`)
.first()
.click()
.then(() => {
expect(onChange).to.be.calledOnce;
expect(onChange).to.be.calledWith('1');
});
.click();
cy.get('@onChange')
.should('be.calledOnceWith', '1');
});

it.only('selects the dropdown item, based on the value property', () => {
it('selects the dropdown item, based on the value property', () => {
cy.mount(
<Dropdown2 value="1" splitTrigger title="Dropdown" theme="primary">
<Dropdown2.Item value="1">Item 1 Item 1 Item 1 Item 1 Item 1 Item 1</Dropdown2.Item>
<Dropdown2.Item value="2">Item 2</Dropdown2.Item>
</Dropdown2>,
)
.get(classes.dropdownTrigger)
.click()
.get(`${classes.dropdownListWrapper} > li`)
.click();
cy.get(`${classes.dropdownListWrapper} > li`)
.first()
.should('have.class', 'tyk-list__item--selected');
});
Expand All @@ -182,25 +168,19 @@ describe('Dropdown2', () => {

it('sets a max-width to the dropdown list', () => {
cy.mount(
<div
style={{
marginLeft: '40%',
}}
>
<Dropdown2 maxWidth="200px" theme="success" value="1" title="Dropdown" setSelectedValueAsTitle>
<Dropdown2.Item value="1">Item 1</Dropdown2.Item>
<Dropdown2.Item value="2">Item 2</Dropdown2.Item>
</Dropdown2>
</div>,
<Dropdown2 maxWidth="200px" theme="success" value="1" title="Dropdown" setSelectedValueAsTitle>
<Dropdown2.Item value="1">Item 1</Dropdown2.Item>
<Dropdown2.Item value="2">Item 2</Dropdown2.Item>
</Dropdown2>,
)
.get(classes.dropdownTrigger)
.click()
.get('.dropdown__menu .tyk-list')
.click();
cy.get('.dropdown__menu .tyk-list')
.should('have.css', 'max-width', '200px');
});

it('calls the onTriggerClick function when the trigger button is clicked in case of splitTrigger is set to true', () => {
const onTriggerClick = cy.stub();
const onTriggerClick = cy.stub().as('onTriggerClick');
cy.mount(
<Dropdown2
splitTrigger
Expand All @@ -214,10 +194,8 @@ describe('Dropdown2', () => {
)
.get(`${classes.buttonGroup} ${classes.btn}`)
.first()
.click()
.then(() => {
expect(onTriggerClick).to.be.calledOnce;
expect(onTriggerClick).to.be.calledWith('1');
});
.click();
cy.get('@onTriggerClick')
.should('be.calledOnceWith', '1');
});
});

0 comments on commit 8494b22

Please sign in to comment.