Create new SplitMenu component and implement it for the pdf compile button (detached & non-detached) (#11772)

* Create a new shared `SplitMenu` component.

* Refactor the pdf compile button & detached compile button:
    - Rename `detach-compile-button` to `detach-compile-button-wrapper`
    - Rename `pdf-compile-button-inner` to `detach-compile-button`
    - Move some of the logic from `detach-compile-button-wrapper` to `detach-compile-button`
    - Create a new `compile-button.less` to centralize all of the compile button (detached/non-detached) custom styles rule.
    - Extract the animated striped CSS definition to the dedicated CSS file, change the class from `btn-recompile-group-has-changes` to `btn-striped-animated`
    - Refactor other className(s) appropriately according to the new component name
    - Delete the unused `changes-to-autocompile` css rule since it has not been used anywhere

* Implement the new pdf compile button with the new `SplitMenu` component.

GitOrigin-RevId: d1d055bffd311923fc47b4681605ce8ba8e26f25
This commit is contained in:
M Fahru 2023-02-21 07:34:35 -07:00 committed by Copybot
parent 9ac982cda7
commit b62cb86bf8
23 changed files with 854 additions and 334 deletions

View file

@ -0,0 +1,23 @@
import { memo } from 'react'
import PropTypes from 'prop-types'
import { useLayoutContext } from '../../../shared/context/layout-context'
import DetachCompileButton from './detach-compile-button'
function DetachCompileButtonWrapper() {
const { detachRole, detachIsLinked } = useLayoutContext(
layoutContextPropTypes
)
if (detachRole !== 'detacher' || !detachIsLinked) {
return null
}
return <DetachCompileButton />
}
const layoutContextPropTypes = {
detachRole: PropTypes.string,
detachIsLinked: PropTypes.bool,
}
export default memo(DetachCompileButtonWrapper)

View file

@ -1,43 +0,0 @@
import { memo } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { useLayoutContext } from '../../../shared/context/layout-context'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import PdfCompileButtonInner from './pdf-compile-button-inner'
export function DetachCompileButton() {
const { compiling, hasChanges, startCompile } = useCompileContext()
return (
<div
className={classnames({
'btn-recompile-group': true,
'btn-recompile-group-has-changes': hasChanges,
})}
>
<PdfCompileButtonInner
startCompile={startCompile}
compiling={compiling}
/>
</div>
)
}
export function DetachCompileButtonWrapper() {
const { detachRole, detachIsLinked } = useLayoutContext(
layoutContextPropTypes
)
if (detachRole !== 'detacher' || !detachIsLinked) {
return null
}
return <DetachCompileButton />
}
const layoutContextPropTypes = {
detachRole: PropTypes.string,
detachIsLinked: PropTypes.bool,
}
export default memo(DetachCompileButtonWrapper)

View file

@ -0,0 +1,50 @@
import { useTranslation } from 'react-i18next'
import { memo } from 'react'
import { Button } from 'react-bootstrap'
import classNames from 'classnames'
import Icon from '../../../shared/components/icon'
import { useDetachCompileContext } from '../../../shared/context/detach-compile-context'
import Tooltip from '../../../shared/components/tooltip'
const modifierKey = /Mac/i.test(navigator.platform) ? 'Cmd' : 'Ctrl'
function DetachCompileButton() {
const { t } = useTranslation()
const { compiling, startCompile, hasChanges } = useDetachCompileContext()
const compileButtonLabel = compiling ? `${t('compiling')}` : t('recompile')
const tooltipElement = (
<>
{t('recompile_pdf')}{' '}
<span className="keyboard-shortcut">({modifierKey} + Enter)</span>
</>
)
return (
<div className="detach-compile-button-container">
<Tooltip
id="logs-toggle"
description={tooltipElement}
tooltipProps={{ className: 'keyboard-tooltip' }}
overlayProps={{ delayShow: 500 }}
>
<Button
bsStyle="primary"
onClick={() => startCompile()}
disabled={compiling}
className={classNames('detach-compile-button', {
'btn-striped-animated': hasChanges,
'detach-compile-button-disabled': compiling,
})}
>
<Icon type="refresh" spin={compiling} />
<span className="detach-compile-button-label">
{compileButtonLabel}
</span>
</Button>
</Tooltip>
</div>
)
}
export default memo(DetachCompileButton)

View file

@ -1,51 +0,0 @@
import { Button } from 'react-bootstrap'
import Tooltip from '../../../shared/components/tooltip'
import Icon from '../../../shared/components/icon'
import { useTranslation } from 'react-i18next'
import { memo } from 'react'
const modifierKey = /Mac/i.test(navigator.platform) ? 'Cmd' : 'Ctrl'
type PdfCompileButtonInnerProps = {
startCompile: () => void
compiling: boolean
}
function PdfCompileButtonInner({
startCompile,
compiling,
}: PdfCompileButtonInnerProps) {
const { t } = useTranslation()
const compileButtonLabel = compiling ? `${t('compiling')}` : t('recompile')
return (
<Tooltip
id="logs-toggle"
description={
<>
{t('recompile_pdf')}{' '}
<span className="keyboard-shortcut">({modifierKey} + Enter)</span>
</>
}
tooltipProps={{ className: 'keyboard-tooltip' }}
overlayProps={{ delayShow: 500 }}
>
<Button
className="btn-recompile"
bsStyle="primary"
onClick={() => startCompile()}
aria-label={compileButtonLabel}
disabled={compiling}
data-ol-loading={compiling}
>
<Icon type="refresh" spin={compiling} />
<span className="toolbar-hide-medium toolbar-hide-small btn-recompile-label">
{compileButtonLabel}
</span>
</Button>
</Tooltip>
)
}
export default memo(PdfCompileButtonInner)

View file

@ -1,12 +1,12 @@
import { Dropdown, MenuItem } from 'react-bootstrap'
import Icon from '../../../shared/components/icon'
import ControlledDropdown from '../../../shared/components/controlled-dropdown'
import { useTranslation } from 'react-i18next'
import { memo } from 'react'
import classnames from 'classnames'
import classNames from 'classnames'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
import PdfCompileButtonInner from './pdf-compile-button-inner'
import SplitMenu from '../../../shared/components/split-menu'
import Icon from '../../../shared/components/icon'
const modifierKey = /Mac/i.test(navigator.platform) ? 'Cmd' : 'Ctrl'
function PdfCompileButton() {
const {
@ -30,100 +30,115 @@ function PdfCompileButton() {
const { t } = useTranslation()
const compileButtonLabel = compiling ? `${t('compiling')}` : t('recompile')
const tooltipElement = (
<>
{t('recompile_pdf')}{' '}
<span className="keyboard-shortcut">({modifierKey} + Enter)</span>
</>
)
const dropdownToggleClassName = classNames({
'detach-compile-button-animate': animateCompileDropdownArrow,
'btn-striped-animated': hasChanges,
})
const buttonClassName = classNames({
'btn-striped-animated': hasChanges,
})
return (
<ControlledDropdown
className={classnames({
'toolbar-item': true,
'btn-recompile-group': true,
'btn-recompile-group-has-changes': hasChanges,
})}
id="pdf-recompile-dropdown"
<SplitMenu
bsStyle="primary"
bsSize="xs"
disabled={compiling}
button={{
tooltip: {
description: tooltipElement,
id: 'logs-toggle',
tooltipProps: { className: 'keyboard-tooltip' },
overlayProps: { delayShow: 500 },
},
icon: { type: 'refresh', spin: compiling },
onClick: () => startCompile(),
text: compileButtonLabel,
className: buttonClassName,
}}
dropdownToggle={{
'aria-label': t('toggle_compile_options_menu'),
handleAnimationEnd: () => setAnimateCompileDropdownArrow(false),
className: dropdownToggleClassName,
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
>
<PdfCompileButtonInner
startCompile={startCompile}
compiling={compiling}
/>
<SplitMenu.Item header>{t('auto_compile')}</SplitMenu.Item>
<Dropdown.Toggle
aria-label={t('toggle_compile_options_menu')}
className={classnames({
'btn-recompile': true,
'btn-recompile-animate': animateCompileDropdownArrow,
})}
bsStyle="primary"
onAnimationEnd={() => {
setAnimateCompileDropdownArrow(false)
}}
/>
<SplitMenu.Item onSelect={() => setAutoCompile(true)}>
<Icon type={autoCompile ? 'check' : ''} fw />
{t('on')}
</SplitMenu.Item>
<Dropdown.Menu>
<MenuItem header>{t('auto_compile')}</MenuItem>
<SplitMenu.Item onSelect={() => setAutoCompile(false)}>
<Icon type={!autoCompile ? 'check' : ''} fw />
{t('off')}
</SplitMenu.Item>
<MenuItem onSelect={() => setAutoCompile(true)}>
<Icon type={autoCompile ? 'check' : ''} fw />
{t('on')}
</MenuItem>
<SplitMenu.Item header>{t('compile_mode')}</SplitMenu.Item>
<MenuItem onSelect={() => setAutoCompile(false)}>
<Icon type={!autoCompile ? 'check' : ''} fw />
{t('off')}
</MenuItem>
<SplitMenu.Item onSelect={() => setDraft(false)}>
<Icon type={!draft ? 'check' : ''} fw />
{t('normal')}
</SplitMenu.Item>
<MenuItem header>{t('compile_mode')}</MenuItem>
<SplitMenu.Item onSelect={() => setDraft(true)}>
<Icon type={draft ? 'check' : ''} fw />
{t('fast')} <span className="subdued">[draft]</span>
</SplitMenu.Item>
<MenuItem onSelect={() => setDraft(false)}>
<Icon type={!draft ? 'check' : ''} fw />
{t('normal')}
</MenuItem>
<SplitMenu.Item header>Syntax Checks</SplitMenu.Item>
<MenuItem onSelect={() => setDraft(true)}>
<Icon type={draft ? 'check' : ''} fw />
{t('fast')} <span className="subdued">[draft]</span>
</MenuItem>
<SplitMenu.Item onSelect={() => setStopOnValidationError(true)}>
<Icon type={stopOnValidationError ? 'check' : ''} fw />
{t('stop_on_validation_error')}
</SplitMenu.Item>
<MenuItem header>Syntax Checks</MenuItem>
<SplitMenu.Item onSelect={() => setStopOnValidationError(false)}>
<Icon type={!stopOnValidationError ? 'check' : ''} fw />
{t('ignore_validation_errors')}
</SplitMenu.Item>
<MenuItem onSelect={() => setStopOnValidationError(true)}>
<Icon type={stopOnValidationError ? 'check' : ''} fw />
{t('stop_on_validation_error')}
</MenuItem>
<SplitMenu.Item header>{t('compile_error_handling')}</SplitMenu.Item>
<MenuItem onSelect={() => setStopOnValidationError(false)}>
<Icon type={!stopOnValidationError ? 'check' : ''} fw />
{t('ignore_validation_errors')}
</MenuItem>
<SplitMenu.Item onSelect={enableStopOnFirstError}>
<Icon type={stopOnFirstError ? 'check' : ''} fw />
{t('stop_on_first_error')}
</SplitMenu.Item>
<MenuItem header>{t('compile_error_handling')}</MenuItem>
<SplitMenu.Item onSelect={disableStopOnFirstError}>
<Icon type={!stopOnFirstError ? 'check' : ''} fw />
{t('try_to_compile_despite_errors')}
</SplitMenu.Item>
<MenuItem onSelect={enableStopOnFirstError}>
<Icon type={stopOnFirstError ? 'check' : ''} fw />
{t('stop_on_first_error')}
</MenuItem>
<SplitMenu.Item divider />
<MenuItem onSelect={disableStopOnFirstError}>
<Icon type={!stopOnFirstError ? 'check' : ''} fw />
{t('try_to_compile_despite_errors')}
</MenuItem>
<SplitMenu.Item
onSelect={() => stopCompile()}
disabled={!compiling}
aria-disabled={!compiling}
>
{t('stop_compile')}
</SplitMenu.Item>
<MenuItem divider />
<MenuItem
onSelect={() => stopCompile()}
disabled={!compiling}
aria-disabled={!compiling}
>
{t('stop_compile')}
</MenuItem>
<MenuItem
onSelect={() => recompileFromScratch()}
disabled={compiling}
aria-disabled={compiling}
>
{t('recompile_from_scratch')}
</MenuItem>
</Dropdown.Menu>
</ControlledDropdown>
<SplitMenu.Item
onSelect={() => recompileFromScratch()}
disabled={compiling}
aria-disabled={compiling}
>
{t('recompile_from_scratch')}
</SplitMenu.Item>
</SplitMenu>
)
}

View file

@ -1,7 +1,7 @@
import App from '../../../base'
import { react2angular } from 'react2angular'
import { rootContext } from '../../../shared/context/root-context'
import DetachCompileButtonWrapper from '../../../features/pdf-preview/components/detach-compile-button'
import DetachCompileButtonWrapper from '../../../features/pdf-preview/components/detach-compile-button-wrapper'
App.component(
'editorCompileButton',

View file

@ -8,7 +8,7 @@ type IconOwnProps = {
accessibilityLabel?: string
}
type IconProps = IconOwnProps &
export type IconProps = IconOwnProps &
Omit<React.ComponentProps<'i'>, keyof IconOwnProps>
function Icon({

View file

@ -0,0 +1,146 @@
import { Button, Dropdown, MenuItem } from 'react-bootstrap'
import type {
ButtonProps,
MenuItemProps,
DropdownButtonProps,
DropdownProps,
} from 'react-bootstrap'
import type { PropsWithChildren } from 'react'
import classNames from 'classnames'
import Tooltip, { type TooltipProps } from './tooltip'
import Icon, { type IconProps } from './icon'
import type { BsSize, BsStyle } from '../../../../types/bootstrap'
type SplitMenuBsStyle = Extract<BsStyle, 'primary' | 'secondary' | 'danger'>
type SplitMenuBsSize = Extract<BsSize, 'md' | 'sm' | 'xs'>
type SplitMenuButtonProps = {
tooltip?: TooltipProps
bsStyle?: SplitMenuBsStyle
text: string
icon?: IconProps
} & Pick<ButtonProps, 'aria-label' | 'onClick' | 'className' | 'disabled'>
type SplitMenuDropdownToggleProps = {
handleAnimationEnd?: () => void
} & Pick<DropdownButtonProps, 'className' | 'aria-label'>
type SplitMenuDropdownProps = Pick<DropdownProps, 'id' | 'className'>
type SplitMenuProps = PropsWithChildren<{
bsStyle: SplitMenuBsStyle
bsSize?: SplitMenuBsSize
button: Omit<SplitMenuButtonProps, 'disabled'>
dropdown: SplitMenuDropdownProps
dropdownToggle?: SplitMenuDropdownToggleProps
disabled?: boolean
}>
function SplitMenu({
bsStyle,
bsSize = 'md',
button,
dropdown,
dropdownToggle,
disabled = false,
children,
}: SplitMenuProps) {
const { tooltip, icon, ...buttonProps } = button
const splitMenuClassName = classNames('split-menu', {
[`btn-${bsSize}`]: true,
})
const dropdownToggleClassName = classNames(
'split-menu-dropdown-toggle',
dropdownToggle?.className
)
return (
<div className={splitMenuClassName}>
<SplitMenuButton
// eslint-disable-next-line react/jsx-handler-names
onClick={buttonProps.onClick}
className={buttonProps.className}
disabled={disabled}
tooltip={tooltip}
bsStyle={bsStyle}
>
{icon ? (
<Icon className="split-menu-icon" type={icon.type} spin={icon.spin} />
) : null}
<span className="split-menu-button">{buttonProps.text}</span>
</SplitMenuButton>
<Dropdown
className={classNames('split-menu-dropdown', dropdown.className)}
id={dropdown.id}
>
<Dropdown.Toggle
aria-label={dropdownToggle?.['aria-label']}
className={dropdownToggleClassName}
bsStyle={bsStyle}
onAnimationEnd={dropdownToggle?.handleAnimationEnd}
data-ol-loading={disabled}
/>
<Dropdown.Menu>{children}</Dropdown.Menu>
</Dropdown>
</div>
)
}
function SplitMenuButton({
onClick,
disabled,
tooltip,
bsStyle,
children,
className,
...props
}: PropsWithChildren<Omit<SplitMenuButtonProps, 'text' | 'icon'>>) {
const buttonClassName = classNames('split-menu-button', className)
if (tooltip) {
return (
<Tooltip
id={tooltip.id}
description={tooltip.description}
tooltipProps={tooltip.tooltipProps}
overlayProps={tooltip.overlayProps}
>
<Button
className={buttonClassName}
bsStyle={bsStyle}
onClick={onClick}
aria-label={props['aria-label']}
disabled={disabled}
data-ol-loading={disabled}
>
{children}
</Button>
</Tooltip>
)
}
return (
<Button
className={buttonClassName}
bsStyle={bsStyle}
onClick={onClick}
aria-label={props['aria-label']}
disabled={disabled}
data-ol-loading={disabled}
>
{children}
</Button>
)
}
function SplitMenuItem(props: MenuItemProps) {
return <MenuItem {...props} />
}
SplitMenu.Item = SplitMenuItem
export default SplitMenu

View file

@ -9,12 +9,20 @@ type OverlayProps = Omit<OverlayTriggerProps, 'overlay'> & {
shouldUpdatePosition?: boolean // Not officially documented https://stackoverflow.com/a/43138470
}
const Tooltip: FC<{
export type TooltipProps = {
description: ReactNode
id: string
overlayProps?: OverlayProps
tooltipProps?: BSTooltip.TooltipProps
}> = ({ id, description, children, tooltipProps, overlayProps }) => {
}
const Tooltip: FC<TooltipProps> = ({
id,
description,
children,
tooltipProps,
overlayProps,
}) => {
return (
<OverlayTrigger
overlay={

View file

@ -0,0 +1,225 @@
import SplitMenu from '../js/shared/components/split-menu'
export const PrimaryWithoutTooltip = () => {
return (
<SplitMenu
bsStyle="primary"
button={{
text: 'Button',
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
>
<SplitMenu.Item>tes</SplitMenu.Item>
</SplitMenu>
)
}
export const PrimaryWithTooltip = () => {
return (
<SplitMenu
bsStyle="primary"
button={{
text: 'Button',
tooltip: {
description: 'tooltip description',
id: 'tooltip-storybook',
overlayProps: {
placement: 'bottom',
},
},
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
>
<SplitMenu.Item>tes</SplitMenu.Item>
</SplitMenu>
)
}
export const Disabled = () => {
return (
<div>
<h2>Primary</h2>
<SplitMenu
bsStyle="primary"
disabled
button={{
text: 'Button',
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
dropdownToggle={{}}
>
<SplitMenu.Item>tes</SplitMenu.Item>
</SplitMenu>
<hr />
<h2>Secondary</h2>
<SplitMenu
bsStyle="secondary"
disabled
button={{
text: 'Button',
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
dropdownToggle={{}}
>
<SplitMenu.Item>tes</SplitMenu.Item>
</SplitMenu>
<hr />
<h2>Danger</h2>
<SplitMenu
bsStyle="danger"
disabled
button={{
text: 'Button',
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
dropdownToggle={{}}
>
<SplitMenu.Item>tes</SplitMenu.Item>
</SplitMenu>
</div>
)
}
export const DifferentSizeAndStyle = () => {
return (
<div>
<h2>Default (medium)</h2>
<div style={{ display: 'flex', gap: '10px' }}>
<SplitMenu
bsStyle="primary"
bsSize="md"
button={{
text: 'Button',
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
>
<SplitMenu.Item>tes</SplitMenu.Item>
</SplitMenu>
<SplitMenu
bsStyle="secondary"
bsSize="md"
button={{
text: 'Button',
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
>
<SplitMenu.Item>tes</SplitMenu.Item>
</SplitMenu>
<SplitMenu
bsStyle="danger"
bsSize="md"
button={{
text: 'Button',
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
>
<SplitMenu.Item>tes</SplitMenu.Item>
</SplitMenu>
</div>
<hr />
<h2>Small</h2>
<div style={{ display: 'flex', gap: '10px' }}>
<SplitMenu
bsStyle="primary"
bsSize="sm"
button={{
text: 'Button',
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
>
<SplitMenu.Item>tes</SplitMenu.Item>
</SplitMenu>
<SplitMenu
bsStyle="secondary"
bsSize="sm"
button={{
text: 'Button',
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
>
<SplitMenu.Item>tes</SplitMenu.Item>
</SplitMenu>
<SplitMenu
bsStyle="danger"
bsSize="sm"
button={{
text: 'Button',
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
>
<SplitMenu.Item>tes</SplitMenu.Item>
</SplitMenu>
</div>
<hr />
<h2>Extra Small</h2>
<div style={{ display: 'flex', gap: '10px' }}>
<SplitMenu
bsStyle="primary"
bsSize="xs"
button={{
text: 'Button',
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
>
<SplitMenu.Item>tes</SplitMenu.Item>
</SplitMenu>
<SplitMenu
bsStyle="secondary"
bsSize="xs"
button={{
text: 'Button',
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
>
<SplitMenu.Item>tes</SplitMenu.Item>
</SplitMenu>
<SplitMenu
bsStyle="danger"
bsSize="xs"
button={{
text: 'Button',
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
>
<SplitMenu.Item>tes</SplitMenu.Item>
</SplitMenu>
</div>
</div>
)
}
export default {
title: 'Shared / Components / Split Menu',
component: SplitMenu,
args: {
source: 'storybook',
},
}

View file

@ -60,6 +60,7 @@
@import 'components/expand-collapse.less';
@import 'components/beta-badges.less';
@import 'components/divider.less';
@import 'components/split-menu.less';
// Components w/ JavaScript
@import 'components/modals.less';

View file

@ -17,6 +17,7 @@
@import './editor/outline.less';
@import './editor/logs.less';
@import './editor/dictionary.less';
@import './editor/compile-button.less';
@ui-layout-toggler-def-height: 50px;
@ui-resizer-size: 7px;
@ -154,21 +155,6 @@
overflow: hidden;
position: relative;
z-index: 10; // Prevent track changes showing over toolbar
.btn-recompile-group {
margin-right: -5px;
border-radius: @btn-border-radius-base 0 0 @btn-border-radius-base;
margin-left: 6px;
&.btn-recompile-group-has-changes {
// prettier-ignore
#gradient > .striped(@color: rgba(255, 255, 255, 0.2), @angle: -45deg);
background-size: @stripe-width @stripe-width;
.animation(pdf-toolbar-stripes 2s linear infinite);
}
.btn-recompile {
border-radius: @btn-border-radius-base 0 0 @btn-border-radius-base;
}
}
}
.loading-screen {

View file

@ -0,0 +1,102 @@
@stripe-width: 20px;
@keyframes pdf-toolbar-stripes {
from {
background-position: 0 0;
}
to {
background-position: @stripe-width 0;
}
}
.detach-compile-button-container {
border-radius: @btn-border-radius-base 0 0 @btn-border-radius-base;
margin-left: 6px;
}
.detach-compile-button-container when (@is-new-css = false) {
margin-right: -5px;
}
// because 2px border on :active state
.detach-compile-button-container when (@is-new-css = true) {
margin-right: -3px;
}
.btn-striped-animated {
// prettier-ignore
#gradient > .striped(@color: rgba(255, 255, 255, 0.2), @angle: -45deg);
background-size: @stripe-width @stripe-width;
.animation(pdf-toolbar-stripes 2s linear infinite);
}
.detach-compile-button when (@is-new-css = false) {
&[disabled],
&[disabled].active,
&[disabled]:hover,
&[disabled]:focus {
background-color: mix(@btn-primary-bg, @toolbar-alt-bg-color, 65%);
.opacity(1);
}
}
.detach-compile-button {
height: 28px;
padding-top: 0;
padding-bottom: 0;
&.detach-compile-button-disabled {
&,
&:hover {
color: @white;
background-color: @ol-green;
}
}
}
.detach-compile-button when (@is-new-css = true) {
border: none;
}
.detach-compile-button-label {
margin-left: @line-height-computed / 4;
}
@keyframes compile-button-flash {
from,
to {
background: rgba(0, 0, 0, 0);
}
25%,
75% {
background: rgba(0, 0, 0, 0.2);
}
}
@keyframes compile-button-bounce {
from,
50%,
to {
transform: translateY(0);
}
25%,
75% {
transform: translateY(2px);
}
}
.detach-compile-button-animate {
animation-duration: 1.2s;
animation-fill-mode: both;
animation-timing-function: ease-in-out;
animation-name: compile-button-flash;
}
.detach-compile-button-animate .caret {
animation-duration: 0.6s;
animation-delay: 0.4s;
animation-fill-mode: both;
animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
animation-name: compile-button-bounce;
}

View file

@ -1,24 +1,8 @@
@stripe-width: 20px;
@keyframes pdf-toolbar-stripes {
from {
background-position: 0 0;
}
to {
background-position: @stripe-width 0;
}
}
.pdf .toolbar.toolbar-pdf {
.toolbar-small-mixin;
.toolbar-alt-mixin;
padding-right: 5px;
margin-left: 0;
&.changes-to-autocompile {
// prettier-ignore
#gradient > .striped(@color: rgba(255, 255, 255, 0.1), @angle: -45deg);
background-size: @stripe-width @stripe-width;
.animation(pdf-toolbar-stripes 2s linear infinite);
}
.auto-compile-status {
color: white;
margin-right: (@line-height-computed / 2);
@ -44,6 +28,10 @@
flex: 1 1 100%;
}
.toolbar-pdf-left when (@is-new-css = true) {
margin-left: 2px;
}
.toolbar-pdf-right {
flex: 1 0 auto;
}
@ -67,7 +55,7 @@
}
.toolbar-pdf-hybrid {
.btn:not(.btn-recompile):not(.btn-orphan):not(.detach-synctex-control):not(.switch-to-editor-btn) {
.btn:not(.detach-compile-button):not(.btn-orphan):not(.detach-synctex-control):not(.switch-to-editor-btn):not(.split-menu-dropdown-toggle):not(.split-menu-button) {
display: inline-block;
color: @toolbar-btn-color;
background-color: transparent;
@ -121,10 +109,6 @@
}
}
}
.btn-recompile {
padding-top: 2px;
}
}
.pdf {
@ -152,89 +136,6 @@
}
}
.btn-recompile-group {
align-self: stretch;
margin-right: 6px;
border-radius: 0 @btn-border-radius-base @btn-border-radius-base 0;
background-color: @btn-primary-bg;
&.btn-recompile-group-has-changes {
// prettier-ignore
#gradient > .striped(@color: rgba(255, 255, 255, 0.2), @angle: -45deg);
background-size: @stripe-width @stripe-width;
.animation(pdf-toolbar-stripes 2s linear infinite);
}
}
.btn-recompile {
height: 100%;
// .btn-primary;
color: #fff;
background-color: transparent;
padding-top: 3px;
padding-bottom: 3px;
&:first-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
.btn-recompile when (@is-new-css = false) {
&[disabled],
&[disabled].active,
&[disabled]:hover,
&[disabled]:focus {
background-color: mix(@btn-primary-bg, @toolbar-alt-bg-color, 65%);
.opacity(1);
}
}
.btn-recompile when (@is-new-css = true) {
border: none;
}
.btn-recompile-label {
margin-left: @line-height-computed / 4;
}
@keyframes compile-button-flash {
from,
to {
background: rgba(0, 0, 0, 0);
}
25%,
75% {
background: rgba(0, 0, 0, 0.2);
}
}
@keyframes compile-button-bounce {
from,
50%,
to {
transform: translateY(0);
}
25%,
75% {
transform: translateY(2px);
}
}
.btn-recompile-animate {
animation-duration: 1.2s;
animation-fill-mode: both;
animation-timing-function: ease-in-out;
animation-name: compile-button-flash;
}
.btn-recompile-animate .caret {
animation-duration: 0.6s;
animation-delay: 0.4s;
animation-fill-mode: both;
animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
animation-name: compile-button-bounce;
}
.toolbar-text {
padding-left: @padding-xs;
}

View file

@ -150,14 +150,6 @@
z-index: 1;
}
&.toolbar-small {
.toolbar-small-mixin;
}
&.toolbar-tall {
.toolbar-small-mixin;
}
&.toolbar-alt {
.toolbar-alt-mixin;
}

View file

@ -242,16 +242,3 @@
display: flex;
flex-wrap: nowrap;
}
// allow hiding toolbar content at various breakpoints
.toolbar-large .toolbar-hide-large {
display: none !important;
}
.toolbar-medium .toolbar-hide-medium {
display: none !important;
}
.toolbar-small .toolbar-hide-small {
display: none !important;
}

View file

@ -0,0 +1,91 @@
.split-menu {
display: flex;
&.btn-md,
&.btn-sm,
&.btn-xs {
padding: 0;
}
&.btn-md {
& > .split-menu-button,
& > .split-menu-dropdown-toggle {
height: 36px;
}
}
&.btn-sm {
& > .split-menu-button,
& > .split-menu-dropdown-toggle {
height: 32px;
}
}
&.btn-xs {
& > .split-menu-button,
& > .split-menu-dropdown-toggle {
height: 28px;
}
}
&.btn-primary {
background-color: @ol-green;
}
.split-menu-icon {
margin-right: @line-height-computed / 4;
}
.split-menu-button {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
height: 100%;
color: @white;
padding-top: 0;
padding-bottom: 0;
&.btn-primary {
border-right: 1px solid @green-10;
}
// on new css, btn-secondary already has a border
&.btn-secondary when (@is-new-css = false) {
border-right: 1px solid @neutral-10;
}
&.btn-danger {
border-right: 1px solid @red-10;
}
&[disabled] when (@is-new-css = false) {
opacity: 1;
}
}
.split-menu-button when (@is-new-css = true) {
// workaround for for the blue 2x border on the new css
// if z-index rule is not added, the border will overlap under the `split-menu-dropdown-toggle` since margin between both component is only 1px
z-index: 1;
}
.split-menu-dropdown {
float: none;
display: flex;
flex-wrap: nowrap;
align-self: stretch;
margin-right: 6px;
.split-menu-dropdown-toggle {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
padding: 0 8px;
// on new css, btn-secondary has a border
// since the border between both buttons already been defined in the `split-menu-button`
// we will remove the rule from the dropdown toggle
&.btn-secondary when (@is-new-css = true) {
border-left: none;
}
}
}
}

View file

@ -595,10 +595,13 @@
}
&[data-ol-loading='true'] {
// use the default state colors when in a loading state
color: @btn-bordered-color!important;
background-color: @btn-bordered-background-color!important;
border-color: @btn-bordered-border-color!important;
&,
&:hover {
// use the default state colors when in a loading state
color: @btn-bordered-color!important;
background-color: @btn-bordered-background-color!important;
border-color: @btn-bordered-border-color!important;
}
}
}
@ -651,10 +654,13 @@
}
&[data-ol-loading='true'] {
// use the default state colors when in a loading state
color: @btn-borderless-color!important;
background-color: @btn-borderless-background-color!important;
border-color: @btn-borderless-border-color!important;
&,
&:hover {
// use the default state colors when in a loading state
color: @btn-borderless-color!important;
background-color: @btn-borderless-background-color!important;
border-color: @btn-borderless-border-color;
}
}
}

View file

@ -11,6 +11,8 @@
@white: #ffffff;
@neutral-20: #e7e9ee;
@neutral-90: #1b222c;
@neutral-40: #afb5c0;
@neutral-10: #f4f5f6;
// Styleguide colors
@ol-blue-gray-0: #f4f5f8;

View file

@ -71,6 +71,7 @@
@import 'components/divider.less';
@import 'components/input-switch.less';
@import 'components/container.less';
@import 'components/split-menu.less';
// Components w/ JavaScript
@import 'components/modals.less';

View file

@ -1,9 +1,9 @@
import { EditorProviders } from '../../helpers/editor-providers'
import DetachCompileButton from '../../../../frontend/js/features/pdf-preview/components/detach-compile-button'
import DetachCompileButtonWrapper from '../../../../frontend/js/features/pdf-preview/components/detach-compile-button-wrapper'
import { mockScope } from './scope'
import { testDetachChannel } from '../../helpers/detach-channel'
describe('<DetachCompileButton/>', function () {
describe('<DetachCompileButtonWrapper />', function () {
beforeEach(function () {
cy.interceptCompile()
cy.interceptEvents()
@ -22,7 +22,7 @@ describe('<DetachCompileButton/>', function () {
cy.mount(
<EditorProviders scope={scope}>
<DetachCompileButton />
<DetachCompileButtonWrapper />
</EditorProviders>
)
@ -38,7 +38,7 @@ describe('<DetachCompileButton/>', function () {
cy.mount(
<EditorProviders scope={scope}>
<DetachCompileButton />
<DetachCompileButtonWrapper />
</EditorProviders>
)
@ -61,7 +61,7 @@ describe('<DetachCompileButton/>', function () {
cy.mount(
<EditorProviders scope={scope}>
<DetachCompileButton />
<DetachCompileButtonWrapper />
</EditorProviders>
)

View file

@ -0,0 +1,67 @@
import SplitMenu from '../../../../frontend/js/shared/components/split-menu'
describe('SplitMenu', function () {
it('renders primary variant', function () {
cy.mount(
<SplitMenu
bsStyle="primary"
button={{
text: 'Button Text',
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
>
<SplitMenu.Item>Item 1</SplitMenu.Item>
<SplitMenu.Item>Item 2</SplitMenu.Item>
<SplitMenu.Item>Item 3</SplitMenu.Item>
</SplitMenu>
)
cy.get('button.split-menu-button').contains('Button Text')
cy.get('button.split-menu-button').should('have.class', 'btn-primary')
cy.get('button.split-menu-dropdown-toggle').should(
'have.class',
'btn-primary'
)
cy.get('li').should('have.length', 3)
cy.get('li').contains('Item 1')
cy.get('li').contains('Item 2')
cy.get('li').contains('Item 3')
cy.get('ul.dropdown-menu').should('not.be.visible')
cy.get('button.split-menu-dropdown-toggle').click()
cy.get('ul.dropdown-menu').should('be.visible')
})
it('with custom classNames', function () {
cy.mount(
<SplitMenu
bsStyle="primary"
button={{
text: 'Button Text',
className: 'split-menu-class-1',
}}
dropdown={{
id: 'pdf-recompile-dropdown',
className: 'split-menu-class-2',
}}
dropdownToggle={{
className: 'split-menu-class-3',
}}
>
<SplitMenu.Item>Item 1</SplitMenu.Item>
</SplitMenu>
)
cy.get('button.split-menu-button').should(
'have.class',
'split-menu-class-1'
)
cy.get('div.split-menu-dropdown').should('have.class', 'split-menu-class-2')
cy.get('button.split-menu-dropdown-toggle').should(
'have.class',
'split-menu-class-3'
)
})
})

View file

@ -0,0 +1,11 @@
export type BsStyle =
| 'primary'
| 'secondary'
| 'danger'
| 'info'
| 'default'
| 'link'
| 'warning'
| 'success'
export type BsSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'