diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4022a446f..19264d17d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,7 +13,7 @@ "@babel/preset-typescript": "^7.22.15", "@emotion/cache": "^11.11.0", "@emotion/react": "^11.11.1", - "@opencast/appkit": "^0.1.4", + "@opencast/appkit": "^0.2.4", "@svgr/webpack": "^8.1.0", "babel-loader": "^9.1.3", "babel-plugin-relay": "^15.0.0", @@ -2151,9 +2151,9 @@ } }, "node_modules/@opencast/appkit": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@opencast/appkit/-/appkit-0.1.4.tgz", - "integrity": "sha512-c+4N7VQPE6eWX6+lBUFQH1YWKPniBp0Lg4zgyw1TEKnNXOFDjeRsScxULomq79JabWrtbVhrD5TVN+H+3MXbhQ==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@opencast/appkit/-/appkit-0.2.4.tgz", + "integrity": "sha512-EHByrN6o8elbQIOI4XB1vqxA5IxStLGgoX8nfOQ3EXWHbx0n/ZNeZ/DrOTTmxgTNfRxvjZR7sbjIlbSIyL5SKw==", "peerDependencies": { "@emotion/react": "^11.11.1", "@floating-ui/react": "^0.24.3", diff --git a/frontend/package.json b/frontend/package.json index 91ffd18ed..17a0fd9cb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,7 +29,7 @@ "@babel/preset-typescript": "^7.22.15", "@emotion/cache": "^11.11.0", "@emotion/react": "^11.11.1", - "@opencast/appkit": "^0.1.4", + "@opencast/appkit": "^0.2.4", "@svgr/webpack": "^8.1.0", "babel-loader": "^9.1.3", "babel-plugin-relay": "^15.0.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ffa75ce7c..0b8e0f1df 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -22,7 +22,7 @@ type Props = { export const App: React.FC = ({ initialRoute }) => ( - + = ({ index, realm }) => { const { t } = useTranslation(); - const isDark = useColorScheme().scheme === "dark"; - const floatingRef = useRef(null); - const { id: realmId } = useFragment(graphql` - fragment AddButtonsRealmData on Realm { - id - } - `, realm); - - const env = useRelayEnvironment(); - - const addBlock = ( - type: string, - prepareBlock?: (store: RecordSourceProxy, block: RecordProxy) => void, - ) => { - commitLocalUpdate(env, store => { - const realm = store.get(realmId) ?? bug("could not find realm"); - - const blocks = [ - ...realm.getLinkedRecords("blocks") ?? bug("realm does not have blocks"), - ]; - - const id = "clNEWBLOCK"; - const block = store.create(id, `${type}Block`); - prepareBlock?.(store, block); - block.setValue(true, "editMode"); - block.setValue(id, "id"); - - blocks.splice(index, 0, block); - - realm.setLinkedRecords(blocks, "blocks"); - }); - }; - const BUTTON_SIZE = 36; return ( @@ -99,66 +66,101 @@ export const AddButtons: React.FC = ({ index, realm }) => { - - -
{t("manage.realm.content.add-popup-title")}
-
    li:not(:last-child)": { - borderBottom: `1px solid ${isDark ? COLORS.neutral25 : COLORS.neutral15}`, - }, - }}> - floatingRef.current?.close()} - Icon={LuHash} - label={t("manage.realm.content.add-title")} - onClick={() => addBlock("Title")} - /> - floatingRef.current?.close()} - Icon={LuType} - label={t("manage.realm.content.add-text")} - onClick={() => addBlock("Text")} - /> - floatingRef.current?.close()} - Icon={LuGrid} - label={t("manage.realm.content.add-series")} - onClick={() => addBlock("Series", (_store, block) => { - block.setValue("NEW_TO_OLD", "order"); - block.setValue(true, "showTitle"); - block.setValue(false, "showMetadata"); - })} - /> - floatingRef.current?.close()} - Icon={LuFilm} - label={t("manage.realm.content.add-video")} - onClick={() => addBlock("Video", (_store, block) => { - block.setValue(true, "showTitle"); - block.setValue(true, "showLink"); - })} - /> -
-
+ ); }; +const AddButtonsMenu: React.FC}> = ({ + index, realm, floatingRef, +}) => { + const isDark = useColorScheme().scheme === "dark"; + const { t } = useTranslation(); + const itemProps = useFloatingItemProps(); + const menuId = useId(); + + const { id: realmId } = useFragment(graphql` + fragment AddButtonsRealmData on Realm { + id + } + `, realm); + + const env = useRelayEnvironment(); + + const addBlock = ( + type: string, + prepareBlock?: (store: RecordSourceProxy, block: RecordProxy) => void, + ) => { + commitLocalUpdate(env, store => { + const realm = store.get(realmId) ?? bug("could not find realm"); + + const blocks = [ + ...realm.getLinkedRecords("blocks") ?? bug("realm does not have blocks"), + ]; + + const id = "clNEWBLOCK"; + const block = store.create(id, `${type}Block`); + prepareBlock?.(store, block); + block.setValue(true, "editMode"); + block.setValue(id, "id"); + + blocks.splice(index, 0, block); + + realm.setLinkedRecords(blocks, "blocks"); + }); + }; + + type Block = "title" | "text" | "series" | "video"; + const buttonProps: [IconType, Block, () => void][] = [ + [LuHash, "title", () => addBlock("Title")], + [LuType, "text", () => addBlock("Text")], + [LuLayoutGrid, "series", () => addBlock("Series", (_store, block) => { + block.setValue("NEW_TO_OLD", "order"); + block.setValue(true, "showTitle"); + block.setValue(false, "showMetadata"); + })], + [LuFilm, "video", () => addBlock("Video", (_store, block) => { + block.setValue(true, "showTitle"); + block.setValue(true, "showLink"); + })], + ]; + + return ( + +
{t("manage.realm.content.add-popup-title")}
+
    li:not(:last-child)": { + borderBottom: `1px solid ${isDark ? COLORS.neutral25 : COLORS.neutral15}`, + }, + }}> + {buttonProps.map(([icon, type, onClick], index) => floatingRef.current?.close()} + Icon={icon} + label={t(`manage.realm.content.add-${type}`)} + {...itemProps(index)} + {...{ onClick }} + />)} +
+
+ ); +}; + type AddItemProps = { label: string; Icon: IconType; @@ -166,7 +168,9 @@ type AddItemProps = { close: () => void; }; -const AddItem: React.FC = ({ label, Icon, onClick, close }) => ( +const AddItem = React.forwardRef(({ + label, Icon, onClick, close, +}, ref) => (
  • button": { borderBottomLeftRadius: 8, @@ -174,6 +178,7 @@ const AddItem: React.FC = ({ label, Icon, onClick, close }) => ( }, }}> { onClick(); close(); @@ -196,4 +201,4 @@ const AddItem: React.FC = ({ label, Icon, onClick, close }) => ( {label}
  • -); +)); diff --git a/frontend/src/ui/Blocks/Series.tsx b/frontend/src/ui/Blocks/Series.tsx index fd39bbd5f..221dd89ad 100644 --- a/frontend/src/ui/Blocks/Series.tsx +++ b/frontend/src/ui/Blocks/Series.tsx @@ -3,6 +3,7 @@ import React, { createContext, useContext, useEffect, + useId, useRef, useState, } from "react"; @@ -10,7 +11,7 @@ import { useTranslation } from "react-i18next"; import { graphql, useFragment } from "react-relay"; import { match, unreachable, ProtoButton, screenWidthAtMost, screenWidthAbove, - useColorScheme, Floating, FloatingHandle, + useColorScheme, Floating, FloatingHandle, useFloatingItemProps, } from "@opencast/appkit"; import { keyOfId, isSynced, SyncedOpencastEntity } from "../../util"; @@ -26,7 +27,9 @@ import { import { isPastLiveEvent, isUpcomingLiveEvent, Thumbnail } from "../Video"; import { RelativeDate } from "../time"; import { Card } from "../Card"; -import { LuColumns, LuGrid, LuList, LuChevronLeft, LuChevronRight, LuPlay } from "react-icons/lu"; +import { + LuColumns, LuList, LuChevronLeft, LuChevronRight, LuPlay, LuLayoutGrid, +} from "react-icons/lu"; import { keyframes } from "@emotion/react"; import { CollapsibleDescription, SmallDescription } from "../metadata"; import { darkModeBoxShadow, ellipsisOverflowCss, focusStyle } from ".."; @@ -331,7 +334,7 @@ const OrderMenu: React.FC = () => { }); return {triggerContent}} list={ ref.current?.close()} />} @@ -345,7 +348,7 @@ const ViewMenu: React.FC = () => { const icon = match(state.viewState, { slider: () => , - gallery: () => , + gallery: () => , list: () => , }); @@ -358,9 +361,8 @@ const ViewMenu: React.FC = () => { ); return ref.current?.close()} />} />; }; @@ -375,6 +377,8 @@ const List: React.FC = ({ type, close }) => { const isDark = useColorScheme().scheme === "dark"; const { viewState, setViewState } = useContext(ViewContext); const { eventOrder, setEventOrder } = useContext(OrderContext); + const itemProps = useFloatingItemProps(); + const itemId = useId(); const listStyle = { minWidth: 125, @@ -397,60 +401,49 @@ const List: React.FC = ({ type, close }) => { } }; + const viewItems: [View, IconType][] = [ + ["slider", LuColumns], + ["gallery", LuLayoutGrid], + ["list", LuList], + ]; + + type OrderTranslationKey = "new-to-old" | "old-to-new" | "a-z" | "z-a"; + const orderItems: [ExtendedVideoListOrder, OrderTranslationKey][] = [ + ["NEW_TO_OLD", "new-to-old"], + ["OLD_TO_NEW", "old-to-new"], + ["A-Z", "a-z"], + ["Z-A", "z-a"], + ]; + + const sharedProps = (key: View | OrderTranslationKey) => ({ + close: close, + label: t(`series.settings.${key}`), + }); + const list = match(type, { view: () => <>
    {t("series.settings.view")}
      - setViewState("slider")} - close={close} - Icon={LuColumns} - label={t("series.settings.slider")} - /> - setViewState("gallery")} - close={close} - Icon={LuGrid} - label={t("series.settings.gallery")} - /> - setViewState("list")} - close={close} - Icon={LuList} - label={t("series.settings.list")} - /> + {viewItems.map(([view, icon], index) => setViewState(view)} + />)}
    , order: () => <>
    {t("series.settings.order")}
      - setEventOrder("NEW_TO_OLD")} - close={close} - label={t("series.settings.new-to-old")} - /> - setEventOrder("OLD_TO_NEW")} - close={close} - label={t("series.settings.old-to-new")} - /> - setEventOrder("A-Z")} - close={close} - label={t("series.settings.a-z")} - /> - setEventOrder("Z-A")} - close={close} - label={t("series.settings.z-a")} - /> + {orderItems.map(([order, orderKey], index) => setEventOrder(order)} + />)}
    , }); @@ -469,13 +462,14 @@ const List: React.FC = ({ type, close }) => { type MenuItemProps = { Icon?: IconType; label: string; - onClick: () => void; + onClick?: () => void; close: () => void; - disabled?: boolean; + disabled: boolean; }; -const MenuItem: React.FC = ({ Icon, label, onClick, close, disabled }) => { - const ref = useRef(null); +const MenuItem = React.forwardRef(({ + Icon, label, onClick, close, disabled, +}, ref) => { const isDark = useColorScheme().scheme === "dark"; return ( @@ -488,11 +482,12 @@ const MenuItem: React.FC = ({ Icon, label, onClick, close, disabl }, }}> { - onClick(); + if (onClick) { + onClick(); + } close(); }} css={{ @@ -519,7 +514,7 @@ const MenuItem: React.FC = ({ Icon, label, onClick, close, disabl ); -}; +}); // ============================================================================================== diff --git a/frontend/src/ui/FloatingBaseMenu.tsx b/frontend/src/ui/FloatingBaseMenu.tsx index ed08ec3b6..56c10619f 100644 --- a/frontend/src/ui/FloatingBaseMenu.tsx +++ b/frontend/src/ui/FloatingBaseMenu.tsx @@ -13,8 +13,6 @@ type FloatingBaseMenuProps = { triggerStyles?: CSSObject; }; - -// TODO: Make menus work with arrow keys. export const FloatingBaseMenu = React.forwardRef( ({ triggerContent, list, label, triggerStyles }, ref) => (