@evolve.ui/core-components 1.1.0 → 1.2.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/{BottomSheet-ZV_eq3xt.mjs → BottomSheet-CNVLnEoD.mjs} +3 -3
- package/BottomSheet-CNVLnEoD.mjs.map +1 -0
- package/{BottomSheet-emWP9Fct.cjs → BottomSheet-D5w0WZDl.cjs} +3 -3
- package/BottomSheet-D5w0WZDl.cjs.map +1 -0
- package/Button.cjs +1 -1
- package/Button.mjs +1 -1
- package/{CookieConsent-CIwm0bTz.mjs → CookieConsent-B9dg9WdC.mjs} +2 -2
- package/{CookieConsent-CIwm0bTz.mjs.map → CookieConsent-B9dg9WdC.mjs.map} +1 -1
- package/{CookieConsent-PIglB_bb.cjs → CookieConsent-BSXJHMwX.cjs} +2 -2
- package/{CookieConsent-PIglB_bb.cjs.map → CookieConsent-BSXJHMwX.cjs.map} +1 -1
- package/{Drawer-9HFG45OA.cjs → Drawer-3rA9dGCl.cjs} +2 -2
- package/{Drawer-9HFG45OA.cjs.map → Drawer-3rA9dGCl.cjs.map} +1 -1
- package/Drawer-CFZbwg0R.d.cts.map +1 -1
- package/Drawer-CFZbwg0R.d.mts.map +1 -1
- package/{Drawer-BkCPfK6P.mjs → Drawer-DzKEmewj.mjs} +2 -2
- package/{Drawer-BkCPfK6P.mjs.map → Drawer-DzKEmewj.mjs.map} +1 -1
- package/{FavoriteButton-CC3qL0lR.mjs → FavoriteButton-7nK77lQP.mjs} +6 -6
- package/FavoriteButton-7nK77lQP.mjs.map +1 -0
- package/{FavoriteButton-BGF-3dze.d.cts → FavoriteButton-B8r_qtlj.d.cts} +2 -2
- package/{FavoriteButton-BGF-3dze.d.cts.map → FavoriteButton-B8r_qtlj.d.cts.map} +1 -1
- package/{FavoriteButton-DliYzkfu.cjs → FavoriteButton-BkIaWfgD.cjs} +6 -6
- package/FavoriteButton-BkIaWfgD.cjs.map +1 -0
- package/{FavoriteButton-BZEGWrRo.d.mts → FavoriteButton-hIkGjcKH.d.mts} +2 -2
- package/{FavoriteButton-BZEGWrRo.d.mts.map → FavoriteButton-hIkGjcKH.d.mts.map} +1 -1
- package/{FileUpload-p_pl9NXr.mjs → FileUpload-BfZOMsLs.mjs} +6 -6
- package/FileUpload-BfZOMsLs.mjs.map +1 -0
- package/{FileUpload--e_UJ_ek.cjs → FileUpload-gjRbkgrL.cjs} +6 -6
- package/FileUpload-gjRbkgrL.cjs.map +1 -0
- package/IconButton.cjs +1 -1
- package/IconButton.mjs +1 -1
- package/{ImageSlider-BidXfnLj.cjs → ImageSlider-CAgxQffP.cjs} +3 -3
- package/ImageSlider-CAgxQffP.cjs.map +1 -0
- package/{ImageSlider-CDrg_TbL.mjs → ImageSlider-fdrJ6lIJ.mjs} +3 -3
- package/ImageSlider-fdrJ6lIJ.mjs.map +1 -0
- package/InfoIconTooltip.cjs +1 -1
- package/InfoIconTooltip.cjs.map +1 -1
- package/InfoIconTooltip.mjs +1 -1
- package/InfoIconTooltip.mjs.map +1 -1
- package/InlineButton-CRLbNJZ3.d.mts.map +1 -1
- package/InlineButton-DoXGLiSE.d.cts.map +1 -1
- package/Input.cjs +1 -1
- package/Input.cjs.map +1 -1
- package/Input.mjs +1 -1
- package/Input.mjs.map +1 -1
- package/JumpMenu-BXYZnPIH.cjs.map +1 -1
- package/JumpMenu-DcZjAiej.mjs.map +1 -1
- package/{Menu-Cxu8CaX1.mjs → Menu-Bn4smlEe.mjs} +2 -2
- package/Menu-Bn4smlEe.mjs.map +1 -0
- package/{Menu-ClUogmwE.cjs → Menu-DuDhtoNP.cjs} +2 -2
- package/Menu-DuDhtoNP.cjs.map +1 -0
- package/Modal-DM0aXJZ5.d.cts.map +1 -1
- package/Modal-DM0aXJZ5.d.mts.map +1 -1
- package/{Modal-C6gatdtT.mjs → Modal-DtxWd7Le.mjs} +3 -2
- package/{Modal-C6gatdtT.mjs.map → Modal-DtxWd7Le.mjs.map} +1 -1
- package/{Modal-CiBqXyiK.cjs → Modal-q4HhBAQU.cjs} +3 -2
- package/Modal-q4HhBAQU.cjs.map +1 -0
- package/{Pagination-bAtbN478.cjs → Pagination-Denb9_zB.cjs} +2 -2
- package/{Pagination-bAtbN478.cjs.map → Pagination-Denb9_zB.cjs.map} +1 -1
- package/{Pagination-DIpGKMLz.mjs → Pagination-Dyfeys4B.mjs} +2 -2
- package/{Pagination-DIpGKMLz.mjs.map → Pagination-Dyfeys4B.mjs.map} +1 -1
- package/PhoneInput-B5pkpkdt.mjs.map +1 -1
- package/PhoneInput-BXnkBME4.d.mts.map +1 -1
- package/PhoneInput-Bkrbrins.d.cts.map +1 -1
- package/PhoneInput-CUMPOSxJ.cjs.map +1 -1
- package/{ProgressCircle-BC81D9sU.cjs → ProgressCircle-B6R4dxoV.cjs} +2 -2
- package/{ProgressCircle-BC81D9sU.cjs.map → ProgressCircle-B6R4dxoV.cjs.map} +1 -1
- package/{ProgressCircle-ae8cvZhe.mjs → ProgressCircle-zSai1HTT.mjs} +2 -2
- package/{ProgressCircle-ae8cvZhe.mjs.map → ProgressCircle-zSai1HTT.mjs.map} +1 -1
- package/SearchInput-DYIPC8Cd.d.cts.map +1 -1
- package/SearchInput-IfEWePDQ.d.mts.map +1 -1
- package/{Select-BkZu8OLs.mjs → Select-CH_ZN88G.mjs} +2 -2
- package/Select-CH_ZN88G.mjs.map +1 -0
- package/{Select-BXDjzyDs.cjs → Select-DCCcyLT4.cjs} +2 -2
- package/Select-DCCcyLT4.cjs.map +1 -0
- package/{SplitButton-DcmmLW8j.cjs → SplitButton-B_Y253RU.cjs} +2 -2
- package/{SplitButton-DcmmLW8j.cjs.map → SplitButton-B_Y253RU.cjs.map} +1 -1
- package/{SplitButton-WE5ymwCQ.mjs → SplitButton-_poP9Ykq.mjs} +2 -2
- package/{SplitButton-WE5ymwCQ.mjs.map → SplitButton-_poP9Ykq.mjs.map} +1 -1
- package/{Table-M-rANdMt.mjs → Table-CQGtDpvH.mjs} +3 -3
- package/{Table-M-rANdMt.mjs.map → Table-CQGtDpvH.mjs.map} +1 -1
- package/{Table-CLlDragJ.cjs → Table-D4UYosb1.cjs} +3 -3
- package/{Table-CLlDragJ.cjs.map → Table-D4UYosb1.cjs.map} +1 -1
- package/Typography/InlineButton.cjs +1 -1
- package/Typography/InlineButton.cjs.map +1 -1
- package/Typography/InlineButton.mjs +1 -1
- package/Typography/InlineButton.mjs.map +1 -1
- package/{ViewMore-BBoUvo7p.cjs → ViewMore-BcoSRZSl.cjs} +14 -1
- package/ViewMore-BcoSRZSl.cjs.map +1 -0
- package/{ViewMore-BrZPuE4E.mjs → ViewMore-Be0TQZ_2.mjs} +14 -1
- package/ViewMore-Be0TQZ_2.mjs.map +1 -0
- package/bottomSheet.cjs +1 -1
- package/bottomSheet.mjs +1 -1
- package/{buttonColorClasses-C3hg1at8.cjs → buttonColorClasses-CQQmLYfk.cjs} +4 -4
- package/buttonColorClasses-CQQmLYfk.cjs.map +1 -0
- package/{buttonColorClasses-B7OU4aQA.mjs → buttonColorClasses-CQnqAJXl.mjs} +4 -4
- package/buttonColorClasses-CQnqAJXl.mjs.map +1 -0
- package/card/components/createFavoriteButton.cjs +1 -1
- package/card/components/createFavoriteButton.mjs +1 -1
- package/cookieConsent.cjs +1 -1
- package/cookieConsent.mjs +1 -1
- package/drawer.cjs +1 -1
- package/drawer.mjs +1 -1
- package/favoriteButton.cjs +1 -1
- package/favoriteButton.d.cts +1 -1
- package/favoriteButton.d.mts +1 -1
- package/favoriteButton.mjs +1 -1
- package/featureFlags/hooks.d.cts +1 -1
- package/featureFlags/hooks.d.mts +1 -1
- package/fileUpload/testUtils/mockFilesAndImages.d.cts +3 -3
- package/fileUpload/testUtils/mockFilesAndImages.d.cts.map +1 -1
- package/fileUpload/testUtils/mockFilesAndImages.d.mts +3 -3
- package/fileUpload/testUtils/mockFilesAndImages.d.mts.map +1 -1
- package/fileUpload/testUtils/testUtils.d.cts +1 -1
- package/fileUpload/testUtils/testUtils.d.cts.map +1 -1
- package/fileUpload/testUtils/testUtils.d.mts +1 -1
- package/fileUpload/testUtils/testUtils.d.mts.map +1 -1
- package/fileUpload.cjs +1 -1
- package/fileUpload.mjs +1 -1
- package/imageSlider.cjs +1 -1
- package/imageSlider.mjs +1 -1
- package/index.cjs +14 -14
- package/index.d.cts +1 -1
- package/index.d.mts +1 -1
- package/index.mjs +14 -14
- package/menu.cjs +1 -1
- package/menu.mjs +1 -1
- package/modal/useCloseModal.cjs +2 -1
- package/modal/useCloseModal.cjs.map +1 -1
- package/modal/useCloseModal.mjs +2 -1
- package/modal/useCloseModal.mjs.map +1 -1
- package/modal.cjs +1 -1
- package/modal.mjs +1 -1
- package/package.json +2 -4
- package/pagination.cjs +1 -1
- package/pagination.mjs +1 -1
- package/progressCircle.cjs +1 -1
- package/progressCircle.mjs +1 -1
- package/searchInput.cjs +4 -4
- package/searchInput.cjs.map +1 -1
- package/searchInput.mjs +4 -4
- package/searchInput.mjs.map +1 -1
- package/select.cjs +1 -1
- package/select.mjs +1 -1
- package/selectAsync.cjs +1 -1
- package/selectAsync.mjs +1 -1
- package/selectAsyncCreatable.cjs +1 -1
- package/selectAsyncCreatable.mjs +1 -1
- package/selectCreatable.cjs +1 -1
- package/selectCreatable.mjs +1 -1
- package/splitButton.cjs +1 -1
- package/splitButton.mjs +1 -1
- package/src/BottomSheet/components/BottomSheetContentArea/BottomSheetContentArea.tsx +4 -3
- package/src/BottomSheet/components/BottomSheetOverlay/BottomSheetOverlayTranslate3d.tsx +5 -2
- package/src/Button/buttonColorClasses.ts +8 -3
- package/src/Drawer/Drawer.tsx +5 -8
- package/src/FavoriteButton/components/IconOnlyFavoriteButton/IconOnlyFavoriteButton.tsx +4 -1
- package/src/FavoriteButton/components/LabeledAnimatedFavoriteButton/LabeledAnimatedFavoriteButton.tsx +13 -6
- package/src/FavoriteButton/types/favoriteButton.ts +1 -1
- package/src/FileUpload/components/GridFileCard/GridFileCard.tsx +1 -1
- package/src/FileUpload/components/ListFileCard/ListFileCard.tsx +2 -2
- package/src/ImageSlider/ImageSlider.tsx +3 -1
- package/src/ImageSlider/components/Thumbnails/Thumbnails.tsx +1 -1
- package/src/JumpMenu/components/JumpMenuItem/JumpMenuItem.tsx +4 -1
- package/src/Menu/components/MenuItem/MenuItem.tsx +1 -1
- package/src/Modal/ComposableModal.tsx +9 -1
- package/src/Modal/hooks/useCloseModal.ts +8 -1
- package/src/PhoneInput/PhoneInput.tsx +7 -1
- package/src/ProgressCircle/ProgressCircle.tsx +1 -1
- package/src/SearchInput/SearchInput.tsx +5 -4
- package/src/Select/selectComponents.tsx +1 -2
- package/src/Tabs/Tabs.tsx +4 -4
- package/src/TextArea/TextArea.tsx +2 -0
- package/src/TextInput/TextInput.tsx +1 -1
- package/src/Tooltip/InfoIconTooltip.tsx +1 -1
- package/src/Typography/InlineButton/InlineButton.tsx +4 -3
- package/src/utils/smoothScrollTo.ts +18 -0
- package/table.cjs +1 -1
- package/table.mjs +1 -1
- package/tabs.cjs +1 -1
- package/tabs.cjs.map +1 -1
- package/tabs.mjs +1 -1
- package/tabs.mjs.map +1 -1
- package/textArea.cjs.map +1 -1
- package/textArea.mjs.map +1 -1
- package/viewMore.cjs +1 -1
- package/viewMore.mjs +1 -1
- package/BottomSheet-ZV_eq3xt.mjs.map +0 -1
- package/BottomSheet-emWP9Fct.cjs.map +0 -1
- package/FavoriteButton-CC3qL0lR.mjs.map +0 -1
- package/FavoriteButton-DliYzkfu.cjs.map +0 -1
- package/FileUpload--e_UJ_ek.cjs.map +0 -1
- package/FileUpload-p_pl9NXr.mjs.map +0 -1
- package/ImageSlider-BidXfnLj.cjs.map +0 -1
- package/ImageSlider-CDrg_TbL.mjs.map +0 -1
- package/Menu-ClUogmwE.cjs.map +0 -1
- package/Menu-Cxu8CaX1.mjs.map +0 -1
- package/Modal-CiBqXyiK.cjs.map +0 -1
- package/Select-BXDjzyDs.cjs.map +0 -1
- package/Select-BkZu8OLs.mjs.map +0 -1
- package/ViewMore-BBoUvo7p.cjs.map +0 -1
- package/ViewMore-BrZPuE4E.mjs.map +0 -1
- package/buttonColorClasses-B7OU4aQA.mjs.map +0 -1
- package/buttonColorClasses-C3hg1at8.cjs.map +0 -1
package/Input.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Input.mjs","names":[],"sources":["../src/TextInput/TextInput.tsx"],"sourcesContent":["import { cn } from '@evolve.ui/theme-system';\nimport { ChangeEvent, forwardRef, HTMLProps, ReactNode, useCallback, useEffect, useState } from 'react';\n\nimport { extractFormLabelProps, FormLabel, FormLabelProps } from '../FormLabel/FormLabel';\nimport { IconButton } from '../IconButton/IconButton';\nimport { Close, Eye, EyeSlash } from '../Icons';\nimport { BodyPrimary, LabelMini } from '../Typography/Text/Text';\n\nexport type AllowedTypes = 'text' | 'email' | 'number' | 'password' | 'url';\n\ntype Props = Omit<HTMLProps<HTMLInputElement>, 'as' | 'value'> & {\n characterLimit?: number;\n clearAriaLabel?: string;\n /**\n * Input emphasis. Currently supports subtle and bold. Defaults to subtle.\n */\n emphasis?: 'subtle' | 'bold';\n overCharacterLimitText?: string;\n prefix?: ReactNode;\n small?: boolean;\n\n suffix?: ReactNode;\n /**\n * Input type. Currently supports text, email, number, password, and url. Defaults to text.\n */\n type?: AllowedTypes;\n value?: string;\n} & Partial<\n Omit<HTMLProps<HTMLInputElement>, 'as' | 'aria-label' | 'aria-invalid' | 'aria-errormessage' | 'children'>\n > &\n ({ onClear: undefined; showClearIcon?: false } | { onClear?: () => void; showClearIcon?: boolean }) &\n FormLabelProps;\n\nexport const TextInput = forwardRef<HTMLInputElement, Props>((unfilteredProps, ref) => {\n const [\n formLabelProps,\n {\n // need this to properly style onClear (only show when placeholder is not showing)\n characterLimit = 0,\n clearAriaLabel = 'Clear text',\n disabled,\n emphasis = 'subtle',\n onChange,\n onClear,\n overCharacterLimitText = 'Too many characters',\n placeholder = ' ',\n prefix,\n showClearIcon: showClearIconProp,\n small,\n suffix,\n type,\n ...props\n },\n ] = extractFormLabelProps(unfilteredProps);\n\n const [characterCount, setCharacterCount] = useState(props.value?.length ?? 0);\n\n useEffect(() => {\n if (props.value !== undefined) {\n setCharacterCount(String(props.value).length);\n }\n }, [props.value]);\n\n const handleOnChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n setCharacterCount(String((e.target as HTMLInputElement).value).length);\n onChange?.(e);\n },\n [onChange]\n );\n\n const overCharacterLimit = characterLimit ? characterCount > characterLimit : false;\n const hasError = Boolean(formLabelProps.hasError) || overCharacterLimit;\n\n const showClearIcon = showClearIconProp ?? Boolean(onClear);\n\n const [inputType, setInputType] = useState(type ?? 'text');\n\n const togglePassword = (): void => {\n if (!disabled) {\n setInputType(inputType === 'password' ? 'text' : 'password');\n }\n };\n\n const showClear = showClearIcon && Boolean(onClear);\n const showPasswordToggle = type === 'password';\n\n return (\n <FormLabel\n {...formLabelProps}\n disabled={disabled}\n errorText={overCharacterLimit ? overCharacterLimitText : formLabelProps.errorText}\n hasError={hasError}\n showTopRight={Boolean(formLabelProps.secure) || characterLimit > 0}\n topRight={\n formLabelProps.secure ? undefined : (\n <LabelMini\n className=\"ml-auto whitespace-nowrap grid-area-[topright]\"\n color={formLabelProps.disabled ? 'disabled' : overCharacterLimit ? 'critical' : 'secondary'}\n >\n {characterCount}/{characterLimit}\n </LabelMini>\n )\n }\n >\n <div\n className={cn(\n 'overflow-none box-content flex items-center gap-2xs rounded-input border border-border-secondary bg-background-primary px-xs hover:border-border-hover has-[:focus]:border-border-selected',\n {\n '!border-border-disabled bg-background-disabled text-text-disabled': disabled,\n 'border-border-critical': hasError,\n 'h-[32px]': small, // Smaller due to box-content to prevent clipping\n 'h-[40px]': !small, // Smaller due to box-content to prevent clipping\n }\n )}\n >\n {Boolean(prefix) && <BodyPrimary color={disabled ? 'disabled' : 'secondary'}>{prefix}</BodyPrimary>}\n <input\n className={cn(\n 'peer hui-text-body-primary hui-autofill-transparent w-full bg-transparent outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',\n {\n 'text-text-primary placeholder:text-text-secondary': emphasis === 'bold',\n }\n )}\n {...props}\n disabled={disabled}\n onChange={handleOnChange}\n placeholder={placeholder}\n ref={ref}\n type={inputType}\n />\n {Boolean(suffix) && <BodyPrimary color={disabled ? 'disabled' : 'secondary'}>{suffix}</BodyPrimary>}\n {showClear && (\n <IconButton\n aria-label={clearAriaLabel}\n className={cn(\n 'self-center justify-self-end !rounded-button peer-placeholder-shown:hidden',\n small ? 'mr-[-8px]' : 'mr-[-4px]'\n )}\n disabled={disabled}\n hideTooltip\n icon={Close}\n onClick={onClear}\n role=\"button\"\n shape=\"square\"\n size=\"sm\"\n type=\"button\"\n />\n )}\n {showPasswordToggle &&\n (inputType === 'password' ? (\n <EyeSlash\n color={disabled ? 'disabled' : 'primary'}\n // hoverColor={disabled ? undefined : 'hover'}\n onClick={togglePassword}\n role=\"button\"\n size=\"sm\"\n />\n ) : (\n <Eye\n color={disabled ? 'disabled' : 'primary'}\n // hoverColor={disabled ? undefined : 'hover'}\n onClick={togglePassword}\n role=\"button\"\n size=\"sm\"\n />\n ))}\n </div>\n </FormLabel>\n );\n});\n\nTextInput.displayName = 'TextInput';\n"],"mappings":";;;;;;;;AAiCA,MAAa,YAAY,YAAqC,iBAAiB,QAAQ;CACnF,MAAM,CACF,gBACA,EAEI,iBAAiB,GACjB,iBAAiB,cACjB,UACA,WAAW,UACX,UACA,SACA,yBAAyB,uBACzB,cAAc,KACd,QACA,eAAe,mBACf,OACA,QACA,MACA,GAAG,WAEP,sBAAsB,eAAe;CAEzC,MAAM,CAAC,gBAAgB,qBAAqB,SAAS,MAAM,OAAO,UAAU,CAAC;CAE7E,gBAAgB;EACZ,IAAI,MAAM,UAAU,KAAA,GAChB,kBAAkB,OAAO,MAAM,KAAK,CAAC,CAAC,MAAM;CAEpD,GAAG,CAAC,MAAM,KAAK,CAAC;CAEhB,MAAM,iBAAiB,aAClB,MAAqC;EAClC,kBAAkB,OAAQ,EAAE,OAA4B,KAAK,CAAC,CAAC,MAAM;EACrE,WAAW,CAAC;CAChB,GACA,CAAC,QAAQ,CACb;CAEA,MAAM,qBAAqB,iBAAiB,iBAAiB,iBAAiB;CAC9E,MAAM,WAAW,QAAQ,eAAe,QAAQ,KAAK;CAErD,MAAM,gBAAgB,qBAAqB,QAAQ,OAAO;CAE1D,MAAM,CAAC,WAAW,gBAAgB,SAAS,QAAQ,MAAM;CAEzD,MAAM,uBAA6B;EAC/B,IAAI,CAAC,UACD,aAAa,cAAc,aAAa,SAAS,UAAU;CAEnE;CAEA,MAAM,YAAY,iBAAiB,QAAQ,OAAO;CAClD,MAAM,qBAAqB,SAAS;CAEpC,OACI,oBAAC,WAAD;EACI,GAAI;EACM;EACV,WAAW,qBAAqB,yBAAyB,eAAe;EAC9D;EACV,cAAc,QAAQ,eAAe,MAAM,KAAK,iBAAiB;EACjE,UACI,eAAe,SAAS,KAAA,IACpB,qBAAC,WAAD;GACI,WAAU;GACV,OAAO,eAAe,WAAW,aAAa,qBAAqB,aAAa;aAFpF;IAIK;IAAe;IAAE;GACX;;YAInB,qBAAC,OAAD;GACI,WAAW,GACP,
|
|
1
|
+
{"version":3,"file":"Input.mjs","names":[],"sources":["../src/TextInput/TextInput.tsx"],"sourcesContent":["import { cn } from '@evolve.ui/theme-system';\nimport { ChangeEvent, forwardRef, HTMLProps, ReactNode, useCallback, useEffect, useState } from 'react';\n\nimport { extractFormLabelProps, FormLabel, FormLabelProps } from '../FormLabel/FormLabel';\nimport { IconButton } from '../IconButton/IconButton';\nimport { Close, Eye, EyeSlash } from '../Icons';\nimport { BodyPrimary, LabelMini } from '../Typography/Text/Text';\n\nexport type AllowedTypes = 'text' | 'email' | 'number' | 'password' | 'url';\n\ntype Props = Omit<HTMLProps<HTMLInputElement>, 'as' | 'value'> & {\n characterLimit?: number;\n clearAriaLabel?: string;\n /**\n * Input emphasis. Currently supports subtle and bold. Defaults to subtle.\n */\n emphasis?: 'subtle' | 'bold';\n overCharacterLimitText?: string;\n prefix?: ReactNode;\n small?: boolean;\n\n suffix?: ReactNode;\n /**\n * Input type. Currently supports text, email, number, password, and url. Defaults to text.\n */\n type?: AllowedTypes;\n value?: string;\n} & Partial<\n Omit<HTMLProps<HTMLInputElement>, 'as' | 'aria-label' | 'aria-invalid' | 'aria-errormessage' | 'children'>\n > &\n ({ onClear: undefined; showClearIcon?: false } | { onClear?: () => void; showClearIcon?: boolean }) &\n FormLabelProps;\n\nexport const TextInput = forwardRef<HTMLInputElement, Props>((unfilteredProps, ref) => {\n const [\n formLabelProps,\n {\n // need this to properly style onClear (only show when placeholder is not showing)\n characterLimit = 0,\n clearAriaLabel = 'Clear text',\n disabled,\n emphasis = 'subtle',\n onChange,\n onClear,\n overCharacterLimitText = 'Too many characters',\n placeholder = ' ',\n prefix,\n showClearIcon: showClearIconProp,\n small,\n suffix,\n type,\n ...props\n },\n ] = extractFormLabelProps(unfilteredProps);\n\n const [characterCount, setCharacterCount] = useState(props.value?.length ?? 0);\n\n useEffect(() => {\n if (props.value !== undefined) {\n setCharacterCount(String(props.value).length);\n }\n }, [props.value]);\n\n const handleOnChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n setCharacterCount(String((e.target as HTMLInputElement).value).length);\n onChange?.(e);\n },\n [onChange]\n );\n\n const overCharacterLimit = characterLimit ? characterCount > characterLimit : false;\n const hasError = Boolean(formLabelProps.hasError) || overCharacterLimit;\n\n const showClearIcon = showClearIconProp ?? Boolean(onClear);\n\n const [inputType, setInputType] = useState(type ?? 'text');\n\n const togglePassword = (): void => {\n if (!disabled) {\n setInputType(inputType === 'password' ? 'text' : 'password');\n }\n };\n\n const showClear = showClearIcon && Boolean(onClear);\n const showPasswordToggle = type === 'password';\n\n return (\n <FormLabel\n {...formLabelProps}\n disabled={disabled}\n errorText={overCharacterLimit ? overCharacterLimitText : formLabelProps.errorText}\n hasError={hasError}\n showTopRight={Boolean(formLabelProps.secure) || characterLimit > 0}\n topRight={\n formLabelProps.secure ? undefined : (\n <LabelMini\n className=\"ml-auto whitespace-nowrap grid-area-[topright]\"\n color={formLabelProps.disabled ? 'disabled' : overCharacterLimit ? 'critical' : 'secondary'}\n >\n {characterCount}/{characterLimit}\n </LabelMini>\n )\n }\n >\n <div\n className={cn(\n 'overflow-none box-content flex items-center gap-2xs rounded-input border border-border-secondary bg-background-primary px-xs hover:border-border-hover has-[:focus-visible]:border-border-selected',\n {\n '!border-border-disabled bg-background-disabled text-text-disabled': disabled,\n 'border-border-critical': hasError,\n 'h-[32px]': small, // Smaller due to box-content to prevent clipping\n 'h-[40px]': !small, // Smaller due to box-content to prevent clipping\n }\n )}\n >\n {Boolean(prefix) && <BodyPrimary color={disabled ? 'disabled' : 'secondary'}>{prefix}</BodyPrimary>}\n <input\n className={cn(\n 'peer hui-text-body-primary hui-autofill-transparent w-full bg-transparent outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',\n {\n 'text-text-primary placeholder:text-text-secondary': emphasis === 'bold',\n }\n )}\n {...props}\n disabled={disabled}\n onChange={handleOnChange}\n placeholder={placeholder}\n ref={ref}\n type={inputType}\n />\n {Boolean(suffix) && <BodyPrimary color={disabled ? 'disabled' : 'secondary'}>{suffix}</BodyPrimary>}\n {showClear && (\n <IconButton\n aria-label={clearAriaLabel}\n className={cn(\n 'self-center justify-self-end !rounded-button peer-placeholder-shown:hidden',\n small ? 'mr-[-8px]' : 'mr-[-4px]'\n )}\n disabled={disabled}\n hideTooltip\n icon={Close}\n onClick={onClear}\n role=\"button\"\n shape=\"square\"\n size=\"sm\"\n type=\"button\"\n />\n )}\n {showPasswordToggle &&\n (inputType === 'password' ? (\n <EyeSlash\n color={disabled ? 'disabled' : 'primary'}\n // hoverColor={disabled ? undefined : 'hover'}\n onClick={togglePassword}\n role=\"button\"\n size=\"sm\"\n />\n ) : (\n <Eye\n color={disabled ? 'disabled' : 'primary'}\n // hoverColor={disabled ? undefined : 'hover'}\n onClick={togglePassword}\n role=\"button\"\n size=\"sm\"\n />\n ))}\n </div>\n </FormLabel>\n );\n});\n\nTextInput.displayName = 'TextInput';\n"],"mappings":";;;;;;;;AAiCA,MAAa,YAAY,YAAqC,iBAAiB,QAAQ;CACnF,MAAM,CACF,gBACA,EAEI,iBAAiB,GACjB,iBAAiB,cACjB,UACA,WAAW,UACX,UACA,SACA,yBAAyB,uBACzB,cAAc,KACd,QACA,eAAe,mBACf,OACA,QACA,MACA,GAAG,WAEP,sBAAsB,eAAe;CAEzC,MAAM,CAAC,gBAAgB,qBAAqB,SAAS,MAAM,OAAO,UAAU,CAAC;CAE7E,gBAAgB;EACZ,IAAI,MAAM,UAAU,KAAA,GAChB,kBAAkB,OAAO,MAAM,KAAK,CAAC,CAAC,MAAM;CAEpD,GAAG,CAAC,MAAM,KAAK,CAAC;CAEhB,MAAM,iBAAiB,aAClB,MAAqC;EAClC,kBAAkB,OAAQ,EAAE,OAA4B,KAAK,CAAC,CAAC,MAAM;EACrE,WAAW,CAAC;CAChB,GACA,CAAC,QAAQ,CACb;CAEA,MAAM,qBAAqB,iBAAiB,iBAAiB,iBAAiB;CAC9E,MAAM,WAAW,QAAQ,eAAe,QAAQ,KAAK;CAErD,MAAM,gBAAgB,qBAAqB,QAAQ,OAAO;CAE1D,MAAM,CAAC,WAAW,gBAAgB,SAAS,QAAQ,MAAM;CAEzD,MAAM,uBAA6B;EAC/B,IAAI,CAAC,UACD,aAAa,cAAc,aAAa,SAAS,UAAU;CAEnE;CAEA,MAAM,YAAY,iBAAiB,QAAQ,OAAO;CAClD,MAAM,qBAAqB,SAAS;CAEpC,OACI,oBAAC,WAAD;EACI,GAAI;EACM;EACV,WAAW,qBAAqB,yBAAyB,eAAe;EAC9D;EACV,cAAc,QAAQ,eAAe,MAAM,KAAK,iBAAiB;EACjE,UACI,eAAe,SAAS,KAAA,IACpB,qBAAC,WAAD;GACI,WAAU;GACV,OAAO,eAAe,WAAW,aAAa,qBAAqB,aAAa;aAFpF;IAIK;IAAe;IAAE;GACX;;YAInB,qBAAC,OAAD;GACI,WAAW,GACP,sMACA;IACI,qEAAqE;IACrE,0BAA0B;IAC1B,YAAY;IACZ,YAAY,CAAC;GACjB,CACJ;aATJ;IAWK,QAAQ,MAAM,KAAK,oBAAC,aAAD;KAAa,OAAO,WAAW,aAAa;eAAc;IAAoB,CAAA;IAClG,oBAAC,SAAD;KACI,WAAW,GACP,+MACA,EACI,qDAAqD,aAAa,OACtE,CACJ;KACA,GAAI;KACM;KACV,UAAU;KACG;KACR;KACL,MAAM;IACT,CAAA;IACA,QAAQ,MAAM,KAAK,oBAAC,aAAD;KAAa,OAAO,WAAW,aAAa;eAAc;IAAoB,CAAA;IACjG,aACG,oBAAC,YAAD;KACI,cAAY;KACZ,WAAW,GACP,8EACA,QAAQ,cAAc,WAC1B;KACU;KACV,aAAA;KACA,MAAM;KACN,SAAS;KACT,MAAK;KACL,OAAM;KACN,MAAK;KACL,MAAK;IACR,CAAA;IAEJ,uBACI,cAAc,aACX,oBAAC,UAAD;KACI,OAAO,WAAW,aAAa;KAE/B,SAAS;KACT,MAAK;KACL,MAAK;IACR,CAAA,IAED,oBAAC,KAAD;KACI,OAAO,WAAW,aAAa;KAE/B,SAAS;KACT,MAAK;KACL,MAAK;IACR,CAAA;GAER;;CACE,CAAA;AAEnB,CAAC;AAED,UAAU,cAAc"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JumpMenu-BXYZnPIH.cjs","names":["isClientSide","useIntersectionObserver","useHorizontalScrollFadeMixin","isClientSide","useWindowIsMobile","Caption"],"sources":["../src/JumpMenu/components/JumpMenuItem/JumpMenuItem.tsx","../src/JumpMenu/hooks/useContainerTracking.ts","../src/JumpMenu/hooks/useKeyboardNavigation.ts","../src/JumpMenu/hooks/useSectionObserver.ts","../src/JumpMenu/JumpMenu.tsx"],"sourcesContent":["import { cn } from '@evolve.ui/theme-system';\n\nimport { JumpMenuItem as JumpMenuItemData } from '../../types';\n\n/**\n * Single jump menu item configuration with click handler\n */\nexport type Props<T extends JumpMenuItemData = JumpMenuItemData> = T & {\n /** Unique identifier for the menu item */\n activeItemId: string | null;\n /** Additional CSS classes for the individual item */\n className?: string;\n /** Handle item click function */\n handleItemClick: (item: T, event: React.MouseEvent) => void;\n};\n\nexport const JumpMenuItem = ({\n activeItemId,\n className,\n handleItemClick,\n href,\n id,\n isActive: isActiveProp,\n label,\n target,\n ...restProps\n}: Props) => {\n const isActive = isActiveProp || id === activeItemId;\n\n return (\n <button\n aria-current={isActive ? 'page' : undefined}\n className={cn(\n // Base styles - use consistent typography class for both states\n 'hui-text-body-secondary relative whitespace-nowrap rounded-md px-xs py-2xs last:mr-sm [&:not(:last-child)]:mr-xs',\n // Default state (Selected=Off, Hover=Off, Focused=Off)\n 'text-text-secondary',\n // Focus styles - inset focus ring to stay within button dimensions\n 'focus-visible:shadow-[inset_0_0_0_4px_rgb(var(--color-focus-primary)/0.50)] focus-visible:outline-none',\n // Interactive states\n 'cursor-pointer',\n // Hover states - completely remove hover on touch devices, only show on pointer devices\n '[&:hover]:[@media(pointer:fine)]:bg-background-hover',\n !isActive && '[&:hover]:[@media(pointer:fine)]:text-text-primary',\n // Touch/Active states for mobile - brief visual feedback on tap\n !isActive && 'active:bg-background-hover active:text-text-primary active:duration-75',\n // Selected states (Selected=On) - using ring for border without affecting dimensions\n isActive && 'bg-background-primary font-bold text-text-primary ring-1 ring-inset ring-border-selected',\n className\n )}\n data-item-id={id}\n data-target={target || href?.replace('#', '')}\n key={id}\n onClick={(event) => {\n handleItemClick({ href, id, isActive: isActiveProp, label, target, ...restProps }, event);\n }}\n type=\"button\"\n >\n {label}\n </button>\n );\n};\n","import { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { isClientSide } from '../../utils/isClientSide';\n\n/**\n * Configuration for container tracking\n */\ntype ContainerTrackingConfig = {\n /** CSS selector for the container element */\n containerSelector?: string;\n /** Reference to the menu element for height tracking */\n menuRef?: React.RefObject<HTMLElement | null>;\n};\n\n/**\n * Return type for the container tracking hook\n */\ntype UseContainerTrackingReturn = {\n /** Whether the container is currently visible */\n isContainerVisible: boolean;\n /** Current height of the menu element in pixels */\n menuHeight: number;\n /** Current width of the container in pixels */\n menuWidth: number;\n};\n\n/**\n * Hook that tracks container visibility and dimensions for fixed positioning behavior\n *\n * This hook provides:\n * - Simple container visibility tracking via scroll detection\n * - Container width tracking for maintaining consistent sizing when position changes\n * - Menu height tracking for placeholder div\n * - Automatic event listener management with cleanup\n * - SSR safety by checking client-side rendering\n *\n * @param config - Configuration object for container tracking\n * @returns Object containing visibility state and dimensions\n *\n * @example\n * ```tsx\n * const { isContainerVisible, menuWidth, menuHeight } = useContainerTracking({\n * containerSelector: '#main-container',\n * menuRef\n * });\n * ```\n */\nexport const useContainerTracking = ({\n containerSelector,\n menuRef,\n}: ContainerTrackingConfig): UseContainerTrackingReturn => {\n const [isContainerVisible, setIsContainerVisible] = useState<boolean>(true);\n const [menuWidth, setMenuWidth] = useState<number>(0);\n const [menuHeight, setMenuHeight] = useState<number>(0);\n const resizeObserverRef = useRef<ResizeObserver | null>(null);\n\n /**\n * Sets up simple scroll-based visibility tracking for the container\n * Menu becomes fixed when container top reaches viewport top and stays fixed until container scrolls past\n */\n useEffect(() => {\n // We don't want to run this in SSR since we are using the window object\n if (!isClientSide) {\n return;\n }\n\n if (!containerSelector) {\n setIsContainerVisible(true);\n return;\n }\n\n const containerElement = document.querySelector(containerSelector);\n if (!containerElement) {\n setIsContainerVisible(true);\n return;\n }\n\n const onScroll = () => {\n const rect = containerElement.getBoundingClientRect();\n\n // Menu should be visible when:\n // 1. Before we scroll to container (rect.top > 0) - show in relative position\n // 2. While scrolling through container (rect.top <= 0 && rect.bottom > 0) - show in fixed position\n // 3. After scrolling past container (rect.bottom <= 0) - hide completely\n const shouldBeVisible = rect.bottom > 0; // Visible as long as container hasn't scrolled completely past\n\n setIsContainerVisible(shouldBeVisible);\n };\n\n // Initial check\n onScroll();\n\n // Listen to scroll events with passive flag for performance\n window.addEventListener('scroll', onScroll, { passive: true });\n\n return () => {\n window.removeEventListener('scroll', onScroll);\n };\n }, [containerSelector]);\n\n /**\n * Updates menu width and height for fixed positioning\n * This ensures the fixed menu maintains the same dimensions as its static position\n * Uses ResizeObserver for precise, real-time dimension tracking\n */\n useEffect(() => {\n // We don't want to run this in SSR since we are using the window object\n if (!isClientSide) {\n return;\n }\n\n const updateContainerWidth = () => {\n if (containerSelector) {\n const containerElement = document.querySelector(containerSelector);\n if (containerElement) {\n const containerRect = containerElement.getBoundingClientRect();\n setMenuWidth(containerRect.width);\n }\n }\n };\n\n const updateMenuHeight = () => {\n if (menuRef?.current) {\n // Use offsetHeight for more accurate measurement including borders and padding\n const height = menuRef.current.offsetHeight;\n if (height > 0) {\n setMenuHeight(height);\n }\n }\n };\n\n // Set up ResizeObserver for menu element height tracking\n if (menuRef?.current && 'ResizeObserver' in window) {\n resizeObserverRef.current = new ResizeObserver((entries) => {\n for (const entry of entries) {\n // Use contentRect height for reliable measurement\n const height = entry.contentRect.height;\n if (height > 0) {\n setMenuHeight(height);\n }\n }\n });\n\n resizeObserverRef.current.observe(menuRef.current);\n }\n\n // Initial measurements\n updateContainerWidth();\n updateMenuHeight();\n\n // Listen for window resize for container width updates\n window.addEventListener('resize', updateContainerWidth);\n\n return () => {\n window.removeEventListener('resize', updateContainerWidth);\n if (resizeObserverRef.current) {\n resizeObserverRef.current.disconnect();\n resizeObserverRef.current = null;\n }\n };\n }, [containerSelector, menuRef]);\n\n return useMemo(\n () => ({\n isContainerVisible,\n menuHeight,\n menuWidth,\n }),\n [isContainerVisible, menuHeight, menuWidth]\n );\n};\n","import { KeyboardEvent, RefObject, useCallback } from 'react';\n\n/**\n * Props for the keyboard navigation hook\n */\ntype UseKeyboardNavigationProps<T> = {\n /** Current active item ID */\n activeItemId: string | null;\n /** Ref to the container element for focusing items */\n containerRef: RefObject<HTMLElement | null>;\n /** Function to extract ID from an item */\n getItemId: (item: T) => string;\n /** Array of items to navigate through */\n items: T[];\n /** Ref to track user-initiated actions */\n lastUserActionRef: RefObject<number>;\n /** Callback when active item changes */\n onActiveChange?: (itemId: string) => void;\n /** Function to scroll item into view */\n scrollItemIntoView: (itemId: string) => void;\n};\n\n/**\n * Return type for the keyboard navigation hook\n */\ntype UseKeyboardNavigationReturn = {\n /** Keyboard event handler for the navigation container */\n handleKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;\n};\n\n/**\n * Custom hook for handling keyboard navigation in horizontal menu components\n *\n * Supports Arrow Left/Right, Home, and End keys for navigation with wrapping\n * and automatic focus management.\n */\nexport const useKeyboardNavigation = <T>({\n activeItemId,\n containerRef,\n getItemId,\n items,\n lastUserActionRef,\n onActiveChange,\n scrollItemIntoView,\n}: UseKeyboardNavigationProps<T>): UseKeyboardNavigationReturn => {\n /**\n * Handles keyboard navigation within the menu\n * Supports Arrow Left/Right, Home, and End keys for navigation\n */\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>) => {\n if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {\n return;\n }\n\n event.preventDefault();\n\n const currentEnabledIndex = items.findIndex((item) => getItemId(item) === activeItemId);\n\n let nextIndex: number;\n\n switch (event.key) {\n case 'ArrowLeft':\n nextIndex = currentEnabledIndex > 0 ? currentEnabledIndex - 1 : items.length - 1;\n break;\n case 'ArrowRight':\n nextIndex = currentEnabledIndex < items.length - 1 ? currentEnabledIndex + 1 : 0;\n break;\n case 'Home':\n nextIndex = 0;\n break;\n case 'End':\n nextIndex = items.length - 1;\n break;\n default:\n return;\n }\n\n const nextItem = items[nextIndex];\n const nextItemId = getItemId(nextItem);\n\n // Mark this as a user-initiated action\n lastUserActionRef.current = Date.now();\n\n onActiveChange?.(nextItemId);\n\n // Scroll the selected item into view\n scrollItemIntoView(nextItemId);\n\n // Focus the next item\n const nextElement = containerRef.current?.querySelector(\n `[data-item-id=\"${nextItemId}\"]`\n ) as HTMLElement | null;\n nextElement?.focus();\n },\n [activeItemId, items, getItemId, onActiveChange, scrollItemIntoView, containerRef, lastUserActionRef]\n );\n\n return {\n handleKeyDown,\n };\n};\n","import { useEffect, useMemo, useRef } from 'react';\n\nimport { useIntersectionObserver } from './useIntersectionObserver';\n\n/**\n * Configuration for section observation\n */\ntype SectionConfig = {\n /** The ID of the section */\n id: string;\n /** The selector or href for the section element */\n selector: string;\n};\n\n/**\n * Hook that observes multiple sections and reports which one is most visible\n * @param sections - Array of section configurations\n * @param onActiveChange - Callback when the active section changes\n * @param enabled - Whether section observation is enabled\n */\nexport const useSectionObserver = (\n sections: SectionConfig[],\n onActiveChange: (sectionId: string) => void,\n enabled: boolean = true\n) => {\n const activeEntriesRef = useRef<Map<string, IntersectionObserverEntry>>(new Map());\n\n // Memoize intersection observer options for better performance\n const observerOptions = useMemo(\n () => ({\n rootMargin: '-10% 0px -10% 0px', // Trigger when section is 10% visible from top/bottom\n threshold: [0, 0.1, 0.25, 0.5, 0.75, 1], // Multiple thresholds for better accuracy\n }),\n []\n );\n\n const handleIntersection = (entries: IntersectionObserverEntry[]) => {\n // Update our map of active entries\n entries.forEach((entry) => {\n const sectionId = sections.find(\n (s) => entry.target.matches(s.selector) || entry.target.id === s.selector.replace('#', '')\n )?.id;\n\n if (sectionId) {\n if (entry.isIntersecting) {\n activeEntriesRef.current.set(sectionId, entry);\n } else {\n activeEntriesRef.current.delete(sectionId);\n }\n }\n });\n\n // Find the topmost visible section (prioritize by vertical position, not intersection ratio)\n if (activeEntriesRef.current.size > 0) {\n let topmostSection = '';\n let smallestTop = Infinity;\n\n activeEntriesRef.current.forEach((entry, sectionId) => {\n // Use boundingClientRect.top to find which section is highest on screen\n const topPosition = entry.boundingClientRect.top;\n\n // Prioritize sections that are actually in the viewport (positive intersection ratio)\n if (entry.intersectionRatio > 0 && topPosition < smallestTop) {\n smallestTop = topPosition;\n topmostSection = sectionId;\n }\n });\n\n if (topmostSection) {\n onActiveChange(topmostSection);\n }\n }\n };\n\n const { disconnect, isSupported, observe } = useIntersectionObserver(handleIntersection, observerOptions);\n\n useEffect(() => {\n if (!enabled || !isSupported) {\n return;\n }\n\n // Clear previous observations\n activeEntriesRef.current.clear();\n disconnect();\n\n // Observe all section elements\n sections.forEach(({ selector }) => {\n const element = document.querySelector(selector);\n if (element) {\n observe(element);\n }\n });\n\n return () => {\n disconnect();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n activeEntriesRef.current.clear();\n };\n }, [sections, enabled, isSupported, observe, disconnect]);\n\n return { isSupported };\n};\n","import { cn } from '@evolve.ui/theme-system';\nimport { HTMLProps, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { useHorizontalScrollFadeMixin } from '../mixins/horizontalScrollFadeMixin';\nimport { useWindowIsMobile } from '../providers/windowSizeProvider';\nimport { Caption } from '../Typography/Text/Text';\nimport { isClientSide } from '../utils/isClientSide';\nimport { JumpMenuItem } from './components/JumpMenuItem/JumpMenuItem';\nimport { useContainerTracking } from './hooks/useContainerTracking';\nimport { useKeyboardNavigation } from './hooks/useKeyboardNavigation';\nimport { useSectionObserver } from './hooks/useSectionObserver';\nimport { JumpMenuItem as JumpMenuItemType } from './types';\n\n/**\n * Props for the JumpMenu component\n */\ntype Props = HTMLProps<HTMLElement> & {\n /** Custom aria-label for the navigation (defaults to 'Jump to section') */\n ariaLabel?: string;\n /** Additional CSS classes for the menu container */\n className?: string;\n /** CSS selector for the container element that defines the scope of fixed positioning behavior */\n containerSelector?: string;\n /** Array of menu items to display in the jump navigation */\n items: JumpMenuItemType[];\n /** Callback when active section changes via scroll or user interaction */\n onActiveChange?: (activeId: string | null) => void;\n /** Additional CSS classes for the outer container of the menu */\n outerContainerClassName?: string;\n /** Scroll offset in pixels to account for sticky headers when navigating to sections (defaults to 0) */\n scrollOffset?: number;\n /** Scroll offset in pixels for mobile viewports (overrides scrollOffset on mobile, defaults to scrollOffset value) */\n scrollOffsetMobile?: number;\n};\n\n/**\n * JumpMenu component for horizontal scrollable navigation between page sections\n *\n * Features:\n * - Horizontal scrolling with fade indicators on overflow\n * - Automatic section highlighting based on scroll position\n * - Keyboard navigation (Arrow keys, Home, End)\n * - Smooth scrolling to target sections\n * - Accessibility compliant with ARIA labels and roles\n * - Fixed positioning with container visibility tracking\n *\n * @param ariaLabel - Custom aria-label for the navigation (defaults to 'Jump to section')\n * @param className - Additional CSS classes for the menu container\n * @param containerSelector - CSS selector for the container element that defines the scope of fixed positioning behavior\n * @param items - Array of menu items to display - each item should have an id and either target or href\n * @param onActiveChange - Callback when active section changes via scroll or user interaction\n * @param outerContainerClassName - Additional CSS classes for the outer container of the menu\n * @param scrollOffset - Scroll offset in pixels to account for sticky headers when navigating to sections (defaults to 0)\n * @param scrollOffsetMobile - Scroll offset in pixels for mobile viewports (overrides scrollOffset on mobile, defaults to scrollOffset value)\n *\n *\n * @example\n * ```tsx\n * const menuItems = [\n * { id: 'overview', label: 'Overview', href: '#overview-section' },\n * { id: 'details', label: 'Details', href: '#details-section' },\n * ];\n *\n * <div id=\"container\">\n * <JumpMenu\n * items={menuItems}\n * containerSelector=\"#container\"\n * onActiveChange={(activeId) => console.log('Active section:', activeId)}\n * scrollOffset={80} // Optional: Account for 80px sticky header\n * scrollOffsetMobile={120} // Optional: Account for larger mobile header\n * />\n * <section id=\"overview-section\">Overview content</section>\n * <section id=\"details-section\">Details content</section>\n * </div>\n * ```\n */\nexport const JumpMenu = ({\n ariaLabel = 'Jump to section',\n className,\n containerSelector,\n items,\n onActiveChange,\n outerContainerClassName = '',\n scrollOffset = 0,\n scrollOffsetMobile,\n ...htmlProps\n}: Props) => {\n // Track user-initiated actions with timestamp to ignore subsequent intersection observer events\n const userActionIgnoreDuration = 1000;\n const lastUserActionRef = useRef<number>(0);\n\n const [activeItemId, setActiveItemId] = useState<string | null>(\n items.find((item) => item.isActive)?.id || items[0]?.id || null\n );\n const menuRef = useRef<HTMLDivElement>(null);\n const scrollContainerRef = useRef<HTMLDivElement>(null);\n const horizontalScrollFadeProps = useHorizontalScrollFadeMixin(0);\n const [navBackgroundColor, setNavBackgroundColor] = useState<string>('rgb(255, 255, 255)');\n\n // Use the container tracking hook for visibility and width management\n const { isContainerVisible, menuHeight, menuWidth } = useContainerTracking({\n containerSelector,\n menuRef,\n });\n\n // Determine if container top has passed viewport top (need to check position separately from visibility)\n const [containerTopPassed, setContainerTopPassed] = useState(false);\n\n // Track pending scroll when clicking item before menu becomes fixed\n const pendingScrollItemRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!containerSelector || !isClientSide) {\n return;\n }\n\n const containerElement = document.querySelector(containerSelector);\n if (!containerElement) {\n return;\n }\n\n const checkPosition = () => {\n const rect = containerElement.getBoundingClientRect();\n setContainerTopPassed(rect.top <= 0);\n };\n\n checkPosition();\n window.addEventListener('scroll', checkPosition, { passive: true });\n return () => window.removeEventListener('scroll', checkPosition);\n }, [containerSelector]);\n\n const isMobile = useWindowIsMobile();\n\n /**\n * Gets the appropriate scroll offset based on viewport size\n * @returns The scroll offset to use for the current viewport\n */\n const getScrollOffset = useCallback(() => {\n return isMobile && scrollOffsetMobile !== undefined ? scrollOffsetMobile : scrollOffset;\n }, [scrollOffset, scrollOffsetMobile, isMobile]);\n\n /**\n * Scrolls the specified menu item into view within the horizontal scroll container\n * Only scrolls when menu is in fixed position to prevent viewport shifts\n * @param itemId - The ID of the menu item to scroll into view\n * @param inline - Scroll alignment: 'center' shows neighbors, 'nearest' minimal scroll\n */\n const scrollActiveItemIntoView = useCallback((itemId: string, inline: 'center' | 'nearest' = 'nearest') => {\n const activeElement = scrollContainerRef.current?.querySelector(\n `[data-item-id=\"${itemId}\"]`\n ) as HTMLElement | null;\n\n if (activeElement) {\n activeElement.scrollIntoView({\n behavior: 'smooth',\n block: 'nearest',\n inline,\n });\n }\n }, []);\n\n // When menu becomes fixed and we have a pending scroll, execute it\n useEffect(() => {\n if (containerTopPassed && pendingScrollItemRef.current) {\n scrollActiveItemIntoView(pendingScrollItemRef.current, 'center');\n pendingScrollItemRef.current = null;\n }\n }, [containerTopPassed, scrollActiveItemIntoView]);\n\n // Update active item when external state changes\n useEffect(() => {\n const externalActiveItem = items.find((item) => item.isActive);\n if (externalActiveItem && externalActiveItem.id !== activeItemId) {\n setActiveItemId(externalActiveItem.id);\n }\n }, [items, activeItemId]);\n\n /**\n * Handles active section changes from the intersection observer\n * Ignores updates that occur shortly after user interactions to prevent conflicts\n * @param sectionId - The ID of the section that became active\n */\n const handleActiveChange = useCallback(\n (sectionId: string) => {\n // Skip if this is a scroll-based update and we recently had a user action\n const timeSinceLastUserAction = Date.now() - lastUserActionRef.current;\n if (timeSinceLastUserAction < userActionIgnoreDuration) {\n return;\n }\n\n if (sectionId !== activeItemId) {\n setActiveItemId(sectionId);\n onActiveChange?.(sectionId);\n // Auto-scroll with 'center' to show neighboring items (only when fixed)\n if (containerTopPassed) {\n scrollActiveItemIntoView(sectionId, 'center');\n }\n }\n },\n [activeItemId, onActiveChange, userActionIgnoreDuration, scrollActiveItemIntoView, containerTopPassed]\n );\n\n // Prepare sections for the observer\n const sections = useMemo(\n () =>\n items\n .filter((item) => item.target || item.href)\n .map((item) => ({\n id: item.id,\n selector: item.target ? `#${item.target}` : item.href!,\n })),\n [items]\n );\n\n // Use modern IntersectionObserver for section detection\n useSectionObserver(sections, handleActiveChange);\n\n // Handle keyboard navigation with the dedicated hook\n const { handleKeyDown } = useKeyboardNavigation({\n activeItemId,\n containerRef: menuRef,\n getItemId: (item: JumpMenuItemType) => item.id,\n items,\n lastUserActionRef,\n onActiveChange: (itemId: string) => {\n setActiveItemId(itemId);\n onActiveChange?.(itemId);\n },\n scrollItemIntoView: scrollActiveItemIntoView,\n });\n\n /**\n * Handles menu item click events\n * Updates active state, calls custom onClick handler, and navigates to target section\n * @param item - The clicked menu item\n * @param event - The mouse event\n */\n const handleItemClick = useCallback(\n (item: JumpMenuItemType, event: MouseEvent) => {\n // Mark this as a user-initiated action\n lastUserActionRef.current = Date.now();\n\n // Scroll the clicked menu item into view FIRST, using 'center' to show neighbors\n // Only scroll horizontally when menu is in fixed position to prevent viewport shifts\n if (containerTopPassed) {\n scrollActiveItemIntoView(item.id, 'center');\n } else {\n // Store pending scroll to execute once menu becomes fixed\n pendingScrollItemRef.current = item.id;\n }\n\n setActiveItemId(item.id);\n onActiveChange?.(item.id);\n item.onClick?.(item.id);\n\n // Navigate to section if target or href is provided\n const targetSelector = item.target ? `#${item.target}` : item.href;\n\n if (targetSelector) {\n // Always prevent default since we're using buttons now\n event.preventDefault();\n\n // Handle navigation manually\n const targetElement = document.querySelector(targetSelector);\n if (targetElement) {\n const offset = getScrollOffset();\n\n if (offset > 0) {\n // Calculate position with offset\n const elementPosition = targetElement.getBoundingClientRect().top + window.pageYOffset;\n const offsetPosition = elementPosition - offset;\n\n window.scrollTo({\n behavior: 'smooth',\n top: offsetPosition,\n });\n } else {\n // Use default scroll behavior when no offset\n targetElement.scrollIntoView({\n behavior: 'smooth',\n block: 'start',\n });\n }\n }\n }\n },\n [onActiveChange, getScrollOffset, scrollActiveItemIntoView, containerTopPassed]\n );\n\n // Simple positioning logic - no complex state management needed\n // Fixed when we've scrolled past container top AND container hasn't scrolled past bottom\n const shouldUseFixedPosition = containerSelector && containerTopPassed && isContainerVisible;\n\n // Extract background color from nav element for dynamic gradient overlays\n useEffect(() => {\n if (menuRef.current && isClientSide) {\n const computedStyle = window.getComputedStyle(menuRef.current);\n const backgroundColor = computedStyle.backgroundColor;\n if (backgroundColor && backgroundColor !== 'rgba(0, 0, 0, 0)' && backgroundColor !== 'transparent') {\n setNavBackgroundColor(backgroundColor);\n }\n }\n }, []);\n\n return (\n <div className={cn(outerContainerClassName)}>\n {/* Simple placeholder to maintain layout when menu becomes fixed or is fading out */}\n {(shouldUseFixedPosition || !isContainerVisible) && (\n <div\n aria-hidden=\"true\"\n style={{\n height: menuHeight,\n }}\n />\n )}\n\n <nav\n aria-label={ariaLabel}\n className={cn(\n 'z-10 w-full bg-background-primary transition-opacity duration-300',\n {\n 'fixed top-0': shouldUseFixedPosition || !isContainerVisible, // Keep fixed during fade-out\n 'pointer-events-none': !isContainerVisible, // Disable interaction when hidden\n relative: !shouldUseFixedPosition && isContainerVisible,\n },\n className\n )}\n ref={menuRef}\n style={{\n opacity: isContainerVisible ? 1 : 0,\n width: shouldUseFixedPosition || !isContainerVisible ? `${menuWidth}px` : undefined,\n ...htmlProps.style,\n }}\n {...htmlProps}\n >\n <div className=\"flex w-full flex-col gap-xs py-xs\">\n <Caption\n // z-[11] is to prevent the caption from being hidden by fade\n className=\"z-[11] w-full px-xs\"\n color=\"secondary\"\n >\n {ariaLabel}\n </Caption>\n {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- Keyboard events bubble from focusable menu items */}\n <div\n className=\"scrollbar-hide flex w-full overflow-x-auto px-xs py-3xs\"\n onKeyDown={handleKeyDown}\n ref={(element) => {\n horizontalScrollFadeProps.ref(element);\n scrollContainerRef.current = element;\n menuRef.current = element;\n }}\n >\n {/* Left fade overlay */}\n <div\n className={cn(\n 'pointer-events-none absolute bottom-0 left-0 top-0 z-10 w-[48px] transition-opacity duration-300 ease-in-out',\n {\n 'opacity-0': !horizontalScrollFadeProps.shouldShowLeftFade,\n 'opacity-100': horizontalScrollFadeProps.shouldShowLeftFade,\n }\n )}\n style={{\n background: `linear-gradient(to right, ${navBackgroundColor}, transparent)`,\n }}\n />\n\n {/* Right fade overlay */}\n <div\n className={cn(\n 'pointer-events-none absolute bottom-0 right-0 top-0 z-10 w-[48px] transition-opacity duration-300 ease-in-out',\n {\n 'opacity-0': !horizontalScrollFadeProps.shouldShowRightFade,\n 'opacity-100': horizontalScrollFadeProps.shouldShowRightFade,\n }\n )}\n style={{\n background: `linear-gradient(to left, ${navBackgroundColor}, transparent)`,\n }}\n />\n\n {items.map((item) => (\n <JumpMenuItem\n {...item}\n activeItemId={activeItemId}\n handleItemClick={handleItemClick}\n key={item.id}\n />\n ))}\n </div>\n </div>\n </nav>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"JumpMenu-BXYZnPIH.cjs","names":["isClientSide","useIntersectionObserver","useHorizontalScrollFadeMixin","isClientSide","useWindowIsMobile","Caption"],"sources":["../src/JumpMenu/components/JumpMenuItem/JumpMenuItem.tsx","../src/JumpMenu/hooks/useContainerTracking.ts","../src/JumpMenu/hooks/useKeyboardNavigation.ts","../src/JumpMenu/hooks/useSectionObserver.ts","../src/JumpMenu/JumpMenu.tsx"],"sourcesContent":["import { cn } from '@evolve.ui/theme-system';\n\nimport { JumpMenuItem as JumpMenuItemData } from '../../types';\n\n/**\n * Single jump menu item configuration with click handler\n */\nexport type Props<T extends JumpMenuItemData = JumpMenuItemData> = T & {\n /** Unique identifier for the menu item */\n activeItemId: string | null;\n /** Additional CSS classes for the individual item */\n className?: string;\n /** Handle item click function */\n handleItemClick: (item: T, event: React.MouseEvent) => void;\n};\n\nexport const JumpMenuItem = ({\n activeItemId,\n className,\n handleItemClick,\n href,\n id,\n isActive: isActiveProp,\n label,\n target,\n ...restProps\n}: Props) => {\n const isActive = isActiveProp || id === activeItemId;\n\n return (\n <button\n aria-current={isActive ? 'page' : undefined}\n className={cn(\n // Base styles - use consistent typography class for both states\n 'hui-text-body-secondary relative whitespace-nowrap rounded-md px-xs py-2xs last:mr-sm [&:not(:last-child)]:mr-xs',\n // Default state (Selected=Off, Hover=Off, Focused=Off)\n 'text-text-secondary',\n // Focus styles - inset focus ring to stay within button dimensions.\n // Permitted clause-2 deviation (DS-98): hui-focus-visible-outline is outline-based and\n // cannot express an inset ring. The outline-none is focus-visible-scoped, so it\n // suppresses only alongside this replacement indicator.\n 'focus-visible:shadow-[inset_0_0_0_4px_rgb(var(--color-focus-primary)/0.50)] focus-visible:outline-none',\n // Interactive states\n 'cursor-pointer',\n // Hover states - completely remove hover on touch devices, only show on pointer devices\n '[&:hover]:[@media(pointer:fine)]:bg-background-hover',\n !isActive && '[&:hover]:[@media(pointer:fine)]:text-text-primary',\n // Touch/Active states for mobile - brief visual feedback on tap\n !isActive && 'active:bg-background-hover active:text-text-primary active:duration-75',\n // Selected states (Selected=On) - using ring for border without affecting dimensions\n isActive && 'bg-background-primary font-bold text-text-primary ring-1 ring-inset ring-border-selected',\n className\n )}\n data-item-id={id}\n data-target={target || href?.replace('#', '')}\n key={id}\n onClick={(event) => {\n handleItemClick({ href, id, isActive: isActiveProp, label, target, ...restProps }, event);\n }}\n type=\"button\"\n >\n {label}\n </button>\n );\n};\n","import { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { isClientSide } from '../../utils/isClientSide';\n\n/**\n * Configuration for container tracking\n */\ntype ContainerTrackingConfig = {\n /** CSS selector for the container element */\n containerSelector?: string;\n /** Reference to the menu element for height tracking */\n menuRef?: React.RefObject<HTMLElement | null>;\n};\n\n/**\n * Return type for the container tracking hook\n */\ntype UseContainerTrackingReturn = {\n /** Whether the container is currently visible */\n isContainerVisible: boolean;\n /** Current height of the menu element in pixels */\n menuHeight: number;\n /** Current width of the container in pixels */\n menuWidth: number;\n};\n\n/**\n * Hook that tracks container visibility and dimensions for fixed positioning behavior\n *\n * This hook provides:\n * - Simple container visibility tracking via scroll detection\n * - Container width tracking for maintaining consistent sizing when position changes\n * - Menu height tracking for placeholder div\n * - Automatic event listener management with cleanup\n * - SSR safety by checking client-side rendering\n *\n * @param config - Configuration object for container tracking\n * @returns Object containing visibility state and dimensions\n *\n * @example\n * ```tsx\n * const { isContainerVisible, menuWidth, menuHeight } = useContainerTracking({\n * containerSelector: '#main-container',\n * menuRef\n * });\n * ```\n */\nexport const useContainerTracking = ({\n containerSelector,\n menuRef,\n}: ContainerTrackingConfig): UseContainerTrackingReturn => {\n const [isContainerVisible, setIsContainerVisible] = useState<boolean>(true);\n const [menuWidth, setMenuWidth] = useState<number>(0);\n const [menuHeight, setMenuHeight] = useState<number>(0);\n const resizeObserverRef = useRef<ResizeObserver | null>(null);\n\n /**\n * Sets up simple scroll-based visibility tracking for the container\n * Menu becomes fixed when container top reaches viewport top and stays fixed until container scrolls past\n */\n useEffect(() => {\n // We don't want to run this in SSR since we are using the window object\n if (!isClientSide) {\n return;\n }\n\n if (!containerSelector) {\n setIsContainerVisible(true);\n return;\n }\n\n const containerElement = document.querySelector(containerSelector);\n if (!containerElement) {\n setIsContainerVisible(true);\n return;\n }\n\n const onScroll = () => {\n const rect = containerElement.getBoundingClientRect();\n\n // Menu should be visible when:\n // 1. Before we scroll to container (rect.top > 0) - show in relative position\n // 2. While scrolling through container (rect.top <= 0 && rect.bottom > 0) - show in fixed position\n // 3. After scrolling past container (rect.bottom <= 0) - hide completely\n const shouldBeVisible = rect.bottom > 0; // Visible as long as container hasn't scrolled completely past\n\n setIsContainerVisible(shouldBeVisible);\n };\n\n // Initial check\n onScroll();\n\n // Listen to scroll events with passive flag for performance\n window.addEventListener('scroll', onScroll, { passive: true });\n\n return () => {\n window.removeEventListener('scroll', onScroll);\n };\n }, [containerSelector]);\n\n /**\n * Updates menu width and height for fixed positioning\n * This ensures the fixed menu maintains the same dimensions as its static position\n * Uses ResizeObserver for precise, real-time dimension tracking\n */\n useEffect(() => {\n // We don't want to run this in SSR since we are using the window object\n if (!isClientSide) {\n return;\n }\n\n const updateContainerWidth = () => {\n if (containerSelector) {\n const containerElement = document.querySelector(containerSelector);\n if (containerElement) {\n const containerRect = containerElement.getBoundingClientRect();\n setMenuWidth(containerRect.width);\n }\n }\n };\n\n const updateMenuHeight = () => {\n if (menuRef?.current) {\n // Use offsetHeight for more accurate measurement including borders and padding\n const height = menuRef.current.offsetHeight;\n if (height > 0) {\n setMenuHeight(height);\n }\n }\n };\n\n // Set up ResizeObserver for menu element height tracking\n if (menuRef?.current && 'ResizeObserver' in window) {\n resizeObserverRef.current = new ResizeObserver((entries) => {\n for (const entry of entries) {\n // Use contentRect height for reliable measurement\n const height = entry.contentRect.height;\n if (height > 0) {\n setMenuHeight(height);\n }\n }\n });\n\n resizeObserverRef.current.observe(menuRef.current);\n }\n\n // Initial measurements\n updateContainerWidth();\n updateMenuHeight();\n\n // Listen for window resize for container width updates\n window.addEventListener('resize', updateContainerWidth);\n\n return () => {\n window.removeEventListener('resize', updateContainerWidth);\n if (resizeObserverRef.current) {\n resizeObserverRef.current.disconnect();\n resizeObserverRef.current = null;\n }\n };\n }, [containerSelector, menuRef]);\n\n return useMemo(\n () => ({\n isContainerVisible,\n menuHeight,\n menuWidth,\n }),\n [isContainerVisible, menuHeight, menuWidth]\n );\n};\n","import { KeyboardEvent, RefObject, useCallback } from 'react';\n\n/**\n * Props for the keyboard navigation hook\n */\ntype UseKeyboardNavigationProps<T> = {\n /** Current active item ID */\n activeItemId: string | null;\n /** Ref to the container element for focusing items */\n containerRef: RefObject<HTMLElement | null>;\n /** Function to extract ID from an item */\n getItemId: (item: T) => string;\n /** Array of items to navigate through */\n items: T[];\n /** Ref to track user-initiated actions */\n lastUserActionRef: RefObject<number>;\n /** Callback when active item changes */\n onActiveChange?: (itemId: string) => void;\n /** Function to scroll item into view */\n scrollItemIntoView: (itemId: string) => void;\n};\n\n/**\n * Return type for the keyboard navigation hook\n */\ntype UseKeyboardNavigationReturn = {\n /** Keyboard event handler for the navigation container */\n handleKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;\n};\n\n/**\n * Custom hook for handling keyboard navigation in horizontal menu components\n *\n * Supports Arrow Left/Right, Home, and End keys for navigation with wrapping\n * and automatic focus management.\n */\nexport const useKeyboardNavigation = <T>({\n activeItemId,\n containerRef,\n getItemId,\n items,\n lastUserActionRef,\n onActiveChange,\n scrollItemIntoView,\n}: UseKeyboardNavigationProps<T>): UseKeyboardNavigationReturn => {\n /**\n * Handles keyboard navigation within the menu\n * Supports Arrow Left/Right, Home, and End keys for navigation\n */\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>) => {\n if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {\n return;\n }\n\n event.preventDefault();\n\n const currentEnabledIndex = items.findIndex((item) => getItemId(item) === activeItemId);\n\n let nextIndex: number;\n\n switch (event.key) {\n case 'ArrowLeft':\n nextIndex = currentEnabledIndex > 0 ? currentEnabledIndex - 1 : items.length - 1;\n break;\n case 'ArrowRight':\n nextIndex = currentEnabledIndex < items.length - 1 ? currentEnabledIndex + 1 : 0;\n break;\n case 'Home':\n nextIndex = 0;\n break;\n case 'End':\n nextIndex = items.length - 1;\n break;\n default:\n return;\n }\n\n const nextItem = items[nextIndex];\n const nextItemId = getItemId(nextItem);\n\n // Mark this as a user-initiated action\n lastUserActionRef.current = Date.now();\n\n onActiveChange?.(nextItemId);\n\n // Scroll the selected item into view\n scrollItemIntoView(nextItemId);\n\n // Focus the next item\n const nextElement = containerRef.current?.querySelector(\n `[data-item-id=\"${nextItemId}\"]`\n ) as HTMLElement | null;\n nextElement?.focus();\n },\n [activeItemId, items, getItemId, onActiveChange, scrollItemIntoView, containerRef, lastUserActionRef]\n );\n\n return {\n handleKeyDown,\n };\n};\n","import { useEffect, useMemo, useRef } from 'react';\n\nimport { useIntersectionObserver } from './useIntersectionObserver';\n\n/**\n * Configuration for section observation\n */\ntype SectionConfig = {\n /** The ID of the section */\n id: string;\n /** The selector or href for the section element */\n selector: string;\n};\n\n/**\n * Hook that observes multiple sections and reports which one is most visible\n * @param sections - Array of section configurations\n * @param onActiveChange - Callback when the active section changes\n * @param enabled - Whether section observation is enabled\n */\nexport const useSectionObserver = (\n sections: SectionConfig[],\n onActiveChange: (sectionId: string) => void,\n enabled: boolean = true\n) => {\n const activeEntriesRef = useRef<Map<string, IntersectionObserverEntry>>(new Map());\n\n // Memoize intersection observer options for better performance\n const observerOptions = useMemo(\n () => ({\n rootMargin: '-10% 0px -10% 0px', // Trigger when section is 10% visible from top/bottom\n threshold: [0, 0.1, 0.25, 0.5, 0.75, 1], // Multiple thresholds for better accuracy\n }),\n []\n );\n\n const handleIntersection = (entries: IntersectionObserverEntry[]) => {\n // Update our map of active entries\n entries.forEach((entry) => {\n const sectionId = sections.find(\n (s) => entry.target.matches(s.selector) || entry.target.id === s.selector.replace('#', '')\n )?.id;\n\n if (sectionId) {\n if (entry.isIntersecting) {\n activeEntriesRef.current.set(sectionId, entry);\n } else {\n activeEntriesRef.current.delete(sectionId);\n }\n }\n });\n\n // Find the topmost visible section (prioritize by vertical position, not intersection ratio)\n if (activeEntriesRef.current.size > 0) {\n let topmostSection = '';\n let smallestTop = Infinity;\n\n activeEntriesRef.current.forEach((entry, sectionId) => {\n // Use boundingClientRect.top to find which section is highest on screen\n const topPosition = entry.boundingClientRect.top;\n\n // Prioritize sections that are actually in the viewport (positive intersection ratio)\n if (entry.intersectionRatio > 0 && topPosition < smallestTop) {\n smallestTop = topPosition;\n topmostSection = sectionId;\n }\n });\n\n if (topmostSection) {\n onActiveChange(topmostSection);\n }\n }\n };\n\n const { disconnect, isSupported, observe } = useIntersectionObserver(handleIntersection, observerOptions);\n\n useEffect(() => {\n if (!enabled || !isSupported) {\n return;\n }\n\n // Clear previous observations\n activeEntriesRef.current.clear();\n disconnect();\n\n // Observe all section elements\n sections.forEach(({ selector }) => {\n const element = document.querySelector(selector);\n if (element) {\n observe(element);\n }\n });\n\n return () => {\n disconnect();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n activeEntriesRef.current.clear();\n };\n }, [sections, enabled, isSupported, observe, disconnect]);\n\n return { isSupported };\n};\n","import { cn } from '@evolve.ui/theme-system';\nimport { HTMLProps, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { useHorizontalScrollFadeMixin } from '../mixins/horizontalScrollFadeMixin';\nimport { useWindowIsMobile } from '../providers/windowSizeProvider';\nimport { Caption } from '../Typography/Text/Text';\nimport { isClientSide } from '../utils/isClientSide';\nimport { JumpMenuItem } from './components/JumpMenuItem/JumpMenuItem';\nimport { useContainerTracking } from './hooks/useContainerTracking';\nimport { useKeyboardNavigation } from './hooks/useKeyboardNavigation';\nimport { useSectionObserver } from './hooks/useSectionObserver';\nimport { JumpMenuItem as JumpMenuItemType } from './types';\n\n/**\n * Props for the JumpMenu component\n */\ntype Props = HTMLProps<HTMLElement> & {\n /** Custom aria-label for the navigation (defaults to 'Jump to section') */\n ariaLabel?: string;\n /** Additional CSS classes for the menu container */\n className?: string;\n /** CSS selector for the container element that defines the scope of fixed positioning behavior */\n containerSelector?: string;\n /** Array of menu items to display in the jump navigation */\n items: JumpMenuItemType[];\n /** Callback when active section changes via scroll or user interaction */\n onActiveChange?: (activeId: string | null) => void;\n /** Additional CSS classes for the outer container of the menu */\n outerContainerClassName?: string;\n /** Scroll offset in pixels to account for sticky headers when navigating to sections (defaults to 0) */\n scrollOffset?: number;\n /** Scroll offset in pixels for mobile viewports (overrides scrollOffset on mobile, defaults to scrollOffset value) */\n scrollOffsetMobile?: number;\n};\n\n/**\n * JumpMenu component for horizontal scrollable navigation between page sections\n *\n * Features:\n * - Horizontal scrolling with fade indicators on overflow\n * - Automatic section highlighting based on scroll position\n * - Keyboard navigation (Arrow keys, Home, End)\n * - Smooth scrolling to target sections\n * - Accessibility compliant with ARIA labels and roles\n * - Fixed positioning with container visibility tracking\n *\n * @param ariaLabel - Custom aria-label for the navigation (defaults to 'Jump to section')\n * @param className - Additional CSS classes for the menu container\n * @param containerSelector - CSS selector for the container element that defines the scope of fixed positioning behavior\n * @param items - Array of menu items to display - each item should have an id and either target or href\n * @param onActiveChange - Callback when active section changes via scroll or user interaction\n * @param outerContainerClassName - Additional CSS classes for the outer container of the menu\n * @param scrollOffset - Scroll offset in pixels to account for sticky headers when navigating to sections (defaults to 0)\n * @param scrollOffsetMobile - Scroll offset in pixels for mobile viewports (overrides scrollOffset on mobile, defaults to scrollOffset value)\n *\n *\n * @example\n * ```tsx\n * const menuItems = [\n * { id: 'overview', label: 'Overview', href: '#overview-section' },\n * { id: 'details', label: 'Details', href: '#details-section' },\n * ];\n *\n * <div id=\"container\">\n * <JumpMenu\n * items={menuItems}\n * containerSelector=\"#container\"\n * onActiveChange={(activeId) => console.log('Active section:', activeId)}\n * scrollOffset={80} // Optional: Account for 80px sticky header\n * scrollOffsetMobile={120} // Optional: Account for larger mobile header\n * />\n * <section id=\"overview-section\">Overview content</section>\n * <section id=\"details-section\">Details content</section>\n * </div>\n * ```\n */\nexport const JumpMenu = ({\n ariaLabel = 'Jump to section',\n className,\n containerSelector,\n items,\n onActiveChange,\n outerContainerClassName = '',\n scrollOffset = 0,\n scrollOffsetMobile,\n ...htmlProps\n}: Props) => {\n // Track user-initiated actions with timestamp to ignore subsequent intersection observer events\n const userActionIgnoreDuration = 1000;\n const lastUserActionRef = useRef<number>(0);\n\n const [activeItemId, setActiveItemId] = useState<string | null>(\n items.find((item) => item.isActive)?.id || items[0]?.id || null\n );\n const menuRef = useRef<HTMLDivElement>(null);\n const scrollContainerRef = useRef<HTMLDivElement>(null);\n const horizontalScrollFadeProps = useHorizontalScrollFadeMixin(0);\n const [navBackgroundColor, setNavBackgroundColor] = useState<string>('rgb(255, 255, 255)');\n\n // Use the container tracking hook for visibility and width management\n const { isContainerVisible, menuHeight, menuWidth } = useContainerTracking({\n containerSelector,\n menuRef,\n });\n\n // Determine if container top has passed viewport top (need to check position separately from visibility)\n const [containerTopPassed, setContainerTopPassed] = useState(false);\n\n // Track pending scroll when clicking item before menu becomes fixed\n const pendingScrollItemRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!containerSelector || !isClientSide) {\n return;\n }\n\n const containerElement = document.querySelector(containerSelector);\n if (!containerElement) {\n return;\n }\n\n const checkPosition = () => {\n const rect = containerElement.getBoundingClientRect();\n setContainerTopPassed(rect.top <= 0);\n };\n\n checkPosition();\n window.addEventListener('scroll', checkPosition, { passive: true });\n return () => window.removeEventListener('scroll', checkPosition);\n }, [containerSelector]);\n\n const isMobile = useWindowIsMobile();\n\n /**\n * Gets the appropriate scroll offset based on viewport size\n * @returns The scroll offset to use for the current viewport\n */\n const getScrollOffset = useCallback(() => {\n return isMobile && scrollOffsetMobile !== undefined ? scrollOffsetMobile : scrollOffset;\n }, [scrollOffset, scrollOffsetMobile, isMobile]);\n\n /**\n * Scrolls the specified menu item into view within the horizontal scroll container\n * Only scrolls when menu is in fixed position to prevent viewport shifts\n * @param itemId - The ID of the menu item to scroll into view\n * @param inline - Scroll alignment: 'center' shows neighbors, 'nearest' minimal scroll\n */\n const scrollActiveItemIntoView = useCallback((itemId: string, inline: 'center' | 'nearest' = 'nearest') => {\n const activeElement = scrollContainerRef.current?.querySelector(\n `[data-item-id=\"${itemId}\"]`\n ) as HTMLElement | null;\n\n if (activeElement) {\n activeElement.scrollIntoView({\n behavior: 'smooth',\n block: 'nearest',\n inline,\n });\n }\n }, []);\n\n // When menu becomes fixed and we have a pending scroll, execute it\n useEffect(() => {\n if (containerTopPassed && pendingScrollItemRef.current) {\n scrollActiveItemIntoView(pendingScrollItemRef.current, 'center');\n pendingScrollItemRef.current = null;\n }\n }, [containerTopPassed, scrollActiveItemIntoView]);\n\n // Update active item when external state changes\n useEffect(() => {\n const externalActiveItem = items.find((item) => item.isActive);\n if (externalActiveItem && externalActiveItem.id !== activeItemId) {\n setActiveItemId(externalActiveItem.id);\n }\n }, [items, activeItemId]);\n\n /**\n * Handles active section changes from the intersection observer\n * Ignores updates that occur shortly after user interactions to prevent conflicts\n * @param sectionId - The ID of the section that became active\n */\n const handleActiveChange = useCallback(\n (sectionId: string) => {\n // Skip if this is a scroll-based update and we recently had a user action\n const timeSinceLastUserAction = Date.now() - lastUserActionRef.current;\n if (timeSinceLastUserAction < userActionIgnoreDuration) {\n return;\n }\n\n if (sectionId !== activeItemId) {\n setActiveItemId(sectionId);\n onActiveChange?.(sectionId);\n // Auto-scroll with 'center' to show neighboring items (only when fixed)\n if (containerTopPassed) {\n scrollActiveItemIntoView(sectionId, 'center');\n }\n }\n },\n [activeItemId, onActiveChange, userActionIgnoreDuration, scrollActiveItemIntoView, containerTopPassed]\n );\n\n // Prepare sections for the observer\n const sections = useMemo(\n () =>\n items\n .filter((item) => item.target || item.href)\n .map((item) => ({\n id: item.id,\n selector: item.target ? `#${item.target}` : item.href!,\n })),\n [items]\n );\n\n // Use modern IntersectionObserver for section detection\n useSectionObserver(sections, handleActiveChange);\n\n // Handle keyboard navigation with the dedicated hook\n const { handleKeyDown } = useKeyboardNavigation({\n activeItemId,\n containerRef: menuRef,\n getItemId: (item: JumpMenuItemType) => item.id,\n items,\n lastUserActionRef,\n onActiveChange: (itemId: string) => {\n setActiveItemId(itemId);\n onActiveChange?.(itemId);\n },\n scrollItemIntoView: scrollActiveItemIntoView,\n });\n\n /**\n * Handles menu item click events\n * Updates active state, calls custom onClick handler, and navigates to target section\n * @param item - The clicked menu item\n * @param event - The mouse event\n */\n const handleItemClick = useCallback(\n (item: JumpMenuItemType, event: MouseEvent) => {\n // Mark this as a user-initiated action\n lastUserActionRef.current = Date.now();\n\n // Scroll the clicked menu item into view FIRST, using 'center' to show neighbors\n // Only scroll horizontally when menu is in fixed position to prevent viewport shifts\n if (containerTopPassed) {\n scrollActiveItemIntoView(item.id, 'center');\n } else {\n // Store pending scroll to execute once menu becomes fixed\n pendingScrollItemRef.current = item.id;\n }\n\n setActiveItemId(item.id);\n onActiveChange?.(item.id);\n item.onClick?.(item.id);\n\n // Navigate to section if target or href is provided\n const targetSelector = item.target ? `#${item.target}` : item.href;\n\n if (targetSelector) {\n // Always prevent default since we're using buttons now\n event.preventDefault();\n\n // Handle navigation manually\n const targetElement = document.querySelector(targetSelector);\n if (targetElement) {\n const offset = getScrollOffset();\n\n if (offset > 0) {\n // Calculate position with offset\n const elementPosition = targetElement.getBoundingClientRect().top + window.pageYOffset;\n const offsetPosition = elementPosition - offset;\n\n window.scrollTo({\n behavior: 'smooth',\n top: offsetPosition,\n });\n } else {\n // Use default scroll behavior when no offset\n targetElement.scrollIntoView({\n behavior: 'smooth',\n block: 'start',\n });\n }\n }\n }\n },\n [onActiveChange, getScrollOffset, scrollActiveItemIntoView, containerTopPassed]\n );\n\n // Simple positioning logic - no complex state management needed\n // Fixed when we've scrolled past container top AND container hasn't scrolled past bottom\n const shouldUseFixedPosition = containerSelector && containerTopPassed && isContainerVisible;\n\n // Extract background color from nav element for dynamic gradient overlays\n useEffect(() => {\n if (menuRef.current && isClientSide) {\n const computedStyle = window.getComputedStyle(menuRef.current);\n const backgroundColor = computedStyle.backgroundColor;\n if (backgroundColor && backgroundColor !== 'rgba(0, 0, 0, 0)' && backgroundColor !== 'transparent') {\n setNavBackgroundColor(backgroundColor);\n }\n }\n }, []);\n\n return (\n <div className={cn(outerContainerClassName)}>\n {/* Simple placeholder to maintain layout when menu becomes fixed or is fading out */}\n {(shouldUseFixedPosition || !isContainerVisible) && (\n <div\n aria-hidden=\"true\"\n style={{\n height: menuHeight,\n }}\n />\n )}\n\n <nav\n aria-label={ariaLabel}\n className={cn(\n 'z-10 w-full bg-background-primary transition-opacity duration-300',\n {\n 'fixed top-0': shouldUseFixedPosition || !isContainerVisible, // Keep fixed during fade-out\n 'pointer-events-none': !isContainerVisible, // Disable interaction when hidden\n relative: !shouldUseFixedPosition && isContainerVisible,\n },\n className\n )}\n ref={menuRef}\n style={{\n opacity: isContainerVisible ? 1 : 0,\n width: shouldUseFixedPosition || !isContainerVisible ? `${menuWidth}px` : undefined,\n ...htmlProps.style,\n }}\n {...htmlProps}\n >\n <div className=\"flex w-full flex-col gap-xs py-xs\">\n <Caption\n // z-[11] is to prevent the caption from being hidden by fade\n className=\"z-[11] w-full px-xs\"\n color=\"secondary\"\n >\n {ariaLabel}\n </Caption>\n {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- Keyboard events bubble from focusable menu items */}\n <div\n className=\"scrollbar-hide flex w-full overflow-x-auto px-xs py-3xs\"\n onKeyDown={handleKeyDown}\n ref={(element) => {\n horizontalScrollFadeProps.ref(element);\n scrollContainerRef.current = element;\n menuRef.current = element;\n }}\n >\n {/* Left fade overlay */}\n <div\n className={cn(\n 'pointer-events-none absolute bottom-0 left-0 top-0 z-10 w-[48px] transition-opacity duration-300 ease-in-out',\n {\n 'opacity-0': !horizontalScrollFadeProps.shouldShowLeftFade,\n 'opacity-100': horizontalScrollFadeProps.shouldShowLeftFade,\n }\n )}\n style={{\n background: `linear-gradient(to right, ${navBackgroundColor}, transparent)`,\n }}\n />\n\n {/* Right fade overlay */}\n <div\n className={cn(\n 'pointer-events-none absolute bottom-0 right-0 top-0 z-10 w-[48px] transition-opacity duration-300 ease-in-out',\n {\n 'opacity-0': !horizontalScrollFadeProps.shouldShowRightFade,\n 'opacity-100': horizontalScrollFadeProps.shouldShowRightFade,\n }\n )}\n style={{\n background: `linear-gradient(to left, ${navBackgroundColor}, transparent)`,\n }}\n />\n\n {items.map((item) => (\n <JumpMenuItem\n {...item}\n activeItemId={activeItemId}\n handleItemClick={handleItemClick}\n key={item.id}\n />\n ))}\n </div>\n </div>\n </nav>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JumpMenu-DcZjAiej.mjs","names":[],"sources":["../src/JumpMenu/components/JumpMenuItem/JumpMenuItem.tsx","../src/JumpMenu/hooks/useContainerTracking.ts","../src/JumpMenu/hooks/useKeyboardNavigation.ts","../src/JumpMenu/hooks/useSectionObserver.ts","../src/JumpMenu/JumpMenu.tsx"],"sourcesContent":["import { cn } from '@evolve.ui/theme-system';\n\nimport { JumpMenuItem as JumpMenuItemData } from '../../types';\n\n/**\n * Single jump menu item configuration with click handler\n */\nexport type Props<T extends JumpMenuItemData = JumpMenuItemData> = T & {\n /** Unique identifier for the menu item */\n activeItemId: string | null;\n /** Additional CSS classes for the individual item */\n className?: string;\n /** Handle item click function */\n handleItemClick: (item: T, event: React.MouseEvent) => void;\n};\n\nexport const JumpMenuItem = ({\n activeItemId,\n className,\n handleItemClick,\n href,\n id,\n isActive: isActiveProp,\n label,\n target,\n ...restProps\n}: Props) => {\n const isActive = isActiveProp || id === activeItemId;\n\n return (\n <button\n aria-current={isActive ? 'page' : undefined}\n className={cn(\n // Base styles - use consistent typography class for both states\n 'hui-text-body-secondary relative whitespace-nowrap rounded-md px-xs py-2xs last:mr-sm [&:not(:last-child)]:mr-xs',\n // Default state (Selected=Off, Hover=Off, Focused=Off)\n 'text-text-secondary',\n // Focus styles - inset focus ring to stay within button dimensions\n 'focus-visible:shadow-[inset_0_0_0_4px_rgb(var(--color-focus-primary)/0.50)] focus-visible:outline-none',\n // Interactive states\n 'cursor-pointer',\n // Hover states - completely remove hover on touch devices, only show on pointer devices\n '[&:hover]:[@media(pointer:fine)]:bg-background-hover',\n !isActive && '[&:hover]:[@media(pointer:fine)]:text-text-primary',\n // Touch/Active states for mobile - brief visual feedback on tap\n !isActive && 'active:bg-background-hover active:text-text-primary active:duration-75',\n // Selected states (Selected=On) - using ring for border without affecting dimensions\n isActive && 'bg-background-primary font-bold text-text-primary ring-1 ring-inset ring-border-selected',\n className\n )}\n data-item-id={id}\n data-target={target || href?.replace('#', '')}\n key={id}\n onClick={(event) => {\n handleItemClick({ href, id, isActive: isActiveProp, label, target, ...restProps }, event);\n }}\n type=\"button\"\n >\n {label}\n </button>\n );\n};\n","import { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { isClientSide } from '../../utils/isClientSide';\n\n/**\n * Configuration for container tracking\n */\ntype ContainerTrackingConfig = {\n /** CSS selector for the container element */\n containerSelector?: string;\n /** Reference to the menu element for height tracking */\n menuRef?: React.RefObject<HTMLElement | null>;\n};\n\n/**\n * Return type for the container tracking hook\n */\ntype UseContainerTrackingReturn = {\n /** Whether the container is currently visible */\n isContainerVisible: boolean;\n /** Current height of the menu element in pixels */\n menuHeight: number;\n /** Current width of the container in pixels */\n menuWidth: number;\n};\n\n/**\n * Hook that tracks container visibility and dimensions for fixed positioning behavior\n *\n * This hook provides:\n * - Simple container visibility tracking via scroll detection\n * - Container width tracking for maintaining consistent sizing when position changes\n * - Menu height tracking for placeholder div\n * - Automatic event listener management with cleanup\n * - SSR safety by checking client-side rendering\n *\n * @param config - Configuration object for container tracking\n * @returns Object containing visibility state and dimensions\n *\n * @example\n * ```tsx\n * const { isContainerVisible, menuWidth, menuHeight } = useContainerTracking({\n * containerSelector: '#main-container',\n * menuRef\n * });\n * ```\n */\nexport const useContainerTracking = ({\n containerSelector,\n menuRef,\n}: ContainerTrackingConfig): UseContainerTrackingReturn => {\n const [isContainerVisible, setIsContainerVisible] = useState<boolean>(true);\n const [menuWidth, setMenuWidth] = useState<number>(0);\n const [menuHeight, setMenuHeight] = useState<number>(0);\n const resizeObserverRef = useRef<ResizeObserver | null>(null);\n\n /**\n * Sets up simple scroll-based visibility tracking for the container\n * Menu becomes fixed when container top reaches viewport top and stays fixed until container scrolls past\n */\n useEffect(() => {\n // We don't want to run this in SSR since we are using the window object\n if (!isClientSide) {\n return;\n }\n\n if (!containerSelector) {\n setIsContainerVisible(true);\n return;\n }\n\n const containerElement = document.querySelector(containerSelector);\n if (!containerElement) {\n setIsContainerVisible(true);\n return;\n }\n\n const onScroll = () => {\n const rect = containerElement.getBoundingClientRect();\n\n // Menu should be visible when:\n // 1. Before we scroll to container (rect.top > 0) - show in relative position\n // 2. While scrolling through container (rect.top <= 0 && rect.bottom > 0) - show in fixed position\n // 3. After scrolling past container (rect.bottom <= 0) - hide completely\n const shouldBeVisible = rect.bottom > 0; // Visible as long as container hasn't scrolled completely past\n\n setIsContainerVisible(shouldBeVisible);\n };\n\n // Initial check\n onScroll();\n\n // Listen to scroll events with passive flag for performance\n window.addEventListener('scroll', onScroll, { passive: true });\n\n return () => {\n window.removeEventListener('scroll', onScroll);\n };\n }, [containerSelector]);\n\n /**\n * Updates menu width and height for fixed positioning\n * This ensures the fixed menu maintains the same dimensions as its static position\n * Uses ResizeObserver for precise, real-time dimension tracking\n */\n useEffect(() => {\n // We don't want to run this in SSR since we are using the window object\n if (!isClientSide) {\n return;\n }\n\n const updateContainerWidth = () => {\n if (containerSelector) {\n const containerElement = document.querySelector(containerSelector);\n if (containerElement) {\n const containerRect = containerElement.getBoundingClientRect();\n setMenuWidth(containerRect.width);\n }\n }\n };\n\n const updateMenuHeight = () => {\n if (menuRef?.current) {\n // Use offsetHeight for more accurate measurement including borders and padding\n const height = menuRef.current.offsetHeight;\n if (height > 0) {\n setMenuHeight(height);\n }\n }\n };\n\n // Set up ResizeObserver for menu element height tracking\n if (menuRef?.current && 'ResizeObserver' in window) {\n resizeObserverRef.current = new ResizeObserver((entries) => {\n for (const entry of entries) {\n // Use contentRect height for reliable measurement\n const height = entry.contentRect.height;\n if (height > 0) {\n setMenuHeight(height);\n }\n }\n });\n\n resizeObserverRef.current.observe(menuRef.current);\n }\n\n // Initial measurements\n updateContainerWidth();\n updateMenuHeight();\n\n // Listen for window resize for container width updates\n window.addEventListener('resize', updateContainerWidth);\n\n return () => {\n window.removeEventListener('resize', updateContainerWidth);\n if (resizeObserverRef.current) {\n resizeObserverRef.current.disconnect();\n resizeObserverRef.current = null;\n }\n };\n }, [containerSelector, menuRef]);\n\n return useMemo(\n () => ({\n isContainerVisible,\n menuHeight,\n menuWidth,\n }),\n [isContainerVisible, menuHeight, menuWidth]\n );\n};\n","import { KeyboardEvent, RefObject, useCallback } from 'react';\n\n/**\n * Props for the keyboard navigation hook\n */\ntype UseKeyboardNavigationProps<T> = {\n /** Current active item ID */\n activeItemId: string | null;\n /** Ref to the container element for focusing items */\n containerRef: RefObject<HTMLElement | null>;\n /** Function to extract ID from an item */\n getItemId: (item: T) => string;\n /** Array of items to navigate through */\n items: T[];\n /** Ref to track user-initiated actions */\n lastUserActionRef: RefObject<number>;\n /** Callback when active item changes */\n onActiveChange?: (itemId: string) => void;\n /** Function to scroll item into view */\n scrollItemIntoView: (itemId: string) => void;\n};\n\n/**\n * Return type for the keyboard navigation hook\n */\ntype UseKeyboardNavigationReturn = {\n /** Keyboard event handler for the navigation container */\n handleKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;\n};\n\n/**\n * Custom hook for handling keyboard navigation in horizontal menu components\n *\n * Supports Arrow Left/Right, Home, and End keys for navigation with wrapping\n * and automatic focus management.\n */\nexport const useKeyboardNavigation = <T>({\n activeItemId,\n containerRef,\n getItemId,\n items,\n lastUserActionRef,\n onActiveChange,\n scrollItemIntoView,\n}: UseKeyboardNavigationProps<T>): UseKeyboardNavigationReturn => {\n /**\n * Handles keyboard navigation within the menu\n * Supports Arrow Left/Right, Home, and End keys for navigation\n */\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>) => {\n if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {\n return;\n }\n\n event.preventDefault();\n\n const currentEnabledIndex = items.findIndex((item) => getItemId(item) === activeItemId);\n\n let nextIndex: number;\n\n switch (event.key) {\n case 'ArrowLeft':\n nextIndex = currentEnabledIndex > 0 ? currentEnabledIndex - 1 : items.length - 1;\n break;\n case 'ArrowRight':\n nextIndex = currentEnabledIndex < items.length - 1 ? currentEnabledIndex + 1 : 0;\n break;\n case 'Home':\n nextIndex = 0;\n break;\n case 'End':\n nextIndex = items.length - 1;\n break;\n default:\n return;\n }\n\n const nextItem = items[nextIndex];\n const nextItemId = getItemId(nextItem);\n\n // Mark this as a user-initiated action\n lastUserActionRef.current = Date.now();\n\n onActiveChange?.(nextItemId);\n\n // Scroll the selected item into view\n scrollItemIntoView(nextItemId);\n\n // Focus the next item\n const nextElement = containerRef.current?.querySelector(\n `[data-item-id=\"${nextItemId}\"]`\n ) as HTMLElement | null;\n nextElement?.focus();\n },\n [activeItemId, items, getItemId, onActiveChange, scrollItemIntoView, containerRef, lastUserActionRef]\n );\n\n return {\n handleKeyDown,\n };\n};\n","import { useEffect, useMemo, useRef } from 'react';\n\nimport { useIntersectionObserver } from './useIntersectionObserver';\n\n/**\n * Configuration for section observation\n */\ntype SectionConfig = {\n /** The ID of the section */\n id: string;\n /** The selector or href for the section element */\n selector: string;\n};\n\n/**\n * Hook that observes multiple sections and reports which one is most visible\n * @param sections - Array of section configurations\n * @param onActiveChange - Callback when the active section changes\n * @param enabled - Whether section observation is enabled\n */\nexport const useSectionObserver = (\n sections: SectionConfig[],\n onActiveChange: (sectionId: string) => void,\n enabled: boolean = true\n) => {\n const activeEntriesRef = useRef<Map<string, IntersectionObserverEntry>>(new Map());\n\n // Memoize intersection observer options for better performance\n const observerOptions = useMemo(\n () => ({\n rootMargin: '-10% 0px -10% 0px', // Trigger when section is 10% visible from top/bottom\n threshold: [0, 0.1, 0.25, 0.5, 0.75, 1], // Multiple thresholds for better accuracy\n }),\n []\n );\n\n const handleIntersection = (entries: IntersectionObserverEntry[]) => {\n // Update our map of active entries\n entries.forEach((entry) => {\n const sectionId = sections.find(\n (s) => entry.target.matches(s.selector) || entry.target.id === s.selector.replace('#', '')\n )?.id;\n\n if (sectionId) {\n if (entry.isIntersecting) {\n activeEntriesRef.current.set(sectionId, entry);\n } else {\n activeEntriesRef.current.delete(sectionId);\n }\n }\n });\n\n // Find the topmost visible section (prioritize by vertical position, not intersection ratio)\n if (activeEntriesRef.current.size > 0) {\n let topmostSection = '';\n let smallestTop = Infinity;\n\n activeEntriesRef.current.forEach((entry, sectionId) => {\n // Use boundingClientRect.top to find which section is highest on screen\n const topPosition = entry.boundingClientRect.top;\n\n // Prioritize sections that are actually in the viewport (positive intersection ratio)\n if (entry.intersectionRatio > 0 && topPosition < smallestTop) {\n smallestTop = topPosition;\n topmostSection = sectionId;\n }\n });\n\n if (topmostSection) {\n onActiveChange(topmostSection);\n }\n }\n };\n\n const { disconnect, isSupported, observe } = useIntersectionObserver(handleIntersection, observerOptions);\n\n useEffect(() => {\n if (!enabled || !isSupported) {\n return;\n }\n\n // Clear previous observations\n activeEntriesRef.current.clear();\n disconnect();\n\n // Observe all section elements\n sections.forEach(({ selector }) => {\n const element = document.querySelector(selector);\n if (element) {\n observe(element);\n }\n });\n\n return () => {\n disconnect();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n activeEntriesRef.current.clear();\n };\n }, [sections, enabled, isSupported, observe, disconnect]);\n\n return { isSupported };\n};\n","import { cn } from '@evolve.ui/theme-system';\nimport { HTMLProps, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { useHorizontalScrollFadeMixin } from '../mixins/horizontalScrollFadeMixin';\nimport { useWindowIsMobile } from '../providers/windowSizeProvider';\nimport { Caption } from '../Typography/Text/Text';\nimport { isClientSide } from '../utils/isClientSide';\nimport { JumpMenuItem } from './components/JumpMenuItem/JumpMenuItem';\nimport { useContainerTracking } from './hooks/useContainerTracking';\nimport { useKeyboardNavigation } from './hooks/useKeyboardNavigation';\nimport { useSectionObserver } from './hooks/useSectionObserver';\nimport { JumpMenuItem as JumpMenuItemType } from './types';\n\n/**\n * Props for the JumpMenu component\n */\ntype Props = HTMLProps<HTMLElement> & {\n /** Custom aria-label for the navigation (defaults to 'Jump to section') */\n ariaLabel?: string;\n /** Additional CSS classes for the menu container */\n className?: string;\n /** CSS selector for the container element that defines the scope of fixed positioning behavior */\n containerSelector?: string;\n /** Array of menu items to display in the jump navigation */\n items: JumpMenuItemType[];\n /** Callback when active section changes via scroll or user interaction */\n onActiveChange?: (activeId: string | null) => void;\n /** Additional CSS classes for the outer container of the menu */\n outerContainerClassName?: string;\n /** Scroll offset in pixels to account for sticky headers when navigating to sections (defaults to 0) */\n scrollOffset?: number;\n /** Scroll offset in pixels for mobile viewports (overrides scrollOffset on mobile, defaults to scrollOffset value) */\n scrollOffsetMobile?: number;\n};\n\n/**\n * JumpMenu component for horizontal scrollable navigation between page sections\n *\n * Features:\n * - Horizontal scrolling with fade indicators on overflow\n * - Automatic section highlighting based on scroll position\n * - Keyboard navigation (Arrow keys, Home, End)\n * - Smooth scrolling to target sections\n * - Accessibility compliant with ARIA labels and roles\n * - Fixed positioning with container visibility tracking\n *\n * @param ariaLabel - Custom aria-label for the navigation (defaults to 'Jump to section')\n * @param className - Additional CSS classes for the menu container\n * @param containerSelector - CSS selector for the container element that defines the scope of fixed positioning behavior\n * @param items - Array of menu items to display - each item should have an id and either target or href\n * @param onActiveChange - Callback when active section changes via scroll or user interaction\n * @param outerContainerClassName - Additional CSS classes for the outer container of the menu\n * @param scrollOffset - Scroll offset in pixels to account for sticky headers when navigating to sections (defaults to 0)\n * @param scrollOffsetMobile - Scroll offset in pixels for mobile viewports (overrides scrollOffset on mobile, defaults to scrollOffset value)\n *\n *\n * @example\n * ```tsx\n * const menuItems = [\n * { id: 'overview', label: 'Overview', href: '#overview-section' },\n * { id: 'details', label: 'Details', href: '#details-section' },\n * ];\n *\n * <div id=\"container\">\n * <JumpMenu\n * items={menuItems}\n * containerSelector=\"#container\"\n * onActiveChange={(activeId) => console.log('Active section:', activeId)}\n * scrollOffset={80} // Optional: Account for 80px sticky header\n * scrollOffsetMobile={120} // Optional: Account for larger mobile header\n * />\n * <section id=\"overview-section\">Overview content</section>\n * <section id=\"details-section\">Details content</section>\n * </div>\n * ```\n */\nexport const JumpMenu = ({\n ariaLabel = 'Jump to section',\n className,\n containerSelector,\n items,\n onActiveChange,\n outerContainerClassName = '',\n scrollOffset = 0,\n scrollOffsetMobile,\n ...htmlProps\n}: Props) => {\n // Track user-initiated actions with timestamp to ignore subsequent intersection observer events\n const userActionIgnoreDuration = 1000;\n const lastUserActionRef = useRef<number>(0);\n\n const [activeItemId, setActiveItemId] = useState<string | null>(\n items.find((item) => item.isActive)?.id || items[0]?.id || null\n );\n const menuRef = useRef<HTMLDivElement>(null);\n const scrollContainerRef = useRef<HTMLDivElement>(null);\n const horizontalScrollFadeProps = useHorizontalScrollFadeMixin(0);\n const [navBackgroundColor, setNavBackgroundColor] = useState<string>('rgb(255, 255, 255)');\n\n // Use the container tracking hook for visibility and width management\n const { isContainerVisible, menuHeight, menuWidth } = useContainerTracking({\n containerSelector,\n menuRef,\n });\n\n // Determine if container top has passed viewport top (need to check position separately from visibility)\n const [containerTopPassed, setContainerTopPassed] = useState(false);\n\n // Track pending scroll when clicking item before menu becomes fixed\n const pendingScrollItemRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!containerSelector || !isClientSide) {\n return;\n }\n\n const containerElement = document.querySelector(containerSelector);\n if (!containerElement) {\n return;\n }\n\n const checkPosition = () => {\n const rect = containerElement.getBoundingClientRect();\n setContainerTopPassed(rect.top <= 0);\n };\n\n checkPosition();\n window.addEventListener('scroll', checkPosition, { passive: true });\n return () => window.removeEventListener('scroll', checkPosition);\n }, [containerSelector]);\n\n const isMobile = useWindowIsMobile();\n\n /**\n * Gets the appropriate scroll offset based on viewport size\n * @returns The scroll offset to use for the current viewport\n */\n const getScrollOffset = useCallback(() => {\n return isMobile && scrollOffsetMobile !== undefined ? scrollOffsetMobile : scrollOffset;\n }, [scrollOffset, scrollOffsetMobile, isMobile]);\n\n /**\n * Scrolls the specified menu item into view within the horizontal scroll container\n * Only scrolls when menu is in fixed position to prevent viewport shifts\n * @param itemId - The ID of the menu item to scroll into view\n * @param inline - Scroll alignment: 'center' shows neighbors, 'nearest' minimal scroll\n */\n const scrollActiveItemIntoView = useCallback((itemId: string, inline: 'center' | 'nearest' = 'nearest') => {\n const activeElement = scrollContainerRef.current?.querySelector(\n `[data-item-id=\"${itemId}\"]`\n ) as HTMLElement | null;\n\n if (activeElement) {\n activeElement.scrollIntoView({\n behavior: 'smooth',\n block: 'nearest',\n inline,\n });\n }\n }, []);\n\n // When menu becomes fixed and we have a pending scroll, execute it\n useEffect(() => {\n if (containerTopPassed && pendingScrollItemRef.current) {\n scrollActiveItemIntoView(pendingScrollItemRef.current, 'center');\n pendingScrollItemRef.current = null;\n }\n }, [containerTopPassed, scrollActiveItemIntoView]);\n\n // Update active item when external state changes\n useEffect(() => {\n const externalActiveItem = items.find((item) => item.isActive);\n if (externalActiveItem && externalActiveItem.id !== activeItemId) {\n setActiveItemId(externalActiveItem.id);\n }\n }, [items, activeItemId]);\n\n /**\n * Handles active section changes from the intersection observer\n * Ignores updates that occur shortly after user interactions to prevent conflicts\n * @param sectionId - The ID of the section that became active\n */\n const handleActiveChange = useCallback(\n (sectionId: string) => {\n // Skip if this is a scroll-based update and we recently had a user action\n const timeSinceLastUserAction = Date.now() - lastUserActionRef.current;\n if (timeSinceLastUserAction < userActionIgnoreDuration) {\n return;\n }\n\n if (sectionId !== activeItemId) {\n setActiveItemId(sectionId);\n onActiveChange?.(sectionId);\n // Auto-scroll with 'center' to show neighboring items (only when fixed)\n if (containerTopPassed) {\n scrollActiveItemIntoView(sectionId, 'center');\n }\n }\n },\n [activeItemId, onActiveChange, userActionIgnoreDuration, scrollActiveItemIntoView, containerTopPassed]\n );\n\n // Prepare sections for the observer\n const sections = useMemo(\n () =>\n items\n .filter((item) => item.target || item.href)\n .map((item) => ({\n id: item.id,\n selector: item.target ? `#${item.target}` : item.href!,\n })),\n [items]\n );\n\n // Use modern IntersectionObserver for section detection\n useSectionObserver(sections, handleActiveChange);\n\n // Handle keyboard navigation with the dedicated hook\n const { handleKeyDown } = useKeyboardNavigation({\n activeItemId,\n containerRef: menuRef,\n getItemId: (item: JumpMenuItemType) => item.id,\n items,\n lastUserActionRef,\n onActiveChange: (itemId: string) => {\n setActiveItemId(itemId);\n onActiveChange?.(itemId);\n },\n scrollItemIntoView: scrollActiveItemIntoView,\n });\n\n /**\n * Handles menu item click events\n * Updates active state, calls custom onClick handler, and navigates to target section\n * @param item - The clicked menu item\n * @param event - The mouse event\n */\n const handleItemClick = useCallback(\n (item: JumpMenuItemType, event: MouseEvent) => {\n // Mark this as a user-initiated action\n lastUserActionRef.current = Date.now();\n\n // Scroll the clicked menu item into view FIRST, using 'center' to show neighbors\n // Only scroll horizontally when menu is in fixed position to prevent viewport shifts\n if (containerTopPassed) {\n scrollActiveItemIntoView(item.id, 'center');\n } else {\n // Store pending scroll to execute once menu becomes fixed\n pendingScrollItemRef.current = item.id;\n }\n\n setActiveItemId(item.id);\n onActiveChange?.(item.id);\n item.onClick?.(item.id);\n\n // Navigate to section if target or href is provided\n const targetSelector = item.target ? `#${item.target}` : item.href;\n\n if (targetSelector) {\n // Always prevent default since we're using buttons now\n event.preventDefault();\n\n // Handle navigation manually\n const targetElement = document.querySelector(targetSelector);\n if (targetElement) {\n const offset = getScrollOffset();\n\n if (offset > 0) {\n // Calculate position with offset\n const elementPosition = targetElement.getBoundingClientRect().top + window.pageYOffset;\n const offsetPosition = elementPosition - offset;\n\n window.scrollTo({\n behavior: 'smooth',\n top: offsetPosition,\n });\n } else {\n // Use default scroll behavior when no offset\n targetElement.scrollIntoView({\n behavior: 'smooth',\n block: 'start',\n });\n }\n }\n }\n },\n [onActiveChange, getScrollOffset, scrollActiveItemIntoView, containerTopPassed]\n );\n\n // Simple positioning logic - no complex state management needed\n // Fixed when we've scrolled past container top AND container hasn't scrolled past bottom\n const shouldUseFixedPosition = containerSelector && containerTopPassed && isContainerVisible;\n\n // Extract background color from nav element for dynamic gradient overlays\n useEffect(() => {\n if (menuRef.current && isClientSide) {\n const computedStyle = window.getComputedStyle(menuRef.current);\n const backgroundColor = computedStyle.backgroundColor;\n if (backgroundColor && backgroundColor !== 'rgba(0, 0, 0, 0)' && backgroundColor !== 'transparent') {\n setNavBackgroundColor(backgroundColor);\n }\n }\n }, []);\n\n return (\n <div className={cn(outerContainerClassName)}>\n {/* Simple placeholder to maintain layout when menu becomes fixed or is fading out */}\n {(shouldUseFixedPosition || !isContainerVisible) && (\n <div\n aria-hidden=\"true\"\n style={{\n height: menuHeight,\n }}\n />\n )}\n\n <nav\n aria-label={ariaLabel}\n className={cn(\n 'z-10 w-full bg-background-primary transition-opacity duration-300',\n {\n 'fixed top-0': shouldUseFixedPosition || !isContainerVisible, // Keep fixed during fade-out\n 'pointer-events-none': !isContainerVisible, // Disable interaction when hidden\n relative: !shouldUseFixedPosition && isContainerVisible,\n },\n className\n )}\n ref={menuRef}\n style={{\n opacity: isContainerVisible ? 1 : 0,\n width: shouldUseFixedPosition || !isContainerVisible ? `${menuWidth}px` : undefined,\n ...htmlProps.style,\n }}\n {...htmlProps}\n >\n <div className=\"flex w-full flex-col gap-xs py-xs\">\n <Caption\n // z-[11] is to prevent the caption from being hidden by fade\n className=\"z-[11] w-full px-xs\"\n color=\"secondary\"\n >\n {ariaLabel}\n </Caption>\n {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- Keyboard events bubble from focusable menu items */}\n <div\n className=\"scrollbar-hide flex w-full overflow-x-auto px-xs py-3xs\"\n onKeyDown={handleKeyDown}\n ref={(element) => {\n horizontalScrollFadeProps.ref(element);\n scrollContainerRef.current = element;\n menuRef.current = element;\n }}\n >\n {/* Left fade overlay */}\n <div\n className={cn(\n 'pointer-events-none absolute bottom-0 left-0 top-0 z-10 w-[48px] transition-opacity duration-300 ease-in-out',\n {\n 'opacity-0': !horizontalScrollFadeProps.shouldShowLeftFade,\n 'opacity-100': horizontalScrollFadeProps.shouldShowLeftFade,\n }\n )}\n style={{\n background: `linear-gradient(to right, ${navBackgroundColor}, transparent)`,\n }}\n />\n\n {/* Right fade overlay */}\n <div\n className={cn(\n 'pointer-events-none absolute bottom-0 right-0 top-0 z-10 w-[48px] transition-opacity duration-300 ease-in-out',\n {\n 'opacity-0': !horizontalScrollFadeProps.shouldShowRightFade,\n 'opacity-100': horizontalScrollFadeProps.shouldShowRightFade,\n }\n )}\n style={{\n background: `linear-gradient(to left, ${navBackgroundColor}, transparent)`,\n }}\n />\n\n {items.map((item) => (\n <JumpMenuItem\n {...item}\n activeItemId={activeItemId}\n handleItemClick={handleItemClick}\n key={item.id}\n />\n ))}\n </div>\n </div>\n </nav>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"JumpMenu-DcZjAiej.mjs","names":[],"sources":["../src/JumpMenu/components/JumpMenuItem/JumpMenuItem.tsx","../src/JumpMenu/hooks/useContainerTracking.ts","../src/JumpMenu/hooks/useKeyboardNavigation.ts","../src/JumpMenu/hooks/useSectionObserver.ts","../src/JumpMenu/JumpMenu.tsx"],"sourcesContent":["import { cn } from '@evolve.ui/theme-system';\n\nimport { JumpMenuItem as JumpMenuItemData } from '../../types';\n\n/**\n * Single jump menu item configuration with click handler\n */\nexport type Props<T extends JumpMenuItemData = JumpMenuItemData> = T & {\n /** Unique identifier for the menu item */\n activeItemId: string | null;\n /** Additional CSS classes for the individual item */\n className?: string;\n /** Handle item click function */\n handleItemClick: (item: T, event: React.MouseEvent) => void;\n};\n\nexport const JumpMenuItem = ({\n activeItemId,\n className,\n handleItemClick,\n href,\n id,\n isActive: isActiveProp,\n label,\n target,\n ...restProps\n}: Props) => {\n const isActive = isActiveProp || id === activeItemId;\n\n return (\n <button\n aria-current={isActive ? 'page' : undefined}\n className={cn(\n // Base styles - use consistent typography class for both states\n 'hui-text-body-secondary relative whitespace-nowrap rounded-md px-xs py-2xs last:mr-sm [&:not(:last-child)]:mr-xs',\n // Default state (Selected=Off, Hover=Off, Focused=Off)\n 'text-text-secondary',\n // Focus styles - inset focus ring to stay within button dimensions.\n // Permitted clause-2 deviation (DS-98): hui-focus-visible-outline is outline-based and\n // cannot express an inset ring. The outline-none is focus-visible-scoped, so it\n // suppresses only alongside this replacement indicator.\n 'focus-visible:shadow-[inset_0_0_0_4px_rgb(var(--color-focus-primary)/0.50)] focus-visible:outline-none',\n // Interactive states\n 'cursor-pointer',\n // Hover states - completely remove hover on touch devices, only show on pointer devices\n '[&:hover]:[@media(pointer:fine)]:bg-background-hover',\n !isActive && '[&:hover]:[@media(pointer:fine)]:text-text-primary',\n // Touch/Active states for mobile - brief visual feedback on tap\n !isActive && 'active:bg-background-hover active:text-text-primary active:duration-75',\n // Selected states (Selected=On) - using ring for border without affecting dimensions\n isActive && 'bg-background-primary font-bold text-text-primary ring-1 ring-inset ring-border-selected',\n className\n )}\n data-item-id={id}\n data-target={target || href?.replace('#', '')}\n key={id}\n onClick={(event) => {\n handleItemClick({ href, id, isActive: isActiveProp, label, target, ...restProps }, event);\n }}\n type=\"button\"\n >\n {label}\n </button>\n );\n};\n","import { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { isClientSide } from '../../utils/isClientSide';\n\n/**\n * Configuration for container tracking\n */\ntype ContainerTrackingConfig = {\n /** CSS selector for the container element */\n containerSelector?: string;\n /** Reference to the menu element for height tracking */\n menuRef?: React.RefObject<HTMLElement | null>;\n};\n\n/**\n * Return type for the container tracking hook\n */\ntype UseContainerTrackingReturn = {\n /** Whether the container is currently visible */\n isContainerVisible: boolean;\n /** Current height of the menu element in pixels */\n menuHeight: number;\n /** Current width of the container in pixels */\n menuWidth: number;\n};\n\n/**\n * Hook that tracks container visibility and dimensions for fixed positioning behavior\n *\n * This hook provides:\n * - Simple container visibility tracking via scroll detection\n * - Container width tracking for maintaining consistent sizing when position changes\n * - Menu height tracking for placeholder div\n * - Automatic event listener management with cleanup\n * - SSR safety by checking client-side rendering\n *\n * @param config - Configuration object for container tracking\n * @returns Object containing visibility state and dimensions\n *\n * @example\n * ```tsx\n * const { isContainerVisible, menuWidth, menuHeight } = useContainerTracking({\n * containerSelector: '#main-container',\n * menuRef\n * });\n * ```\n */\nexport const useContainerTracking = ({\n containerSelector,\n menuRef,\n}: ContainerTrackingConfig): UseContainerTrackingReturn => {\n const [isContainerVisible, setIsContainerVisible] = useState<boolean>(true);\n const [menuWidth, setMenuWidth] = useState<number>(0);\n const [menuHeight, setMenuHeight] = useState<number>(0);\n const resizeObserverRef = useRef<ResizeObserver | null>(null);\n\n /**\n * Sets up simple scroll-based visibility tracking for the container\n * Menu becomes fixed when container top reaches viewport top and stays fixed until container scrolls past\n */\n useEffect(() => {\n // We don't want to run this in SSR since we are using the window object\n if (!isClientSide) {\n return;\n }\n\n if (!containerSelector) {\n setIsContainerVisible(true);\n return;\n }\n\n const containerElement = document.querySelector(containerSelector);\n if (!containerElement) {\n setIsContainerVisible(true);\n return;\n }\n\n const onScroll = () => {\n const rect = containerElement.getBoundingClientRect();\n\n // Menu should be visible when:\n // 1. Before we scroll to container (rect.top > 0) - show in relative position\n // 2. While scrolling through container (rect.top <= 0 && rect.bottom > 0) - show in fixed position\n // 3. After scrolling past container (rect.bottom <= 0) - hide completely\n const shouldBeVisible = rect.bottom > 0; // Visible as long as container hasn't scrolled completely past\n\n setIsContainerVisible(shouldBeVisible);\n };\n\n // Initial check\n onScroll();\n\n // Listen to scroll events with passive flag for performance\n window.addEventListener('scroll', onScroll, { passive: true });\n\n return () => {\n window.removeEventListener('scroll', onScroll);\n };\n }, [containerSelector]);\n\n /**\n * Updates menu width and height for fixed positioning\n * This ensures the fixed menu maintains the same dimensions as its static position\n * Uses ResizeObserver for precise, real-time dimension tracking\n */\n useEffect(() => {\n // We don't want to run this in SSR since we are using the window object\n if (!isClientSide) {\n return;\n }\n\n const updateContainerWidth = () => {\n if (containerSelector) {\n const containerElement = document.querySelector(containerSelector);\n if (containerElement) {\n const containerRect = containerElement.getBoundingClientRect();\n setMenuWidth(containerRect.width);\n }\n }\n };\n\n const updateMenuHeight = () => {\n if (menuRef?.current) {\n // Use offsetHeight for more accurate measurement including borders and padding\n const height = menuRef.current.offsetHeight;\n if (height > 0) {\n setMenuHeight(height);\n }\n }\n };\n\n // Set up ResizeObserver for menu element height tracking\n if (menuRef?.current && 'ResizeObserver' in window) {\n resizeObserverRef.current = new ResizeObserver((entries) => {\n for (const entry of entries) {\n // Use contentRect height for reliable measurement\n const height = entry.contentRect.height;\n if (height > 0) {\n setMenuHeight(height);\n }\n }\n });\n\n resizeObserverRef.current.observe(menuRef.current);\n }\n\n // Initial measurements\n updateContainerWidth();\n updateMenuHeight();\n\n // Listen for window resize for container width updates\n window.addEventListener('resize', updateContainerWidth);\n\n return () => {\n window.removeEventListener('resize', updateContainerWidth);\n if (resizeObserverRef.current) {\n resizeObserverRef.current.disconnect();\n resizeObserverRef.current = null;\n }\n };\n }, [containerSelector, menuRef]);\n\n return useMemo(\n () => ({\n isContainerVisible,\n menuHeight,\n menuWidth,\n }),\n [isContainerVisible, menuHeight, menuWidth]\n );\n};\n","import { KeyboardEvent, RefObject, useCallback } from 'react';\n\n/**\n * Props for the keyboard navigation hook\n */\ntype UseKeyboardNavigationProps<T> = {\n /** Current active item ID */\n activeItemId: string | null;\n /** Ref to the container element for focusing items */\n containerRef: RefObject<HTMLElement | null>;\n /** Function to extract ID from an item */\n getItemId: (item: T) => string;\n /** Array of items to navigate through */\n items: T[];\n /** Ref to track user-initiated actions */\n lastUserActionRef: RefObject<number>;\n /** Callback when active item changes */\n onActiveChange?: (itemId: string) => void;\n /** Function to scroll item into view */\n scrollItemIntoView: (itemId: string) => void;\n};\n\n/**\n * Return type for the keyboard navigation hook\n */\ntype UseKeyboardNavigationReturn = {\n /** Keyboard event handler for the navigation container */\n handleKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;\n};\n\n/**\n * Custom hook for handling keyboard navigation in horizontal menu components\n *\n * Supports Arrow Left/Right, Home, and End keys for navigation with wrapping\n * and automatic focus management.\n */\nexport const useKeyboardNavigation = <T>({\n activeItemId,\n containerRef,\n getItemId,\n items,\n lastUserActionRef,\n onActiveChange,\n scrollItemIntoView,\n}: UseKeyboardNavigationProps<T>): UseKeyboardNavigationReturn => {\n /**\n * Handles keyboard navigation within the menu\n * Supports Arrow Left/Right, Home, and End keys for navigation\n */\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>) => {\n if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {\n return;\n }\n\n event.preventDefault();\n\n const currentEnabledIndex = items.findIndex((item) => getItemId(item) === activeItemId);\n\n let nextIndex: number;\n\n switch (event.key) {\n case 'ArrowLeft':\n nextIndex = currentEnabledIndex > 0 ? currentEnabledIndex - 1 : items.length - 1;\n break;\n case 'ArrowRight':\n nextIndex = currentEnabledIndex < items.length - 1 ? currentEnabledIndex + 1 : 0;\n break;\n case 'Home':\n nextIndex = 0;\n break;\n case 'End':\n nextIndex = items.length - 1;\n break;\n default:\n return;\n }\n\n const nextItem = items[nextIndex];\n const nextItemId = getItemId(nextItem);\n\n // Mark this as a user-initiated action\n lastUserActionRef.current = Date.now();\n\n onActiveChange?.(nextItemId);\n\n // Scroll the selected item into view\n scrollItemIntoView(nextItemId);\n\n // Focus the next item\n const nextElement = containerRef.current?.querySelector(\n `[data-item-id=\"${nextItemId}\"]`\n ) as HTMLElement | null;\n nextElement?.focus();\n },\n [activeItemId, items, getItemId, onActiveChange, scrollItemIntoView, containerRef, lastUserActionRef]\n );\n\n return {\n handleKeyDown,\n };\n};\n","import { useEffect, useMemo, useRef } from 'react';\n\nimport { useIntersectionObserver } from './useIntersectionObserver';\n\n/**\n * Configuration for section observation\n */\ntype SectionConfig = {\n /** The ID of the section */\n id: string;\n /** The selector or href for the section element */\n selector: string;\n};\n\n/**\n * Hook that observes multiple sections and reports which one is most visible\n * @param sections - Array of section configurations\n * @param onActiveChange - Callback when the active section changes\n * @param enabled - Whether section observation is enabled\n */\nexport const useSectionObserver = (\n sections: SectionConfig[],\n onActiveChange: (sectionId: string) => void,\n enabled: boolean = true\n) => {\n const activeEntriesRef = useRef<Map<string, IntersectionObserverEntry>>(new Map());\n\n // Memoize intersection observer options for better performance\n const observerOptions = useMemo(\n () => ({\n rootMargin: '-10% 0px -10% 0px', // Trigger when section is 10% visible from top/bottom\n threshold: [0, 0.1, 0.25, 0.5, 0.75, 1], // Multiple thresholds for better accuracy\n }),\n []\n );\n\n const handleIntersection = (entries: IntersectionObserverEntry[]) => {\n // Update our map of active entries\n entries.forEach((entry) => {\n const sectionId = sections.find(\n (s) => entry.target.matches(s.selector) || entry.target.id === s.selector.replace('#', '')\n )?.id;\n\n if (sectionId) {\n if (entry.isIntersecting) {\n activeEntriesRef.current.set(sectionId, entry);\n } else {\n activeEntriesRef.current.delete(sectionId);\n }\n }\n });\n\n // Find the topmost visible section (prioritize by vertical position, not intersection ratio)\n if (activeEntriesRef.current.size > 0) {\n let topmostSection = '';\n let smallestTop = Infinity;\n\n activeEntriesRef.current.forEach((entry, sectionId) => {\n // Use boundingClientRect.top to find which section is highest on screen\n const topPosition = entry.boundingClientRect.top;\n\n // Prioritize sections that are actually in the viewport (positive intersection ratio)\n if (entry.intersectionRatio > 0 && topPosition < smallestTop) {\n smallestTop = topPosition;\n topmostSection = sectionId;\n }\n });\n\n if (topmostSection) {\n onActiveChange(topmostSection);\n }\n }\n };\n\n const { disconnect, isSupported, observe } = useIntersectionObserver(handleIntersection, observerOptions);\n\n useEffect(() => {\n if (!enabled || !isSupported) {\n return;\n }\n\n // Clear previous observations\n activeEntriesRef.current.clear();\n disconnect();\n\n // Observe all section elements\n sections.forEach(({ selector }) => {\n const element = document.querySelector(selector);\n if (element) {\n observe(element);\n }\n });\n\n return () => {\n disconnect();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n activeEntriesRef.current.clear();\n };\n }, [sections, enabled, isSupported, observe, disconnect]);\n\n return { isSupported };\n};\n","import { cn } from '@evolve.ui/theme-system';\nimport { HTMLProps, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { useHorizontalScrollFadeMixin } from '../mixins/horizontalScrollFadeMixin';\nimport { useWindowIsMobile } from '../providers/windowSizeProvider';\nimport { Caption } from '../Typography/Text/Text';\nimport { isClientSide } from '../utils/isClientSide';\nimport { JumpMenuItem } from './components/JumpMenuItem/JumpMenuItem';\nimport { useContainerTracking } from './hooks/useContainerTracking';\nimport { useKeyboardNavigation } from './hooks/useKeyboardNavigation';\nimport { useSectionObserver } from './hooks/useSectionObserver';\nimport { JumpMenuItem as JumpMenuItemType } from './types';\n\n/**\n * Props for the JumpMenu component\n */\ntype Props = HTMLProps<HTMLElement> & {\n /** Custom aria-label for the navigation (defaults to 'Jump to section') */\n ariaLabel?: string;\n /** Additional CSS classes for the menu container */\n className?: string;\n /** CSS selector for the container element that defines the scope of fixed positioning behavior */\n containerSelector?: string;\n /** Array of menu items to display in the jump navigation */\n items: JumpMenuItemType[];\n /** Callback when active section changes via scroll or user interaction */\n onActiveChange?: (activeId: string | null) => void;\n /** Additional CSS classes for the outer container of the menu */\n outerContainerClassName?: string;\n /** Scroll offset in pixels to account for sticky headers when navigating to sections (defaults to 0) */\n scrollOffset?: number;\n /** Scroll offset in pixels for mobile viewports (overrides scrollOffset on mobile, defaults to scrollOffset value) */\n scrollOffsetMobile?: number;\n};\n\n/**\n * JumpMenu component for horizontal scrollable navigation between page sections\n *\n * Features:\n * - Horizontal scrolling with fade indicators on overflow\n * - Automatic section highlighting based on scroll position\n * - Keyboard navigation (Arrow keys, Home, End)\n * - Smooth scrolling to target sections\n * - Accessibility compliant with ARIA labels and roles\n * - Fixed positioning with container visibility tracking\n *\n * @param ariaLabel - Custom aria-label for the navigation (defaults to 'Jump to section')\n * @param className - Additional CSS classes for the menu container\n * @param containerSelector - CSS selector for the container element that defines the scope of fixed positioning behavior\n * @param items - Array of menu items to display - each item should have an id and either target or href\n * @param onActiveChange - Callback when active section changes via scroll or user interaction\n * @param outerContainerClassName - Additional CSS classes for the outer container of the menu\n * @param scrollOffset - Scroll offset in pixels to account for sticky headers when navigating to sections (defaults to 0)\n * @param scrollOffsetMobile - Scroll offset in pixels for mobile viewports (overrides scrollOffset on mobile, defaults to scrollOffset value)\n *\n *\n * @example\n * ```tsx\n * const menuItems = [\n * { id: 'overview', label: 'Overview', href: '#overview-section' },\n * { id: 'details', label: 'Details', href: '#details-section' },\n * ];\n *\n * <div id=\"container\">\n * <JumpMenu\n * items={menuItems}\n * containerSelector=\"#container\"\n * onActiveChange={(activeId) => console.log('Active section:', activeId)}\n * scrollOffset={80} // Optional: Account for 80px sticky header\n * scrollOffsetMobile={120} // Optional: Account for larger mobile header\n * />\n * <section id=\"overview-section\">Overview content</section>\n * <section id=\"details-section\">Details content</section>\n * </div>\n * ```\n */\nexport const JumpMenu = ({\n ariaLabel = 'Jump to section',\n className,\n containerSelector,\n items,\n onActiveChange,\n outerContainerClassName = '',\n scrollOffset = 0,\n scrollOffsetMobile,\n ...htmlProps\n}: Props) => {\n // Track user-initiated actions with timestamp to ignore subsequent intersection observer events\n const userActionIgnoreDuration = 1000;\n const lastUserActionRef = useRef<number>(0);\n\n const [activeItemId, setActiveItemId] = useState<string | null>(\n items.find((item) => item.isActive)?.id || items[0]?.id || null\n );\n const menuRef = useRef<HTMLDivElement>(null);\n const scrollContainerRef = useRef<HTMLDivElement>(null);\n const horizontalScrollFadeProps = useHorizontalScrollFadeMixin(0);\n const [navBackgroundColor, setNavBackgroundColor] = useState<string>('rgb(255, 255, 255)');\n\n // Use the container tracking hook for visibility and width management\n const { isContainerVisible, menuHeight, menuWidth } = useContainerTracking({\n containerSelector,\n menuRef,\n });\n\n // Determine if container top has passed viewport top (need to check position separately from visibility)\n const [containerTopPassed, setContainerTopPassed] = useState(false);\n\n // Track pending scroll when clicking item before menu becomes fixed\n const pendingScrollItemRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!containerSelector || !isClientSide) {\n return;\n }\n\n const containerElement = document.querySelector(containerSelector);\n if (!containerElement) {\n return;\n }\n\n const checkPosition = () => {\n const rect = containerElement.getBoundingClientRect();\n setContainerTopPassed(rect.top <= 0);\n };\n\n checkPosition();\n window.addEventListener('scroll', checkPosition, { passive: true });\n return () => window.removeEventListener('scroll', checkPosition);\n }, [containerSelector]);\n\n const isMobile = useWindowIsMobile();\n\n /**\n * Gets the appropriate scroll offset based on viewport size\n * @returns The scroll offset to use for the current viewport\n */\n const getScrollOffset = useCallback(() => {\n return isMobile && scrollOffsetMobile !== undefined ? scrollOffsetMobile : scrollOffset;\n }, [scrollOffset, scrollOffsetMobile, isMobile]);\n\n /**\n * Scrolls the specified menu item into view within the horizontal scroll container\n * Only scrolls when menu is in fixed position to prevent viewport shifts\n * @param itemId - The ID of the menu item to scroll into view\n * @param inline - Scroll alignment: 'center' shows neighbors, 'nearest' minimal scroll\n */\n const scrollActiveItemIntoView = useCallback((itemId: string, inline: 'center' | 'nearest' = 'nearest') => {\n const activeElement = scrollContainerRef.current?.querySelector(\n `[data-item-id=\"${itemId}\"]`\n ) as HTMLElement | null;\n\n if (activeElement) {\n activeElement.scrollIntoView({\n behavior: 'smooth',\n block: 'nearest',\n inline,\n });\n }\n }, []);\n\n // When menu becomes fixed and we have a pending scroll, execute it\n useEffect(() => {\n if (containerTopPassed && pendingScrollItemRef.current) {\n scrollActiveItemIntoView(pendingScrollItemRef.current, 'center');\n pendingScrollItemRef.current = null;\n }\n }, [containerTopPassed, scrollActiveItemIntoView]);\n\n // Update active item when external state changes\n useEffect(() => {\n const externalActiveItem = items.find((item) => item.isActive);\n if (externalActiveItem && externalActiveItem.id !== activeItemId) {\n setActiveItemId(externalActiveItem.id);\n }\n }, [items, activeItemId]);\n\n /**\n * Handles active section changes from the intersection observer\n * Ignores updates that occur shortly after user interactions to prevent conflicts\n * @param sectionId - The ID of the section that became active\n */\n const handleActiveChange = useCallback(\n (sectionId: string) => {\n // Skip if this is a scroll-based update and we recently had a user action\n const timeSinceLastUserAction = Date.now() - lastUserActionRef.current;\n if (timeSinceLastUserAction < userActionIgnoreDuration) {\n return;\n }\n\n if (sectionId !== activeItemId) {\n setActiveItemId(sectionId);\n onActiveChange?.(sectionId);\n // Auto-scroll with 'center' to show neighboring items (only when fixed)\n if (containerTopPassed) {\n scrollActiveItemIntoView(sectionId, 'center');\n }\n }\n },\n [activeItemId, onActiveChange, userActionIgnoreDuration, scrollActiveItemIntoView, containerTopPassed]\n );\n\n // Prepare sections for the observer\n const sections = useMemo(\n () =>\n items\n .filter((item) => item.target || item.href)\n .map((item) => ({\n id: item.id,\n selector: item.target ? `#${item.target}` : item.href!,\n })),\n [items]\n );\n\n // Use modern IntersectionObserver for section detection\n useSectionObserver(sections, handleActiveChange);\n\n // Handle keyboard navigation with the dedicated hook\n const { handleKeyDown } = useKeyboardNavigation({\n activeItemId,\n containerRef: menuRef,\n getItemId: (item: JumpMenuItemType) => item.id,\n items,\n lastUserActionRef,\n onActiveChange: (itemId: string) => {\n setActiveItemId(itemId);\n onActiveChange?.(itemId);\n },\n scrollItemIntoView: scrollActiveItemIntoView,\n });\n\n /**\n * Handles menu item click events\n * Updates active state, calls custom onClick handler, and navigates to target section\n * @param item - The clicked menu item\n * @param event - The mouse event\n */\n const handleItemClick = useCallback(\n (item: JumpMenuItemType, event: MouseEvent) => {\n // Mark this as a user-initiated action\n lastUserActionRef.current = Date.now();\n\n // Scroll the clicked menu item into view FIRST, using 'center' to show neighbors\n // Only scroll horizontally when menu is in fixed position to prevent viewport shifts\n if (containerTopPassed) {\n scrollActiveItemIntoView(item.id, 'center');\n } else {\n // Store pending scroll to execute once menu becomes fixed\n pendingScrollItemRef.current = item.id;\n }\n\n setActiveItemId(item.id);\n onActiveChange?.(item.id);\n item.onClick?.(item.id);\n\n // Navigate to section if target or href is provided\n const targetSelector = item.target ? `#${item.target}` : item.href;\n\n if (targetSelector) {\n // Always prevent default since we're using buttons now\n event.preventDefault();\n\n // Handle navigation manually\n const targetElement = document.querySelector(targetSelector);\n if (targetElement) {\n const offset = getScrollOffset();\n\n if (offset > 0) {\n // Calculate position with offset\n const elementPosition = targetElement.getBoundingClientRect().top + window.pageYOffset;\n const offsetPosition = elementPosition - offset;\n\n window.scrollTo({\n behavior: 'smooth',\n top: offsetPosition,\n });\n } else {\n // Use default scroll behavior when no offset\n targetElement.scrollIntoView({\n behavior: 'smooth',\n block: 'start',\n });\n }\n }\n }\n },\n [onActiveChange, getScrollOffset, scrollActiveItemIntoView, containerTopPassed]\n );\n\n // Simple positioning logic - no complex state management needed\n // Fixed when we've scrolled past container top AND container hasn't scrolled past bottom\n const shouldUseFixedPosition = containerSelector && containerTopPassed && isContainerVisible;\n\n // Extract background color from nav element for dynamic gradient overlays\n useEffect(() => {\n if (menuRef.current && isClientSide) {\n const computedStyle = window.getComputedStyle(menuRef.current);\n const backgroundColor = computedStyle.backgroundColor;\n if (backgroundColor && backgroundColor !== 'rgba(0, 0, 0, 0)' && backgroundColor !== 'transparent') {\n setNavBackgroundColor(backgroundColor);\n }\n }\n }, []);\n\n return (\n <div className={cn(outerContainerClassName)}>\n {/* Simple placeholder to maintain layout when menu becomes fixed or is fading out */}\n {(shouldUseFixedPosition || !isContainerVisible) && (\n <div\n aria-hidden=\"true\"\n style={{\n height: menuHeight,\n }}\n />\n )}\n\n <nav\n aria-label={ariaLabel}\n className={cn(\n 'z-10 w-full bg-background-primary transition-opacity duration-300',\n {\n 'fixed top-0': shouldUseFixedPosition || !isContainerVisible, // Keep fixed during fade-out\n 'pointer-events-none': !isContainerVisible, // Disable interaction when hidden\n relative: !shouldUseFixedPosition && isContainerVisible,\n },\n className\n )}\n ref={menuRef}\n style={{\n opacity: isContainerVisible ? 1 : 0,\n width: shouldUseFixedPosition || !isContainerVisible ? `${menuWidth}px` : undefined,\n ...htmlProps.style,\n }}\n {...htmlProps}\n >\n <div className=\"flex w-full flex-col gap-xs py-xs\">\n <Caption\n // z-[11] is to prevent the caption from being hidden by fade\n className=\"z-[11] w-full px-xs\"\n color=\"secondary\"\n >\n {ariaLabel}\n </Caption>\n {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- Keyboard events bubble from focusable menu items */}\n <div\n className=\"scrollbar-hide flex w-full overflow-x-auto px-xs py-3xs\"\n onKeyDown={handleKeyDown}\n ref={(element) => {\n horizontalScrollFadeProps.ref(element);\n scrollContainerRef.current = element;\n menuRef.current = element;\n }}\n >\n {/* Left fade overlay */}\n <div\n className={cn(\n 'pointer-events-none absolute bottom-0 left-0 top-0 z-10 w-[48px] transition-opacity duration-300 ease-in-out',\n {\n 'opacity-0': !horizontalScrollFadeProps.shouldShowLeftFade,\n 'opacity-100': horizontalScrollFadeProps.shouldShowLeftFade,\n }\n )}\n style={{\n background: `linear-gradient(to right, ${navBackgroundColor}, transparent)`,\n }}\n />\n\n {/* Right fade overlay */}\n <div\n className={cn(\n 'pointer-events-none absolute bottom-0 right-0 top-0 z-10 w-[48px] transition-opacity duration-300 ease-in-out',\n {\n 'opacity-0': !horizontalScrollFadeProps.shouldShowRightFade,\n 'opacity-100': horizontalScrollFadeProps.shouldShowRightFade,\n }\n )}\n style={{\n background: `linear-gradient(to left, ${navBackgroundColor}, transparent)`,\n }}\n />\n\n {items.map((item) => (\n <JumpMenuItem\n {...item}\n activeItemId={activeItemId}\n handleItemClick={handleItemClick}\n key={item.id}\n />\n ))}\n </div>\n </div>\n </nav>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -98,7 +98,7 @@ const MenuItem = forwardRef(({ checked = false, className, closeOnAction = false
|
|
|
98
98
|
"aria-label": primaryLabel,
|
|
99
99
|
className: cn({
|
|
100
100
|
"active:bg-background-hover": !isHeader,
|
|
101
|
-
"focus:bg-background-hover": !isHeader,
|
|
101
|
+
"focus-visible:bg-background-hover": !isHeader,
|
|
102
102
|
"hover:bg-background-hover": !isHeader
|
|
103
103
|
}, "MenuItem relative grid w-full cursor-pointer items-center justify-start gap-x-xs border-none px-sm py-3 outline-0", className),
|
|
104
104
|
"data-testid": `menu-item-${id}`,
|
|
@@ -365,4 +365,4 @@ Menu.displayName = "Menu";
|
|
|
365
365
|
//#endregion
|
|
366
366
|
export { MenuDivider as n, MenuItem as r, Menu as t };
|
|
367
367
|
|
|
368
|
-
//# sourceMappingURL=Menu-
|
|
368
|
+
//# sourceMappingURL=Menu-Bn4smlEe.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Menu-Bn4smlEe.mjs","names":[],"sources":["../src/Menu/components/MenuItem/MenuItem.tsx","../src/Menu/components/MenuDivider/MenuDivider.tsx","../src/Menu/Menu.tsx"],"sourcesContent":["import { cn } from '@evolve.ui/theme-system';\nimport { useFloatingTree, useListItem, useMergeRefs } from '@floating-ui/react';\nimport { forwardRef, HTMLAttributes, KeyboardEvent, useContext, useEffect, useMemo, useState } from 'react';\n\nimport { Checkbox, RadioOption } from '../../../ChoiceListItem';\nimport { AnyIcon } from '../../../Icons/anyIcon';\nimport { ToggleSwitch } from '../../../ToggleSwitch/ToggleSwitch';\nimport { BodyPrimary, Caption, H5 } from '../../../Typography/Text/Text';\nimport { MenuContext } from '../../context/menuContext';\n\nexport type MenuItemProps = {\n /** additional CSS class name for the menu item*/\n className?: string;\n /** close menu after action is performed */\n closeOnAction?: boolean;\n /** color of menu item */\n color?: 'disabled' | 'inherit' | 'critical' | 'info' | 'inverse' | 'success' | 'secondary' | 'primary' | 'warning';\n /** second row description, optional */\n description?: string;\n /** disable menu item */\n disabled?: boolean;\n /** id required for all event handlers: click, checkbox,\n * radio or toggle\n */\n id?: string | number;\n /** flag if using as header, alters style and hover state */\n isHeader?: boolean;\n /** Icon to use on Left side of menu item, requires numberOfColumns of 3 */\n leftIcon?: AnyIcon;\n /** primary menu label string, required */\n primaryLabel: string;\n /** Icon to use on Right side of menu, requires numberOfColumns of 2 or 3 */\n rightIcon?: AnyIcon;\n /** secondary label to be used to right of primary label, cannot be used with rightIcon, requires numberOfColumns of 2 or 3 */\n secondaryLabel?: string;\n} & (\n | {\n checked?: never;\n id?: never;\n onCheckboxChange?: never;\n onClick?: never;\n onFocus?: never;\n onRadioChange?: never;\n onToggleChange?: never;\n }\n | {\n /** current state of control */\n checked?: boolean;\n /** id required for all event handlers: click, checkbox,\n * radio or toggle\n */\n className?: string;\n id?: string | number;\n /** if present, will show checkbox on in LeftIcon space, requires numberOfColumns of 3 */\n onCheckboxChange?: (id: string | number, checked: boolean) => void;\n /** if present, will show checkbox on in LeftIcon space */\n onClick?: (id: string | number) => void;\n /** if present, trigger when MenuItem is focused */\n onFocus?: (id: string | number) => void;\n /** if present, will show checkbox on in LeftIcon space, requires numberOfColumns of 3 */\n onRadioChange?: (id: string | number, checked: boolean) => void;\n /** if present, will show checkbox on in RightIcon space, requires numberOfColumns of 2 or 3 */\n onToggleChange?: (id: string | number, checked: boolean) => void;\n }\n);\n\nexport const MenuItem = forwardRef<HTMLDivElement, MenuItemProps & HTMLAttributes<HTMLDivElement>>(\n (\n {\n checked = false,\n className,\n closeOnAction = false,\n color = 'inherit',\n description,\n disabled,\n id = '',\n isHeader,\n leftIcon: LeftIcon,\n onCheckboxChange,\n onClick,\n onFocus,\n onRadioChange,\n onToggleChange,\n primaryLabel,\n rightIcon: RightIcon,\n secondaryLabel,\n ...rest\n }: MenuItemProps & HTMLAttributes<HTMLDivElement>,\n forwardedRef\n ) => {\n const [isChecked, setIsChecked] = useState(checked);\n const { getItemProps, numberOfColumns } = useContext(MenuContext);\n const { ref: itemRef } = useListItem();\n\n const renderColor = disabled ? 'disabled' : color;\n\n useEffect(() => {\n setIsChecked(checked);\n }, [checked]);\n\n const tree = useFloatingTree();\n\n const rightSection = useMemo(() => {\n if (RightIcon) {\n return (\n <RightIcon\n className=\"self-center justify-self-center grid-area-[right]\"\n color={renderColor}\n size=\"sm\"\n />\n );\n }\n if (onToggleChange) {\n // Note, event handled by component level onClick\n return (\n <ToggleSwitch\n className=\"grid-area-[right]\"\n data-testid={`menu-toggle-${id}`}\n dense\n isToggled={isChecked}\n tabIndex={-1}\n />\n );\n }\n return null;\n }, [id, RightIcon, onToggleChange, isChecked, renderColor]);\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (disabled) {\n return;\n }\n if (e.code === 'Space' || e.code === 'Enter') {\n if (onClick) {\n onClick(id);\n tree?.events.emit('click');\n } else if (onCheckboxChange) {\n const newChecked = !isChecked;\n setIsChecked(newChecked);\n onCheckboxChange(id, newChecked);\n if (closeOnAction) {\n tree?.events.emit('click');\n }\n } else if (onRadioChange) {\n const newChecked = !isChecked;\n setIsChecked(newChecked);\n onRadioChange(id, newChecked);\n if (closeOnAction) {\n tree?.events.emit('click');\n }\n } else if (onToggleChange) {\n const newChecked = !isChecked;\n setIsChecked(newChecked);\n onToggleChange(id, newChecked);\n if (closeOnAction) {\n tree?.events.emit('click');\n }\n }\n }\n };\n\n const handleItemClick = (event: React.MouseEvent<HTMLElement>) => {\n event.stopPropagation();\n if (disabled) {\n return;\n }\n // this is to prevent UI elements like SVGs from triggering an additional click\n if (\n event.target instanceof HTMLButtonElement ||\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLSpanElement ||\n event.target instanceof HTMLDivElement\n ) {\n if (onClick) {\n onClick(id);\n tree?.events.emit('click');\n } else if (onCheckboxChange) {\n const newChecked = !isChecked;\n setIsChecked(newChecked);\n onCheckboxChange(id, newChecked);\n if (closeOnAction) {\n tree?.events.emit('click');\n }\n } else if (onRadioChange) {\n const newChecked = !isChecked;\n setIsChecked(newChecked);\n onRadioChange(id, newChecked);\n if (closeOnAction) {\n tree?.events.emit('click');\n }\n } else if (onToggleChange) {\n const newChecked = !isChecked;\n setIsChecked(newChecked);\n onToggleChange(id, newChecked);\n if (closeOnAction) {\n tree?.events.emit('click');\n }\n }\n }\n };\n\n const handleFocus = () => {\n if (disabled) {\n return;\n }\n onFocus?.(id);\n };\n\n return (\n <div\n {...rest}\n {...getItemProps({})}\n aria-describedby={description ? `menu-item-description-${id}` : undefined}\n aria-disabled={disabled}\n aria-label={primaryLabel}\n className={cn(\n {\n 'active:bg-background-hover': !isHeader,\n 'focus-visible:bg-background-hover': !isHeader,\n 'hover:bg-background-hover': !isHeader,\n },\n 'MenuItem relative grid w-full cursor-pointer items-center justify-start gap-x-xs border-none px-sm py-3 outline-0',\n className\n )}\n data-testid={`menu-item-${id}`}\n onClick={handleItemClick}\n onFocus={handleFocus}\n onKeyDown={handleKeyDown}\n ref={useMergeRefs([itemRef, forwardedRef])}\n role=\"menuitem\"\n style={{\n gridTemplateAreas:\n numberOfColumns === 2\n ? \"'title right' 'description right'\"\n : numberOfColumns === 1\n ? \"'title' 'description'\"\n : // 3 column default\n \"'icon title right' 'icon description right'\",\n gridTemplateColumns:\n numberOfColumns === 2\n ? '1fr min-content'\n : numberOfColumns === 1\n ? '1fr'\n : // 3 column default\n '24px 1fr min-content',\n gridTemplateRows: 'min-content min-content',\n }}\n tabIndex={isHeader ? -1 : 0}\n >\n {/* LEFT */}\n {LeftIcon && (\n <LeftIcon\n className=\"self-center justify-self-center grid-area-[icon]\"\n color={renderColor}\n size=\"sm\"\n />\n )}\n {onCheckboxChange && (\n <Checkbox.Option\n checked={isChecked}\n className=\"grid-area-[icon]\"\n data-testid={`menu-checkbox-${id}`}\n readOnly // no onChange event is provided, handled by MenuItem propagation\n tabIndex={-1}\n />\n )}\n {onRadioChange && (\n <RadioOption\n checked={isChecked}\n className=\"grid-area-[icon]\"\n data-testid={`menu-radio-${id}`}\n readOnly // no onChange event is provided, handled by MenuItem propagation\n tabIndex={-1}\n />\n )}\n\n {/* MAIN */}\n {isHeader ? (\n <H5 className={cn('grid-area-[title]', `text-text-${renderColor}`)}>{primaryLabel}</H5>\n ) : (\n <div className=\"flex items-center justify-between gap-x-xs grid-area-[title]\">\n <BodyPrimary className={`text-text-${renderColor}`}>{primaryLabel}</BodyPrimary>\n {secondaryLabel && <BodyPrimary className=\"text-text-secondary\">{secondaryLabel}</BodyPrimary>}\n </div>\n )}\n {Boolean(description?.trim()) && (\n <Caption className={cn('grid-area-[description]', `text-text-${renderColor}`)}>\n {description}\n </Caption>\n )}\n\n {/* RIGHT */}\n {rightSection}\n </div>\n );\n }\n);\n\nMenuItem.displayName = 'MenuItem';\n","import { cn } from '@evolve.ui/theme-system';\n\nexport const MenuDivider = ({ className }: { className?: string }) => {\n return <div className={cn('border-t border-border-secondary', className)} />;\n};\n","import { cn } from '@evolve.ui/theme-system';\nimport {\n arrow,\n autoUpdate,\n flip,\n FloatingArrow,\n FloatingFocusManager,\n FloatingList,\n FloatingNode,\n FloatingPortal,\n FloatingTree,\n offset,\n Placement,\n safePolygon,\n shift,\n useClick,\n useDismiss,\n useFloating,\n useFloatingNodeId,\n useFloatingParentNodeId,\n useFloatingTree,\n useHover,\n useInteractions,\n useListItem,\n useListNavigation,\n useMergeRefs,\n useRole,\n useTypeahead,\n} from '@floating-ui/react';\nimport {\n cloneElement,\n FocusEvent,\n forwardRef,\n HTMLProps,\n isValidElement,\n MouseEvent,\n ReactElement,\n ReactNode,\n useEffect,\n useRef,\n useState,\n} from 'react';\n\nimport { MenuContext, useMenuContext } from './context/menuContext';\n\n/**\n * Hook to detect if component has mounted (client-side only)\n * Useful for preventing SSR hydration mismatches\n */\nconst useHasMounted = () => {\n const [hasMounted, setHasMounted] = useState(false);\n useEffect(() => {\n setHasMounted(true);\n }, []);\n return hasMounted;\n};\n\nexport type MenuProps = {\n /** MenuItem children */\n children?: ReactNode;\n /** additional CSS class name for the menu container*/\n className?: string;\n /** when true, menu will open on first render */\n defaultOpen?: boolean;\n /** when true, menu borders are rounded */\n hasRoundedBorders?: boolean;\n /**\n * Optional stable ID for SSR scenarios. If not provided, a unique ID will be generated.\n * This helps prevent hydration mismatches when using SSR.\n */\n id?: string;\n /** Enable menu custom content vs MenuItem children */\n isCustomContent?: boolean;\n /** initial menu placement */\n menuPlacement?: Placement;\n /** custom menu width */\n menuSize?: 'sm' | 'md' | 'lg' | 'xl';\n /** number of grid columns required for children */\n numberOfColumns: 1 | 2 | 3;\n /** callback that triggers when the menu is closed */\n onMenuClose?: () => void;\n /** callback that triggers when the menu is opened */\n onMenuOpen?: () => void;\n /** when true, menu will open on hover */\n openOnHover?: boolean;\n /** trigger component to open menu */\n trigger: ReactNode;\n /** additional CSS class name for the trigger */\n triggerClassName?: string;\n};\n\nconst MenuComponent = forwardRef<HTMLSpanElement, MenuProps & HTMLProps<HTMLSpanElement>>(\n (\n {\n children,\n className,\n defaultOpen,\n hasRoundedBorders = false,\n id,\n isCustomContent,\n menuPlacement,\n menuSize = 'lg',\n numberOfColumns,\n onMenuClose,\n onMenuOpen,\n openOnHover = false,\n trigger,\n triggerClassName,\n ...props\n },\n forwardedRef\n ) => {\n const [isOpen, setIsOpen] = useState(defaultOpen ?? false);\n const [activeIndex, setActiveIndex] = useState<number | null>(null);\n const elementsRef = useRef<Array<HTMLSpanElement | null>>([]);\n const labelsRef = useRef<Array<string | null>>([]);\n const parent = useMenuContext();\n const hasMounted = useHasMounted();\n\n const tree = useFloatingTree();\n const nodeId = useFloatingNodeId(id);\n const parentId = useFloatingParentNodeId();\n const item = useListItem();\n\n const isNested = parentId !== null;\n\n const arrowRef = useRef(null);\n\n const { context, floatingStyles, refs } = useFloating<HTMLButtonElement>({\n middleware: [\n offset({ alignmentAxis: -2, mainAxis: isNested ? 2 : 8 }),\n flip(),\n shift({ padding: !isNested ? 2 : 0 }),\n arrow({ element: arrowRef, padding: 5 }),\n ],\n nodeId,\n onOpenChange: (open) => {\n setIsOpen(open);\n if (!open) {\n onMenuClose?.();\n } else {\n onMenuOpen?.();\n }\n },\n open: isOpen,\n placement: isNested ? 'right-start' : (menuPlacement ?? 'bottom'),\n whileElementsMounted: autoUpdate,\n });\n const hover = useHover(context, {\n delay: { open: 75 },\n enabled: isNested || openOnHover,\n handleClose: safePolygon({ blockPointerEvents: true }),\n });\n const click = useClick(context, {\n enabled: !context.dataRef.current.openEvent?.defaultPrevented && !openOnHover,\n event: 'click',\n ignoreMouse: isNested,\n toggle: !isNested,\n });\n\n const role = useRole(context, { role: 'menu' });\n const dismiss = useDismiss(context, { bubbles: true });\n const listNavigation = useListNavigation(context, {\n activeIndex,\n focusItemOnOpen: false,\n listRef: elementsRef,\n nested: isNested,\n onNavigate: setActiveIndex,\n });\n const typeahead = useTypeahead(context, {\n activeIndex,\n enabled: !isCustomContent,\n listRef: labelsRef,\n onMatch: isOpen ? setActiveIndex : undefined,\n });\n\n const { getFloatingProps, getItemProps, getReferenceProps } = useInteractions([\n hover,\n click,\n role,\n dismiss,\n listNavigation,\n typeahead,\n ]);\n\n // Event emitter allows you to communicate across tree components.\n // This effect closes all menus when an item gets clicked anywhere\n // in the tree.\n useEffect(() => {\n if (!tree) {\n return;\n }\n\n function handleTreeClick() {\n setIsOpen(false);\n onMenuClose?.();\n }\n\n function onSubMenuOpen(event: { nodeId: string; parentId: string }) {\n if (event.nodeId !== nodeId && event.parentId === parentId) {\n setIsOpen(false);\n onMenuClose?.();\n }\n }\n\n tree.events.on('click', handleTreeClick);\n tree.events.on('menuopen', onSubMenuOpen);\n\n return () => {\n tree.events.off('click', handleTreeClick);\n tree.events.off('menuopen', onSubMenuOpen);\n };\n }, [tree, nodeId, parentId, onMenuClose]);\n\n useEffect(() => {\n if (isOpen && tree) {\n tree.events.emit('menuopen', { nodeId, parentId });\n }\n }, [tree, isOpen, nodeId, parentId]);\n\n // Strip the three ARIA attrs from the floating-ui reference props so the wrapper span keeps\n // every behavior it had before (refs, click/keyboard handlers, positioning), but no longer\n // carries ARIA that its implicit `generic` role disallows (the axe `aria-allowed-attr`\n // violation). The three ARIA attrs are reapplied to the trigger child via `cloneElement`,\n // landing on a node whose role natively permits them (button / link / menuitem).\n const {\n 'aria-controls': ariaControls,\n 'aria-expanded': ariaExpanded,\n 'aria-haspopup': ariaHasPopup,\n ...referencePropsWithoutAria\n } = getReferenceProps(\n parent.getItemProps({\n ...props,\n onClick(event: MouseEvent<HTMLButtonElement>) {\n event.stopPropagation();\n props.onClick?.(event);\n },\n onFocus(event: FocusEvent<HTMLButtonElement>) {\n props.onFocus?.(event);\n },\n })\n ) as Record<string, unknown>;\n\n const triggerIsElement = isValidElement(trigger);\n const triggerElement = trigger as ReactElement;\n const ariaPropsForTrigger: Record<string, unknown> = {};\n if (ariaHasPopup !== undefined) {\n ariaPropsForTrigger['aria-haspopup'] = ariaHasPopup;\n }\n if (ariaExpanded !== undefined) {\n ariaPropsForTrigger['aria-expanded'] = ariaExpanded;\n }\n if (ariaControls !== undefined) {\n ariaPropsForTrigger['aria-controls'] = ariaControls;\n }\n\n // For element triggers, clone the ARIA onto the child (preferred — lands on a node whose role\n // natively permits these attrs). Note: this intentionally overrides any same-named ARIA the\n // caller already set on the trigger; menu disclosure semantics (`aria-haspopup=\"menu\"`) must\n // win for the popup to be announced correctly.\n const clonedTrigger = triggerIsElement ? cloneElement(triggerElement, ariaPropsForTrigger) : trigger;\n\n // For non-element triggers (string / number / null), we cannot clone ARIA anywhere meaningful,\n // so the wrapper span itself becomes the disclosure control. Adding `role=\"button\"` +\n // `tabIndex={0}` makes the ARIA attrs valid (no `aria-allowed-attr` violation) and preserves\n // the screen-reader disclosure pattern. Safe because non-element triggers don't contain a\n // nested interactive element (no `nested-interactive` violation).\n const wrapperFallbackProps = !triggerIsElement ? { ...ariaPropsForTrigger, role: 'button' as const } : {};\n\n return (\n <FloatingNode id={nodeId}>\n <span\n className={cn(\n {\n 'w-full': isNested,\n },\n 'm-0 inline-flex cursor-pointer appearance-none items-center border-none bg-none p-0 text-left no-underline outline-0 hover:no-underline',\n triggerClassName\n )}\n data-testid=\"menu-trigger-wrapper\"\n ref={useMergeRefs([refs.setReference, item.ref, forwardedRef])}\n tabIndex={\n !triggerIsElement ? 0 : !isNested ? undefined : parent.activeIndex === item.index ? 0 : -1\n }\n {...wrapperFallbackProps}\n {...referencePropsWithoutAria}\n >\n {clonedTrigger}\n </span>\n <MenuContext.Provider\n value={{\n activeIndex,\n getItemProps,\n isOpen,\n numberOfColumns,\n setActiveIndex,\n }}\n >\n <FloatingList\n elementsRef={elementsRef}\n labelsRef={labelsRef}\n >\n {isOpen && hasMounted && Boolean(refs.domReference) && (\n <FloatingPortal root={refs.domReference}>\n <FloatingFocusManager\n context={context}\n initialFocus={-1}\n modal={false}\n returnFocus={!isNested}\n >\n <div\n className={cn(\n 'Menu relative z-50 w-min cursor-default bg-background-primary py-2 shadow-03 outline-0',\n {\n 'min-w-[200px]': menuSize === 'sm',\n 'min-w-[250px]': menuSize === 'md',\n 'min-w-[300px]': menuSize === 'lg',\n 'min-w-[350px]': menuSize === 'xl',\n 'rounded-md': hasRoundedBorders,\n },\n className\n )}\n data-testid=\"menu-wrapper\"\n ref={refs.setFloating}\n style={floatingStyles}\n {...getFloatingProps()}\n >\n {!isNested && (\n <FloatingArrow\n className=\"fill-background-primary\"\n context={context}\n ref={arrowRef}\n tipRadius={hasRoundedBorders ? 2 : 0}\n />\n )}\n {children}\n </div>\n </FloatingFocusManager>\n </FloatingPortal>\n )}\n </FloatingList>\n </MenuContext.Provider>\n </FloatingNode>\n );\n }\n);\n\nMenuComponent.displayName = 'MenuComponent';\n\nexport const Menu = forwardRef<HTMLSpanElement, MenuProps & HTMLProps<HTMLSpanElement>>((props, ref) => {\n const parentId = useFloatingParentNodeId();\n\n if (parentId === null) {\n return (\n <FloatingTree>\n <MenuComponent\n {...props}\n ref={ref}\n />\n </FloatingTree>\n );\n }\n\n return (\n <MenuComponent\n {...props}\n ref={ref}\n />\n );\n});\n\nMenu.displayName = 'Menu';\n"],"mappings":"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"}
|
|
@@ -98,7 +98,7 @@ const MenuItem = (0, react.forwardRef)(({ checked = false, className, closeOnAct
|
|
|
98
98
|
"aria-label": primaryLabel,
|
|
99
99
|
className: (0, _evolve_ui_theme_system.cn)({
|
|
100
100
|
"active:bg-background-hover": !isHeader,
|
|
101
|
-
"focus:bg-background-hover": !isHeader,
|
|
101
|
+
"focus-visible:bg-background-hover": !isHeader,
|
|
102
102
|
"hover:bg-background-hover": !isHeader
|
|
103
103
|
}, "MenuItem relative grid w-full cursor-pointer items-center justify-start gap-x-xs border-none px-sm py-3 outline-0", className),
|
|
104
104
|
"data-testid": `menu-item-${id}`,
|
|
@@ -382,4 +382,4 @@ Object.defineProperty(exports, "MenuItem", {
|
|
|
382
382
|
}
|
|
383
383
|
});
|
|
384
384
|
|
|
385
|
-
//# sourceMappingURL=Menu-
|
|
385
|
+
//# sourceMappingURL=Menu-DuDhtoNP.cjs.map
|