Path: blob/master/src/packages/frontend/antd-bootstrap.tsx
1496 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,24Tabs as AntdTabs,25TabsProps as AntdTabsProps,26Space,27Tooltip,28} from "antd";29import type { MouseEventHandler } from "react";3031import { inDarkMode } from "@cocalc/frontend/account/dark-mode";32import { Gap } from "@cocalc/frontend/components/gap";33import { r_join } from "@cocalc/frontend/components/r_join";34import { COLORS } from "@cocalc/util/theme";3536// Note regarding buttons -- there are 6 semantics meanings in bootstrap, but37// only four in antd, and it we can't automatically collapse them down in a meaningful38// way without fundamentally removing information and breaking our UI (e.g., buttons39// change look after an assignment is sent successfully in a course).40export type ButtonStyle =41| "primary"42| "success"43| "default"44| "info"45| "warning"46| "danger"47| "link"48| "ghost";4950const BS_STYLE_TO_TYPE: {51[name in ButtonStyle]:52| "primary"53| "default"54| "dashed"55| "danger"56| "link"57| "text";58} = {59primary: "primary",60success: "default", // antd doesn't have this so we do it via style below.61default: "default",62info: "default", // antd doesn't have this so we do it via style below.63warning: "default", // antd doesn't have this so we do it via style below.64danger: "danger",65link: "link",66ghost: "text",67};6869export type ButtonSize = "large" | "small" | "xsmall";7071function parse_bsStyle(props: {72bsStyle?: ButtonStyle;73style?: React.CSSProperties;74disabled?: boolean;75}): {76type: "primary" | "default" | "dashed" | "link" | "text";77style: React.CSSProperties;78danger?: boolean;79ghost?: boolean;80disabled?: boolean;81loading?: boolean;82} {83let type =84props.bsStyle == null85? "default"86: BS_STYLE_TO_TYPE[props.bsStyle] ?? "default";8788let style: React.CSSProperties | undefined = undefined;89// antd has no analogue of "success" & "warning", it's not clear to me what90// it should be so for now just copy the style from react-bootstrap.91if (!inDarkMode()) {92if (props.bsStyle === "warning") {93// antd has no analogue of "warning", it's not clear to me what94// it should be so for95// now just copy the style.96style = {97backgroundColor: COLORS.BG_WARNING,98borderColor: "#eea236",99color: "#ffffff",100};101} else if (props.bsStyle === "success") {102style = {103backgroundColor: "#5cb85c",104borderColor: "#4cae4c",105color: "#ffffff",106};107} else if (props.bsStyle == "info") {108style = {109backgroundColor: "rgb(91, 192, 222)",110borderColor: "rgb(70, 184, 218)",111color: "#ffffff",112};113}114}115if (props.disabled && style != null) {116style.opacity = 0.65;117}118119style = { ...style, ...props.style };120let danger: boolean | undefined = undefined;121let loading: boolean | undefined = undefined; // nothing mapped to this yet122let ghost: boolean | undefined = undefined; // nothing mapped to this yet123if (type == "danger") {124type = "default";125danger = true;126}127return { type, style, danger, ghost, loading };128}129130export const Button = (props: {131bsStyle?: ButtonStyle;132bsSize?: ButtonSize;133style?: React.CSSProperties;134disabled?: boolean;135onClick?: (e?: any) => void;136key?;137children?: any;138className?: string;139href?: string;140target?: string;141title?: string | React.JSX.Element;142tabIndex?: number;143active?: boolean;144id?: string;145autoFocus?: boolean;146placement?;147block?: boolean;148}) => {149// The span is needed inside below, otherwise icons and labels get squashed together150// due to button having word-spacing 0.151const { type, style, danger, ghost, loading } = parse_bsStyle(props);152let size: "middle" | "large" | "small" | undefined = undefined;153if (props.bsSize == "large") {154size = "large";155} else if (props.bsSize == "small") {156size = "middle";157} else if (props.bsSize == "xsmall") {158size = "small";159}160if (props.active) {161style.backgroundColor = "#d4d4d4";162style.boxShadow = "inset 0 3px 5px rgb(0 0 0 / 13%)";163}164const btn = (165<AntdButton166onClick={props.onClick}167type={type}168disabled={props.disabled}169style={style}170size={size}171className={props.className}172href={props.href}173target={props.target}174danger={danger}175ghost={ghost}176loading={loading}177tabIndex={props.tabIndex}178id={props.id}179autoFocus={props.autoFocus}180block={props.block}181>182<>{props.children}</>183</AntdButton>184);185if (props.title) {186return (187<Tooltip188title={props.title}189mouseEnterDelay={0.7}190placement={props.placement}191>192{btn}193</Tooltip>194);195} else {196return btn;197}198};199200export function ButtonGroup(props: {201style?: React.CSSProperties;202children?: any;203className?: string;204}) {205return (206<Space.Compact className={props.className} style={props.style}>207{props.children}208</Space.Compact>209);210}211212export function ButtonToolbar(props: {213style?: React.CSSProperties;214children?: any;215className?: string;216}) {217return (218<div className={props.className} style={props.style}>219{r_join(props.children, <Gap />)}220</div>221);222}223224export function Grid(props: {225onClick?: MouseEventHandler<HTMLDivElement>;226style?: React.CSSProperties;227children?: any;228}) {229return (230<div231onClick={props.onClick}232style={{ ...{ padding: "0 8px" }, ...props.style }}233>234{props.children}235</div>236);237}238239export function Well(props: {240style?: React.CSSProperties;241children?: any;242className?: string;243onDoubleClick?;244onMouseDown?;245}) {246let style: React.CSSProperties = {247...{ backgroundColor: "white", border: "1px solid #e3e3e3" },248...props.style,249};250return (251<AntdCard252style={style}253className={props.className}254onDoubleClick={props.onDoubleClick}255onMouseDown={props.onMouseDown}256>257{props.children}258</AntdCard>259);260}261262export function Checkbox(props) {263const style: React.CSSProperties = props.style != null ? props.style : {};264if (style.fontWeight == null) {265// Antd checkbox uses the label DOM element, and bootstrap css266// changes the weight of that DOM element to 700, which is267// really ugly and conflicts with the antd design style. So268// we manually change it back here. This will go away if/when269// we no longer include bootstrap css...270style.fontWeight = 400;271}272// The margin and div is to be like react-bootstrap which273// has that margin.274return (275<div style={{ margin: "10px 0" }}>276<AntdCheckbox {...{ ...props, style }}>{props.children}</AntdCheckbox>277</div>278);279}280281export function Row(props: any) {282props = { ...{ gutter: 16 }, ...props };283return <AntdRow {...props}>{props.children}</AntdRow>;284}285286export function Col(props: {287xs?: number;288sm?: number;289md?: number;290lg?: number;291xsOffset?: number;292smOffset?: number;293mdOffset?: number;294lgOffset?: number;295style?: React.CSSProperties;296className?: string;297onClick?;298children?: any;299push?;300pull?;301}) {302const props2: any = {};303for (const p of ["xs", "sm", "md", "lg", "push", "pull"]) {304if (props[p] != null) {305if (props2[p] == null) {306props2[p] = {};307}308props2[p].span = 2 * props[p];309}310if (props[p + "Offset"] != null) {311if (props2[p] == null) {312props2[p] = {};313}314props2[p].offset = 2 * props[p + "Offset"];315}316}317for (const p of ["className", "onClick", "style"]) {318props2[p] = props[p];319}320return <AntdCol {...props2}>{props.children}</AntdCol>;321}322323export type AntdTabItem = NonNullable<AntdTabsProps["items"]>[number];324325interface TabsProps {326id?: string;327key?;328activeKey: string;329onSelect?: (activeKey: string) => void;330animation?: boolean;331style?: React.CSSProperties;332tabBarExtraContent?;333tabPosition?: "left" | "top" | "right" | "bottom";334size?: "small";335items: AntdTabItem[]; // This is mandatory: Tabs.TabPane (was in "Tab") is deprecated.336}337338export function Tabs(props: Readonly<TabsProps>) {339return (340<AntdTabs341activeKey={props.activeKey}342onChange={props.onSelect}343animated={props.animation ?? false}344style={props.style}345tabBarExtraContent={props.tabBarExtraContent}346tabPosition={props.tabPosition}347size={props.size}348items={props.items}349/>350);351}352353export function Tab(props: {354id?: string;355key?: string;356eventKey: string;357title: string | React.JSX.Element;358children?: any;359style?: React.CSSProperties;360}): AntdTabItem {361let title = props.title;362if (!title) {363// In case of useless title, some sort of fallback.364// This is important since a tab with no title can't365// be selected.366title = props.eventKey ?? props.key;367if (!title) title = "Tab";368}369370// Get rid of the fade transition, which is inconsistent with371// react-bootstrap (and also really annoying to me). See372// https://github.com/ant-design/ant-design/issues/951#issuecomment-176291275373const style = { ...{ transition: "0s" }, ...props.style };374375return {376key: props.key ?? props.eventKey,377label: title,378style,379children: props.children,380};381}382383export function Modal(props: {384show?: boolean;385onHide: () => void;386children?: any;387}) {388return (389<AntdModal open={props.show} footer={null} closable={false}>390{props.children}391</AntdModal>392);393}394395Modal.Body = function (props: any) {396return <>{props.children}</>;397};398399interface AlertProps {400bsStyle?: ButtonStyle;401style?: React.CSSProperties;402banner?: boolean;403children?: any;404icon?: React.JSX.Element;405}406407export function Alert(props: AlertProps) {408const { bsStyle, style, banner, children, icon } = props;409410let type: "success" | "info" | "warning" | "error" | undefined = undefined;411// success, info, warning, error412if (bsStyle == "success" || bsStyle == "warning" || bsStyle == "info") {413type = bsStyle;414} else if (bsStyle == "danger") {415type = "error";416} else if (bsStyle == "link") {417type = "info";418} else if (bsStyle == "primary") {419type = "success";420}421return (422<AntdAlert423message={children}424type={type}425style={style}426banner={banner}427icon={icon}428/>429);430}431432export function Panel(props: {433key?;434style?: React.CSSProperties;435header?;436children?: any;437onClick?;438}) {439const style = { ...{ marginBottom: "20px" }, ...props.style };440return (441<AntdCard442style={style}443title={props.header}444styles={{445header: { color: COLORS.GRAY_DD, backgroundColor: COLORS.GRAY_LLL },446}}447onClick={props.onClick}448>449{props.children}450</AntdCard>451);452}453454455