@ahrowe/ui 0.50.0 → 0.51.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @ahrowe/ui
2
2
 
3
+ ## 0.51.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f26bb70: **Breaking:** `Dropdown`'s `onScroll`, which was never called, is `onLoadMore`, as on VirtualList and ChatMessageList. It loads as the end of the open list comes near, with a spinner meanwhile, and in a bottom sheet the list now scrolls itself. See "Migration from `onScroll`" in docs/Dropdown.md
8
+
9
+ ### Patch Changes
10
+
11
+ - f26bb70: `ChatMessageList` starts loading older messages shortly before the top comes into view, and no longer calls `onLoadMore` again after a load that added nothing
12
+ - f26bb70: Fixed `Loader`'s Spinner and Comet making a scroll container they end gain and lose a scrollbar on every turn, which pushed the loader sideways
13
+
3
14
  ## 0.50.0
4
15
 
5
16
  ### Minor Changes
package/README.md CHANGED
@@ -39,7 +39,7 @@ Things that might rule the package out.
39
39
  that and no polyfill ships. Two details want a newer Firefox: ScrollFade's edge fade eases from
40
40
  128 and snaps before it, and OptionPicker rings the whole group from 121. Both still work either
41
41
  way. CI only runs Chromium.
42
- - **One stylesheet.** `@ahrowe/ui/style.css` is 138 KB, 26 KB gzipped, and covers everything. There
42
+ - **One stylesheet.** `@ahrowe/ui/style.css` is 180 KB, 35 KB gzipped, and covers everything. There
43
43
  is no per-component CSS. It sits in `@layer ahrowe-ui`, so your own unlayered CSS wins no matter
44
44
  which loads first.
45
45
  - **No right to left.** The stylesheets use `margin-left` and friends rather than logical
@@ -15,7 +15,7 @@ export interface ChatMessageListHandle {
15
15
  export interface ChatMessageListProps extends HtmlProps<HTMLDivElement> {
16
16
  /** `ChatMessage`s, and anything between them: a `Divider` with a date, a typing indicator. Oldest first. */
17
17
  children?: ReactNode;
18
- /** Loads older messages when the user scrolls to the top. Prepend them; the view stays where it was. Leave it out once there are no more. */
18
+ /** Loads older messages as the user nears the top. Prepend them; the view stays where it was. Leave it out once there are no more. */
19
19
  onLoadMore?: () => Promise<void>;
20
20
  ref?: Ref<ChatMessageListHandle>;
21
21
  /** Overrides the text ChatMessageList renders of its own. Merged per key. */
@@ -8,7 +8,7 @@ export interface DropdownItem {
8
8
  disabled?: boolean;
9
9
  }
10
10
  export type DropdownSlots = 'root' | 'label' | 'field' | 'control' | 'icon' | 'placeholder' | 'list' | 'option';
11
- export interface DropdownProps extends Omit<HtmlProps, 'onFocus' | 'onBlur' | 'onMouseEnter' | 'onMouseLeave'> {
11
+ export interface DropdownProps extends Omit<HtmlProps, 'onFocus' | 'onBlur' | 'onMouseEnter' | 'onMouseLeave' | 'onScroll'> {
12
12
  formValidator?: FormValidator | null;
13
13
  onMouseEnter?: ((event: React.MouseEvent) => void) | null;
14
14
  onMouseLeave?: ((event: React.MouseEvent) => void) | null;
@@ -27,7 +27,8 @@ export interface DropdownProps extends Omit<HtmlProps, 'onFocus' | 'onBlur' | 'o
27
27
  isRow?: boolean;
28
28
  disabled?: boolean;
29
29
  readOnly?: boolean;
30
- onScroll?: () => Promise<void>;
30
+ /** Loads more items as the end of the open list comes near. Pass `undefined` once there are no more. */
31
+ onLoadMore?: () => Promise<void>;
31
32
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
32
33
  dropdownLabel?: string;
33
34
  isDropdownListWider?: boolean;
@@ -1,5 +1,5 @@
1
1
  import { default as React } from 'react';
2
- export declare function useInfiniteScroll(containerRef: React.RefObject<HTMLDivElement | null>, onScroll: () => Promise<void>, disabled: boolean): {
2
+ export declare function useInfiniteScroll(containerRef: React.RefObject<HTMLDivElement | null>, onLoadMore: (() => Promise<void>) | undefined): {
3
3
  isLoading: boolean;
4
4
  sentinelRef: React.RefObject<HTMLDivElement | null>;
5
5
  };
@@ -1,2 +1,2 @@
1
- "use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{useLabels as t}from"../configProvider/useLabels.mjs";import{ThemeColor as n}from"../themeProvider/color.types.mjs";import r from"../loader/loader.mjs";import{ButtonStyleType as i}from"../button/button.types.mjs";import a from"../actionIcon/actionIcon.mjs";import{useInfiniteScroll as o}from"../infiniteBlock/useInfiniteScroll.mjs";import s from"./chatMessageList.module.mjs";import{useCallback as c,useImperativeHandle as l,useLayoutEffect as u,useRef as d,useState as f}from"react";import p from"classnames";import{faArrowDown as m}from"@fortawesome/free-solid-svg-icons";import{jsx as h,jsxs as g}from"react/jsx-runtime";var _={messages:`Messages`,scrollToBottom:`Jump to latest`},v=32,y=()=>Promise.resolve();function b(e,t,n){let r=e.getBoundingClientRect().top,i=t.children,a=+(i[0]===n),o=i.length-1,s=null;for(;a<=o;){let e=a+o>>1;i[e].getBoundingClientRect().bottom>r?(s=i[e],o=e-1):a=e+1}return s&&{element:s,top:s.getBoundingClientRect().top-r}}function x(){return typeof window.matchMedia==`function`&&window.matchMedia(`(prefers-reduced-motion: reduce)`).matches}function S(S){let{children:C,onLoadMore:w,ref:T,labels:E,className:D,style:O,classNames:k,styles:A,...j}=e(`ChatMessageList`,S),M=t(`ChatMessageList`,E,_),N=d(null),P=d(null),F=d(!0),I=d(0),L=d(null),R=d(null),[z,B]=f(!1),[V,H]=f(!1),{isLoading:U,sentinelRef:W}=o(N,w??y,!w),G=()=>{let e=N.current,t=P.current;if(!e||!t)return;let{scrollTop:n,scrollHeight:r,clientHeight:i}=e,a=r-n-i;n<I.current&&a>1?F.current=!1:a<=v&&(F.current=!0),I.current=n,B(!F.current),F.current&&H(!1),L.current=b(e,t,W.current)};u(()=>{let e=N.current,t=P.current;if(!e||!t)return;let n=()=>{let n=t.lastElementChild;if(!F.current&&n!==R.current&&H(!0),R.current=n,F.current)e.scrollTop=e.scrollHeight;else{let t=L.current;t?.element.isConnected&&(e.scrollTop+=t.element.getBoundingClientRect().top-e.getBoundingClientRect().top-t.top)}I.current=e.scrollTop,L.current=b(e,t,W.current)};n();let r=new ResizeObserver(n);return r.observe(e),r.observe(t),()=>r.disconnect()},[W]);let K=c(()=>{let e=N.current;e&&(F.current=!0,B(!1),H(!1),e.scrollTo({top:e.scrollHeight,behavior:x()?`auto`:`smooth`}))},[]);return l(T,()=>({scrollToBottom:K}),[K]),g(`div`,{...j,className:p(s.chatMessageList,D),style:O,children:[h(`div`,{ref:N,role:`log`,"aria-label":M.messages,"aria-busy":U||void 0,tabIndex:0,className:p(s.chatMessageListViewport,k?.viewport),style:A?.viewport,onScroll:G,children:g(`div`,{ref:P,className:p(s.chatMessageListContent,k?.content),style:A?.content,children:[w&&h(`div`,{ref:W,className:s.chatMessageListLoader,children:U&&h(r,{size:`1.5em`})}),C]})}),z&&h(a,{icon:m,styleType:V?i.Filled:i.Plain,color:V?n.Primary:n.Neutral,"aria-label":M.scrollToBottom,title:M.scrollToBottom,className:p(s.chatMessageListScrollButton,!V&&s.chatMessageListScrollButtonSurface,k?.scrollButton),style:A?.scrollButton,onClick:K})]})}export{S as default};
1
+ "use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{useLabels as t}from"../configProvider/useLabels.mjs";import{ThemeColor as n}from"../themeProvider/color.types.mjs";import r from"../loader/loader.mjs";import{ButtonStyleType as i}from"../button/button.types.mjs";import a from"../actionIcon/actionIcon.mjs";import{useInfiniteScroll as o}from"../infiniteBlock/useInfiniteScroll.mjs";import s from"./chatMessageList.module.mjs";import{useCallback as c,useImperativeHandle as l,useLayoutEffect as u,useRef as d,useState as f}from"react";import p from"classnames";import{faArrowDown as m}from"@fortawesome/free-solid-svg-icons";import{jsx as h,jsxs as g}from"react/jsx-runtime";var _={messages:`Messages`,scrollToBottom:`Jump to latest`},v=32;function y(e,t,n){let r=e.getBoundingClientRect().top,i=t.children,a=+(i[0]===n),o=i.length-1,s=null;for(;a<=o;){let e=a+o>>1;i[e].getBoundingClientRect().bottom>r?(s=i[e],o=e-1):a=e+1}return s&&{element:s,top:s.getBoundingClientRect().top-r}}function b(){return typeof window.matchMedia==`function`&&window.matchMedia(`(prefers-reduced-motion: reduce)`).matches}function x(x){let{children:S,onLoadMore:C,ref:w,labels:T,className:E,style:D,classNames:O,styles:k,...A}=e(`ChatMessageList`,x),j=t(`ChatMessageList`,T,_),M=d(null),N=d(null),P=d(!0),F=d(0),I=d(null),L=d(null),[R,z]=f(!1),[B,V]=f(!1),{isLoading:H,sentinelRef:U}=o(M,C),W=()=>{let e=M.current,t=N.current;if(!e||!t)return;let{scrollTop:n,scrollHeight:r,clientHeight:i}=e,a=r-n-i;n<F.current&&a>1?P.current=!1:a<=v&&(P.current=!0),F.current=n,z(!P.current),P.current&&V(!1),I.current=y(e,t,U.current)};u(()=>{let e=M.current,t=N.current;if(!e||!t)return;let n=()=>{let n=t.lastElementChild;if(!P.current&&n!==L.current&&V(!0),L.current=n,P.current)e.scrollTop=e.scrollHeight;else{let t=I.current;t?.element.isConnected&&(e.scrollTop+=t.element.getBoundingClientRect().top-e.getBoundingClientRect().top-t.top)}F.current=e.scrollTop,I.current=y(e,t,U.current)};n();let r=new ResizeObserver(n);return r.observe(e),r.observe(t),()=>r.disconnect()},[U]);let G=c(()=>{let e=M.current;e&&(P.current=!0,z(!1),V(!1),e.scrollTo({top:e.scrollHeight,behavior:b()?`auto`:`smooth`}))},[]);return l(w,()=>({scrollToBottom:G}),[G]),g(`div`,{...A,className:p(s.chatMessageList,E),style:D,children:[h(`div`,{ref:M,role:`log`,"aria-label":j.messages,"aria-busy":H||void 0,tabIndex:0,className:p(s.chatMessageListViewport,O?.viewport),style:k?.viewport,onScroll:W,children:g(`div`,{ref:N,className:p(s.chatMessageListContent,O?.content),style:k?.content,children:[C&&h(`div`,{ref:U,className:s.chatMessageListLoader,children:H&&h(r,{size:`1.5em`})}),S]})}),R&&h(a,{icon:m,styleType:B?i.Filled:i.Plain,color:B?n.Primary:n.Neutral,"aria-label":j.scrollToBottom,title:j.scrollToBottom,className:p(s.chatMessageListScrollButton,!B&&s.chatMessageListScrollButtonSurface,O?.scrollButton),style:k?.scrollButton,onClick:G})]})}export{x as default};
2
2
  //# sourceMappingURL=chatMessageList.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"chatMessageList.mjs","names":[],"sources":["../../../../package/common/chatMessageList/chatMessageList.tsx"],"sourcesContent":["import { useCallback, useImperativeHandle, useLayoutEffect, useRef, useState, type ReactElement } from 'react';\nimport cx from 'classnames';\nimport { faArrowDown } from '@fortawesome/free-solid-svg-icons';\n\nimport ActionIcon from '../actionIcon';\nimport { ButtonStyleType } from '../button/button.types';\nimport Loader from '../loader';\nimport { useComponentDefaults, useLabels } from '../configProvider';\nimport { useInfiniteScroll } from '../infiniteBlock/useInfiniteScroll';\nimport { ThemeColor } from '../themeProvider/color.types';\nimport type { ChatMessageListLabels, ChatMessageListProps } from './chatMessageList.types';\nimport styles from './chatMessageList.module.pcss';\n\nconst DEFAULT_LABELS: Required<ChatMessageListLabels> = {\n messages: 'Messages',\n scrollToBottom: 'Jump to latest',\n};\n\n/** How close to the bottom a user scrolling down gets before new messages are followed again. */\nconst STICK_DISTANCE = 32;\n\nconst NOTHING_TO_LOAD = () => Promise.resolve();\n\ninterface Anchor {\n element: Element;\n /** Distance from the viewport's top edge. */\n top: number;\n}\n\n/**\n * The first message still showing at the top of the viewport, found by bisection because a long\n * conversation has thousands of children and this runs on every scroll event.\n */\nfunction findAnchor(viewport: HTMLElement, content: HTMLElement, skip: Element | null): Anchor | null {\n const viewportTop = viewport.getBoundingClientRect().top;\n const items = content.children;\n let low = items[0] === skip ? 1 : 0;\n let high = items.length - 1;\n let found: Element | null = null;\n while (low <= high) {\n const middle = (low + high) >> 1;\n if (items[middle].getBoundingClientRect().bottom > viewportTop) {\n found = items[middle];\n high = middle - 1;\n } else {\n low = middle + 1;\n }\n }\n return found && { element: found, top: found.getBoundingClientRect().top - viewportTop };\n}\n\nfunction prefersReducedMotion(): boolean {\n return typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n}\n\nfunction ChatMessageList(props: ChatMessageListProps): ReactElement {\n const {\n children,\n onLoadMore,\n ref,\n labels,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('ChatMessageList', props);\n const text = useLabels('ChatMessageList', labels, DEFAULT_LABELS);\n const viewportRef = useRef<HTMLDivElement>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n const isStuckRef = useRef(true);\n const lastScrollTopRef = useRef(0);\n const anchorRef = useRef<Anchor | null>(null);\n const lastChildRef = useRef<Element | null>(null);\n const [isAway, setIsAway] = useState(false);\n const [hasNew, setHasNew] = useState(false);\n const { isLoading, sentinelRef } = useInfiniteScroll(viewportRef, onLoadMore ?? NOTHING_TO_LOAD, !onLoadMore);\n\n const handleScroll = () => {\n const viewport = viewportRef.current;\n const content = contentRef.current;\n if (!viewport || !content) return;\n const { scrollTop, scrollHeight, clientHeight } = viewport;\n const distance = scrollHeight - scrollTop - clientHeight;\n // Any move up lets go, however small: a slow drag would otherwise be pulled back down by each\n // line of a streaming answer. The browser clamping scrollTop as content shrinks also moves it\n // up, but lands on the bottom, which is why the distance has to be checked too.\n if (scrollTop < lastScrollTopRef.current && distance > 1) isStuckRef.current = false;\n else if (distance <= STICK_DISTANCE) isStuckRef.current = true;\n lastScrollTopRef.current = scrollTop;\n setIsAway(!isStuckRef.current);\n if (isStuckRef.current) setHasNew(false);\n anchorRef.current = findAnchor(viewport, content, sentinelRef.current);\n };\n\n // Scroll anchoring by hand: Safari has no `overflow-anchor`, and where it exists it does nothing\n // at scrollTop 0, which is exactly where older messages get prepended. Runs in a ResizeObserver\n // callback, after layout and before paint, so neither the jump nor its correction is ever seen.\n useLayoutEffect(() => {\n const viewport = viewportRef.current;\n const content = contentRef.current;\n if (!viewport || !content) return undefined;\n\n const follow = () => {\n // A new last child is a message arriving; one prepended at the top or growing as it streams\n // leaves it the same.\n const last = content.lastElementChild;\n if (!isStuckRef.current && last !== lastChildRef.current) setHasNew(true);\n lastChildRef.current = last;\n if (isStuckRef.current) {\n viewport.scrollTop = viewport.scrollHeight;\n } else {\n const anchor = anchorRef.current;\n if (anchor?.element.isConnected) {\n viewport.scrollTop +=\n anchor.element.getBoundingClientRect().top - viewport.getBoundingClientRect().top - anchor.top;\n }\n }\n // So the scroll event this causes is not taken for the user scrolling up.\n lastScrollTopRef.current = viewport.scrollTop;\n anchorRef.current = findAnchor(viewport, content, sentinelRef.current);\n };\n\n follow();\n // The viewport too: a composer growing or a phone keyboard opening shrinks it from below.\n const observer = new ResizeObserver(follow);\n observer.observe(viewport);\n observer.observe(content);\n return () => observer.disconnect();\n }, [sentinelRef]);\n\n const scrollToBottom = useCallback(() => {\n const viewport = viewportRef.current;\n if (!viewport) return;\n isStuckRef.current = true;\n setIsAway(false);\n setHasNew(false);\n viewport.scrollTo({ top: viewport.scrollHeight, behavior: prefersReducedMotion() ? 'auto' : 'smooth' });\n }, []);\n\n useImperativeHandle(ref, () => ({ scrollToBottom }), [scrollToBottom]);\n\n return (\n <div {...rest} className={cx(styles.chatMessageList, className)} style={style}>\n <div\n ref={viewportRef}\n role='log'\n aria-label={text.messages}\n aria-busy={isLoading || undefined}\n // A stop of its own, so the history can be scrolled from the keyboard between messages\n // that hold nothing focusable.\n tabIndex={0}\n className={cx(styles.chatMessageListViewport, classNames?.viewport)}\n style={slotStyles?.viewport}\n onScroll={handleScroll}\n >\n <div\n ref={contentRef}\n className={cx(styles.chatMessageListContent, classNames?.content)}\n style={slotStyles?.content}\n >\n {onLoadMore && (\n <div ref={sentinelRef} className={styles.chatMessageListLoader}>\n {isLoading && <Loader size='1.5em' />}\n </div>\n )}\n {children}\n </div>\n </div>\n {isAway && (\n <ActionIcon\n icon={faArrowDown}\n styleType={hasNew ? ButtonStyleType.Filled : ButtonStyleType.Plain}\n color={hasNew ? ThemeColor.Primary : ThemeColor.Neutral}\n aria-label={text.scrollToBottom}\n title={text.scrollToBottom}\n className={cx(\n styles.chatMessageListScrollButton,\n !hasNew && styles.chatMessageListScrollButtonSurface,\n classNames?.scrollButton,\n )}\n style={slotStyles?.scrollButton}\n onClick={scrollToBottom}\n />\n )}\n </div>\n );\n}\n\nexport default ChatMessageList;\n"],"mappings":"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"}
1
+ {"version":3,"file":"chatMessageList.mjs","names":[],"sources":["../../../../package/common/chatMessageList/chatMessageList.tsx"],"sourcesContent":["import { useCallback, useImperativeHandle, useLayoutEffect, useRef, useState, type ReactElement } from 'react';\nimport cx from 'classnames';\nimport { faArrowDown } from '@fortawesome/free-solid-svg-icons';\n\nimport ActionIcon from '../actionIcon';\nimport { ButtonStyleType } from '../button/button.types';\nimport Loader from '../loader';\nimport { useComponentDefaults, useLabels } from '../configProvider';\nimport { useInfiniteScroll } from '../infiniteBlock/useInfiniteScroll';\nimport { ThemeColor } from '../themeProvider/color.types';\nimport type { ChatMessageListLabels, ChatMessageListProps } from './chatMessageList.types';\nimport styles from './chatMessageList.module.pcss';\n\nconst DEFAULT_LABELS: Required<ChatMessageListLabels> = {\n messages: 'Messages',\n scrollToBottom: 'Jump to latest',\n};\n\n/** How close to the bottom a user scrolling down gets before new messages are followed again. */\nconst STICK_DISTANCE = 32;\n\ninterface Anchor {\n element: Element;\n /** Distance from the viewport's top edge. */\n top: number;\n}\n\n/**\n * The first message still showing at the top of the viewport, found by bisection because a long\n * conversation has thousands of children and this runs on every scroll event.\n */\nfunction findAnchor(viewport: HTMLElement, content: HTMLElement, skip: Element | null): Anchor | null {\n const viewportTop = viewport.getBoundingClientRect().top;\n const items = content.children;\n let low = items[0] === skip ? 1 : 0;\n let high = items.length - 1;\n let found: Element | null = null;\n while (low <= high) {\n const middle = (low + high) >> 1;\n if (items[middle].getBoundingClientRect().bottom > viewportTop) {\n found = items[middle];\n high = middle - 1;\n } else {\n low = middle + 1;\n }\n }\n return found && { element: found, top: found.getBoundingClientRect().top - viewportTop };\n}\n\nfunction prefersReducedMotion(): boolean {\n return typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n}\n\nfunction ChatMessageList(props: ChatMessageListProps): ReactElement {\n const {\n children,\n onLoadMore,\n ref,\n labels,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('ChatMessageList', props);\n const text = useLabels('ChatMessageList', labels, DEFAULT_LABELS);\n const viewportRef = useRef<HTMLDivElement>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n const isStuckRef = useRef(true);\n const lastScrollTopRef = useRef(0);\n const anchorRef = useRef<Anchor | null>(null);\n const lastChildRef = useRef<Element | null>(null);\n const [isAway, setIsAway] = useState(false);\n const [hasNew, setHasNew] = useState(false);\n const { isLoading, sentinelRef } = useInfiniteScroll(viewportRef, onLoadMore);\n\n const handleScroll = () => {\n const viewport = viewportRef.current;\n const content = contentRef.current;\n if (!viewport || !content) return;\n const { scrollTop, scrollHeight, clientHeight } = viewport;\n const distance = scrollHeight - scrollTop - clientHeight;\n // Any move up lets go, however small: a slow drag would otherwise be pulled back down by each\n // line of a streaming answer. The browser clamping scrollTop as content shrinks also moves it\n // up, but lands on the bottom, which is why the distance has to be checked too.\n if (scrollTop < lastScrollTopRef.current && distance > 1) isStuckRef.current = false;\n else if (distance <= STICK_DISTANCE) isStuckRef.current = true;\n lastScrollTopRef.current = scrollTop;\n setIsAway(!isStuckRef.current);\n if (isStuckRef.current) setHasNew(false);\n anchorRef.current = findAnchor(viewport, content, sentinelRef.current);\n };\n\n // Scroll anchoring by hand: Safari has no `overflow-anchor`, and where it exists it does nothing\n // at scrollTop 0, which is exactly where older messages get prepended. Runs in a ResizeObserver\n // callback, after layout and before paint, so neither the jump nor its correction is ever seen.\n useLayoutEffect(() => {\n const viewport = viewportRef.current;\n const content = contentRef.current;\n if (!viewport || !content) return undefined;\n\n const follow = () => {\n // A new last child is a message arriving; one prepended at the top or growing as it streams\n // leaves it the same.\n const last = content.lastElementChild;\n if (!isStuckRef.current && last !== lastChildRef.current) setHasNew(true);\n lastChildRef.current = last;\n if (isStuckRef.current) {\n viewport.scrollTop = viewport.scrollHeight;\n } else {\n const anchor = anchorRef.current;\n if (anchor?.element.isConnected) {\n viewport.scrollTop +=\n anchor.element.getBoundingClientRect().top - viewport.getBoundingClientRect().top - anchor.top;\n }\n }\n // So the scroll event this causes is not taken for the user scrolling up.\n lastScrollTopRef.current = viewport.scrollTop;\n anchorRef.current = findAnchor(viewport, content, sentinelRef.current);\n };\n\n follow();\n // The viewport too: a composer growing or a phone keyboard opening shrinks it from below.\n const observer = new ResizeObserver(follow);\n observer.observe(viewport);\n observer.observe(content);\n return () => observer.disconnect();\n }, [sentinelRef]);\n\n const scrollToBottom = useCallback(() => {\n const viewport = viewportRef.current;\n if (!viewport) return;\n isStuckRef.current = true;\n setIsAway(false);\n setHasNew(false);\n viewport.scrollTo({ top: viewport.scrollHeight, behavior: prefersReducedMotion() ? 'auto' : 'smooth' });\n }, []);\n\n useImperativeHandle(ref, () => ({ scrollToBottom }), [scrollToBottom]);\n\n return (\n <div {...rest} className={cx(styles.chatMessageList, className)} style={style}>\n <div\n ref={viewportRef}\n role='log'\n aria-label={text.messages}\n aria-busy={isLoading || undefined}\n // A stop of its own, so the history can be scrolled from the keyboard between messages\n // that hold nothing focusable.\n tabIndex={0}\n className={cx(styles.chatMessageListViewport, classNames?.viewport)}\n style={slotStyles?.viewport}\n onScroll={handleScroll}\n >\n <div\n ref={contentRef}\n className={cx(styles.chatMessageListContent, classNames?.content)}\n style={slotStyles?.content}\n >\n {onLoadMore && (\n <div ref={sentinelRef} className={styles.chatMessageListLoader}>\n {isLoading && <Loader size='1.5em' />}\n </div>\n )}\n {children}\n </div>\n </div>\n {isAway && (\n <ActionIcon\n icon={faArrowDown}\n styleType={hasNew ? ButtonStyleType.Filled : ButtonStyleType.Plain}\n color={hasNew ? ThemeColor.Primary : ThemeColor.Neutral}\n aria-label={text.scrollToBottom}\n title={text.scrollToBottom}\n className={cx(\n styles.chatMessageListScrollButton,\n !hasNew && styles.chatMessageListScrollButtonSurface,\n classNames?.scrollButton,\n )}\n style={slotStyles?.scrollButton}\n onClick={scrollToBottom}\n />\n )}\n </div>\n );\n}\n\nexport default ChatMessageList;\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{usePresentationDefault as t}from"../configProvider/usePresentationDefault.mjs";import{Presentation as n}from"../configProvider/presentation.types.mjs";import r from"../loader/loader.mjs";import i from"../styles/sheet.module.mjs";import{PopoverAlign as ee,PopoverPlacement as te}from"../popover/popover.types.mjs";import ne from"../hooks/useSheet.mjs";import re from"../popover/popover.mjs";import{useInfiniteScroll as ie}from"../infiniteBlock/useInfiniteScroll.mjs";import ae from"../tooltip/tooltip.mjs";import{edgeIndex as oe,nextIndex as se,scrollOptionIntoView as ce}from"../utils/listNavigation.mjs";import a from"./dropdown.module.mjs";import{useEffect as o,useId as le,useLayoutEffect as ue,useRef as s,useState as c}from"react";import l from"classnames";import{FontAwesomeIcon as de}from"@fortawesome/react-fontawesome";import{faChevronDown as fe}from"@fortawesome/free-solid-svg-icons";import{jsx as u,jsxs as d}from"react/jsx-runtime";var pe=500;function f(f){let{errorMessage:p=``,className:me=``,style:he,label:m=``,value:ge=``,isValid:_e=!0,tabIndex:ve=0,isRequired:ye=!1,customProps:h={},isRow:be=!1,disabled:g=!1,readOnly:_=!1,onScroll:xe=()=>Promise.resolve(),onClick:v,onChange:y,onFocus:Se=null,onBlur:Ce=null,onMouseEnter:we=null,onMouseLeave:Te=null,dropdownLabel:Ee,isDropdownListWider:b=!1,formValidator:x=null,classNames:S,styles:C,items:w=[],useMatLabelStyle:T=!0,alwaysFloatLabel:E=!1,placeholder:D=``,plainStyle:O=!1,scrollToSelected:De=!1,presentation:Oe,reactRef:ke,...Ae}=e(`Dropdown`,f),[je,Me]=c(!1),[Ne,Pe]=c(!1),[,Fe]=c(0),k=s(x),A=s(null),j=s(null),M=s(null),[Ie,Le]=c(!1),[N,P]=c(null),F=s({buffer:``,at:0}),I=le(),L=`${I}-listbox`,R=`${I}-label`,z=e=>`${I}-option-${e}`,B=!g&&!_,V=Ie&&B,{isLoading:Re,sentinelRef:ze}=ie(M,xe,!1),Be=t(),H=Oe??Be??n.Default,{isSheet:U}=ne(H);ue(()=>{let e=A.current,t=j.current;V&&!U&&e&&t&&(t.style.fontSize=window.getComputedStyle(e).fontSize)},[V,U]),o(()=>{if(k.current=x,!k.current)return;k.current.label=m??``,k.current.validate();let e=()=>Fe(e=>e+1);return k.current.registerOnUpdateListener(e),()=>k.current?.removeOnUpdateListener(e)},[x,m]);let W=x?x.value:ge,G=w.findIndex(e=>e.key===W);o(()=>{V&&N!==null&&ce(M.current,document.getElementById(z(N)))},[V,N]);let Ve=e=>{Pe(!0),we?.(e)},He=e=>{Pe(!1),Te?.(e)},Ue=e=>{Me(!0),Se?.(e)},We=e=>{k.current&&(k.current.touched=!0),Me(!1),Ce?.(e)},Ge=()=>x?x.touched&&x.hasError():!_e||!!p,K=()=>!!(ye||x?.validators.find(e=>e.validatorId===`required`)),Ke=e=>{x?(x.set(e),y?.(x.value,h)):y?.(e,h)};function q(e){(!e||B)&&(Le(e),e?P(De&&G>=0?G:null):F.current={buffer:``,at:0})}function J(e){e.disabled||(Ke(e.key),q(!1))}let qe={isDisabled:e=>!!w[e].disabled};function Je(e){let t=Date.now(),n=t-F.current.at>pe?e:F.current.buffer+e;F.current={buffer:n,at:t};let r=n.toLowerCase(),i=w.findIndex(e=>!e.disabled&&typeof e.label==`string`&&e.label.toLowerCase().startsWith(r));i!==-1&&(V?P(i):J(w[i]))}function Ye(e){if(B){if(!V){e.key===`ArrowDown`||e.key===`ArrowUp`||e.key===`Enter`||e.key===` `?(e.preventDefault(),q(!0)):e.key.length===1&&Je(e.key);return}if(e.key===`ArrowDown`||e.key===`ArrowUp`)e.preventDefault(),P(se(N,e.key===`ArrowDown`?1:-1,w.length,qe));else if(e.key===`Home`||e.key===`End`)e.preventDefault(),P(oe(e.key===`Home`?`first`:`last`,w.length,qe));else if(e.key===`Enter`||e.key===` `){e.preventDefault();let t=N===null?void 0:w[N];t?J(t):q(!1)}else e.key===`Escape`||e.key===`Tab`?q(!1):e.key.length===1&&Je(e.key)}}let Y=Ee,X=Ge();w.forEach(e=>{e.key===W&&(Y=e.label)});let Z=Y||W,Q=Z==null||typeof Z==`string`&&Z.trim()===``,Xe=K()?u(`span`,{className:a.requiredMark,children:`*`}):null,Ze=m&&K()?`${m} *`:m,$=je&&!_;return d(`div`,{...Ae,className:l(me,S?.root,a.dropdownLabel,a.dropdown,{[a.dropdownLabelColumn]:!be,[a.dropdownWithLabel]:T&&!!m}),style:{...he,...C?.root},onMouseEnter:Ve,onMouseLeave:He,children:[u(`div`,{children:u(re,{isOpen:V,onOpenChange:q,disabled:!B,placement:te.Bottom,align:ee.Start,offset:0,withArrow:!1,constrainWidth:!1,width:`trigger`,focusOnOpen:!1,presentation:H,classNames:{panel:a.dropdownPanel},content:u(`div`,{ref:j,className:l(!U&&a.dropdownInputListAnim,{[a.dropdownInputListAnimWider]:b&&!U}),children:d(`div`,{ref:M,id:L,role:`listbox`,"aria-label":m||D||void 0,className:l(a.dropdownInputList,U&&i.sheetScroll,S?.list),style:C?.list,children:[w.map((e,t)=>u(`div`,{id:z(t),role:`option`,"aria-selected":e.key===W,"aria-disabled":e.disabled||void 0,style:C?.option,className:l(a.dropdownInputListItem,S?.option,{[a.dropdownInputListItemActive]:N===t,[a.dropdownInputListItemSelected]:e.key===W,[a.dropdownInputListItemDisabled]:!!e.disabled}),onMouseDown:e=>e.preventDefault(),onMouseEnter:()=>P(t),onClick:()=>J(e),children:e.label},e.key)),u(`div`,{ref:ze,className:a.dropdownSentinel,"aria-hidden":`true`}),Re&&u(`div`,{className:a.dropdownLoadMore,"aria-hidden":`true`,children:u(r,{size:32})})]})}),children:d(`div`,{ref:A,onFocus:Ue,onBlur:We,className:l(a.dropdownInput,{[a.dropdownInputPlain]:O,[a.dropdownInputReadOnly]:_&&!g},S?.field),style:C?.field,children:[u(`button`,{tabIndex:ve,type:`button`,disabled:g,role:`combobox`,"aria-haspopup":`listbox`,"aria-expanded":V,"aria-controls":L,"aria-activedescendant":V&&N!==null?z(N):void 0,"aria-labelledby":m?R:void 0,"aria-label":m?void 0:D||void 0,"aria-readonly":_||void 0,className:l(a.dropdownInputButton,{[a.dropdownInputButtonDisabled]:g,[a.dropdownInputButtonReadOnly]:_&&!g},S?.control),style:{...b?{padding:`6px`,border:`none`}:{},...C?.control},onKeyDown:Ye,onClick:v,ref:ke,children:Q?`\xA0`:Z}),u(`button`,{type:`button`,tabIndex:-1,disabled:g,"aria-hidden":`true`,className:l(a.dropdownInputButtonIcon,S?.icon),style:C?.icon,onMouseDown:e=>e.preventDefault(),onClick:v,children:!g&&!_&&u(de,{icon:fe,className:l(a.dropdownInputButtonIconArrow,{[a.dropdownInputButtonIconArrowOpen]:V})})}),u(`fieldset`,{className:l(a.dropdownFieldset,{[a.dropdownBorderPlain]:O,[a.dropdownBorderFocused]:$,[a.dropdownBorderInvalid]:X,[a.dropdownBorderError]:X}),children:u(`legend`,{className:l(a.dropdownFieldsetLegend,{[a.dropdownFieldsetLegendActive]:T&&(E||$||!Q)}),children:u(`span`,{children:Ze})})}),T&&m&&d(`div`,{id:R,className:l(a.dropdownMatLabel,{[a.dropdownMatLabelFloating]:E||$||!Q},S?.label),style:C?.label,children:[m,Xe]}),T&&D&&u(`div`,{className:l(a.dropdownPlaceholder,$&&!Y&&a.dropdownPlaceholderShown),children:D})]})})}),m&&!T&&d(`div`,{id:R,className:l(a.dropdownLabelText,S?.label),style:C?.label,children:[m,Xe]}),u(ae,{variant:`error`,message:x?x.getCurrentErrorMessage():p||``,isVisible:X&&(je||Ne)})]})}export{f as default};
1
+ "use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{usePresentationDefault as t}from"../configProvider/usePresentationDefault.mjs";import{Presentation as n}from"../configProvider/presentation.types.mjs";import ee from"../loader/loader.mjs";import r from"../styles/sheet.module.mjs";import{PopoverAlign as te,PopoverPlacement as ne}from"../popover/popover.types.mjs";import re from"../hooks/useSheet.mjs";import ie from"../popover/popover.mjs";import{useInfiniteScroll as ae}from"../infiniteBlock/useInfiniteScroll.mjs";import oe from"../tooltip/tooltip.mjs";import{edgeIndex as se,nextIndex as ce,scrollOptionIntoView as le}from"../utils/listNavigation.mjs";import i from"./dropdown.module.mjs";import{useEffect as ue,useId as de,useLayoutEffect as fe,useRef as a,useState as o}from"react";import s from"classnames";import{FontAwesomeIcon as pe}from"@fortawesome/react-fontawesome";import{faChevronDown as me}from"@fortawesome/free-solid-svg-icons";import{jsx as c,jsxs as l}from"react/jsx-runtime";var he=500;function u(u){let{errorMessage:ge=``,className:_e=``,style:ve,label:d=``,value:ye=``,isValid:be=!0,tabIndex:xe=0,isRequired:Se=!1,customProps:Ce={},isRow:we=!1,disabled:f=!1,readOnly:p=!1,onLoadMore:Te,onClick:m,onChange:h,onFocus:Ee=null,onBlur:De=null,onMouseEnter:Oe=null,onMouseLeave:ke=null,dropdownLabel:Ae,isDropdownListWider:je=!1,formValidator:g=null,classNames:_,styles:v,items:y=[],useMatLabelStyle:b=!0,alwaysFloatLabel:x=!1,placeholder:S=``,plainStyle:C=!1,scrollToSelected:Me=!1,presentation:Ne,reactRef:Pe,...Fe}=e(`Dropdown`,u),[w,T]=o(!1),[Ie,E]=o(!1),[,Le]=o(0),D=a(g),O=a(null),Re=a(null),k=a(null),[ze,Be]=o(!1),[A,j]=o(null),M=a({buffer:``,at:0}),N=de(),P=`${N}-listbox`,F=`${N}-label`,I=e=>`${N}-option-${e}`,L=!f&&!p,R=ze&&L,{isLoading:z,sentinelRef:Ve}=ae(k,R?Te:void 0),He=t(),B=Ne??He??n.Default,{isSheet:V}=re(B);fe(()=>{let e=O.current,t=Re.current;R&&!V&&e&&t&&(t.style.fontSize=window.getComputedStyle(e).fontSize)},[R,V]),ue(()=>{if(D.current=g,!D.current)return;D.current.label=d??``,D.current.validate();let e=()=>Le(e=>e+1);return D.current.registerOnUpdateListener(e),()=>D.current?.removeOnUpdateListener(e)},[g,d]);let H=g?g.value:ye,U=y.findIndex(e=>e.key===H);ue(()=>{R&&A!==null&&le(k.current,document.getElementById(I(A)))},[R,A]);let Ue=e=>{E(!0),Oe?.(e)},We=e=>{E(!1),ke?.(e)},Ge=e=>{T(!0),Ee?.(e)},Ke=e=>{D.current&&(D.current.touched=!0),T(!1),De?.(e)},qe=()=>g?g.touched&&g.hasError():!be||!!ge,W=()=>!!(Se||g?.validators.find(e=>e.validatorId===`required`)),Je=e=>{g?(g.set(e),h?.(g.value,Ce)):h?.(e,Ce)};function G(e){(!e||L)&&(Be(e),e?j(Me&&U>=0?U:null):M.current={buffer:``,at:0})}function K(e){e.disabled||(Je(e.key),G(!1))}let q={isDisabled:e=>!!y[e].disabled};function J(e){let t=Date.now(),n=t-M.current.at>he?e:M.current.buffer+e;M.current={buffer:n,at:t};let ee=n.toLowerCase(),r=y.findIndex(e=>!e.disabled&&typeof e.label==`string`&&e.label.toLowerCase().startsWith(ee));r!==-1&&(R?j(r):K(y[r]))}function Ye(e){if(L){if(!R){e.key===`ArrowDown`||e.key===`ArrowUp`||e.key===`Enter`||e.key===` `?(e.preventDefault(),G(!0)):e.key.length===1&&J(e.key);return}if(e.key===`ArrowDown`||e.key===`ArrowUp`)e.preventDefault(),j(ce(A,e.key===`ArrowDown`?1:-1,y.length,q));else if(e.key===`Home`||e.key===`End`)e.preventDefault(),j(se(e.key===`Home`?`first`:`last`,y.length,q));else if(e.key===`Enter`||e.key===` `){e.preventDefault();let t=A===null?void 0:y[A];t?K(t):G(!1)}else e.key===`Escape`||e.key===`Tab`?G(!1):e.key.length===1&&J(e.key)}}let Y=Ae,X=qe();y.forEach(e=>{e.key===H&&(Y=e.label)});let Z=Y||H,Q=Z==null||typeof Z==`string`&&Z.trim()===``,Xe=W()?c(`span`,{className:i.requiredMark,children:`*`}):null,Ze=d&&W()?`${d} *`:d,$=w&&!p;return l(`div`,{...Fe,className:s(_e,_?.root,i.dropdownLabel,i.dropdown,{[i.dropdownLabelColumn]:!we,[i.dropdownWithLabel]:b&&!!d}),style:{...ve,...v?.root},onMouseEnter:Ue,onMouseLeave:We,children:[c(`div`,{children:c(ie,{isOpen:R,onOpenChange:G,disabled:!L,placement:ne.Bottom,align:te.Start,offset:0,withArrow:!1,constrainWidth:!1,width:`trigger`,focusOnOpen:!1,presentation:B,classNames:{panel:i.dropdownPanel},content:c(`div`,{ref:Re,className:s(V?i.dropdownInputListSheet:i.dropdownInputListAnim,{[i.dropdownInputListAnimWider]:je&&!V}),children:l(`div`,{ref:k,id:P,role:`listbox`,"aria-label":d||S||void 0,"aria-busy":z||void 0,className:s(i.dropdownInputList,V&&r.sheetScroll,_?.list),style:v?.list,children:[y.map((e,t)=>c(`div`,{id:I(t),role:`option`,"aria-selected":e.key===H,"aria-disabled":e.disabled||void 0,style:v?.option,className:s(i.dropdownInputListItem,_?.option,{[i.dropdownInputListItemActive]:A===t,[i.dropdownInputListItemSelected]:e.key===H,[i.dropdownInputListItemDisabled]:!!e.disabled}),onMouseDown:e=>e.preventDefault(),onMouseEnter:()=>j(t),onClick:()=>K(e),children:e.label},e.key)),c(`div`,{ref:Ve,className:i.dropdownSentinel,"aria-hidden":`true`}),z&&c(`div`,{className:i.dropdownLoadMore,"aria-hidden":`true`,children:c(ee,{size:`1.125em`})})]})}),children:l(`div`,{ref:O,onFocus:Ge,onBlur:Ke,className:s(i.dropdownInput,{[i.dropdownInputPlain]:C,[i.dropdownInputReadOnly]:p&&!f},_?.field),style:v?.field,children:[c(`button`,{tabIndex:xe,type:`button`,disabled:f,role:`combobox`,"aria-haspopup":`listbox`,"aria-expanded":R,"aria-controls":P,"aria-activedescendant":R&&A!==null?I(A):void 0,"aria-labelledby":d?F:void 0,"aria-label":d?void 0:S||void 0,"aria-readonly":p||void 0,className:s(i.dropdownInputButton,{[i.dropdownInputButtonDisabled]:f,[i.dropdownInputButtonReadOnly]:p&&!f},_?.control),style:{...je?{padding:`6px`,border:`none`}:{},...v?.control},onKeyDown:Ye,onClick:m,ref:Pe,children:Q?`\xA0`:Z}),c(`button`,{type:`button`,tabIndex:-1,disabled:f,"aria-hidden":`true`,className:s(i.dropdownInputButtonIcon,_?.icon),style:v?.icon,onMouseDown:e=>e.preventDefault(),onClick:m,children:!f&&!p&&c(pe,{icon:me,className:s(i.dropdownInputButtonIconArrow,{[i.dropdownInputButtonIconArrowOpen]:R})})}),c(`fieldset`,{className:s(i.dropdownFieldset,{[i.dropdownBorderPlain]:C,[i.dropdownBorderFocused]:$,[i.dropdownBorderInvalid]:X,[i.dropdownBorderError]:X}),children:c(`legend`,{className:s(i.dropdownFieldsetLegend,{[i.dropdownFieldsetLegendActive]:b&&(x||$||!Q)}),children:c(`span`,{children:Ze})})}),b&&d&&l(`div`,{id:F,className:s(i.dropdownMatLabel,{[i.dropdownMatLabelFloating]:x||$||!Q},_?.label),style:v?.label,children:[d,Xe]}),b&&S&&c(`div`,{className:s(i.dropdownPlaceholder,$&&!Y&&i.dropdownPlaceholderShown),children:S})]})})}),d&&!b&&l(`div`,{id:F,className:s(i.dropdownLabelText,_?.label),style:v?.label,children:[d,Xe]}),c(oe,{variant:`error`,message:g?g.getCurrentErrorMessage():ge||``,isVisible:X&&(w||Ie)})]})}export{u as default};
2
2
  //# sourceMappingURL=dropdown.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"dropdown.mjs","names":[],"sources":["../../../../package/common/dropdown/dropdown.tsx"],"sourcesContent":["import React, { useEffect, useId, useLayoutEffect, useRef, useState } from 'react';\nimport cx from 'classnames';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faChevronDown as dropdownIcon } from '@fortawesome/free-solid-svg-icons';\nimport Tooltip from 'components/common/tooltip';\n\nimport Popover from '../popover';\nimport Loader from '../loader';\nimport { useInfiniteScroll } from '../infiniteBlock/useInfiniteScroll';\nimport { edgeIndex, nextIndex, scrollOptionIntoView, type ActiveIndex } from '../utils/listNavigation';\nimport { PopoverAlign, PopoverPlacement } from '../popover/popover.types';\nimport { useComponentDefaults, usePresentationDefault } from '../configProvider';\nimport { useSheet } from '../hooks/useSheet';\nimport { Presentation } from '../configProvider/presentation.types';\nimport styles from './dropdown.module.pcss';\nimport sheetStyles from '../styles/sheet.module.pcss';\nimport type { DropdownProps, DropdownItem } from './dropdown.types';\n\n/** How long a pause resets the type-ahead buffer, matching a native `<select>`. */\nconst TYPEAHEAD_RESET_MS = 500;\n\nfunction Dropdown(props: DropdownProps) {\n const {\n errorMessage = '',\n className = '',\n style,\n label = '',\n value = '',\n isValid = true,\n tabIndex = 0,\n isRequired = false,\n customProps = {},\n isRow = false,\n disabled = false,\n readOnly = false,\n onScroll = () => Promise.resolve(),\n onClick,\n onChange,\n onFocus: onFocusProp = null,\n onBlur: onBlurProp = null,\n onMouseEnter: onMouseEnterProp = null,\n onMouseLeave: onMouseLeaveProp = null,\n dropdownLabel: dropdownLabelProp,\n isDropdownListWider = false,\n formValidator = null,\n classNames,\n styles: slotStyles,\n items = [],\n useMatLabelStyle = true,\n alwaysFloatLabel = false,\n placeholder = '',\n plainStyle = false,\n scrollToSelected = false,\n presentation: presentationProp,\n reactRef,\n ...rest\n } = useComponentDefaults('Dropdown', props);\n const [isFocused, setIsFocused] = useState(false);\n const [isHovered, setIsHovered] = useState(false);\n const [, forceUpdate] = useState(0);\n const fvRef = useRef(formValidator);\n const triggerRef = useRef<HTMLDivElement>(null);\n const anchorRef = useRef<HTMLDivElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const [menuIsOpen, setMenuIsOpen] = useState(false);\n // `null` is \"nothing highlighted\", the state a list opens in unless `scrollToSelected`\n // asks for the current value. Arrowing from there enters the list at either end.\n const [activeIndex, setActiveIndex] = useState<ActiveIndex>(null);\n const typeahead = useRef({ buffer: '', at: 0 });\n\n const baseId = useId();\n const listboxId = `${baseId}-listbox`;\n const labelId = `${baseId}-label`;\n const optionId = (index: number) => `${baseId}-option-${index}`;\n\n const isInteractive = !disabled && !readOnly;\n const isOpen = menuIsOpen && isInteractive;\n\n // The list is its own scroll container. It used to render through an `InfiniteBlock`, which\n // also brought a tabbable container and a second key handler: inside a listbox that put a\n // focusable element with no role in the tab order and gave the arrows two owners.\n const { isLoading: showScrollSpinner, sentinelRef } = useInfiniteScroll(listRef, onScroll, false);\n\n // Explicit prop → this component's global default → the app-wide one → anchored.\n // Resolved here rather than left to Popover so that this component and the panel\n // it opens can never disagree about whether they are presenting as a sheet.\n const appPresentation = usePresentationDefault();\n const presentation = presentationProp ?? appPresentation ?? Presentation.Default;\n const { isSheet } = useSheet(presentation);\n\n // The list reads at the field's size, which is not always the page's: a Dropdown\n // inside something with a smaller font should not open a list at the default one.\n // Width is Popover's job, through `width='trigger'`.\n useLayoutEffect(() => {\n const trigger = triggerRef.current;\n const menu = anchorRef.current;\n if (!isOpen || isSheet || !trigger || !menu) return;\n menu.style.fontSize = window.getComputedStyle(trigger).fontSize;\n }, [isOpen, isSheet]);\n\n useEffect(() => {\n fvRef.current = formValidator;\n if (!fvRef.current) return;\n fvRef.current.label = label ?? '';\n fvRef.current.validate();\n const listener = () => forceUpdate((n) => n + 1);\n fvRef.current.registerOnUpdateListener(listener);\n return () => fvRef.current?.removeOnUpdateListener(listener);\n }, [formValidator, label]);\n\n const currentValue = formValidator ? (formValidator.value as string | number) : value;\n const selectedIndex = items.findIndex((item) => item.key === currentValue);\n\n useEffect(() => {\n if (!isOpen || activeIndex === null) return;\n scrollOptionIntoView(listRef.current, document.getElementById(optionId(activeIndex)));\n // optionId is derived from a stable useId, so it cannot go stale between renders.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen, activeIndex]);\n\n const onMouseEnterHandler = (event: React.MouseEvent) => {\n setIsHovered(true);\n onMouseEnterProp?.(event);\n };\n\n const onMouseLeaveHandler = (event: React.MouseEvent) => {\n setIsHovered(false);\n onMouseLeaveProp?.(event);\n };\n\n const onFocusHandler = (event: React.FocusEvent) => {\n setIsFocused(true);\n onFocusProp?.(event);\n };\n\n const onBlurHandler = (event: React.FocusEvent) => {\n if (fvRef.current) fvRef.current.touched = true;\n setIsFocused(false);\n onBlurProp?.(event);\n };\n\n const hasError = (): boolean => {\n return formValidator ? formValidator.touched && formValidator.hasError() : !isValid || !!errorMessage;\n };\n\n const computedIsRequired = (): boolean => {\n return !!(\n isRequired || formValidator?.validators.find((v) => (v as { validatorId?: string }).validatorId === 'required')\n );\n };\n\n const onChangeHandler = (val: string | number): void => {\n if (formValidator) {\n formValidator.set(val);\n onChange?.(formValidator.value, customProps);\n } else {\n onChange?.(val, customProps);\n }\n };\n\n function handleOpenChange(next: boolean) {\n if (next && !isInteractive) return;\n setMenuIsOpen(next);\n if (next) setActiveIndex(scrollToSelected && selectedIndex >= 0 ? selectedIndex : null);\n else typeahead.current = { buffer: '', at: 0 };\n }\n\n function select(item: DropdownItem) {\n if (item.disabled) return;\n onChangeHandler(item.key);\n handleOpenChange(false);\n }\n\n const navigation = { isDisabled: (index: number) => !!items[index].disabled };\n\n /** Jumps to the option whose label starts with what was typed, as a native `<select>` does. */\n function handleTypeahead(key: string) {\n const now = Date.now();\n const buffer = now - typeahead.current.at > TYPEAHEAD_RESET_MS ? key : typeahead.current.buffer + key;\n typeahead.current = { buffer, at: now };\n\n const needle = buffer.toLowerCase();\n const match = items.findIndex(\n (item) => !item.disabled && typeof item.label === 'string' && item.label.toLowerCase().startsWith(needle),\n );\n if (match === -1) return;\n if (isOpen) setActiveIndex(match);\n else select(items[match]);\n }\n\n function handleKeyDown(event: React.KeyboardEvent) {\n if (!isInteractive) return;\n\n if (!isOpen) {\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter' || event.key === ' ') {\n // Without this the browser turns the key into a click on the trigger, which\n // Popover would read as a second toggle and close the list again.\n event.preventDefault();\n handleOpenChange(true);\n } else if (event.key.length === 1) {\n handleTypeahead(event.key);\n }\n return;\n }\n\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault();\n setActiveIndex(nextIndex(activeIndex, event.key === 'ArrowDown' ? 1 : -1, items.length, navigation));\n } else if (event.key === 'Home' || event.key === 'End') {\n event.preventDefault();\n setActiveIndex(edgeIndex(event.key === 'Home' ? 'first' : 'last', items.length, navigation));\n } else if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n const item = activeIndex === null ? undefined : items[activeIndex];\n if (item) select(item);\n else handleOpenChange(false);\n } else if (event.key === 'Escape' || event.key === 'Tab') {\n handleOpenChange(false);\n } else if (event.key.length === 1) {\n handleTypeahead(event.key);\n }\n }\n\n let dropdownLabel = dropdownLabelProp;\n const hasErr = hasError();\n\n items.forEach((item: DropdownItem) => {\n if (item.key === currentValue) {\n dropdownLabel = item.label as string;\n }\n });\n\n const displayValue = dropdownLabel || currentValue;\n const isDisplayBlank = displayValue == null || (typeof displayValue === 'string' && displayValue.trim() === '');\n\n const requiredMark = computedIsRequired() ? <span className={styles.requiredMark}>*</span> : null;\n const editedLabel = label && computedIsRequired() ? `${label} *` : label;\n\n const showFocus = isFocused && !readOnly;\n\n return (\n <div\n {...rest}\n className={cx(className, classNames?.root, styles.dropdownLabel, styles.dropdown, {\n [styles.dropdownLabelColumn]: !isRow,\n [styles.dropdownWithLabel]: useMatLabelStyle && !!label,\n })}\n style={{ ...style, ...slotStyles?.root }}\n onMouseEnter={onMouseEnterHandler}\n onMouseLeave={onMouseLeaveHandler}\n >\n <div>\n <Popover\n // Popover owns opening and closing, including the outside click, Escape and the\n // click on the field itself. The trigger only has to say what it wants.\n isOpen={isOpen}\n onOpenChange={handleOpenChange}\n disabled={!isInteractive}\n placement={PopoverPlacement.Bottom}\n align={PopoverAlign.Start}\n offset={0}\n withArrow={false}\n constrainWidth={false}\n width='trigger'\n // The focus stays on the trigger while the list is open, because that is what\n // `aria-activedescendant` points from.\n focusOnOpen={false}\n presentation={presentation}\n classNames={{ panel: styles.dropdownPanel }}\n content={\n <div\n ref={anchorRef}\n // A sheet's own panel does the clipping and the scrolling, and\n // spans the screen rather than sizing itself to the field.\n className={cx(!isSheet && styles.dropdownInputListAnim, {\n [styles.dropdownInputListAnimWider]: isDropdownListWider && !isSheet,\n })}\n >\n <div\n ref={listRef}\n id={listboxId}\n role='listbox'\n aria-label={label || placeholder || undefined}\n className={cx(styles.dropdownInputList, isSheet && sheetStyles.sheetScroll, classNames?.list)}\n style={slotStyles?.list}\n >\n {items.map((item: DropdownItem, index: number) => (\n <div\n key={item.key}\n id={optionId(index)}\n role='option'\n aria-selected={item.key === currentValue}\n aria-disabled={item.disabled || undefined}\n style={slotStyles?.option}\n className={cx(styles.dropdownInputListItem, classNames?.option, {\n [styles.dropdownInputListItemActive]: activeIndex === index,\n [styles.dropdownInputListItemSelected]: item.key === currentValue,\n [styles.dropdownInputListItemDisabled]: !!item.disabled,\n })}\n // Selecting must not move the focus off the trigger, which is what\n // `aria-activedescendant` is anchored to.\n onMouseDown={(event) => event.preventDefault()}\n onMouseEnter={() => setActiveIndex(index)}\n onClick={() => select(item)}\n >\n {item.label}\n </div>\n ))}\n <div ref={sentinelRef} className={styles.dropdownSentinel} aria-hidden='true' />\n {showScrollSpinner && (\n <div className={styles.dropdownLoadMore} aria-hidden='true'>\n <Loader size={32} />\n </div>\n )}\n </div>\n </div>\n }\n >\n <div\n ref={triggerRef}\n onFocus={onFocusHandler}\n onBlur={onBlurHandler}\n className={cx(\n styles.dropdownInput,\n {\n [styles.dropdownInputPlain]: plainStyle,\n [styles.dropdownInputReadOnly]: readOnly && !disabled,\n },\n classNames?.field,\n )}\n style={slotStyles?.field}\n >\n <button\n tabIndex={tabIndex}\n type='button'\n disabled={disabled}\n // A select-only combobox: the accessible name comes from the field's label,\n // and the element's own content is read as its value. A plain `button` role\n // would announce the value as the name and never mention the label.\n role='combobox'\n aria-haspopup='listbox'\n aria-expanded={isOpen}\n aria-controls={listboxId}\n aria-activedescendant={isOpen && activeIndex !== null ? optionId(activeIndex) : undefined}\n aria-labelledby={label ? labelId : undefined}\n aria-label={label ? undefined : placeholder || undefined}\n aria-readonly={readOnly || undefined}\n className={cx(\n styles.dropdownInputButton,\n {\n [styles.dropdownInputButtonDisabled]: disabled,\n [styles.dropdownInputButtonReadOnly]: readOnly && !disabled,\n },\n classNames?.control,\n )}\n style={{ ...(isDropdownListWider ? { padding: '6px', border: 'none' } : {}), ...slotStyles?.control }}\n onKeyDown={handleKeyDown}\n onClick={onClick}\n ref={reactRef}\n >\n {/* Non-breaking space when blank (same text-node layout as a\n real value) so the button keeps a full line of height. */}\n {isDisplayBlank ? ' ' : displayValue}\n </button>\n <button\n type='button'\n tabIndex={-1}\n disabled={disabled}\n // The chevron repeats what the combobox beside it already does, so it is a\n // pointer affordance only. Announcing it would offer the same control twice.\n aria-hidden='true'\n className={cx(styles.dropdownInputButtonIcon, classNames?.icon)}\n style={slotStyles?.icon}\n // Keeping the focus where it is, rather than letting mousedown move it here:\n // the trigger is what `aria-activedescendant` points from, and the click\n // still reaches Popover by bubbling.\n onMouseDown={(event: React.MouseEvent<HTMLButtonElement>) => event.preventDefault()}\n onClick={onClick}\n >\n {!disabled && !readOnly && (\n <FontAwesomeIcon\n icon={dropdownIcon}\n className={cx(styles.dropdownInputButtonIconArrow, {\n [styles.dropdownInputButtonIconArrowOpen]: isOpen,\n })}\n />\n )}\n </button>\n <fieldset\n className={cx(styles.dropdownFieldset, {\n [styles.dropdownBorderPlain]: plainStyle,\n [styles.dropdownBorderFocused]: showFocus,\n [styles.dropdownBorderInvalid]: hasErr,\n [styles.dropdownBorderError]: hasErr,\n })}\n >\n <legend\n className={cx(styles.dropdownFieldsetLegend, {\n [styles.dropdownFieldsetLegendActive]:\n useMatLabelStyle && (alwaysFloatLabel || showFocus || !isDisplayBlank),\n })}\n >\n <span>{editedLabel}</span>\n </legend>\n </fieldset>\n {useMatLabelStyle && label && (\n <div\n id={labelId}\n className={cx(\n styles.dropdownMatLabel,\n {\n [styles.dropdownMatLabelFloating]: alwaysFloatLabel || showFocus || !isDisplayBlank,\n },\n // The floating and the stacked label are two renderings of\n // the same thing, so one slot reaches whichever is in use.\n classNames?.label,\n )}\n style={slotStyles?.label}\n >\n {label}\n {requiredMark}\n </div>\n )}\n {useMatLabelStyle && placeholder && (\n <div\n className={cx(\n styles.dropdownPlaceholder,\n showFocus && !dropdownLabel && styles.dropdownPlaceholderShown,\n )}\n >\n {placeholder}\n </div>\n )}\n </div>\n </Popover>\n </div>\n {label && !useMatLabelStyle && (\n <div id={labelId} className={cx(styles.dropdownLabelText, classNames?.label)} style={slotStyles?.label}>\n {label}\n {requiredMark}\n </div>\n )}\n <Tooltip\n variant='error'\n message={formValidator ? formValidator.getCurrentErrorMessage() : errorMessage || ''}\n isVisible={hasErr && (isFocused || isHovered)}\n />\n </div>\n );\n}\n\nexport default Dropdown;\n"],"mappings":"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"}
1
+ {"version":3,"file":"dropdown.mjs","names":[],"sources":["../../../../package/common/dropdown/dropdown.tsx"],"sourcesContent":["import React, { useEffect, useId, useLayoutEffect, useRef, useState } from 'react';\nimport cx from 'classnames';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faChevronDown as dropdownIcon } from '@fortawesome/free-solid-svg-icons';\nimport Tooltip from 'components/common/tooltip';\n\nimport Popover from '../popover';\nimport Loader from '../loader';\nimport { useInfiniteScroll } from '../infiniteBlock/useInfiniteScroll';\nimport { edgeIndex, nextIndex, scrollOptionIntoView, type ActiveIndex } from '../utils/listNavigation';\nimport { PopoverAlign, PopoverPlacement } from '../popover/popover.types';\nimport { useComponentDefaults, usePresentationDefault } from '../configProvider';\nimport { useSheet } from '../hooks/useSheet';\nimport { Presentation } from '../configProvider/presentation.types';\nimport styles from './dropdown.module.pcss';\nimport sheetStyles from '../styles/sheet.module.pcss';\nimport type { DropdownProps, DropdownItem } from './dropdown.types';\n\n/** How long a pause resets the type-ahead buffer, matching a native `<select>`. */\nconst TYPEAHEAD_RESET_MS = 500;\n\nfunction Dropdown(props: DropdownProps) {\n const {\n errorMessage = '',\n className = '',\n style,\n label = '',\n value = '',\n isValid = true,\n tabIndex = 0,\n isRequired = false,\n customProps = {},\n isRow = false,\n disabled = false,\n readOnly = false,\n onLoadMore,\n onClick,\n onChange,\n onFocus: onFocusProp = null,\n onBlur: onBlurProp = null,\n onMouseEnter: onMouseEnterProp = null,\n onMouseLeave: onMouseLeaveProp = null,\n dropdownLabel: dropdownLabelProp,\n isDropdownListWider = false,\n formValidator = null,\n classNames,\n styles: slotStyles,\n items = [],\n useMatLabelStyle = true,\n alwaysFloatLabel = false,\n placeholder = '',\n plainStyle = false,\n scrollToSelected = false,\n presentation: presentationProp,\n reactRef,\n ...rest\n } = useComponentDefaults('Dropdown', props);\n const [isFocused, setIsFocused] = useState(false);\n const [isHovered, setIsHovered] = useState(false);\n const [, forceUpdate] = useState(0);\n const fvRef = useRef(formValidator);\n const triggerRef = useRef<HTMLDivElement>(null);\n const anchorRef = useRef<HTMLDivElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const [menuIsOpen, setMenuIsOpen] = useState(false);\n // `null` is \"nothing highlighted\", the state a list opens in unless `scrollToSelected`\n // asks for the current value. Arrowing from there enters the list at either end.\n const [activeIndex, setActiveIndex] = useState<ActiveIndex>(null);\n const typeahead = useRef({ buffer: '', at: 0 });\n\n const baseId = useId();\n const listboxId = `${baseId}-listbox`;\n const labelId = `${baseId}-label`;\n const optionId = (index: number) => `${baseId}-option-${index}`;\n\n const isInteractive = !disabled && !readOnly;\n const isOpen = menuIsOpen && isInteractive;\n\n // The list is its own scroll container. It used to render through an `InfiniteBlock`, which\n // also brought a tabbable container and a second key handler: inside a listbox that put a\n // focusable element with no role in the tab order and gave the arrows two owners.\n // Only while open: the list it watches is rendered then, and not before.\n const { isLoading: showScrollSpinner, sentinelRef } = useInfiniteScroll(listRef, isOpen ? onLoadMore : undefined);\n\n // Explicit prop → this component's global default → the app-wide one → anchored.\n // Resolved here rather than left to Popover so that this component and the panel\n // it opens can never disagree about whether they are presenting as a sheet.\n const appPresentation = usePresentationDefault();\n const presentation = presentationProp ?? appPresentation ?? Presentation.Default;\n const { isSheet } = useSheet(presentation);\n\n // The list reads at the field's size, which is not always the page's: a Dropdown\n // inside something with a smaller font should not open a list at the default one.\n // Width is Popover's job, through `width='trigger'`.\n useLayoutEffect(() => {\n const trigger = triggerRef.current;\n const menu = anchorRef.current;\n if (!isOpen || isSheet || !trigger || !menu) return;\n menu.style.fontSize = window.getComputedStyle(trigger).fontSize;\n }, [isOpen, isSheet]);\n\n useEffect(() => {\n fvRef.current = formValidator;\n if (!fvRef.current) return;\n fvRef.current.label = label ?? '';\n fvRef.current.validate();\n const listener = () => forceUpdate((n) => n + 1);\n fvRef.current.registerOnUpdateListener(listener);\n return () => fvRef.current?.removeOnUpdateListener(listener);\n }, [formValidator, label]);\n\n const currentValue = formValidator ? (formValidator.value as string | number) : value;\n const selectedIndex = items.findIndex((item) => item.key === currentValue);\n\n useEffect(() => {\n if (!isOpen || activeIndex === null) return;\n scrollOptionIntoView(listRef.current, document.getElementById(optionId(activeIndex)));\n // optionId is derived from a stable useId, so it cannot go stale between renders.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen, activeIndex]);\n\n const onMouseEnterHandler = (event: React.MouseEvent) => {\n setIsHovered(true);\n onMouseEnterProp?.(event);\n };\n\n const onMouseLeaveHandler = (event: React.MouseEvent) => {\n setIsHovered(false);\n onMouseLeaveProp?.(event);\n };\n\n const onFocusHandler = (event: React.FocusEvent) => {\n setIsFocused(true);\n onFocusProp?.(event);\n };\n\n const onBlurHandler = (event: React.FocusEvent) => {\n if (fvRef.current) fvRef.current.touched = true;\n setIsFocused(false);\n onBlurProp?.(event);\n };\n\n const hasError = (): boolean => {\n return formValidator ? formValidator.touched && formValidator.hasError() : !isValid || !!errorMessage;\n };\n\n const computedIsRequired = (): boolean => {\n return !!(\n isRequired || formValidator?.validators.find((v) => (v as { validatorId?: string }).validatorId === 'required')\n );\n };\n\n const onChangeHandler = (val: string | number): void => {\n if (formValidator) {\n formValidator.set(val);\n onChange?.(formValidator.value, customProps);\n } else {\n onChange?.(val, customProps);\n }\n };\n\n function handleOpenChange(next: boolean) {\n if (next && !isInteractive) return;\n setMenuIsOpen(next);\n if (next) setActiveIndex(scrollToSelected && selectedIndex >= 0 ? selectedIndex : null);\n else typeahead.current = { buffer: '', at: 0 };\n }\n\n function select(item: DropdownItem) {\n if (item.disabled) return;\n onChangeHandler(item.key);\n handleOpenChange(false);\n }\n\n const navigation = { isDisabled: (index: number) => !!items[index].disabled };\n\n /** Jumps to the option whose label starts with what was typed, as a native `<select>` does. */\n function handleTypeahead(key: string) {\n const now = Date.now();\n const buffer = now - typeahead.current.at > TYPEAHEAD_RESET_MS ? key : typeahead.current.buffer + key;\n typeahead.current = { buffer, at: now };\n\n const needle = buffer.toLowerCase();\n const match = items.findIndex(\n (item) => !item.disabled && typeof item.label === 'string' && item.label.toLowerCase().startsWith(needle),\n );\n if (match === -1) return;\n if (isOpen) setActiveIndex(match);\n else select(items[match]);\n }\n\n function handleKeyDown(event: React.KeyboardEvent) {\n if (!isInteractive) return;\n\n if (!isOpen) {\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter' || event.key === ' ') {\n // Without this the browser turns the key into a click on the trigger, which\n // Popover would read as a second toggle and close the list again.\n event.preventDefault();\n handleOpenChange(true);\n } else if (event.key.length === 1) {\n handleTypeahead(event.key);\n }\n return;\n }\n\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault();\n setActiveIndex(nextIndex(activeIndex, event.key === 'ArrowDown' ? 1 : -1, items.length, navigation));\n } else if (event.key === 'Home' || event.key === 'End') {\n event.preventDefault();\n setActiveIndex(edgeIndex(event.key === 'Home' ? 'first' : 'last', items.length, navigation));\n } else if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n const item = activeIndex === null ? undefined : items[activeIndex];\n if (item) select(item);\n else handleOpenChange(false);\n } else if (event.key === 'Escape' || event.key === 'Tab') {\n handleOpenChange(false);\n } else if (event.key.length === 1) {\n handleTypeahead(event.key);\n }\n }\n\n let dropdownLabel = dropdownLabelProp;\n const hasErr = hasError();\n\n items.forEach((item: DropdownItem) => {\n if (item.key === currentValue) {\n dropdownLabel = item.label as string;\n }\n });\n\n const displayValue = dropdownLabel || currentValue;\n const isDisplayBlank = displayValue == null || (typeof displayValue === 'string' && displayValue.trim() === '');\n\n const requiredMark = computedIsRequired() ? <span className={styles.requiredMark}>*</span> : null;\n const editedLabel = label && computedIsRequired() ? `${label} *` : label;\n\n const showFocus = isFocused && !readOnly;\n\n return (\n <div\n {...rest}\n className={cx(className, classNames?.root, styles.dropdownLabel, styles.dropdown, {\n [styles.dropdownLabelColumn]: !isRow,\n [styles.dropdownWithLabel]: useMatLabelStyle && !!label,\n })}\n style={{ ...style, ...slotStyles?.root }}\n onMouseEnter={onMouseEnterHandler}\n onMouseLeave={onMouseLeaveHandler}\n >\n <div>\n <Popover\n // Popover owns opening and closing, including the outside click, Escape and the\n // click on the field itself. The trigger only has to say what it wants.\n isOpen={isOpen}\n onOpenChange={handleOpenChange}\n disabled={!isInteractive}\n placement={PopoverPlacement.Bottom}\n align={PopoverAlign.Start}\n offset={0}\n withArrow={false}\n constrainWidth={false}\n width='trigger'\n // The focus stays on the trigger while the list is open, because that is what\n // `aria-activedescendant` points from.\n focusOnOpen={false}\n presentation={presentation}\n classNames={{ panel: styles.dropdownPanel }}\n content={\n <div\n ref={anchorRef}\n // A sheet's own panel does the clipping, and spans the screen rather than\n // sizing itself to the field.\n className={cx(isSheet ? styles.dropdownInputListSheet : styles.dropdownInputListAnim, {\n [styles.dropdownInputListAnimWider]: isDropdownListWider && !isSheet,\n })}\n >\n <div\n ref={listRef}\n id={listboxId}\n role='listbox'\n aria-label={label || placeholder || undefined}\n aria-busy={showScrollSpinner || undefined}\n className={cx(styles.dropdownInputList, isSheet && sheetStyles.sheetScroll, classNames?.list)}\n style={slotStyles?.list}\n >\n {items.map((item: DropdownItem, index: number) => (\n <div\n key={item.key}\n id={optionId(index)}\n role='option'\n aria-selected={item.key === currentValue}\n aria-disabled={item.disabled || undefined}\n style={slotStyles?.option}\n className={cx(styles.dropdownInputListItem, classNames?.option, {\n [styles.dropdownInputListItemActive]: activeIndex === index,\n [styles.dropdownInputListItemSelected]: item.key === currentValue,\n [styles.dropdownInputListItemDisabled]: !!item.disabled,\n })}\n // Selecting must not move the focus off the trigger, which is what\n // `aria-activedescendant` is anchored to.\n onMouseDown={(event) => event.preventDefault()}\n onMouseEnter={() => setActiveIndex(index)}\n onClick={() => select(item)}\n >\n {item.label}\n </div>\n ))}\n <div ref={sentinelRef} className={styles.dropdownSentinel} aria-hidden='true' />\n {showScrollSpinner && (\n <div className={styles.dropdownLoadMore} aria-hidden='true'>\n <Loader size='1.125em' />\n </div>\n )}\n </div>\n </div>\n }\n >\n <div\n ref={triggerRef}\n onFocus={onFocusHandler}\n onBlur={onBlurHandler}\n className={cx(\n styles.dropdownInput,\n {\n [styles.dropdownInputPlain]: plainStyle,\n [styles.dropdownInputReadOnly]: readOnly && !disabled,\n },\n classNames?.field,\n )}\n style={slotStyles?.field}\n >\n <button\n tabIndex={tabIndex}\n type='button'\n disabled={disabled}\n // A select-only combobox: the accessible name comes from the field's label,\n // and the element's own content is read as its value. A plain `button` role\n // would announce the value as the name and never mention the label.\n role='combobox'\n aria-haspopup='listbox'\n aria-expanded={isOpen}\n aria-controls={listboxId}\n aria-activedescendant={isOpen && activeIndex !== null ? optionId(activeIndex) : undefined}\n aria-labelledby={label ? labelId : undefined}\n aria-label={label ? undefined : placeholder || undefined}\n aria-readonly={readOnly || undefined}\n className={cx(\n styles.dropdownInputButton,\n {\n [styles.dropdownInputButtonDisabled]: disabled,\n [styles.dropdownInputButtonReadOnly]: readOnly && !disabled,\n },\n classNames?.control,\n )}\n style={{ ...(isDropdownListWider ? { padding: '6px', border: 'none' } : {}), ...slotStyles?.control }}\n onKeyDown={handleKeyDown}\n onClick={onClick}\n ref={reactRef}\n >\n {/* Non-breaking space when blank (same text-node layout as a\n real value) so the button keeps a full line of height. */}\n {isDisplayBlank ? ' ' : displayValue}\n </button>\n <button\n type='button'\n tabIndex={-1}\n disabled={disabled}\n // The chevron repeats what the combobox beside it already does, so it is a\n // pointer affordance only. Announcing it would offer the same control twice.\n aria-hidden='true'\n className={cx(styles.dropdownInputButtonIcon, classNames?.icon)}\n style={slotStyles?.icon}\n // Keeping the focus where it is, rather than letting mousedown move it here:\n // the trigger is what `aria-activedescendant` points from, and the click\n // still reaches Popover by bubbling.\n onMouseDown={(event: React.MouseEvent<HTMLButtonElement>) => event.preventDefault()}\n onClick={onClick}\n >\n {!disabled && !readOnly && (\n <FontAwesomeIcon\n icon={dropdownIcon}\n className={cx(styles.dropdownInputButtonIconArrow, {\n [styles.dropdownInputButtonIconArrowOpen]: isOpen,\n })}\n />\n )}\n </button>\n <fieldset\n className={cx(styles.dropdownFieldset, {\n [styles.dropdownBorderPlain]: plainStyle,\n [styles.dropdownBorderFocused]: showFocus,\n [styles.dropdownBorderInvalid]: hasErr,\n [styles.dropdownBorderError]: hasErr,\n })}\n >\n <legend\n className={cx(styles.dropdownFieldsetLegend, {\n [styles.dropdownFieldsetLegendActive]:\n useMatLabelStyle && (alwaysFloatLabel || showFocus || !isDisplayBlank),\n })}\n >\n <span>{editedLabel}</span>\n </legend>\n </fieldset>\n {useMatLabelStyle && label && (\n <div\n id={labelId}\n className={cx(\n styles.dropdownMatLabel,\n {\n [styles.dropdownMatLabelFloating]: alwaysFloatLabel || showFocus || !isDisplayBlank,\n },\n // The floating and the stacked label are two renderings of\n // the same thing, so one slot reaches whichever is in use.\n classNames?.label,\n )}\n style={slotStyles?.label}\n >\n {label}\n {requiredMark}\n </div>\n )}\n {useMatLabelStyle && placeholder && (\n <div\n className={cx(\n styles.dropdownPlaceholder,\n showFocus && !dropdownLabel && styles.dropdownPlaceholderShown,\n )}\n >\n {placeholder}\n </div>\n )}\n </div>\n </Popover>\n </div>\n {label && !useMatLabelStyle && (\n <div id={labelId} className={cx(styles.dropdownLabelText, classNames?.label)} style={slotStyles?.label}>\n {label}\n {requiredMark}\n </div>\n )}\n <Tooltip\n variant='error'\n message={formValidator ? formValidator.getCurrentErrorMessage() : errorMessage || ''}\n isVisible={hasErr && (isFocused || isHovered)}\n />\n </div>\n );\n}\n\nexport default Dropdown;\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use client";var e=`required-mark_fy7Fm`,t=`dropdown_XTshY`,n=`dropdown-withLabel_Izzz3`,r=`dropdown-label_TIpeK`,i=`dropdown-label-column_cMCBc`,a=`dropdown-label-text_u-UKq`,o=`dropdown-input_WiPcb`,s=`dropdown-input-plain_Ap12N`,c=`dropdown-input-readOnly_3GWSO`,l=`dropdown-input-button_ziKKi`,u=`dropdown-input-button-disabled_JEaO-`,d=`dropdown-input-button-readOnly_QhBYY`,f=`dropdown-input-button-icon_jl7EY`,p=`dropdown-input-button-icon-arrow_SFJbx`,m=`dropdown-input-button-icon-arrow-open_8yQbZ`,h=`dropdown-input-list-anim_knSMT`,g=`dropdown-input-list-anim-wider_-YyLJ`,_=`dropdown-input-list_JnMbA`,v=`dropdown-input-list-item_EAy5L`,y=`dropdown-input-list-item-active_0dhia`,b=`dropdown-input-list-item-selected_hShJf`,x=`dropdown-input-list-item-disabled_3r-Yn`,S=`dropdown-fieldset_wQwTf`,C=`dropdown-fieldset-legend_a1M4L`,w=`dropdown-fieldset-legend-active_bnR5F`,T=`dropdown-placeholder_QYMPZ`,E=`dropdown-placeholder-shown_-IkNH`,D=`fadeIn_oMJZj`,O=`dropdown-matLabel_iHQnt`,k=`dropdown-matLabel-floating_zY51S`,A=`dropdown-border-focused_Te-ER`,j=`dropdown-border-plain_Bbh-3`,M=`dropdown-border-invalid_ySVaS`,N=`dropdown-border-error_-JB33`,P=`dropdownPanel_RYxpN`,F=`dropdown-sentinel_TVDF9`,I=`dropdown-load-more_UQvzV`,L={"required-mark":`required-mark_fy7Fm`,requiredMark:e,dropdown:t,"dropdown-withLabel":`dropdown-withLabel_Izzz3`,dropdownWithLabel:n,"dropdown-label":`dropdown-label_TIpeK`,dropdownLabel:r,"dropdown-label-column":`dropdown-label-column_cMCBc`,dropdownLabelColumn:i,"dropdown-label-text":`dropdown-label-text_u-UKq`,dropdownLabelText:a,"dropdown-input":`dropdown-input_WiPcb`,dropdownInput:o,"dropdown-input-plain":`dropdown-input-plain_Ap12N`,dropdownInputPlain:s,"dropdown-input-readOnly":`dropdown-input-readOnly_3GWSO`,dropdownInputReadOnly:c,"dropdown-input-button":`dropdown-input-button_ziKKi`,dropdownInputButton:l,"dropdown-input-button-disabled":`dropdown-input-button-disabled_JEaO-`,dropdownInputButtonDisabled:u,"dropdown-input-button-readOnly":`dropdown-input-button-readOnly_QhBYY`,dropdownInputButtonReadOnly:d,"dropdown-input-button-icon":`dropdown-input-button-icon_jl7EY`,dropdownInputButtonIcon:f,"dropdown-input-button-icon-arrow":`dropdown-input-button-icon-arrow_SFJbx`,dropdownInputButtonIconArrow:p,"dropdown-input-button-icon-arrow-open":`dropdown-input-button-icon-arrow-open_8yQbZ`,dropdownInputButtonIconArrowOpen:m,"dropdown-input-list-anim":`dropdown-input-list-anim_knSMT`,dropdownInputListAnim:h,"dropdown-input-list-anim-wider":`dropdown-input-list-anim-wider_-YyLJ`,dropdownInputListAnimWider:g,"dropdown-input-list":`dropdown-input-list_JnMbA`,dropdownInputList:_,"dropdown-input-list-item":`dropdown-input-list-item_EAy5L`,dropdownInputListItem:v,"dropdown-input-list-item-active":`dropdown-input-list-item-active_0dhia`,dropdownInputListItemActive:y,"dropdown-input-list-item-selected":`dropdown-input-list-item-selected_hShJf`,dropdownInputListItemSelected:b,"dropdown-input-list-item-disabled":`dropdown-input-list-item-disabled_3r-Yn`,dropdownInputListItemDisabled:x,"dropdown-fieldset":`dropdown-fieldset_wQwTf`,dropdownFieldset:S,"dropdown-fieldset-legend":`dropdown-fieldset-legend_a1M4L`,dropdownFieldsetLegend:C,"dropdown-fieldset-legend-active":`dropdown-fieldset-legend-active_bnR5F`,dropdownFieldsetLegendActive:w,"dropdown-placeholder":`dropdown-placeholder_QYMPZ`,dropdownPlaceholder:T,"dropdown-placeholder-shown":`dropdown-placeholder-shown_-IkNH`,dropdownPlaceholderShown:E,fadeIn:D,"dropdown-matLabel":`dropdown-matLabel_iHQnt`,dropdownMatLabel:O,"dropdown-matLabel-floating":`dropdown-matLabel-floating_zY51S`,dropdownMatLabelFloating:k,"dropdown-border-focused":`dropdown-border-focused_Te-ER`,dropdownBorderFocused:A,"dropdown-border-plain":`dropdown-border-plain_Bbh-3`,dropdownBorderPlain:j,"dropdown-border-invalid":`dropdown-border-invalid_ySVaS`,dropdownBorderInvalid:M,"dropdown-border-error":`dropdown-border-error_-JB33`,dropdownBorderError:N,dropdownPanel:P,"dropdown-sentinel":`dropdown-sentinel_TVDF9`,dropdownSentinel:F,"dropdown-load-more":`dropdown-load-more_UQvzV`,dropdownLoadMore:I};export{L as default,t as dropdown,N as dropdownBorderError,A as dropdownBorderFocused,M as dropdownBorderInvalid,j as dropdownBorderPlain,S as dropdownFieldset,C as dropdownFieldsetLegend,w as dropdownFieldsetLegendActive,o as dropdownInput,l as dropdownInputButton,u as dropdownInputButtonDisabled,f as dropdownInputButtonIcon,p as dropdownInputButtonIconArrow,m as dropdownInputButtonIconArrowOpen,d as dropdownInputButtonReadOnly,_ as dropdownInputList,h as dropdownInputListAnim,g as dropdownInputListAnimWider,v as dropdownInputListItem,y as dropdownInputListItemActive,x as dropdownInputListItemDisabled,b as dropdownInputListItemSelected,s as dropdownInputPlain,c as dropdownInputReadOnly,r as dropdownLabel,i as dropdownLabelColumn,a as dropdownLabelText,I as dropdownLoadMore,O as dropdownMatLabel,k as dropdownMatLabelFloating,P as dropdownPanel,T as dropdownPlaceholder,E as dropdownPlaceholderShown,F as dropdownSentinel,n as dropdownWithLabel,D as fadeIn,e as requiredMark};
1
+ "use client";var e=`required-mark_fy7Fm`,t=`dropdown_XTshY`,n=`dropdown-withLabel_Izzz3`,r=`dropdown-label_TIpeK`,i=`dropdown-label-column_cMCBc`,a=`dropdown-label-text_u-UKq`,o=`dropdown-input_WiPcb`,s=`dropdown-input-plain_Ap12N`,c=`dropdown-input-readOnly_3GWSO`,l=`dropdown-input-button_ziKKi`,u=`dropdown-input-button-disabled_JEaO-`,d=`dropdown-input-button-readOnly_QhBYY`,f=`dropdown-input-button-icon_jl7EY`,p=`dropdown-input-button-icon-arrow_SFJbx`,m=`dropdown-input-button-icon-arrow-open_8yQbZ`,h=`dropdown-input-list-anim_knSMT`,g=`dropdown-input-list-anim-wider_-YyLJ`,_=`dropdown-input-list-sheet_4gRYD`,v=`dropdown-input-list_JnMbA`,y=`dropdown-input-list-item_EAy5L`,b=`dropdown-input-list-item-active_0dhia`,x=`dropdown-input-list-item-selected_hShJf`,S=`dropdown-input-list-item-disabled_3r-Yn`,C=`dropdown-fieldset_wQwTf`,w=`dropdown-fieldset-legend_a1M4L`,T=`dropdown-fieldset-legend-active_bnR5F`,E=`dropdown-placeholder_QYMPZ`,D=`dropdown-placeholder-shown_-IkNH`,O=`fadeIn_oMJZj`,k=`dropdown-matLabel_iHQnt`,A=`dropdown-matLabel-floating_zY51S`,j=`dropdown-border-focused_Te-ER`,M=`dropdown-border-plain_Bbh-3`,N=`dropdown-border-invalid_ySVaS`,P=`dropdown-border-error_-JB33`,F=`dropdownPanel_RYxpN`,I=`dropdown-sentinel_TVDF9`,L=`dropdown-load-more_UQvzV`,R={"required-mark":`required-mark_fy7Fm`,requiredMark:e,dropdown:t,"dropdown-withLabel":`dropdown-withLabel_Izzz3`,dropdownWithLabel:n,"dropdown-label":`dropdown-label_TIpeK`,dropdownLabel:r,"dropdown-label-column":`dropdown-label-column_cMCBc`,dropdownLabelColumn:i,"dropdown-label-text":`dropdown-label-text_u-UKq`,dropdownLabelText:a,"dropdown-input":`dropdown-input_WiPcb`,dropdownInput:o,"dropdown-input-plain":`dropdown-input-plain_Ap12N`,dropdownInputPlain:s,"dropdown-input-readOnly":`dropdown-input-readOnly_3GWSO`,dropdownInputReadOnly:c,"dropdown-input-button":`dropdown-input-button_ziKKi`,dropdownInputButton:l,"dropdown-input-button-disabled":`dropdown-input-button-disabled_JEaO-`,dropdownInputButtonDisabled:u,"dropdown-input-button-readOnly":`dropdown-input-button-readOnly_QhBYY`,dropdownInputButtonReadOnly:d,"dropdown-input-button-icon":`dropdown-input-button-icon_jl7EY`,dropdownInputButtonIcon:f,"dropdown-input-button-icon-arrow":`dropdown-input-button-icon-arrow_SFJbx`,dropdownInputButtonIconArrow:p,"dropdown-input-button-icon-arrow-open":`dropdown-input-button-icon-arrow-open_8yQbZ`,dropdownInputButtonIconArrowOpen:m,"dropdown-input-list-anim":`dropdown-input-list-anim_knSMT`,dropdownInputListAnim:h,"dropdown-input-list-anim-wider":`dropdown-input-list-anim-wider_-YyLJ`,dropdownInputListAnimWider:g,"dropdown-input-list-sheet":`dropdown-input-list-sheet_4gRYD`,dropdownInputListSheet:_,"dropdown-input-list":`dropdown-input-list_JnMbA`,dropdownInputList:v,"dropdown-input-list-item":`dropdown-input-list-item_EAy5L`,dropdownInputListItem:y,"dropdown-input-list-item-active":`dropdown-input-list-item-active_0dhia`,dropdownInputListItemActive:b,"dropdown-input-list-item-selected":`dropdown-input-list-item-selected_hShJf`,dropdownInputListItemSelected:x,"dropdown-input-list-item-disabled":`dropdown-input-list-item-disabled_3r-Yn`,dropdownInputListItemDisabled:S,"dropdown-fieldset":`dropdown-fieldset_wQwTf`,dropdownFieldset:C,"dropdown-fieldset-legend":`dropdown-fieldset-legend_a1M4L`,dropdownFieldsetLegend:w,"dropdown-fieldset-legend-active":`dropdown-fieldset-legend-active_bnR5F`,dropdownFieldsetLegendActive:T,"dropdown-placeholder":`dropdown-placeholder_QYMPZ`,dropdownPlaceholder:E,"dropdown-placeholder-shown":`dropdown-placeholder-shown_-IkNH`,dropdownPlaceholderShown:D,fadeIn:O,"dropdown-matLabel":`dropdown-matLabel_iHQnt`,dropdownMatLabel:k,"dropdown-matLabel-floating":`dropdown-matLabel-floating_zY51S`,dropdownMatLabelFloating:A,"dropdown-border-focused":`dropdown-border-focused_Te-ER`,dropdownBorderFocused:j,"dropdown-border-plain":`dropdown-border-plain_Bbh-3`,dropdownBorderPlain:M,"dropdown-border-invalid":`dropdown-border-invalid_ySVaS`,dropdownBorderInvalid:N,"dropdown-border-error":`dropdown-border-error_-JB33`,dropdownBorderError:P,dropdownPanel:F,"dropdown-sentinel":`dropdown-sentinel_TVDF9`,dropdownSentinel:I,"dropdown-load-more":`dropdown-load-more_UQvzV`,dropdownLoadMore:L};export{R as default,t as dropdown,P as dropdownBorderError,j as dropdownBorderFocused,N as dropdownBorderInvalid,M as dropdownBorderPlain,C as dropdownFieldset,w as dropdownFieldsetLegend,T as dropdownFieldsetLegendActive,o as dropdownInput,l as dropdownInputButton,u as dropdownInputButtonDisabled,f as dropdownInputButtonIcon,p as dropdownInputButtonIconArrow,m as dropdownInputButtonIconArrowOpen,d as dropdownInputButtonReadOnly,v as dropdownInputList,h as dropdownInputListAnim,g as dropdownInputListAnimWider,y as dropdownInputListItem,b as dropdownInputListItemActive,S as dropdownInputListItemDisabled,x as dropdownInputListItemSelected,_ as dropdownInputListSheet,s as dropdownInputPlain,c as dropdownInputReadOnly,r as dropdownLabel,i as dropdownLabelColumn,a as dropdownLabelText,L as dropdownLoadMore,k as dropdownMatLabel,A as dropdownMatLabelFloating,F as dropdownPanel,E as dropdownPlaceholder,D as dropdownPlaceholderShown,I as dropdownSentinel,n as dropdownWithLabel,O as fadeIn,e as requiredMark};
2
2
  //# sourceMappingURL=dropdown.module.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"dropdown.module.mjs","names":[],"sources":["../../../../package/common/dropdown/dropdown.module.pcss"],"sourcesContent":[".required-mark {\n padding: 0 0 0 4px;\n color: var(--required-mark-color, var(--error-color));\n}\n\n.dropdown {\n position: relative;\n\n /* Nothing inside gives the root a width, so as a flex or grid item it collapsed to the chevron\n and drew its border through the label. `input` and `multiSelect` say the same. */\n width: 100%;\n\n --dropdown-notch-height: 1.75em;\n --field-label-offset: 0px;\n\n /* Input's --field-label-offset, for the same reason: a floating label hangs into the field. */\n &-withLabel {\n --field-label-offset: 1px;\n }\n\n /* See input.module.pcss for why this exists: the visible border lives on `-input`/`-fieldset`\n below, not this root, so a wrapping ButtonGroup's --group-radius-* custom properties (which\n inherit through any nesting depth, unlike border-radius itself) are what let this component\n flatten correctly when it sits inside a ButtonGroup. */\n --dropdown-radius: var(--group-radius-tl, var(--default-border-radius))\n var(--group-radius-tr, var(--default-border-radius)) var(--group-radius-br, var(--default-border-radius))\n var(--group-radius-bl, var(--default-border-radius));\n\n &-label {\n display: flex;\n\n &-column {\n flex-direction: column;\n }\n\n &-text {\n white-space: nowrap;\n margin-right: 5px;\n margin-bottom: 5px;\n display: flex;\n align-items: center;\n color: var(--text-color);\n font-size: 12px;\n position: absolute;\n top: -15px;\n }\n }\n\n &-input {\n position: relative;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n background-color: var(--input-background);\n border-radius: var(--dropdown-radius);\n\n &-plain {\n background: transparent;\n }\n\n &-readOnly {\n cursor: default;\n }\n\n &:focus-within {\n outline: none;\n }\n\n &-button {\n padding: calc(7px + var(--field-label-offset)) 20% calc(7px - var(--field-label-offset)) 7.5px !important;\n background-color: transparent;\n border: none;\n font-family: inherit;\n font-size: inherit;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n text-align: left;\n width: 100%;\n line-height: 1.25;\n color: var(--text-color);\n\n &-disabled {\n background-color: var(--disabled-input-color);\n color: var(--disabled-font-color);\n border-radius: var(--dropdown-radius);\n }\n\n &-readOnly {\n cursor: default;\n }\n\n &:focus {\n outline: none;\n }\n\n &-icon {\n float: right;\n background-color: transparent;\n border: none;\n position: absolute;\n outline: none;\n right: var(--spacing-s);\n top: calc(50% + var(--field-label-offset));\n transform: translateY(-50%);\n color: var(--text-color);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n\n &-arrow {\n transition: transform 0.25s;\n pointer-events: none;\n\n &-open {\n transform: rotate(180deg);\n }\n }\n }\n }\n\n /* Chrome, placement and the portal come from the Popover this renders inside.\n What is left is the clipping that keeps the first and last row inside the\n panel's rounded corners. */\n &-list-anim {\n overflow: hidden;\n border-radius: inherit;\n\n &-wider {\n width: max-content !important;\n }\n }\n\n &-list {\n width: 100%;\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n max-height: 10em;\n list-style-type: none;\n\n &-item {\n border: none;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n background-color: var(--background-modal);\n padding: 0.5em;\n text-align: left;\n border-radius: 0;\n transition: background-color 0.3s;\n\n &-active {\n background-color: var(--background-accent);\n }\n\n &-selected {\n font-weight: 500;\n }\n\n &:focus {\n outline: none;\n }\n\n &:hover {\n background-color: var(--background-accent);\n }\n\n &-disabled {\n background-color: var(--disabled-input-color);\n color: var(--disabled-font-color);\n cursor: not-allowed;\n\n &:hover {\n background-color: var(--disabled-input-color);\n }\n }\n }\n }\n }\n\n &-fieldset {\n position: absolute;\n width: 100%;\n margin: 0;\n box-sizing: border-box;\n border-radius: var(--dropdown-radius);\n font-size: 0.8em;\n inset: calc(var(--dropdown-notch-height) / -2) 0 0;\n padding-left: 8px;\n border-width: 1px;\n border-style: solid;\n border-color: var(--border-color);\n pointer-events: none;\n user-select: none;\n\n &-legend {\n display: block;\n\n /* Not taken from the text: an empty span gets no line box, which raised a label-less\n field's border half a notch. */\n height: var(--dropdown-notch-height);\n max-width: 0;\n transition: max-width 0.2s;\n white-space: nowrap;\n padding: 0;\n\n &-active {\n max-width: 300px;\n }\n\n span {\n line-height: var(--dropdown-notch-height);\n padding: 0 5px;\n opacity: 0;\n }\n\n /* With an empty label the span would still reserve its padding, leaving a gap in\n the fieldset's top border. Collapse it so the notch closes when there's no label. */\n span:empty {\n padding: 0;\n }\n }\n }\n\n &-placeholder {\n position: absolute;\n top: calc(50% + var(--field-label-offset));\n left: 7.5px;\n transform: translateY(-50%);\n transition: all 0.2s;\n opacity: 0;\n\n &-shown {\n animation: 0.2s fadeIn 0.3s forwards;\n }\n }\n\n &-matLabel {\n position: absolute;\n transition: all 0.3s;\n left: 7.5px;\n right: 0;\n top: calc(50% + var(--field-label-offset));\n transform: translateY(-50%);\n font-size: 1em;\n color: var(--input-label-resting-color, var(--text-light));\n pointer-events: none;\n user-select: none;\n\n &-floating {\n inset: 0 0 auto 14px;\n font-size: 0.8em;\n color: var(--input-label-floating-color, var(--text-color));\n transform: translateY(-50%);\n }\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 0.5;\n }\n }\n\n &-border {\n &-focused {\n outline-color: var(--focus-border-color, var(--primary-color));\n border-color: var(--focus-border-color, var(--primary-color));\n }\n\n &-plain:not(&-focused) {\n border-color: transparent;\n outline-color: transparent;\n }\n\n &-invalid {\n outline-color: var(--error-border-color);\n border-color: var(--error-border-color);\n }\n\n &-invalid:hover {\n outline-color: var(--error-border-color);\n border-color: var(--error-border-color);\n }\n\n &-error {\n outline-color: var(--error-border-color);\n border-color: var(--error-border-color);\n }\n }\n}\n\n/* The list brings its own padding and its rows reach the panel edge. */\n.dropdownPanel {\n padding: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dropdown-matLabel,\n .dropdown-placeholder,\n .dropdown-input-button-icon-arrow {\n transition: none;\n }\n}\n\n/* The sentinel the infinite-scroll hook watches, and the spinner shown while a page loads.\n These came with `InfiniteBlock` when the list rendered through it; the list is its own scroll\n container now, so they live here. */\n.dropdown-sentinel {\n height: 1px;\n}\n\n.dropdown-load-more {\n display: flex;\n justify-content: center;\n align-items: center;\n padding: 4px 0;\n}\n"],"mappings":""}
1
+ {"version":3,"file":"dropdown.module.mjs","names":[],"sources":["../../../../package/common/dropdown/dropdown.module.pcss"],"sourcesContent":[".required-mark {\n padding: 0 0 0 4px;\n color: var(--required-mark-color, var(--error-color));\n}\n\n.dropdown {\n position: relative;\n\n /* Nothing inside gives the root a width, so as a flex or grid item it collapsed to the chevron\n and drew its border through the label. `input` and `multiSelect` say the same. */\n width: 100%;\n\n --dropdown-notch-height: 1.75em;\n --field-label-offset: 0px;\n\n /* Input's --field-label-offset, for the same reason: a floating label hangs into the field. */\n &-withLabel {\n --field-label-offset: 1px;\n }\n\n /* See input.module.pcss for why this exists: the visible border lives on `-input`/`-fieldset`\n below, not this root, so a wrapping ButtonGroup's --group-radius-* custom properties (which\n inherit through any nesting depth, unlike border-radius itself) are what let this component\n flatten correctly when it sits inside a ButtonGroup. */\n --dropdown-radius: var(--group-radius-tl, var(--default-border-radius))\n var(--group-radius-tr, var(--default-border-radius)) var(--group-radius-br, var(--default-border-radius))\n var(--group-radius-bl, var(--default-border-radius));\n\n &-label {\n display: flex;\n\n &-column {\n flex-direction: column;\n }\n\n &-text {\n white-space: nowrap;\n margin-right: 5px;\n margin-bottom: 5px;\n display: flex;\n align-items: center;\n color: var(--text-color);\n font-size: 12px;\n position: absolute;\n top: -15px;\n }\n }\n\n &-input {\n position: relative;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n background-color: var(--input-background);\n border-radius: var(--dropdown-radius);\n\n &-plain {\n background: transparent;\n }\n\n &-readOnly {\n cursor: default;\n }\n\n &:focus-within {\n outline: none;\n }\n\n &-button {\n padding: calc(7px + var(--field-label-offset)) 20% calc(7px - var(--field-label-offset)) 7.5px !important;\n background-color: transparent;\n border: none;\n font-family: inherit;\n font-size: inherit;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n text-align: left;\n width: 100%;\n line-height: 1.25;\n color: var(--text-color);\n\n &-disabled {\n background-color: var(--disabled-input-color);\n color: var(--disabled-font-color);\n border-radius: var(--dropdown-radius);\n }\n\n &-readOnly {\n cursor: default;\n }\n\n &:focus {\n outline: none;\n }\n\n &-icon {\n float: right;\n background-color: transparent;\n border: none;\n position: absolute;\n outline: none;\n right: var(--spacing-s);\n top: calc(50% + var(--field-label-offset));\n transform: translateY(-50%);\n color: var(--text-color);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n\n &-arrow {\n transition: transform 0.25s;\n pointer-events: none;\n\n &-open {\n transform: rotate(180deg);\n }\n }\n }\n }\n\n /* Chrome, placement and the portal come from the Popover this renders inside.\n What is left is the clipping that keeps the first and last row inside the\n panel's rounded corners. */\n &-list-anim {\n overflow: hidden;\n border-radius: inherit;\n\n &-wider {\n width: max-content !important;\n }\n }\n\n /* Passes the sheet's height on to the list, so that the list scrolls rather than the panel:\n it is what keyboard navigation scrolls and what loading more watches. */\n &-list-sheet {\n display: flex;\n flex-direction: column;\n min-height: 0;\n }\n\n &-list {\n width: 100%;\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n max-height: 10em;\n list-style-type: none;\n\n &-item {\n border: none;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n background-color: var(--background-modal);\n padding: 0.5em;\n text-align: left;\n border-radius: 0;\n transition: background-color 0.3s;\n\n &-active {\n background-color: var(--background-accent);\n }\n\n &-selected {\n font-weight: 500;\n }\n\n &:focus {\n outline: none;\n }\n\n &:hover {\n background-color: var(--background-accent);\n }\n\n &-disabled {\n background-color: var(--disabled-input-color);\n color: var(--disabled-font-color);\n cursor: not-allowed;\n\n &:hover {\n background-color: var(--disabled-input-color);\n }\n }\n }\n }\n }\n\n &-fieldset {\n position: absolute;\n width: 100%;\n margin: 0;\n box-sizing: border-box;\n border-radius: var(--dropdown-radius);\n font-size: 0.8em;\n inset: calc(var(--dropdown-notch-height) / -2) 0 0;\n padding-left: 8px;\n border-width: 1px;\n border-style: solid;\n border-color: var(--border-color);\n pointer-events: none;\n user-select: none;\n\n &-legend {\n display: block;\n\n /* Not taken from the text: an empty span gets no line box, which raised a label-less\n field's border half a notch. */\n height: var(--dropdown-notch-height);\n max-width: 0;\n transition: max-width 0.2s;\n white-space: nowrap;\n padding: 0;\n\n &-active {\n max-width: 300px;\n }\n\n span {\n line-height: var(--dropdown-notch-height);\n padding: 0 5px;\n opacity: 0;\n }\n\n /* With an empty label the span would still reserve its padding, leaving a gap in\n the fieldset's top border. Collapse it so the notch closes when there's no label. */\n span:empty {\n padding: 0;\n }\n }\n }\n\n &-placeholder {\n position: absolute;\n top: calc(50% + var(--field-label-offset));\n left: 7.5px;\n transform: translateY(-50%);\n transition: all 0.2s;\n opacity: 0;\n\n &-shown {\n animation: 0.2s fadeIn 0.3s forwards;\n }\n }\n\n &-matLabel {\n position: absolute;\n transition: all 0.3s;\n left: 7.5px;\n right: 0;\n top: calc(50% + var(--field-label-offset));\n transform: translateY(-50%);\n font-size: 1em;\n color: var(--input-label-resting-color, var(--text-light));\n pointer-events: none;\n user-select: none;\n\n &-floating {\n inset: 0 0 auto 14px;\n font-size: 0.8em;\n color: var(--input-label-floating-color, var(--text-color));\n transform: translateY(-50%);\n }\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 0.5;\n }\n }\n\n &-border {\n &-focused {\n outline-color: var(--focus-border-color, var(--primary-color));\n border-color: var(--focus-border-color, var(--primary-color));\n }\n\n &-plain:not(&-focused) {\n border-color: transparent;\n outline-color: transparent;\n }\n\n &-invalid {\n outline-color: var(--error-border-color);\n border-color: var(--error-border-color);\n }\n\n &-invalid:hover {\n outline-color: var(--error-border-color);\n border-color: var(--error-border-color);\n }\n\n &-error {\n outline-color: var(--error-border-color);\n border-color: var(--error-border-color);\n }\n }\n}\n\n/* The list brings its own padding and its rows reach the panel edge. */\n.dropdownPanel {\n padding: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dropdown-matLabel,\n .dropdown-placeholder,\n .dropdown-input-button-icon-arrow {\n transition: none;\n }\n}\n\n/* The sentinel the infinite-scroll hook watches, and the spinner shown while a page loads.\n These came with `InfiniteBlock` when the list rendered through it; the list is its own scroll\n container now, so they live here. */\n.dropdown-sentinel {\n height: 1px;\n}\n\n/* An option's padding around a loader one line of text high, so the row is an option's height. */\n.dropdown-load-more {\n display: flex;\n justify-content: center;\n align-items: center;\n padding: 0.5em 0;\n}\n"],"mappings":""}
@@ -1,2 +1,2 @@
1
- "use client";import{useEffect as e,useRef as t,useState as n}from"react";var r=3e3;function i(i,a,o){let[s,c]=n(!1),l=t(!1),u=t(null),d=t(a);return e(()=>{d.current=a}),e(()=>{let e=i.current,t=u.current;if(o||!e||!t)return;let n=!1,a=0,s=()=>{p.unobserve(t),p.observe(t)},f=()=>{if(l.current)return;l.current=!0,c(!0);let e=e=>{l.current=!1,c(!1),!n&&(e?s():a=window.setTimeout(s,r))};d.current().then(()=>e(!0),()=>e(!1))},p=new IntersectionObserver(([e])=>{e.isIntersecting&&f()},{root:e});return p.observe(t),()=>{n=!0,window.clearTimeout(a),p.disconnect()}},[i,o]),{isLoading:s,sentinelRef:u}}export{i as useInfiniteScroll};
1
+ "use client";import{useEffect as e,useLayoutEffect as t,useRef as n,useState as r}from"react";var i=3e3,a=`100px 0px`;function o(o,s){let[c,l]=r(!1),u=n(!1),d=n(null),f=n(()=>{}),p=n(0),m=n(null),h=n(s);e(()=>{h.current=s});let g=s!==void 0;return t(()=>{let e=m.current;c||e===null||d.current?.parentElement?.childElementCount!==e&&(m.current=null,f.current())}),e(()=>{let e=o.current,t=d.current;if(!g||!e||!t)return;let n=()=>{s.unobserve(t),s.observe(t)},r=()=>{let e=h.current;e&&!u.current&&(u.current=!0,m.current=t.parentElement?.childElementCount??null,l(!0),e().then(()=>{u.current=!1,l(!1)},()=>{u.current=!1,m.current=null,l(!1),p.current=window.setTimeout(()=>f.current(),i)}))},s=new IntersectionObserver(([e])=>{e.isIntersecting&&r()},{root:e,rootMargin:a});return s.observe(t),f.current=n,()=>{window.clearTimeout(p.current),s.disconnect(),f.current=()=>{}}},[o,g]),{isLoading:c,sentinelRef:d}}export{o as useInfiniteScroll};
2
2
  //# sourceMappingURL=useInfiniteScroll.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useInfiniteScroll.mjs","names":[],"sources":["../../../../package/common/infiniteBlock/useInfiniteScroll.ts"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport type React from 'react';\n\nconst RETRY_DELAY = 3000;\n\n// Loads more items once a sentinel element at the end of the list scrolls into view.\n// IntersectionObserver only fires on a change of intersection state, so if a page of\n// freshly-loaded items still doesn't fill the container, the sentinel stays visible and\n// we have to re-arm the observer ourselves to pick up the next page.\nexport function useInfiniteScroll(\n containerRef: React.RefObject<HTMLDivElement | null>,\n onScroll: () => Promise<void>,\n disabled: boolean,\n) {\n const [isLoading, setIsLoading] = useState(false);\n const isLoadingRef = useRef(false);\n const sentinelRef = useRef<HTMLDivElement>(null);\n\n // Kept in a ref so a caller passing a fresh onScroll function each render doesn't\n // tear down and recreate the observer every time.\n const onScrollRef = useRef(onScroll);\n useEffect(() => {\n onScrollRef.current = onScroll;\n });\n\n useEffect(() => {\n const root = containerRef.current;\n const sentinel = sentinelRef.current;\n if (disabled || !root || !sentinel) return undefined;\n\n let cancelled = false;\n let retry = 0;\n\n // Observing again reports the sentinel at once, for a page that still left it in view.\n const rearm = () => {\n observer.unobserve(sentinel);\n observer.observe(sentinel);\n };\n\n const loadMore = () => {\n if (isLoadingRef.current) return;\n isLoadingRef.current = true;\n setIsLoading(true);\n\n const settle = (succeeded: boolean) => {\n // Reset even once cancelled: a load can outlive its observer when the caller disables it\n // mid-request, and a flag left set would block every load after it.\n isLoadingRef.current = false;\n setIsLoading(false);\n if (cancelled) return;\n if (succeeded) rearm();\n // A failure waits before trying again, or it would retry without end while the sentinel\n // stays in view. It does try again: a list too short to scroll offers no other way.\n else retry = window.setTimeout(rearm, RETRY_DELAY);\n };\n\n onScrollRef.current().then(\n () => settle(true),\n () => settle(false),\n );\n };\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) loadMore();\n },\n { root },\n );\n\n observer.observe(sentinel);\n\n return () => {\n cancelled = true;\n window.clearTimeout(retry);\n observer.disconnect();\n };\n }, [containerRef, disabled]);\n\n return { isLoading, sentinelRef };\n}\n"],"mappings":"yEAGA,IAAM,EAAc,IAMpB,SAAgB,EACd,EACA,EACA,EACA,CACA,GAAM,CAAC,EAAW,GAAgB,EAAS,EAAK,EAC1C,EAAe,EAAO,EAAK,EAC3B,EAAc,EAAuB,IAAI,EAIzC,EAAc,EAAO,CAAQ,EA0DnC,OAzDA,MAAgB,CACd,EAAY,QAAU,CACxB,CAAC,EAED,MAAgB,CACd,IAAM,EAAO,EAAa,QACpB,EAAW,EAAY,QAC7B,GAAI,GAAY,CAAC,GAAQ,CAAC,EAAU,OAEpC,IAAI,EAAY,GACZ,EAAQ,EAGN,MAAc,CAClB,EAAS,UAAU,CAAQ,EAC3B,EAAS,QAAQ,CAAQ,CAC3B,EAEM,MAAiB,CACrB,GAAI,EAAa,QAAS,OAC1B,EAAa,QAAU,GACvB,EAAa,EAAI,EAEjB,IAAM,EAAU,GAAuB,CAGrC,EAAa,QAAU,GACvB,EAAa,EAAK,EACd,KACA,EAAW,EAAM,EAGhB,EAAQ,OAAO,WAAW,EAAO,CAAW,EACnD,EAEA,EAAY,QAAQ,CAAC,CAAC,SACd,EAAO,EAAI,MACX,EAAO,EAAK,CACpB,CACF,EAEM,EAAW,IAAI,sBAClB,CAAC,KAAW,CACP,EAAM,gBAAgB,EAAS,CACrC,EACA,CAAE,MAAK,CACT,EAIA,OAFA,EAAS,QAAQ,CAAQ,MAEZ,CACX,EAAY,GACZ,OAAO,aAAa,CAAK,EACzB,EAAS,WAAW,CACtB,CACF,EAAG,CAAC,EAAc,CAAQ,CAAC,EAEpB,CAAE,YAAW,aAAY,CAClC"}
1
+ {"version":3,"file":"useInfiniteScroll.mjs","names":[],"sources":["../../../../package/common/infiniteBlock/useInfiniteScroll.ts"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport type React from 'react';\n\nconst RETRY_DELAY = 3000;\n// How far ahead of the end a load starts, so that scrolling at a normal pace rarely reaches the\n// spinner. The same as VirtualList's default `loadMoreThreshold`.\nconst LOAD_AHEAD = '100px 0px';\n\n// Loads more items once a sentinel element at the end of the list comes near.\n// IntersectionObserver only fires on a change of intersection state, so if a page of\n// freshly-loaded items still doesn't fill the container, the sentinel stays in range and\n// we have to re-arm the observer ourselves to pick up the next page.\n// Without `onLoadMore` it watches nothing. Passing it only once the container and sentinel are\n// rendered is what starts the watching: an observer set up before then never would.\nexport function useInfiniteScroll(\n containerRef: React.RefObject<HTMLDivElement | null>,\n onLoadMore: (() => Promise<void>) | undefined,\n) {\n const [isLoading, setIsLoading] = useState(false);\n const isLoadingRef = useRef(false);\n const sentinelRef = useRef<HTMLDivElement>(null);\n // The observer of the current effect, so that a load which outlives its own still re-arms this one.\n const rearmRef = useRef(() => {});\n const retryRef = useRef(0);\n // Elements beside the sentinel when the last load started, until the list holds a different number.\n const loadedFromRef = useRef<number | null>(null);\n\n // Kept in a ref so a caller passing a fresh onLoadMore function each render doesn't\n // tear down and recreate the observer every time.\n const onLoadMoreRef = useRef(onLoadMore);\n useEffect(() => {\n onLoadMoreRef.current = onLoadMore;\n });\n const isEnabled = onLoadMore !== undefined;\n\n // A load re-arms only once the list has changed, so one that added nothing ends the asking. Not\n // when it settles: its items often render later, a React Query page a whole task later. Counted\n // in the DOM rather than from the caller's items, where a Fragment of messages counts as one.\n useLayoutEffect(() => {\n const from = loadedFromRef.current;\n if (isLoading || from === null) return;\n if (sentinelRef.current?.parentElement?.childElementCount === from) return;\n loadedFromRef.current = null;\n rearmRef.current();\n });\n\n useEffect(() => {\n const root = containerRef.current;\n const sentinel = sentinelRef.current;\n if (!isEnabled || !root || !sentinel) return undefined;\n\n // Observing again reports the sentinel at once, for a page that still left it in range.\n const rearm = () => {\n observer.unobserve(sentinel);\n observer.observe(sentinel);\n };\n\n const loadMore = () => {\n const load = onLoadMoreRef.current;\n if (!load || isLoadingRef.current) return;\n isLoadingRef.current = true;\n loadedFromRef.current = sentinel.parentElement?.childElementCount ?? null;\n setIsLoading(true);\n\n load().then(\n () => {\n isLoadingRef.current = false;\n setIsLoading(false);\n },\n () => {\n isLoadingRef.current = false;\n loadedFromRef.current = null;\n setIsLoading(false);\n // A failure waits before trying again, or it would retry without end while the sentinel\n // stays in range. It does try again: a list too short to scroll offers no other way.\n retryRef.current = window.setTimeout(() => rearmRef.current(), RETRY_DELAY);\n },\n );\n };\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) loadMore();\n },\n { root, rootMargin: LOAD_AHEAD },\n );\n\n observer.observe(sentinel);\n rearmRef.current = rearm;\n\n return () => {\n window.clearTimeout(retryRef.current);\n observer.disconnect();\n rearmRef.current = () => {};\n };\n }, [containerRef, isEnabled]);\n\n return { isLoading, sentinelRef };\n}\n"],"mappings":"8FAGA,IAAM,EAAc,IAGd,EAAa,YAQnB,SAAgB,EACd,EACA,EACA,CACA,GAAM,CAAC,EAAW,GAAgB,EAAS,EAAK,EAC1C,EAAe,EAAO,EAAK,EAC3B,EAAc,EAAuB,IAAI,EAEzC,EAAW,MAAa,CAAC,CAAC,EAC1B,EAAW,EAAO,CAAC,EAEnB,EAAgB,EAAsB,IAAI,EAI1C,EAAgB,EAAO,CAAU,EACvC,MAAgB,CACd,EAAc,QAAU,CAC1B,CAAC,EACD,IAAM,EAAY,IAAe,IAAA,GAgEjC,OA3DA,MAAsB,CACpB,IAAM,EAAO,EAAc,QACvB,GAAa,IAAS,MACtB,EAAY,SAAS,eAAe,oBAAsB,IAC9D,EAAc,QAAU,KACxB,EAAS,QAAQ,EACnB,CAAC,EAED,MAAgB,CACd,IAAM,EAAO,EAAa,QACpB,EAAW,EAAY,QAC7B,GAAI,CAAC,GAAa,CAAC,GAAQ,CAAC,EAAU,OAGtC,IAAM,MAAc,CAClB,EAAS,UAAU,CAAQ,EAC3B,EAAS,QAAQ,CAAQ,CAC3B,EAEM,MAAiB,CACrB,IAAM,EAAO,EAAc,QACtB,GAAQ,GAAa,UAC1B,EAAa,QAAU,GACvB,EAAc,QAAU,EAAS,eAAe,mBAAqB,KACrE,EAAa,EAAI,EAEjB,EAAK,CAAC,CAAC,SACC,CACJ,EAAa,QAAU,GACvB,EAAa,EAAK,CACpB,MACM,CACJ,EAAa,QAAU,GACvB,EAAc,QAAU,KACxB,EAAa,EAAK,EAGlB,EAAS,QAAU,OAAO,eAAiB,EAAS,QAAQ,EAAG,CAAW,CAC5E,CACF,EACF,EAEM,EAAW,IAAI,sBAClB,CAAC,KAAW,CACP,EAAM,gBAAgB,EAAS,CACrC,EACA,CAAE,OAAM,WAAY,CAAW,CACjC,EAKA,OAHA,EAAS,QAAQ,CAAQ,EACzB,EAAS,QAAU,MAEN,CACX,OAAO,aAAa,EAAS,OAAO,EACpC,EAAS,WAAW,EACpB,EAAS,YAAgB,CAAC,CAC5B,CACF,EAAG,CAAC,EAAc,CAAS,CAAC,EAErB,CAAE,YAAW,aAAY,CAClC"}
@@ -1 +1 @@
1
- {"version":3,"file":"loader.module.mjs","names":[],"sources":["../../../../package/common/loader/loader.module.pcss"],"sourcesContent":["/* No prefers-reduced-motion block: a loader that stands still says the app has hung. */\n.loader {\n display: inline-block;\n position: relative;\n flex-shrink: 0;\n width: var(--loader-size);\n height: var(--loader-size);\n vertical-align: middle;\n\n &-spinner {\n display: block;\n width: 100%;\n height: 100%;\n overflow: visible;\n animation: loaderSpin 1.4s linear infinite;\n\n &-track,\n &-arc {\n fill: none;\n stroke-width: 4.5;\n }\n\n &-track {\n stroke: var(--tone-light);\n }\n\n &-arc {\n stroke: var(--tone);\n stroke-linecap: round;\n stroke-dasharray: 1 150;\n animation: loaderArc 1.4s ease-in-out infinite;\n }\n }\n\n &-comet {\n position: absolute;\n\n /* The spinner's ring reaches 89% of the box; this keeps the two the same size. */\n inset: 5%;\n animation: loaderSpin 0.9s linear infinite;\n\n &-tail {\n position: absolute;\n inset: 0;\n border-radius: 50%;\n\n /* Brightest just behind the head at 0deg, which covers the seam where it starts again. */\n background: conic-gradient(transparent 25%, var(--tone));\n\n /* The hole sits off centre towards the bottom right, so the ring is 12% thick at the head\n and thins to under 5% along the tail. */\n -webkit-mask-image: radial-gradient(41% 41% at 53% 53%, transparent calc(100% - 0.5px), #000 100%);\n mask-image: radial-gradient(41% 41% at 53% 53%, transparent calc(100% - 0.5px), #000 100%);\n }\n\n &-head {\n position: absolute;\n top: 0;\n left: 44%;\n width: 12%;\n height: 12%;\n border-radius: 50%;\n background: var(--tone);\n\n /* A gradient rather than a box-shadow, whose blur cannot be a percentage of the size. */\n &::before {\n content: '';\n position: absolute;\n inset: -100%;\n border-radius: 50%;\n background: radial-gradient(closest-side, color-mix(in srgb, var(--tone) 60%, transparent), transparent);\n }\n }\n }\n\n /* In text, Dots rest on the baseline like an ellipsis: 0.32 is the resting dot's bottom edge in\n loaderBounce's 0% frame. Must follow .loader's own vertical-align, as both are one class. */\n &-on-baseline {\n vertical-align: calc(var(--loader-size) * -0.32);\n }\n\n &-dots {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n\n &-dot {\n width: 24%;\n aspect-ratio: 1;\n border-radius: 50%;\n background: var(--tone);\n animation: loaderBounce 1.2s ease-in-out infinite;\n\n /* Negative delays, so every dot is already in flight on the first frame. */\n &:nth-child(1) {\n animation-delay: -0.3s;\n }\n\n &:nth-child(2) {\n animation-delay: -0.15s;\n }\n }\n\n &-bars {\n position: absolute;\n inset: 8% 4%;\n display: flex;\n justify-content: space-between;\n }\n\n /* A clip-path rather than height, which would lay the bars out again every frame, or scaleY,\n which would squash the rounded ends flat. */\n &-bar {\n width: 12%;\n background: var(--tone);\n animation: loaderEqualise 1s ease-in-out infinite;\n\n &:nth-child(1) {\n animation-delay: -0.6s;\n }\n\n &:nth-child(2) {\n animation-delay: -0.45s;\n }\n\n &:nth-child(3) {\n animation-delay: -0.3s;\n }\n\n &:nth-child(4) {\n animation-delay: -0.15s;\n }\n }\n\n &-pulse {\n position: absolute;\n inset: 0;\n\n &-ring {\n position: absolute;\n inset: 0;\n border-radius: 50%;\n background: var(--tone);\n opacity: 0;\n animation: loaderRipple 1.8s cubic-bezier(0.25, 0.6, 0.4, 1) infinite;\n\n &:nth-child(2) {\n animation-delay: -0.9s;\n }\n }\n\n &-core {\n position: absolute;\n inset: 34%;\n border-radius: 50%;\n background: var(--tone);\n animation: loaderBreathe 1.8s ease-in-out infinite;\n }\n }\n}\n\n/* No keyframe may share a name with a class's camelCase key (loaderDot for .loader-dot): CSS\n Modules exports both under that key, and the class gets the keyframe's name. */\n@keyframes loaderSpin {\n to {\n transform: rotate(360deg);\n }\n}\n\n/* r=20, so the circumference is 125.7: the arc grows to 90 and slides off the end. */\n@keyframes loaderArc {\n 0% {\n stroke-dasharray: 1 150;\n stroke-dashoffset: 0;\n }\n\n 50% {\n stroke-dasharray: 90 150;\n stroke-dashoffset: -35;\n }\n\n 100% {\n stroke-dasharray: 90 150;\n stroke-dashoffset: -124;\n }\n}\n\n/* Resting dots stay plainly visible: faded, only the raised ones showed, and the dots read as\n floating mid-line rather than on the baseline. */\n@keyframes loaderBounce {\n 0%,\n 30%,\n 100% {\n transform: translateY(25%);\n opacity: 0.55;\n }\n\n 15% {\n transform: translateY(-35%);\n opacity: 1;\n }\n}\n\n@keyframes loaderEqualise {\n 0%,\n 100% {\n clip-path: inset(35% 0 round 999px);\n opacity: 0.45;\n }\n\n 50% {\n clip-path: inset(0 0 round 999px);\n opacity: 1;\n }\n}\n\n@keyframes loaderRipple {\n from {\n transform: scale(0.3);\n opacity: 0.7;\n }\n\n to {\n transform: scale(1);\n opacity: 0;\n }\n}\n\n@keyframes loaderBreathe {\n 0%,\n 100% {\n transform: scale(1);\n }\n\n 50% {\n transform: scale(0.8);\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"loader.module.mjs","names":[],"sources":["../../../../package/common/loader/loader.module.pcss"],"sourcesContent":["/* No prefers-reduced-motion block: a loader that stands still says the app has hung. */\n.loader {\n display: inline-block;\n position: relative;\n flex-shrink: 0;\n width: var(--loader-size);\n height: var(--loader-size);\n vertical-align: middle;\n\n /* A spinning box reaches past its corners. This keeps that out of a scroll container's overflow,\n where it toggled a scrollbar every turn, and still paints it: `overflow: hidden` would cut off\n the comet's glow. */\n contain: layout;\n\n &-spinner {\n display: block;\n width: 100%;\n height: 100%;\n overflow: visible;\n animation: loaderSpin 1.4s linear infinite;\n\n &-track,\n &-arc {\n fill: none;\n stroke-width: 4.5;\n }\n\n &-track {\n stroke: var(--tone-light);\n }\n\n &-arc {\n stroke: var(--tone);\n stroke-linecap: round;\n stroke-dasharray: 1 150;\n animation: loaderArc 1.4s ease-in-out infinite;\n }\n }\n\n &-comet {\n position: absolute;\n\n /* The spinner's ring reaches 89% of the box; this keeps the two the same size. */\n inset: 5%;\n animation: loaderSpin 0.9s linear infinite;\n\n &-tail {\n position: absolute;\n inset: 0;\n border-radius: 50%;\n\n /* Brightest just behind the head at 0deg, which covers the seam where it starts again. */\n background: conic-gradient(transparent 25%, var(--tone));\n\n /* The hole sits off centre towards the bottom right, so the ring is 12% thick at the head\n and thins to under 5% along the tail. */\n -webkit-mask-image: radial-gradient(41% 41% at 53% 53%, transparent calc(100% - 0.5px), #000 100%);\n mask-image: radial-gradient(41% 41% at 53% 53%, transparent calc(100% - 0.5px), #000 100%);\n }\n\n &-head {\n position: absolute;\n top: 0;\n left: 44%;\n width: 12%;\n height: 12%;\n border-radius: 50%;\n background: var(--tone);\n\n /* A gradient rather than a box-shadow, whose blur cannot be a percentage of the size. */\n &::before {\n content: '';\n position: absolute;\n inset: -100%;\n border-radius: 50%;\n background: radial-gradient(closest-side, color-mix(in srgb, var(--tone) 60%, transparent), transparent);\n }\n }\n }\n\n /* In text, Dots rest on the baseline like an ellipsis: 0.32 is the resting dot's bottom edge in\n loaderBounce's 0% frame. Must follow .loader's own vertical-align, as both are one class. */\n &-on-baseline {\n vertical-align: calc(var(--loader-size) * -0.32);\n }\n\n &-dots {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n\n &-dot {\n width: 24%;\n aspect-ratio: 1;\n border-radius: 50%;\n background: var(--tone);\n animation: loaderBounce 1.2s ease-in-out infinite;\n\n /* Negative delays, so every dot is already in flight on the first frame. */\n &:nth-child(1) {\n animation-delay: -0.3s;\n }\n\n &:nth-child(2) {\n animation-delay: -0.15s;\n }\n }\n\n &-bars {\n position: absolute;\n inset: 8% 4%;\n display: flex;\n justify-content: space-between;\n }\n\n /* A clip-path rather than height, which would lay the bars out again every frame, or scaleY,\n which would squash the rounded ends flat. */\n &-bar {\n width: 12%;\n background: var(--tone);\n animation: loaderEqualise 1s ease-in-out infinite;\n\n &:nth-child(1) {\n animation-delay: -0.6s;\n }\n\n &:nth-child(2) {\n animation-delay: -0.45s;\n }\n\n &:nth-child(3) {\n animation-delay: -0.3s;\n }\n\n &:nth-child(4) {\n animation-delay: -0.15s;\n }\n }\n\n &-pulse {\n position: absolute;\n inset: 0;\n\n &-ring {\n position: absolute;\n inset: 0;\n border-radius: 50%;\n background: var(--tone);\n opacity: 0;\n animation: loaderRipple 1.8s cubic-bezier(0.25, 0.6, 0.4, 1) infinite;\n\n &:nth-child(2) {\n animation-delay: -0.9s;\n }\n }\n\n &-core {\n position: absolute;\n inset: 34%;\n border-radius: 50%;\n background: var(--tone);\n animation: loaderBreathe 1.8s ease-in-out infinite;\n }\n }\n}\n\n/* No keyframe may share a name with a class's camelCase key (loaderDot for .loader-dot): CSS\n Modules exports both under that key, and the class gets the keyframe's name. */\n@keyframes loaderSpin {\n to {\n transform: rotate(360deg);\n }\n}\n\n/* r=20, so the circumference is 125.7: the arc grows to 90 and slides off the end. */\n@keyframes loaderArc {\n 0% {\n stroke-dasharray: 1 150;\n stroke-dashoffset: 0;\n }\n\n 50% {\n stroke-dasharray: 90 150;\n stroke-dashoffset: -35;\n }\n\n 100% {\n stroke-dasharray: 90 150;\n stroke-dashoffset: -124;\n }\n}\n\n/* Resting dots stay plainly visible: faded, only the raised ones showed, and the dots read as\n floating mid-line rather than on the baseline. */\n@keyframes loaderBounce {\n 0%,\n 30%,\n 100% {\n transform: translateY(25%);\n opacity: 0.55;\n }\n\n 15% {\n transform: translateY(-35%);\n opacity: 1;\n }\n}\n\n@keyframes loaderEqualise {\n 0%,\n 100% {\n clip-path: inset(35% 0 round 999px);\n opacity: 0.45;\n }\n\n 50% {\n clip-path: inset(0 0 round 999px);\n opacity: 1;\n }\n}\n\n@keyframes loaderRipple {\n from {\n transform: scale(0.3);\n opacity: 0.7;\n }\n\n to {\n transform: scale(1);\n opacity: 0;\n }\n}\n\n@keyframes loaderBreathe {\n 0%,\n 100% {\n transform: scale(1);\n }\n\n 50% {\n transform: scale(0.8);\n }\n}\n"],"mappings":""}