feat(4D): Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode (#3664)
Co-authored-by: Alireza <ar.sedghi@gmail.com> Co-authored-by: Neil <neil.a.macphee@gmail.com>
This commit is contained in:
200 files changed
+9589
-2178
No files matched your search
@@ -0,0 +1,27 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { ProgressDropdownOption, ProgressDropdownOptionPropType } from './types';
|
||||
|
||||
const ProgressDiscreteBar = ({ options }: { options: ProgressDropdownOption[] }): ReactElement => {
|
||||
return (
|
||||
<div className="flex">
|
||||
{options.map((option, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={classnames('mr-1 h-1 grow first:rounded-l-sm last:mr-0 last:rounded-r-sm', {
|
||||
'bg-black': !option.activated && !option.completed,
|
||||
'bg-primary-main': option.activated && !option.completed,
|
||||
'bg-primary-light': option.completed,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProgressDiscreteBar.propTypes = {
|
||||
options: PropTypes.arrayOf(ProgressDropdownOptionPropType).isRequired,
|
||||
};
|
||||
|
||||
export default ProgressDiscreteBar;
|
||||
@@ -0,0 +1,168 @@
|
||||
import React, { ReactNode, useEffect, useCallback, useState, useMemo, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import ProgressDiscreteBar from './ProgressDiscreteBar';
|
||||
import ProgressItemDetail from './ProgressItemDetail';
|
||||
import ProgressItem from './ProgressItem';
|
||||
import { Icon } from '../../components';
|
||||
import { ProgressDropdownOption, ProgressDropdownOptionPropType } from './types';
|
||||
|
||||
const ProgressDropdown = ({
|
||||
options: optionsProps,
|
||||
value,
|
||||
children,
|
||||
dropDownWidth = '170',
|
||||
onChange,
|
||||
}: {
|
||||
options: ProgressDropdownOption[];
|
||||
value?: string;
|
||||
children?: ReactNode;
|
||||
onChange?: ({ selectedOption }) => void;
|
||||
}): JSX.Element => {
|
||||
const element = useRef(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const toggleOpen = () => setOpen(s => !s);
|
||||
const [options, setOptions] = useState(optionsProps);
|
||||
const [selectedOption, setSelectedOption] = useState(undefined);
|
||||
|
||||
const selectedOptionIndex = useMemo(
|
||||
() => options.findIndex(option => option.value === selectedOption?.value),
|
||||
[options, selectedOption]
|
||||
);
|
||||
|
||||
const canMoveNext = useMemo(
|
||||
() => selectedOptionIndex < options.length - 1,
|
||||
[selectedOptionIndex, options]
|
||||
);
|
||||
|
||||
const handleOptionSelected = useCallback(
|
||||
(newSelectedOption?: ProgressDropdownOption): void => {
|
||||
if (newSelectedOption?.value === selectedOption?.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
setSelectedOption(newSelectedOption);
|
||||
|
||||
if (newSelectedOption) {
|
||||
newSelectedOption.activated = true;
|
||||
newSelectedOption.onSelect?.();
|
||||
}
|
||||
|
||||
if (onChange) {
|
||||
onChange({ selectedOption: newSelectedOption });
|
||||
}
|
||||
},
|
||||
[selectedOption, onChange]
|
||||
);
|
||||
|
||||
const handleNextButtonClick = useCallback(() => {
|
||||
if (canMoveNext) {
|
||||
handleOptionSelected(options[selectedOptionIndex + 1]);
|
||||
}
|
||||
}, [options, selectedOptionIndex, canMoveNext, handleOptionSelected]);
|
||||
|
||||
// Update the options in case the options from props has changed
|
||||
useEffect(() => setOptions(optionsProps), [optionsProps]);
|
||||
|
||||
// Updates the selected item based on the value from props
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOption = value ? options.find(option => option.value === value) : undefined;
|
||||
|
||||
handleOptionSelected(newOption);
|
||||
}, [value, options, handleOptionSelected]);
|
||||
|
||||
// Listen to any click event outside of the dropdown context to hide the options
|
||||
useEffect(() => {
|
||||
const handleDocumentClick = e => {
|
||||
if (element.current && !element.current.contains(e.target)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleDocumentClick);
|
||||
|
||||
if (!open) {
|
||||
document.removeEventListener('click', handleDocumentClick);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={element}
|
||||
className="relative grow select-none text-[0px] text-white"
|
||||
>
|
||||
<div>
|
||||
<div className="mb-1.5 flex h-[26px]">
|
||||
<div
|
||||
className="bg-secondary-dark border-primary-main flex grow cursor-pointer rounded border"
|
||||
style={{ width: `${dropDownWidth}px` }}
|
||||
onClick={toggleOpen}
|
||||
>
|
||||
<div className="flex grow">
|
||||
{selectedOption && <ProgressItemDetail option={selectedOption} />}
|
||||
|
||||
{!selectedOption && <div className="ml-1 grow text-base leading-6">{children}</div>}
|
||||
</div>
|
||||
<Icon
|
||||
name="chevron-down"
|
||||
className="text-primary-active mt-0.5 ml-1"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className={classnames('ml-1.5 w-[26px] rounded text-base', {
|
||||
'bg-primary-main': canMoveNext,
|
||||
'bg-primary-dark pointer-events-none': !canMoveNext,
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
name="arrow-right"
|
||||
className={classnames('relative left-0.5 h-6 w-6 text-white', {
|
||||
'text-white': canMoveNext,
|
||||
'text-secondary-light': !canMoveNext,
|
||||
})}
|
||||
onClick={handleNextButtonClick}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={classnames(
|
||||
'absolute top-7 left-0 right-8 z-10 mt-0.5 origin-top',
|
||||
'bg-primary-dark overflow-hidden transition-[max-height] duration-300 ',
|
||||
'border-secondary-main rounded border shadow',
|
||||
{
|
||||
hidden: !open,
|
||||
'max-h-[500px]': open,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<ProgressItem
|
||||
key={index}
|
||||
option={option}
|
||||
onSelect={() => handleOptionSelected(option)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ProgressDiscreteBar options={options} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProgressDropdown.propTypes = {
|
||||
options: PropTypes.arrayOf(ProgressDropdownOptionPropType).isRequired,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
dropDownWidth: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ProgressDropdown;
|
||||
@@ -0,0 +1,31 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ProgressItemDetail from './ProgressItemDetail';
|
||||
import { ProgressDropdownOption, ProgressDropdownOptionPropType } from './types';
|
||||
|
||||
const ProgressItem = ({
|
||||
option,
|
||||
onSelect,
|
||||
}: {
|
||||
option: ProgressDropdownOption;
|
||||
onSelect: (option: ProgressDropdownOption) => void;
|
||||
}): ReactElement => {
|
||||
const { value } = option;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={value}
|
||||
className={'hover:bg-secondary-main flex cursor-pointer py-1 transition duration-1000'}
|
||||
onClick={() => onSelect(option)}
|
||||
>
|
||||
<ProgressItemDetail option={option} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProgressItem.propTypes = {
|
||||
option: ProgressDropdownOptionPropType.isRequired,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ProgressItem;
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, { useState, useMemo, ReactElement } from 'react';
|
||||
import { Icon, Tooltip } from '../../components';
|
||||
import { ProgressDropdownOption, ProgressDropdownOptionPropType } from './types';
|
||||
|
||||
const MAX_TOOLTIP_LENGTH = 150;
|
||||
const iconClassNames = 'grow-0 text-primary-light h-4 w-4 mt-1 mr-2 mb-0 ml-1';
|
||||
|
||||
const ProgressItemDetail = ({ option }: { option: ProgressDropdownOption }): ReactElement => {
|
||||
const { label, info, completed } = option;
|
||||
const [truncate, setTruncate] = useState(true);
|
||||
const handleOnHideTooltip = () => setTruncate(true);
|
||||
let icon;
|
||||
|
||||
if (completed) {
|
||||
icon = 'status-tracked';
|
||||
} else if (info) {
|
||||
icon = 'launch-info';
|
||||
}
|
||||
|
||||
const tooltipText = useMemo(() => {
|
||||
if (!truncate || !info || info.length <= MAX_TOOLTIP_LENGTH) {
|
||||
return info;
|
||||
}
|
||||
|
||||
const handleReadMoreClick = e => {
|
||||
setTruncate(false);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{info.slice(0, MAX_TOOLTIP_LENGTH)}
|
||||
<button
|
||||
className="text-primary-active font-bold"
|
||||
onClick={handleReadMoreClick}
|
||||
>
|
||||
Read more...
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}, [info, truncate]);
|
||||
|
||||
const iconContent = (
|
||||
<>
|
||||
{icon && (
|
||||
<div>
|
||||
<Icon
|
||||
name={icon}
|
||||
className={iconClassNames}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!icon && <div className={iconClassNames} />}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{info && (
|
||||
<Tooltip
|
||||
content={tooltipText}
|
||||
position="bottom-left"
|
||||
tooltipBoxClassName={'max-w-xs'}
|
||||
onHide={handleOnHideTooltip}
|
||||
>
|
||||
{iconContent}
|
||||
</Tooltip>
|
||||
)}
|
||||
{!info && iconContent}
|
||||
|
||||
<div className="grow text-base leading-6">{label}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ProgressItemDetail.propTypes = {
|
||||
option: ProgressDropdownOptionPropType.isRequired,
|
||||
};
|
||||
|
||||
export default ProgressItemDetail;
|
||||
@@ -0,0 +1,106 @@
|
||||
import { ProgressDropdown } from '../../../components';
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
|
||||
|
||||
export const argTypes = {
|
||||
component: ProgressDropdown,
|
||||
title: 'Components/ProgressDropdown',
|
||||
};
|
||||
|
||||
<Meta
|
||||
title="Components/ProgressDropdown"
|
||||
component={ProgressDropdown}
|
||||
/>
|
||||
|
||||
export const ProgressDropdownTemplate = args => (
|
||||
<div className="mb-2.5 flex h-60 max-w-xs">
|
||||
<ProgressDropdown {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const loremIpsumText = new Array(7)
|
||||
.join(` Lorem ipsum, dolor sit amet consectetur adipisicing elit`)
|
||||
.substring(1);
|
||||
|
||||
<Heading
|
||||
title="ProgressDropdown"
|
||||
componentRelativePath="ProgressDropdown/ProgressDropdown.tsx"
|
||||
/>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Props](#props)
|
||||
- [Usage](#usage)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
## Overview
|
||||
|
||||
Dropdown is a modal Dropdown component which enabled to show a modal Dropdown.
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="ProgressDropdownOverview"
|
||||
args={{
|
||||
options: [],
|
||||
children: '[empty dropdown]',
|
||||
}}
|
||||
>
|
||||
{ProgressDropdownTemplate.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={ProgressDropdown} />
|
||||
|
||||
## Usage
|
||||
|
||||
You can list the items in the dropdown. The status of each item changes from black to dark blue in
|
||||
the status bar beneath the dropdown when the item is selected.
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="ProgressDropdownList"
|
||||
onChange={selectedItem => (selectedItem.completed = true)}
|
||||
args={{
|
||||
options: [
|
||||
{
|
||||
label: 'Item 1',
|
||||
value: 'item1',
|
||||
info: `Item 1 - ${loremIpsumText}`,
|
||||
onClick: () => {
|
||||
alert('Item 1 clicked');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Item 2',
|
||||
value: 'item2',
|
||||
info: `Item 2 - ${loremIpsumText}`,
|
||||
onClick: () => {
|
||||
alert('Item 2 clicked');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Item 3',
|
||||
value: 'item3',
|
||||
info: `Item 3 - ${loremIpsumText}`,
|
||||
onClick: () => {
|
||||
alert('Item 3 clicked');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Item 4',
|
||||
info: `Item 4 - ${loremIpsumText}`,
|
||||
onClick: () => {
|
||||
alert('Item 4 clicked');
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{ProgressDropdownTemplate.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Contribute
|
||||
|
||||
<Footer componentRelativePath="ProgressDropdown/__stories__/progressDropdown.stories.mdx" />
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ProgressDropdown';
|
||||
@@ -0,0 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export type ProgressDropdownOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
info?: string;
|
||||
activated?: boolean;
|
||||
completed?: boolean;
|
||||
onSelect?: () => void;
|
||||
};
|
||||
|
||||
export const ProgressDropdownOptionPropType = PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
info: PropTypes.string,
|
||||
activated: PropTypes.bool,
|
||||
completed: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
});
|
||||
Reference in new issue
Block a user