(core) Collapse inactive view sections on mobile screens.

Summary:
Implement an approach to makind multi-section screens usable on mobile by
collapsing inactive sections to a small area. When clicked, they become active
and expand, while the rest of the sections are collapsed.

Test Plan: Added a basic test case of collapsing inactive sections.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2725
This commit is contained in:
Dmitry S 2021-02-08 23:40:04 -05:00
parent 890a8709f3
commit 6c10a43c5d
5 changed files with 108 additions and 42 deletions

View File

@ -34,7 +34,7 @@ import {App} from 'app/client/ui/App';
import {DocHistory} from 'app/client/ui/DocHistory'; import {DocHistory} from 'app/client/ui/DocHistory';
import {IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker'; import {IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
import {IPageWidgetLink, linkFromId, selectBy} from 'app/client/ui/selectBy'; import {IPageWidgetLink, linkFromId, selectBy} from 'app/client/ui/selectBy';
import {testId} from 'app/client/ui2018/cssVars'; import {mediaSmall, testId} from 'app/client/ui2018/cssVars';
import {IconName} from 'app/client/ui2018/IconList'; import {IconName} from 'app/client/ui2018/IconList';
import {ActionGroup} from 'app/common/ActionGroup'; import {ActionGroup} from 'app/common/ActionGroup';
import {delay} from 'app/common/delay'; import {delay} from 'app/common/delay';
@ -687,6 +687,11 @@ const cssViewContentPane = styled('div', `
position: relative; position: relative;
min-width: 240px; min-width: 240px;
margin: 12px; margin: 12px;
@media ${mediaSmall} {
& {
margin: 4px;
}
}
@media print { @media print {
& { & {
margin: 0px; margin: 0px;

View File

@ -17,6 +17,7 @@
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
min-width: 0px; min-width: 0px;
flex-grow: var(--flex-grow, 1) !important;
} }
.layout_hbox.layout_fill_window { .layout_hbox.layout_fill_window {

View File

@ -140,7 +140,7 @@ LayoutBox.prototype.buildDom = function() {
kd.cssClass(wrap(function() { return (self.layout.fillWindow ? 'layout_fill_window' : kd.cssClass(wrap(function() { return (self.layout.fillWindow ? 'layout_fill_window' :
(self.isLastChild() ? 'layout_last_child' : null)); (self.isLastChild() ? 'layout_last_child' : null));
})), })),
kd.style('flexGrow', wrap(function() { kd.style('--flex-grow', wrap(function() {
return (self.isVBox() || (self.isHBox() && self.layout.fillWindow)) ? self.flexSize() : ''; return (self.isVBox() || (self.isHBox() && self.layout.fillWindow)) ? self.flexSize() : '';
})), })),
kd.domData('layoutBox', this), kd.domData('layoutBox', this),

View File

@ -4,34 +4,15 @@
} }
.viewsection_title { .viewsection_title {
background-color: #e5e5e5;
color: black;
flex-shrink: 0; flex-shrink: 0;
align-items: baseline; align-items: baseline;
line-height: 2;
cursor: default; cursor: default;
}
.active_section > .viewsection_title {
background-color: #3e568e;
color: white;
}
.viewsection_content.newui > .viewsection_title {
height: 24px; height: 24px;
margin-left: -16px; /* to include drag handle that shows up on hover */ margin-left: -16px; /* to include drag handle that shows up on hover */
color: var(--grist-color-slate); color: var(--grist-color-slate);
background-color: unset;
font-size: var(--grist-small-font-size); font-size: var(--grist-small-font-size);
font-weight: 500; font-weight: 500;
text-transform: uppercase; text-transform: uppercase;
line-height: inherit;
}
.active_section.newui > .viewsection_title {
background-color: unset;
color: var(--grist-color-slate);
} }
.viewsection_titletext { .viewsection_titletext {
@ -41,14 +22,8 @@
.viewsection_content { .viewsection_content {
background-color: #ffffff; background-color: #ffffff;
margin: 4px;
overflow: visible; overflow: visible;
box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.8);
}
.viewsection_content.newui {
margin: 12px; margin: 12px;
box-shadow: none;
} }
.viewsection_title_colorbox { .viewsection_title_colorbox {
@ -59,15 +34,10 @@
box-shadow: inset 0px 0px 5px rgba(0,0,0,0.5); box-shadow: inset 0px 0px 5px rgba(0,0,0,0.5);
} }
/* TODO should be switched to use new icon */
.viewsection_drag_indicator { .viewsection_drag_indicator {
visibility: hidden; visibility: hidden;
margin: auto;
padding: 0px 2px;
top: 0px; top: 0px;
}
/* TODO should be switched to use new icon */
.viewsection_drag_indicator.newui {
width: 16px; width: 16px;
height: 16px; height: 16px;
margin: 0; margin: 0;
@ -131,19 +101,16 @@
.view_data_pane_container { .view_data_pane_container {
position: relative; position: relative;
flex: auto; flex: auto;
}
.viewsection_content.newui > .view_data_pane_container {
border: 1px solid var(--grist-color-dark-grey); border: 1px solid var(--grist-color-dark-grey);
} }
@media not print { @media not print {
.active_section.newui > .view_data_pane_container { .active_section > .view_data_pane_container {
box-shadow: -2px 0 0 0px var(--grist-color-light-green); box-shadow: -2px 0 0 0px var(--grist-color-light-green);
border-left: 1px solid var(--grist-color-light-green); border-left: 1px solid var(--grist-color-light-green);
} }
.active_section.newui > .view_data_pane_container.viewsection_type_detail { .active_section > .view_data_pane_container.viewsection_type_detail {
/* this color is a translucent version of grist-color-light-green */ /* this color is a translucent version of grist-color-light-green */
box-shadow: -2px 0 0 0px var(--grist-color-inactive-cursor); box-shadow: -2px 0 0 0px var(--grist-color-inactive-cursor);
border-left: 1px solid var(--grist-color-inactive-cursor); border-left: 1px solid var(--grist-color-inactive-cursor);

View File

@ -13,11 +13,11 @@ import {createObsArray} from 'app/client/lib/koArrayWrap';
import {ViewRec, ViewSectionRec} from 'app/client/models/DocModel'; import {ViewRec, ViewSectionRec} from 'app/client/models/DocModel';
import {reportError} from 'app/client/models/errors'; import {reportError} from 'app/client/models/errors';
import {viewSectionMenu} from 'app/client/ui/ViewSectionMenu'; import {viewSectionMenu} from 'app/client/ui/ViewSectionMenu';
import {testId} from 'app/client/ui2018/cssVars'; import {colors, mediaSmall, testId} from 'app/client/ui2018/cssVars';
import {editableLabel} from 'app/client/ui2018/editableLabel'; import {editableLabel} from 'app/client/ui2018/editableLabel';
import {DisposableWithEvents} from 'app/common/DisposableWithEvents'; import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
import {mod} from 'app/common/gutil'; import {mod} from 'app/common/gutil';
import {computedArray, Disposable, dom, fromKo, Holder, IDomComponent, subscribe} from 'grainjs'; import {computedArray, Disposable, dom, fromKo, Holder, IDomComponent, styled, subscribe} from 'grainjs';
import * as ko from 'knockout'; import * as ko from 'knockout';
import * as _ from 'underscore'; import * as _ from 'underscore';
@ -121,6 +121,21 @@ export class ViewLayout extends DisposableWithEvents implements IDomComponent {
// (See https://stackoverflow.com/questions/2381336/detect-click-into-iframe-using-javascript). // (See https://stackoverflow.com/questions/2381336/detect-click-into-iframe-using-javascript).
this.listenTo(this.gristDoc.app, 'clipboard_blur', this._maybeFocusInSection); this.listenTo(this.gristDoc.app, 'clipboard_blur', this._maybeFocusInSection);
const classActive = cssLayoutBox.className + '-active';
const classInactive = cssLayoutBox.className + '-inactive';
this.autoDispose(subscribe(fromKo(this.viewModel.activeSectionId), (use, id) => {
this._layout.forEachBox((box: {dom: Element}) => {
box.dom.classList.add(classInactive);
box.dom.classList.remove(classActive);
});
let elem: Element|null = this._layout.getLeafBox(id)?.dom;
while (elem?.matches('.layout_box')) {
elem.classList.remove(classInactive);
elem.classList.add(classActive);
elem = elem.parentElement;
}
}));
const commandGroup = { const commandGroup = {
deleteSection: () => { this._removeViewSection(this.viewModel.activeSectionId()); }, deleteSection: () => { this._removeViewSection(this.viewModel.activeSectionId()); },
nextSection: () => { this._otherSection(+1); }, nextSection: () => { this._otherSection(+1); },
@ -158,11 +173,13 @@ export class ViewLayout extends DisposableWithEvents implements IDomComponent {
const vs: ViewSectionRec = this.docModel.viewSections.getRowModel(sectionRowId); const vs: ViewSectionRec = this.docModel.viewSections.getRowModel(sectionRowId);
return dom('div.view_leaf.viewsection_content.flexvbox.flexauto', return dom('div.view_leaf.viewsection_content.flexvbox.flexauto',
testId(`viewlayout-section-${sectionRowId}`), testId(`viewlayout-section-${sectionRowId}`),
this.gristDoc.app.addNewUIClass(),
cssViewLeaf.cls(''),
cssViewLeafInactive.cls('', (use) => !vs.isDisposed() && !use(vs.hasFocus)),
dom.cls('active_section', vs.hasFocus), dom.cls('active_section', vs.hasFocus),
dom.maybe((use) => use(vs.viewInstance) !== null, () => dom('div.viewsection_title.flexhbox', dom.maybe((use) => use(vs.viewInstance) !== null, () => dom('div.viewsection_title.flexhbox',
dom('span.viewsection_drag_indicator.glyphicon.glyphicon-option-vertical', dom('span.viewsection_drag_indicator.glyphicon.glyphicon-option-vertical',
this.gristDoc.app.addNewUIClass(),
// Makes element grabbable only if grist is not readonly. // Makes element grabbable only if grist is not readonly.
dom.cls('layout_grabbable', (use) => !use(this.gristDoc.isReadonlyKo))), dom.cls('layout_grabbable', (use) => !use(this.gristDoc.isReadonlyKo))),
dom('div.flexitem.flexhbox', dom('div.flexitem.flexhbox',
@ -270,3 +287,79 @@ export class ViewLayout extends DisposableWithEvents implements IDomComponent {
} }
} }
} }
const cssViewLeaf = styled('div', `
@media ${mediaSmall} {
& {
margin: 4px;
}
}
`);
const cssViewLeafInactive = styled('div', `
@media ${mediaSmall} {
& {
overflow: hidden;
background: repeating-linear-gradient(
-45deg,
${colors.mediumGreyOpaque},
${colors.mediumGreyOpaque} 10px,
${colors.lightGrey} 10px,
${colors.lightGrey} 20px
);
border: 1px solid ${colors.darkGrey};
border-radius: 4px;
padding: 0 2px;
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
&.layout_vbox {
max-width: 32px;
}
&.layout_hbox {
max-height: 32px;
}
& > .viewsection_title.flexhbox {
position: absolute;
}
& > .view_data_pane_container,
& .viewsection_buttons,
& .grist-single-record__menu {
display: none;
}
}
`);
const cssLayoutBox = styled('div', `
@media ${mediaSmall} {
&-active, &-inactive {
transition: flex-grow 0.4s;
}
&-active > &-inactive,
&-active > &-inactive.layout_hbox .layout_hbox,
&-active > &-inactive.layout_vbox .layout_vbox {
flex: none !important;
}
&-active > &-inactive.layout_hbox.layout_leaf,
&-active > &-inactive.layout_hbox .layout_hbox.layout_leaf {
height: 40px;
}
&-active > &-inactive.layout_vbox.layout_leaf,
&-active > &-inactive.layout_vbox .layout_vbox.layout_leaf {
width: 40px;
}
&-inactive.layout_leaf {
min-height: 40px;
min-width: 40px;
}
}
`);