Skip to content

Commit

Permalink
change: [M3-7182] - Update RemovableSelectionsList default maximum he…
Browse files Browse the repository at this point in the history
…ight to cut off the last list item (#9827)

* update removable list height to cut off last item if scrolling is necessary

* add changeset

* box shadow for removable selections list

* fix safari box shadow display (css really really got me ;-;)

* cleanup, make dropshadow appear more dynamically
  • Loading branch information
coliu-akamai authored Oct 26, 2023
1 parent 9cd7af2 commit 55b1205
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 105 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Update `RemovableSelectionsList` default maximum height to cut off last list item and indicate scrolling ([#9827](https://github.com/linode/manager/pull/9827))
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { styled } from '@mui/material/styles';

import { Box } from 'src/components/Box';
import { List } from 'src/components/List';
import { ListItem } from 'src/components/ListItem';
import { omittedProps } from 'src/utilities/omittedProps';

import type { RemovableSelectionsListProps } from './RemovableSelectionsList';

export const StyledNoAssignedLinodesBox = styled(Box, {
label: 'StyledNoAssignedLinodesBox',
shouldForwardProp: omittedProps(['maxWidth']),
})(({ maxWidth, theme }) => ({
background: theme.name === 'light' ? theme.bg.main : theme.bg.app,
display: 'flex',
flexDirection: 'column',
height: '52px',
justifyContent: 'center',
maxWidth: maxWidth ? `${maxWidth}px` : '416px',
paddingLeft: theme.spacing(2),
width: '100%',
}));

export const SelectedOptionsHeader = styled('h4', {
label: 'SelectedOptionsHeader',
})(({ theme }) => ({
color: theme.color.headline,
fontFamily: theme.font.bold,
fontSize: '14px',
textTransform: 'initial',
}));

export const SelectedOptionsList = styled(List, {
label: 'SelectedOptionsList',
shouldForwardProp: omittedProps(['isRemovable']),
})<{ isRemovable?: boolean }>(({ isRemovable, theme }) => ({
background: theme.name === 'light' ? theme.bg.main : theme.bg.app,
padding: !isRemovable ? `${theme.spacing(2)} 0` : '5px 0',
width: '100%',
}));

export const SelectedOptionsListItem = styled(ListItem, {
label: 'SelectedOptionsListItem',
})(() => ({
justifyContent: 'space-between',
paddingBottom: 0,
paddingRight: 4,
paddingTop: 0,
}));

export const StyledLabel = styled('span', { label: 'StyledLabel' })(
({ theme }) => ({
color: theme.color.label,
fontFamily: theme.font.semiBold,
fontSize: '14px',
})
);

type StyledBoxShadowWrapperBoxProps = Pick<
RemovableSelectionsListProps,
'maxHeight' | 'maxWidth'
>;

export const StyledBoxShadowWrapper = styled(Box, {
label: 'StyledBoxShadowWrapper',
shouldForwardProp: omittedProps(['displayShadow']),
})<{ displayShadow: boolean; maxWidth: number }>(
({ displayShadow, maxWidth, theme }) => ({
'&:after': {
bottom: 0,
content: '""',
height: '15px',
position: 'absolute',
width: '100%',
...(displayShadow && {
boxShadow: `${theme.color.boxShadow} 0px -15px 10px -10px inset`,
}),
},
maxWidth: `${maxWidth}px`,
position: 'relative',
})
);

export const StyledScrollBox = styled(Box, {
label: 'StyledScrollBox',
})<StyledBoxShadowWrapperBoxProps>(({ maxHeight, maxWidth }) => ({
maxHeight: `${maxHeight}px`,
maxWidth: `${maxWidth}px`,
overflow: 'auto',
}));
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,12 @@ describe('Removable Selections List', () => {
fireEvent.click(removeButton);
expect(props.onRemove).toHaveBeenCalled();
});

it('should not display the remove button for a list item', () => {
const screen = renderWithTheme(
<RemovableSelectionsList {...props} isRemovable={false} />
);
const removeButton = screen.queryByLabelText(`remove my-linode-1`);
expect(removeButton).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import Close from '@mui/icons-material/Close';
import { styled } from '@mui/material/styles';
import * as React from 'react';

import { Box } from 'src/components/Box';
import { IconButton } from 'src/components/IconButton';
import { List } from 'src/components/List';
import { ListItem } from 'src/components/ListItem';
import { omittedProps } from 'src/utilities/omittedProps';

import {
SelectedOptionsHeader,
SelectedOptionsList,
SelectedOptionsListItem,
StyledBoxShadowWrapper,
StyledLabel,
StyledNoAssignedLinodesBox,
StyledScrollBox,
} from './RemovableSelectionsList.style';

export type RemovableItem = {
id: number;
Expand All @@ -16,7 +21,7 @@ export type RemovableItem = {
// Trying to type them as 'unknown' led to type errors.
} & { [key: string]: any };

interface Props {
export interface RemovableSelectionsListProps {
/**
* The descriptive text to display above the list
*/
Expand All @@ -26,11 +31,11 @@ interface Props {
*/
isRemovable?: boolean;
/**
* The maxHeight of the list component, in px
* The maxHeight of the list component, in px. The default max height is 427px.
*/
maxHeight?: number;
/**
* The maxWidth of the list component, in px
* The maxWidth of the list component, in px. The default max width is 416px.
*/
maxWidth?: number;
/**
Expand All @@ -53,18 +58,30 @@ interface Props {
selectionData: RemovableItem[];
}

export const RemovableSelectionsList = (props: Props) => {
export const RemovableSelectionsList = (
props: RemovableSelectionsListProps
) => {
const {
headerText,
isRemovable = true,
maxHeight,
maxWidth,
maxHeight = 427,
maxWidth = 416,
noDataText,
onRemove,
preferredDataLabel,
selectionData,
} = props;

// used to determine when to display a box-shadow to indicate scrollability
const listRef = React.useRef<HTMLUListElement>(null);
const [listHeight, setListHeight] = React.useState<number>(0);

React.useEffect(() => {
if (listRef.current) {
setListHeight(listRef.current.clientHeight);
}
}, [selectionData]);

const handleOnClick = (selection: RemovableItem) => {
onRemove(selection);
};
Expand All @@ -73,37 +90,38 @@ export const RemovableSelectionsList = (props: Props) => {
<>
<SelectedOptionsHeader>{headerText}</SelectedOptionsHeader>
{selectionData.length > 0 ? (
<SelectedOptionsList
sx={{
maxHeight: maxHeight ? `${maxHeight}px` : '450px',
maxWidth: maxWidth ? `${maxWidth}px` : '416px',
}}
isRemovable={isRemovable}
<StyledBoxShadowWrapper
displayShadow={listHeight > maxHeight}
maxWidth={maxWidth}
>
{selectionData.map((selection) => (
<SelectedOptionsListItem alignItems="center" key={selection.id}>
<StyledLabel>
{preferredDataLabel
? selection[preferredDataLabel]
: selection.label}
</StyledLabel>
{isRemovable && (
<IconButton
aria-label={`remove ${
preferredDataLabel
<StyledScrollBox maxHeight={maxHeight} maxWidth={maxWidth}>
<SelectedOptionsList isRemovable={isRemovable} ref={listRef}>
{selectionData.map((selection) => (
<SelectedOptionsListItem alignItems="center" key={selection.id}>
<StyledLabel>
{preferredDataLabel
? selection[preferredDataLabel]
: selection.label
}`}
disableRipple
onClick={() => handleOnClick(selection)}
size="medium"
>
<Close />
</IconButton>
)}
</SelectedOptionsListItem>
))}
</SelectedOptionsList>
: selection.label}
</StyledLabel>
{isRemovable && (
<IconButton
aria-label={`remove ${
preferredDataLabel
? selection[preferredDataLabel]
: selection.label
}`}
disableRipple
onClick={() => handleOnClick(selection)}
size="medium"
>
<Close />
</IconButton>
)}
</SelectedOptionsListItem>
))}
</SelectedOptionsList>
</StyledScrollBox>
</StyledBoxShadowWrapper>
) : (
<StyledNoAssignedLinodesBox maxWidth={maxWidth}>
<StyledLabel>{noDataText}</StyledLabel>
Expand All @@ -112,51 +130,3 @@ export const RemovableSelectionsList = (props: Props) => {
</>
);
};

const StyledNoAssignedLinodesBox = styled(Box, {
label: 'StyledNoAssignedLinodesBox',
shouldForwardProp: omittedProps(['maxWidth']),
})(({ maxWidth, theme }) => ({
background: theme.name === 'light' ? theme.bg.main : theme.bg.app,
display: 'flex',
flexDirection: 'column',
height: '52px',
justifyContent: 'center',
maxWidth: maxWidth ? `${maxWidth}px` : '416px',
paddingLeft: theme.spacing(2),
width: '100%',
}));

const SelectedOptionsHeader = styled('h4', {
label: 'SelectedOptionsHeader',
})(({ theme }) => ({
color: theme.color.headline,
fontFamily: theme.font.bold,
fontSize: '14px',
textTransform: 'initial',
}));

const SelectedOptionsList = styled(List, {
label: 'SelectedOptionsList',
shouldForwardProp: omittedProps(['isRemovable']),
})<{ isRemovable?: boolean }>(({ isRemovable, theme }) => ({
background: theme.name === 'light' ? theme.bg.main : theme.bg.app,
overflow: 'auto',
padding: !isRemovable ? '16px 0' : '5px 0',
width: '100%',
}));

const SelectedOptionsListItem = styled(ListItem, {
label: 'SelectedOptionsListItem',
})(() => ({
justifyContent: 'space-between',
paddingBottom: 0,
paddingRight: 4,
paddingTop: 0,
}));

const StyledLabel = styled('span', { label: 'StyledLabel' })(({ theme }) => ({
color: theme.color.label,
fontFamily: theme.font.semiBold,
fontSize: '14px',
}));
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,6 @@ class SelectAppPanel extends React.PureComponent<Props> {
}

const commonStyling = (theme: Theme) => ({
boxShadow: `${theme.color.boxShadow} 0px -15px 10px -10px inset`,
height: 450,
marginBottom: theme.spacing(3),
overflowY: 'auto' as const,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,22 +233,24 @@ export class FromAppsContent extends React.Component<CombinedProps, State> {
</StyledFilterBox>
</StyledSearchFilterBox>
</Paper>
<SelectAppPanel
appInstances={
isSearching || isFiltering ? filteredApps : appInstances
}
appInstancesError={appInstancesError}
appInstancesLoading={appInstancesLoading}
disabled={userCannotCreateLinode}
error={hasErrorFor('stackscript_id')}
flags={flags}
handleClick={handleSelectStackScript}
isFiltering={isFiltering}
isSearching={isSearching}
openDrawer={this.openDrawer}
searchValue={query}
selectedStackScriptID={selectedStackScriptID}
/>
<StyledBoxShadowWrapper>
<SelectAppPanel
appInstances={
isSearching || isFiltering ? filteredApps : appInstances
}
appInstancesError={appInstancesError}
appInstancesLoading={appInstancesLoading}
disabled={userCannotCreateLinode}
error={hasErrorFor('stackscript_id')}
flags={flags}
handleClick={handleSelectStackScript}
isFiltering={isFiltering}
isSearching={isSearching}
openDrawer={this.openDrawer}
searchValue={query}
selectedStackScriptID={selectedStackScriptID}
/>
</StyledBoxShadowWrapper>
{!userCannotCreateLinode && selectedStackScriptLabel ? (
<UserDefinedFieldsPanel
updateFor={[
Expand Down Expand Up @@ -415,3 +417,17 @@ const StyledSearchBox = styled(Box, { label: 'StyledSearchBox' })({
},
flexGrow: 10,
});

const StyledBoxShadowWrapper = styled('div', {
label: 'StyledBoxShadowWrapper',
})(({ theme }) => ({
'&:after': {
bottom: 0,
boxShadow: `${theme.color.boxShadow} 0px -15px 10px -10px inset`,
content: '""',
height: '15px',
position: 'absolute',
width: '100%',
},
position: 'relative',
}));

0 comments on commit 55b1205

Please sign in to comment.