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) ? (