From 44297af9656edc8ebf2d18b747b8133603a7c10e Mon Sep 17 00:00:00 2001 From: igoroctaviano Date: Thu, 25 Jun 2020 15:27:42 -0300 Subject: [PATCH 1/2] Create window level dropdown menu --- extensions/default/src/ViewerLayout/index.jsx | 9 +- modes/longitudinal/src/toolbarButtons.js | 50 ++++++++++- .../services/ToolBarService/ToolBarService.js | 15 ++-- platform/ui/index.js | 2 + .../ExpandableToolbarButton.css | 16 ++++ .../ExpandableToolbarButton.jsx | 86 +++++++++++++++++++ .../ExpandableToolbarButton.mdx | 58 +++++++++++++ .../ExpandableToolbarButton/index.js | 2 + .../src/components/IconButton/IconButton.jsx | 4 +- .../ui/src/components/ListMenu/ListMenu.jsx | 65 ++++++++++++++ .../ui/src/components/ListMenu/ListMenu.mdx | 52 +++++++++++ platform/ui/src/components/ListMenu/index.js | 2 + platform/ui/src/components/Select/Select.jsx | 2 +- .../ui/src/components/Tooltip/Tooltip.jsx | 6 +- platform/ui/src/components/index.js | 4 + platform/ui/tailwind.config.js | 7 ++ 16 files changed, 367 insertions(+), 13 deletions(-) create mode 100644 platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.css create mode 100644 platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.jsx create mode 100644 platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.mdx create mode 100644 platform/ui/src/components/ExpandableToolbarButton/index.js create mode 100644 platform/ui/src/components/ListMenu/ListMenu.jsx create mode 100644 platform/ui/src/components/ListMenu/ListMenu.mdx create mode 100644 platform/ui/src/components/ListMenu/index.js diff --git a/extensions/default/src/ViewerLayout/index.jsx b/extensions/default/src/ViewerLayout/index.jsx index 730ee515b..941a5487a 100644 --- a/extensions/default/src/ViewerLayout/index.jsx +++ b/extensions/default/src/ViewerLayout/index.jsx @@ -57,13 +57,20 @@ function ViewerLayout({ const [toolbars, setToolbars] = useState({ primary: [], secondary: [] }); + const onPrimaryClickHandler = (evt, btn) => { + if (btn.props && btn.props.commands && evt.value && btn.props.commands[evt.value]) { + const { commandName, commandOptions } = btn.props.commands[evt.value]; + commandsManager.runCommand(commandName, commandOptions); + } + }; + useEffect(() => { const { unsubscribe } = ToolBarService.subscribe( ToolBarService.EVENTS.TOOL_BAR_MODIFIED, () => { console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT'); const updatedToolbars = { - primary: ToolBarService.getButtonSection('primary'), + primary: ToolBarService.getButtonSection('primary', { onClick: onPrimaryClickHandler }), secondary: ToolBarService.getButtonSection('secondary'), }; setToolbars(updatedToolbars); diff --git a/modes/longitudinal/src/toolbarButtons.js b/modes/longitudinal/src/toolbarButtons.js index 3f4db8160..6fc5634ea 100644 --- a/modes/longitudinal/src/toolbarButtons.js +++ b/modes/longitudinal/src/toolbarButtons.js @@ -1,5 +1,8 @@ // TODO: torn, can either bake this here; or have to create a whole new button type // Only ways that you can pass in a custom React component for render :l +import { ExpandableToolbarButton, ListMenu } from '@ohif/ui'; +import React from 'react'; +import classnames from 'classnames'; export default [ // Divider @@ -29,13 +32,58 @@ export default [ config: { groupName: 'primaryTool', }, + component: ExpandableToolbarButton, props: { isActive: true, icon: 'tool-window-level', - label: 'Levels', commandName: 'setToolActive', commandOptions: { toolName: 'Wwwc' }, + commands: { + 'windowLevelPreset1': { + commandName: 'windowLevelPreset1', + commandOptions: {}, + }, + 'windowLevelPreset2': { + commandName: 'windowLevelPreset2', + commandOptions: {}, + }, + 'windowLevelPreset3': { + commandName: 'windowLevelPreset3', + commandOptions: {}, + }, + 'windowLevelPreset4': { + commandName: 'windowLevelPreset4', + commandOptions: {}, + }, + 'windowLevelPreset5': { + commandName: 'windowLevelPreset5', + commandOptions: {}, + } + }, type: 'primary', + content: ListMenu, + contentProps: { + options: [ + { value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' }, + { value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' }, + { value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' }, + { value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' }, + { value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' }, + ], + renderer: ({ title, subtitle, isActive, index }) => ( + <> +
+ + {title} + + + {subtitle} + +
+ {index + 1} + + ) + } }, }, { diff --git a/platform/core/src/services/ToolBarService/ToolBarService.js b/platform/core/src/services/ToolBarService/ToolBarService.js index 95cc1b431..589839448 100644 --- a/platform/core/src/services/ToolBarService/ToolBarService.js +++ b/platform/core/src/services/ToolBarService/ToolBarService.js @@ -62,7 +62,7 @@ export default class ToolBarService { this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {}); } - getButtonSection(key) { + getButtonSection(key, props) { const buttonSectionIds = this.buttonSections[key]; const buttonsInSection = []; @@ -79,7 +79,7 @@ export default class ToolBarService { btnIds.forEach(nestedBtnId => { const nestedBtn = this.buttons[nestedBtnId]; - const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key); + const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key, props); nestedButtons.push(mappedNestedBtn); }); @@ -90,7 +90,7 @@ export default class ToolBarService { } else { const btnId = btnIdOrArray; const btn = this.buttons[btnId]; - const mappedBtn = this._mapButtonToDisplay(btn, key); + const mappedBtn = this._mapButtonToDisplay(btn, key, props); buttonsInSection.push(mappedBtn); } @@ -136,8 +136,8 @@ export default class ToolBarService { * @param {*} btn * @param {*} btnSection */ - _mapButtonToDisplay(btn, btnSection) { - const { id, type, component, props } = btn; + _mapButtonToDisplay(btn, btnSection, props) { + const { id, type, component } = btn; const buttonType = this._buttonTypes()[type]; if (!buttonType) { @@ -154,12 +154,15 @@ export default class ToolBarService { if (btn.props.clickHandler) { btn.clickHandler(evt, btn, btnSection); } + if (props && props.onClick) { + props.onClick(evt, btn, btnSection, props); + } }; return { id, Component: component || buttonType.defaultComponent, - componentProps: Object.assign({}, props, { onClick }), // + componentProps: Object.assign({}, btn.props, props, { onClick }), // }; } } diff --git a/platform/ui/index.js b/platform/ui/index.js index db85039d9..2025cfb3b 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -32,6 +32,8 @@ export { DateRange, Dialog, EmptyStudies, + ExpandableToolbarButton, + ListMenu, Icon, IconButton, Input, diff --git a/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.css b/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.css new file mode 100644 index 000000000..3a5a6c8be --- /dev/null +++ b/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.css @@ -0,0 +1,16 @@ +.ExpandableToolbarButton:hover .ExpandableToolbarButton__arrow:after { + content: ""; + position: absolute; + bottom: -10px; + border-width: 10px 10px 0; + border-style: solid; + border-color:#5acce6 transparent; +} + +.ExpandableToolbarButton .ExpandableToolbarButton__content { + display: none; +} + +.ExpandableToolbarButton:hover .ExpandableToolbarButton__content { + display: block; +} diff --git a/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.jsx b/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.jsx new file mode 100644 index 000000000..b343d9ed7 --- /dev/null +++ b/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.jsx @@ -0,0 +1,86 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import { IconButton, Icon } from '@ohif/ui'; + +import './ExpandableToolbarButton.css'; + +const ExpandableToolbarButton = ({ + type, + id, + isActive, + onClick, + icon, + className, + content: Content, + contentProps +}) => { + const classes = { + type: { + primary: isActive + ? 'text-black' + : 'text-common-bright hover:bg-primary-dark hover:text-primary-light', + secondary: isActive + ? 'text-black' + : 'text-white hover:bg-secondary-dark focus:bg-secondary-dark', + }, + }; + + const onChildClickHandler = (...args) => { + onClick(...args); + + if (contentProps.onClick) { + contentProps.onClick(...args); + } + }; + + const onClickHandler = (...args) => { + onClick(...args); + }; + + return ( +
+ + + +
+
+ +
+
+
+ ); +}; + +const noop = () => { }; + +ExpandableToolbarButton.defaultProps = { + isActive: false, + type: 'primary', + content: null, + onClick: noop, +}; + +ExpandableToolbarButton.propTypes = { + /* Influences background/hover styling */ + type: PropTypes.oneOf(['primary', 'secondary']), + id: PropTypes.string.isRequired, + isActive: PropTypes.bool, + onClick: PropTypes.func.isRequired, + icon: PropTypes.string.isRequired, + /** Expandable toolbar button content can be replaced for a customized content by passing a node to this value. */ + content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + contentProps: PropTypes.object, +}; + +export default ExpandableToolbarButton; diff --git a/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.mdx b/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.mdx new file mode 100644 index 000000000..b6867f370 --- /dev/null +++ b/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.mdx @@ -0,0 +1,58 @@ +--- +name: Expandable Toolbar Button +menu: General +route: components/expandableToolbarButton +--- + +import { useState } from 'react'; +import { Playground, Props } from 'docz'; +import { ExpandableToolbarButton, ListMenu } from '@ohif/ui'; + +# Toolbar Button + +Expandable Toolbar Buttons are used to populate the Toolbar. + +## Import + +```javascript +import { ExpandableToolbarButton, ListMenu } from '@ohif/ui'; +``` + + + {() => { + const props = { + content: ListMenu, + contentProps: { + options: [ + { value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' }, + { value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' }, + { value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' }, + { value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' }, + { value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' }, + ], + renderer: ({ title, subtitle, isActive, index }) => ( + <> +
+ + {title} + + + {subtitle} + +
+ {index + 1} + + ) + } + }; + return ( +
+ +
+ ); + }} +
+ +## Properties + + diff --git a/platform/ui/src/components/ExpandableToolbarButton/index.js b/platform/ui/src/components/ExpandableToolbarButton/index.js new file mode 100644 index 000000000..2a5e6032c --- /dev/null +++ b/platform/ui/src/components/ExpandableToolbarButton/index.js @@ -0,0 +1,2 @@ +import ExpandableToolbarButton from './ExpandableToolbarButton'; +export default ExpandableToolbarButton; diff --git a/platform/ui/src/components/IconButton/IconButton.jsx b/platform/ui/src/components/IconButton/IconButton.jsx index 475e61620..d92a6b9ca 100644 --- a/platform/ui/src/components/IconButton/IconButton.jsx +++ b/platform/ui/src/components/IconButton/IconButton.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; const baseClasses = - 'text-center items-center justify-center outline-none transition duration-300 ease-in-out font-bold focus:outline-none'; + 'text-center items-center justify-center outline-none font-bold focus:outline-none'; const roundedClasses = { none: '', @@ -113,7 +113,7 @@ const IconButton = ({ }; IconButton.defaultProps = { - onClick: () => {}, + onClick: () => { }, color: 'default', disabled: false, fullWidth: false, diff --git a/platform/ui/src/components/ListMenu/ListMenu.jsx b/platform/ui/src/components/ListMenu/ListMenu.jsx new file mode 100644 index 000000000..56a693740 --- /dev/null +++ b/platform/ui/src/components/ListMenu/ListMenu.jsx @@ -0,0 +1,65 @@ +import React, { useState } from 'react'; +import classnames from 'classnames'; +import PropTypes from 'prop-types'; + +const ListMenu = ({ options = [], renderer, onClick }) => { + const [selectedIndex, setSelectedIndex] = useState(null); + + const ListItem = (props) => { + const flex = 'flex flex-row justify-between items-center'; + const theme = 'bg-indigo-dark'; + const hover = 'hover:bg-primary-dark'; + const spacing = 'p-3 h-8'; + + return ( +
+ {renderer && renderer(props)} +
+ ); + }; + + return ( +
+ {options.map((option, index) => { + const onClickHandler = () => { + setSelectedIndex(index); + onClick({ ...option, index }); + }; + + return ( + + ); + })} +
+ ); +}; + +const noop = () => { }; + +ListMenu.propTypes = { + options: PropTypes.array.isRequired, + renderer: PropTypes.func.isRequired, + onClick: PropTypes.func +}; + +ListMenu.defaultProps = { + onClick: noop +}; + +export default ListMenu; diff --git a/platform/ui/src/components/ListMenu/ListMenu.mdx b/platform/ui/src/components/ListMenu/ListMenu.mdx new file mode 100644 index 000000000..195361e21 --- /dev/null +++ b/platform/ui/src/components/ListMenu/ListMenu.mdx @@ -0,0 +1,52 @@ +--- +name: List Menu +menu: General +route: components/listMenu +--- + +import { useState } from 'react'; +import { Playground, Props } from 'docz'; +import { ListMenu } from '@ohif/ui'; + +# List Menu + +List Menus are used to populate expandable Toolbar. + +## Import + +```javascript +import { ListMenu } from '@ohif/ui'; +``` + + + {() => { + return ( + ( + <> +
+ + {title} + + + {subtitle} + +
+ {index + 1} + + )} + /> + ); + }} +
+ +## Properties + + diff --git a/platform/ui/src/components/ListMenu/index.js b/platform/ui/src/components/ListMenu/index.js new file mode 100644 index 000000000..b9d95c885 --- /dev/null +++ b/platform/ui/src/components/ListMenu/index.js @@ -0,0 +1,2 @@ +import ListMenu from './ListMenu'; +export default ListMenu; diff --git a/platform/ui/src/components/Select/Select.jsx b/platform/ui/src/components/Select/Select.jsx index 3a47d8a6a..399edd541 100644 --- a/platform/ui/src/components/Select/Select.jsx +++ b/platform/ui/src/components/Select/Select.jsx @@ -76,7 +76,7 @@ const Select = ({ options={options} value={selectedOptions} onChange={(selectedOptions, { action }) => { - const newSelection = selectedOptions.reduce( + const newSelection = !selectedOptions.length ? selectedOptions : selectedOptions.reduce( (acc, curr) => acc.concat([curr.value]), [] ); diff --git a/platform/ui/src/components/Tooltip/Tooltip.jsx b/platform/ui/src/components/Tooltip/Tooltip.jsx index fabfa18f6..85119f7c1 100644 --- a/platform/ui/src/components/Tooltip/Tooltip.jsx +++ b/platform/ui/src/components/Tooltip/Tooltip.jsx @@ -24,7 +24,7 @@ const arrowPositionStyle = { }, }; -const Tooltip = ({ content, isSticky, position, tight, children }) => { +const Tooltip = ({ content, isSticky, position, tight, children, isDisabled }) => { const [isActive, setIsActive] = useState(false); const handleMouseOver = () => { @@ -39,7 +39,7 @@ const Tooltip = ({ content, isSticky, position, tight, children }) => { } }; - const isOpen = isSticky || isActive; + const isOpen = (isSticky || isActive) && !isDisabled; return (
Date: Thu, 25 Jun 2020 15:06:14 -0400 Subject: [PATCH 2/2] more descriptive comment for isDisabled prop --- platform/ui/src/components/Tooltip/Tooltip.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/ui/src/components/Tooltip/Tooltip.jsx b/platform/ui/src/components/Tooltip/Tooltip.jsx index 85119f7c1..42ef4498b 100644 --- a/platform/ui/src/components/Tooltip/Tooltip.jsx +++ b/platform/ui/src/components/Tooltip/Tooltip.jsx @@ -88,7 +88,7 @@ Tooltip.defaultProps = { }; Tooltip.propTypes = { - // Allow null +/** prevents tooltip from rendering despite hover/active/sticky */ isDisabled: PropTypes.bool, content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), position: PropTypes.oneOf([