Skip to content

Commit

Permalink
🚸(frontend) improve Grommet Select component
Browse files Browse the repository at this point in the history
Created a wrapper on top of Grommet Select component to improve its
drop down list positioning. The drop down list is now displayed
according to the available space in the viewport.
Its width is also set to the width of the select box.
  • Loading branch information
AntoLC committed Mar 22, 2023
1 parent 9b33deb commit d4ef93b
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 38 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Versioning](https://semver.org/spec/v2.0.0.html).
- Update live sessions API to use nested video ID route
- Move generic widget components from lib-video to lib-components
- Make video dashboard collapsed by default
- improve the dropdown languages positionning in the dashboard (#2138)

### Fixed

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Box, FormField, Image, Select, Text, ThemeContext } from 'grommet';
import { useResponsive } from 'lib-components';
import React, { useEffect, useRef, useState } from 'react';
import { Box, FormField, Image, Text, ThemeContext } from 'grommet';
import { useResponsive, Select } from 'lib-components';
import { useEffect, useState } from 'react';
import { useIntl, defineMessages } from 'react-intl';

import {
Expand All @@ -26,14 +26,6 @@ export const RenaterAuthenticator = () => {
const [optionsDefault, setOptionsDefault] = useState<RenaterSamlFerIdp[]>([]);
const [options, setOptions] = useState<RenaterSamlFerIdp[]>([]);
const { breakpoint, isSmallerBreakpoint } = useResponsive();
const refSelect = useRef<HTMLInputElement>(null);
const [selectDropWidth, setSelectDropWidth] = useState(0);
const [selectDropPosition, setSelectDropPosition] = useState<{
bottom?: 'bottom' | 'top';
top?: 'bottom' | 'top';
}>({ bottom: 'top' });

const selectDropHeight = 400; // px

const renderOption = (option: RenaterSamlFerIdp) => (
<Box align="center" direction="row">
Expand Down Expand Up @@ -68,25 +60,6 @@ export const RenaterAuthenticator = () => {
};
}, []);

useEffect(() => {
function handleResize() {
setSelectDropWidth(refSelect.current?.clientWidth || 0);
setSelectDropPosition(
window.innerHeight - (refSelect.current?.offsetTop || 0) <
selectDropHeight
? { bottom: 'top' }
: { top: 'bottom' },
);
}

handleResize();
window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResize);
};
}, []);

return (
<Box
background="bg-select"
Expand Down Expand Up @@ -121,17 +94,11 @@ export const RenaterAuthenticator = () => {
<ThemeContext.Extend value={{ select: { step: options.length || 20 } }}>
<FormField label={intl.formatMessage(messages.labelSelectRenater)}>
<Select
ref={refSelect}
size="medium"
options={options}
onChange={({ option }: { option: RenaterSamlFerIdp }) => {
window.location.replace(option.login_url);
}}
dropAlign={{ ...selectDropPosition, left: 'left' }}
dropHeight={`${selectDropHeight}px}`}
dropProps={{
width: `${selectDropWidth}px`,
}}
onSearch={(text) => {
// The line below escapes regular expression special characters:
// [ \ ^ $ . | ? * + ( )
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { screen } from '@testing-library/react';
import { render } from 'lib-tests';
import React from 'react';

import { Select } from './index';

window.scrollTo = jest.fn();

describe('<Select />', () => {
it('checks drop position under select', async () => {
render(
<Select
aria-label="my-select"
options={['option 1', 'option 2', 'option 3', 'option 4']}
dropProps={{
'aria-label': 'my-drop',
}}
/>,
);

screen
.getByRole('button', {
name: 'my-select',
})
.click();

expect(await screen.findByLabelText('my-drop')).toHaveStyle(
'transform-origin: bottom left;',
);
});

it('checks drop position above select', async () => {
render(
<Select
aria-label="my-select"
options={['option 1', 'option 2', 'option 3', 'option 4']}
dropProps={{
'aria-label': 'my-drop',
}}
maxDropHeight={-100}
/>,
);

screen
.getByRole('button', {
name: 'my-select',
})
.click();

expect(await screen.findByLabelText('my-drop')).toHaveStyle(
'transform-origin: top left;',
);
});

it('checks if children are correctly displayed', async () => {
render(
<Select
aria-label="my-select"
options={['option 1', 'option 2', 'option 3', 'option 4']}
>
{(option: string) => <div>My {option}</div>}
</Select>,
);

screen
.getByRole('button', {
name: 'my-select',
})
.click();

expect(await screen.findByText('My option 1')).toBeInTheDocument();
expect(screen.getByText('My option 2')).toBeInTheDocument();
expect(screen.getByText('My option 3')).toBeInTheDocument();
expect(screen.getByText('My option 4')).toBeInTheDocument();
});
});
58 changes: 58 additions & 0 deletions src/frontend/packages/lib_components/src/common/Select/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
Select as SelectGrommet,
SelectProps as SelectPropsGrommet,
} from 'grommet';
import React, { PropsWithChildren, useEffect, useRef, useState } from 'react';

/**
* @param maxDropHeight - in pixel, the place needed to the drop to be displayed at the bottom of the select
*/
interface SelectProps extends SelectPropsGrommet {
maxDropHeight?: number; // px
}

export const Select = ({
children,
maxDropHeight = 400,
...props
}: PropsWithChildren<SelectProps>) => {
const refSelect = useRef<HTMLInputElement>(null);
const [selectDropWidth, setSelectDropWidth] = useState(0);
const [selectDropPosition, setSelectDropPosition] = useState<{
bottom?: 'bottom' | 'top';
top?: 'bottom' | 'top';
}>({ bottom: 'top' });

useEffect(() => {
function handleResize() {
setSelectDropWidth(refSelect.current?.clientWidth || 0);
setSelectDropPosition(
document.body.clientHeight - (refSelect.current?.offsetTop || 0) <
maxDropHeight
? { bottom: 'top' }
: { top: 'bottom' },
);
}

handleResize();
window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResize);
};
}, [maxDropHeight]);

return (
<SelectGrommet
ref={refSelect}
dropAlign={{ ...selectDropPosition, left: 'left' }}
dropProps={{
width: `${selectDropWidth}px`,
}}
dropHeight={`${maxDropHeight}px`}
{...props}
>
{children}
</SelectGrommet>
);
};
1 change: 1 addition & 0 deletions src/frontend/packages/lib_components/src/common/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './Button/ButtonLayout';
export * from './Card';

export * from './Form';
export * from './Select';

export * from './Loader';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Select } from 'grommet';
import { useTimedTextTrack, timedTextMode } from 'lib-components';
import { useTimedTextTrack, timedTextMode, Select } from 'lib-components';
import React, { useEffect, useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';

Expand Down

0 comments on commit d4ef93b

Please sign in to comment.