gristlabs_grist-core/app/client/components/Banner.ts

166 lines
3.6 KiB
TypeScript
Raw Normal View History

import {colors, isNarrowScreenObs} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {Disposable, dom, DomArg, DomElementArg, makeTestId, Observable, styled} from 'grainjs';
const testId = makeTestId('test-banner-');
export interface BannerOptions {
/**
* Content to display in the banner.
*/
content: DomArg;
/**
* The banner style.
*
* Warning banners have a yellow background. Error banners have a red
* background.
*/
style: 'warning' | 'error';
/**
* Optional variant of `content` to display when screen width becomes narrow.
*/
contentSmall?: DomArg;
/**
* Whether a button to close the banner should be shown.
*
* If true, `onClose` should also be specified; it will be called when the close
* button is clicked.
*
* Defaults to false.
*/
showCloseButton?: boolean;
/**
* Whether a button to collapse/expand the banner should be shown on narrow screens.
*
* Defaults to false.
*/
showExpandButton?: boolean;
/**
* Function that is called when the banner close button is clicked.
*
* Should be used to handle disposal of the Banner.
*/
onClose?(): void;
}
/**
* A customizable banner for displaying at the top of a page.
*/
export class Banner extends Disposable {
private readonly _isExpanded = Observable.create(this, true);
constructor(private _options: BannerOptions) {
super();
}
public buildDom() {
return cssBanner(
cssBanner.cls(`-${this._options.style}`),
this._buildContent(),
this._buildButtons(),
);
}
private _buildContent() {
const {content, contentSmall} = this._options;
return dom.domComputed(use => {
if (contentSmall === undefined) { return [content]; }
const isExpanded = use(this._isExpanded);
const isNarrowScreen = use(isNarrowScreenObs());
return [isNarrowScreen && !isExpanded ? contentSmall : content];
});
}
private _buildButtons() {
return cssButtons(
this._options.showExpandButton ? this._buildExpandButton() : null,
this._options.showCloseButton ? this._buildCloseButton() : null,
);
}
private _buildCloseButton() {
return cssButton('CrossBig',
dom.on('click', () => this._options.onClose?.()),
testId('close'),
);
}
private _buildExpandButton() {
return dom.maybe(isNarrowScreenObs(), () => {
return cssExpandButton('Dropdown',
cssExpandButton.cls('-expanded', this._isExpanded),
dom.on('click', () => this._isExpanded.set(!this._isExpanded.get())),
);
});
}
}
export function buildBannerMessage(...domArgs: DomElementArg[]) {
return cssBannerMessage(
cssIcon('Idea'),
cssLightlyBoldedText(domArgs),
);
}
const cssBanner = styled('div', `
display: flex;
padding: 10px;
gap: 16px;
color: white;
&-warning {
background: #E6A117;
}
&-error {
background: ${colors.error};
}
`);
const cssButtons = styled('div', `
display: flex;
gap: 16px;
flex-shrink: 0;
margin-left: auto;
`);
const cssButton = styled(icon, `
width: 16px;
height: 16px;
cursor: pointer;
background-color: white;
`);
const cssExpandButton = styled(cssButton, `
&-expanded {
-webkit-mask-image: var(--icon-DropdownUp);
}
`);
const cssLightlyBoldedText = styled('div', `
font-weight: 500;
`);
const cssIconAndText = styled('div', `
display: flex;
gap: 16px;
`);
const cssBannerMessage = styled(cssIconAndText, `
flex-grow: 1;
justify-content: center;
`);
const cssIcon = styled(icon, `
flex-shrink: 0;
width: 16px;
height: 16px;
background-color: white;
`);