diff --git a/app/components/Breadcrumb.tsx b/app/components/Breadcrumb.tsx index 8b4e9a2d4..197b08efa 100644 --- a/app/components/Breadcrumb.tsx +++ b/app/components/Breadcrumb.tsx @@ -4,6 +4,7 @@ import { Link } from "react-router-dom"; import styled from "styled-components"; import Flex from "~/components/Flex"; import BreadcrumbMenu from "~/menus/BreadcrumbMenu"; +import { ellipsis } from "~/styles"; import { MenuInternalLink } from "~/types"; type Props = { @@ -64,6 +65,7 @@ const Slash = styled(GoToIcon)` `; const Item = styled(Link)<{ $highlight: boolean; $withIcon: boolean }>` + ${ellipsis()} display: flex; flex-shrink: 1; min-width: 0; @@ -71,9 +73,6 @@ const Item = styled(Link)<{ $highlight: boolean; $withIcon: boolean }>` color: ${(props) => props.theme.text}; font-size: 15px; height: 24px; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; font-weight: ${(props) => (props.$highlight ? "500" : "inherit")}; margin-left: ${(props) => (props.$withIcon ? "4px" : "0")}; diff --git a/app/components/CommandBarItem.tsx b/app/components/CommandBarItem.tsx index f9de5a0d4..c011f8a5e 100644 --- a/app/components/CommandBarItem.tsx +++ b/app/components/CommandBarItem.tsx @@ -4,6 +4,7 @@ import * as React from "react"; import styled, { css, useTheme } from "styled-components"; import Flex from "~/components/Flex"; import Key from "~/components/Key"; +import { ellipsis } from "~/styles"; type Props = { action: ActionImpl; @@ -85,8 +86,7 @@ const Ancestor = styled.span` `; const Content = styled(Flex)` - overflow: hidden; - text-overflow: ellipsis; + ${ellipsis()} flex-shrink: 1; `; @@ -102,9 +102,7 @@ const Item = styled.div<{ active?: boolean }>` justify-content: space-between; cursor: var(--pointer); - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; + ${ellipsis()} user-select: none; min-width: 0; diff --git a/app/components/DocumentCard.tsx b/app/components/DocumentCard.tsx index c295f539a..f11d33bcc 100644 --- a/app/components/DocumentCard.tsx +++ b/app/components/DocumentCard.tsx @@ -13,6 +13,7 @@ import Flex from "~/components/Flex"; import NudeButton from "~/components/NudeButton"; import Time from "~/components/Time"; import useStores from "~/hooks/useStores"; +import { ellipsis } from "~/styles"; import CollectionIcon from "./Icons/CollectionIcon"; import EmojiIcon from "./Icons/EmojiIcon"; import Squircle from "./Squircle"; @@ -217,14 +218,12 @@ const Content = styled(Flex)` `; const DocumentMeta = styled(Text)` + ${ellipsis()} display: flex; align-items: center; gap: 2px; color: ${(props) => props.theme.textTertiary}; margin: 0 0 0 -2px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; `; const DocumentLink = styled(Link)<{ diff --git a/app/components/DocumentExplorerNode.tsx b/app/components/DocumentExplorerNode.tsx index 0f45c0b2b..fddcc57dc 100644 --- a/app/components/DocumentExplorerNode.tsx +++ b/app/components/DocumentExplorerNode.tsx @@ -6,6 +6,7 @@ import breakpoint from "styled-components-breakpoint"; import Flex from "~/components/Flex"; import Disclosure from "~/components/Sidebar/components/Disclosure"; import Text from "~/components/Text"; +import { ellipsis } from "~/styles"; type Props = { selected: boolean; @@ -70,9 +71,7 @@ function DocumentExplorerNode( } const Title = styled(Text)` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + ${ellipsis()} margin: 0 4px 0 4px; color: inherit; `; diff --git a/app/components/DocumentExplorerSearchResult.tsx b/app/components/DocumentExplorerSearchResult.tsx index bb4675851..989c65d75 100644 --- a/app/components/DocumentExplorerSearchResult.tsx +++ b/app/components/DocumentExplorerSearchResult.tsx @@ -6,6 +6,7 @@ import styled from "styled-components"; import { Node as SearchResult } from "~/components/DocumentExplorerNode"; import Flex from "~/components/Flex"; import Text from "~/components/Text"; +import { ellipsis } from "~/styles"; type Props = { selected: boolean; @@ -73,10 +74,8 @@ const Title = styled(Text)` `; const Path = styled(Text)<{ $selected: boolean }>` + ${ellipsis()} padding-top: 2px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; margin: 0 4px 0 8px; color: ${(props) => props.$selected ? props.theme.white50 : props.theme.textTertiary}; diff --git a/app/components/DocumentMeta.tsx b/app/components/DocumentMeta.tsx index f96c9be94..8c6c339bb 100644 --- a/app/components/DocumentMeta.tsx +++ b/app/components/DocumentMeta.tsx @@ -11,6 +11,7 @@ import Flex from "~/components/Flex"; import Time from "~/components/Time"; import useCurrentUser from "~/hooks/useCurrentUser"; import useStores from "~/hooks/useStores"; +import { ellipsis } from "~/styles"; type Props = { showCollection?: boolean; @@ -192,8 +193,7 @@ const Container = styled(Flex)<{ rtl?: boolean }>` `; const Viewed = styled.span` - text-overflow: ellipsis; - overflow: hidden; + ${ellipsis()} `; const Modified = styled.span<{ highlight?: boolean }>` diff --git a/app/components/Input.tsx b/app/components/Input.tsx index 83db3344f..78dc84d9a 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -4,7 +4,7 @@ import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; import Flex from "~/components/Flex"; import Text from "~/components/Text"; -import { undraggableOnDesktop } from "~/styles"; +import { ellipsis, undraggableOnDesktop } from "~/styles"; const RealTextarea = styled.textarea<{ hasIcon?: boolean }>` border: 0; @@ -29,9 +29,7 @@ const RealInput = styled.input<{ hasIcon?: boolean }>` color: ${(props) => props.theme.text}; height: 30px; min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + ${ellipsis()} ${undraggableOnDesktop()} &:disabled, diff --git a/app/components/List/Item.tsx b/app/components/List/Item.tsx index 6b42e27c0..6a41ae6ea 100644 --- a/app/components/List/Item.tsx +++ b/app/components/List/Item.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import styled, { useTheme } from "styled-components"; import Flex from "~/components/Flex"; import NavLink from "~/components/NavLink"; +import { ellipsis } from "~/styles"; export type Props = Omit, "title"> & { image?: React.ReactNode; @@ -103,9 +104,7 @@ const Image = styled(Flex)` const Heading = styled.p<{ $small?: boolean }>` font-size: ${(props) => (props.$small ? 14 : 16)}px; font-weight: 500; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; + ${ellipsis()} line-height: ${(props) => (props.$small ? 1.3 : 1.2)}; margin: 0; `; diff --git a/app/components/ResizingHeightContainer.tsx b/app/components/ResizingHeightContainer.tsx index 779eac277..39c3efcb7 100644 --- a/app/components/ResizingHeightContainer.tsx +++ b/app/components/ResizingHeightContainer.tsx @@ -19,7 +19,17 @@ type Props = { * Automatically animates the height of a container based on it's contents. */ export function ResizingHeightContainer(props: Props) { - const { hideOverflow, children, config, style } = props; + const { + hideOverflow, + children, + config = { + transition: { + duration: 0.1, + ease: "easeInOut", + }, + }, + style, + } = props; const ref = React.useRef(null); const { height } = useComponentSize(ref); diff --git a/app/components/SearchListItem.tsx b/app/components/SearchListItem.tsx index 1f47144b9..5b63a16ec 100644 --- a/app/components/SearchListItem.tsx +++ b/app/components/SearchListItem.tsx @@ -6,7 +6,7 @@ import styled, { css } from "styled-components"; import breakpoint from "styled-components-breakpoint"; import Document from "~/models/Document"; import Highlight, { Mark } from "~/components/Highlight"; -import { hover } from "~/styles"; +import { ellipsis, hover } from "~/styles"; import { sharedDocumentPath } from "~/utils/routeHelpers"; type Props = { @@ -125,8 +125,7 @@ const Heading = styled.h4<{ rtl?: boolean }>` const Title = styled(Highlight)` max-width: 90%; - overflow: hidden; - text-overflow: ellipsis; + ${ellipsis()} ${Mark} { padding: 0; @@ -139,10 +138,7 @@ const ResultContext = styled(Highlight)` font-size: 14px; margin-top: -0.25em; margin-bottom: 0.25em; - - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + ${ellipsis()} ${Mark} { padding: 0; diff --git a/app/editor/components/FloatingToolbar.tsx b/app/editor/components/FloatingToolbar.tsx index 8d38df698..c99f08cf0 100644 --- a/app/editor/components/FloatingToolbar.tsx +++ b/app/editor/components/FloatingToolbar.tsx @@ -13,13 +13,15 @@ import { useEditor } from "./EditorContext"; type Props = { active?: boolean; children: React.ReactNode; + width?: number; forwardedRef?: React.RefObject | null; }; const defaultPosition = { - left: -1000, + left: -10000, top: 0, offset: 0, + maxWidth: 1000, visible: false, }; @@ -48,6 +50,7 @@ function usePosition({ right: 0, top: viewportHeight - menuHeight, offset: 0, + maxWidth: 1000, visible: true, }; } @@ -134,7 +137,7 @@ function usePosition({ const margin = 12; const left = Math.min( Math.min( - offsetParent.x + offsetParent.width - menuWidth, + offsetParent.x + offsetParent.width - menuWidth - margin, window.innerWidth - margin ), Math.max( @@ -155,6 +158,7 @@ function usePosition({ left: Math.round(left - offsetParent.left), top: Math.round(top - offsetParent.top), offset: Math.round(offset), + maxWidth: offsetParent.width, visible: true, }; } @@ -189,8 +193,10 @@ const FloatingToolbar = React.forwardRef( ` will-change: opacity, transform; padding: 8px 16px; @@ -234,7 +240,7 @@ const Wrapper = styled.div<{ z-index: -1; position: absolute; bottom: -2px; - left: calc(50% - ${(props) => props.offset || 0}px); + left: calc(50% - ${(props) => props.$offset || 0}px); pointer-events: none; } diff --git a/app/editor/components/Input.tsx b/app/editor/components/Input.tsx index b6acfe94f..15e7137be 100644 --- a/app/editor/components/Input.tsx +++ b/app/editor/components/Input.tsx @@ -10,6 +10,7 @@ const Input = styled.input` margin: 0; outline: none; flex-grow: 1; + min-width: 0; @media (hover: none) and (pointer: coarse) { font-size: 16px; diff --git a/app/editor/components/LinkEditor.tsx b/app/editor/components/LinkEditor.tsx index 4f23aae96..bf92ef828 100644 --- a/app/editor/components/LinkEditor.tsx +++ b/app/editor/components/LinkEditor.tsx @@ -3,7 +3,6 @@ import { DocumentIcon, CloseIcon, PlusIcon, - TrashIcon, OpenIcon, } from "outline-icons"; import { Mark } from "prosemirror-model"; @@ -13,6 +12,8 @@ import * as React from "react"; import styled from "styled-components"; import { isInternalUrl, sanitizeUrl } from "@shared/utils/urls"; import Flex from "~/components/Flex"; +import { ResizingHeightContainer } from "~/components/ResizingHeightContainer"; +import Scrollable from "~/components/Scrollable"; import { Dictionary } from "~/hooks/useDictionary"; import { ToastOptions } from "~/types"; import Input from "./Input"; @@ -61,6 +62,7 @@ class LinkEditor extends React.Component { discardInputValue = false; initialValue = this.href; initialSelectionLength = this.props.to - this.props.from; + resultsRef = React.createRef(); state: State = { selectedIndex: -1, @@ -122,11 +124,12 @@ class LinkEditor extends React.Component { }; handleKeyDown = (event: React.KeyboardEvent): void => { + const results = this.results; + switch (event.key) { case "Enter": { event.preventDefault(); const { selectedIndex, value } = this.state; - const results = this.state.results[value] || []; const { onCreateLink } = this.props; if (selectedIndex >= 0) { @@ -181,8 +184,7 @@ class LinkEditor extends React.Component { event.preventDefault(); event.stopPropagation(); - const { selectedIndex, value } = this.state; - const results = this.state.results[value] || []; + const { selectedIndex } = this.state; const total = results.length; const nextIndex = selectedIndex + 1; @@ -264,10 +266,7 @@ class LinkEditor extends React.Component { dispatch(state.tr.removeMark(from, to, mark)); } - if (onRemoveLink) { - onRemoveLink(); - } - + onRemoveLink?.(); view.focus(); }; @@ -289,14 +288,19 @@ class LinkEditor extends React.Component { view.focus(); }; + get results() { + const { value } = this.state; + return ( + this.state.results[value.trim()] || + this.state.results[this.state.previousValue] || + [] + ); + } + render() { const { dictionary } = this.props; const { value, selectedIndex } = this.state; - const results = - this.state.results[value.trim()] || - this.state.results[this.state.previousValue] || - []; - + const results = this.results; const looksLikeUrl = value.match(/^https?:\/\//i); const suggestedLinkTitle = this.suggestedLinkTitle; const isInternal = isInternalUrl(value); @@ -307,7 +311,7 @@ class LinkEditor extends React.Component { suggestedLinkTitle.length > 0 && !looksLikeUrl; - const showResults = + const hasResults = !!suggestedLinkTitle && (showCreateLink || results.length > 0); return ( @@ -339,47 +343,53 @@ class LinkEditor extends React.Component { - {this.initialValue ? ( - - ) : ( - - )} + - {showResults && ( - - {results.map((result, index) => ( - } - onPointerMove={() => this.handleFocusLink(index)} - onClick={this.handleSelectLink(result.url, result.title)} - selected={index === selectedIndex} - /> - ))} + + + {hasResults && ( + <> + {results.map((result, index) => ( + } + onPointerMove={() => this.handleFocusLink(index)} + onClick={this.handleSelectLink(result.url, result.title)} + selected={index === selectedIndex} + containerRef={this.resultsRef} + /> + ))} - {showCreateLink && ( - } - onPointerMove={() => this.handleFocusLink(results.length)} - onClick={() => { - this.handleCreateLink(suggestedLinkTitle); + {showCreateLink && ( + } + onPointerMove={() => this.handleFocusLink(results.length)} + onClick={() => { + this.handleCreateLink(suggestedLinkTitle); - if (this.initialSelectionLength) { - this.moveSelectionToEnd(); - } - }} - selected={results.length === selectedIndex} - /> + if (this.initialSelectionLength) { + this.moveSelectionToEnd(); + } + }} + selected={results.length === selectedIndex} + /> + )} + )} - - )} + + ); } @@ -388,25 +398,20 @@ class LinkEditor extends React.Component { const Wrapper = styled(Flex)` margin-left: -8px; margin-right: -8px; - min-width: 336px; pointer-events: all; gap: 8px; `; -const SearchResults = styled.ol` +const SearchResults = styled(Scrollable)<{ $hasResults: boolean }>` background: ${(props) => props.theme.toolbarBackground}; position: absolute; top: 100%; width: 100%; height: auto; left: 0; - padding: 0; - margin: 0; - margin-top: -3px; - margin-bottom: 0; + margin: -8px 0 0; border-radius: 0 0 4px 4px; - overflow-y: auto; - overscroll-behavior: none; + padding: ${(props) => (props.$hasResults ? "8px 0" : "0")}; max-height: 260px; @media (hover: none) and (pointer: coarse) { diff --git a/app/editor/components/LinkSearchResult.tsx b/app/editor/components/LinkSearchResult.tsx index 3253cc1df..4aecf2c64 100644 --- a/app/editor/components/LinkSearchResult.tsx +++ b/app/editor/components/LinkSearchResult.tsx @@ -1,15 +1,24 @@ import * as React from "react"; import scrollIntoView from "smooth-scroll-into-view-if-needed"; import styled from "styled-components"; +import { ellipsis } from "~/styles"; -type Props = React.HTMLAttributes & { +type Props = React.HTMLAttributes & { icon: React.ReactNode; selected: boolean; title: React.ReactNode; subtitle?: React.ReactNode; + containerRef: React.RefObject; }; -function LinkSearchResult({ title, subtitle, selected, icon, ...rest }: Props) { +function LinkSearchResult({ + title, + subtitle, + containerRef, + selected, + icon, + ...rest +}: Props) { const ref = React.useCallback( (node: HTMLElement | null) => { if (selected && node) { @@ -17,36 +26,46 @@ function LinkSearchResult({ title, subtitle, selected, icon, ...rest }: Props) { scrollMode: "if-needed", block: "center", boundary: (parent) => { - // All the parent elements of your target are checked until they - // reach the #link-search-results. Prevents body and other parent - // elements from being scrolled - return parent.id !== "link-search-results"; + // Prevents body and other parent elements from being scrolled + return parent !== containerRef.current; }, }); } }, - [selected] + [containerRef, selected] ); return ( - - {icon} -
+ + {icon} + {title} {subtitle ? {subtitle} : null} -
+
); } -const IconWrapper = styled.span` - flex-shrink: 0; - margin-right: 4px; - opacity: 0.8; - color: ${(props) => props.theme.toolbarItem}; +const Content = styled.div` + overflow: hidden; `; -const ListItem = styled.li<{ +const IconWrapper = styled.span<{ selected: boolean }>` + flex-shrink: 0; + margin-right: 4px; + height: 24px; + opacity: 0.8; + color: ${(props) => + props.selected ? props.theme.accentText : props.theme.toolbarItem}; +`; + +const ListItem = styled.div<{ selected: boolean; compact: boolean; }>` @@ -54,9 +73,11 @@ const ListItem = styled.li<{ align-items: center; padding: 8px; border-radius: 4px; - color: ${(props) => props.theme.toolbarItem}; + margin: 0 8px; + color: ${(props) => + props.selected ? props.theme.accentText : props.theme.toolbarItem}; background: ${(props) => - props.selected ? props.theme.toolbarHoverBackground : "transparent"}; + props.selected ? props.theme.accent : "transparent"}; font-family: ${(props) => props.theme.fontFamily}; text-decoration: none; overflow: hidden; @@ -68,6 +89,7 @@ const ListItem = styled.li<{ `; const Title = styled.div` + ${ellipsis()} font-size: 14px; font-weight: 500; `; @@ -75,6 +97,7 @@ const Title = styled.div` const Subtitle = styled.div<{ selected: boolean; }>` + ${ellipsis()} font-size: 13px; opacity: ${(props) => (props.selected ? 0.75 : 0.5)}; `; diff --git a/app/editor/components/LinkToolbar.tsx b/app/editor/components/LinkToolbar.tsx index ef8463bf0..6bb2046fe 100644 --- a/app/editor/components/LinkToolbar.tsx +++ b/app/editor/components/LinkToolbar.tsx @@ -128,7 +128,7 @@ export default function LinkToolbar({ const active = isActive(view, rest.isActive); return ( - + {active && ( - {link && range ? ( + + {showLinkToolbar ? ( {!readOnly && this.view && ( <> + {this.marks.link && ( + + )} + {this.nodes.emoji && ( + + )} + {this.nodes.mention && ( + + )} - - - Record ) => { - setData(value(false, true)); + const text = value(true, true); + setData(text ? value(false, true) : undefined); onTyping?.(); }; @@ -251,7 +252,7 @@ function CommentForm({ : `${t("Add a reply")}…`) } /> - {inputFocused && ( + {(inputFocused || data) && ( {thread && !thread.isNew ? t("Reply") : t("Post")} diff --git a/app/scenes/Document/components/CommentThread.tsx b/app/scenes/Document/components/CommentThread.tsx index 21f3775b7..f6a88b865 100644 --- a/app/scenes/Document/components/CommentThread.tsx +++ b/app/scenes/Document/components/CommentThread.tsx @@ -173,15 +173,7 @@ function CommentThread({ ))} - + {(focused || commentsInThread.length === 0) && ( props.theme.text}; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; diff --git a/app/scenes/Document/components/SidebarLayout.tsx b/app/scenes/Document/components/SidebarLayout.tsx index be1d4d280..335900e03 100644 --- a/app/scenes/Document/components/SidebarLayout.tsx +++ b/app/scenes/Document/components/SidebarLayout.tsx @@ -10,6 +10,7 @@ import Flex from "~/components/Flex"; import Scrollable from "~/components/Scrollable"; import Tooltip from "~/components/Tooltip"; import useMobile from "~/hooks/useMobile"; +import { ellipsis } from "~/styles"; import { fadeIn } from "~/styles/animations"; type Props = React.HTMLAttributes & { @@ -75,15 +76,13 @@ const ForwardIcon = styled(BackIcon)` `; const Title = styled(Flex)` + ${ellipsis()} font-size: 16px; font-weight: 600; text-align: center; align-items: center; justify-content: flex-start; - text-overflow: ellipsis; - white-space: nowrap; user-select: none; - overflow: hidden; width: 0; flex-grow: 1; `; diff --git a/app/scenes/DocumentMove.tsx b/app/scenes/DocumentMove.tsx index 0aa9fc0f7..2e489b04a 100644 --- a/app/scenes/DocumentMove.tsx +++ b/app/scenes/DocumentMove.tsx @@ -12,6 +12,7 @@ import Text from "~/components/Text"; import useCollectionTrees from "~/hooks/useCollectionTrees"; import useStores from "~/hooks/useStores"; import useToasts from "~/hooks/useToasts"; +import { ellipsis } from "~/styles"; import { flattenTree } from "~/utils/tree"; type Props = { @@ -123,9 +124,7 @@ const Footer = styled(Flex)` `; const StyledText = styled(Text)` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + ${ellipsis()} margin-bottom: 0; `; diff --git a/app/scenes/DocumentPublish.tsx b/app/scenes/DocumentPublish.tsx index 593e80837..2cd38f8c5 100644 --- a/app/scenes/DocumentPublish.tsx +++ b/app/scenes/DocumentPublish.tsx @@ -12,6 +12,7 @@ import Text from "~/components/Text"; import useCollectionTrees from "~/hooks/useCollectionTrees"; import useStores from "~/hooks/useStores"; import useToasts from "~/hooks/useToasts"; +import { ellipsis } from "~/styles"; import { flattenTree } from "~/utils/tree"; type Props = { @@ -111,9 +112,7 @@ const Footer = styled(Flex)` `; const StyledText = styled(Text)` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + ${ellipsis()} margin-bottom: 0; `; diff --git a/app/styles/index.ts b/app/styles/index.ts index d2a1ec4fc..cc1513465 100644 --- a/app/styles/index.ts +++ b/app/styles/index.ts @@ -52,3 +52,14 @@ export const hideScrollbars = () => ` display: none; } `; + +/** + * Mixin to make text ellipse when it overflows. + * + * @returns string of CSS + */ +export const ellipsis = () => ` + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +`; diff --git a/shared/editor/nodes/index.ts b/shared/editor/nodes/index.ts index 8becd9d01..58b76409c 100644 --- a/shared/editor/nodes/index.ts +++ b/shared/editor/nodes/index.ts @@ -116,4 +116,4 @@ export const richExtensions: Nodes = [ /** * Add commenting and mentions to a set of nodes */ -export const withComments = (nodes: Nodes) => [...nodes, Mention, Comment]; +export const withComments = (nodes: Nodes) => [Mention, Comment, ...nodes];