Path: blob/master/src/packages/frontend/antd-bootstrap.tsx
5803 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6We use so little of react-bootstrap in CoCalc that for a first quick round7of switching to antd, I'm going to see if it isn't easy to re-implement8much of the same functionality on top of antd910Obviously, this is meant to be temporary, since it is far better if our11code consistently uses the antd api explicitly. However, there are12some serious problems / bug /issues with using our stupid old react-bootstrap13*at all*, hence this.14*/1516import {17Alert as AntdAlert,18Button as AntdButton,19Card as AntdCard,20Checkbox as AntdCheckbox,21Col as AntdCol,22Modal as AntdModal,23Row as AntdRow,24Switch as AntdSwitch,25Tabs as AntdTabs,26TabsProps as AntdTabsProps,27Space,28Tooltip,29} from "antd";30import type { MouseEventHandler } from "react";3132import { inDarkMode } from "@cocalc/frontend/account/dark-mode";33import { Gap } from "@cocalc/frontend/components/gap";34import { r_join } from "@cocalc/frontend/components/r_join";35import { COLORS } from "@cocalc/util/theme";36import { CSS } from "./app-framework";3738// Note regarding buttons -- there are 6 semantics meanings in bootstrap, but39// only four in antd, and it we can't automatically collapse them down in a meaningful40// way without fundamentally removing information and breaking our UI (e.g., buttons41// change look after an assignment is sent successfully in a course).42export type ButtonStyle =43| "primary"44| "success"45| "default"46| "info"47| "warning"48| "danger"49| "link"50| "ghost";5152const BS_STYLE_TO_TYPE: {53[name in ButtonStyle]:54| "primary"55| "default"56| "dashed"57| "danger"58| "link"59| "text";60} = {61primary: "primary",62success: "default", // antd doesn't have this so we do it via style below.63default: "default",64info: "default", // antd doesn't have this so we do it via style below.65warning: "default", // antd doesn't have this so we do it via style below.66danger: "danger",67link: "link",68ghost: "text",69};7071export type ButtonSize = "large" | "small" | "xsmall";7273function parse_bsStyle(props: {74bsStyle?: ButtonStyle;75style?: React.CSSProperties;76disabled?: boolean;77}): {78type: "primary" | "default" | "dashed" | "link" | "text";79style: React.CSSProperties;80danger?: boolean;81ghost?: boolean;82disabled?: boolean;83loading?: boolean;84} {85let type =86props.bsStyle == null87? "default"88: (BS_STYLE_TO_TYPE[props.bsStyle] ?? "default");8990let style: React.CSSProperties | undefined = undefined;91// antd has no analogue of "success" & "warning", it's not clear to me what92// it should be so for now just copy the style from react-bootstrap.93if (!inDarkMode()) {94if (props.bsStyle === "warning") {95// antd has no analogue of "warning", it's not clear to me what96// it should be so for97// now just copy the style.98style = {99backgroundColor: COLORS.BG_WARNING,100borderColor: "#eea236",101color: "#ffffff",102};103} else if (props.bsStyle === "success") {104style = {105backgroundColor: "#5cb85c",106borderColor: "#4cae4c",107color: "#ffffff",108};109} else if (props.bsStyle == "info") {110style = {111backgroundColor: "rgb(91, 192, 222)",112borderColor: "rgb(70, 184, 218)",113color: "#ffffff",114};115}116}117if (props.disabled && style != null) {118style.opacity = 0.65;119}120121style = { ...style, ...props.style };122let danger: boolean | undefined = undefined;123let loading: boolean | undefined = undefined; // nothing mapped to this yet124let ghost: boolean | undefined = undefined; // nothing mapped to this yet125if (type == "danger") {126type = "default";127danger = true;128}129return { type, style, danger, ghost, loading };130}131132export const Button = (props: {133bsStyle?: ButtonStyle;134bsSize?: ButtonSize;135style?: React.CSSProperties;136disabled?: boolean;137onClick?: (e?: any) => void;138key?;139children?: any;140className?: string;141href?: string;142target?: string;143title?: string | React.JSX.Element;144tabIndex?: number;145active?: boolean;146id?: string;147autoFocus?: boolean;148placement?;149block?: boolean;150}) => {151// The span is needed inside below, otherwise icons and labels get squashed together152// due to button having word-spacing 0.153const { type, style, danger, ghost, loading } = parse_bsStyle(props);154let size: "middle" | "large" | "small" | undefined = undefined;155if (props.bsSize == "large") {156size = "large";157} else if (props.bsSize == "small") {158size = "middle";159} else if (props.bsSize == "xsmall") {160size = "small";161}162if (props.active) {163style.backgroundColor = "#d4d4d4";164style.boxShadow = "inset 0 3px 5px rgb(0 0 0 / 13%)";165}166const btn = (167<AntdButton168onClick={props.onClick}169type={type}170disabled={props.disabled}171style={style}172size={size}173className={props.className}174href={props.href}175target={props.target}176danger={danger}177ghost={ghost}178loading={loading}179tabIndex={props.tabIndex}180id={props.id}181autoFocus={props.autoFocus}182block={props.block}183>184<>{props.children}</>185</AntdButton>186);187if (props.title) {188return (189<Tooltip190title={props.title}191mouseEnterDelay={0.7}192placement={props.placement}193>194{btn}195</Tooltip>196);197} else {198return btn;199}200};201202export function ButtonGroup(props: {203style?: React.CSSProperties;204children?: any;205className?: string;206}) {207return (208<Space.Compact className={props.className} style={props.style}>209{props.children}210</Space.Compact>211);212}213214export function ButtonToolbar(props: {215style?: React.CSSProperties;216children?: any;217className?: string;218}) {219return (220<div className={props.className} style={props.style}>221{r_join(props.children, <Gap />)}222</div>223);224}225226export function Grid(props: {227onClick?: MouseEventHandler<HTMLDivElement>;228style?: React.CSSProperties;229children?: any;230}) {231return (232<div233onClick={props.onClick}234style={{ ...{ padding: "0 8px" }, ...props.style }}235>236{props.children}237</div>238);239}240241export function Well(props: {242style?: React.CSSProperties;243children?: any;244className?: string;245onDoubleClick?;246onMouseDown?;247}) {248let style: React.CSSProperties = {249...{ backgroundColor: "white", border: "1px solid #e3e3e3" },250...props.style,251};252return (253<AntdCard254style={style}255className={props.className}256onDoubleClick={props.onDoubleClick}257onMouseDown={props.onMouseDown}258>259{props.children}260</AntdCard>261);262}263264export function Checkbox(props) {265const style: React.CSSProperties = props.style != null ? props.style : {};266if (style.fontWeight == null) {267// Antd checkbox uses the label DOM element, and bootstrap css268// changes the weight of that DOM element to 700, which is269// really ugly and conflicts with the antd design style. So270// we manually change it back here. This will go away if/when271// we no longer include bootstrap css...272style.fontWeight = 400;273}274// The margin and div is to be like react-bootstrap which275// has that margin.276return (277<div style={{ margin: "10px 0" }}>278<AntdCheckbox {...{ ...props, style }}>{props.children}</AntdCheckbox>279</div>280);281}282283export function Switch(props: {284checked?: boolean;285onChange?: (e: { target: { checked: boolean } }) => void;286disabled?: boolean;287style?: CSS;288labelStyle?: CSS;289children?: any;290}) {291// Default font weight for label292const labelStyle: CSS = {293fontWeight: 400,294...props.labelStyle,295cursor: props.disabled ? "default" : "pointer",296} as const;297298function handleChange(checked: boolean) {299if (props.onChange) {300// Call onChange with same signature as Checkbox - event object with target.checked301props.onChange({ target: { checked } });302}303}304305return (306<div style={{ margin: "15px 0" }}>307<div308style={{309display: "flex",310alignItems: "center",311gap: 8,312...props.style,313}}314>315<AntdSwitch316checked={props.checked}317onChange={handleChange}318disabled={props.disabled}319/>320<span321onClick={() => !props.disabled && handleChange(!props.checked)}322style={labelStyle}323>324{props.children}325</span>326</div>327</div>328);329}330331export function Row(props: any) {332props = { ...{ gutter: 16 }, ...props };333return <AntdRow {...props}>{props.children}</AntdRow>;334}335336export function Col(props: {337xs?: number;338sm?: number;339md?: number;340lg?: number;341xsOffset?: number;342smOffset?: number;343mdOffset?: number;344lgOffset?: number;345style?: React.CSSProperties;346className?: string;347onClick?;348children?: any;349push?;350pull?;351}) {352const props2: any = {};353for (const p of ["xs", "sm", "md", "lg", "push", "pull"]) {354if (props[p] != null) {355if (props2[p] == null) {356props2[p] = {};357}358props2[p].span = 2 * props[p];359}360if (props[p + "Offset"] != null) {361if (props2[p] == null) {362props2[p] = {};363}364props2[p].offset = 2 * props[p + "Offset"];365}366}367for (const p of ["className", "onClick", "style"]) {368props2[p] = props[p];369}370return <AntdCol {...props2}>{props.children}</AntdCol>;371}372373export type AntdTabItem = NonNullable<AntdTabsProps["items"]>[number];374375interface TabsProps {376id?: string;377key?;378activeKey: string;379onSelect?: (activeKey: string) => void;380animation?: boolean;381style?: React.CSSProperties;382tabBarExtraContent?;383tabPosition?: "left" | "top" | "right" | "bottom";384size?: "small";385items: AntdTabItem[]; // This is mandatory: Tabs.TabPane (was in "Tab") is deprecated.386}387388export function Tabs(props: Readonly<TabsProps>) {389return (390<AntdTabs391activeKey={props.activeKey}392onChange={props.onSelect}393animated={props.animation ?? false}394style={props.style}395tabBarExtraContent={props.tabBarExtraContent}396tabPosition={props.tabPosition}397size={props.size}398items={props.items}399/>400);401}402403export function Tab(props: {404id?: string;405key?: string;406eventKey: string;407title: string | React.JSX.Element;408children?: any;409style?: React.CSSProperties;410}): AntdTabItem {411let title = props.title;412if (!title) {413// In case of useless title, some sort of fallback.414// This is important since a tab with no title can't415// be selected.416title = props.eventKey ?? props.key;417if (!title) title = "Tab";418}419420// Get rid of the fade transition, which is inconsistent with421// react-bootstrap (and also really annoying to me). See422// https://github.com/ant-design/ant-design/issues/951#issuecomment-176291275423const style = { ...{ transition: "0s" }, ...props.style };424425return {426key: props.key ?? props.eventKey,427label: title,428style,429children: props.children,430};431}432433export function Modal(props: {434show?: boolean;435onHide: () => void;436children?: any;437}) {438return (439<AntdModal open={props.show} footer={null} closable={false}>440{props.children}441</AntdModal>442);443}444445Modal.Body = function (props: any) {446return <>{props.children}</>;447};448449interface AlertProps {450bsStyle?: ButtonStyle;451style?: React.CSSProperties;452banner?: boolean;453children?: any;454icon?: React.JSX.Element;455}456457export function Alert(props: AlertProps) {458const { bsStyle, style, banner, children, icon } = props;459460let type: "success" | "info" | "warning" | "error" | undefined = undefined;461// success, info, warning, error462if (bsStyle == "success" || bsStyle == "warning" || bsStyle == "info") {463type = bsStyle;464} else if (bsStyle == "danger") {465type = "error";466} else if (bsStyle == "link") {467type = "info";468} else if (bsStyle == "primary") {469type = "success";470}471return (472<AntdAlert473message={children}474type={type}475style={style}476banner={banner}477icon={icon}478/>479);480}481482const PANEL_DEFAULT_STYLES: { header: CSS } = {483header: { color: COLORS.GRAY_DD, backgroundColor: COLORS.GRAY_LLL },484} as const;485486export function Panel(props: {487key?;488style?: React.CSSProperties;489styles?: {490header?: React.CSSProperties;491body?: React.CSSProperties;492};493header?;494children?: any;495onClick?;496size?: "small";497}) {498const style: CSS = { ...{ marginBottom: "20px" }, ...props.style };499500const styles = {501...PANEL_DEFAULT_STYLES,502...props.styles,503};504505return (506<AntdCard507style={style}508title={props.header}509styles={styles}510onClick={props.onClick}511size={props.size}512>513{props.children}514</AntdCard>515);516}517518519