Skip to content

Commit

Permalink
fix(Dropdown): keep options inside the viewport and hide them on outs…
Browse files Browse the repository at this point in the history
…ide scroll
  • Loading branch information
patricia0817 authored and cipak committed Feb 23, 2022
1 parent 208f886 commit 34b3eda
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 28 deletions.
22 changes: 8 additions & 14 deletions src/components/generic/OptionsList/OptionsList.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, {useEffect, useState} from 'react';
import React from 'react';
import PropTypes from 'prop-types';

import webexComponentClasses from '../../helpers';
import {useRef, useElementPosition} from '../../hooks';

import Option from './Option';

/**
Expand All @@ -14,6 +13,7 @@ import Option from './Option';
* @param {Function} props.onSelect A function which will be triggered on option selection
* @param {object[]} props.options Array of options
* @param {string} props.selected Selected option label
* @param {object} props.style Custom style to apply
* @param {number} props.tabIndex Value of the parent's tabIndex
* @param {boolean} props.withKey Options list was opened with keyboard
* @returns {object} JSX of the element
Expand All @@ -24,29 +24,21 @@ export default function OptionsList({
onSelect,
options,
selected,
style,
tabIndex,
withKey,
}) {
const [cssClasses, sc] = webexComponentClasses('options-list', className);
const ref = useRef();
const position = useElementPosition(ref);
const [maxHeight, setMaxHeight] = useState(0);

const onKeyDown = (event) => {
if (event.key === 'Tab') {
onBlur();
}
};

useEffect(() => {
if (position) {
setMaxHeight(window.innerHeight - position.top - window.scrollY - 50);
}
}, [position]);

return (
<div ref={ref} className={cssClasses}>
<ul style={{maxHeight}} role="menu" className={sc('list')} tabIndex={tabIndex} onKeyDown={onKeyDown}>
<div style={style} className={cssClasses}>
<ul role="menu" className={sc('list')} tabIndex={tabIndex} onKeyDown={onKeyDown}>
{options.map((option, index) => (
<Option
key={option.value}
Expand All @@ -72,6 +64,7 @@ OptionsList.propTypes = {
icon: PropTypes.string,
})),
selected: PropTypes.string,
style: PropTypes.shape(),
tabIndex: PropTypes.number,
withKey: PropTypes.bool,
};
Expand All @@ -81,6 +74,7 @@ OptionsList.defaultProps = {
onBlur: undefined,
options: [],
selected: '',
style: undefined,
tabIndex: 0,
withKey: false,
};
12 changes: 8 additions & 4 deletions src/components/hooks/useElementPosition.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,14 @@ export default function useElementPosition(elementRef) {
let cleanup;

if (target) {
const onWindowResize = () => setPosition(target.getBoundingClientRect());

window.addEventListener('resize', onWindowResize);
cleanup = () => window.removeEventListener('resize', onWindowResize);
const updatePosition = () => setPosition(target.getBoundingClientRect());

window.addEventListener('resize', updatePosition);
window.addEventListener('scroll', updatePosition, true);
cleanup = () => {
window.removeEventListener('resize', updatePosition);
window.removeEventListener('scroll', updatePosition, true);
};
}

return cleanup;
Expand Down
44 changes: 38 additions & 6 deletions src/components/inputs/Dropdown/Dropdown.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, {useState, useEffect} from 'react';
import PropTypes from 'prop-types';
import webexComponentClasses from '../../helpers';
import {useElementPosition, useRef} from '../../hooks';

import Icon from '../../generic/Icon/Icon';
import OptionsList from '../../generic/OptionsList/OptionsList';
import webexComponentClasses from '../../helpers';
import {useRef} from '../../hooks';
import Label from '../Label/Label';

/**
Expand Down Expand Up @@ -42,9 +42,12 @@ export default function Dropdown({
value,
}) {
const [expanded, setExpanded] = useState(undefined);
const [layout, setLayout] = useState(undefined);
const [cssClasses, sc] = webexComponentClasses('dropdown', className, {disabled});
const label = options?.find((option) => option.value === value)?.label;
const ref = useRef();
const controlRef = useRef();
const selectedOptionRef = useRef();
const position = useElementPosition(controlRef);

const collapse = () => setExpanded(undefined);
const expand = (withKey) => setExpanded({withKey});
Expand All @@ -57,7 +60,7 @@ export default function Dropdown({
const handleOptionSelect = (option) => {
collapse();
onChange(option.value);
ref.current.focus();
selectedOptionRef.current.focus();
};

const handleKeyDown = (event) => {
Expand Down Expand Up @@ -90,6 +93,34 @@ export default function Dropdown({
return cleanup;
}, [expanded]);

useEffect(() => {
if (position) {
setLayout({
maxHeight: window.innerHeight - position.bottom - window.scrollY - 24,
minWidth: position.width,
top: position.bottom + 4,
});
}
}, [position]);

useEffect(() => {
let cleanup;

if (expanded) {
const handleScroll = (event) => {
if (controlRef.current && !controlRef.current.contains(event.target)) {
collapse();
}
};

window.addEventListener('scroll', handleScroll, true);

cleanup = () => window.removeEventListener('scroll', handleScroll, true);
}

return cleanup;
}, [expanded, controlRef]);

return (
<Label
className={cssClasses}
Expand All @@ -100,7 +131,7 @@ export default function Dropdown({
>
{/* This element handles delegated keyboard events from its descendants (Esc key) */}
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div className={sc('control')} disabled={disabled} onKeyDown={handleKeyDown}>
<div className={sc('control')} ref={controlRef} disabled={disabled} onKeyDown={handleKeyDown}>
<div
className={`${sc('selected-option')} ${expanded ? sc('expanded') : ''}`}
onClick={() => toggleExpanded(false)}
Expand All @@ -109,7 +140,7 @@ export default function Dropdown({
title={tooltip}
onKeyDown={handleSelectedOptionKeyDown}
aria-label={`${label ? `${label}. ` : ''}${ariaLabel}`}
ref={ref}
ref={selectedOptionRef}
>
<span className={sc('label')}>{options === null ? 'Loading...' : (label || value || placeholder)}</span>
<Icon name={expanded ? 'arrow-up' : 'arrow-down'} size={13} />
Expand All @@ -121,6 +152,7 @@ export default function Dropdown({
onSelect={handleOptionSelect}
withKey={expanded.withKey}
selected={value}
style={layout}
tabIndex={tabIndex}
onBlur={collapse}
/>
Expand Down
5 changes: 1 addition & 4 deletions src/components/inputs/Dropdown/Dropdown.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,8 @@ $C: #{$WEBEX_COMPONENTS_CLASS_PREFIX}-dropdown;
}

.#{$C}__options-list {
position: absolute;
position: fixed;
z-index: 1;
min-width: 100%;
top: calc(100% + 0.25rem);
left: 0;
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Storyshots Dropdown/Scroll Example Scroll 1`] = `
Array [
<div
style={
Object {
"border": "1px solid red",
"height": "300px",
}
}
>
<input
type="text"
/>
<div
style={
Object {
"background": "red",
"height": "100px",
"width": "100%",
}
}
/>
<label
className="wxc wxc-label wxc wxc-dropdown"
>
<div
className="wxc-label__control"
>
<div
className="wxc-dropdown__control"
disabled={false}
>
<div
aria-label="undefined"
className="wxc-dropdown__selected-option "
onClick={[Function]}
onKeyDown={[Function]}
role="button"
tabIndex={0}
>
<span
className="wxc-dropdown__label"
/>
<svg
className="wxc-icon "
height={13}
style={Object {}}
viewBox="0 0 26 14"
width={13}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M25.7075 0.293202C25.52 0.105701 25.2656 0.000366211 25.0004 0.000366211C24.7353 0.000366211 24.4809 0.105701 24.2934 0.293202L13 11.5862L1.70699 0.293202C1.51851 0.110381 1.26568 0.00902984 1.0031 0.0110359C0.740528 0.013042 0.489278 0.118244 0.303611 0.303925C0.117943 0.489605 0.0127584 0.740862 0.0107709 1.00344C0.00878336 1.26601 0.110153 1.51883 0.292988 1.7073L12.293 13.7073C12.4805 13.8948 12.7348 14.0001 13 14.0001C13.2652 14.0001 13.5195 13.8948 13.707 13.7073L25.707 1.7073C25.8946 1.51984 26 1.26555 26.0001 1.00036C26.0002 0.735168 25.8949 0.4808 25.7075 0.293202Z"
/>
</svg>
</div>
</div>
</div>
</label>
</div>,
<video
autoPlay={true}
height="0"
id="remote-video"
loop={true}
muted={true}
playsInline={true}
src="./video/ongoing-meeting.mp4"
width="0"
/>,
<video
autoPlay={true}
height="0"
id="remote-share"
loop={true}
muted={true}
playsInline={true}
src="./video/ongoing-share.mp4"
width="0"
/>,
]
`;

exports[`Storyshots Dropdown/Scroll Example Select Example 1`] = `
Array [
<label
className="wxc wxc-label wxc wxc-dropdown"
>
<div
className="wxc-label__control"
>
<div
className="wxc-dropdown__control"
disabled={false}
>
<div
aria-label="undefined"
className="wxc-dropdown__selected-option "
onClick={[Function]}
onKeyDown={[Function]}
role="button"
tabIndex={0}
>
<span
className="wxc-dropdown__label"
/>
<svg
className="wxc-icon "
height={13}
style={Object {}}
viewBox="0 0 26 14"
width={13}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M25.7075 0.293202C25.52 0.105701 25.2656 0.000366211 25.0004 0.000366211C24.7353 0.000366211 24.4809 0.105701 24.2934 0.293202L13 11.5862L1.70699 0.293202C1.51851 0.110381 1.26568 0.00902984 1.0031 0.0110359C0.740528 0.013042 0.489278 0.118244 0.303611 0.303925C0.117943 0.489605 0.0127584 0.740862 0.0107709 1.00344C0.00878336 1.26601 0.110153 1.51883 0.292988 1.7073L12.293 13.7073C12.4805 13.8948 12.7348 14.0001 13 14.0001C13.2652 14.0001 13.5195 13.8948 13.707 13.7073L25.707 1.7073C25.8946 1.51984 26 1.26555 26.0001 1.00036C26.0002 0.735168 25.8949 0.4808 25.7075 0.293202Z"
/>
</svg>
</div>
</div>
</div>
</label>,
<video
autoPlay={true}
height="0"
id="remote-video"
loop={true}
muted={true}
playsInline={true}
src="./video/ongoing-meeting.mp4"
width="0"
/>,
<video
autoPlay={true}
height="0"
id="remote-share"
loop={true}
muted={true}
playsInline={true}
src="./video/ongoing-share.mp4"
width="0"
/>,
]
`;
Loading

0 comments on commit 34b3eda

Please sign in to comment.