diff --git a/library/src/components/Banner.tsx b/library/src/components/Banner.tsx index e6ac61e3..181f9475 100644 --- a/library/src/components/Banner.tsx +++ b/library/src/components/Banner.tsx @@ -17,7 +17,7 @@ type BannerProps = { } const announcementStyles = "bg-neutral-full text-text-inverse" -const warningStyles = "bg-warning-bold text-text" +const warningStyles = "bg-warning-bold text-text-inverse" const errorStyles = "bg-danger-bold text-text-inverse" const successStyles = "bg-success-bold text-text-inverse" const informationStyles = "bg-information-bold text-text-inverse" diff --git a/library/src/components/Blanket.tsx b/library/src/components/Blanket.tsx index 5eece37d..c4cedaee 100644 --- a/library/src/components/Blanket.tsx +++ b/library/src/components/Blanket.tsx @@ -1,4 +1,3 @@ -import React from "react" import ReactDOM from "react-dom" import type { ComponentPropsWithoutRef } from "react" import { twMerge } from "tailwind-merge" @@ -6,6 +5,7 @@ import { getPortal } from "../utils" type BlanketProps = ComponentPropsWithoutRef<"div"> & { usePortal?: boolean + portalContainer?: HTMLElement | null } export function Blanket({ @@ -14,6 +14,7 @@ export function Blanket({ "aria-label": ariaLabel, role, usePortal = true, + portalContainer = getPortal("uikts-blanket"), ...props }: BlanketProps) { const ele = ( @@ -38,8 +39,8 @@ export function Blanket({ ) - if (!usePortal) { + if (!usePortal || portalContainer === null) { return ele } - return ReactDOM.createPortal(ele, getPortal("uikts-blanket")) + return ReactDOM.createPortal(ele, portalContainer) } diff --git a/library/src/components/EventList.tsx b/library/src/components/EventList.tsx index ca8249bb..dd4bf664 100644 --- a/library/src/components/EventList.tsx +++ b/library/src/components/EventList.tsx @@ -3,7 +3,7 @@ import type { Dayjs } from "dayjs" import { twMerge } from "tailwind-merge" import type { TimeType } from "../utils" -export interface EventObject { +export interface EventListObject { key: string title?: string subtitle?: string @@ -11,13 +11,13 @@ export interface EventObject { endDate: Dayjs | undefined } -interface EventWrapper { +interface EventWrapper { booking: T renderStartDate: Dayjs renderEndDate: Dayjs } -export interface EventListProps { +export interface EventListProps { items: T[] minStartTime: Dayjs maxEndTime: Dayjs @@ -33,7 +33,7 @@ export interface EventListProps { style?: React.CSSProperties } -function useOrderByDateBookings( +function useOrderByDateBookings( items: T[], _minStartTime: Dayjs, maxEndTime: Dayjs, @@ -122,7 +122,7 @@ const dateFormat = Intl.DateTimeFormat(undefined, { year: "numeric", }) -function defaultRenderEvent( +function defaultRenderEvent( booking: T, startDate: Dayjs | undefined, endDate: Dayjs | undefined, @@ -149,7 +149,7 @@ function defaultRenderEvent( ) } -export function EventList({ +export function EventList({ items, renderEvent = defaultRenderEvent, renderTimeHeader, diff --git a/library/src/components/form/elements/CheckboxFormField.tsx b/library/src/components/form/elements/CheckboxFormField.tsx index fc5ba62b..9889dc0e 100644 --- a/library/src/components/form/elements/CheckboxFormField.tsx +++ b/library/src/components/form/elements/CheckboxFormField.tsx @@ -4,7 +4,8 @@ import type { FormField } from "../DynamicForm" import { Label } from "../../inputs" import { Checkbox } from "../../Checkbox" -export interface CheckboxFormField extends FormField { +export interface CheckboxFormFieldProps + extends FormField { onChange?: (value: string) => void } @@ -15,7 +16,7 @@ export function CheckboxFormField({ required, description, title, -}: CheckboxFormField) { +}: CheckboxFormFieldProps) { const fieldValue = formProps.watch(name) const onChangeCB = useRef(onChange) if (onChangeCB.current !== onChange) { diff --git a/library/src/components/form/elements/InputFormField.tsx b/library/src/components/form/elements/InputFormField.tsx index 01f6263c..7e3a748e 100644 --- a/library/src/components/form/elements/InputFormField.tsx +++ b/library/src/components/form/elements/InputFormField.tsx @@ -3,7 +3,8 @@ import type { FieldValues } from "react-hook-form" import type { FormField } from "../DynamicForm" import { Input, Label } from "../../inputs" -export interface InputFormField extends FormField { +export interface InputFormFieldProps + extends FormField { onChange?: (value: string) => void placeholder?: string } @@ -16,7 +17,7 @@ export function InputFormField({ description, title, placeholder, -}: InputFormField) { +}: InputFormFieldProps) { const fieldValue = formProps.watch(name) const onChangeCB = useRef(onChange) if (onChangeCB.current !== onChange) { diff --git a/library/src/components/form/elements/SelectMultiFormField.tsx b/library/src/components/form/elements/SelectMultiFormField.tsx index 906adbe6..fffcef41 100644 --- a/library/src/components/form/elements/SelectMultiFormField.tsx +++ b/library/src/components/form/elements/SelectMultiFormField.tsx @@ -3,7 +3,7 @@ import type { FieldValues } from "react-hook-form" import type { FormField } from "../DynamicForm" import { Label, Select } from "../../inputs" -export interface SelectMultiFormField< +export interface SelectMultiFormFieldProps< T extends FieldValues, A extends string | number, > extends FormField { @@ -24,7 +24,7 @@ export function SelectMultiFormField< title, options, placeholder, -}: SelectMultiFormField) { +}: SelectMultiFormFieldProps) { const fieldValue = formProps.watch(name) const onChangeCB = useRef(onChange) if (onChangeCB.current !== onChange) { diff --git a/library/src/components/form/elements/SelectSingleFormField.tsx b/library/src/components/form/elements/SelectSingleFormField.tsx index 5ea0b0be..4d435af7 100644 --- a/library/src/components/form/elements/SelectSingleFormField.tsx +++ b/library/src/components/form/elements/SelectSingleFormField.tsx @@ -3,12 +3,12 @@ import type { FieldValues, Path } from "react-hook-form" import type { FormField } from "../DynamicForm" import { Label, Select } from "../../inputs" -export interface SelectSingleFormField< +export interface SelectSingleFormFieldProps< T extends FieldValues, A extends string | number, > extends FormField { options: Array<{ label: string; value: A }> - onChange?: (value: string) => void + onChange?: (value: A) => void placeholder?: string } @@ -24,7 +24,7 @@ export function SelectSingleFormField< required, options, placeholder, -}: SelectSingleFormField) { +}: SelectSingleFormFieldProps) { const fieldValue = formProps.watch(name) const onChangeCB = useRef(onChange) if (onChangeCB.current !== onChange) { diff --git a/library/src/components/form/index.ts b/library/src/components/form/index.ts new file mode 100644 index 00000000..13a449bd --- /dev/null +++ b/library/src/components/form/index.ts @@ -0,0 +1,50 @@ +import { + CheckboxFormField, + type CheckboxFormFieldProps as _CheckboxFormFieldProps, +} from "./elements/CheckboxFormField" +import { + InputFormField, + type InputFormFieldProps as _InputFormFieldProps, +} from "./elements/InputFormField" +import { + SelectMultiFormField, + type SelectMultiFormFieldProps as _SelectMultiFormFieldProps, +} from "./elements/SelectMultiFormField" +import { + SelectSingleFormField, + type SelectSingleFormFieldProps as _SelectSingleFormFieldProps, +} from "./elements/SelectSingleFormField" +import { + DynamicForm as Form, + type FormField as _FormField, + type FormProps as _FormProps, + type DynamicFormProps as _DynamicFormProps, +} from "./DynamicForm" +import type { FieldValues } from "react-hook-form" + +const DynamicForm = { + CheckboxFormField, + InputFormField, + SelectMultiFormField, + SelectSingleFormField, + Form, +} +export { DynamicForm } + +export namespace DynamicFormTypes { + export type CheckboxFormFieldProps = + _CheckboxFormFieldProps + export type InputFormFieldProps = + _InputFormFieldProps + export type SelectMultiFormFieldProps< + T extends FieldValues, + A extends string | number, + > = _SelectMultiFormFieldProps + export type SelectSingleFormFieldProps< + T extends FieldValues, + A extends string | number, + > = _SelectSingleFormFieldProps + export type FormField = _FormField + export type FormProps = _FormProps + export type DynamicFormProps = _DynamicFormProps +} diff --git a/library/src/components/index.ts b/library/src/components/index.ts index ee12097e..f0ec09dc 100644 --- a/library/src/components/index.ts +++ b/library/src/components/index.ts @@ -34,3 +34,6 @@ export * from "./Breadcrumbs" export * from "./SectionMessage" export * from "./codeblock" export * from "./dnd" +export * from "./tour" +export * from "./form" +export * from "./EventList" diff --git a/library/src/components/inputs/Select.tsx b/library/src/components/inputs/Select.tsx index 0bb36657..41a1e376 100644 --- a/library/src/components/inputs/Select.tsx +++ b/library/src/components/inputs/Select.tsx @@ -347,7 +347,6 @@ const SelectInner = ({ invalid, ) - // get the browsers locale const locale = navigator.language @@ -373,6 +372,7 @@ const SelectInner = ({ return (
role="button" className={_props.getClassNames( "clearIndicator", @@ -421,6 +421,7 @@ const SelectInner = ({ }` return (
role="button" {..._props.innerProps} title={title} @@ -480,8 +481,10 @@ const SelectInner = ({ _props.selectProps.menuIsOpen ? "close" : "open" } the menu` return ( + // biome-ignore lint/a11y/useFocusableInteractive:
role="button" data-action="open_select" //aria-disabled={_props.isDisabled} diff --git a/library/src/components/tour/TourWrapper.tsx b/library/src/components/tour/TourWrapper.tsx new file mode 100644 index 00000000..43a4fce4 --- /dev/null +++ b/library/src/components/tour/TourWrapper.tsx @@ -0,0 +1,224 @@ +import ReactJoyride, { + type Locale, + type Styles, + type Step, + type FloaterProps, + type CallBackProps, +} from "react-joyride" +import { useCallback, useMemo, useRef, useState } from "react" +import { showErrorFlag, showInformationFlag } from "../ToastFlag" +import { flushSync } from "react-dom" + +export abstract class TourStep { + step: Step + + constructor() { + this.step = { + content: <>Step should be overwritten., + target: "body", + } + } + + onInit?(): void + + onPrepare?(): void + + onExit?(): void +} + +export interface TourProps { + isActive: boolean + setActive: (active: boolean) => void + steps: Array + skipOnError: boolean + showInfoAndError: boolean + beforeAll: () => void + afterAll: () => void + + /** + * the scroll offset from the top for the tour (to remove the fixed header) + * @default 220 + * */ + scrollOffset?: number + + /** + * Scrolls to the first step element when the tour starts + * @default true + */ + scrollToFirstStep?: boolean + + /** + * Disables the closing of the overlay when clicking outside of the tour + * @default true + * */ + disabledOverlayClose?: boolean +} + +const floaterProps: Partial = { + styles: { + floater: { + zIndex: 2000, + pointerEvents: "auto" as const, + }, + }, +} + +const styles: Partial = { + overlay: { + zIndex: 1000, + //opacity: 0.0, + }, +} + +const locale: Locale = { + back: "Zurück", + close: "Schließen", + last: "Fertig", + next: "Weiter", + open: "Öffnen", + skip: "Überspringen", +} + +export function Tour({ + isActive, + setActive, + steps, + skipOnError = true, + showInfoAndError = true, + beforeAll, + afterAll, + scrollOffset = 220, + scrollToFirstStep = true, + disabledOverlayClose = true, +}: TourProps) { + const [stepIndex, setStepIndex] = useState(0) + const isInit = useRef(false) + + // run the set stepIndex update in a timeout that is runs after the rendering is done, and use flushSync to make sure the DOM is updated + const next = useCallback((i: number) => { + window.setTimeout(() => + flushSync(() => setStepIndex((prev) => prev + i)), + ) + }, []) + + const reset = useCallback(() => { + setActive(false) + setStepIndex(0) + isInit.current = false + afterAll() + }, [afterAll, setActive]) + + const _steps = useMemo(() => steps.map((it) => it.step), [steps]) + + const callback = useCallback( + (joyrideState: CallBackProps) => { + const { action, index, lifecycle, type, step } = joyrideState + console.log("ACTION", action, "TYPE", type, "INDEX", index) + + switch (type) { + case "tour:start": + beforeAll() + isInit.current = true + setStepIndex(0) + break + case "tour:end": + reset() + break + case "step:before": + steps[index]?.onPrepare?.() + break + case "step:after": + steps[index]?.onExit?.() + switch (action) { + case "next": + steps[index + 1]?.onInit?.() + next(1) + break + case "prev": + steps[index - 1]?.onInit?.() + next(-1) + break + case "skip": + steps[index + 2]?.onInit?.() + next(2) + break + case "close": + reset() + break + case "reset": + reset() + break + case "stop": + reset() + break + default: + break + } + break + case "error:target_not_found": + if (skipOnError) { + if (showInfoAndError) { + showInformationFlag({ + title: "Tour-Info", + description: `Ein Step [${steps[index].step?.title ?? "Unbekannt"}] wurde übersprungen, das Element wurde nicht gefunden.`, + }) + } + next(1) + } else { + if (showInfoAndError) { + showErrorFlag({ + title: "Tour-Fehler", + description: `Fehler bei Step [${steps[index].step?.title ?? "Unbekannt"}]. Das Element ${step.target} wurde nicht gefunden.`, + }) + } + reset() + } + break + case "error": + if (skipOnError) { + if (showInfoAndError) { + showInformationFlag({ + title: "Tour-Info", + description: `Ein Step [${steps[index].step?.title ?? "Unbekannt"}] wurde übersprungen.`, + }) + } + next(1) + } else { + if (showInfoAndError) { + showErrorFlag({ + title: "Tour-Fehler", + description: `Fehler bei Step [${steps[index].step?.title ?? "Unbekannt"}].`, + }) + } + reset() + } + break + + default: + break + } + }, + [beforeAll, reset, next, showInfoAndError, skipOnError, steps], + ) + + return ( + + ) +} diff --git a/library/src/components/tour/index.ts b/library/src/components/tour/index.ts new file mode 100644 index 00000000..a8207e0e --- /dev/null +++ b/library/src/components/tour/index.ts @@ -0,0 +1,3 @@ +import { Tour, TourStep, type TourProps } from "./TourWrapper" + +export { Tour, TourStep, type TourProps } diff --git a/showcase/applayoutexample/index.tsx b/showcase/applayoutexample/index.tsx index 654ba1ff..e1dc60bc 100644 --- a/showcase/applayoutexample/index.tsx +++ b/showcase/applayoutexample/index.tsx @@ -1,11 +1,16 @@ import React from "react" -import ReactDOM from "react-dom" +import { createRoot } from "react-dom/client" import AppLayoutExample from "./AppLayoutExample" -ReactDOM.render( +const container = document.getElementById("applayout-root") +if (!container) { + throw new Error("Could not find root element") +} + +const root = createRoot(container) +root.render( , - document.getElementById("applayout-root"), ) diff --git a/showcase/public/showcase-sources.txt b/showcase/public/showcase-sources.txt index 5ae5b735..9260da88 100644 --- a/showcase/public/showcase-sources.txt +++ b/showcase/public/showcase-sources.txt @@ -3499,7 +3499,7 @@ function EventListExample() { ) : undefined } - renderEvent={(obj, startDate, endDate) => { + /*renderEvent={(obj, startDate, endDate) => { return (
) - }} + }}*/ />
) @@ -3556,7 +3556,7 @@ function EventListStartEndExample() { ] return ( -
+
@@ -3613,7 +3613,7 @@ export default function EventListShowcase(props: ShowcaseProps) { sourceCodeExampleId: "event-list", }, { - title: "Custom Start/End-Times", + title: "Custom Start/End-Times and Custom Render", example: , sourceCodeExampleId: "event-list-start-end", }, @@ -4085,12 +4085,8 @@ export default FlagShowcase import ShowcaseWrapperItem, { type ShowcaseProps, } from "../../ShowCaseWrapperItem/ShowcaseWrapperItem" -import { DynamicForm } from "@linked-planet/ui-kit-ts/components/form/DynamicForm" -import { InputFormField } from "@linked-planet/ui-kit-ts/components/form/elements/InputFormField" -import { SelectSingleFormField } from "@linked-planet/ui-kit-ts/components/form/elements/SelectSingleFormField" -import { CheckboxFormField } from "@linked-planet/ui-kit-ts/components/form/elements/CheckboxFormField" +import { DynamicForm } from "@linked-planet/ui-kit-ts" import { Button, ButtonGroup } from "@linked-planet/ui-kit-ts" -import { SelectMultiFormField } from "@linked-planet/ui-kit-ts/components/form/elements/SelectMultiFormField" interface TestObject { firstname: string @@ -4126,7 +4122,7 @@ const hobbies = [ function FormVerticalExample() { return (
- + obj={testObject} onSubmit={(data) => { console.info("Saving form", data) @@ -4134,17 +4130,17 @@ function FormVerticalExample() { > {(formProps) => ( <> - - - - - - )} - +
) } @@ -4181,7 +4177,7 @@ function FormVerticalExample() { function FormHorizontalExample() { return (
- + vertical obj={testObject} onSubmit={(data) => { @@ -4190,19 +4186,19 @@ function FormHorizontalExample() { > {(formProps) => ( <> - - - - - - )} - +
) } @@ -4239,7 +4235,7 @@ function FormHorizontalExample() { function FormCustomExample() { return (
- + hideReset hideSave className="max-w-4xl mt-3" @@ -4251,18 +4247,18 @@ function FormCustomExample() { {(formProps) => ( <>
- - -
- - - )} - +
) } @@ -9022,6 +9018,227 @@ function TooltipShowcase(props: ShowcaseProps) { export default TooltipShowcase +import CrossIcon from "@atlaskit/icon/glyph/cross" +import { + Button, + ButtonGroup, + Modal, + Select, + ToastFlagContainer, +} from "@linked-planet/ui-kit-ts" +import { Tour, TourStep } from "@linked-planet/ui-kit-ts" +import { useMemo, useState } from "react" +import ShowcaseWrapperItem, { + type ShowcaseProps, +} from "../../ShowCaseWrapperItem/ShowcaseWrapperItem" +import type { Step } from "react-joyride" + +//#region tour +const defaultLocale = { + back: "Back", + close: "Close", + last: "Done", + next: "Next", + open: "Open", + skip: "Skip", +} as const + +function TourExample() { + const [isActive, setActive] = useState(false) + const [popup, setPopup] = useState(false) + + const steps = useMemo(() => { + const InitStep = new (class extends TourStep { + step: Step = { + title: "Tour starten", + target: "#tour-start", + disableBeacon: true, + showSkipButton: false, + placement: "bottom", + locale: { ...defaultLocale, next: "Start Tour" }, + content: ( + + The first step selects the tour start to start the tour. + + ), + } + })() + + const SecondStep = new (class extends TourStep { + step: Step = { + title: "Button", + target: "#joyride-first", + disableBeacon: true, + showSkipButton: false, + placement: "right", + locale: defaultLocale, + content: ( + + This step selects the popup which would open the popup. + + ), + } + })() + + const ThirdPopupStep = new (class extends TourStep { + step: Step = { + title: "Popup", + target: "#test-select", + disableBeacon: true, + showSkipButton: false, + placement: "right", + locale: defaultLocale, + content: ( + + This step opens the popup and selects the dropdown in + it. + + ), + } + + onInit() { + setPopup(true) + } + + onPrepare() { + console.log("prepare message") + } + + onExit() { + setPopup(false) + } + })() + + const FourthStep = new (class extends TourStep { + step: Step = { + title: "Weiterer Button", + target: "#joyride-second", + disableBeacon: true, + showSkipButton: false, + placement: "right", + locale: defaultLocale, + content: ( + + This step closes the popup and continues with this + button. + + ), + } + })() + return [InitStep, SecondStep, ThirdPopupStep, FourthStep] + }, []) + + return ( +
+ +
+ + { + // initialize dummy data or other inits before tour starts + console.info("Starting Tour") + }} + afterAll={() => { + // cleanup dummy data or other inits after tour finished + console.info("Ending Tour") + }} + /> +
+ + +
+ { + console.log("OOPEN POPUP CHANGE", popup, opened) + if (!opened) setPopup(false) + }} + //shouldCloseOnEscapePress={false} + shouldCloseOnOverlayClick={false} // this is required, the show "clicks" outside of the dialog closing the modal, which results in the failing of the next step because the element is not mounted anymore + accessibleDialogDescription="This is a modal dialog example" + > + + + Sample Modal + + + + +
+

This is the body of the modal.

+
+ +
+ + + + + +
+ +
+ ) +} + +//#endregion tour + +export default function TourShowcase(props: ShowcaseProps) { + return ( + , + sourceCodeExampleId: "tour", + }, + ]} + /> + ) +} diff --git a/showcase/src/useShowcases.tsx b/showcase/src/useShowcases.tsx index d7d90c10..6e12c86b 100644 --- a/showcase/src/useShowcases.tsx +++ b/showcase/src/useShowcases.tsx @@ -51,6 +51,7 @@ import BreadcrumbsShowcase from "./components/showcase/wrapper/BreadcrumbsShowca import SectionMessageShowcase from "./components/showcase/wrapper/SectionMessageShowcase" import DragAndDropShowcase from "./components/showcase/wrapper/DragAndDropShowcase" import GlobalStateShowcase from "./components/showcase/wrapper/GlobalStateShowcase" +import TourShowcase from "./components/showcase/wrapper/TourShowcase" import FormShowcase from "./components/showcase/wrapper/FormShowcase" import EventListShowcase from "./components/showcase/wrapper/EventListShowcase" export default function useShowcases({ @@ -179,6 +180,7 @@ export default function useShowcases({ "Truncated Text": ( ), + Tour: , Utils: , }), [overallSourceCode],