From 853e0dae69ab9740cf71912667df6024af38b77b Mon Sep 17 00:00:00 2001 From: Egor Gorbachev <7gorbachevm@gmail.com> Date: Mon, 5 Feb 2024 14:48:52 +0700 Subject: [PATCH] iOS big card allow scrollable (#11) --- src/lib/mobx-form/boolean-toggle.ts | 4 ++ src/lib/react/use-is-overflowing.ts | 18 ++++++++ src/screens/app.tsx | 6 +++ .../component-catalog/card-preview-story.tsx | 45 +++++++++++++++++++ .../component-catalog-page.tsx | 38 ++++++++++++++++ src/screens/component-catalog/components.tsx | 42 +++++++++++++++++ src/screens/deck-form/card-list.tsx | 2 +- src/screens/deck-form/card-preview.tsx | 9 +++- .../deck-review/card-review-with-controls.tsx | 2 +- .../deck-review/deck-finished-modal.tsx | 4 +- src/screens/deck-review/deck-finished.tsx | 2 +- src/screens/deck-review/review.tsx | 7 ++- .../deck-review/store/card-preview-store.ts | 4 ++ .../store/card-under-review-store.ts | 4 ++ src/screens/shared/card/card.tsx | 12 ++++- src/store/screen-store.ts | 1 + src/translations/t.ts | 8 ++-- src/ui/wysiwyg-field/wysiwig-field.tsx | 4 +- 18 files changed, 195 insertions(+), 17 deletions(-) create mode 100644 src/lib/react/use-is-overflowing.ts create mode 100644 src/screens/component-catalog/card-preview-story.tsx create mode 100644 src/screens/component-catalog/component-catalog-page.tsx create mode 100644 src/screens/component-catalog/components.tsx diff --git a/src/lib/mobx-form/boolean-toggle.ts b/src/lib/mobx-form/boolean-toggle.ts index 5a3e8e82..e5a28de6 100644 --- a/src/lib/mobx-form/boolean-toggle.ts +++ b/src/lib/mobx-form/boolean-toggle.ts @@ -17,4 +17,8 @@ export class BooleanToggle implements FieldWithValue { setFalse() { this.value = false; } + + setValue(value: boolean) { + this.value = value; + } } diff --git a/src/lib/react/use-is-overflowing.ts b/src/lib/react/use-is-overflowing.ts new file mode 100644 index 00000000..7e5ab4a0 --- /dev/null +++ b/src/lib/react/use-is-overflowing.ts @@ -0,0 +1,18 @@ +import { useEffect, useRef } from "react"; + +export const useIsOverflowing = ( + key: boolean, + setIsOverflowing: (is: boolean) => void, +) => { + const ref = useRef(null); + + useEffect(() => { + const current = ref.current; + if (current) { + const isContentOverflowing = current.scrollHeight > current.clientHeight; + setIsOverflowing(isContentOverflowing); + } + }, [setIsOverflowing, ref, key]); + + return { setIsOverflowing, ref }; +}; diff --git a/src/screens/app.tsx b/src/screens/app.tsx index e64f2471..34f567d6 100644 --- a/src/screens/app.tsx +++ b/src/screens/app.tsx @@ -26,6 +26,7 @@ import { ShareDeckOrFormStoreProvider } from "./share-deck/store/share-deck-stor import { FolderFormStoreProvider } from "./folder-form/store/folder-form-store-context.tsx"; import { FolderScreen } from "./folder-review/folder-screen.tsx"; import { useSettingsButton } from "../lib/telegram/use-settings-button.ts"; +import { ComponentCatalogPage } from "./component-catalog/component-catalog-page.tsx"; export const App = observer(() => { useRestoreFullScreenExpand(); @@ -119,6 +120,11 @@ export const App = observer(() => { )} + {screenStore.screen.type === "componentCatalog" && ( + + + + )} ); }); diff --git a/src/screens/component-catalog/card-preview-story.tsx b/src/screens/component-catalog/card-preview-story.tsx new file mode 100644 index 00000000..5063726d --- /dev/null +++ b/src/screens/component-catalog/card-preview-story.tsx @@ -0,0 +1,45 @@ +import { CardPreview } from "../deck-form/card-preview.tsx"; +import { useState } from "react"; +import { CardFormStoreInterface } from "../deck-form/store/card-form-store-interface.ts"; +import { TextField } from "../../lib/mobx-form/text-field.ts"; +import { CardAnswerType } from "../../../functions/db/custom-types.ts"; +import { BooleanToggle } from "../../lib/mobx-form/boolean-toggle.ts"; +import { CardAnswerFormType } from "../deck-form/store/deck-form-store.ts"; +import { ListField } from "../../lib/mobx-form/list-field.ts"; + +const createCardPreviewForm = (card: { + front: string; + back: string; + example?: string; +}): CardFormStoreInterface => { + return { + cardForm: { + front: new TextField(card.front), + back: new TextField(card.back), + example: new TextField(card.example ?? ""), + answerType: new TextField("remember"), + answerFormType: "new", + answers: new ListField([]), + answerId: "0", + }, + form: undefined, + isCardPreviewSelected: new BooleanToggle(false), + isSaveCardButtonActive: false, + onBackCard: () => {}, + onSaveCard: () => {}, + isSending: false, + markCardAsRemoved: () => {}, + }; +}; + +export const CardPreviewStory = (props: { + card: { + front: string; + back: string; + example?: string; + }; +}) => { + const [form] = useState(createCardPreviewForm(props.card)); + + return {}} />; +}; diff --git a/src/screens/component-catalog/component-catalog-page.tsx b/src/screens/component-catalog/component-catalog-page.tsx new file mode 100644 index 00000000..a334d4a9 --- /dev/null +++ b/src/screens/component-catalog/component-catalog-page.tsx @@ -0,0 +1,38 @@ +import { Screen } from "../shared/screen.tsx"; +import { useState } from "react"; +import { useBackButton } from "../../lib/telegram/use-back-button.tsx"; +import { css } from "@emotion/css"; +import { theme } from "../../ui/theme.tsx"; +import { Component, components } from "./components.tsx"; + +export const ComponentCatalogPage = () => { + const [selectedComponent, setSelectedComponent] = useState( + null, + ); + useBackButton(() => { + setSelectedComponent(null); + }); + + if (selectedComponent) { + return selectedComponent.component; + } + + return ( + +
    + {components.map((component) => ( +
  • setSelectedComponent(component)} + className={css({ + cursor: "pointer", + color: theme.linkColor, + })} + > + {component.name} +
  • + ))} +
+
+ ); +}; diff --git a/src/screens/component-catalog/components.tsx b/src/screens/component-catalog/components.tsx new file mode 100644 index 00000000..adb4b87b --- /dev/null +++ b/src/screens/component-catalog/components.tsx @@ -0,0 +1,42 @@ +import { Button } from "../../ui/button.tsx"; +import { ReactNode } from "react"; +import { CardPreviewStory } from "./card-preview-story.tsx"; + +export type Component = { name: string; component: ReactNode }; + +export const components: Array = [ + { + name: "Button", + component: , + }, + + { + name: "Button - outline", + component: , + }, + { + name: "Card preview - normal", + component: ( + + ), + }, + { + name: "Card preview - big text", + component: ( +
  • FAS-Risiko
  • viele Eltern meiden den Kontakt zum Hilfesystem
  • keine verlässliche Bezugsperson/häufiger Wechsel
  • Erleben von Armut/Arbeitslosigkeit/beengten Wohnverhältnissen
  • desolater elterlicher Gesundheitszustand/Notfälle/Sorge um die Eltern
  • Gefährdung durch elterliche Intoxikation (direkte Gewalterfahrung oder Zeugenschaft)
  • Familienklima: Familiengeheimnisse, Tabuisierungen, Loyalitätskon-flikte
  • kindliche Verhaltensauffälligkeiten (Hyperaktivität, Angste, Rückzug, extrem schüchtern)
  • typische Rollen und Familienregeln (Umkreisen und Verschweigen des Alkoholthemas)
  • Familienstruktur: aufgeweichte Generationsgrenzen, altersunangemes-sene Aufgaben,
  • erhöhte Gefahr emotionalen/sexuellen Mißbrauchs
  • grundlegende Stressoren:

    • elterliche Unzuverlässigkeit
    • Bindungsstörungen: unsicher, ambivalent
    • Duldungs- und Katastrophenstress
    • Krisenstress (bei Unfähigkeit zur Problembewältigung)
    • überfordernde Aufgaben und Rollen

    Quelle: GVS (2012, 2014)

    `, + }} + /> + ), + }, +]; diff --git a/src/screens/deck-form/card-list.tsx b/src/screens/deck-form/card-list.tsx index 3f8a31cc..479d9980 100644 --- a/src/screens/deck-form/card-list.tsx +++ b/src/screens/deck-form/card-list.tsx @@ -96,7 +96,7 @@ export const CardList = observer(() => { padding: 12, // If the card content is too big then hide it maxHeight: 120, - overflow: 'hidden', + overflow: "hidden", ...tapScale, })} > diff --git a/src/screens/deck-form/card-preview.tsx b/src/screens/deck-form/card-preview.tsx index 075c707c..aa19744f 100644 --- a/src/screens/deck-form/card-preview.tsx +++ b/src/screens/deck-form/card-preview.tsx @@ -2,9 +2,10 @@ import { observer } from "mobx-react-lite"; import { useBackButton } from "../../lib/telegram/use-back-button.tsx"; import { css } from "@emotion/css"; import { CardReviewWithControls } from "../deck-review/card-review-with-controls.tsx"; -import { useState } from "react"; +import React, { useState } from "react"; import { CardPreviewStore } from "../deck-review/store/card-preview-store.ts"; import { CardFormStoreInterface } from "./store/card-form-store-interface.ts"; +import { createPortal } from "react-dom"; type Props = { form: CardFormStoreInterface; @@ -19,7 +20,7 @@ export const CardPreview = observer((props: Props) => { onBack(); }); - return ( + const component = (
    { />
    ); + + return cardPreviewStore.isOverflowing.value + ? createPortal(component, document.body) + : component; }); diff --git a/src/screens/deck-review/card-review-with-controls.tsx b/src/screens/deck-review/card-review-with-controls.tsx index a7354662..51d512d5 100644 --- a/src/screens/deck-review/card-review-with-controls.tsx +++ b/src/screens/deck-review/card-review-with-controls.tsx @@ -36,7 +36,7 @@ export const CardReviewWithControls = observer((props: Props) => { {card && card.answerType === "remember" && (
    { const { children } = props; - const marginTop = props.marginTop || "200px"; const modal = { hidden: { @@ -19,7 +17,7 @@ export const DeckFinishedModal = (props: Props) => { opacity: 0, }, visible: { - y: marginTop, + y: "32px", opacity: 1, transition: { delay: 0.2 }, }, diff --git a/src/screens/deck-review/deck-finished.tsx b/src/screens/deck-review/deck-finished.tsx index cbe672fb..62d1707f 100644 --- a/src/screens/deck-review/deck-finished.tsx +++ b/src/screens/deck-review/deck-finished.tsx @@ -32,7 +32,7 @@ export const DeckFinished = observer((props: Props) => { useTelegramProgress(() => reviewStore.isReviewSending); return ( - +
    { const reviewStore = useReviewStore(); @@ -53,7 +54,7 @@ export const Review = observer(() => { } }); - return ( + const component = (
    { />
    ); + + return reviewStore.currentCard?.isOverflowing.value + ? createPortal(component, document.body) + : component; }); diff --git a/src/screens/deck-review/store/card-preview-store.ts b/src/screens/deck-review/store/card-preview-store.ts index 7b4dcf48..e8a0de12 100644 --- a/src/screens/deck-review/store/card-preview-store.ts +++ b/src/screens/deck-review/store/card-preview-store.ts @@ -11,6 +11,7 @@ import { speak, SpeakLanguageEnum } from "../../../lib/voice-playback/speak.ts"; import { removeAllTags } from "../../../lib/sanitize-html/remove-all-tags.ts"; import { CardFormStoreInterface } from "../../deck-form/store/card-form-store-interface.ts"; import { assert } from "../../../lib/typescript/assert.ts"; +import { BooleanToggle } from "../../../lib/mobx-form/boolean-toggle.ts"; export class CardPreviewStore implements LimitedCardUnderReviewStore { id: number; @@ -26,6 +27,9 @@ export class CardPreviewStore implements LimitedCardUnderReviewStore { isOpened = false; + // A hack for iOS when the card content is too large + isOverflowing = new BooleanToggle(false); + constructor(cardFormStore: CardFormStoreInterface) { makeAutoObservable(this, {}, { autoBind: true }); const form = cardFormStore.cardForm; diff --git a/src/screens/deck-review/store/card-under-review-store.ts b/src/screens/deck-review/store/card-under-review-store.ts index 41a38a97..ec243ae7 100644 --- a/src/screens/deck-review/store/card-under-review-store.ts +++ b/src/screens/deck-review/store/card-under-review-store.ts @@ -11,6 +11,7 @@ import { CardAnswerType } from "../../../../functions/db/custom-types.ts"; import { assert } from "../../../lib/typescript/assert.ts"; import { removeAllTags } from "../../../lib/sanitize-html/remove-all-tags.ts"; import { userStore } from "../../../store/user-store.ts"; +import { BooleanToggle } from "../../../lib/mobx-form/boolean-toggle.ts"; export enum CardState { Remember = "remember", @@ -32,6 +33,9 @@ export class CardUnderReviewStore { isOpened = false; state?: CardState; + // A hack for iOS when the card content is too large + isOverflowing = new BooleanToggle(false); + constructor(card: DeckCardDbType, deck: DeckWithCardsWithReviewType) { this.id = card.id; this.front = card.front; diff --git a/src/screens/shared/card/card.tsx b/src/screens/shared/card/card.tsx index ff6ded66..79a16526 100644 --- a/src/screens/shared/card/card.tsx +++ b/src/screens/shared/card/card.tsx @@ -7,6 +7,7 @@ import { HorizontalDivider } from "../../../ui/horizontal-divider.tsx"; import { CardSpeaker } from "./card-speaker.tsx"; import { CardFieldView } from "./card-field-view.tsx"; import { assert } from "../../../lib/typescript/assert.ts"; +import { useIsOverflowing } from "../../../lib/react/use-is-overflowing.ts"; export const cardSize = 310; @@ -23,15 +24,23 @@ export type LimitedCardUnderReviewStore = Pick< | "answer" | "openWithAnswer" | "open" + | "isOverflowing" >; type Props = { card: LimitedCardUnderReviewStore; }; -export const Card = observer(({ card }: Props) => { +export const Card = observer((props: Props) => { + const { card } = props; + const { ref: cardRef } = useIsOverflowing( + card.isOpened, + (is) => card?.isOverflowing.setValue(is), + ); + return (
    { placeItems: "center center", padding: 10, background: theme.secondaryBgColor, + overflowX: "auto", }) : css({ color: theme.textColor, diff --git a/src/store/screen-store.ts b/src/store/screen-store.ts index 8207d89e..a57a2e23 100644 --- a/src/store/screen-store.ts +++ b/src/store/screen-store.ts @@ -15,6 +15,7 @@ type Route = | { type: "deckCatalog" } | { type: "shareDeck"; deckId: number; shareId: string } | { type: "shareFolder"; folderId: number; shareId: string } + | { type: "componentCatalog" } | { type: "userSettings" }; export type RouteType = Route["type"]; diff --git a/src/translations/t.ts b/src/translations/t.ts index 183992d1..26c68e85 100644 --- a/src/translations/t.ts +++ b/src/translations/t.ts @@ -3,7 +3,7 @@ import { getUserLanguage } from "./get-user-language.ts"; const en = { wysiwyg_big_header: "Big header", - next: 'Next', + next: "Next", wysiwyg_small_header: "Small header", wysiwyg_middle_header: "Middle header", wysiwyg_bold: "Bold", @@ -168,7 +168,7 @@ type Translation = typeof en; const ru: Translation = { wysiwyg_italic: "Курсив", wysiwyg_red: "Красный", - next: 'Далее', + next: "Далее", wysiwyg_clear_formatting: "Очистить форматирование", wysiwyg_bold: "Жирный", wysiwyg_small_header: "Маленький заголовок", @@ -325,7 +325,7 @@ const ru: Translation = { }; const es: Translation = { - next: 'Siguiente', + next: "Siguiente", wysiwyg_red: "Rojo", wysiwyg_bold: "Negrita", wysiwyg_undo: "Deshacer", @@ -490,7 +490,7 @@ const es: Translation = { }; const ptBr: Translation = { - next: 'Próximo', + next: "Próximo", wysiwyg_red: "Vermelho", wysiwyg_bold: "Negrito", wysiwyg_clear_formatting: "Limpar formatação", diff --git a/src/ui/wysiwyg-field/wysiwig-field.tsx b/src/ui/wysiwyg-field/wysiwig-field.tsx index 3508f6b3..46162537 100644 --- a/src/ui/wysiwyg-field/wysiwig-field.tsx +++ b/src/ui/wysiwyg-field/wysiwig-field.tsx @@ -12,9 +12,7 @@ import { TextField } from "../../lib/mobx-form/text-field.ts"; import { ValidationError } from "../validation-error.tsx"; import { ColorIcon } from "./color-icon.tsx"; import { t } from "../../translations/t.ts"; -import { - sanitizeTextForCard -} from "../../lib/sanitize-html/sanitize-text-for-card.ts"; +import { sanitizeTextForCard } from "../../lib/sanitize-html/sanitize-text-for-card.ts"; const BtnBigHeader = createButton( t("wysiwyg_big_header"),