From 746d7b00321d96a5ae2bcd6959842db5fb99f2f3 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Tue, 5 Jan 2021 15:39:30 +0100 Subject: [PATCH] feat: add fullscreen option to visualization items [DHIS2-9879] (#1358) Adds fullscreen button to Visualization items * No cypress tests added since it isn't currently possible to test the fullscreen api in cypress. I'll be keeping an eye on if/when this gets implemented in cypress (cypress-io/cypress#7776) * Minor refactor: move check for whether to show ItemHeaderButtons (pluginIsAvailable) out of ItemHeaderButtons and to Item, so that the logic doesn't get too complex in ItemHeaderButtons (would have been first checking pluginIsAvailable, then checking the fullsreen prop) --- .browserslistrc | 5 ++ cypress/assets/backends/sierraLeone_236.js | 8 +++ cypress/integration/ui/fullscreen.feature | 15 +++++ .../integration/ui/fullscreen/fullscreen.js | 18 ++++++ cypress/selectors/dashboardItem.js | 5 +- i18n/en.pot | 7 +- src/components/App.css | 29 +++++++++ src/components/Item/VisualizationItem/Item.js | 64 ++++++++++++++++++- .../VisualizationItem/ItemHeaderButtons.js | 34 ++++++++-- .../Visualization/Visualization.js | 2 +- .../__tests__/ItemHeaderButtons.spec.js | 23 ++++++- .../__tests__/__snapshots__/Item.spec.js.snap | 3 + .../ItemHeaderButtons.spec.js.snap | 13 +++- .../Item/VisualizationItem/assets/icons.js | 28 ++++++++ src/components/ItemGrid/ItemGrid.js | 1 + 15 files changed, 239 insertions(+), 16 deletions(-) create mode 100644 .browserslistrc create mode 100644 cypress/integration/ui/fullscreen.feature create mode 100644 cypress/integration/ui/fullscreen/fullscreen.js diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 000000000..a0b80b79b --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,5 @@ +> 0.5% +last 2 versions +Firefox ESR +not ie 11 +not dead \ No newline at end of file diff --git a/cypress/assets/backends/sierraLeone_236.js b/cypress/assets/backends/sierraLeone_236.js index 7531cdbae..977a5c0f9 100644 --- a/cypress/assets/backends/sierraLeone_236.js +++ b/cypress/assets/backends/sierraLeone_236.js @@ -1,6 +1,14 @@ export const dashboards = { 'Antenatal Care': { route: '#/nghVC4wtyzi', + items: { + text: { + itemUid: 'ILRTXgXvurM', + }, + chart: { + itemUid: 'azz0KRlHgLs', + }, + }, }, Delivery: { route: '#/iMnYyBfSxmM', diff --git a/cypress/integration/ui/fullscreen.feature b/cypress/integration/ui/fullscreen.feature new file mode 100644 index 000000000..ad499715c --- /dev/null +++ b/cypress/integration/ui/fullscreen.feature @@ -0,0 +1,15 @@ +Feature: Item context menu fullscreen + + Background: + Given I open the "Antenatal Care" dashboard + And the "Antenatal Care" dashboard displays in view mode + + @nonmutating + Scenario: Text item does not have a context menu + Then the text item does not have a context menu + + @nonmutating + Scenario: Chart item has a fullscreen option + Then the chart item has a fullscreen option in the context menu + + diff --git a/cypress/integration/ui/fullscreen/fullscreen.js b/cypress/integration/ui/fullscreen/fullscreen.js new file mode 100644 index 000000000..bdb5566f9 --- /dev/null +++ b/cypress/integration/ui/fullscreen/fullscreen.js @@ -0,0 +1,18 @@ +import { Then } from 'cypress-cucumber-preprocessor/steps' +import { + getDashboardItem, + itemMenuButton, + clickMenuButton, +} from '../../../selectors/dashboardItem' +import { dashboards } from '../../../assets/backends' + +Then('the text item does not have a context menu', () => { + getDashboardItem(dashboards['Antenatal Care'].items.text.itemUid) + .find(itemMenuButton) + .should('not.exist') +}) + +Then('the chart item has a fullscreen option in the context menu', () => { + clickMenuButton(dashboards['Antenatal Care'].items.chart.itemUid) + cy.contains('View fullscreen').should('be.visible') +}) diff --git a/cypress/selectors/dashboardItem.js b/cypress/selectors/dashboardItem.js index ed19f5299..0681fec0c 100644 --- a/cypress/selectors/dashboardItem.js +++ b/cypress/selectors/dashboardItem.js @@ -6,11 +6,10 @@ export const tableSel = '.pivot-table-container' export const gridItemSel = '.react-grid-item' export const itemDetailsSel = '[data-test="dashboarditem-footer"]' +export const itemMenuButton = '[data-test="dashboarditem-menu-button"]' export const getDashboardItem = itemUid => cy.get(`[data-test="dashboarditem-${itemUid}"]`) export const clickMenuButton = itemUid => - getDashboardItem(itemUid) - .find('[data-test="dashboarditem-menu-button"]') - .click() + getDashboardItem(itemUid).find(itemMenuButton).click() diff --git a/i18n/en.pot b/i18n/en.pot index 852d282eb..0f32be4f6 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2020-12-09T13:38:15.043Z\n" -"PO-Revision-Date: 2020-12-09T13:38:15.043Z\n" +"POT-Creation-Date: 2020-12-10T12:16:21.390Z\n" +"PO-Revision-Date: 2020-12-10T12:16:21.391Z\n" msgid "Untitled dashboard" msgstr "" @@ -153,6 +153,9 @@ msgstr "" msgid "Open in {{appName}} app" msgstr "" +msgid "View fullscreen" +msgstr "" + msgid "Unable to load the plugin for this item" msgstr "" diff --git a/src/components/App.css b/src/components/App.css index cecfe7266..17c6ef986 100644 --- a/src/components/App.css +++ b/src/components/App.css @@ -29,6 +29,35 @@ table.pivot * { background-color: #48a999; } +div:fullscreen, +div:-webkit-full-screen { + background-color: white; +} + +div:-webkit-full-screen { + object-fit: contain; + position: fixed !important; + top: 0px !important; + right: 0px !important; + bottom: 0px !important; + left: 0px !important; + box-sizing: border-box !important; + min-width: 0px !important; + max-width: none !important; + min-height: 0px !important; + max-height: none !important; + width: 100% !important; + height: 100% !important; + transform: none !important; + margin: 0px !important; +} + +div:fullscreen .dashboard-item-content, +div:-webkit-full-screen .dashboard-item-content { + height: 95vh !important; + width: 100vw !important; +} + @media print { body { width: 100% !important; diff --git a/src/components/Item/VisualizationItem/Item.js b/src/components/Item/VisualizationItem/Item.js index ed4ef4ef5..293b2cc5f 100644 --- a/src/components/Item/VisualizationItem/Item.js +++ b/src/components/Item/VisualizationItem/Item.js @@ -21,6 +21,7 @@ import { import { sGatherAnalyticalObjectStatisticsInDashboardViews } from '../../../reducers/settings' import { acAddVisualization } from '../../../actions/visualizations' import { acSetSelectedItemActiveType } from '../../../actions/selected' +import { pluginIsAvailable } from './Visualization/plugin' import { getDataStatisticsName } from '../../../modules/itemTypes' import { getVisualizationId, getVisualizationName } from '../../../modules/item' import memoizeOne from '../../../modules/memoizeOne' @@ -36,6 +37,7 @@ export class Item extends Component { state = { showFooter: false, configLoaded: false, + isFullscreen: false, } constructor(props, context) { @@ -45,6 +47,7 @@ export class Item extends Component { this.contentRef = React.createRef() this.headerRef = React.createRef() + this.itemDomElSelector = `.reactgriditem-${this.props.item.id}` this.memoizedGetContentHeight = memoizeOne( (calculatedHeight, measuredHeight, preferMeasured) => @@ -73,11 +76,59 @@ export class Item extends Component { console.log(e) } + this.setState({ configLoaded: true }) + + const el = document.querySelector(this.itemDomElSelector) + if (el?.requestFullscreen) { + el.onfullscreenchange = this.handleFullscreenChange + } else if (el?.webkitRequestFullscreen) { + el.onwebkitfullscreenchange = this.handleFullscreenChange + } + } + + componentWillUnmount() { + const el = document.querySelector(this.itemDomElSelector) + if (el?.onfullscreenchange) { + el.removeEventListener( + 'onfullscreenchange', + this.handleFullscreenChange + ) + } else if (el?.onwebkitfullscreenchange) { + el.removeEventListener( + 'onwebkitfullscreenchange', + this.handleFullscreenChange + ) + } + } + + isFullscreenSupported = () => { + const el = document.querySelector(this.itemDomElSelector) + return !!(el?.requestFullscreen || el?.webkitRequestFullscreen) + } + + handleFullscreenChange = () => { this.setState({ - configLoaded: true, + isFullscreen: + !!document.fullscreenElement || + !!document.webkitFullscreenElement, }) } + onToggleFullscreen = () => { + if (!this.state.isFullscreen) { + const el = document.querySelector(this.itemDomElSelector) + if (el?.requestFullscreen) { + el.requestFullscreen() + } else if (el?.webkitRequestFullscreen) { + el.webkitRequestFullscreen() + } + } else { + document.exitFullscreen + ? document.exitFullscreen() + : document.webkitExitFullscreen() + } + } + getUniqueKey = memoizeOne(() => uniqueId()) onToggleFooter = () => { @@ -100,6 +151,10 @@ export class Item extends Component { } getAvailableHeight = () => { + if (this.state.isFullscreen) { + return '95vh' + } + const calculatedHeight = this.props.item.originalHeight - this.headerRef.current.clientHeight - @@ -119,16 +174,19 @@ export class Item extends Component { const { showFooter } = this.state const activeType = this.getActiveType() - const actionButtons = ( + const actionButtons = pluginIsAvailable(activeType || item.type) ? ( - ) + ) : null return ( <> diff --git a/src/components/Item/VisualizationItem/ItemHeaderButtons.js b/src/components/Item/VisualizationItem/ItemHeaderButtons.js index 5eb1f24df..e03ed94cd 100644 --- a/src/components/Item/VisualizationItem/ItemHeaderButtons.js +++ b/src/components/Item/VisualizationItem/ItemHeaderButtons.js @@ -14,8 +14,13 @@ import ChartIcon from '@material-ui/icons/InsertChart' import MapIcon from '@material-ui/icons/Public' import LaunchIcon from '@material-ui/icons/Launch' -import { ThreeDots, SpeechBubble } from './assets/icons' -import { pluginIsAvailable, getLink } from './Visualization/plugin' +import { + ThreeDots, + SpeechBubble, + Fullscreen, + ExitFullscreen, +} from './assets/icons' +import { getLink } from './Visualization/plugin' import { CHART, MAP, @@ -31,7 +36,6 @@ const iconFill = { fill: colors.grey600 } const ItemHeaderButtons = (props, context) => { const [menuIsOpen, setMenuIsOpen] = useState(null) - const { item, visualization, onSelectActiveType, activeType } = props const isTrackerType = isTrackerDomainType(item.type) @@ -60,6 +64,11 @@ const ItemHeaderButtons = (props, context) => { } } + const handleToggleFullscreenClick = () => { + props.onToggleFullscreen() + closeMenu() + } + const openMenu = () => setMenuIsOpen(true) const closeMenu = () => setMenuIsOpen(false) @@ -105,7 +114,11 @@ const ItemHeaderButtons = (props, context) => { const buttonRef = createRef() - return pluginIsAvailable(activeType || item.type) ? ( + return props.isFullscreen ? ( + + ) : ( <>
+`; + +exports[`renders correctly when not fullscreen 1`] = `