@ahrowe/ui 0.41.0 → 0.41.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs-types/common/hooks/useAnchorTracking.d.ts +4 -3
- package/dist/cjs-types/common/hooks/useOverlay.d.ts +4 -1
- package/dist/cjs-types/common/popover/popover.types.d.ts +1 -1
- package/dist/cjs-types/common/popover/usePopoverPosition.d.ts +6 -1
- package/dist/esm/common/ahroweProvider/ahroweProvider.mjs +1 -1
- package/dist/esm/common/ahroweProvider/ahroweProvider.mjs.map +1 -1
- package/dist/esm/common/calendar/calendar.mjs +1 -1
- package/dist/esm/common/calendar/calendar.mjs.map +1 -1
- package/dist/esm/common/fab/fab.module.mjs.map +1 -1
- package/dist/esm/common/hooks/useAnchorTracking.mjs +1 -1
- package/dist/esm/common/hooks/useAnchorTracking.mjs.map +1 -1
- package/dist/esm/common/hooks/useFocusBoundary.mjs +1 -1
- package/dist/esm/common/hooks/useFocusBoundary.mjs.map +1 -1
- package/dist/esm/common/hooks/useOverlay.mjs +1 -1
- package/dist/esm/common/hooks/useOverlay.mjs.map +1 -1
- package/dist/esm/common/modal/modal.mjs +1 -1
- package/dist/esm/common/modal/modal.mjs.map +1 -1
- package/dist/esm/common/modal/modal.module.mjs.map +1 -1
- package/dist/esm/common/otpInput/otpInput.mjs +1 -1
- package/dist/esm/common/otpInput/otpInput.mjs.map +1 -1
- package/dist/esm/common/popover/popover.mjs +1 -1
- package/dist/esm/common/popover/popover.mjs.map +1 -1
- package/dist/esm/common/popover/popover.module.mjs.map +1 -1
- package/dist/esm/common/popover/popover.types.mjs.map +1 -1
- package/dist/esm/common/popover/usePopoverPosition.mjs +1 -1
- package/dist/esm/common/popover/usePopoverPosition.mjs.map +1 -1
- package/dist/esm/common/styles/sheet.module.mjs.map +1 -1
- package/dist/esm/common/timer/timer.mjs +1 -1
- package/dist/esm/common/timer/timer.mjs.map +1 -1
- package/dist/esm/common/tooltip/tooltip.module.mjs.map +1 -1
- package/dist/esm/common/virtualList/virtualList.mjs +1 -1
- package/dist/esm/common/virtualList/virtualList.mjs.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/common/hooks/useAnchorTracking.d.ts +4 -3
- package/dist/types/common/hooks/useOverlay.d.ts +4 -1
- package/dist/types/common/popover/popover.types.d.ts +1 -1
- package/dist/types/common/popover/usePopoverPosition.d.ts +6 -1
- package/docs/Modal.md +1 -1
- package/docs/Popover.md +2 -2
- package/package.json +1 -1
|
@@ -9,10 +9,11 @@ export interface UseAnchorTrackingArgs {
|
|
|
9
9
|
*/
|
|
10
10
|
elementRefs: RefObject<HTMLElement | null>[];
|
|
11
11
|
/**
|
|
12
|
-
* Positions the popup. Called once on setup and then on every scroll, resize
|
|
13
|
-
* change
|
|
12
|
+
* Positions the popup. Called once on setup and then on every scroll, resize, tracked size
|
|
13
|
+
* change and end of a CSS animation that moved the trigger, plus whenever it calls `rerun` for
|
|
14
|
+
* an end none of those report. Reads the caller's own refs for everything else.
|
|
14
15
|
*/
|
|
15
|
-
update: (trigger: HTMLElement, ancestors: HTMLElement[]) => void;
|
|
16
|
+
update: (trigger: HTMLElement, ancestors: HTMLElement[], rerun: () => void) => void;
|
|
16
17
|
/**
|
|
17
18
|
* Elements whose own size changes must re-run `update`: a popup whose content changes size
|
|
18
19
|
* while open (a filtered list shrinking, an async list loading in) otherwise keeps coordinates
|
|
@@ -6,7 +6,10 @@ export interface UseOverlayArgs {
|
|
|
6
6
|
containerRef: RefObject<HTMLElement | null>;
|
|
7
7
|
/** Called when Escape is pressed while this is the topmost overlay. Omit to not handle Escape. */
|
|
8
8
|
onEscape?: () => void;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Keep Tab inside the panel and restore focus to the previously focused element on close, unless
|
|
11
|
+
* an overlay opened after this one is still open.
|
|
12
|
+
*/
|
|
10
13
|
trapFocus?: boolean;
|
|
11
14
|
/** Prevent the page behind the overlay from scrolling. */
|
|
12
15
|
lockScroll?: boolean;
|
|
@@ -53,7 +53,7 @@ export interface PopoverProps extends Omit<HtmlProps, 'content'> {
|
|
|
53
53
|
offset?: number;
|
|
54
54
|
/** Render the arrow pointing at the trigger (default `true`). */
|
|
55
55
|
withArrow?: boolean;
|
|
56
|
-
/** Close
|
|
56
|
+
/** Close on a click outside, or when focus leaves the trigger and panel (default `true`). */
|
|
57
57
|
closeOnOutsideClick?: boolean;
|
|
58
58
|
/** Close when pressing Escape (default `true`). */
|
|
59
59
|
closeOnEscape?: boolean;
|
|
@@ -32,6 +32,11 @@ export interface UsePopoverPositionArgs {
|
|
|
32
32
|
* panel that overhangs it.
|
|
33
33
|
*/
|
|
34
34
|
shrinkToFit?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Called when a newly opened panel first becomes visible. Focus has to wait for it: a panel
|
|
37
|
+
* still hidden, waiting out an ancestor's animation, takes none.
|
|
38
|
+
*/
|
|
39
|
+
onShown?: () => void;
|
|
35
40
|
}
|
|
36
41
|
/**
|
|
37
42
|
* Positions a portaled, `position: fixed` popover on any of the four sides of its trigger with
|
|
@@ -42,4 +47,4 @@ export interface UsePopoverPositionArgs {
|
|
|
42
47
|
* `useAnchorTracking` supplies the lifecycle around this: mount wait, ancestor resolution, and
|
|
43
48
|
* re-running on scroll, viewport resize and panel size changes.
|
|
44
49
|
*/
|
|
45
|
-
export declare function usePopoverPosition({ isOpen, placement, align, offset, triggerRef, floatingRef, panelRef, arrowRef, matchTriggerWidth, shrinkToFit, }: UsePopoverPositionArgs): void;
|
|
50
|
+
export declare function usePopoverPosition({ isOpen, placement, align, offset, triggerRef, floatingRef, panelRef, arrowRef, matchTriggerWidth, shrinkToFit, onShown, }: UsePopoverPositionArgs): void;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use client";import e from"../configProvider/configProvider.mjs";import t from"../scrollbarProvider/index.mjs";import n from"../themeProvider/themeProvider.mjs";import{PortalHostContext as r}from"../bodyEnd/portalHostContext.mjs";import i from"../toast/toastHost.mjs";import{useCallback as a,useContext as o,useEffect as s,useLayoutEffect as c,useState as l,useSyncExternalStore as u}from"react";import{jsx as d,jsxs as f}from"react/jsx-runtime";var p=()=>()=>{};function m({children:m,themes:h,currentThemeId:g,defaultProps:_,presentation:v,toast:y,scrollbars:b}){let x=o(r)!==void 0,S=u(p,()=>!0,()=>!1),[C]=l(()=>typeof document>`u`?null:document.createElement(`div`)),w=a(e=>{let t=e?.parentElement?.lastElementChild;
|
|
1
|
+
"use client";import e from"../configProvider/configProvider.mjs";import t from"../scrollbarProvider/index.mjs";import n from"../themeProvider/themeProvider.mjs";import{PortalHostContext as r}from"../bodyEnd/portalHostContext.mjs";import i from"../toast/toastHost.mjs";import{useCallback as a,useContext as o,useEffect as s,useLayoutEffect as c,useState as l,useSyncExternalStore as u}from"react";import{jsx as d,jsxs as f}from"react/jsx-runtime";var p=()=>()=>{};function m({children:m,themes:h,currentThemeId:g,defaultProps:_,presentation:v,toast:y,scrollbars:b}){let x=o(r)!==void 0,S=u(p,()=>!0,()=>!1),[C]=l(()=>typeof document>`u`?null:document.createElement(`div`)),w=a(e=>{let t=e?.parentElement?.lastElementChild;t&&C&&C.parentNode!==t&&t.appendChild(C)},[C]),T=!x&&b!==!1;c(()=>{if(!T||!C)return;let e=getComputedStyle(C).getPropertyValue(`--background-accent-light`).trim(),t=document.documentElement.style;return t.setProperty(`--ahrowe-page-scrollbar-thumb`,e),()=>{t.removeProperty(`--ahrowe-page-scrollbar-thumb`)}},[T,C,h,g]);let E=x&&(y!==void 0||b!==void 0);s(()=>{E&&console.warn("[AhroweProvider] `toast` and `scrollbars` are read by the outermost AhroweProvider only. Set them there.")},[E]);let D=f(r.Provider,{value:S?C:null,children:[d(`template`,{ref:w}),d(i,{...y,children:m}),d(`div`,{})]});return d(e,{defaultProps:_,presentation:v,children:d(n,{themes:h,currentThemeId:g,children:x?m:b===!1?D:d(t,{children:D})})})}export{m as default};
|
|
2
2
|
//# sourceMappingURL=ahroweProvider.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ahroweProvider.mjs","names":[],"sources":["../../../../package/common/ahroweProvider/ahroweProvider.tsx"],"sourcesContent":["import {\n useCallback,\n useContext,\n useEffect,\n useLayoutEffect,\n useState,\n useSyncExternalStore,\n type ReactElement,\n} from 'react';\n\nimport ConfigProvider from 'components/common/configProvider';\nimport ScrollbarProvider from 'components/common/scrollbarProvider';\nimport ThemeProvider from 'components/common/themeProvider';\nimport ToastHost from '../toast/toastHost';\nimport { PortalHostContext } from '../bodyEnd/portalHostContext';\nimport type { AhroweProviderProps } from './ahroweProvider.types';\n\nconst subscribeToNothing = () => () => {};\n\nfunction AhroweProvider({\n children,\n themes,\n currentThemeId,\n defaultProps,\n presentation,\n toast,\n scrollbars,\n}: AhroweProviderProps): ReactElement {\n // A nested provider themes and configures its section only. A second toast list would stack\n // over the first in the same corner, and a second host would split the portals in two.\n const isNested = useContext(PortalHostContext) !== undefined;\n\n // The server renders no portal, so hydration must not either: `false` there, and `true` from\n // the first render of an app that is not hydrating.\n const canPortal = useSyncExternalStore(\n subscribeToNothing,\n () => true,\n () => false,\n );\n\n // Created up front instead of rendered: a rendered host exists only after the first commit, so\n // a modal open on the first render mounted a commit late and its initial focus found nothing.\n const [host] = useState(() => (typeof document === 'undefined' ? null : document.createElement('div')));\n\n // Attached from a marker before the app, because refs attach in tree order: this way the host is\n // in the page before any layout effect runs. The slot stays last, so panels paint above the app.\n const attachHost = useCallback(\n (marker: HTMLTemplateElement | null) => {\n const slot = marker?.parentElement?.lastElementChild;\n if (!slot || !host) return;\n slot
|
|
1
|
+
{"version":3,"file":"ahroweProvider.mjs","names":[],"sources":["../../../../package/common/ahroweProvider/ahroweProvider.tsx"],"sourcesContent":["import {\n useCallback,\n useContext,\n useEffect,\n useLayoutEffect,\n useState,\n useSyncExternalStore,\n type ReactElement,\n} from 'react';\n\nimport ConfigProvider from 'components/common/configProvider';\nimport ScrollbarProvider from 'components/common/scrollbarProvider';\nimport ThemeProvider from 'components/common/themeProvider';\nimport ToastHost from '../toast/toastHost';\nimport { PortalHostContext } from '../bodyEnd/portalHostContext';\nimport type { AhroweProviderProps } from './ahroweProvider.types';\n\nconst subscribeToNothing = () => () => {};\n\nfunction AhroweProvider({\n children,\n themes,\n currentThemeId,\n defaultProps,\n presentation,\n toast,\n scrollbars,\n}: AhroweProviderProps): ReactElement {\n // A nested provider themes and configures its section only. A second toast list would stack\n // over the first in the same corner, and a second host would split the portals in two.\n const isNested = useContext(PortalHostContext) !== undefined;\n\n // The server renders no portal, so hydration must not either: `false` there, and `true` from\n // the first render of an app that is not hydrating.\n const canPortal = useSyncExternalStore(\n subscribeToNothing,\n () => true,\n () => false,\n );\n\n // Created up front instead of rendered: a rendered host exists only after the first commit, so\n // a modal open on the first render mounted a commit late and its initial focus found nothing.\n const [host] = useState(() => (typeof document === 'undefined' ? null : document.createElement('div')));\n\n // Attached from a marker before the app, because refs attach in tree order: this way the host is\n // in the page before any layout effect runs. The slot stays last, so panels paint above the app.\n const attachHost = useCallback(\n (marker: HTMLTemplateElement | null) => {\n const slot = marker?.parentElement?.lastElementChild;\n if (!slot || !host) return;\n // No cleanup, and no move when already in place: the host leaves with its slot anyway, and\n // StrictMode's extra detach and reattach otherwise took a focused element in it along.\n if (host.parentNode !== slot) slot.appendChild(host);\n },\n [host],\n );\n\n // Read from the host rather than `themes`, for the resolved value, `var()` references included.\n const stylesPage = !isNested && scrollbars !== false;\n useLayoutEffect(() => {\n if (!stylesPage || !host) return;\n const thumb = getComputedStyle(host).getPropertyValue('--background-accent-light').trim();\n const page = document.documentElement.style;\n page.setProperty('--ahrowe-page-scrollbar-thumb', thumb);\n return () => {\n page.removeProperty('--ahrowe-page-scrollbar-thumb');\n };\n }, [stylesPage, host, themes, currentThemeId]);\n\n const ignoresRootProps = isNested && (toast !== undefined || scrollbars !== undefined);\n useEffect(() => {\n if (ignoresRootProps) {\n console.warn(\n '[AhroweProvider] `toast` and `scrollbars` are read by the outermost AhroweProvider only. Set them there.',\n );\n }\n }, [ignoresRootProps]);\n\n const root = (\n <PortalHostContext.Provider value={canPortal ? host : null}>\n <template ref={attachHost} />\n <ToastHost {...toast}>{children}</ToastHost>\n <div />\n </PortalHostContext.Provider>\n );\n\n return (\n <ConfigProvider defaultProps={defaultProps} presentation={presentation}>\n <ThemeProvider themes={themes} currentThemeId={currentThemeId}>\n {/* Around the portal slot too, so the scrollbars of the panels in it are styled as well. */}\n {isNested ? children : scrollbars === false ? root : <ScrollbarProvider>{root}</ScrollbarProvider>}\n </ThemeProvider>\n </ConfigProvider>\n );\n}\n\nexport default AhroweProvider;\n"],"mappings":"8bAiBA,IAAM,UAAiC,CAAC,EAExC,SAAS,EAAe,CACtB,WACA,SACA,iBACA,eACA,eACA,QACA,cACoC,CAGpC,IAAM,EAAW,EAAW,CAAiB,IAAM,IAAA,GAI7C,EAAY,EAChB,MACM,OACA,EACR,EAIM,CAAC,GAAQ,MAAgB,OAAO,SAAa,IAAc,KAAO,SAAS,cAAc,KAAK,CAAE,EAIhG,EAAa,EAChB,GAAuC,CACtC,IAAM,EAAO,GAAQ,eAAe,iBAC/B,GAAS,GAGV,EAAK,aAAe,GAAM,EAAK,YAAY,CAAI,CACrD,EACA,CAAC,CAAI,CACP,EAGM,EAAa,CAAC,GAAY,IAAe,GAC/C,MAAsB,CACpB,GAAI,CAAC,GAAc,CAAC,EAAM,OAC1B,IAAM,EAAQ,iBAAiB,CAAI,CAAC,CAAC,iBAAiB,2BAA2B,CAAC,CAAC,KAAK,EAClF,EAAO,SAAS,gBAAgB,MAEtC,OADA,EAAK,YAAY,gCAAiC,CAAK,MAC1C,CACX,EAAK,eAAe,+BAA+B,CACrD,CACF,EAAG,CAAC,EAAY,EAAM,EAAQ,CAAc,CAAC,EAE7C,IAAM,EAAmB,IAAa,IAAU,IAAA,IAAa,IAAe,IAAA,IAC5E,MAAgB,CACV,GACF,QAAQ,KACN,0GACF,CAEJ,EAAG,CAAC,CAAgB,CAAC,EAErB,IAAM,EACJ,EAAC,EAAkB,SAAnB,CAA4B,MAAO,EAAY,EAAO,KAAtD,SAAA,CACE,EAAC,WAAD,CAAU,IAAK,CAAa,CAAA,EAC5B,EAAC,EAAD,CAAW,GAAI,EAAQ,UAAoB,CAAA,EAC3C,EAAC,MAAD,CAAM,CAAA,CACoB,IAG9B,OACE,EAAC,EAAD,CAA8B,eAA4B,eACxD,SAAA,EAAC,EAAD,CAAuB,SAAwB,iBAE5C,SAAA,EAAW,EAAW,IAAe,GAAQ,EAAO,EAAC,EAAD,CAAA,SAAoB,CAAwB,CAAA,CACpF,CAAA,CACD,CAAA,CAEpB"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{useLabels as t}from"../configProvider/useLabels.mjs";import{mergeRefs as n}from"../utils/mergeRefs.mjs";import{asDay as r,dateToDay as i,dayToDate as a}from"../utils/day.mjs";import{CalendarView as o}from"./calendar.types.mjs";import{addDays as
|
|
1
|
+
"use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{useLabels as t}from"../configProvider/useLabels.mjs";import{mergeRefs as n}from"../utils/mergeRefs.mjs";import{asDay as r,dateToDay as i,dayToDate as a}from"../utils/day.mjs";import{CalendarView as o}from"./calendar.types.mjs";import{addDays as ee,dayDiff as s,hiddenDaySet as te,periodRange as ne,rangeWithNeighbours as re,shownDay as ie,shownDays as c,startOfDay as l,stepDate as u}from"./dates.mjs";import{formatPeriodTitle as d,localeWeekStart as ae}from"./format.mjs";import oe from"./components/agendaList/agendaList.mjs";import se from"./components/calendarHeader/calendarHeader.mjs";import ce from"./components/compactMonth/compactMonth.mjs";import le from"./components/monthView/monthView.mjs";import ue from"./components/timeGrid/timeGrid.mjs";import de from"./calendar.module.mjs";import{useEffect as f,useLayoutEffect as p,useRef as m,useState as h}from"react";import fe from"classnames";import{jsx as g,jsxs as pe}from"react/jsx-runtime";var me=[o.Month,o.Week,o.Day,o.Agenda],he=600,ge={previous:`Previous`,next:`Next`,today:`Today`,viewSwitcher:`Calendar view`,month:`Month`,week:`Week`,day:`Day`,agenda:`Agenda`,allDay:`All day`,more:e=>`+${e} more`,eventCount:e=>e===1?`1 event`:`${e} events`,noEvents:`No events`,newEvent:`New event`};function _(_){let{events:v,date:y,defaultDate:_e,onDateChange:ve,view:b,defaultView:ye=o.Month,onViewChange:be,views:x=me,locale:S,weekStartsOn:xe,slotMinutes:Se=15,hiddenDays:Ce,scrollToHour:we,onRangeChange:C,onEventClick:w,onEventChange:T,onSlotCreate:E,renderEvent:D,labels:O,ref:k,className:A,style:Te,classNames:j,styles:M,...Ee}=e(`Calendar`,_),N=t(`Calendar`,O,ge),P=xe??ae(S),F=te(Ce),I=m(null),De=m(null),[L,Oe]=h(0),[ke,Ae]=h(1),[je,Me]=h(()=>{let e=r(_e);return e?a(e):l(new Date)}),R=r(y),z=ie(R?a(R):je,F),[Ne,Pe]=h(ye),B=b??Ne;p(()=>{let e=I.current;if(!e)return;let t=()=>Oe(e.getBoundingClientRect().width);t();let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[]);let V=L>0&&L<he,H=re(B,z,P,F),U=`${i(H.start)}/${i(H.end)}`,W=m({range:H,onRangeChange:C}),G=m(null);p(()=>{W.current={range:H,onRangeChange:C}}),f(()=>{G.current!==U&&(G.current=U,W.current.onRangeChange?.(W.current.range))},[U]);function K(e){let t=l(e);i(t)!==i(z)&&(Ae(t>z?1:-1),y===void 0&&Me(t),ve?.(i(t)))}function q(e){e!==B&&(b===void 0&&Pe(e),be?.(e))}let[J,Y]=h(()=>new Map);function Fe(e){let t=T?.(e);if(typeof t?.then!=`function`)return;let n=String(e.event.id),r={start:e.start,end:e.end};Y(e=>new Map(e).set(n,r));let i=()=>Y(e=>{if(e.get(n)!==r)return e;let t=new Map(e);return t.delete(n),t});t.then(i,i)}let X=J.size===0?v:v.map(e=>{let t=J.get(String(e.id));return t?{...e,...t}:e}),Z={events:X,date:z,view:B,locale:S,weekStartsOn:P,slotMinutes:Se,hidden:F,scrollToHour:we,text:N,onDateChange:K,onOpenDay:x.includes(o.Day)?e=>{K(e),q(o.Day)}:void 0,onEventClick:w,onEventChange:T&&Fe,onSlotCreate:E,renderEvent:D,classNames:j,slotStyles:M},Q=ne(B,z,P),$=c(Q.start,s(Q.start,Q.end),F),Ie={start:$[0],end:ee($[$.length-1],1)};function Le(){switch(B){case o.Week:case o.Day:return g(ue,{...Z,zoomRef:De});case o.Agenda:return g(oe,{events:X,start:Q.start,days:s(Q.start,Q.end),hidden:F,scrollTo:z,showDayHeaders:!0,locale:S,text:N,onEventClick:w,renderEvent:D,classNames:j,slotStyles:M});default:return g(V?ce:le,{...Z})}}return pe(`div`,{...Ee,ref:n(I,k),className:fe(de.calendar,A,j?.root),style:{...Te,...M?.root},children:[g(se,{className:j?.header,style:M?.header,compact:V,title:d(B,z,Ie,S),direction:ke,view:B,views:x,text:N,onStep:e=>K(u(B,z,e,F)),onToday:()=>K(new Date),onViewChange:q}),Le()]})}export{_ as default};
|
|
2
2
|
//# sourceMappingURL=calendar.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calendar.mjs","names":[],"sources":["../../../../package/common/calendar/calendar.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState, type ReactElement } from 'react';\nimport cx from 'classnames';\n\nimport { useComponentDefaults, useLabels } from '../configProvider';\nimport { mergeRefs } from '../utils/mergeRefs';\nimport { asDay, dayToDate } from '../utils/day';\nimport {\n CalendarView,\n type CalendarEvent,\n type CalendarEventChange,\n type CalendarLabels,\n type CalendarProps,\n} from './calendar.types';\nimport {\n dayDiff,\n dateToDay,\n addDays,\n hiddenDaySet,\n periodRange,\n rangeWithNeighbours,\n shownDay,\n shownDays,\n startOfDay,\n stepDate,\n} from './dates';\nimport { formatPeriodTitle, localeWeekStart } from './format';\nimport AgendaList from './components/agendaList';\nimport CalendarHeader from './components/calendarHeader';\nimport CompactMonth from './components/compactMonth';\nimport MonthView from './components/monthView';\nimport TimeGrid from './components/timeGrid';\nimport type { TimeGridZoom } from './components/timeGrid/timeGrid.types';\nimport type { CalendarViewProps } from './view.types';\n\nimport styles from './calendar.module.pcss';\n\nconst ALL_VIEWS = [CalendarView.Month, CalendarView.Week, CalendarView.Day, CalendarView.Agenda];\n\n/** Below this width the month view folds into the compact month with a day list. */\nconst COMPACT_WIDTH = 600;\n\nconst DEFAULT_LABELS: Required<CalendarLabels> = {\n previous: 'Previous',\n next: 'Next',\n today: 'Today',\n viewSwitcher: 'Calendar view',\n month: 'Month',\n week: 'Week',\n day: 'Day',\n agenda: 'Agenda',\n allDay: 'All day',\n more: (count) => `+${count} more`,\n eventCount: (count) => (count === 1 ? '1 event' : `${count} events`),\n noEvents: 'No events',\n newEvent: 'New event',\n};\n\nfunction Calendar<E extends CalendarEvent = CalendarEvent>(props: CalendarProps<E>): ReactElement {\n const {\n events,\n date: controlledDate,\n defaultDate,\n onDateChange,\n view: controlledView,\n defaultView = CalendarView.Month,\n onViewChange,\n views = ALL_VIEWS,\n locale,\n weekStartsOn: weekStartsOnProp,\n slotMinutes = 15,\n hiddenDays,\n scrollToHour,\n onRangeChange,\n onEventClick,\n onEventChange,\n onSlotCreate,\n renderEvent,\n labels,\n ref,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('Calendar', props);\n const text = useLabels('Calendar', labels, DEFAULT_LABELS);\n const weekStartsOn = weekStartsOnProp ?? localeWeekStart(locale);\n const hidden = hiddenDaySet(hiddenDays);\n\n const rootRef = useRef<HTMLDivElement>(null);\n const zoomRef = useRef<TimeGridZoom | null>(null);\n const [width, setWidth] = useState(0);\n const [direction, setDirection] = useState<1 | -1>(1);\n\n // `date` is a day, the same wherever the calendar is opened, and read as its first local moment.\n const [internalDate, setInternalDate] = useState(() => {\n const initial = asDay(defaultDate);\n return initial ? dayToDate(initial) : startOfDay(new Date());\n });\n const controlledDay = asDay(controlledDate);\n // Only ever a day the views show. The selection itself is left alone: a hidden day in `date` is\n // the parent's, and stepping on from it lands where the parent would expect.\n const date = shownDay(controlledDay ? dayToDate(controlledDay) : internalDate, hidden);\n const [internalView, setInternalView] = useState(defaultView);\n const view = controlledView ?? internalView;\n\n // The layout switch follows the calendar's own width rather than the viewport's, so it also\n // happens in a sidebar on a wide screen. Width 0 means no layout (server, jsdom): stay wide.\n useLayoutEffect(() => {\n const root = rootRef.current;\n if (!root) return;\n // The border box, both times: an observer's `contentRect` leaves the border out, and 600px\n // wide would then read as 598.\n const measure = () => setWidth(root.getBoundingClientRect().width);\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(root);\n return () => observer.disconnect();\n }, []);\n const compact = width > 0 && width < COMPACT_WIDTH;\n\n // Reported per range, not per render: `date` is a new object on every render of a controlled\n // parent. Not `useEffectEvent` for the latest callback, which React only has from 19.2.\n const range = rangeWithNeighbours(view, date, weekStartsOn, hidden);\n const rangeKey = `${dateToDay(range.start)}/${dateToDay(range.end)}`;\n const latest = useRef({ range, onRangeChange });\n useLayoutEffect(() => {\n latest.current = { range, onRangeChange };\n });\n useEffect(() => {\n latest.current.onRangeChange?.(latest.current.range);\n }, [rangeKey]);\n\n function changeDate(next: Date) {\n const day = startOfDay(next);\n if (dateToDay(day) === dateToDay(date)) return;\n setDirection(day > date ? 1 : -1);\n if (controlledDate === undefined) setInternalDate(day);\n onDateChange?.(dateToDay(day));\n }\n\n function changeView(next: CalendarView) {\n if (next === view) return;\n if (controlledView === undefined) setInternalView(next);\n onViewChange?.(next);\n }\n\n // Moves whose promise has not settled yet, drawn where they were dropped meanwhile.\n const [pending, setPending] = useState<ReadonlyMap<string, { start: Date; end: Date }>>(() => new Map());\n function changeEvent(change: CalendarEventChange<E>) {\n const result = onEventChange?.(change);\n if (typeof (result as Promise<unknown> | undefined)?.then !== 'function') return;\n const key = String(change.event.id);\n const entry = { start: change.start, end: change.end };\n setPending((current) => new Map(current).set(key, entry));\n // A later move of the same event owns the entry by then, and keeps it.\n const settle = () =>\n setPending((current) => {\n if (current.get(key) !== entry) return current;\n const next = new Map(current);\n next.delete(key);\n return next;\n });\n (result as Promise<unknown>).then(settle, settle);\n }\n const shownEvents =\n pending.size === 0\n ? events\n : events.map((event) => {\n const moved = pending.get(String(event.id));\n return moved ? { ...event, ...moved } : event;\n });\n\n const viewProps: CalendarViewProps<E> = {\n events: shownEvents,\n date,\n view,\n locale,\n weekStartsOn,\n slotMinutes,\n hidden,\n scrollToHour,\n text,\n onDateChange: changeDate,\n onOpenDay: views.includes(CalendarView.Day)\n ? (day) => {\n changeDate(day);\n changeView(CalendarView.Day);\n }\n : undefined,\n onEventClick,\n onEventChange: onEventChange && changeEvent,\n onSlotCreate,\n renderEvent,\n classNames,\n slotStyles,\n };\n\n // The week's title names the days it shows, not the ones it hides at either end.\n const period = periodRange(view, date, weekStartsOn);\n const columns = shownDays(period.start, dayDiff(period.start, period.end), hidden);\n const titleRange = { start: columns[0], end: addDays(columns[columns.length - 1], 1) };\n\n function renderView() {\n switch (view) {\n case CalendarView.Week:\n case CalendarView.Day:\n return <TimeGrid {...viewProps} zoomRef={zoomRef} />;\n case CalendarView.Agenda: {\n return (\n <AgendaList\n events={shownEvents}\n start={period.start}\n days={dayDiff(period.start, period.end)}\n hidden={hidden}\n scrollTo={date}\n showDayHeaders\n locale={locale}\n text={text}\n onEventClick={onEventClick}\n renderEvent={renderEvent}\n classNames={classNames}\n slotStyles={slotStyles}\n />\n );\n }\n default:\n return compact ? <CompactMonth {...viewProps} /> : <MonthView {...viewProps} />;\n }\n }\n\n return (\n <div\n {...rest}\n ref={mergeRefs(rootRef, ref)}\n className={cx(styles.calendar, className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <CalendarHeader\n className={classNames?.header}\n style={slotStyles?.header}\n compact={compact}\n title={formatPeriodTitle(view, date, titleRange, locale)}\n direction={direction}\n view={view}\n views={views}\n text={text}\n onStep={(step) => changeDate(stepDate(view, date, step, hidden))}\n onToday={() => changeDate(new Date())}\n onViewChange={changeView}\n />\n {renderView()}\n </div>\n );\n}\n\nexport default Calendar;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"calendar.mjs","names":[],"sources":["../../../../package/common/calendar/calendar.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState, type ReactElement } from 'react';\nimport cx from 'classnames';\n\nimport { useComponentDefaults, useLabels } from '../configProvider';\nimport { mergeRefs } from '../utils/mergeRefs';\nimport { asDay, dayToDate } from '../utils/day';\nimport {\n CalendarView,\n type CalendarEvent,\n type CalendarEventChange,\n type CalendarLabels,\n type CalendarProps,\n} from './calendar.types';\nimport {\n dayDiff,\n dateToDay,\n addDays,\n hiddenDaySet,\n periodRange,\n rangeWithNeighbours,\n shownDay,\n shownDays,\n startOfDay,\n stepDate,\n} from './dates';\nimport { formatPeriodTitle, localeWeekStart } from './format';\nimport AgendaList from './components/agendaList';\nimport CalendarHeader from './components/calendarHeader';\nimport CompactMonth from './components/compactMonth';\nimport MonthView from './components/monthView';\nimport TimeGrid from './components/timeGrid';\nimport type { TimeGridZoom } from './components/timeGrid/timeGrid.types';\nimport type { CalendarViewProps } from './view.types';\n\nimport styles from './calendar.module.pcss';\n\nconst ALL_VIEWS = [CalendarView.Month, CalendarView.Week, CalendarView.Day, CalendarView.Agenda];\n\n/** Below this width the month view folds into the compact month with a day list. */\nconst COMPACT_WIDTH = 600;\n\nconst DEFAULT_LABELS: Required<CalendarLabels> = {\n previous: 'Previous',\n next: 'Next',\n today: 'Today',\n viewSwitcher: 'Calendar view',\n month: 'Month',\n week: 'Week',\n day: 'Day',\n agenda: 'Agenda',\n allDay: 'All day',\n more: (count) => `+${count} more`,\n eventCount: (count) => (count === 1 ? '1 event' : `${count} events`),\n noEvents: 'No events',\n newEvent: 'New event',\n};\n\nfunction Calendar<E extends CalendarEvent = CalendarEvent>(props: CalendarProps<E>): ReactElement {\n const {\n events,\n date: controlledDate,\n defaultDate,\n onDateChange,\n view: controlledView,\n defaultView = CalendarView.Month,\n onViewChange,\n views = ALL_VIEWS,\n locale,\n weekStartsOn: weekStartsOnProp,\n slotMinutes = 15,\n hiddenDays,\n scrollToHour,\n onRangeChange,\n onEventClick,\n onEventChange,\n onSlotCreate,\n renderEvent,\n labels,\n ref,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('Calendar', props);\n const text = useLabels('Calendar', labels, DEFAULT_LABELS);\n const weekStartsOn = weekStartsOnProp ?? localeWeekStart(locale);\n const hidden = hiddenDaySet(hiddenDays);\n\n const rootRef = useRef<HTMLDivElement>(null);\n const zoomRef = useRef<TimeGridZoom | null>(null);\n const [width, setWidth] = useState(0);\n const [direction, setDirection] = useState<1 | -1>(1);\n\n // `date` is a day, the same wherever the calendar is opened, and read as its first local moment.\n const [internalDate, setInternalDate] = useState(() => {\n const initial = asDay(defaultDate);\n return initial ? dayToDate(initial) : startOfDay(new Date());\n });\n const controlledDay = asDay(controlledDate);\n // Only ever a day the views show. The selection itself is left alone: a hidden day in `date` is\n // the parent's, and stepping on from it lands where the parent would expect.\n const date = shownDay(controlledDay ? dayToDate(controlledDay) : internalDate, hidden);\n const [internalView, setInternalView] = useState(defaultView);\n const view = controlledView ?? internalView;\n\n // The layout switch follows the calendar's own width rather than the viewport's, so it also\n // happens in a sidebar on a wide screen. Width 0 means no layout (server, jsdom): stay wide.\n useLayoutEffect(() => {\n const root = rootRef.current;\n if (!root) return;\n // The border box, both times: an observer's `contentRect` leaves the border out, and 600px\n // wide would then read as 598.\n const measure = () => setWidth(root.getBoundingClientRect().width);\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(root);\n return () => observer.disconnect();\n }, []);\n const compact = width > 0 && width < COMPACT_WIDTH;\n\n // Reported per range, not per render: `date` is a new object on every render of a controlled\n // parent. Not `useEffectEvent` for the latest callback, which React only has from 19.2.\n // An effect, since controlled props move the range too; `reported` keeps StrictMode's re-run\n // from reporting a range twice, which a consumer fetching in the callback pays for.\n const range = rangeWithNeighbours(view, date, weekStartsOn, hidden);\n const rangeKey = `${dateToDay(range.start)}/${dateToDay(range.end)}`;\n const latest = useRef({ range, onRangeChange });\n const reported = useRef<string | null>(null);\n useLayoutEffect(() => {\n latest.current = { range, onRangeChange };\n });\n useEffect(() => {\n if (reported.current === rangeKey) return;\n reported.current = rangeKey;\n latest.current.onRangeChange?.(latest.current.range);\n }, [rangeKey]);\n\n function changeDate(next: Date) {\n const day = startOfDay(next);\n if (dateToDay(day) === dateToDay(date)) return;\n setDirection(day > date ? 1 : -1);\n if (controlledDate === undefined) setInternalDate(day);\n onDateChange?.(dateToDay(day));\n }\n\n function changeView(next: CalendarView) {\n if (next === view) return;\n if (controlledView === undefined) setInternalView(next);\n onViewChange?.(next);\n }\n\n // Moves whose promise has not settled yet, drawn where they were dropped meanwhile.\n const [pending, setPending] = useState<ReadonlyMap<string, { start: Date; end: Date }>>(() => new Map());\n function changeEvent(change: CalendarEventChange<E>) {\n const result = onEventChange?.(change);\n if (typeof (result as Promise<unknown> | undefined)?.then !== 'function') return;\n const key = String(change.event.id);\n const entry = { start: change.start, end: change.end };\n setPending((current) => new Map(current).set(key, entry));\n // A later move of the same event owns the entry by then, and keeps it.\n const settle = () =>\n setPending((current) => {\n if (current.get(key) !== entry) return current;\n const next = new Map(current);\n next.delete(key);\n return next;\n });\n (result as Promise<unknown>).then(settle, settle);\n }\n const shownEvents =\n pending.size === 0\n ? events\n : events.map((event) => {\n const moved = pending.get(String(event.id));\n return moved ? { ...event, ...moved } : event;\n });\n\n const viewProps: CalendarViewProps<E> = {\n events: shownEvents,\n date,\n view,\n locale,\n weekStartsOn,\n slotMinutes,\n hidden,\n scrollToHour,\n text,\n onDateChange: changeDate,\n onOpenDay: views.includes(CalendarView.Day)\n ? (day) => {\n changeDate(day);\n changeView(CalendarView.Day);\n }\n : undefined,\n onEventClick,\n onEventChange: onEventChange && changeEvent,\n onSlotCreate,\n renderEvent,\n classNames,\n slotStyles,\n };\n\n // The week's title names the days it shows, not the ones it hides at either end.\n const period = periodRange(view, date, weekStartsOn);\n const columns = shownDays(period.start, dayDiff(period.start, period.end), hidden);\n const titleRange = { start: columns[0], end: addDays(columns[columns.length - 1], 1) };\n\n function renderView() {\n switch (view) {\n case CalendarView.Week:\n case CalendarView.Day:\n return <TimeGrid {...viewProps} zoomRef={zoomRef} />;\n case CalendarView.Agenda: {\n return (\n <AgendaList\n events={shownEvents}\n start={period.start}\n days={dayDiff(period.start, period.end)}\n hidden={hidden}\n scrollTo={date}\n showDayHeaders\n locale={locale}\n text={text}\n onEventClick={onEventClick}\n renderEvent={renderEvent}\n classNames={classNames}\n slotStyles={slotStyles}\n />\n );\n }\n default:\n return compact ? <CompactMonth {...viewProps} /> : <MonthView {...viewProps} />;\n }\n }\n\n return (\n <div\n {...rest}\n ref={mergeRefs(rootRef, ref)}\n className={cx(styles.calendar, className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <CalendarHeader\n className={classNames?.header}\n style={slotStyles?.header}\n compact={compact}\n title={formatPeriodTitle(view, date, titleRange, locale)}\n direction={direction}\n view={view}\n views={views}\n text={text}\n onStep={(step) => changeDate(stepDate(view, date, step, hidden))}\n onToday={() => changeDate(new Date())}\n onViewChange={changeView}\n />\n {renderView()}\n </div>\n );\n}\n\nexport default Calendar;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fab.module.mjs","names":[],"sources":["../../../../package/common/fab/fab.module.pcss"],"sourcesContent":[".fab {\n animation: appear 0.3s forwards;\n opacity: 0;\n transform: scale(0);\n box-shadow:\n 0 3px 5px -1px rgb(0 0 0 / 20%),\n 0 6px 10px 0 rgb(0 0 0 / 14%),\n 0 1px 18px 0 rgb(0 0 0 / 12%);\n border-radius: 50%;\n\n &-container {\n position: fixed;\n bottom: 16px;\n right: 16px;\n
|
|
1
|
+
{"version":3,"file":"fab.module.mjs","names":[],"sources":["../../../../package/common/fab/fab.module.pcss"],"sourcesContent":[".fab {\n animation: appear 0.3s forwards;\n opacity: 0;\n transform: scale(0);\n box-shadow:\n 0 3px 5px -1px rgb(0 0 0 / 20%),\n 0 6px 10px 0 rgb(0 0 0 / 14%),\n 0 1px 18px 0 rgb(0 0 0 / 12%);\n border-radius: 50%;\n\n &-container {\n position: fixed;\n bottom: 16px;\n right: 16px;\n }\n\n &-menu {\n position: absolute;\n top: 0;\n transform: translateY(calc(-100% - 16px));\n width: 100%;\n display: flex;\n flex-flow: column-reverse;\n gap: 16px;\n align-items: center;\n pointer-events: none;\n user-select: none;\n\n &-backdrop {\n width: 0%;\n height: 0%;\n position: fixed;\n top: 0;\n left: 0;\n background: black;\n opacity: 0;\n transition: opacity 0.4s;\n\n &-open {\n width: 100%;\n height: 100%;\n opacity: 0.5;\n }\n }\n\n &-entry {\n position: relative;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n opacity: 0;\n transform: scale(0);\n transition: all 0.2s;\n pointer-events: all;\n\n &:nth-child(2) {\n transition-delay: 0.05s;\n }\n\n &:nth-child(3) {\n transition-delay: 0.1s;\n }\n\n &:nth-child(4) {\n transition-delay: 0.15s;\n }\n\n &:nth-child(5) {\n transition-delay: 0.2s;\n }\n\n &:nth-child(6) {\n transition-delay: 0.25s;\n }\n\n &:nth-child(7) {\n transition-delay: 0.3s;\n }\n\n &-shown {\n opacity: 1;\n transform: scale(1);\n }\n\n &-label {\n background: var(--background-accent-light);\n border-radius: var(--default-border-radius);\n padding: 4px 8px;\n\n &-container {\n position: absolute;\n width: max-content;\n transform: translateX(-100%);\n height: 100%;\n margin-left: -8px;\n align-items: center;\n display: flex;\n }\n }\n }\n }\n\n &-icon {\n transition: all 0.4s;\n display: flex;\n justify-content: center;\n align-items: center;\n position: relative;\n width: 100%;\n height: 100%;\n\n &-menuOpen {\n transform: rotate(225deg);\n }\n\n &-content {\n position: absolute;\n width: var(--custom-fab-icon-size);\n height: var(--custom-fab-icon-size);\n display: flex;\n align-items: center;\n justify-content: center;\n opacity: 0;\n transition: opacity 0.3s;\n\n &-shown {\n opacity: 1;\n }\n }\n }\n\n @keyframes appear {\n to {\n opacity: 1;\n transform: scale(1);\n }\n }\n}\n\n/* `.fab` is opacity 0 and scale 0 until `appear` runs, so removing the animation has to put it\n at the end state by hand or the button never shows up at all. */\n@media (prefers-reduced-motion: reduce) {\n .fab {\n animation: none;\n opacity: 1;\n transform: none;\n }\n\n .fab-menu-entry {\n transition: none;\n }\n}\n"],"mappings":""}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use client";import{getScrollableAncestors as e}from"../utils/scrollAncestors.mjs";import{useLayoutEffect as t,useRef as n}from"react";function r({isOpen:r,triggerRef:i,elementRefs:a,update:o,resizeRefs:s,teardown:c,deps:l=[]}){let u=n({elementRefs:a,update:o,resizeRefs:s,teardown:c});u.current={elementRefs:a,update:o,resizeRefs:s,teardown:c},t(()=>{if(!r)return;let t,n;function a(){let r=i.current,o=u.current.elementRefs;if(!r||o.some(e=>!e.current)){t=requestAnimationFrame(a);return}let s=e(r),c=new ResizeObserver(()=>
|
|
1
|
+
"use client";import{getScrollableAncestors as e}from"../utils/scrollAncestors.mjs";import{useLayoutEffect as t,useRef as n}from"react";function r({isOpen:r,triggerRef:i,elementRefs:a,update:o,resizeRefs:s,teardown:c,deps:l=[]}){let u=n({elementRefs:a,update:o,resizeRefs:s,teardown:c});u.current={elementRefs:a,update:o,resizeRefs:s,teardown:c},t(()=>{if(!r)return;let t,n;function a(){let r=i.current,o=u.current.elementRefs;if(!r||o.some(e=>!e.current)){t=requestAnimationFrame(a);return}let s=e(r),c=new ResizeObserver(()=>f()),l=new Set,d=!0;function f(){d&&(u.current.update(r,s,f),u.current.resizeRefs?.forEach(({current:e})=>{e&&!l.has(e)&&(l.add(e),c.observe(e))}))}f();let p=[window,...s];p.forEach(e=>e.addEventListener(`scroll`,f,{passive:!0})),window.addEventListener(`resize`,f),window.visualViewport?.addEventListener(`resize`,f),window.visualViewport?.addEventListener(`scroll`,f);function m(e){e.target.contains(r)&&f()}document.addEventListener(`transitionend`,m,!0),document.addEventListener(`animationend`,m,!0),n=()=>{d=!1,p.forEach(e=>e.removeEventListener(`scroll`,f)),window.removeEventListener(`resize`,f),window.visualViewport?.removeEventListener(`resize`,f),window.visualViewport?.removeEventListener(`scroll`,f),document.removeEventListener(`transitionend`,m,!0),document.removeEventListener(`animationend`,m,!0),c.disconnect(),u.current.teardown?.()}}return a(),()=>{t!==void 0&&cancelAnimationFrame(t),n?.()}},[r,i,...l])}export{r as useAnchorTracking};
|
|
2
2
|
//# sourceMappingURL=useAnchorTracking.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAnchorTracking.mjs","names":[],"sources":["../../../../package/common/hooks/useAnchorTracking.ts"],"sourcesContent":["import { useLayoutEffect, useRef, type RefObject } from 'react';\nimport { getScrollableAncestors } from 'components/common/utils/scrollAncestors';\n\nexport interface UseAnchorTrackingArgs {\n isOpen: boolean;\n /** The element the popup is anchored to. Its scrollable ancestors are what get tracked. */\n triggerRef: RefObject<HTMLElement | null>;\n /**\n * Every element the positioning maths needs. Tracking starts only once all of them, plus the\n * trigger, are mounted. Read fresh on each setup, so an inline array literal is fine.\n */\n elementRefs: RefObject<HTMLElement | null>[];\n /**\n * Positions the popup. Called once on setup and then on every scroll, resize
|
|
1
|
+
{"version":3,"file":"useAnchorTracking.mjs","names":[],"sources":["../../../../package/common/hooks/useAnchorTracking.ts"],"sourcesContent":["import { useLayoutEffect, useRef, type RefObject } from 'react';\nimport { getScrollableAncestors } from 'components/common/utils/scrollAncestors';\n\nexport interface UseAnchorTrackingArgs {\n isOpen: boolean;\n /** The element the popup is anchored to. Its scrollable ancestors are what get tracked. */\n triggerRef: RefObject<HTMLElement | null>;\n /**\n * Every element the positioning maths needs. Tracking starts only once all of them, plus the\n * trigger, are mounted. Read fresh on each setup, so an inline array literal is fine.\n */\n elementRefs: RefObject<HTMLElement | null>[];\n /**\n * Positions the popup. Called once on setup and then on every scroll, resize, tracked size\n * change and end of a CSS animation that moved the trigger, plus whenever it calls `rerun` for\n * an end none of those report. Reads the caller's own refs for everything else.\n */\n update: (trigger: HTMLElement, ancestors: HTMLElement[], rerun: () => void) => void;\n /**\n * Elements whose own size changes must re-run `update`: a popup whose content changes size\n * while open (a filtered list shrinking, an async list loading in) otherwise keeps coordinates\n * computed for its old height and ends up detached from the trigger. Re-read on every update,\n * since a panel can mount a commit later than the element it hangs off.\n */\n resizeRefs?: RefObject<HTMLElement | null>[];\n /** Hands the popup's elements back as they were found. Runs when tracking stops. */\n teardown?: () => void;\n /** Values that must restart tracking, and so re-run `update`, when they change. */\n deps?: readonly unknown[];\n}\n\n/**\n * The lifecycle half of anchoring a portaled, `position: fixed` popup to a trigger: waits for the\n * elements to mount, resolves the trigger's scrolling ancestors once, and re-runs the caller's\n * positioning on every scroll, viewport resize and tracked size change until it closes.\n *\n * Callers supply only the geometry (`update`) and the inline styles to clear again (`teardown`);\n * `usePopoverPosition` is built on this.\n *\n * Position is deliberately written straight to the DOM by the caller rather than through React\n * state: going through setState -> re-render -> commit adds a round-trip that lags behind the\n * browser's own scroll painting by at least a frame.\n */\nexport function useAnchorTracking({\n isOpen,\n triggerRef,\n elementRefs,\n update,\n resizeRefs,\n teardown,\n deps = [],\n}: UseAnchorTrackingArgs) {\n // Latest-value refs, so the effect restarts only for `deps` the caller names, never merely\n // because these were passed as fresh array/function literals this render.\n const latest = useRef({ elementRefs, update, resizeRefs, teardown });\n latest.current = { elementRefs, update, resizeRefs, teardown };\n\n useLayoutEffect(() => {\n if (!isOpen) return;\n\n let rafId: number | undefined;\n let stop: (() => void) | undefined;\n\n // The elements may not be mounted yet on the very first invocation of this effect: a portal\n // target can still be missing (a `#bodyEnd` the app renders, or any while hydrating), and then\n // the portaled content and its ref commits one render later. Since refs never change\n // identity, that later commit doesn't re-trigger this effect on its own, so retry each frame.\n function trySetup() {\n const trigger = triggerRef.current;\n const elements = latest.current.elementRefs;\n if (!trigger || elements.some((ref) => !ref.current)) {\n rafId = requestAnimationFrame(trySetup);\n return;\n }\n const ancestors = getScrollableAncestors(trigger);\n\n const resizeObserver = new ResizeObserver(() => runUpdate());\n const observed = new Set<Element>();\n\n let active = true;\n function runUpdate() {\n // `rerun` may be called after tracking stopped, from a promise that settled late.\n if (!active) return;\n latest.current.update(trigger!, ancestors, runUpdate);\n // After the update, so an element that only mounts during it is picked up too. Once\n // observed this is a handful of Set lookups, so it costs nothing on the scroll path.\n latest.current.resizeRefs?.forEach(({ current }) => {\n if (!current || observed.has(current)) return;\n observed.add(current);\n resizeObserver.observe(current);\n });\n }\n\n runUpdate();\n\n const scrollTargets: (Window | HTMLElement)[] = [window, ...ancestors];\n scrollTargets.forEach((t) => t.addEventListener('scroll', runUpdate, { passive: true }));\n window.addEventListener('resize', runUpdate);\n // `window.resize` doesn't reliably fire when a mobile keyboard opens/closes — visualViewport\n // does, and is what the callers' flip/clamp maths is measured against.\n window.visualViewport?.addEventListener('resize', runUpdate);\n window.visualViewport?.addEventListener('scroll', runUpdate);\n // A transform on an ancestor moves the trigger without any of the events above: a popup\n // opened as a Modal scales in was placed, and hidden, against rects collapsed at scale(0).\n function onAncestorMotionEnd(event: Event) {\n if ((event.target as Node).contains(trigger)) runUpdate();\n }\n document.addEventListener('transitionend', onAncestorMotionEnd, true);\n document.addEventListener('animationend', onAncestorMotionEnd, true);\n\n stop = () => {\n active = false;\n scrollTargets.forEach((t) => t.removeEventListener('scroll', runUpdate));\n window.removeEventListener('resize', runUpdate);\n window.visualViewport?.removeEventListener('resize', runUpdate);\n window.visualViewport?.removeEventListener('scroll', runUpdate);\n document.removeEventListener('transitionend', onAncestorMotionEnd, true);\n document.removeEventListener('animationend', onAncestorMotionEnd, true);\n resizeObserver.disconnect();\n latest.current.teardown?.();\n };\n }\n\n trySetup();\n\n return () => {\n if (rafId !== undefined) cancelAnimationFrame(rafId);\n stop?.();\n };\n // Spread rather than listed: `deps` is the caller's own set of geometry inputs, and changing\n // any of them has to restart tracking so the new values get applied immediately.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen, triggerRef, ...deps]);\n}\n"],"mappings":"uIA2CA,SAAgB,EAAkB,CAChC,SACA,aACA,cACA,SACA,aACA,WACA,OAAO,CAAC,GACgB,CAGxB,IAAM,EAAS,EAAO,CAAE,cAAa,SAAQ,aAAY,UAAS,CAAC,EACnE,EAAO,QAAU,CAAE,cAAa,SAAQ,aAAY,UAAS,EAE7D,MAAsB,CACpB,GAAI,CAAC,EAAQ,OAEb,IAAI,EACA,EAMJ,SAAS,GAAW,CAClB,IAAM,EAAU,EAAW,QACrB,EAAW,EAAO,QAAQ,YAChC,GAAI,CAAC,GAAW,EAAS,KAAM,GAAQ,CAAC,EAAI,OAAO,EAAG,CACpD,EAAQ,sBAAsB,CAAQ,EACtC,MACF,CACA,IAAM,EAAY,EAAuB,CAAO,EAE1C,EAAiB,IAAI,mBAAqB,EAAU,CAAC,EACrD,EAAW,IAAI,IAEjB,EAAS,GACb,SAAS,GAAY,CAEd,IACL,EAAO,QAAQ,OAAO,EAAU,EAAW,CAAS,EAGpD,EAAO,QAAQ,YAAY,SAAS,CAAE,aAAc,CAC7C,GAAW,GAAS,IAAI,CAAO,IACpC,EAAS,IAAI,CAAO,EACpB,EAAe,QAAQ,CAAO,EAChC,CAAC,EACH,CAEA,EAAU,EAEV,IAAM,EAA0C,CAAC,OAAQ,GAAG,CAAS,EACrE,EAAc,QAAS,GAAM,EAAE,iBAAiB,SAAU,EAAW,CAAE,QAAS,EAAK,CAAC,CAAC,EACvF,OAAO,iBAAiB,SAAU,CAAS,EAG3C,OAAO,gBAAgB,iBAAiB,SAAU,CAAS,EAC3D,OAAO,gBAAgB,iBAAiB,SAAU,CAAS,EAG3D,SAAS,EAAoB,EAAc,CACpC,EAAM,OAAgB,SAAS,CAAO,GAAG,EAAU,CAC1D,CACA,SAAS,iBAAiB,gBAAiB,EAAqB,EAAI,EACpE,SAAS,iBAAiB,eAAgB,EAAqB,EAAI,EAEnE,MAAa,CACX,EAAS,GACT,EAAc,QAAS,GAAM,EAAE,oBAAoB,SAAU,CAAS,CAAC,EACvE,OAAO,oBAAoB,SAAU,CAAS,EAC9C,OAAO,gBAAgB,oBAAoB,SAAU,CAAS,EAC9D,OAAO,gBAAgB,oBAAoB,SAAU,CAAS,EAC9D,SAAS,oBAAoB,gBAAiB,EAAqB,EAAI,EACvE,SAAS,oBAAoB,eAAgB,EAAqB,EAAI,EACtE,EAAe,WAAW,EAC1B,EAAO,QAAQ,WAAW,CAC5B,CACF,CAIA,OAFA,EAAS,MAEI,CACP,IAAU,IAAA,IAAW,qBAAqB,CAAK,EACnD,IAAO,CACT,CAIF,EAAG,CAAC,EAAQ,EAAY,GAAG,CAAI,CAAC,CAClC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use client";var e=[`a[href]`,`area[href]`,`button:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`textarea:not([disabled])`,`[tabindex]:not([tabindex="-1"])`,`[contenteditable="true"]`].join(`,`);function t(t){return t?Array.from(t.querySelectorAll(e)).filter(e=>e.getAttribute(`tabindex`)!==`-1`):[]}function n(t){return t.matches(e)&&t.getAttribute(`tabindex`)!==`-1`}function r({triggerRef:e,contentRef:r}){function i(){let r=e.current;return r&&n(r)?r:t(r)[0]??r}function a(){i()?.focus()}function o(){let e=t(document.body).filter(e=>!r.current?.contains(e)),
|
|
1
|
+
"use client";var e=[`a[href]`,`area[href]`,`button:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`textarea:not([disabled])`,`[tabindex]:not([tabindex="-1"])`,`[contenteditable="true"]`].join(`,`);function t(t){return t?Array.from(t.querySelectorAll(e)).filter(e=>e.getAttribute(`tabindex`)!==`-1`):[]}function n(t){return t.matches(e)&&t.getAttribute(`tabindex`)!==`-1`}function r({triggerRef:e,contentRef:r}){function i(){let r=e.current;return r&&n(r)?r:t(r)[0]??r}function a(){i()?.focus()}function o(){let e=i(),n=e?.closest(`[aria-modal="true"]`),a=t(n??document.body).filter(e=>!r.current?.contains(e)),o=e?a.indexOf(e):-1;o<0||(a[o+1]??(n?a[0]:void 0))?.focus()}return{firstTriggerFocusable:i,restoreFocusToTrigger:a,focusAfterTrigger:o}}export{t as tabbablesIn,r as useFocusBoundary};
|
|
2
2
|
//# sourceMappingURL=useFocusBoundary.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useFocusBoundary.mjs","names":[],"sources":["../../../../package/common/hooks/useFocusBoundary.ts"],"sourcesContent":["import type { RefObject } from 'react';\n\nconst FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n '[contenteditable=\"true\"]',\n].join(',');\n\n/** Tabbable descendants of `root`, in DOM order. */\nexport function tabbablesIn(root: ParentNode | null): HTMLElement[] {\n if (!root) return [];\n return (\n Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))\n // A button or a link can be taken out of the tab order too, which the\n // selector above only catches for elements whose focusability comes from\n // the attribute. A roving tabindex, where one item of a list is the tab\n // stop and the rest are reachable by arrow key, otherwise reads as a list\n // of tab stops, and the Tab boundary lands on the wrong entry.\n .filter((el) => el.getAttribute('tabindex') !== '-1')\n );\n}\n\nfunction isTabbable(element: HTMLElement): boolean {\n return element.matches(FOCUSABLE_SELECTOR) && element.getAttribute('tabindex') !== '-1';\n}\n\nexport interface UseFocusBoundaryArgs {\n /** The trigger element focus returns to on Escape / Shift+Tab out of the content's top. */\n triggerRef: RefObject<HTMLElement | null>;\n /** The portaled content root — tabbables inside it define the Tab boundary. */\n contentRef: RefObject<HTMLElement | null>;\n}\n\n/**\n * Focus helpers for content portaled to the end of the DOM (e.g. via BodyEnd), where Tab can't\n * reach it in visual document order on its own. Used by Popover and by Menu, which both need to\n * hand focus back to the trigger on Escape / Shift+Tab out of the top, and continue focus\n * naturally past the trigger when Tab exits the bottom.\n */\nexport function useFocusBoundary({ triggerRef, contentRef }: UseFocusBoundaryArgs) {\n function firstTriggerFocusable(): HTMLElement | null {\n const trigger = triggerRef.current;\n // The trigger itself where it takes focus, as an element handed to Popover's `anchor` can;\n // else the first element inside it that does, as inside Popover's and Menu's own wrappers.\n if (trigger && isTabbable(trigger)) return trigger;\n return tabbablesIn(trigger)[0] ?? trigger;\n }\n\n function restoreFocusToTrigger() {\n firstTriggerFocusable()?.focus();\n }\n\n // Move focus to the tabbable element that follows the trigger in the document, skipping the\n // portaled content — the \"natural next\" stop when Tab exits the bottom of the content.\n function focusAfterTrigger() {\n const all = tabbablesIn(document.body).filter((el) => !contentRef.current?.contains(el));\n const
|
|
1
|
+
{"version":3,"file":"useFocusBoundary.mjs","names":[],"sources":["../../../../package/common/hooks/useFocusBoundary.ts"],"sourcesContent":["import type { RefObject } from 'react';\n\nconst FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n '[contenteditable=\"true\"]',\n].join(',');\n\n/** Tabbable descendants of `root`, in DOM order. */\nexport function tabbablesIn(root: ParentNode | null): HTMLElement[] {\n if (!root) return [];\n return (\n Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))\n // A button or a link can be taken out of the tab order too, which the\n // selector above only catches for elements whose focusability comes from\n // the attribute. A roving tabindex, where one item of a list is the tab\n // stop and the rest are reachable by arrow key, otherwise reads as a list\n // of tab stops, and the Tab boundary lands on the wrong entry.\n .filter((el) => el.getAttribute('tabindex') !== '-1')\n );\n}\n\nfunction isTabbable(element: HTMLElement): boolean {\n return element.matches(FOCUSABLE_SELECTOR) && element.getAttribute('tabindex') !== '-1';\n}\n\nexport interface UseFocusBoundaryArgs {\n /** The trigger element focus returns to on Escape / Shift+Tab out of the content's top. */\n triggerRef: RefObject<HTMLElement | null>;\n /** The portaled content root — tabbables inside it define the Tab boundary. */\n contentRef: RefObject<HTMLElement | null>;\n}\n\n/**\n * Focus helpers for content portaled to the end of the DOM (e.g. via BodyEnd), where Tab can't\n * reach it in visual document order on its own. Used by Popover and by Menu, which both need to\n * hand focus back to the trigger on Escape / Shift+Tab out of the top, and continue focus\n * naturally past the trigger when Tab exits the bottom.\n */\nexport function useFocusBoundary({ triggerRef, contentRef }: UseFocusBoundaryArgs) {\n function firstTriggerFocusable(): HTMLElement | null {\n const trigger = triggerRef.current;\n // The trigger itself where it takes focus, as an element handed to Popover's `anchor` can;\n // else the first element inside it that does, as inside Popover's and Menu's own wrappers.\n if (trigger && isTabbable(trigger)) return trigger;\n return tabbablesIn(trigger)[0] ?? trigger;\n }\n\n function restoreFocusToTrigger() {\n firstTriggerFocusable()?.focus();\n }\n\n // Move focus to the tabbable element that follows the trigger in the document, skipping the\n // portaled content — the \"natural next\" stop when Tab exits the bottom of the content.\n function focusAfterTrigger() {\n const trig = firstTriggerFocusable();\n // Inside a Modal the next stop is the Modal's, wrapping as its trap does: past its last\n // element lies the page behind it, and the focus stayed on the panel being removed.\n const modal = trig?.closest<HTMLElement>('[aria-modal=\"true\"]');\n const all = tabbablesIn(modal ?? document.body).filter((el) => !contentRef.current?.contains(el));\n const idx = trig ? all.indexOf(trig) : -1;\n if (idx < 0) return;\n (all[idx + 1] ?? (modal ? all[0] : undefined))?.focus();\n }\n\n return { firstTriggerFocusable, restoreFocusToTrigger, focusAfterTrigger };\n}\n"],"mappings":"aAEA,IAAM,EAAqB,CACzB,UACA,aACA,yBACA,wBACA,yBACA,2BACA,kCACA,0BACF,CAAC,CAAC,KAAK,GAAG,EAGV,SAAgB,EAAY,EAAwC,CAElE,OADK,EAEH,MAAM,KAAK,EAAK,iBAA8B,CAAkB,CAAC,CAAC,CAM/D,OAAQ,GAAO,EAAG,aAAa,UAAU,IAAM,IAAI,EARtC,CAAC,CAUrB,CAEA,SAAS,EAAW,EAA+B,CACjD,OAAO,EAAQ,QAAQ,CAAkB,GAAK,EAAQ,aAAa,UAAU,IAAM,IACrF,CAeA,SAAgB,EAAiB,CAAE,aAAY,cAAoC,CACjF,SAAS,GAA4C,CACnD,IAAM,EAAU,EAAW,QAI3B,OADI,GAAW,EAAW,CAAO,EAAU,EACpC,EAAY,CAAO,CAAC,CAAC,IAAM,CACpC,CAEA,SAAS,GAAwB,CAC/B,EAAsB,CAAC,EAAE,MAAM,CACjC,CAIA,SAAS,GAAoB,CAC3B,IAAM,EAAO,EAAsB,EAG7B,EAAQ,GAAM,QAAqB,qBAAqB,EACxD,EAAM,EAAY,GAAS,SAAS,IAAI,CAAC,CAAC,OAAQ,GAAO,CAAC,EAAW,SAAS,SAAS,CAAE,CAAC,EAC1F,EAAM,EAAO,EAAI,QAAQ,CAAI,EAAI,GACnC,EAAM,IACT,EAAI,EAAM,KAAO,EAAQ,EAAI,GAAK,IAAA,IAAA,EAAa,MAAM,CACxD,CAEA,MAAO,CAAE,wBAAuB,wBAAuB,mBAAkB,CAC3E"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use client";import{tabbablesIn as e}from"./useFocusBoundary.mjs";import{useEffect as t,useRef as n}from"react";function r({isActive:r,containerRef:o,onEscape:s,trapFocus:c=!0,lockScroll:d=!0}){let f=n(s);t(()=>{f.current=s});let p=n(0);r&&p.current===0&&(p.current=i++),!r&&p.current!==0&&(p.current=0),t(()=>{if(!r)return;let t=p.current;a.
|
|
1
|
+
"use client";import{tabbablesIn as e}from"./useFocusBoundary.mjs";import{useEffect as t,useRef as n}from"react";function r({isActive:r,containerRef:o,onEscape:s,trapFocus:c=!0,lockScroll:d=!0}){let f=n(s);t(()=>{f.current=s});let p=n(0);r&&p.current===0&&(p.current=i++),!r&&p.current!==0&&(p.current=0),t(()=>{if(!r)return;let t=p.current;a.set(t,{trapFocus:c,containerRef:o});let n=()=>t===Math.max(...a.keys()),i=()=>[...a].filter(([e])=>e>t).map(([,e])=>e),s=document.activeElement;if(c){let t=o.current;(e(t)[0]??t)?.focus()}function m(t){if(t.key===`Escape`&&f.current&&n()){t.preventDefault(),f.current();return}if(t.key!==`Tab`||!c||t.defaultPrevented)return;let r=i();if(r.some(e=>e.trapFocus)||r.some(e=>e.containerRef.current?.contains(document.activeElement)))return;let a=e(o.current);if(a.length===0){t.preventDefault();return}let s=a[0],l=a[a.length-1],u=document.activeElement;o.current?.contains(u)?!t.shiftKey&&u===l?(t.preventDefault(),s.focus()):t.shiftKey&&u===s&&(t.preventDefault(),l.focus()):(t.preventDefault(),s.focus())}let h=new Set,g=e=>e.code||e.key;function _(e){e.repeat?!h.has(g(e))&&o.current?.contains(e.target)&&(e.preventDefault(),e.stopPropagation()):h.add(g(e))}function v(e){h.delete(g(e))}return window.addEventListener(`keydown`,m),window.addEventListener(`keydown`,_,!0),window.addEventListener(`keyup`,v,!0),d&&l(),()=>{window.removeEventListener(`keydown`,m),window.removeEventListener(`keydown`,_,!0),window.removeEventListener(`keyup`,v,!0),d&&u();let e=i().length>0;a.delete(t),c&&!e&&s?.focus()}},[r,c,d,o])}var i=1,a=new Map,o=0,s=``,c=``;function l(){let{body:e}=document;if(o===0){s=e.style.overflow,c=e.style.paddingRight;let t=window.innerWidth-document.documentElement.clientWidth;if(t>0){let n=parseFloat(window.getComputedStyle(e).paddingRight)||0;e.style.paddingRight=`${n+t}px`}e.style.overflow=`hidden`}o+=1}function u(){--o,!(o>0)&&(o=0,document.body.style.overflow=s,document.body.style.paddingRight=c)}export{r as useOverlay};
|
|
2
2
|
//# sourceMappingURL=useOverlay.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useOverlay.mjs","names":[],"sources":["../../../../package/common/hooks/useOverlay.ts"],"sourcesContent":["import { useEffect, useRef, type RefObject } from 'react';\nimport { tabbablesIn } from './useFocusBoundary';\n\nexport interface UseOverlayArgs {\n /** Whether the overlay is currently visible. Everything below is set up while this is true. */\n isActive: boolean;\n /** The overlay panel. Focus is trapped inside it and Escape is scoped to it. */\n containerRef: RefObject<HTMLElement | null>;\n /** Called when Escape is pressed while this is the topmost overlay. Omit to not handle Escape. */\n onEscape?: () => void;\n
|
|
1
|
+
{"version":3,"file":"useOverlay.mjs","names":[],"sources":["../../../../package/common/hooks/useOverlay.ts"],"sourcesContent":["import { useEffect, useRef, type RefObject } from 'react';\nimport { tabbablesIn } from './useFocusBoundary';\n\nexport interface UseOverlayArgs {\n /** Whether the overlay is currently visible. Everything below is set up while this is true. */\n isActive: boolean;\n /** The overlay panel. Focus is trapped inside it and Escape is scoped to it. */\n containerRef: RefObject<HTMLElement | null>;\n /** Called when Escape is pressed while this is the topmost overlay. Omit to not handle Escape. */\n onEscape?: () => void;\n /**\n * Keep Tab inside the panel and restore focus to the previously focused element on close, unless\n * an overlay opened after this one is still open.\n */\n trapFocus?: boolean;\n /** Prevent the page behind the overlay from scrolling. */\n lockScroll?: boolean;\n}\n\n/**\n * The three behaviours every blocking overlay needs: Escape to dismiss, a focus trap, and a\n * scroll lock on the page behind it.\n *\n * They share one module-level stack because they share one question: overlays nest (a\n * ConfirmModal opened from inside a Modal), every instance listens on `document`, and only the\n * innermost one may react. Without the stack, Escape would close every open layer at once and\n * the outer trap would yank focus out of the inner panel on the next Tab.\n *\n * The scroll lock is reference-counted for the same reason: the page stays locked until the last\n * overlay closes, and the body's original inline styles are restored rather than cleared.\n */\nexport function useOverlay({ isActive, containerRef, onEscape, trapFocus = true, lockScroll = true }: UseOverlayArgs) {\n const onEscapeRef = useRef(onEscape);\n useEffect(() => {\n onEscapeRef.current = onEscape;\n });\n\n // The layer's position in the stack is claimed during render, not in the effect below, because\n // effects run child-first: an overlay opened from *inside* another one (a menu in a modal) has\n // its effect run before its own parent's and would come out as the lower layer. Render runs\n // parent-first, which is the order the layers are actually nested in. DOM order is no help\n // either — React commits the inner portal ahead of the outer one.\n const orderRef = useRef(0);\n if (isActive && orderRef.current === 0) orderRef.current = nextLayerOrder++;\n if (!isActive && orderRef.current !== 0) orderRef.current = 0;\n\n useEffect(() => {\n if (!isActive) return;\n\n const order = orderRef.current;\n activeLayers.set(order, { trapFocus, containerRef });\n const isTopmost = () => order === Math.max(...activeLayers.keys());\n const layersAbove = () => [...activeLayers].filter(([other]) => other > order).map(([, layer]) => layer);\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n if (trapFocus) {\n const container = containerRef.current;\n (tabbablesIn(container)[0] ?? container)?.focus();\n }\n\n function onKeyDown(event: KeyboardEvent) {\n if (event.key === 'Escape' && onEscapeRef.current && isTopmost()) {\n event.preventDefault();\n onEscapeRef.current();\n return;\n }\n\n if (event.key !== 'Tab' || !trapFocus || event.defaultPrevented) return;\n // A layer that does not trap, a Popover, leaves Tab to the trap below it: with nothing to\n // focus in its panel, Tab otherwise left the Modal for the browser's own controls. Tab\n // inside it stays its own, and so does the move it already made out of either end.\n const above = layersAbove();\n if (above.some((layer) => layer.trapFocus)) return;\n if (above.some((layer) => layer.containerRef.current?.contains(document.activeElement))) return;\n const focusables = tabbablesIn(containerRef.current);\n // Nothing to move to inside the panel, but Tab must still not escape it.\n if (focusables.length === 0) {\n event.preventDefault();\n return;\n }\n const first = focusables[0];\n const last = focusables[focusables.length - 1];\n const active = document.activeElement;\n if (!containerRef.current?.contains(active)) {\n event.preventDefault();\n first.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first.focus();\n } else if (event.shiftKey && active === first) {\n event.preventDefault();\n last.focus();\n }\n }\n\n // A key held as this opened would repeat into what it focused: a held Enter pressed Close.\n // Outside the panel it still repeats, since suggestions open while the user types.\n const pressedSinceOpen = new Set<string>();\n const keyId = (event: KeyboardEvent) => event.code || event.key;\n function onKeyDownCapture(event: KeyboardEvent) {\n if (!event.repeat) pressedSinceOpen.add(keyId(event));\n else if (!pressedSinceOpen.has(keyId(event)) && containerRef.current?.contains(event.target as Node)) {\n event.preventDefault();\n event.stopPropagation();\n }\n }\n function onKeyUp(event: KeyboardEvent) {\n pressedSinceOpen.delete(keyId(event));\n }\n\n // `window`, not `document`: a key event on a focused element reaches both, but a synthetic\n // one dispatched straight at `window` (as tests and some consumers do) never reaches\n // `document`, since it sits above it in the propagation path.\n window.addEventListener('keydown', onKeyDown);\n window.addEventListener('keydown', onKeyDownCapture, true);\n window.addEventListener('keyup', onKeyUp, true);\n if (lockScroll) acquireScrollLock();\n\n return () => {\n window.removeEventListener('keydown', onKeyDown);\n window.removeEventListener('keydown', onKeyDownCapture, true);\n window.removeEventListener('keyup', onKeyUp, true);\n if (lockScroll) releaseScrollLock();\n // Modal closes a commit late, so a Popover opened by the same click already holds the focus.\n const coveredByLaterLayer = layersAbove().length > 0;\n activeLayers.delete(order);\n if (trapFocus && !coveredByLaterLayer) previouslyFocused?.focus();\n };\n }, [isActive, trapFocus, lockScroll, containerRef]);\n}\n\n/** Monotonic, so a layer opened later always outranks one opened earlier. */\nlet nextLayerOrder = 1;\nconst activeLayers = new Map<number, { trapFocus: boolean; containerRef: RefObject<HTMLElement | null> }>();\n\nlet scrollLockCount = 0;\nlet previousOverflow = '';\nlet previousPaddingRight = '';\n\nfunction acquireScrollLock() {\n const { body } = document;\n if (scrollLockCount === 0) {\n previousOverflow = body.style.overflow;\n previousPaddingRight = body.style.paddingRight;\n // Removing the page's scrollbar reclaims its width, so everything behind the overlay would\n // shift right by that amount without the compensating padding.\n const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;\n if (scrollbarWidth > 0) {\n const current = parseFloat(window.getComputedStyle(body).paddingRight) || 0;\n body.style.paddingRight = `${current + scrollbarWidth}px`;\n }\n body.style.overflow = 'hidden';\n }\n scrollLockCount += 1;\n}\n\nfunction releaseScrollLock() {\n scrollLockCount -= 1;\n if (scrollLockCount > 0) return;\n scrollLockCount = 0;\n document.body.style.overflow = previousOverflow;\n document.body.style.paddingRight = previousPaddingRight;\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{useLabels as t}from"../configProvider/useLabels.mjs";import{usePresentationDefault as n}from"../configProvider/usePresentationDefault.mjs";import r from"../bodyEnd/bodyEnd.mjs";import i from"../styles/sheet.module.mjs";import{useVisualViewportSize as a}from"../hooks/useVisualViewportSize.mjs";import o from"../hooks/useSheet.mjs";import{useOverlay as s}from"../hooks/useOverlay.mjs";import c from"../styles/scrollFade.module.mjs";import l from"./modal.module.mjs";import{useScrollEdges as u}from"../hooks/useScrollEdges.mjs";import{useEffect as d,useId as f,useRef as p,useState as m}from"react";import h from"classnames";import{FontAwesomeIcon as g}from"@fortawesome/react-fontawesome";import{faXmark as _}from"@fortawesome/free-solid-svg-icons";import{jsx as v,jsxs as y}from"react/jsx-runtime";var b={close:`Close`};function x(x){let{children:S=null,title:C,isOpen:w=!1,onClose:T=()=>null,footer:E=null,className:D=``,style:O,hideHeader:k=!1,closeOnEscape:A=!0,presentation:j,classNames:M,styles:N,labels:P,...F}=e(`Modal`,x),I=t(`Modal`,P,b),[L,R]=m(w),[z,B]=m(w),V=p(null),H=p(null),U=f(),{height:W,offsetTop:G}=a(),K=n(),{isSheet:q,sheetStyle:J}=o(j??K);d(()=>{w
|
|
1
|
+
"use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{useLabels as t}from"../configProvider/useLabels.mjs";import{usePresentationDefault as n}from"../configProvider/usePresentationDefault.mjs";import r from"../bodyEnd/bodyEnd.mjs";import i from"../styles/sheet.module.mjs";import{useVisualViewportSize as a}from"../hooks/useVisualViewportSize.mjs";import o from"../hooks/useSheet.mjs";import{useOverlay as s}from"../hooks/useOverlay.mjs";import c from"../styles/scrollFade.module.mjs";import l from"./modal.module.mjs";import{useScrollEdges as u}from"../hooks/useScrollEdges.mjs";import{useEffect as d,useId as f,useRef as p,useState as m}from"react";import h from"classnames";import{FontAwesomeIcon as g}from"@fortawesome/react-fontawesome";import{faXmark as _}from"@fortawesome/free-solid-svg-icons";import{jsx as v,jsxs as y}from"react/jsx-runtime";var b={close:`Close`};function x(x){let{children:S=null,title:C,isOpen:w=!1,onClose:T=()=>null,footer:E=null,className:D=``,style:O,hideHeader:k=!1,closeOnEscape:A=!0,presentation:j,classNames:M,styles:N,labels:P,...F}=e(`Modal`,x),I=t(`Modal`,P,b),[L,R]=m(w),[z,B]=m(w),V=p(null),H=p(null),U=f(),{height:W,offsetTop:G}=a(),K=n(),{isSheet:q,sheetStyle:J}=o(j??K);d(()=>{if(w){R(!0);let e=setTimeout(()=>B(!0),1);return()=>clearTimeout(e)}B(!1);let e=setTimeout(()=>R(!1),300);return()=>clearTimeout(e)},[w]),s({isActive:L&&z,containerRef:V,onEscape:A?T:void 0});let{hasStart:Y,hasEnd:X}=u(H,{isEnabled:L&&z});if(!L)return null;let Z=!k&&!!C;return v(r,{children:y(`div`,{className:h(l.modalContainer,M?.container),style:{height:W,top:G,...N?.container},children:[v(`div`,{className:h(l.modalWrapper,z&&l.modalWrapperShown,M?.backdrop),style:N?.backdrop,onClick:T}),z&&y(`div`,{...F,ref:V,role:`dialog`,"aria-modal":`true`,"aria-labelledby":Z&&!F[`aria-label`]?U:void 0,tabIndex:-1,className:h(l.modal,z&&l.modalShown,q&&[i.sheet,i.sheetSurface,l.modalSheet],D,M?.modal),style:{...q?J:void 0,...O,...N?.modal},onClick:e=>e.stopPropagation(),children:[!k&&y(`div`,{className:h(l.modalHeader,M?.header),style:N?.header,children:[v(`div`,{id:U,className:M?.title,style:N?.title,children:C}),v(`button`,{type:`button`,onClick:T,"aria-label":I.close,className:h(l.modalCloseButton,M?.closeButton),style:N?.closeButton,children:v(g,{icon:_})})]}),v(`div`,{ref:H,className:h(l.modalBody,c.scrollFade,c.scrollFadeY,Y&&c.scrollFadeFadedStart,X&&c.scrollFadeFadedEnd,M?.body),style:N?.body,children:S}),E&&v(`div`,{className:h(l.modalFooter,M?.footer),style:N?.footer,children:E})]})]})})}export{x as default};
|
|
2
2
|
//# sourceMappingURL=modal.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modal.mjs","names":[],"sources":["../../../../package/common/modal/modal.tsx"],"sourcesContent":["import cx from 'classnames';\nimport BodyEnd from 'components/common/bodyEnd';\n\nimport styles from './modal.module.pcss';\nimport sheetStyles from 'components/common/styles/sheet.module.pcss';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faXmark as closeIcon } from '@fortawesome/free-solid-svg-icons';\nimport { ModalProps } from './modal.types';\nimport type { ModalLabels } from './modal.types';\nimport { useEffect, useId, useRef, useState } from 'react';\nimport { useVisualViewportSize } from 'components/common/hooks/useVisualViewportSize';\nimport { useOverlay } from 'components/common/hooks/useOverlay';\nimport { useSheet } from 'components/common/hooks/useSheet';\nimport { useScrollEdges } from 'components/common/hooks/useScrollEdges';\nimport fadeStyles from 'components/common/styles/scrollFade.module.pcss';\nimport { useComponentDefaults, useLabels, usePresentationDefault } from 'components/common/configProvider';\n\nconst DEFAULT_LABELS: Required<ModalLabels> = {\n close: 'Close',\n};\n\nfunction Modal(props: ModalProps) {\n const {\n children = null,\n title,\n isOpen = false,\n onClose = () => null,\n footer = null,\n className = '',\n style,\n hideHeader = false,\n closeOnEscape = true,\n presentation: presentationProp,\n classNames,\n styles: slotStyles,\n labels,\n ...rest\n } = useComponentDefaults('Modal', props);\n const text = useLabels('Modal', labels, DEFAULT_LABELS);\n const [isRendered, setIsRendered] = useState(isOpen);\n const [animateIn, setAnimateIn] = useState(isOpen);\n const modalRef = useRef<HTMLDivElement>(null);\n const bodyRef = useRef<HTMLDivElement>(null);\n const titleId = useId();\n // Centering/sizing against the actually-visible viewport (not the full layout viewport) keeps\n // the modal — and whatever's focused inside it — from ending up under a mobile keyboard.\n const { height: viewportHeight, offsetTop: viewportOffsetTop } = useVisualViewportSize();\n // Explicit prop → this component's global default → the app-wide one. There's no built-in\n // fallback to add: `Default` and no presentation at all both mean the ordinary centred dialog.\n const appPresentation = usePresentationDefault();\n const presentation = presentationProp ?? appPresentation;\n const { isSheet, sheetStyle } = useSheet(presentation);\n\n useEffect(() => {\n if (isOpen) {\n // eslint-disable-next-line react-hooks/set-state-in-effect\n setIsRendered(
|
|
1
|
+
{"version":3,"file":"modal.mjs","names":[],"sources":["../../../../package/common/modal/modal.tsx"],"sourcesContent":["import cx from 'classnames';\nimport BodyEnd from 'components/common/bodyEnd';\n\nimport styles from './modal.module.pcss';\nimport sheetStyles from 'components/common/styles/sheet.module.pcss';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faXmark as closeIcon } from '@fortawesome/free-solid-svg-icons';\nimport { ModalProps } from './modal.types';\nimport type { ModalLabels } from './modal.types';\nimport { useEffect, useId, useRef, useState } from 'react';\nimport { useVisualViewportSize } from 'components/common/hooks/useVisualViewportSize';\nimport { useOverlay } from 'components/common/hooks/useOverlay';\nimport { useSheet } from 'components/common/hooks/useSheet';\nimport { useScrollEdges } from 'components/common/hooks/useScrollEdges';\nimport fadeStyles from 'components/common/styles/scrollFade.module.pcss';\nimport { useComponentDefaults, useLabels, usePresentationDefault } from 'components/common/configProvider';\n\nconst DEFAULT_LABELS: Required<ModalLabels> = {\n close: 'Close',\n};\n\nfunction Modal(props: ModalProps) {\n const {\n children = null,\n title,\n isOpen = false,\n onClose = () => null,\n footer = null,\n className = '',\n style,\n hideHeader = false,\n closeOnEscape = true,\n presentation: presentationProp,\n classNames,\n styles: slotStyles,\n labels,\n ...rest\n } = useComponentDefaults('Modal', props);\n const text = useLabels('Modal', labels, DEFAULT_LABELS);\n const [isRendered, setIsRendered] = useState(isOpen);\n const [animateIn, setAnimateIn] = useState(isOpen);\n const modalRef = useRef<HTMLDivElement>(null);\n const bodyRef = useRef<HTMLDivElement>(null);\n const titleId = useId();\n // Centering/sizing against the actually-visible viewport (not the full layout viewport) keeps\n // the modal — and whatever's focused inside it — from ending up under a mobile keyboard.\n const { height: viewportHeight, offsetTop: viewportOffsetTop } = useVisualViewportSize();\n // Explicit prop → this component's global default → the app-wide one. There's no built-in\n // fallback to add: `Default` and no presentation at all both mean the ordinary centred dialog.\n const appPresentation = usePresentationDefault();\n const presentation = presentationProp ?? appPresentation;\n const { isSheet, sheetStyle } = useSheet(presentation);\n\n // Cancelled on a flip back, or a reopen during the close animation is hidden by its late unmount.\n useEffect(() => {\n if (isOpen) {\n // eslint-disable-next-line react-hooks/set-state-in-effect\n setIsRendered(true);\n const id = setTimeout(() => setAnimateIn(true), 1);\n return () => clearTimeout(id);\n }\n setAnimateIn(false);\n const id = setTimeout(() => setIsRendered(false), 300);\n return () => clearTimeout(id);\n }, [isOpen]);\n\n // Keyed on `animateIn` rather than `isOpen`, because the panel this needs to focus and trap\n // focus inside of only mounts once that flips.\n useOverlay({\n isActive: isRendered && animateIn,\n containerRef: modalRef,\n onEscape: closeOnEscape ? onClose : undefined,\n });\n\n const { hasStart, hasEnd } = useScrollEdges(bodyRef, { isEnabled: isRendered && animateIn });\n\n if (!isRendered) return null;\n\n const hasHeaderTitle = !hideHeader && !!title;\n\n return (\n <BodyEnd>\n <div\n className={cx(styles.modalContainer, classNames?.container)}\n style={{ height: viewportHeight, top: viewportOffsetTop, ...slotStyles?.container }}\n >\n <div\n className={cx(styles.modalWrapper, animateIn && styles.modalWrapperShown, classNames?.backdrop)}\n style={slotStyles?.backdrop}\n onClick={onClose}\n />\n {animateIn && (\n <div\n {...rest}\n ref={modalRef}\n role='dialog'\n aria-modal='true'\n // aria-labelledby would outrank aria-label, which is the only way to name a title with no text.\n aria-labelledby={hasHeaderTitle && !rest['aria-label'] ? titleId : undefined}\n // Focusable so the trap has somewhere to put focus when the content has no tabbables.\n tabIndex={-1}\n className={cx(\n styles.modal,\n animateIn && styles.modalShown,\n // The panel is its own sheet surface here: there's no inner panel to fill, since\n // Modal already splits itself into a fixed header and a scrolling body.\n isSheet && [sheetStyles.sheet, sheetStyles.sheetSurface, styles.modalSheet],\n className,\n classNames?.modal,\n )}\n style={{ ...(isSheet ? sheetStyle : undefined), ...style, ...slotStyles?.modal }}\n onClick={(e) => e.stopPropagation()}\n >\n {!hideHeader && (\n <div className={cx(styles.modalHeader, classNames?.header)} style={slotStyles?.header}>\n <div id={titleId} className={classNames?.title} style={slotStyles?.title}>\n {title}\n </div>\n <button\n type='button'\n onClick={onClose}\n aria-label={text.close}\n className={cx(styles.modalCloseButton, classNames?.closeButton)}\n style={slotStyles?.closeButton}\n >\n <FontAwesomeIcon icon={closeIcon} />\n </button>\n </div>\n )}\n <div\n ref={bodyRef}\n className={cx(\n styles.modalBody,\n fadeStyles.scrollFade,\n fadeStyles.scrollFadeY,\n hasStart && fadeStyles.scrollFadeFadedStart,\n hasEnd && fadeStyles.scrollFadeFadedEnd,\n classNames?.body,\n )}\n style={slotStyles?.body}\n >\n {children}\n </div>\n {footer && (\n <div className={cx(styles.modalFooter, classNames?.footer)} style={slotStyles?.footer}>\n {footer}\n </div>\n )}\n </div>\n )}\n </div>\n </BodyEnd>\n );\n}\n\nexport default Modal;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modal.module.mjs","names":[],"sources":["../../../../package/common/modal/modal.module.pcss"],"sourcesContent":[".modal {\n /* Theme tokens resolved once, each with a built-in fallback. Set any of the public\n `--modal-*` variables via ThemeProvider (theme-wide) or `style` (per instance) to\n override; unset ones fall back to the values below. */\n --custom-modal-padding: var(--modal-padding, var(--spacing-l));\n --custom-modal-gap: var(--modal-gap, var(--spacing-l));\n\n /* Half the gap, so the scroll port's clipping edge — where the divider sits — lands midway\n between the chrome and where the content rests, with the same room either side. Flush\n against the chrome (bleed = gap) reads as a sliced line of text colliding with the title;\n an unrelated value (this was `0.6em`) puts it at a distance that drifts with the font size. */\n --custom-modal-body-bleed: var(--modal-body-bleed, calc(var(--custom-modal-gap) / 2));\n\n /* The panel never comes closer to the screen edge than its own padding, at every size. A\n percentage instead reads as a different inset on every screen, and pairing one with a\n breakpoint made the two axes disagree: 5% of a 390px width is 19.5px but 5% of a 780px\n height is 39px, so the same rule came out narrower and taller than the default on one phone. */\n --custom-modal-max-width: var(--modal-max-width, calc(100% - 2 * var(--custom-modal-padding)));\n --custom-modal-max-height: var(--modal-max-height, calc(100% - 2 * var(--custom-modal-padding)));\n\n padding: var(--custom-modal-padding);\n border-radius: var(--default-border-radius);\n background: var(--background-modal);\n color: var(--text-color);\n font-family: var(--default-font);\n box-sizing: border-box;\n position: relative;\n display: flex;\n flex-direction: column;\n transform-origin: bottom center;\n animation:\n slideIn 0.2s,\n fadeIn 0.3s forwards;\n max-width: var(--custom-modal-max-width);\n max-height: var(--custom-modal-max-height);\n\n /* The panel itself never scrolls, so the header stays put; `&-body` is what scrolls. */\n overflow: hidden;\n\n /* A sheet-presented modal carries forms and detail views, not a short menu, so it takes more\n of the screen than the shared default — and keeps the panel's own padding at the bottom edge. */\n &-sheet {\n --sheet-max-height: 90dvh;\n --sheet-safe-padding: var(--custom-modal-padding);\n }\n\n &-container {\n position: fixed;\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n top: 0;\n left: 0;\n }\n\n &-header {\n display: flex;\n gap: var(--spacing-m);\n justify-content: space-between;\n align-items: center;\n margin-bottom: var(--custom-modal-gap);\n flex: 0 0 auto;\n\n div {\n overflow: hidden;\n text-overflow: ellipsis;\n }\n }\n\n &-body {\n /* `min-height: 0` lets this shrink below its content's height so it, not the panel, scrolls. */\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n\n /* A negative margin cancelled by an equal padding, so a focus ring or a floating label at\n the body's edge isn't clipped by the scroll container. Override the pair with\n `--modal-body-bleed`, never with a plain `padding`, which drops the negative half and\n brings the clipping back. See `docs/Modal.md` for what the two do together. */\n margin: calc(-1 * var(--custom-modal-body-bleed));\n padding: var(--custom-modal-body-bleed);\n\n /* The mask lives in `styles/scrollFade.module.pcss`; the component applies it along with the\n modifiers saying which end genuinely hides content. The run has to clear the port's own\n padding before any content fades at all, hence gap + bleed. Set here as the shared sheet's\n private length rather than as `--scroll-fade-length`, which would inherit down into a\n `ScrollFade` nested in the body. */\n --custom-scroll-fade-length: var(\n --modal-scroll-fade,\n calc(var(--custom-modal-gap) + var(--custom-modal-body-bleed))\n );\n }\n\n &-footer {\n margin-top: var(--custom-modal-gap);\n flex: 0 0 auto;\n }\n\n &-close-button {\n flex-shrink: 0;\n background: none;\n border: none;\n padding: 0;\n color: inherit;\n font-size: inherit;\n line-height: 1;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n }\n\n &-wrapper {\n position: fixed;\n width: 100%;\n height: 100%;\n background: #00000052;\n top: 0;\n left: 0;\n transition: all 0.3s ease-in-out;\n opacity: 0;\n\n &-shown {\n opacity: 1;\n }\n }\n}\n\n@keyframes slideIn {\n from {\n transform: scale(0);\n }\n\n to {\n transform: scale(0.99);\n }\n}\n\n@keyframes fadeIn {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n}\n\n/* Only the scale-in goes; the backdrop and the panel still fade, which is not motion.\n `slideIn` ends at scale(0.99) and the panel has no transform of its own, so dropping the\n animation lands it at scale(1) rather than leaving it collapsed. */\n@media (prefers-reduced-motion: reduce) {\n .modal {\n animation: fadeIn 0.3s forwards;\n }\n}\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"modal.module.mjs","names":[],"sources":["../../../../package/common/modal/modal.module.pcss"],"sourcesContent":[".modal {\n /* Theme tokens resolved once, each with a built-in fallback. Set any of the public\n `--modal-*` variables via ThemeProvider (theme-wide) or `style` (per instance) to\n override; unset ones fall back to the values below. */\n --custom-modal-padding: var(--modal-padding, var(--spacing-l));\n --custom-modal-gap: var(--modal-gap, var(--spacing-l));\n\n /* Half the gap, so the scroll port's clipping edge — where the divider sits — lands midway\n between the chrome and where the content rests, with the same room either side. Flush\n against the chrome (bleed = gap) reads as a sliced line of text colliding with the title;\n an unrelated value (this was `0.6em`) puts it at a distance that drifts with the font size. */\n --custom-modal-body-bleed: var(--modal-body-bleed, calc(var(--custom-modal-gap) / 2));\n\n /* The panel never comes closer to the screen edge than its own padding, at every size. A\n percentage instead reads as a different inset on every screen, and pairing one with a\n breakpoint made the two axes disagree: 5% of a 390px width is 19.5px but 5% of a 780px\n height is 39px, so the same rule came out narrower and taller than the default on one phone. */\n --custom-modal-max-width: var(--modal-max-width, calc(100% - 2 * var(--custom-modal-padding)));\n --custom-modal-max-height: var(--modal-max-height, calc(100% - 2 * var(--custom-modal-padding)));\n\n padding: var(--custom-modal-padding);\n border-radius: var(--default-border-radius);\n background: var(--background-modal);\n color: var(--text-color);\n font-family: var(--default-font);\n box-sizing: border-box;\n position: relative;\n display: flex;\n flex-direction: column;\n transform-origin: bottom center;\n animation:\n slideIn 0.2s,\n fadeIn 0.3s forwards;\n max-width: var(--custom-modal-max-width);\n max-height: var(--custom-modal-max-height);\n\n /* The panel itself never scrolls, so the header stays put; `&-body` is what scrolls. */\n overflow: hidden;\n\n /* A sheet-presented modal carries forms and detail views, not a short menu, so it takes more\n of the screen than the shared default — and keeps the panel's own padding at the bottom edge. */\n &-sheet {\n --sheet-max-height: 90dvh;\n --sheet-safe-padding: var(--custom-modal-padding);\n }\n\n &-container {\n position: fixed;\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n top: 0;\n left: 0;\n\n /* Shared by every overlay, so the order they are portaled in, which is the order they opened in, decides. */\n z-index: 1;\n }\n\n &-header {\n display: flex;\n gap: var(--spacing-m);\n justify-content: space-between;\n align-items: center;\n margin-bottom: var(--custom-modal-gap);\n flex: 0 0 auto;\n\n div {\n overflow: hidden;\n text-overflow: ellipsis;\n }\n }\n\n &-body {\n /* `min-height: 0` lets this shrink below its content's height so it, not the panel, scrolls. */\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n\n /* A negative margin cancelled by an equal padding, so a focus ring or a floating label at\n the body's edge isn't clipped by the scroll container. Override the pair with\n `--modal-body-bleed`, never with a plain `padding`, which drops the negative half and\n brings the clipping back. See `docs/Modal.md` for what the two do together. */\n margin: calc(-1 * var(--custom-modal-body-bleed));\n padding: var(--custom-modal-body-bleed);\n\n /* The mask lives in `styles/scrollFade.module.pcss`; the component applies it along with the\n modifiers saying which end genuinely hides content. The run has to clear the port's own\n padding before any content fades at all, hence gap + bleed. Set here as the shared sheet's\n private length rather than as `--scroll-fade-length`, which would inherit down into a\n `ScrollFade` nested in the body. */\n --custom-scroll-fade-length: var(\n --modal-scroll-fade,\n calc(var(--custom-modal-gap) + var(--custom-modal-body-bleed))\n );\n }\n\n &-footer {\n margin-top: var(--custom-modal-gap);\n flex: 0 0 auto;\n }\n\n &-close-button {\n flex-shrink: 0;\n background: none;\n border: none;\n padding: 0;\n color: inherit;\n font-size: inherit;\n line-height: 1;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n }\n\n &-wrapper {\n position: fixed;\n width: 100%;\n height: 100%;\n background: #00000052;\n top: 0;\n left: 0;\n transition: all 0.3s ease-in-out;\n opacity: 0;\n\n &-shown {\n opacity: 1;\n }\n }\n}\n\n@keyframes slideIn {\n from {\n transform: scale(0);\n }\n\n to {\n transform: scale(0.99);\n }\n}\n\n@keyframes fadeIn {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n}\n\n/* Only the scale-in goes; the backdrop and the panel still fade, which is not motion.\n `slideIn` ends at scale(0.99) and the panel has no transform of its own, so dropping the\n animation lands it at scale(1) rather than leaving it collapsed. */\n@media (prefers-reduced-motion: reduce) {\n .modal {\n animation: fadeIn 0.3s forwards;\n }\n}\n"],"mappings":""}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{useLabels as t}from"../configProvider/useLabels.mjs";import n from"../tooltip/tooltip.mjs";import{OtpInputCharset as r}from"./otpInput.types.mjs";import i from"./otpInput.module.mjs";import{useEffect as a,useRef as o,useState as s}from"react";import c from"classnames";import{jsx as l,jsxs as u}from"react/jsx-runtime";var d=/[0-9]/g,f=/[a-zA-Z0-9]/g,
|
|
1
|
+
"use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{useLabels as t}from"../configProvider/useLabels.mjs";import n from"../tooltip/tooltip.mjs";import{OtpInputCharset as r}from"./otpInput.types.mjs";import i from"./otpInput.module.mjs";import{useEffect as a,useRef as o,useState as s}from"react";import c from"classnames";import{jsx as l,jsxs as u}from"react/jsx-runtime";var d=/[0-9]/g,f=/[a-zA-Z0-9]/g,ee={field:`One-time code`,digit:(e,t)=>`Digit ${e} of ${t}`};function p(e,t){let n=t===r.Alphanumeric?f:d;return(e.match(n)||[]).join(``)}function m(d){let{length:f=6,value:m,onChange:h=()=>{},onComplete:g=()=>{},formValidator:_=null,charset:v=r.Numeric,masked:y=!1,groupAfter:b=[],label:x=``,isValid:S=!0,errorMessage:C=``,isRequired:w=!1,clearOnError:T=!1,clearErrorDelay:E=900,disabled:D=!1,autoFocus:O=!1,className:k=``,style:A,classNames:j,styles:M,labels:N,...P}=e(`OtpInput`,d),F=t(`OtpInput`,N,ee),I=(_?_.value:m)??``,[,L]=s(0),[R,z]=s(!1),[B,V]=s(!1),[H,U]=s(!1),[W,G]=s(!1),[K,q]=s(!1),J=o([]);a(()=>{if(!_)return;_.validate();let e=()=>L(e=>e+1);return _.registerOnUpdateListener(e),()=>_.removeOnUpdateListener(e)},[_]),a(()=>{if(!O)return;let e=Math.min(I.length,f-1);J.current[e]?.focus()},[]);let Y=_?_.touched&&_.hasError():!S||!!C,X=_?_.getCurrentErrorMessage():C||``;Y!==K&&(q(Y),Y&&z(!0),G(Y&&T)),a(()=>{if(!R)return;let e=window.setTimeout(()=>z(!1),400);return()=>window.clearTimeout(e)},[R]),a(()=>{if(!W||!T)return;let e=window.setTimeout(()=>{G(!1),Z(``),J.current[0]?.focus()},E);return()=>window.clearTimeout(e)},[W,T,E]);function Z(e){let t=I.length===f,n=p(e,v).slice(0,f);_&&_.set(n),h?.(n),n.length===f&&!t&&g?.(n)}function Q(e,t){let n=I.split(``);n[e]=t,Z(n.join(``))}function te(e,t){if(D)return;if(t===``){Q(e,``);return}let n=p(t,v);if(!n)return;let r=Math.min(e,I.length);if(t.length>1){$(r,n);return}Q(r,n),r<f-1?J.current[r+1]?.focus():J.current[r]?.blur()}function $(e,t){let n=p(t,v);if(!n)return;let r=I.padEnd(f,` `).split(``),i=e;for(let e of n){if(i>=f)break;r[i]=e,i+=1}Z(r.join(``).trimEnd());let a=Math.min(i,f-1);J.current[a]?.focus(),i>=f&&J.current[f-1]?.blur()}function ne(e,t){if(D)return;let n=t.clipboardData.getData(`text`);n&&(t.preventDefault(),$(Math.min(e,I.length),n))}function re(e,t){if(!D){if(t.key.length===1&&!t.ctrlKey&&!t.metaKey&&!t.altKey){let n=p(t.key,v);if(n&&I[e]===n){t.preventDefault(),Q(e,n),e<f-1?J.current[e+1]?.focus():J.current[e]?.blur();return}}if(t.key===`Backspace`){if(I[e]){Q(e,``);return}e>0&&(t.preventDefault(),Q(e-1,``),J.current[e-1]?.focus());return}if(t.key===`Delete`){I[e]&&Q(e,``);return}if(t.key===`ArrowLeft`&&e>0){t.preventDefault(),J.current[e-1]?.focus();return}t.key===`ArrowRight`&&e<f-1&&(t.preventDefault(),J.current[e+1]?.focus())}}function ie(e){e.target.select()}function ae(){V(!0)}function oe(e){e.currentTarget.contains(e.relatedTarget)||(V(!1),_&&(_.touched=!0))}return u(`div`,{...P,className:c(i.otpInput,D&&i.otpInputDisabled,k),style:A,children:[x&&u(`div`,{className:c(i.otpInputLabel,j?.label),style:M?.label,children:[x,w&&l(`span`,{className:i.otpInputRequiredMark,children:` *`})]}),l(`div`,{role:`group`,"aria-label":x||F.field,className:c(i.otpInputBoxes,R&&i.otpInputBoxesShake,j?.inputs),style:M?.inputs,onFocus:ae,onBlur:oe,onMouseEnter:()=>U(!0),onMouseLeave:()=>U(!1),children:Array.from({length:f},(e,t)=>{let n=I[t]||``;return u(`div`,{className:i.otpInputBoxWrapper,children:[l(`input`,{ref:e=>{J.current[t]=e},type:y?`password`:`text`,inputMode:v===r.Numeric?`numeric`:`text`,pattern:v===r.Numeric?`[0-9]*`:void 0,autoComplete:t===0?`one-time-code`:`off`,maxLength:1,disabled:D,value:n,"aria-label":F.digit(t+1,f),"aria-invalid":Y||void 0,className:c(i.otpInputBox,Y&&i.otpInputBoxError,n&&i.otpInputBoxFilled,j?.input),style:M?.input,onChange:e=>te(t,e.target.value),onKeyDown:e=>re(t,e),onPaste:e=>ne(t,e),onFocus:ie}),b.includes(t+1)&&t<f-1&&l(`span`,{className:c(i.otpInputSeparator,j?.separator),style:M?.separator})]},t)})}),l(n,{variant:`error`,message:X,isVisible:Y&&(B||H)})]})}export{m as default};
|
|
2
2
|
//# sourceMappingURL=otpInput.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otpInput.mjs","names":[],"sources":["../../../../package/common/otpInput/otpInput.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactElement } from 'react';\nimport cx from 'classnames';\nimport Tooltip from 'components/common/tooltip';\nimport { useComponentDefaults, useLabels } from '../configProvider';\n\nimport { OtpInputCharset, type OtpInputLabels, type OtpInputProps } from './otpInput.types';\nimport styles from './otpInput.module.pcss';\n\nconst NUMERIC_RE = /[0-9]/g;\nconst ALPHANUMERIC_RE = /[a-zA-Z0-9]/g;\n\nconst DEFAULT_LABELS: Required<OtpInputLabels> = {\n field: 'One-time code',\n digit: (position, length) => `Digit ${position} of ${length}`,\n};\n\nfunction filterChars(raw: string, charset: OtpInputCharset): string {\n const re = charset === OtpInputCharset.Alphanumeric ? ALPHANUMERIC_RE : NUMERIC_RE;\n return (raw.match(re) || []).join('');\n}\n\nfunction OtpInput(props: OtpInputProps): ReactElement {\n const {\n length = 6,\n value,\n onChange = () => {},\n onComplete = () => {},\n formValidator = null,\n charset = OtpInputCharset.Numeric,\n masked = false,\n groupAfter = [],\n label = '',\n isValid = true,\n errorMessage = '',\n isRequired = false,\n clearOnError = false,\n clearErrorDelay = 900,\n disabled = false,\n autoFocus = false,\n className = '',\n style,\n classNames,\n styles: slotStyles,\n labels,\n ...rest\n } = useComponentDefaults('OtpInput', props);\n const text = useLabels('OtpInput', labels, DEFAULT_LABELS);\n\n const currentValue = (formValidator ? (formValidator.value as string) : value) ?? '';\n const [, forceUpdate] = useState(0);\n const [isShaking, setIsShaking] = useState(false);\n const [isFocused, setIsFocused] = useState(false);\n const [isHovered, setIsHovered] = useState(false);\n const inputRefs = useRef<Array<HTMLInputElement | null>>([]);\n const wasErrorRef = useRef(false);\n\n useEffect(() => {\n if (!formValidator) return;\n formValidator.validate();\n const listener = () => forceUpdate((n) => n + 1);\n formValidator.registerOnUpdateListener(listener);\n return () => formValidator.removeOnUpdateListener(listener);\n }, [formValidator]);\n\n useEffect(() => {\n if (!autoFocus) return;\n const firstEmpty = Math.min(currentValue.length, length - 1);\n inputRefs.current[firstEmpty]?.focus();\n // Only run once, on mount.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const showError = formValidator ? formValidator.touched && formValidator.hasError() : !isValid || !!errorMessage;\n const displayedErrorMessage = formValidator ? formValidator.getCurrentErrorMessage() : errorMessage || '';\n\n useEffect(() => {\n const justErrored = showError && !wasErrorRef.current;\n wasErrorRef.current = showError;\n if (!justErrored) return;\n\n setIsShaking(true);\n const shakeTimeoutId = window.setTimeout(() => setIsShaking(false), 400);\n const clearTimeoutId = clearOnError\n ? window.setTimeout(() => {\n commit('');\n inputRefs.current[0]?.focus();\n }, clearErrorDelay)\n : null;\n\n return () => {\n window.clearTimeout(shakeTimeoutId);\n if (clearTimeoutId != null) window.clearTimeout(clearTimeoutId);\n };\n // Only re-evaluate on an actual error-state or config change, not on every value/render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [showError, clearOnError, clearErrorDelay]);\n\n function commit(next: string) {\n const wasComplete = currentValue.length === length;\n const clipped = filterChars(next, charset).slice(0, length);\n if (formValidator) {\n formValidator.set(clipped);\n }\n onChange?.(clipped);\n // Only fire once per genuinely new completion (going from partial to full), not on every\n // keystroke made while correcting an already-full code.\n if (clipped.length === length && !wasComplete) {\n onComplete?.(clipped);\n }\n }\n\n function setCharAt(index: number, char: string) {\n const chars = currentValue.split('');\n chars[index] = char;\n commit(chars.join(''));\n }\n\n function handleChange(index: number, rawInput: string) {\n if (disabled) return;\n if (rawInput === '') {\n // Box was cleared (e.g. select + delete) — Backspace itself is handled in onKeyDown.\n setCharAt(index, '');\n return;\n }\n\n const filtered = filterChars(rawInput, charset);\n if (!filtered) {\n // Typed an invalid/disallowed character — the controlled `value` re-render discards it.\n return;\n }\n\n // Clicking straight into a box past the contiguous filled prefix (skipping ahead of\n // empty boxes) fills the next expected slot instead, rather than leaving a gap before it\n // that setCharAt/distributeFrom can't represent in a plain string and would silently collapse.\n const targetIndex = Math.min(index, currentValue.length);\n\n if (rawInput.length > 1) {\n // Mobile autofill / SMS suggestions can drop the whole code into one box, bypassing maxLength.\n distributeFrom(targetIndex, filtered);\n return;\n }\n\n setCharAt(targetIndex, filtered);\n if (targetIndex < length - 1) {\n inputRefs.current[targetIndex + 1]?.focus();\n } else {\n inputRefs.current[targetIndex]?.blur();\n }\n }\n\n function distributeFrom(startIndex: number, chars: string) {\n const clean = filterChars(chars, charset);\n if (!clean) return;\n const existing = currentValue.padEnd(length, ' ').split('');\n let cursor = startIndex;\n for (const char of clean) {\n if (cursor >= length) break;\n existing[cursor] = char;\n cursor += 1;\n }\n commit(existing.join('').trimEnd());\n const nextFocus = Math.min(cursor, length - 1);\n inputRefs.current[nextFocus]?.focus();\n if (cursor >= length) {\n inputRefs.current[length - 1]?.blur();\n }\n }\n\n function handlePaste(index: number, event: React.ClipboardEvent<HTMLInputElement>) {\n if (disabled) return;\n const pasted = event.clipboardData.getData('text');\n if (!pasted) return;\n event.preventDefault();\n // Same clamp as handleChange — pasting into a box past the filled prefix starts at the\n // next expected slot instead of leaving an unrepresentable gap before it.\n distributeFrom(Math.min(index, currentValue.length), pasted);\n }\n\n function handleKeyDown(index: number, event: React.KeyboardEvent<HTMLInputElement>) {\n if (disabled) return;\n\n // React's controlled-input onChange never fires when the resulting value is identical to\n // what's already rendered (its value tracker sees no change) — so retyping the exact digit\n // already in a box would otherwise silently do nothing. keydown always fires, so handle that\n // one case here and skip the native edit entirely.\n if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {\n const typed = filterChars(event.key, charset);\n if (typed && currentValue[index] === typed) {\n event.preventDefault();\n setCharAt(index, typed);\n if (index < length - 1) {\n inputRefs.current[index + 1]?.focus();\n } else {\n inputRefs.current[index]?.blur();\n }\n return;\n }\n }\n\n if (event.key === 'Backspace') {\n if (currentValue[index]) {\n setCharAt(index, '');\n return;\n }\n if (index > 0) {\n event.preventDefault();\n setCharAt(index - 1, '');\n inputRefs.current[index - 1]?.focus();\n }\n return;\n }\n if (event.key === 'Delete') {\n if (currentValue[index]) setCharAt(index, '');\n return;\n }\n if (event.key === 'ArrowLeft' && index > 0) {\n event.preventDefault();\n inputRefs.current[index - 1]?.focus();\n return;\n }\n if (event.key === 'ArrowRight' && index < length - 1) {\n event.preventDefault();\n inputRefs.current[index + 1]?.focus();\n }\n }\n\n function handleFocus(event: React.FocusEvent<HTMLInputElement>) {\n event.target.select();\n }\n\n function handleGroupFocus() {\n setIsFocused(true);\n }\n\n function handleGroupBlur(event: React.FocusEvent<HTMLDivElement>) {\n // Auto-advance moves focus between boxes constantly while typing — only treat this as\n // leaving the group once focus actually lands outside it, not on the next box.\n if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;\n setIsFocused(false);\n if (formValidator) formValidator.touched = true;\n }\n\n return (\n <div {...rest} className={cx(styles.otpInput, disabled && styles.otpInputDisabled, className)} style={style}>\n {label && (\n <div className={cx(styles.otpInputLabel, classNames?.label)} style={slotStyles?.label}>\n {label}\n {isRequired && <span className={styles.otpInputRequiredMark}> *</span>}\n </div>\n )}\n <div\n role='group'\n aria-label={label || text.field}\n className={cx(styles.otpInputBoxes, isShaking && styles.otpInputBoxesShake, classNames?.inputs)}\n style={slotStyles?.inputs}\n onFocus={handleGroupFocus}\n onBlur={handleGroupBlur}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n {Array.from({ length }, (_, index) => {\n const char = currentValue[index] || '';\n return (\n <div key={index} className={styles.otpInputBoxWrapper}>\n <input\n ref={(el) => {\n inputRefs.current[index] = el;\n }}\n type={masked ? 'password' : 'text'}\n inputMode={charset === OtpInputCharset.Numeric ? 'numeric' : 'text'}\n pattern={charset === OtpInputCharset.Numeric ? '[0-9]*' : undefined}\n autoComplete={index === 0 ? 'one-time-code' : 'off'}\n maxLength={1}\n disabled={disabled}\n value={char}\n aria-label={text.digit(index + 1, length)}\n aria-invalid={showError || undefined}\n className={cx(\n styles.otpInputBox,\n showError && styles.otpInputBoxError,\n char && styles.otpInputBoxFilled,\n classNames?.input,\n )}\n style={slotStyles?.input}\n onChange={(e) => handleChange(index, e.target.value)}\n onKeyDown={(e) => handleKeyDown(index, e)}\n onPaste={(e) => handlePaste(index, e)}\n onFocus={handleFocus}\n />\n {groupAfter.includes(index + 1) && index < length - 1 && (\n <span className={cx(styles.otpInputSeparator, classNames?.separator)} style={slotStyles?.separator} />\n )}\n </div>\n );\n })}\n </div>\n <Tooltip variant='error' message={displayedErrorMessage} isVisible={showError && (isFocused || isHovered)} />\n </div>\n );\n}\n\nexport default OtpInput;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"otpInput.mjs","names":[],"sources":["../../../../package/common/otpInput/otpInput.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactElement } from 'react';\nimport cx from 'classnames';\nimport Tooltip from 'components/common/tooltip';\nimport { useComponentDefaults, useLabels } from '../configProvider';\n\nimport { OtpInputCharset, type OtpInputLabels, type OtpInputProps } from './otpInput.types';\nimport styles from './otpInput.module.pcss';\n\nconst NUMERIC_RE = /[0-9]/g;\nconst ALPHANUMERIC_RE = /[a-zA-Z0-9]/g;\n\nconst DEFAULT_LABELS: Required<OtpInputLabels> = {\n field: 'One-time code',\n digit: (position, length) => `Digit ${position} of ${length}`,\n};\n\nfunction filterChars(raw: string, charset: OtpInputCharset): string {\n const re = charset === OtpInputCharset.Alphanumeric ? ALPHANUMERIC_RE : NUMERIC_RE;\n return (raw.match(re) || []).join('');\n}\n\nfunction OtpInput(props: OtpInputProps): ReactElement {\n const {\n length = 6,\n value,\n onChange = () => {},\n onComplete = () => {},\n formValidator = null,\n charset = OtpInputCharset.Numeric,\n masked = false,\n groupAfter = [],\n label = '',\n isValid = true,\n errorMessage = '',\n isRequired = false,\n clearOnError = false,\n clearErrorDelay = 900,\n disabled = false,\n autoFocus = false,\n className = '',\n style,\n classNames,\n styles: slotStyles,\n labels,\n ...rest\n } = useComponentDefaults('OtpInput', props);\n const text = useLabels('OtpInput', labels, DEFAULT_LABELS);\n\n const currentValue = (formValidator ? (formValidator.value as string) : value) ?? '';\n const [, forceUpdate] = useState(0);\n const [isShaking, setIsShaking] = useState(false);\n const [isFocused, setIsFocused] = useState(false);\n const [isHovered, setIsHovered] = useState(false);\n const [pendingClear, setPendingClear] = useState(false);\n const [wasError, setWasError] = useState(false);\n const inputRefs = useRef<Array<HTMLInputElement | null>>([]);\n\n useEffect(() => {\n if (!formValidator) return;\n formValidator.validate();\n const listener = () => forceUpdate((n) => n + 1);\n formValidator.registerOnUpdateListener(listener);\n return () => formValidator.removeOnUpdateListener(listener);\n }, [formValidator]);\n\n useEffect(() => {\n if (!autoFocus) return;\n const firstEmpty = Math.min(currentValue.length, length - 1);\n inputRefs.current[firstEmpty]?.focus();\n // Only run once, on mount.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const showError = formValidator ? formValidator.touched && formValidator.hasError() : !isValid || !!errorMessage;\n const displayedErrorMessage = formValidator ? formValidator.getCurrentErrorMessage() : errorMessage || '';\n\n // State set during render, not a ref read in an effect: the ref survives a remount (StrictMode,\n // <Activity>), so the re-run saw no new error and dropped the pending clear.\n if (showError !== wasError) {\n setWasError(showError);\n if (showError) setIsShaking(true);\n setPendingClear(showError && clearOnError);\n }\n\n useEffect(() => {\n if (!isShaking) return;\n const id = window.setTimeout(() => setIsShaking(false), 400);\n return () => window.clearTimeout(id);\n }, [isShaking]);\n\n useEffect(() => {\n if (!pendingClear || !clearOnError) return;\n const id = window.setTimeout(() => {\n setPendingClear(false);\n commit('');\n inputRefs.current[0]?.focus();\n }, clearErrorDelay);\n return () => window.clearTimeout(id);\n // commit is recreated every render; listing it would restart the delay on each keystroke.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [pendingClear, clearOnError, clearErrorDelay]);\n\n function commit(next: string) {\n const wasComplete = currentValue.length === length;\n const clipped = filterChars(next, charset).slice(0, length);\n if (formValidator) {\n formValidator.set(clipped);\n }\n onChange?.(clipped);\n // Only fire once per genuinely new completion (going from partial to full), not on every\n // keystroke made while correcting an already-full code.\n if (clipped.length === length && !wasComplete) {\n onComplete?.(clipped);\n }\n }\n\n function setCharAt(index: number, char: string) {\n const chars = currentValue.split('');\n chars[index] = char;\n commit(chars.join(''));\n }\n\n function handleChange(index: number, rawInput: string) {\n if (disabled) return;\n if (rawInput === '') {\n // Box was cleared (e.g. select + delete) — Backspace itself is handled in onKeyDown.\n setCharAt(index, '');\n return;\n }\n\n const filtered = filterChars(rawInput, charset);\n if (!filtered) {\n // Typed an invalid/disallowed character — the controlled `value` re-render discards it.\n return;\n }\n\n // Clicking straight into a box past the contiguous filled prefix (skipping ahead of\n // empty boxes) fills the next expected slot instead, rather than leaving a gap before it\n // that setCharAt/distributeFrom can't represent in a plain string and would silently collapse.\n const targetIndex = Math.min(index, currentValue.length);\n\n if (rawInput.length > 1) {\n // Mobile autofill / SMS suggestions can drop the whole code into one box, bypassing maxLength.\n distributeFrom(targetIndex, filtered);\n return;\n }\n\n setCharAt(targetIndex, filtered);\n if (targetIndex < length - 1) {\n inputRefs.current[targetIndex + 1]?.focus();\n } else {\n inputRefs.current[targetIndex]?.blur();\n }\n }\n\n function distributeFrom(startIndex: number, chars: string) {\n const clean = filterChars(chars, charset);\n if (!clean) return;\n const existing = currentValue.padEnd(length, ' ').split('');\n let cursor = startIndex;\n for (const char of clean) {\n if (cursor >= length) break;\n existing[cursor] = char;\n cursor += 1;\n }\n commit(existing.join('').trimEnd());\n const nextFocus = Math.min(cursor, length - 1);\n inputRefs.current[nextFocus]?.focus();\n if (cursor >= length) {\n inputRefs.current[length - 1]?.blur();\n }\n }\n\n function handlePaste(index: number, event: React.ClipboardEvent<HTMLInputElement>) {\n if (disabled) return;\n const pasted = event.clipboardData.getData('text');\n if (!pasted) return;\n event.preventDefault();\n // Same clamp as handleChange — pasting into a box past the filled prefix starts at the\n // next expected slot instead of leaving an unrepresentable gap before it.\n distributeFrom(Math.min(index, currentValue.length), pasted);\n }\n\n function handleKeyDown(index: number, event: React.KeyboardEvent<HTMLInputElement>) {\n if (disabled) return;\n\n // React's controlled-input onChange never fires when the resulting value is identical to\n // what's already rendered (its value tracker sees no change) — so retyping the exact digit\n // already in a box would otherwise silently do nothing. keydown always fires, so handle that\n // one case here and skip the native edit entirely.\n if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {\n const typed = filterChars(event.key, charset);\n if (typed && currentValue[index] === typed) {\n event.preventDefault();\n setCharAt(index, typed);\n if (index < length - 1) {\n inputRefs.current[index + 1]?.focus();\n } else {\n inputRefs.current[index]?.blur();\n }\n return;\n }\n }\n\n if (event.key === 'Backspace') {\n if (currentValue[index]) {\n setCharAt(index, '');\n return;\n }\n if (index > 0) {\n event.preventDefault();\n setCharAt(index - 1, '');\n inputRefs.current[index - 1]?.focus();\n }\n return;\n }\n if (event.key === 'Delete') {\n if (currentValue[index]) setCharAt(index, '');\n return;\n }\n if (event.key === 'ArrowLeft' && index > 0) {\n event.preventDefault();\n inputRefs.current[index - 1]?.focus();\n return;\n }\n if (event.key === 'ArrowRight' && index < length - 1) {\n event.preventDefault();\n inputRefs.current[index + 1]?.focus();\n }\n }\n\n function handleFocus(event: React.FocusEvent<HTMLInputElement>) {\n event.target.select();\n }\n\n function handleGroupFocus() {\n setIsFocused(true);\n }\n\n function handleGroupBlur(event: React.FocusEvent<HTMLDivElement>) {\n // Auto-advance moves focus between boxes constantly while typing — only treat this as\n // leaving the group once focus actually lands outside it, not on the next box.\n if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;\n setIsFocused(false);\n if (formValidator) formValidator.touched = true;\n }\n\n return (\n <div {...rest} className={cx(styles.otpInput, disabled && styles.otpInputDisabled, className)} style={style}>\n {label && (\n <div className={cx(styles.otpInputLabel, classNames?.label)} style={slotStyles?.label}>\n {label}\n {isRequired && <span className={styles.otpInputRequiredMark}> *</span>}\n </div>\n )}\n <div\n role='group'\n aria-label={label || text.field}\n className={cx(styles.otpInputBoxes, isShaking && styles.otpInputBoxesShake, classNames?.inputs)}\n style={slotStyles?.inputs}\n onFocus={handleGroupFocus}\n onBlur={handleGroupBlur}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n {Array.from({ length }, (_, index) => {\n const char = currentValue[index] || '';\n return (\n <div key={index} className={styles.otpInputBoxWrapper}>\n <input\n ref={(el) => {\n inputRefs.current[index] = el;\n }}\n type={masked ? 'password' : 'text'}\n inputMode={charset === OtpInputCharset.Numeric ? 'numeric' : 'text'}\n pattern={charset === OtpInputCharset.Numeric ? '[0-9]*' : undefined}\n autoComplete={index === 0 ? 'one-time-code' : 'off'}\n maxLength={1}\n disabled={disabled}\n value={char}\n aria-label={text.digit(index + 1, length)}\n aria-invalid={showError || undefined}\n className={cx(\n styles.otpInputBox,\n showError && styles.otpInputBoxError,\n char && styles.otpInputBoxFilled,\n classNames?.input,\n )}\n style={slotStyles?.input}\n onChange={(e) => handleChange(index, e.target.value)}\n onKeyDown={(e) => handleKeyDown(index, e)}\n onPaste={(e) => handlePaste(index, e)}\n onFocus={handleFocus}\n />\n {groupAfter.includes(index + 1) && index < length - 1 && (\n <span className={cx(styles.otpInputSeparator, classNames?.separator)} style={slotStyles?.separator} />\n )}\n </div>\n );\n })}\n </div>\n <Tooltip variant='error' message={displayedErrorMessage} isVisible={showError && (isFocused || isHovered)} />\n </div>\n );\n}\n\nexport default OtpInput;\n"],"mappings":"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"}
|