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`] = `