diff --git a/.eslintrc.json b/.eslintrc.json index 6bf70b00..3bbc35a9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["cozy-app/react"], - "ignorePatterns": ["src/plugins/**/*.ts", "src/plugins/**/*.tsx"], + "ignorePatterns": ["src/plugins/**/*.ts", "src/plugins/**/*.tsx", "src/ui/**/*.ts", "src/ui/**/*.tsx"], "parser": "@babel/eslint-parser", "overrides": [ { diff --git a/app.config.js b/app.config.js index b6331fcd..3a0f0e74 100644 --- a/app.config.js +++ b/app.config.js @@ -27,6 +27,14 @@ const extraConfig = { __dirname, 'node_modules/cozy-editor-core/src/plugins/image-upload' )]: path.resolve(__dirname, './src/plugins/image-upload'), + [path.resolve( + __dirname, + 'node_modules/cozy-editor-core/src/ui/Dropdown' + )]: path.resolve(__dirname, './src/ui/Dropdown'), + [path.resolve( + __dirname, + 'node_modules/cozy-editor-core/src/ui/DropdownMenu' + )]: path.resolve(__dirname, './src/ui/DropdownMenu'), ['@atlaskit/editor-core']: path.resolve( __dirname, 'node_modules/cozy-editor-core/src' diff --git a/src/components/header_menu.jsx b/src/components/header_menu.jsx index bc819c44..2fb86a39 100644 --- a/src/components/header_menu.jsx +++ b/src/components/header_menu.jsx @@ -1,19 +1,19 @@ import React, { useMemo } from 'react' +import { models } from 'cozy-client' +import { SharedRecipients } from 'cozy-sharing' import AppLinker from 'cozy-ui/transpiled/react/AppLinker' import AppIcon from 'cozy-ui/transpiled/react/AppIcon' import Divider from 'cozy-ui/transpiled/react/MuiCozyTheme/Divider' import Link from 'cozy-ui/transpiled/react/Link' -import { SharedRecipients } from 'cozy-sharing' -import { Typography } from '@material-ui/core' +import Typography from 'cozy-ui/transpiled/react/Typography' + import { Slugs } from 'constants/strings' -import { NotePath } from './notes/List/NotePath' -import { CozyFile } from 'cozy-doctypes' +import { Breakpoints } from 'types/enums' import { getDriveLink } from 'lib/utils' - -import styles from './header_menu.styl' +import { NotePath } from './notes/List/NotePath' import { WithBreakpoints } from './notes/List/WithBreakpoints' -import { Breakpoints } from 'types/enums' +import styles from './header_menu.styl' const HeaderMenu = ({ homeHref, @@ -28,8 +28,9 @@ const HeaderMenu = ({ () => getDriveLink(client, file.attributes.dir_id), [client, file.attributes.dir_id] ) + const simplifiedDrivePath = drivePath.split('#')[1] - const { filename } = CozyFile.splitFilename(file.attributes) + const { filename } = models.file.splitFilename(file.attributes) return (
@@ -42,7 +43,7 @@ const HeaderMenu = ({ )} - + {backFromEditing &&
{backFromEditing}
} @@ -63,7 +64,7 @@ const HeaderMenu = ({ diff --git a/src/components/header_menu.styl b/src/components/header_menu.styl index 48f33dfe..d1baa978 100644 --- a/src/components/header_menu.styl +++ b/src/components/header_menu.styl @@ -5,7 +5,7 @@ justify-content space-between padding 0.5rem 1rem position relative - z-index 1 + z-index var(--zIndex-low) // Needed for the drop-shadow to be above the editor area .app-icon .home-link @@ -21,5 +21,3 @@ display flex flex-grow 1 -.divider - margin 0 1rem !important diff --git a/src/locales/atlassian_missing_french.json b/src/locales/atlassian_missing_french.json index 1825b0f3..3d69b288 100644 --- a/src/locales/atlassian_missing_french.json +++ b/src/locales/atlassian_missing_french.json @@ -9,5 +9,7 @@ "fabric.editor.layoutFixedWidth": "Retour au centre", "fabric.editor.alignImageLeft": "Aligner l'image à gauche", "fabric.editor.alignImageCenter": "Centrer l'image", - "fabric.editor.alignImageRight": "Aligner l'image à droite" + "fabric.editor.alignImageRight": "Aligner l'image à droite", + "fabric.editor.numberedColumn": "Lignes numérotées" + } diff --git a/src/styles/index.css b/src/styles/index.css index 9f06a3f6..bc5ce6aa 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -35,7 +35,7 @@ html { --note-title5-color: var(--coolGrey); --note-title6-color: var(--coolGrey); --note-border-radius: 8px; - --note-header-height: 3rem; + --note-header-height: 64px; --note-header-height-half: 1.5rem; color: var(--note-base-color); } @@ -72,7 +72,7 @@ html .akEditor > div:first-child { } .note-editor-container { - height: calc(100% - 48px); + overflow: hidden; } /* prose mirror toolbar in the upper bar */ @@ -82,16 +82,19 @@ html .akEditor > div:first-child { height: auto; position: fixed; top: auto; - bottom: 1rem; + bottom: calc(env(safe-area-inset-bottom, 1rem) + 1rem); left: 50%; transform: translateX(-50%); + background: none; + box-shadow: none; } /* toolbar inner wrapper */ html .akEditor > div:first-child > div > div:last-child { box-shadow: 0px 0px 0px 0.5px rgba(29, 33, 42, 0.12), 0px 2px 4px rgba(29, 33, 42, 0.0793047), 0px 4px 16px rgba(29, 33, 42, 0.06); border-radius: 8px; - padding: 0 0.5rem; + padding: 0.5rem; + background: white; } /* This is the main toolbar wrapper */ @@ -120,14 +123,12 @@ html .akEditor > div:first-child > div:first-child + div hr { [data-testid="ak-editor-main-toolbar"] > div > div:nth-child(2) { display: flex; align-items: center; - width: 730px; /* full toolbar */ - max-width: 100%; + max-width: 768px; margin: auto; - height: var(--note-header-height); } [data-testid="ak-editor-main-toolbar"] + div { - height: calc(100% - 130px); + height: calc(100%); } /* @@ -402,7 +403,7 @@ html .fabric-editor-popup-scroll-parent > div > div { } html .ak-editor-content-area { - padding: var(--documentTopPadding) var(--documentPadding) var(--documentPadding) var(--documentPadding) !important; + padding: var(--documentTopPadding) var(--documentPadding) calc(3 * var(--note-base-size)) var(--documentPadding) !important; background-color: var(--white); } @@ -454,3 +455,14 @@ html .fabric-editor-popup-scroll-parent > div > div { .richMedia-resize-handle-right { z-index: auto !important; } + +/* Required for toolbar's popups because unidentified internals miscalculate the popups position */ +/* Since we always display the toolbar at the bottom of the screen we do not care about any other top position than zero */ +[data-testid="ak-editor-main-toolbar"] [aria-label*="Popup"] { + top: 0 !important; +} + +/* Weird overflow linked to the floating toolbar, could not locate the exact origin of the problem */ +body { + overflow-x: hidden; +} diff --git a/src/ui/Dropdown/index.tsx b/src/ui/Dropdown/index.tsx new file mode 100644 index 00000000..6fd3f496 --- /dev/null +++ b/src/ui/Dropdown/index.tsx @@ -0,0 +1,102 @@ +import React from 'react' +import { PureComponent } from 'react' +import DropdownList from '@atlaskit/droplist' +import { Popup } from '@atlaskit/editor-common' +import withOuterListeners from '../with-outer-listeners' + +export interface Props { + mountTo?: HTMLElement + boundariesElement?: HTMLElement + scrollableElement?: HTMLElement + trigger: React.ReactElement + isOpen?: boolean + onOpenChange?: (attrs: any) => void + fitWidth?: number + fitHeight?: number + zIndex?: number +} + +export interface State { + target?: HTMLElement + popupPlacement: [string, string] +} + +/** + * Wrapper around @atlaskit/droplist which uses Popup and Portal to render + * droplist outside of "overflow: hidden" containers when needed. + * + * Also it controls popper's placement. + */ +export class Dropdown extends PureComponent { + constructor(props: Props) { + super(props) + + this.state = { + popupPlacement: ['bottom', 'left'] + } + } + + private handleRef = (target: HTMLElement | null) => { + this.setState({ target: target || undefined }) + } + + private updatePopupPlacement = (placement: [string, string]) => { + this.setState({ popupPlacement: placement }) + } + + private renderDropdown() { + const { target } = this.state + const { + children, + mountTo, + boundariesElement, + scrollableElement, + onOpenChange, + fitHeight, + fitWidth, + zIndex + } = this.props + + return ( + +
+ + {children} + +
+
+ ) + } + + render() { + const { trigger, isOpen } = this.props + + return ( + <> +
{trigger}
+ {isOpen ? this.renderDropdown() : null} + + ) + } +} + +const DropdownWithOuterListeners = withOuterListeners(Dropdown) + +export default DropdownWithOuterListeners diff --git a/src/ui/DropdownMenu/index.tsx b/src/ui/DropdownMenu/index.tsx new file mode 100644 index 00000000..7a0f8ae3 --- /dev/null +++ b/src/ui/DropdownMenu/index.tsx @@ -0,0 +1,162 @@ +import React, { PureComponent } from 'react' +import styled from 'styled-components' +import DropList from '@atlaskit/droplist' +import Item, { ItemGroup } from '@atlaskit/item' +import Tooltip from '@atlaskit/tooltip' +import { Popup } from '@atlaskit/editor-common' +import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles' +import withOuterListeners from '../with-outer-listeners' +import { Props, State } from './types' + +const Wrapper = styled.div` + /* tooltip in ToolbarButton is display:block */ + & > div > div { + display: flex; + } +` + +const DropListWithOutsideListeners: any = withOuterListeners(DropList) + +/** + * Hack for item to imitate old dropdown-menu selected styles + */ +const ItemWrapper: any = styled.div` + ${(props: any) => + props.isSelected + ? '&& > span, && > span:hover { background: #6c798f; color: #fff; }' + : ''}; +` + +const ItemContentWrapper: any = styled.span` + ${(props: any) => (props.hasElemBefore ? 'margin-left: 8px;' : '')}; +` + +/** + * Wrapper around @atlaskit/droplist which uses Popup and Portal to render + * dropdown-menu outside of "overflow: hidden" containers when needed. + * + * Also it controls popper's placement. + */ +export default class DropdownMenuWrapper extends PureComponent { + state: State = { + popupPlacement: ['bottom', 'left'] + } + + private handleRef = (target: HTMLElement | null) => { + this.setState({ target: target || undefined }) + } + + private updatePopupPlacement = (placement: [string, string]) => { + this.setState({ popupPlacement: placement }) + } + + private handleClose = () => { + if (this.props.onOpenChange) { + this.props.onOpenChange({ isOpen: false }) + } + } + + private renderItem(item: typeof Item) { + const { onItemActivated, onMouseEnter, onMouseLeave } = this.props + + // onClick and value.name are the action indicators in the handlers + // If neither are present, don't wrap in an Item. + if (!item.onClick && !item.value && !item.value.name) { + return {item.content} + } + + const dropListItem = ( + + onItemActivated && onItemActivated({ item })} + onMouseEnter={() => onMouseEnter && onMouseEnter({ item })} + onMouseLeave={() => onMouseLeave && onMouseLeave({ item })} + className={item.className} + > + + {item.content} + + + + ) + + if (item.tooltipDescription) { + return ( + + {dropListItem} + + ) + } + + return dropListItem + } + + private renderDropdownMenu() { + const { target, popupPlacement } = this.state + const { + items, + mountTo, + boundariesElement, + scrollableElement, + offset, + fitHeight, + fitWidth, + isOpen, + zIndex + } = this.props + const isCellPopup = items[0].items.some( + ({ content }) => content === 'Cell background' + ) + const position = isCellPopup ? popupPlacement.join(' ') : 'top left' + + return ( + + +
+ {items.map((group, index) => ( + + {group.items.map(item => this.renderItem(item))} + + ))} + + + ) + } + + render() { + const { children, isOpen } = this.props + + return ( + +
{children}
+ {isOpen ? this.renderDropdownMenu() : null} +
+ ) + } +} diff --git a/src/ui/DropdownMenu/types.ts b/src/ui/DropdownMenu/types.ts new file mode 100644 index 00000000..06630cb4 --- /dev/null +++ b/src/ui/DropdownMenu/types.ts @@ -0,0 +1,43 @@ +import React from 'react' +import EditorActions from '../../actions' + +export interface Props { + mountTo?: HTMLElement + boundariesElement?: HTMLElement + scrollableElement?: HTMLElement + isOpen?: boolean + onOpenChange?: (attrs: any) => void + onItemActivated?: (attrs: any) => void + onMouseEnter?: (attrs: any) => void + onMouseLeave?: (attrs: any) => void + fitWidth?: number + fitHeight?: number + offset?: Array + zIndex?: number + items: Array<{ + items: MenuItem[] + }> +} + +export interface MenuItem { + key?: string + content: string | React.ReactChild | React.ReactFragment + value: { + name: string + } + shortcut?: string + elemBefore?: React.ReactElement + elemAfter?: React.ReactElement + tooltipDescription?: string + tooltipPosition?: string + isActive?: boolean + isDisabled?: boolean + handleRef?: any + className?: string + onClick?: (editorActions: EditorActions) => void +} + +export interface State { + target?: HTMLElement + popupPlacement: [string, string] +} diff --git a/src/ui/with-outer-listeners.tsx b/src/ui/with-outer-listeners.tsx new file mode 100644 index 00000000..7e6da083 --- /dev/null +++ b/src/ui/with-outer-listeners.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import { ComponentClass, StatelessComponent, PureComponent } from 'react' +import ReactDOM from 'react-dom' + +type SimpleEventHandler = (event: T) => void + +export interface WithOutsideClickProps { + handleClickOutside?: SimpleEventHandler + handleEscapeKeydown?: SimpleEventHandler + handleEnterKeydown?: SimpleEventHandler +} + +export default function withOuterListeners

( + Component: ComponentClass

| StatelessComponent

+): ComponentClass

{ + return class WithOutsideClick extends PureComponent< + P & WithOutsideClickProps, + {} + > { + componentDidMount() { + if (this.props.handleClickOutside) { + document.addEventListener('click', this.handleClick, false) + } + + if (this.props.handleEscapeKeydown) { + document.addEventListener('keydown', this.handleKeydown, false) + } + } + + componentWillUnmount() { + if (this.props.handleClickOutside) { + document.removeEventListener('click', this.handleClick, false) + } + + if (this.props.handleEscapeKeydown) { + document.removeEventListener('keydown', this.handleKeydown, false) + } + } + + handleClick = (evt: MouseEvent) => { + const domNode = ReactDOM.findDOMNode(this) // eslint-disable-line react/no-find-dom-node + if ( + !domNode || + (evt.target instanceof Node && !domNode.contains(evt.target)) + ) { + if (this.props.handleClickOutside) { + this.props.handleClickOutside(evt) + } + } + } + + handleKeydown = (evt: KeyboardEvent) => { + if (evt.code === 'Escape' && this.props.handleEscapeKeydown) { + this.props.handleEscapeKeydown(evt) + } else if (evt.code === 'Enter' && this.props.handleEnterKeydown) { + this.props.handleEnterKeydown(evt) + } + } + + render() { + return + } + } +}