mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
aae4a58300
Summary: - Adjust ExampleCard css to fit on x-small screens, including its close button, so that it may be closed. - In TopBar on small screens, hide Notifications button and default action for Share; adjust spacing. - Add overflow: hidden to PagePanels, to make sure it doesn't slide off of visible area. Test Plan: Tested on iOS. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2741
407 lines
11 KiB
TypeScript
407 lines
11 KiB
TypeScript
/**
|
|
* Note that it assumes the presence of cssVars.cssRootVars on <body>.
|
|
*/
|
|
import {urlState} from "app/client/models/gristUrlState";
|
|
import {resizeFlexVHandle} from 'app/client/ui/resizeHandle';
|
|
import {transition} from 'app/client/ui/transitions';
|
|
import {colors, cssHideForNarrowScreen, mediaNotSmall, mediaSmall} from 'app/client/ui2018/cssVars';
|
|
import {isNarrowScreenObs} from 'app/client/ui2018/cssVars';
|
|
import {icon} from 'app/client/ui2018/icons';
|
|
import {dom, DomArg, noTestId, Observable, styled, subscribe, TestId} from "grainjs";
|
|
|
|
export interface PageSidePanel {
|
|
// Note that widths need to start out with a correct default in JS (having them in CSS is not
|
|
// enough), needed for open/close transitions.
|
|
panelWidth: Observable<number>;
|
|
panelOpen: Observable<boolean>;
|
|
hideOpener?: boolean; // If true, don't show the opener handle.
|
|
header: DomArg;
|
|
content: DomArg;
|
|
}
|
|
|
|
export interface PageContents {
|
|
leftPanel: PageSidePanel;
|
|
rightPanel?: PageSidePanel; // If omitted, the right panel isn't shown at all.
|
|
|
|
headerMain: DomArg;
|
|
contentMain: DomArg;
|
|
|
|
onResize?: () => void; // Callback for when either pane is opened, closed, or resized.
|
|
testId?: TestId;
|
|
contentBottom?: DomArg;
|
|
}
|
|
|
|
export function pagePanels(page: PageContents) {
|
|
const testId = page.testId || noTestId;
|
|
const left = page.leftPanel;
|
|
const right = page.rightPanel;
|
|
const onResize = page.onResize || (() => null);
|
|
|
|
let lastLeftOpen = left.panelOpen.get();
|
|
let lastRightOpen = right?.panelOpen.get() || false;
|
|
|
|
// When switching to mobile mode, close panels; when switching to desktop, restore the
|
|
// last desktop state.
|
|
const sub1 = subscribe(isNarrowScreenObs(), (use, narrow) => {
|
|
if (narrow) {
|
|
lastLeftOpen = left.panelOpen.get();
|
|
lastRightOpen = right?.panelOpen.get() || false;
|
|
}
|
|
left.panelOpen.set(narrow ? false : lastLeftOpen);
|
|
right?.panelOpen.set(narrow ? false : lastRightOpen);
|
|
});
|
|
|
|
// When url changes, we must have navigated; close the left panel since if it were open, it was
|
|
// the likely cause of the navigation (e.g. switch to another page or workspace).
|
|
const sub2 = subscribe(isNarrowScreenObs(), urlState().state, (use, narrow, state) => {
|
|
if (narrow) {
|
|
left.panelOpen.set(false);
|
|
}
|
|
});
|
|
|
|
return cssPageContainer(
|
|
dom.autoDispose(sub1),
|
|
dom.autoDispose(sub2),
|
|
cssLeftPane(
|
|
testId('left-panel'),
|
|
cssTopHeader(left.header),
|
|
left.content,
|
|
|
|
dom.style('width', (use) => use(left.panelOpen) ? use(left.panelWidth) + 'px' : ''),
|
|
|
|
// Opening/closing the left pane, with transitions.
|
|
cssLeftPane.cls('-open', left.panelOpen),
|
|
transition(use => (use(isNarrowScreenObs()) ? false : use(left.panelOpen)), {
|
|
prepare(elem, open) { elem.style.marginRight = (open ? -1 : 1) * (left.panelWidth.get() - 48) + 'px'; },
|
|
run(elem, open) { elem.style.marginRight = ''; },
|
|
finish: onResize,
|
|
}),
|
|
),
|
|
|
|
// Resizer for the left pane.
|
|
// TODO: resizing to small size should collapse. possibly should allow expanding too
|
|
cssResizeFlexVHandle(
|
|
{target: 'left', onSave: (val) => { left.panelWidth.set(val); onResize(); }},
|
|
testId('left-resizer'),
|
|
dom.show(left.panelOpen),
|
|
cssHideForNarrowScreen.cls('')),
|
|
|
|
// Show plain border when the resize handle is hidden.
|
|
cssResizeDisabledBorder(
|
|
dom.hide(left.panelOpen),
|
|
cssHideForNarrowScreen.cls('')),
|
|
|
|
cssMainPane(
|
|
cssTopHeader(
|
|
testId('top-header'),
|
|
(left.hideOpener ? null :
|
|
cssPanelOpener('PanelRight', cssPanelOpener.cls('-open', left.panelOpen),
|
|
testId('left-opener'),
|
|
dom.on('click', () => toggleObs(left.panelOpen)),
|
|
cssHideForNarrowScreen.cls(''))
|
|
),
|
|
|
|
page.headerMain,
|
|
|
|
(!right || right.hideOpener ? null :
|
|
cssPanelOpener('PanelLeft', cssPanelOpener.cls('-open', right.panelOpen),
|
|
testId('right-opener'),
|
|
dom.on('click', () => toggleObs(right.panelOpen)),
|
|
cssHideForNarrowScreen.cls(''))
|
|
),
|
|
),
|
|
page.contentMain,
|
|
testId('main-pane'),
|
|
),
|
|
(right ? [
|
|
// Resizer for the right pane.
|
|
cssResizeFlexVHandle(
|
|
{target: 'right', onSave: (val) => { right.panelWidth.set(val); onResize(); }},
|
|
testId('right-resizer'),
|
|
dom.show(right.panelOpen),
|
|
cssHideForNarrowScreen.cls('')),
|
|
|
|
cssRightPane(
|
|
testId('right-panel'),
|
|
cssTopHeader(right.header),
|
|
right.content,
|
|
|
|
dom.style('width', (use) => use(right.panelOpen) ? use(right.panelWidth) + 'px' : ''),
|
|
|
|
// Opening/closing the right pane, with transitions.
|
|
cssRightPane.cls('-open', right.panelOpen),
|
|
transition(use => (use(isNarrowScreenObs()) ? false : use(right.panelOpen)), {
|
|
prepare(elem, open) { elem.style.marginLeft = (open ? -1 : 1) * right.panelWidth.get() + 'px'; },
|
|
run(elem, open) { elem.style.marginLeft = ''; },
|
|
finish: onResize,
|
|
}),
|
|
)] : null
|
|
),
|
|
cssContentOverlay(
|
|
dom.show((use) => use(left.panelOpen) || Boolean(right && use(right.panelOpen))),
|
|
dom.on('click', () => {
|
|
left.panelOpen.set(false);
|
|
if (right) { right.panelOpen.set(false); }
|
|
}),
|
|
testId('overlay')
|
|
),
|
|
dom.maybe(isNarrowScreenObs(), () =>
|
|
cssBottomFooter(
|
|
testId('bottom-footer'),
|
|
cssPanelOpenerNarrowScreenBtn(
|
|
cssPanelOpenerNarrowScreen(
|
|
'FieldTextbox',
|
|
dom.on('click', () => {
|
|
right?.panelOpen.set(false);
|
|
toggleObs(left.panelOpen);
|
|
}),
|
|
testId('left-opener-ns')
|
|
),
|
|
cssPanelOpenerNarrowScreenBtn.cls('-open', left.panelOpen)
|
|
),
|
|
page.contentBottom,
|
|
(!right ? null :
|
|
cssPanelOpenerNarrowScreenBtn(
|
|
cssPanelOpenerNarrowScreen(
|
|
'Settings',
|
|
dom.on('click', () => {
|
|
left.panelOpen.set(false);
|
|
toggleObs(right.panelOpen);
|
|
}),
|
|
testId('right-opener-ns')
|
|
),
|
|
cssPanelOpenerNarrowScreenBtn.cls('-open', right.panelOpen),
|
|
)
|
|
),
|
|
)
|
|
),
|
|
);
|
|
}
|
|
|
|
function toggleObs(boolObs: Observable<boolean>) {
|
|
boolObs.set(!boolObs.get());
|
|
}
|
|
|
|
const cssVBox = styled('div', `
|
|
display: flex;
|
|
flex-direction: column;
|
|
`);
|
|
const cssHBox = styled('div', `
|
|
display: flex;
|
|
`);
|
|
const cssPageContainer = styled(cssHBox, `
|
|
position: absolute;
|
|
isolation: isolate; /* Create a new stacking context */
|
|
z-index: 0; /* As of March 2019, isolation does not have Edge support, so force one with z-index */
|
|
overflow: hidden;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
min-width: 600px;
|
|
background-color: ${colors.lightGrey};
|
|
|
|
@media ${mediaSmall} {
|
|
& {
|
|
padding-bottom: 48px;
|
|
min-width: 240px;
|
|
}
|
|
.interface-light & {
|
|
padding-bottom: 0;
|
|
}
|
|
}
|
|
`);
|
|
|
|
export const cssLeftPane = styled(cssVBox, `
|
|
position: relative;
|
|
background-color: ${colors.lightGrey};
|
|
width: 48px;
|
|
margin-right: 0px;
|
|
overflow: hidden;
|
|
transition: margin-right 0.4s;
|
|
@media ${mediaSmall} {
|
|
& {
|
|
width: 240px;
|
|
position: fixed;
|
|
z-index: 10;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: -${240 + 15}px; /* adds an extra 15 pixels to also hide the box shadow */
|
|
visibility: hidden;
|
|
box-shadow: 10px 0 5px rgba(0, 0, 0, 0.2);
|
|
transition: left 0.4s, visibility 0.4s;
|
|
will-change: left;
|
|
}
|
|
&-open {
|
|
left: 0;
|
|
visibility: visible;
|
|
}
|
|
}
|
|
&-open {
|
|
width: 240px;
|
|
min-width: 160px;
|
|
max-width: 320px;
|
|
}
|
|
@media print {
|
|
& {
|
|
display: none;
|
|
}
|
|
}
|
|
.interface-light & {
|
|
display: none;
|
|
}
|
|
`);
|
|
const cssMainPane = styled(cssVBox, `
|
|
position: relative;
|
|
flex: 1 1 0px;
|
|
min-width: 0px;
|
|
background-color: white;
|
|
z-index: 1;
|
|
`);
|
|
const cssRightPane = styled(cssVBox, `
|
|
position: relative;
|
|
background-color: ${colors.lightGrey};
|
|
width: 0px;
|
|
margin-left: 0px;
|
|
overflow: hidden;
|
|
transition: margin-left 0.4s;
|
|
z-index: 0;
|
|
@media ${mediaSmall} {
|
|
& {
|
|
width: 240px;
|
|
position: fixed;
|
|
z-index: 10;
|
|
top: 0;
|
|
bottom: 0;
|
|
right: -${240 + 15}px; /* adds an extra 15 pixels to also hide the box shadow */
|
|
box-shadow: -10px 0 5px rgba(0, 0, 0, 0.2);
|
|
visibility: hidden;
|
|
transition: right 0.4s, visibility 0.4s;
|
|
will-change: right;
|
|
}
|
|
&-open {
|
|
right: 0;
|
|
visibility: visible;
|
|
}
|
|
}
|
|
&-open {
|
|
width: 240px;
|
|
min-width: 240px;
|
|
max-width: 320px;
|
|
}
|
|
@media print {
|
|
& {
|
|
display: none;
|
|
}
|
|
}
|
|
.interface-light & {
|
|
display: none;
|
|
}
|
|
`);
|
|
const cssTopHeader = styled('div', `
|
|
height: 48px;
|
|
flex: none;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
border-bottom: 1px solid ${colors.mediumGrey};
|
|
|
|
@media print {
|
|
& {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.interface-light & {
|
|
display: none;
|
|
}
|
|
`);
|
|
const cssBottomFooter = styled ('div', `
|
|
height: 48px;
|
|
background-color: white;
|
|
z-index: 20;
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 8px 16px;
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
border-top: 1px solid ${colors.mediumGrey};
|
|
@media ${mediaNotSmall} {
|
|
& {
|
|
display: none;
|
|
}
|
|
}
|
|
@media print {
|
|
& {
|
|
display: none;
|
|
}
|
|
}
|
|
.interface-light & {
|
|
display: none;
|
|
}
|
|
`);
|
|
const cssResizeFlexVHandle = styled(resizeFlexVHandle, `
|
|
--resize-handle-color: ${colors.mediumGrey};
|
|
--resize-handle-highlight: ${colors.lightGreen};
|
|
|
|
@media print {
|
|
& {
|
|
display: none;
|
|
}
|
|
}
|
|
`);
|
|
const cssResizeDisabledBorder = styled('div', `
|
|
flex: none;
|
|
width: 1px;
|
|
height: 100%;
|
|
background-color: ${colors.mediumGrey};
|
|
`);
|
|
const cssPanelOpener = styled(icon, `
|
|
flex: none;
|
|
width: 32px;
|
|
height: 32px;
|
|
padding: 8px 8px;
|
|
cursor: pointer;
|
|
-webkit-mask-size: 16px 16px;
|
|
background-color: ${colors.lightGreen};
|
|
transition: transform 0.4s;
|
|
&:hover { background-color: ${colors.darkGreen}; }
|
|
&-open { transform: rotateY(180deg); }
|
|
`);
|
|
const cssPanelOpenerNarrowScreenBtn = styled('div', `
|
|
width: 32px;
|
|
height: 32px;
|
|
--icon-color: ${colors.slate};
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
&-open {
|
|
background-color: ${colors.lightGreen};
|
|
--icon-color: white;
|
|
}
|
|
`);
|
|
const cssPanelOpenerNarrowScreen = styled(icon, `
|
|
width: 24px;
|
|
height: 24px;
|
|
margin: 4px;
|
|
`);
|
|
const cssContentOverlay = styled('div', `
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
right: 0;
|
|
background-color: grey;
|
|
opacity: 0.5;
|
|
display: none;
|
|
z-index: 9;
|
|
@media ${mediaSmall} {
|
|
& {
|
|
display: unset;
|
|
}
|
|
}
|
|
`);
|