Skip to content

Commit

Permalink
Merge pull request #32 from readium/action-refinements
Browse files Browse the repository at this point in the history
Action refinements
  • Loading branch information
JayPanoz authored Dec 9, 2024
2 parents 2447e1f + eff60e7 commit 044992f
Show file tree
Hide file tree
Showing 38 changed files with 524 additions and 202 deletions.
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,15 @@
"eslint": "^8",
"eslint-config-next": "14.2.7",
"readium-css": "github:readium/readium-css#json",
"typescript": "^5"
"typescript": "^5",
"vercel": "39.1.1"
},
"overrides": {
"@babel/core": "7.25.2"
"@babel/core": "7.25.2",
"vercel": "$vercel"
},
"resolutions": {
"@babel/core": "7.25.2"
"@babel/core": "7.25.2",
"vercel": "$vercel"
}
}
6 changes: 6 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export default function Home() {
<li>
<Link href="/read?book=https%3A%2F%2Fpublication-server.readium.org%2FbW9ieS1kaWNrLmVwdWI">Moby Dick (reflow)</Link>
</li>
<li>
<Link href="/read?book=https%3A%2F%2Fpublication-server.readium.org%2FbmF0aGFuaWVsLWhhd3Rob3JuZV90aGUtaG91c2Utb2YtdGhlLXNldmVuLWdhYmxlc19hZHZhbmNlZC5lcHVi">The House of the Seven Gables (reflow advanced)</Link>
</li>
<li>
<Link href="/read?book=https%3A%2F%2Fpublication-server.readium.org%2FbGVzX2RpYWJvbGlxdWVzLmVwdWI">Les Diaboliques (reflow french)</Link>
</li>
<li>
<Link href="/read?book=https%3A%2F%2Fpublication-server.readium.org%2FQmVsbGFPcmlnaW5hbDMuZXB1Yg">Bella the Dragon (FXL)</Link>
</li>
Expand Down
30 changes: 24 additions & 6 deletions src/components/ArrowButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { Button, PressEvent, Tooltip, TooltipTrigger } from "react-aria-componen
import { useAppSelector } from "@/lib/hooks";

import classNames from "classnames";
import { isActiveElement } from "@/helpers/focus";
import { StaticBreakpoints } from "@/hooks/useBreakpoints";

export interface ReaderArrowProps {
direction: "left" | "right";
Expand All @@ -26,16 +28,18 @@ export const ArrowButton = (props: ReaderArrowProps) => {
const button = useRef<HTMLButtonElement>(null);
const isImmersive = useAppSelector(state => state.reader.isImmersive);
const isFullscreen = useAppSelector(state => state.reader.isFullscreen);
const hasReachedBreakpoint = useAppSelector(state => state.reader.hasReachedBreakpoint);
const hasReachedDynamicBreakpoint = useAppSelector(state => state.reader.hasReachedDynamicBreakpoint);
const staticBreakpoint = useAppSelector(state => state.reader.staticBreakpoint);
const isRTL = useAppSelector(state => state.publication.isRTL);
const isFXL = useAppSelector(state => state.publication.isFXL);

const [isHovering, setIsHovering] = useState(false);

const label = (props.direction === "right" && !isRTL || props.direction === "left" && isRTL) ? Locale.reader.navigation.goForward : Locale.reader.navigation.goBackward;

const handleClassNameFromState = () => {
let className = "";
if (isImmersive && !hasReachedBreakpoint || isFullscreen) {
if (isImmersive && !hasReachedDynamicBreakpoint || isFullscreen) {
className = readerStateStyles.immersiveHidden;
} else if (isImmersive) {
className = readerStateStyles.immersive;
Expand All @@ -44,22 +48,34 @@ export const ArrowButton = (props: ReaderArrowProps) => {
};

const handleClassNameFromBreakpoint = () => {
return hasReachedBreakpoint ? arrowStyles.viewportLarge : "";
if (isFXL && (staticBreakpoint === StaticBreakpoints.large || staticBreakpoint === StaticBreakpoints.xLarge)) {
return arrowStyles.viewportLarge;
} else if (!isFXL && hasReachedDynamicBreakpoint) {
return arrowStyles.viewportLarge;
} else {
return "";
}
};

useEffect(() => {
if ((props.disabled || (isImmersive && !isHovering)) && document.activeElement === button.current) {
if ((props.disabled || (isImmersive && !isHovering)) && isActiveElement(button.current)) {
button.current!.blur();
}
});

const blurOnEsc = (event: React.KeyboardEvent) => {
if (isActiveElement(button.current) && event.code === "Escape") {
button.current!.blur();
}
};

// Unlike preventFocusOnPress, this gives a visual feedback
// the button has been pressed in immersive mode (esp. when hidden)
// CSS needs to take care of hover state though, as it will be applied
// on mobile depending on the length of the press
const handleNonKeyboardFocus = (event: PressEvent) => {
if (event.pointerType !== "keyboard") {
if (document.activeElement === button.current) {
if (isActiveElement(button.current)) {
button.current!.blur()
}
}
Expand All @@ -74,8 +90,10 @@ export const ArrowButton = (props: ReaderArrowProps) => {
onPress={ props.onPressCallback }
onPressEnd={ handleNonKeyboardFocus }
onHoverChange={ (e) => setIsHovering(e) }
onKeyDown={ blurOnEsc }
className={ classNames(props.className, handleClassNameFromBreakpoint(), handleClassNameFromState()) }
isDisabled={ props.disabled }>
isDisabled={ props.disabled }
>
{ props.direction === "left" ?
<LeftArrow aria-hidden="true" focusable="false" /> :
<RightArrow aria-hidden="true" focusable="false" />
Expand Down
19 changes: 16 additions & 3 deletions src/components/FullscreenAction.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from "react";

import Locale from "../resources/locales/en.json";
import { ActionKeys, RSPrefs } from "@/preferences";
import { RSPrefs } from "@/preferences";
import readerSharedUI from "./assets/styles/readerSharedUI.module.css";

import FullscreenCorners from "./assets/icons/fullscreen.svg";
import FullscreenExit from "./assets/icons/fullscreen_exit.svg";
Expand All @@ -10,13 +11,24 @@ import { OverflowMenuItem } from "./Templates/OverflowMenuItem";
import { ActionIcon } from "./Templates/ActionIcon";

import { useFullscreen } from "@/hooks/useFullscreen";
import { ActionComponentVariant, IActionComponent } from "./Templates/ActionComponent";
import { ActionComponentVariant, ActionKeys, IActionComponent } from "./Templates/ActionComponent";

import { useAppDispatch } from "@/lib/hooks";
import { setHovering } from "@/lib/readerReducer";

export const FullscreenAction: React.FC<IActionComponent> = ({ variant }) => {
// Note: Not using React Aria ToggleButton here as fullscreen is quite
// difficult to control in isolation due to collapsibility + shortcuts

const fs = useFullscreen();
const dispatch = useAppDispatch();

const handlePress = () => {
fs.handleFullscreen();
// Has to be dispatched manually, otherwise stays true…
dispatch(setHovering(false));
// TODO: fix hover state on exit, if even possible w/o a lot of getting around…
};

if (variant && variant === ActionComponentVariant.menu) {
return(
Expand All @@ -38,12 +50,13 @@ export const FullscreenAction: React.FC<IActionComponent> = ({ variant }) => {
<>
{ document.fullscreenEnabled
? <ActionIcon
className={ readerSharedUI.iconCompSm }
visibility={ RSPrefs.actions[ActionKeys.fullscreen].visibility }
ariaLabel={ fs.isFullscreen ? Locale.reader.fullscreen.close : Locale.reader.fullscreen.trigger }
SVG={ fs.isFullscreen ? FullscreenExit : FullscreenCorners }
placement="bottom"
tooltipLabel={ Locale.reader.fullscreen.tooltip }
onPressCallback={ fs.handleFullscreen }
onPressCallback={ handlePress }
/>
: <></>
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/JumpToPositionAction.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from "react";

import Locale from "../resources/locales/en.json";
import { ActionKeys, RSPrefs } from "@/preferences";
import { RSPrefs } from "@/preferences";

import TargetIcon from "./assets/icons/point_scan.svg";

import { ActionIcon } from "./Templates/ActionIcon";
import { OverflowMenuItem } from "./Templates/OverflowMenuItem";
import { ActionComponentVariant, IActionComponent } from "./Templates/ActionComponent";
import { ActionComponentVariant, ActionKeys, IActionComponent } from "./Templates/ActionComponent";

export const JumpToPositionAction: React.FC<IActionComponent> = ({ variant }) => {
if (variant && variant === ActionComponentVariant.menu) {
Expand Down
9 changes: 5 additions & 4 deletions src/components/OverflowMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import React, { ReactNode } from "react";

import { ActionVisibility } from "@/preferences";

import Locale from "../resources/locales/en.json";
import overflowMenuStyles from "./assets/styles/overflowMenu.module.css";

import MenuIcon from "./assets/icons/more_vert.svg";

import { Key, Menu, MenuTrigger, Popover } from "react-aria-components";
import { ActionIcon } from "./Templates/ActionIcon";
import { useAppDispatch } from "@/lib/hooks";
import { useAppDispatch, useAppSelector } from "@/lib/hooks";
import { setOverflowMenuOpen, toggleImmersive } from "@/lib/readerReducer";
import { ActionVisibility } from "./Templates/ActionComponent";

export const OverflowMenu = ({ children }: { children?: ReactNode }) => {
const isImmersive = useAppSelector(state => state.reader.isImmersive);
const isHovered = useAppSelector(state => state.reader.isHovering);
const dispatch = useAppDispatch();

const toggleMenuState = (value: boolean) => {
Expand All @@ -21,7 +22,7 @@ export const OverflowMenu = ({ children }: { children?: ReactNode }) => {

return(
<>
{ React.Children.toArray(children).length > 0 ?
{ React.Children.toArray(children).length > 0 && (!isImmersive || isHovered) ?
<>
<MenuTrigger onOpenChange={ (val) => toggleMenuState(val) }>
<ActionIcon
Expand Down
77 changes: 31 additions & 46 deletions src/components/Reader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { RSPrefs, ScrollBackTo } from "@/preferences";
import { RSPrefs } from "@/preferences";
import Locale from "../resources/locales/en.json";

import "./assets/styles/reader.css";
Expand All @@ -23,16 +23,17 @@ import { useEpubNavigator } from "@/hooks/useEpubNavigator";
import { useFullscreen } from "@/hooks/useFullscreen";

import Peripherals from "@/helpers/peripherals";
import { CUSTOM_SCHEME, ScrollActions } from "@/helpers/scrollAffordance";
import { CUSTOM_SCHEME, ScrollActions, ScrollBackTo } from "@/helpers/scrollAffordance";
import { propsToCSSVars } from "@/helpers/propsToCSSVars";
import { localData } from "@/helpers/localData";
import { getPlatformModifier, metaKeys } from "@/helpers/keyboard/getMetaKeys";
import { getPlatformModifier } from "@/helpers/keyboard/getMetaKeys";

import { setImmersive, setBreakpoint, setHovering, toggleImmersive, setPlatformModifier } from "@/lib/readerReducer";
import { setImmersive, setHovering, toggleImmersive, setPlatformModifier } from "@/lib/readerReducer";
import { setFXL, setRTL, setProgression, setRunningHead } from "@/lib/publicationReducer";
import { useAppSelector, useAppDispatch } from "@/lib/hooks";

import debounce from "debounce";
import { useBreakpoints } from "@/hooks/useBreakpoints";

interface IRCSSSettings {
paginated: boolean;
Expand All @@ -55,13 +56,13 @@ export const Reader = ({ rawManifest, selfHref }: { rawManifest: object, selfHre
const isImmersive = useAppSelector(state => state.reader.isImmersive);
const isImmersiveRef = useRef(isImmersive);

const runningHead = useAppSelector(state => state.publication.runningHead);
const atPublicationStart = useAppSelector(state => state.publication.atPublicationStart);
const atPublicationEnd = useAppSelector(state => state.publication.atPublicationEnd);

const dispatch = useAppDispatch();

const fs = useFullscreen();
const staticBreakpoint = useBreakpoints();

const {
EpubNavigatorLoad,
Expand Down Expand Up @@ -272,27 +273,17 @@ export const Reader = ({ rawManifest, selfHref }: { rawManifest: object, selfHre
}
}, 250);

const mq = "(min-width:"+ RSPrefs.breakpoint + "px)";
const breakpointQuery = window.matchMedia(mq);
const handleBreakpointChange = useCallback((event: MediaQueryListEvent) => {
dispatch(setBreakpoint(event.matches))}, [dispatch]);

useEffect(() => {
dispatch(setPlatformModifier(getPlatformModifier()));

// Initial setup
dispatch(setBreakpoint(breakpointQuery.matches));
breakpointQuery.addEventListener("change", handleBreakpointChange);

window.addEventListener("resize", handleResize);
window.addEventListener("orientationchange", handleResize);

return () => {
breakpointQuery.removeEventListener("change", handleBreakpointChange);
window.removeEventListener("resize", handleResize);
window.removeEventListener("orientationchange", handleResize);
}
}, [dispatch, breakpointQuery, handleBreakpointChange, handleResize]);
}, [dispatch, handleResize]);

useEffect(() => {
const fetcher: Fetcher = new HttpFetcher(undefined, selfHref);
Expand All @@ -302,44 +293,38 @@ export const Reader = ({ rawManifest, selfHref }: { rawManifest: object, selfHre
publication.current = new Publication({
manifest: manifest,
fetcher: fetcher,
});

let positionsList: Locator[] | undefined;
});

dispatch(setRunningHead(publication.current.metadata.title.getTranslation("en")));
dispatch(setRTL(publication.current.metadata.effectiveReadingProgression === ReadingProgression.rtl));
dispatch(setFXL(publication.current.metadata.getPresentation()?.layout === EPUBLayout.fixed));

dispatch(setProgression({ currentPublication: runningHead }));
const pubTitle = publication.current.metadata.title.getTranslation("en");

dispatch(setRunningHead(pubTitle));
dispatch(setProgression({ currentPublication: pubTitle }));

let positionsList: Locator[] | undefined;


const fetchPositions = async () => {
const positionsJSON = publication.current?.manifest.links.findWithMediaType("application/vnd.readium.position-list+json");
if (positionsJSON) {
const fetcher = new HttpFetcher(undefined, selfHref);
const fetched = fetcher.get(positionsJSON);
try {
const positionObj = await fetched.readAsJSON() as {total: number, positions: Locator[]};
positionsList = positionObj.positions;
dispatch(setProgression( { totalPositions: positionObj.total }));
} catch(err) {
console.error(err)
}
}
positionsList = await publication.current?.positionsFromManifest();
if (positionsList && positionsList.length > 0) dispatch(setProgression( { totalPositions: positionsList.length }));
};

fetchPositions()
.catch(console.error);

const initialPosition = localData.get(localDataKey.current);

EpubNavigatorLoad({
container: container.current,
publication: publication.current,
listeners: listeners,
positionsList: positionsList,
initialPosition: initialPosition,
localDataKey: localDataKey.current,
}, () => p.observe(window));
.catch(console.error)
.then(() => {
const initialPosition = localData.get(localDataKey.current);

EpubNavigatorLoad({
container: container.current,
publication: publication.current!,
listeners: listeners,
positionsList: positionsList,
initialPosition: initialPosition,
localDataKey: localDataKey.current,
}, () => p.observe(window));
});

return () => {
EpubNavigatorDestroy(() => p.destroy());
Expand Down Expand Up @@ -379,7 +364,7 @@ export const Reader = ({ rawManifest, selfHref }: { rawManifest: object, selfHre
<></>
}

{ isPaged ? <ReaderFooter /> : <></>}
{ isPaged ? <ReaderFooter /> : <></> }
</main>
</>
)};
6 changes: 3 additions & 3 deletions src/components/ReaderHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,19 @@ export const ReaderHeader = ({ toc }: { toc: Links }) => {
onMouseEnter={ setHover }
onMouseLeave={ removeHover }
>
<RunningHead />
<RunningHead syncDocTitle={ true } />

<div
className={ readerHeaderStyles.actionsWrapper }
aria-label={ Locale.reader.app.header.actions }
>
{ Actions.ActionIcons }

{/*
{/*
<OverflowMenu>
{ Actions.MenuItems }
</OverflowMenu>
*/}
*/}

</div>
</header>
Expand Down
Loading

0 comments on commit 044992f

Please sign in to comment.