diff --git a/packages/web/src/javascripts/Components/ContentListView/ContentList.tsx b/packages/web/src/javascripts/Components/ContentListView/ContentList.tsx index af032ea03..3a909466b 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ContentList.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/ContentList.tsx @@ -66,7 +66,7 @@ const ContentList: FunctionComponent = ({ return (
= ({ selectionController, }) => { const itemsViewPanelRef = useRef(null) - const displayOptionsMenuRef = useRef(null) const { + clearFilterText, completedFullSync, + createNewNote, noteFilterText, + onFilterEnter, optionsSubtitle, + paginate, panelTitle, + panelWidth, renderedItems, - setNoteFilterText, searchBarElement, selectNextItem, selectPreviousItem, - onFilterEnter, - clearFilterText, - paginate, - panelWidth, - createNewNote, + setNoteFilterText, } = itemListController const { selectedItems } = selectionController - const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false) const [focusedSearch, setFocusedSearch] = useState(false) - const [closeDisplayOptMenuOnBlur] = useCloseOnBlur(displayOptionsMenuRef, setShowDisplayOptionsMenu) - const isFilesSmartView = useMemo( () => navigationController.selected?.uuid === SystemViewId.Files, [navigationController.selected?.uuid], @@ -205,10 +198,6 @@ const ContentListView: FunctionComponent = ({ noteTagsController.reloadTagsContainerMaxWidth() }, [noteTagsController]) - const toggleDisplayOptionsMenu = useCallback(() => { - setShowDisplayOptionsMenu(!showDisplayOptionsMenu) - }, [showDisplayOptionsMenu]) - const addButtonLabel = useMemo( () => (isFilesSmartView ? 'Upload file' : 'Create a new note in the selected tag'), [isFilesSmartView], @@ -224,17 +213,14 @@ const ContentListView: FunctionComponent = ({
-
-
{panelTitle}
- -
+
= ({ noAccountWarningController={noAccountWarningController} />
-
-
-
- - -
-
Options
-
-
-
{optionsSubtitle}
-
-
- - {showDisplayOptionsMenu && ( - - )} - -
-
-
-
{completedFullSync && !renderedItems.length ?

No items.

: null} {!completedFullSync && !renderedItems.length ?

Loading...

: null} diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/ContentListHeader.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/ContentListHeader.tsx new file mode 100644 index 000000000..cc8fef8d8 --- /dev/null +++ b/packages/web/src/javascripts/Components/ContentListView/Header/ContentListHeader.tsx @@ -0,0 +1,87 @@ +import { WebApplication } from '@/Application/Application' +import { Disclosure, DisclosurePanel } from '@reach/disclosure' +import { memo, useCallback, useRef, useState } from 'react' +import Icon from '../../Icon/Icon' +import { DisplayOptionsMenuPositionProps } from './DisplayOptionsMenuProps' +import DisplayOptionsMenuPortal from './DisplayOptionsMenuPortal' +import StyledDisplayOptionsButton from './StyledDisplayOptionsButton' + +type Props = { + application: { + getPreference: WebApplication['getPreference'] + setPreference: WebApplication['setPreference'] + } + panelTitle: string + addButtonLabel: string + addNewItem: () => void + isFilesSmartView: boolean + optionsSubtitle?: string +} + +const ContentListHeader = ({ + application, + panelTitle, + addButtonLabel, + addNewItem, + isFilesSmartView, + optionsSubtitle, +}: Props) => { + const [displayOptionsMenuPosition, setDisplayOptionsMenuPosition] = useState() + const displayOptionsContainerRef = useRef(null) + const displayOptionsButtonRef = useRef(null) + + const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false) + + const toggleDisplayOptionsMenu = useCallback(() => { + if (displayOptionsButtonRef.current) { + const buttonBoundingRect = displayOptionsButtonRef.current.getBoundingClientRect() + setDisplayOptionsMenuPosition({ + top: buttonBoundingRect.bottom, + left: buttonBoundingRect.right - buttonBoundingRect.width, + }) + } + + setShowDisplayOptionsMenu((show) => !show) + }, []) + + return ( +
+
+
{panelTitle}
+ {optionsSubtitle &&
{optionsSubtitle}
} +
+
+
+ + + + + + {showDisplayOptionsMenu && displayOptionsMenuPosition && ( + + )} + + +
+ +
+
+ ) +} + +export default memo(ContentListHeader) diff --git a/packages/web/src/javascripts/Components/ContentListView/ContentListOptionsMenu.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx similarity index 88% rename from packages/web/src/javascripts/Components/ContentListView/ContentListOptionsMenu.tsx rename to packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx index b6d3311f2..2f1ff5fb3 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ContentListOptionsMenu.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx @@ -1,5 +1,4 @@ -import { WebApplication } from '@/Application/Application' -import { CollectionSort, CollectionSortProperty, PrefKey, SystemViewId } from '@standardnotes/snjs' +import { CollectionSort, CollectionSortProperty, PrefKey } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useCallback, useState } from 'react' import Icon from '@/Components/Icon/Icon' @@ -7,22 +6,13 @@ import Menu from '@/Components/Menu/Menu' import MenuItem from '@/Components/Menu/MenuItem' import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator' import { MenuItemType } from '@/Components/Menu/MenuItemType' -import { NavigationController } from '@/Controllers/Navigation/NavigationController' +import { DisplayOptionsMenuProps } from './DisplayOptionsMenuProps' -type Props = { - application: WebApplication - closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void - closeDisplayOptionsMenu: () => void - isOpen: boolean - navigationController: NavigationController -} - -const ContentListOptionsMenu: FunctionComponent = ({ +const DisplayOptionsMenu: FunctionComponent = ({ closeDisplayOptionsMenu, - closeOnBlur, application, isOpen, - navigationController, + isFilesSmartView, }) => { const [sortBy, setSortBy] = useState(() => application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt)) const [sortReverse, setSortReverse] = useState(() => application.getPreference(PrefKey.SortNotesReverse, false)) @@ -109,9 +99,9 @@ const ContentListOptionsMenu: FunctionComponent = ({ return ( = ({ type={MenuItemType.RadioButton} onClick={toggleSortByDateModified} checked={sortBy === CollectionSort.UpdatedAt} - onBlur={closeOnBlur} >
Date modified @@ -141,7 +130,6 @@ const ContentListOptionsMenu: FunctionComponent = ({ type={MenuItemType.RadioButton} onClick={toggleSortByCreationDate} checked={sortBy === CollectionSort.CreatedAt} - onBlur={closeOnBlur} >
Creation date @@ -159,7 +147,6 @@ const ContentListOptionsMenu: FunctionComponent = ({ type={MenuItemType.RadioButton} onClick={toggleSortByTitle} checked={sortBy === CollectionSort.Title} - onBlur={closeOnBlur} >
Title @@ -174,13 +161,12 @@ const ContentListOptionsMenu: FunctionComponent = ({
View
- {navigationController.selectedUuid !== SystemViewId.Files && ( + {!isFilesSmartView && (
Show note preview
@@ -190,7 +176,6 @@ const ContentListOptionsMenu: FunctionComponent = ({ className="py-1 hover:bg-contrast focus:bg-info-backdrop" checked={!hideDate} onChange={toggleHideDate} - onBlur={closeOnBlur} > Show date @@ -199,7 +184,6 @@ const ContentListOptionsMenu: FunctionComponent = ({ className="py-1 hover:bg-contrast focus:bg-info-backdrop" checked={!hideTags} onChange={toggleHideTags} - onBlur={closeOnBlur} > Show tags @@ -208,7 +192,6 @@ const ContentListOptionsMenu: FunctionComponent = ({ className="py-1 hover:bg-contrast focus:bg-info-backdrop" checked={!hideEditorIcon} onChange={toggleEditorIcon} - onBlur={closeOnBlur} > Show icon @@ -219,7 +202,6 @@ const ContentListOptionsMenu: FunctionComponent = ({ className="py-1 hover:bg-contrast focus:bg-info-backdrop" checked={!hidePinned} onChange={toggleHidePinned} - onBlur={closeOnBlur} > Show pinned @@ -228,7 +210,6 @@ const ContentListOptionsMenu: FunctionComponent = ({ className="py-1 hover:bg-contrast focus:bg-info-backdrop" checked={!hideProtected} onChange={toggleHideProtected} - onBlur={closeOnBlur} > Show protected @@ -237,7 +218,6 @@ const ContentListOptionsMenu: FunctionComponent = ({ className="py-1 hover:bg-contrast focus:bg-info-backdrop" checked={showArchived} onChange={toggleShowArchived} - onBlur={closeOnBlur} > Show archived @@ -246,7 +226,6 @@ const ContentListOptionsMenu: FunctionComponent = ({ className="py-1 hover:bg-contrast focus:bg-info-backdrop" checked={showTrashed} onChange={toggleShowTrashed} - onBlur={closeOnBlur} > Show trashed @@ -254,4 +233,4 @@ const ContentListOptionsMenu: FunctionComponent = ({ ) } -export default observer(ContentListOptionsMenu) +export default observer(DisplayOptionsMenu) diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenuPortal.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenuPortal.tsx new file mode 100644 index 000000000..0155114c6 --- /dev/null +++ b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenuPortal.tsx @@ -0,0 +1,63 @@ +import { createPortal } from 'react-dom' +import styled from 'styled-components' +import { DisplayOptionsMenuPositionProps, DisplayOptionsMenuProps } from './DisplayOptionsMenuProps' +import DisplayOptionsMenu from './DisplayOptionsMenu' +import { useRef, useEffect, RefObject } from 'react' + +type Props = DisplayOptionsMenuProps & + DisplayOptionsMenuPositionProps & { + containerRef: RefObject + } + +const PositionedOptionsMenu = styled.div` + position: absolute; + top: ${(props) => props.top}px; + left: ${(props) => props.left}px; + z-index: var(--z-index-dropdown-menu); +` + +const DisplayOptionsMenuPortal = ({ + application, + closeDisplayOptionsMenu, + containerRef, + isFilesSmartView, + isOpen, + top, + left, +}: Props) => { + const menuRef = useRef(null) + + useEffect(() => { + const closeIfClickedOutside = (event: MouseEvent) => { + const isDescendantOfMenu = menuRef.current?.contains(event.target as Node) + const isDescendantOfContainer = containerRef.current?.contains(event.target as Node) + + if (!isDescendantOfMenu && !isDescendantOfContainer) { + closeDisplayOptionsMenu() + } + } + + document.addEventListener('click', closeIfClickedOutside, { capture: true }) + return () => { + document.removeEventListener('click', closeIfClickedOutside, { + capture: true, + }) + } + }, [closeDisplayOptionsMenu, containerRef]) + + return createPortal( + +
+ +
+
, + document.body, + ) +} + +export default DisplayOptionsMenuPortal diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenuProps.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenuProps.tsx new file mode 100644 index 000000000..dc52d2f1c --- /dev/null +++ b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenuProps.tsx @@ -0,0 +1,16 @@ +import { WebApplication } from '@/Application/Application' + +export type DisplayOptionsMenuPositionProps = { + top: number + left: number +} + +export type DisplayOptionsMenuProps = { + application: { + getPreference: WebApplication['getPreference'] + setPreference: WebApplication['setPreference'] + } + closeDisplayOptionsMenu: () => void + isOpen: boolean + isFilesSmartView: boolean +} diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/StyledDisplayOptionsButton.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/StyledDisplayOptionsButton.tsx new file mode 100644 index 000000000..bdec7e085 --- /dev/null +++ b/packages/web/src/javascripts/Components/ContentListView/Header/StyledDisplayOptionsButton.tsx @@ -0,0 +1,13 @@ +import { DisclosureButton } from '@reach/disclosure' +import styled from 'styled-components' + +const StyledDisplayOptionsButton = styled(DisclosureButton).attrs(() => ({ + className: + 'flex justify-center items-center min-w-8 h-8 bg-color-padding hover:bg-contrast focus:bg-contrast color-neutral border-1 border-solid border-main rounded-full cursor-pointer', +}))<{ + pressed: boolean +}>` + background-color: ${(props) => (props.pressed ? 'var(--sn-stylekit-contrast-background-color)' : 'transparent')}; +` + +export default StyledDisplayOptionsButton diff --git a/packages/web/src/javascripts/Components/Icon/Icon.tsx b/packages/web/src/javascripts/Components/Icon/Icon.tsx index c9a0ce930..558969b7d 100644 --- a/packages/web/src/javascripts/Components/Icon/Icon.tsx +++ b/packages/web/src/javascripts/Components/Icon/Icon.tsx @@ -72,6 +72,7 @@ import { SettingsIcon, SignInIcon, SignOutIcon, + SortDescendingIcon, SpreadsheetsIcon, StarIcon, SyncIcon, @@ -121,6 +122,7 @@ export const ICONS = { 'menu-arrow-down': MenuArrowDownIcon, 'menu-arrow-right': MenuArrowRightIcon, 'menu-close': MenuCloseIcon, + 'sort-descending': SortDescendingIcon, 'pencil-filled': PencilFilledIcon, 'pencil-off': PencilOffIcon, 'pin-filled': PinFilledIcon, diff --git a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts index 677d5803e..ce449abb4 100644 --- a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts +++ b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts @@ -503,38 +503,18 @@ export class ItemListController extends AbstractViewController implements Intern return this.createNewNote() } - get optionsSubtitle(): string { - let base = '' - - if (this.displayOptions.sortBy === CollectionSort.CreatedAt) { - base += ' Date Added' - } else if (this.displayOptions.sortBy === CollectionSort.UpdatedAt) { - base += ' Date Modified' - } else if (this.displayOptions.sortBy === CollectionSort.Title) { - base += ' Title' + get optionsSubtitle(): string | undefined { + if (!this.displayOptions.includePinned && !this.displayOptions.includeProtected) { + return 'Excluding pinned and protected' } - - if (this.displayOptions.includeArchived) { - base += ' | + Archived' - } - - if (this.displayOptions.includeTrashed) { - base += ' | + Trashed' - } - if (!this.displayOptions.includePinned) { - base += ' | – Pinned' + return 'Excluding pinned' } - if (!this.displayOptions.includeProtected) { - base += ' | – Protected' + return 'Excluding protected' } - if (this.displayOptions.sortDirection === 'asc') { - base += ' | Reversed' - } - - return base + return undefined } paginate = () => { diff --git a/packages/web/src/stylesheets/_main.scss b/packages/web/src/stylesheets/_main.scss index 6135f1f66..a0eacbaa3 100644 --- a/packages/web/src/stylesheets/_main.scss +++ b/packages/web/src/stylesheets/_main.scss @@ -1,7 +1,6 @@ $z-index-editor-content: 10; $z-index-editor-title-bar: 100; -$z-index-dropdown-menu: 1002; $z-index-resizer-overlay: 1000; @@ -20,6 +19,7 @@ $z-index-modal: 10000; :root { --z-index-panel-resizer: 1001; + --z-index-dropdown-menu: 1002; } html { diff --git a/packages/web/src/stylesheets/_menus.scss b/packages/web/src/stylesheets/_menus.scss index 0ad666b14..062c4d22d 100644 --- a/packages/web/src/stylesheets/_menus.scss +++ b/packages/web/src/stylesheets/_menus.scss @@ -10,7 +10,7 @@ left: 0; float: left; min-width: 160px; - z-index: $z-index-dropdown-menu; + z-index: var(--z-index-dropdown-menu); margin-top: 5px; width: 280px; diff --git a/packages/web/src/stylesheets/_sn.scss b/packages/web/src/stylesheets/_sn.scss index b500d5300..c54810b4a 100644 --- a/packages/web/src/stylesheets/_sn.scss +++ b/packages/web/src/stylesheets/_sn.scss @@ -17,7 +17,7 @@ @extend .rounded; @extend .box-shadow; - z-index: $z-index-dropdown-menu; + z-index: var(--z-index-dropdown-menu); &.sn-dropdown--anchor-right { right: 0;