Skip to content

Commit

Permalink
add: mobile popover menu to seasonal reservations
Browse files Browse the repository at this point in the history
  • Loading branch information
joonatank committed Dec 18, 2024
1 parent 0d644f3 commit 43536d1
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 94 deletions.
31 changes: 15 additions & 16 deletions apps/ui/components/AccordionWithIcons.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { breakpoints } from "common";
import { toggleButtonCss } from "common/styles/buttonCss";
import { truncatedText } from "common/styles/cssFragments";
import { Flex } from "common/styles/util";
import { Button, IconAngleDown, IconAngleUp, useAccordion } from "hds-react";
import { IconAngleDown, IconAngleUp, useAccordion } from "hds-react";
import { useTranslation } from "next-i18next";
import styled from "styled-components";

Expand Down Expand Up @@ -80,9 +82,7 @@ const IconLabel = styled(Flex).attrs({
min-width: 0;
max-width: 100%;
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
${truncatedText}
}
span:last-child {
flex-shrink: 0;
Expand All @@ -97,6 +97,10 @@ const Content = styled.div<{ $open: boolean }>`
display: ${({ $open }) => ($open ? "block" : "none")};
`;

const ToggleButton = styled.button`
${toggleButtonCss}
`;

/// Stylistically different from regular Accordion
/// Regular accordion uses the card title as a button to open/close the card
/// and has no options for other content inside the accordion.
Expand Down Expand Up @@ -136,23 +140,18 @@ export function AccordionWithIcons({
))}
</IconListWrapper>
<ButtonListWrapper>
<Button
key="toggle"
<ToggleButton
onClick={handleToggle}
variant="supplementary"
theme="black"
// we are hiding the text on mobile
aria-label={isOpen ? t("common:close") : t("common:show")}
iconRight={
isOpen ? (
<IconAngleUp aria-hidden />
) : (
<IconAngleDown aria-hidden />
)
}
>
{isOpen ? (
<IconAngleUp aria-hidden />
) : (
<IconAngleDown aria-hidden />
)}
{isOpen ? t("common:close") : t("common:show")}
</Button>
</ToggleButton>
</ButtonListWrapper>
</ClosedAccordionWrapper>
<Content $open={isOpen}>{children}</Content>
Expand Down
59 changes: 48 additions & 11 deletions apps/ui/components/application/ApprovedReservations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
ReservationCancellableReason,
} from "@/modules/reservation";
import { formatDateTimeStrings } from "@/modules/util";
import { PopupMenu } from "common/src/components/PopupMenu";

const N_RESERVATIONS_TO_SHOW = 20;

Expand All @@ -76,6 +77,10 @@ export const BREAKPOINT = breakpoints.m;

// Tables can't do horizontal scroll without wrapping the table in a div
// NOTE HDS Table can't be styled so have to wrap it in an extra div.
// NOTE hide-on-desktop and hide-on-mobile function differently
// - hide-on-desktop hides only the element
// - hide-on-mobile hides the whole cell
// they are needed for different use cases (e.g. on mobile empty cells create extra gaps)
const TableWrapper = styled.div`
/* TODO move this to a more general TableWrapper shared with admin-ui */
/* Mobile uses cards, so no horizontal scroll */
Expand All @@ -92,6 +97,9 @@ const TableWrapper = styled.div`
min-width: 100%;
}
}
.hide-on-desktop {
display: none;
}
}
@media (max-width: ${BREAKPOINT}) {
Expand Down Expand Up @@ -539,16 +547,45 @@ function ReservationsTable({
key: "date",
headerName: t("common:dateLabel"),
isSortable: false,
transform: ({ date, dayOfWeek }: ReservationsTableElem) => (
<span aria-label={t("common:dateLabel")}>
<span>{toUIDate(date)}</span>
<OnlyForMobile>
{/* span removes whitespace */}
<pre style={{ display: "inline" }}>{" - "}</pre>
<span>{dayOfWeek}</span>
</OnlyForMobile>
</span>
),
transform: ({
pk,
date,
dayOfWeek,
isCancellableReason,
}: ReservationsTableElem) => {
const isDisabled = isCancellableReason !== "";
const items = [
{
name: t("common:cancel"),
onClick: () => handleCancel(pk),
disabled: isDisabled,
},
] as const;

return (
<Flex
$direction="row"
$gap="2-xs"
$justifyContent="space-between"
$width="full"
>
<span aria-label={t("common:dateLabel")}>
<span>{toUIDate(date)}</span>
<OnlyForMobile>
{/* span removes whitespace */}
<pre style={{ display: "inline" }}>{" - "}</pre>
<span>{dayOfWeek}</span>
</OnlyForMobile>
</span>
{!isDisabled ? (
<PopupMenu
items={items}
className="popover-menu-toggle hide-on-desktop"
/>
) : null}
</Flex>
);
},
},
{
key: "dayOfWeek",
Expand Down Expand Up @@ -619,7 +656,7 @@ function ReservationsTable({
? t("common:cancel")
: t(`reservations:modifyTimeReasons.${isCancellableReason}`)
}
// TODO on mobile this should be hidden behind a popover (for now it's hidden)
// Corresponding mobile menu is on the first row
className="hide-on-mobile"
>
{t("common:cancel")}
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/components/calendar/Legend.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from "react";
import { useTranslation } from "next-i18next";
import styled, { css, FlattenSimpleInterpolation } from "styled-components";
import { truncatedText } from "../../styles/util";
import { Flex } from "common/styles/util";
import { truncatedText } from "common/styles/cssFragments";

type LegendItemT = {
title: string;
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/components/common/StartApplicationBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import styled from "styled-components";
import { breakpoints } from "common/src/common/style";
import ClientOnly from "common/src/ClientOnly";
import { JustForDesktop, JustForMobile } from "@/modules/style/layout";
import { truncatedText } from "@/styles/util";
import { useRouter } from "next/router";
import {
ApplicationCreateMutationInput,
Expand All @@ -14,6 +13,7 @@ import {
import { errorToast } from "common/src/common/toast";
import { getApplicationPath } from "@/modules/urls";
import { Flex, NoWrap } from "common/styles/util";
import { truncatedText } from "common/styles/cssFragments";

type Props = {
count: number;
Expand Down
6 changes: 0 additions & 6 deletions apps/ui/styles/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@ import { Button } from "hds-react";
import Link from "next/link";
import styled, { css } from "styled-components";

export const truncatedText = css`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;

export const pixel =
"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";

Expand Down
120 changes: 61 additions & 59 deletions packages/common/src/components/PopupMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,87 +1,87 @@
import { Button, IconMenuDots } from "hds-react";
import { IconMenuDots } from "hds-react";
import React, { useEffect, useRef, useState } from "react";
import styled from "styled-components";
import FocusTrap from "focus-trap-react";
import ReactDOM from "react-dom";
import { Flex } from "../../styles/util";

interface PopupMenuProps {
items: {
name: string;
disabled?: boolean;
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}[];
}

const RowButton = styled(Button)`
color: var(--color-black);
padding: 0;
border-radius: 0;
span {
padding: 0;
}
`;

const MenuIcon = styled(IconMenuDots)`
margin-left: auto;
`;
import { toggleButtonCss } from "../../styles/buttonCss";
import { useTranslation } from "next-i18next";

const Container = styled.div`
position: relative;
`;

const Popup = styled(Flex).attrs({ $gap: "none" })`
display: flex;
flex-direction: column;
background-color: white;
padding: 0;
position: absolute;
right: 0;
z-index: var(--tilavaraus-stack-popup-menu);
border: 1px solid var(--color-black-50);
:not(:has(> button:disabled)) {
border: 1px solid var(--color-black);
}
`;

top: 36;
right: 0;
position: absolute;
button {
text-align: left;
padding: var(--spacing-xs);
white-space: nowrap;
width: 100%;
border: none;
background-color: transparent;
:focus {
background-color: var(--color-bus);
outline: none;
color: var(--color-white);
}
:disabled {
color: var(--color-black-50);
}
const ListButton = styled.button`
text-align: left;
padding: var(--spacing-xs);
white-space: nowrap;
width: 100%;
border: none;
background-color: transparent;
:hover {
background-color: var(--color-black-10);
cursor: pointer;
}
:focus {
background-color: var(--color-bus);
outline: none;
color: var(--color-white);
}
:disabled {
color: var(--color-black-50);
}
`;

/* required to allow clicking the button to close it */
const Overlay = styled.div`
position: absolute;
width: 100%;
height: 100%;
top: 0;
z-index: 500;
left: 0;
background-color: transparent;
z-index: var(--tilavaraus-stack-popup-menu-overlay);
inset: 0;
`;

const ToggleButton = styled.button`
${toggleButtonCss}
`;

interface Item {
name: string;
disabled?: boolean;
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}

interface PopupMenuProps {
items: Readonly<Item[]>;
style?: React.CSSProperties;
className?: string;
}

// TODO now this is relative to the button, but that causes few other issues
// - the popup is forced to open on the left side so using it on the left of a page would cause an overflow
// - the popup will expand containers the buttons are inside of (like <table>, not the cell)
// These seem to be ok for this use case, but for others would need some more work.
export function PopupMenu({ items }: PopupMenuProps): JSX.Element {
export function PopupMenu({
items,
style,
className,
}: PopupMenuProps): JSX.Element {
const buttonRef = useRef<HTMLDivElement>(null);
const firstMenuItemRef = useRef<HTMLButtonElement>(null);

const { t } = useTranslation();

useEffect(() => {
if (firstMenuItemRef.current) {
firstMenuItemRef.current.focus();
Expand All @@ -100,18 +100,21 @@ export function PopupMenu({ items }: PopupMenuProps): JSX.Element {
document.removeEventListener("click", closePopup);
};

const disabled = items.length === 0 || items.every((i) => i.disabled);

return (
<Container ref={buttonRef}>
<RowButton
<Container ref={buttonRef} style={style} className={className}>
<ToggleButton
onClick={(e) => {
e.stopPropagation();
openPopup();
}}
iconLeft={<MenuIcon />}
variant="supplementary"
disabled={disabled}
type="button"
aria-label={isOpen ? t("common:close") : t("common:show")}
>
{" "}
</RowButton>
<IconMenuDots />
</ToggleButton>
{isOpen && buttonRef.current
? ReactDOM.createPortal(
<PopupContent
Expand Down Expand Up @@ -154,8 +157,7 @@ function PopupContent({
>
<Popup>
{items.map((i, index) => (
// TODO should use HDS button
<button
<ListButton
key={i.name}
ref={index === 0 ? firstMenuItemRef : undefined}
type="button"
Expand All @@ -172,7 +174,7 @@ function PopupContent({
}}
>
{i.name}
</button>
</ListButton>
))}
</Popup>
</FocusTrap>
Expand Down
Loading

0 comments on commit 43536d1

Please sign in to comment.